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;
|
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||||
if (detail?.tableId === tableId) {
|
if (detail?.tableId === tableId) {
|
||||||
refresh();
|
refresh();
|
||||||
setIframeVersion((value) => value + 1);
|
|
||||||
setIframeLoading(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleDeleted = (event: Event) => {
|
const handleDeleted = (event: Event) => {
|
||||||
|
|||||||
@@ -76,6 +76,23 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
const [lastSyncedAt, setLastSyncedAt] = useState<number | null>(null);
|
const [lastSyncedAt, setLastSyncedAt] = useState<number | null>(null);
|
||||||
const persistSnapshotRef = useRef<((reason: "auto" | "close") => Promise<void>) | null>(null);
|
const persistSnapshotRef = useRef<((reason: "auto" | "close") => Promise<void>) | null>(null);
|
||||||
const lastPointerDownInGridRef = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -145,6 +162,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
if (!tableData || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
if (!tableData || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
startSavingHint();
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
try {
|
try {
|
||||||
@@ -170,9 +188,10 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
console.error("保存 Luckysheet 数据失败", error);
|
console.error("保存 Luckysheet 数据失败", error);
|
||||||
setSaveError(reason === "close" ? "关闭前保存失败,请重试" : "自动保存失败");
|
setSaveError(reason === "close" ? "关闭前保存失败,请重试" : "自动保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
|
stopSavingHint();
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [tableData, tableId]);
|
}, [startSavingHint, stopSavingHint, tableData, tableId]);
|
||||||
|
|
||||||
const debouncedPersist = useDebouncedCallback(() => {
|
const debouncedPersist = useDebouncedCallback(() => {
|
||||||
void persistSnapshot("auto");
|
void persistSnapshot("auto");
|
||||||
@@ -186,8 +205,9 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
return () => {
|
return () => {
|
||||||
debouncedPersist.cancel();
|
debouncedPersist.cancel();
|
||||||
void persistSnapshotRef.current?.("close");
|
void persistSnapshotRef.current?.("close");
|
||||||
|
stopSavingHint();
|
||||||
};
|
};
|
||||||
}, [debouncedPersist]);
|
}, [debouncedPersist, stopSavingHint]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLuckysheetReady) {
|
if (!isLuckysheetReady) {
|
||||||
@@ -371,6 +391,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
if (target && !container.contains(target)) {
|
if (target && !container.contains(target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
containerRef.current?.focus();
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const selection = window.luckysheet?.getluckysheet_select_save?.();
|
const selection = window.luckysheet?.getluckysheet_select_save?.();
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
@@ -511,7 +532,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
|
|
||||||
const statusText = saveError
|
const statusText = saveError
|
||||||
? saveError
|
? saveError
|
||||||
: isSaving
|
: isSaving && showSavingHint
|
||||||
? "同步中..."
|
? "同步中..."
|
||||||
: hasPendingChanges
|
: hasPendingChanges
|
||||||
? "有未保存变更"
|
? "有未保存变更"
|
||||||
@@ -548,6 +569,8 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
id={LUCKY_SHEET_CONTAINER_ID}
|
id={LUCKY_SHEET_CONTAINER_ID}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex-grow w-full h-full"
|
className="flex-grow w-full h-full"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label="在线表格全屏编辑区域"
|
||||||
style={{ display: isLuckysheetReady && !isTableLoading && !tableError ? "block" : "none" }}
|
style={{ display: isLuckysheetReady && !isTableLoading && !tableError ? "block" : "none" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
|
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
|
||||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||||
import { extractRowsForPreview } from "@/components/online-table/utils";
|
import { extractRowsForPreview } from "@/components/online-table/utils";
|
||||||
|
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||||
|
|
||||||
type LuckysheetSelection =
|
type LuckysheetSelection =
|
||||||
| {
|
| {
|
||||||
@@ -59,6 +60,33 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
const [reloadVersion, setReloadVersion] = useState(0);
|
const [reloadVersion, setReloadVersion] = useState(0);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
@@ -68,6 +96,10 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
getDocumentTable(tableId)
|
getDocumentTable(tableId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!canceled) {
|
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);
|
setTable(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -157,6 +189,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
startSavingHint();
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
try {
|
try {
|
||||||
@@ -170,20 +203,21 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
rows,
|
rows,
|
||||||
luckysheet: Array.isArray(luckysheetData) ? luckysheetData : table.snapshot?.luckysheet ?? [],
|
luckysheet: Array.isArray(luckysheetData) ? luckysheetData : table.snapshot?.luckysheet ?? [],
|
||||||
};
|
};
|
||||||
|
lastSnapshotHashRef.current = computeSnapshotHash(snapshot);
|
||||||
await saveOnlineTable(tableId, {
|
await saveOnlineTable(tableId, {
|
||||||
snapshot,
|
snapshot,
|
||||||
rows,
|
rows,
|
||||||
schema: table.schema,
|
schema: table.schema,
|
||||||
});
|
});
|
||||||
setTable((prev) => (prev ? { ...prev, snapshot, rows } : prev));
|
|
||||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("内嵌表格保存失败", err);
|
console.error("内嵌表格保存失败", err);
|
||||||
setSaveError("自动保存失败");
|
setSaveError("自动保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
|
stopSavingHint();
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [allowInlineEdit, table, tableId]);
|
}, [allowInlineEdit, startSavingHint, stopSavingHint, table, tableId]);
|
||||||
|
|
||||||
const debouncedPersist = useDebouncedCallback(() => {
|
const debouncedPersist = useDebouncedCallback(() => {
|
||||||
void persistSnapshot();
|
void persistSnapshot();
|
||||||
@@ -192,8 +226,40 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
debouncedPersist.cancel();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isLuckysheetReady || !table || !containerRef.current || !window.luckysheet) {
|
if (!isLuckysheetReady || !table || !containerRef.current || !window.luckysheet) {
|
||||||
@@ -332,9 +398,11 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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>}
|
{saveError && <span className="mt-1 rounded-md bg-white/80 px-2 py-0.5 text-red-500 shadow">{saveError}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user