From 58340d40801c9d61441b785a6fa772169e0b82fe Mon Sep 17 00:00:00 2001 From: liaibo Date: Sat, 29 Nov 2025 10:46:40 +0800 Subject: [PATCH] git save current version as 0.03 --- .../online-table/CompactTablePreview.tsx | 2 - .../online-table/FullScreenTableEditor.tsx | 29 ++++++- .../online-table/HeadlessTableViewer.tsx | 78 +++++++++++++++++-- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/wolai-frontend/src/components/online-table/CompactTablePreview.tsx b/wolai-frontend/src/components/online-table/CompactTablePreview.tsx index db882236..a2f6390b 100644 --- a/wolai-frontend/src/components/online-table/CompactTablePreview.tsx +++ b/wolai-frontend/src/components/online-table/CompactTablePreview.tsx @@ -67,8 +67,6 @@ const CompactTablePreview: React.FC = ({ const detail = (event as CustomEvent<{ tableId?: string }>).detail; if (detail?.tableId === tableId) { refresh(); - setIframeVersion((value) => value + 1); - setIframeLoading(true); } }; const handleDeleted = (event: Event) => { diff --git a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx index bda2a640..3b5916db 100644 --- a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx +++ b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx @@ -76,6 +76,23 @@ const FullScreenTableEditor: React.FC = ({ tableId, const [lastSyncedAt, setLastSyncedAt] = useState(null); const persistSnapshotRef = useRef<((reason: "auto" | "close") => Promise) | null>(null); const lastPointerDownInGridRef = useRef(false); + const savingHintTimerRef = useRef(null); + const [showSavingHint, setShowSavingHint] = useState(false); + + const startSavingHint = useCallback(() => { + if (savingHintTimerRef.current !== null) return; + savingHintTimerRef.current = window.setTimeout(() => { + setShowSavingHint(true); + }, 700); + }, []); + + const stopSavingHint = useCallback(() => { + if (savingHintTimerRef.current !== null) { + clearTimeout(savingHintTimerRef.current); + savingHintTimerRef.current = null; + } + setShowSavingHint(false); + }, []); useEffect(() => { if (typeof window !== "undefined") { @@ -145,6 +162,7 @@ const FullScreenTableEditor: React.FC = ({ tableId, if (!tableData || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") { return; } + startSavingHint(); setIsSaving(true); setSaveError(null); try { @@ -170,9 +188,10 @@ const FullScreenTableEditor: React.FC = ({ tableId, console.error("保存 Luckysheet 数据失败", error); setSaveError(reason === "close" ? "关闭前保存失败,请重试" : "自动保存失败"); } finally { + stopSavingHint(); setIsSaving(false); } - }, [tableData, tableId]); + }, [startSavingHint, stopSavingHint, tableData, tableId]); const debouncedPersist = useDebouncedCallback(() => { void persistSnapshot("auto"); @@ -186,8 +205,9 @@ const FullScreenTableEditor: React.FC = ({ tableId, return () => { debouncedPersist.cancel(); void persistSnapshotRef.current?.("close"); + stopSavingHint(); }; - }, [debouncedPersist]); + }, [debouncedPersist, stopSavingHint]); useEffect(() => { if (!isLuckysheetReady) { @@ -371,6 +391,7 @@ const FullScreenTableEditor: React.FC = ({ tableId, if (target && !container.contains(target)) { return; } + containerRef.current?.focus(); requestAnimationFrame(() => { const selection = window.luckysheet?.getluckysheet_select_save?.(); if (!selection) { @@ -511,7 +532,7 @@ const FullScreenTableEditor: React.FC = ({ tableId, const statusText = saveError ? saveError - : isSaving + : isSaving && showSavingHint ? "同步中..." : hasPendingChanges ? "有未保存变更" @@ -548,6 +569,8 @@ const FullScreenTableEditor: React.FC = ({ tableId, id={LUCKY_SHEET_CONTAINER_ID} ref={containerRef} className="flex-grow w-full h-full" + tabIndex={-1} + aria-label="在线表格全屏编辑区域" style={{ display: isLuckysheetReady && !isTableLoading && !tableError ? "block" : "none" }} /> diff --git a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx index e6e035ca..28b30251 100644 --- a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx +++ b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx @@ -14,6 +14,7 @@ import { import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { extractRowsForPreview } from "@/components/online-table/utils"; +import { supabaseBrowser } from "@/lib/supabase/client"; type LuckysheetSelection = | { @@ -59,6 +60,33 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe const [reloadVersion, setReloadVersion] = useState(0); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); + const [showSavingHint, setShowSavingHint] = useState(false); + const savingHintTimerRef = useRef(null); + const lastRemoteSyncedAtRef = useRef(null); + const lastSnapshotHashRef = useRef(null); + + const computeSnapshotHash = useCallback((snapshot: unknown) => { + try { + return JSON.stringify(snapshot ?? {}); + } catch { + return null; + } + }, []); + + const startSavingHint = useCallback(() => { + if (savingHintTimerRef.current !== null) return; + savingHintTimerRef.current = window.setTimeout(() => { + setShowSavingHint(true); + }, 700); + }, []); + + const stopSavingHint = useCallback(() => { + if (savingHintTimerRef.current !== null) { + clearTimeout(savingHintTimerRef.current); + savingHintTimerRef.current = null; + } + setShowSavingHint(false); + }, []); useEffect(() => { let canceled = false; @@ -68,6 +96,10 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe getDocumentTable(tableId) .then((data) => { if (!canceled) { + lastSnapshotHashRef.current = computeSnapshotHash(data.snapshot); + if ((data as { last_synced_at?: string }).last_synced_at) { + lastRemoteSyncedAtRef.current = (data as { last_synced_at?: string }).last_synced_at ?? null; + } setTable(data); } }) @@ -157,6 +189,7 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") { return; } + startSavingHint(); setIsSaving(true); setSaveError(null); try { @@ -170,20 +203,21 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe rows, luckysheet: Array.isArray(luckysheetData) ? luckysheetData : table.snapshot?.luckysheet ?? [], }; + lastSnapshotHashRef.current = computeSnapshotHash(snapshot); await saveOnlineTable(tableId, { snapshot, rows, schema: table.schema, }); - setTable((prev) => (prev ? { ...prev, snapshot, rows } : prev)); window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } })); } catch (err) { console.error("内嵌表格保存失败", err); setSaveError("自动保存失败"); } finally { + stopSavingHint(); setIsSaving(false); } - }, [allowInlineEdit, table, tableId]); + }, [allowInlineEdit, startSavingHint, stopSavingHint, table, tableId]); const debouncedPersist = useDebouncedCallback(() => { void persistSnapshot(); @@ -192,8 +226,40 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe useEffect(() => { return () => { debouncedPersist.cancel(); + stopSavingHint(); }; - }, [debouncedPersist]); + }, [debouncedPersist, stopSavingHint]); + + useEffect(() => { + if (!tableId) return; + const channel = supabaseBrowser + .channel(`table-${tableId}-live`) + .on( + "postgres_changes", + { event: "UPDATE", schema: "public", table: "document_tables", filter: `id=eq.${tableId}` }, + (payload) => { + const next = payload.new as DocumentTable | null; + if (!next) return; + const nextSynced = (next as { last_synced_at?: string }).last_synced_at ?? null; + if (nextSynced && lastRemoteSyncedAtRef.current && nextSynced <= lastRemoteSyncedAtRef.current) { + return; + } + lastRemoteSyncedAtRef.current = nextSynced; + const nextHash = computeSnapshotHash(next.snapshot); + const currentHash = lastSnapshotHashRef.current; + if (nextHash && currentHash && nextHash === currentHash) { + return; // 相同快照无需重建,避免闪烁 + } + lastSnapshotHashRef.current = nextHash; + setTable(next); + }, + ) + .subscribe(); + + return () => { + supabaseBrowser.removeChannel(channel); + }; + }, [tableId]); useEffect(() => { if (!isLuckysheetReady || !table || !containerRef.current || !window.luckysheet) { @@ -332,9 +398,11 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe )} - {allowInlineEdit && (isSaving || saveError) && ( + {allowInlineEdit && ((isSaving && showSavingHint) || saveError) && (
- {isSaving && 同步中...} + {isSaving && showSavingHint && ( + 同步中... + )} {saveError && {saveError}}
)}