git save current version as 0.03
This commit is contained in:
@@ -67,8 +67,6 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||
if (detail?.tableId === tableId) {
|
||||
refresh();
|
||||
setIframeVersion((value) => value + 1);
|
||||
setIframeLoading(true);
|
||||
}
|
||||
};
|
||||
const handleDeleted = (event: Event) => {
|
||||
|
||||
@@ -76,6 +76,23 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
const [lastSyncedAt, setLastSyncedAt] = useState<number | null>(null);
|
||||
const persistSnapshotRef = useRef<((reason: "auto" | "close") => Promise<void>) | null>(null);
|
||||
const lastPointerDownInGridRef = useRef(false);
|
||||
const savingHintTimerRef = useRef<number | null>(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<FullScreenTableEditorProps> = ({ 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<FullScreenTableEditorProps> = ({ 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<FullScreenTableEditorProps> = ({ tableId,
|
||||
return () => {
|
||||
debouncedPersist.cancel();
|
||||
void persistSnapshotRef.current?.("close");
|
||||
stopSavingHint();
|
||||
};
|
||||
}, [debouncedPersist]);
|
||||
}, [debouncedPersist, stopSavingHint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLuckysheetReady) {
|
||||
@@ -371,6 +391,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ 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<FullScreenTableEditorProps> = ({ tableId,
|
||||
|
||||
const statusText = saveError
|
||||
? saveError
|
||||
: isSaving
|
||||
: isSaving && showSavingHint
|
||||
? "同步中..."
|
||||
: hasPendingChanges
|
||||
? "有未保存变更"
|
||||
@@ -548,6 +569,8 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ 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" }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
const [reloadVersion, setReloadVersion] = useState(0);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [showSavingHint, setShowSavingHint] = useState(false);
|
||||
const savingHintTimerRef = useRef<number | null>(null);
|
||||
const lastRemoteSyncedAtRef = useRef<string | null>(null);
|
||||
const lastSnapshotHashRef = useRef<string | null>(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<HeadlessTableViewerProps> = ({ 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<HeadlessTableViewerProps> = ({ 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<HeadlessTableViewerProps> = ({ 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<HeadlessTableViewerProps> = ({ 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<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{allowInlineEdit && (isSaving || saveError) && (
|
||||
{allowInlineEdit && ((isSaving && showSavingHint) || saveError) && (
|
||||
<div className="pointer-events-none absolute bottom-2 right-3 flex flex-col items-end text-xs">
|
||||
{isSaving && <span className="rounded-md bg-white/80 px-2 py-0.5 text-gray-500 shadow">同步中...</span>}
|
||||
{isSaving && showSavingHint && (
|
||||
<span className="rounded-md bg-white/80 px-2 py-0.5 text-gray-500 shadow">同步中...</span>
|
||||
)}
|
||||
{saveError && <span className="mt-1 rounded-md bg-white/80 px-2 py-0.5 text-red-500 shadow">{saveError}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user