git save current version as 0.0.4

This commit is contained in:
liaibo
2025-11-29 13:37:57 +08:00
parent 58340d4080
commit 140bce9768
13 changed files with 333 additions and 47 deletions
+97
View File
@@ -79,6 +79,13 @@
--sidebar-ring: oklch(0.708 0 0);
}
html,
body,
#__next {
height: 100%;
min-height: 100%;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
@@ -407,6 +414,96 @@
.wolai-media__resize-handle.is-dragging {
background: rgba(37, 99, 235, 0.85);
}
.online-table-block {
position: relative;
}
.online-table-block:hover .wolai-table-resize-handle,
.online-table-block:focus-within .wolai-table-resize-handle {
opacity: 1;
}
.wolai-table-resize-handle {
position: absolute;
background: rgba(37, 99, 235, 0.12);
border-radius: 999px;
transition: opacity 0.15s ease, background 0.2s ease, transform 0.12s ease;
opacity: 0;
z-index: 40;
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.08);
}
.wolai-table-resize-handle.is-dragging {
background: rgba(37, 99, 235, 0.38);
}
.wolai-table-resize-handle--left,
.wolai-table-resize-handle--right {
top: 50%;
width: 10px;
height: 34px;
transform: translateY(-50%);
cursor: ew-resize;
}
.wolai-table-resize-handle--left {
left: 6px;
}
.wolai-table-resize-handle--right {
right: 6px;
}
.wolai-table-resize-handle--top,
.wolai-table-resize-handle--bottom {
left: 50%;
width: 34px;
height: 10px;
transform: translateX(-50%);
cursor: ns-resize;
}
.wolai-table-resize-handle--top {
top: 6px;
}
.wolai-table-resize-handle--bottom {
bottom: 6px;
}
.wolai-table-resize-handle--top-left,
.wolai-table-resize-handle--top-right,
.wolai-table-resize-handle--bottom-left,
.wolai-table-resize-handle--bottom-right {
width: 12px;
height: 12px;
border-radius: 6px;
}
.wolai-table-resize-handle--top-left {
top: 6px;
left: 6px;
cursor: nwse-resize;
}
.wolai-table-resize-handle--bottom-right {
bottom: 6px;
right: 6px;
cursor: nwse-resize;
}
.wolai-table-resize-handle--top-right {
top: 6px;
right: 6px;
cursor: nesw-resize;
}
.wolai-table-resize-handle--bottom-left {
bottom: 6px;
left: 6px;
cursor: nesw-resize;
}
/* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */
.luckysheet-input-box,
#luckysheet-rich-text-editor {
@@ -16,7 +16,7 @@ export default async function TableViewerPage({ params, searchParams }: TableVie
const allowEditing = resolvedSearch?.readonly !== "1";
return (
<div className={embedMode ? "min-h-screen bg-transparent" : "min-h-screen bg-white"}>
<div className={embedMode ? "h-full w-full bg-transparent" : "min-h-screen bg-white"}>
<HeadlessTableViewer tableId={tableId} embed={embedMode} editable={allowEditing} />
</div>
);
@@ -2,7 +2,7 @@
import React, { useEffect, useState, useMemo, useCallback } from "react";
import type { DocumentTable } from "@/types/online-table";
import { deleteOnlineTable, getDocumentTable } from "@/lib/online-table";
import { deleteOnlineTable, getDocumentTable, saveOnlineTable } from "@/lib/online-table";
import { Loader2, Table as TableIcon, Maximize2, Trash2, RotateCw } from "lucide-react";
interface CompactTablePreviewProps {
@@ -56,6 +56,13 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
const { table, isLoading, refresh } = useTableData(tableId);
const [iframeVersion, setIframeVersion] = useState(0);
const [iframeLoading, setIframeLoading] = useState(true);
const [isRenaming, setIsRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const [isSavingTitle, setIsSavingTitle] = useState(false);
const fixedViewerHeight = 320; // 默认视窗高度
const minEmbedHeight = 260;
const maxEmbedHeight = 440;
const rowHeight = 26; // 预估单行高度,便于动态收缩高度
const iframeSrc = useMemo(
() => `/tables/${tableId}/view?embed=1&v=${iframeVersion}`,
@@ -83,6 +90,12 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
};
}, [refresh, tableId]);
useEffect(() => {
if (table?.title !== undefined) {
setRenameValue(table.title ?? "");
}
}, [table?.title]);
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
@@ -105,8 +118,56 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
setIframeLoading(true);
refresh();
}, [refresh]);
const estimatedRows = useMemo(() => {
const rowsBySnapshot =
Array.isArray(table?.snapshot?.rows) && table.snapshot?.rows
? table.snapshot.rows.length
: 0;
const celldata = table?.snapshot?.luckysheet?.[0]?.celldata;
const rowsByCells =
Array.isArray(celldata) && celldata.length > 0
? Math.max(
...celldata.map((cell) =>
typeof cell?.r === "number" ? cell.r : -1,
),
) + 1
: 0;
const fallbackRows = 10;
return Math.max(rowsBySnapshot, rowsByCells, fallbackRows);
}, [table?.snapshot]);
const effectiveHeight = height ?? 520;
const clampHeight = useCallback(
(value: number) => Math.min(maxEmbedHeight, Math.max(minEmbedHeight, value)),
[maxEmbedHeight, minEmbedHeight],
);
const autoHeight = clampHeight(estimatedRows * rowHeight);
const effectiveHeight = clampHeight(height ?? autoHeight ?? fixedViewerHeight);
const effectiveWidth: number | string = "100%";
const handleRenameSubmit = useCallback(async () => {
if (!table) {
return;
}
const nextTitle = (renameValue || "").trim() || "未命名表格";
if (nextTitle === table.title) {
setIsRenaming(false);
return;
}
setIsSavingTitle(true);
try {
const updated = await saveOnlineTable(tableId, { title: nextTitle });
const finalTitle = updated.title ?? nextTitle;
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
refresh();
setRenameValue(finalTitle);
} catch (error) {
console.error("重命名表格失败", error);
setRenameValue(table.title ?? "");
} finally {
setIsRenaming(false);
setIsSavingTitle(false);
}
}, [refresh, renameValue, table, tableId]);
if (isLoading) {
return (
@@ -144,10 +205,48 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
onMouseUp={suppressEditorEvents}
onMouseMove={suppressEditorEvents}
>
<div className="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-md">
<div className="flex items-center justify-between border-b border-gray-100 bg-white/90 px-4 py-2 backdrop-blur">
<div>
<p className="text-sm font-semibold text-gray-700">{table.title}</p>
<div
className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white/90 shadow-[0_10px_36px_rgba(15,23,42,0.05)] transition-all hover:shadow-[0_14px_44px_rgba(15,23,42,0.08)]"
style={{
width: effectiveWidth,
maxWidth: "100%",
marginLeft: "auto",
marginRight: "auto",
overflowX: "hidden",
}}
>
<div className="flex items-center justify-between border-b border-gray-100 bg-white/80 px-4 py-2 backdrop-blur-sm">
<div className="flex flex-col">
{isRenaming ? (
<input
autoFocus
className="w-48 rounded border border-gray-200 px-2 py-1 text-sm font-semibold text-gray-700 focus:border-emerald-500 focus:outline-none"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={handleRenameSubmit}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleRenameSubmit();
}
if (e.key === "Escape") {
e.preventDefault();
setRenameValue(table.title ?? "");
setIsRenaming(false);
}
}}
/>
) : (
<button
type="button"
className="flex items-center gap-2 text-left text-sm font-semibold text-gray-700 hover:text-emerald-600"
title="点击重命名表格"
onClick={() => setIsRenaming(true)}
>
<span className="truncate max-w-xs">{table.title}</span>
{isSavingTitle && <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-400" />}
</button>
)}
<p className="text-xs text-gray-400"> · </p>
</div>
<div className="flex items-center gap-1">
@@ -178,18 +277,23 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
</div>
</div>
<div className="relative w-full bg-gray-50" style={{ height: effectiveHeight, minHeight: 320 }}>
<iframe
key={`${tableId}-${iframeVersion}`}
src={iframeSrc}
title={`online-table-${tableId}`}
className="h-full w-full border-0"
loading="lazy"
onLoad={() => setIframeLoading(false)}
allow="clipboard-read; clipboard-write"
/>
<div
className="relative w-full overflow-hidden bg-white select-none px-4 pb-4 pt-3"
style={{ height: effectiveHeight, minHeight: minEmbedHeight }}
>
<div className="relative h-full w-full overflow-hidden rounded-xl border border-gray-100 bg-white">
<iframe
key={`${tableId}-${iframeVersion}`}
src={iframeSrc}
title={`online-table-${tableId}`}
className="block h-full w-full border-0"
loading="lazy"
onLoad={() => setIframeLoading(false)}
allow="clipboard-read; clipboard-write"
/>
</div>
{iframeLoading && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-white/90">
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-white/90">
<Loader2 className="h-5 w-5 animate-spin text-gray-500" />
<span className="text-xs text-gray-500"> Luckysheet ...</span>
</div>
@@ -78,6 +78,9 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
const lastPointerDownInGridRef = useRef(false);
const savingHintTimerRef = useRef<number | null>(null);
const [showSavingHint, setShowSavingHint] = useState(false);
const [isRenaming, setIsRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const [isSavingTitle, setIsSavingTitle] = useState(false);
const startSavingHint = useCallback(() => {
if (savingHintTimerRef.current !== null) return;
@@ -137,6 +140,12 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
}
}, [tableData]);
useEffect(() => {
if (tableData?.title !== undefined) {
setRenameValue(tableData.title ?? "");
}
}, [tableData?.title]);
const luckysheetSheets = useMemo(() => {
if (tableData?.snapshot?.luckysheet && Array.isArray(tableData.snapshot.luckysheet) && tableData.snapshot.luckysheet.length > 0) {
return tableData.snapshot.luckysheet;
@@ -530,6 +539,31 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
const showLoadingOverlay = !isLuckysheetReady || isTableLoading;
const loadingMessage = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
const handleRenameSubmit = useCallback(async () => {
if (!tableData) {
setIsRenaming(false);
return;
}
const nextTitle = (renameValue || "").trim() || "未命名表格";
if (nextTitle === tableData.title) {
setIsRenaming(false);
return;
}
setIsSavingTitle(true);
try {
const updated = await saveOnlineTable(tableId, { title: nextTitle });
const finalTitle = updated.title ?? nextTitle;
setTableData((prev) => (prev ? { ...prev, title: finalTitle } : prev));
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
} catch (error) {
console.error("重命名表格失败", error);
setRenameValue(tableData.title ?? "");
} finally {
setIsRenaming(false);
setIsSavingTitle(false);
}
}, [renameValue, tableData, tableId]);
const statusText = saveError
? saveError
: isSaving && showSavingHint
@@ -546,9 +580,38 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
<header className="flex justify-between items-center p-3 border-b border-gray-200 shadow-sm">
<div className="flex items-center space-x-3">
<Table className="h-5 w-5 text-blue-500" />
<h1 className="text-lg font-bold">
线 - {(tableData?.title ?? tableId).slice(0, 24)}
</h1>
{isRenaming ? (
<input
autoFocus
className="w-64 rounded border border-gray-200 px-2 py-1 text-lg font-semibold text-gray-800 focus:border-emerald-500 focus:outline-none"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={handleRenameSubmit}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleRenameSubmit();
}
if (e.key === "Escape") {
e.preventDefault();
setRenameValue(tableData?.title ?? "");
setIsRenaming(false);
}
}}
/>
) : (
<button
type="button"
className="flex items-center gap-2 text-left text-lg font-bold text-gray-900 hover:text-emerald-600"
onClick={() => setIsRenaming(true)}
title="点击重命名表格"
>
<span className="truncate max-w-xs">
线 - {(tableData?.title ?? tableId).slice(0, 24)}
</span>
{isSavingTitle && <Loader2 className="h-4 w-4 animate-spin text-gray-400" />}
</button>
)}
<Zap className="h-4 w-4 text-yellow-500" />
<span className="text-xs text-yellow-600 font-medium" title="协同模式"> ()</span>
</div>
@@ -121,6 +121,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
};
}, [tableId, reloadVersion]);
const allowInlineEdit = editable ?? embed;
const focusLuckysheetEditor = useCallback(() => {
const applyFocus = () => {
@@ -360,15 +361,33 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
const overlayVisible = isLoading || !isLuckysheetReady;
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
const embedContainerStyle = embed ? { minHeight: "100vh", height: "100vh" } : undefined;
const embedContainerStyle = embed
? { height: "360px", minHeight: "360px", width: "100%", overflow: "hidden" as const }
: undefined;
useEffect(() => {
if (!embed) return;
const prevDocOverflow = document.documentElement.style.overflow;
const prevBodyOverflow = document.body.style.overflow;
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
return () => {
document.documentElement.style.overflow = prevDocOverflow;
document.body.style.overflow = prevBodyOverflow;
};
}, [embed]);
return (
<div
className={embed ? "w-full bg-transparent" : "min-h-screen w-full bg-white"}
className={
embed ? "h-full w-full bg-transparent overflow-hidden" : "min-h-screen w-full bg-white"
}
style={embedContainerStyle}
>
<div
className={embed ? "relative w-full" : "relative h-[calc(100vh-64px)] w-full"}
className={
embed ? "relative h-full w-full overflow-hidden" : "relative h-[calc(100vh-64px)] w-full"
}
style={embedContainerStyle}
>
<div
@@ -412,3 +431,4 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
};
export default HeadlessTableViewer;