0.4.0 convex及界面修改

This commit is contained in:
liaibo
2026-02-01 08:47:40 +08:00
parent d1f055f51a
commit af92c4b149
636 changed files with 7522 additions and 1815 deletions
@@ -2,6 +2,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Loader2, RotateCw } from "lucide-react";
import { useConvexAuth, useMutation, useQuery } from "convex/react";
import type { DocumentTable } from "@/types/online-table";
import {
DEFAULT_TABLE_COLUMNS,
@@ -11,6 +12,8 @@ import {
getDocumentTable,
saveOnlineTable,
} from "@/lib/online-table";
import { api } from "@/lib/convex/api";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { extractRowsForPreview } from "@/components/online-table/utils";
@@ -61,16 +64,24 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
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 lastLocalPersistAtRef = useRef<number>(0);
const computeSnapshotHash = useCallback((snapshot: unknown) => {
try {
return JSON.stringify(snapshot ?? {});
} catch {
return null;
}
}, []);
const convexEnabled = isConvexEnabled();
const { isAuthenticated } = useConvexAuth();
const currentUser = useQuery(api.users.currentUser, convexEnabled && isAuthenticated ? {} : "skip");
const userId =
currentUser && typeof currentUser === "object" && typeof (currentUser as any)._id === "string"
? String((currentUser as any)._id)
: "";
const shouldFetchTable = Boolean(convexEnabled && userId && tableId);
const tableFromConvex = useQuery(
api.tables.get,
shouldFetchTable ? { userId, tableId } : "skip",
);
const updateTable = useMutation(api.tables.update);
const allowInlineEdit = editable ?? embed;
const startSavingHint = useCallback(() => {
if (savingHintTimerRef.current !== null) return;
@@ -92,13 +103,37 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
setIsLoading(true);
setError(null);
if (convexEnabled) {
// Convex 模式下走 useQuery 实时订阅,这里仅维护与旧逻辑兼容的 loading/error 状态
if (tableFromConvex === undefined) {
// still loading
return () => {
canceled = true;
};
}
if (!canceled) {
if (tableFromConvex === null) {
setTable(null);
setError("无法加载表格数据");
} else {
// 可编辑内嵌视图:避免每次自动保存后立即重建 UI(会闪烁)。
// 我们在本组件触发保存后的短时间内,忽略来自订阅的回写更新。
if (allowInlineEdit && Date.now() - lastLocalPersistAtRef.current < 1500) {
// ignore
} else {
setTable(tableFromConvex as unknown as DocumentTable);
}
}
setIsLoading(false);
}
return () => {
canceled = true;
};
}
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);
}
})
@@ -118,10 +153,9 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
return () => {
canceled = true;
};
}, [computeSnapshotHash, reloadVersion, tableId]);
}, [allowInlineEdit, convexEnabled, reloadVersion, tableFromConvex, tableId]);
const allowInlineEdit = editable ?? embed;
const focusLuckysheetEditor = useCallback(() => {
const applyFocus = () => {
const editor = document.getElementById("luckysheet-rich-text-editor");
@@ -203,12 +237,22 @@ 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,
});
lastLocalPersistAtRef.current = Date.now();
if (convexEnabled && userId) {
await updateTable({
userId,
tableId,
snapshot,
rows,
schema: table.schema,
});
} else {
await saveOnlineTable(tableId, {
snapshot,
rows,
schema: table.schema,
});
}
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
} catch (err) {
console.error("内嵌表格保存失败", err);
@@ -217,7 +261,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
stopSavingHint();
setIsSaving(false);
}
}, [allowInlineEdit, computeSnapshotHash, startSavingHint, stopSavingHint, table, tableId]);
}, [allowInlineEdit, convexEnabled, startSavingHint, stopSavingHint, table, tableId, updateTable, userId]);
const debouncedPersist = useDebouncedCallback(() => {
void persistSnapshot();
@@ -232,36 +276,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
useEffect(() => {
if (!tableId) return;
// Supabase 已移除:此处不再做实时订阅
/* Supabase subscription removed
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);
};
*/
// Supabase 已移除:实时订阅由 Convex useQuery 承担(见上方 tableFromConvex
}, [tableId]);
useEffect(() => {