0.4.0 convex及界面修改
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useConvexAuth, useQuery } from "convex/react";
|
||||
import type { DocumentTable } from "@/types/online-table";
|
||||
import { deleteOnlineTable, getDocumentTable, saveOnlineTable } from "@/lib/online-table";
|
||||
import { Loader2, Table as TableIcon, Maximize2, Trash2, RotateCw } from "lucide-react";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
interface CompactTablePreviewProps {
|
||||
tableId: string;
|
||||
@@ -13,16 +16,45 @@ interface CompactTablePreviewProps {
|
||||
}
|
||||
|
||||
const useTableData = (tableId: string) => {
|
||||
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 tableFromConvex = useQuery(
|
||||
api.tables.get,
|
||||
convexEnabled && userId && tableId ? { userId, tableId } : "skip",
|
||||
);
|
||||
|
||||
const [table, setTable] = useState<DocumentTable | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [version, setVersion] = useState(0);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setVersion((prev) => prev + 1);
|
||||
}, []);
|
||||
if (!convexEnabled) {
|
||||
setIsLoading(true);
|
||||
setVersion((prev) => prev + 1);
|
||||
}
|
||||
}, [convexEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (convexEnabled) {
|
||||
if (tableFromConvex === undefined) {
|
||||
setIsLoading(true);
|
||||
return;
|
||||
}
|
||||
if (tableFromConvex === null) {
|
||||
setTable(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setTable({ ...(tableFromConvex as unknown as DocumentTable), title: (tableFromConvex as any)?.title || "未命名表格" });
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
getDocumentTable(tableId)
|
||||
.then((data) => {
|
||||
@@ -44,7 +76,7 @@ const useTableData = (tableId: string) => {
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [tableId, version]);
|
||||
}, [convexEnabled, tableFromConvex, tableId, version]);
|
||||
|
||||
return { table, isLoading, refresh };
|
||||
};
|
||||
@@ -103,7 +135,7 @@ const CompactTablePreviewInner: React.FC<CompactTablePreviewProps> = ({
|
||||
}, []);
|
||||
|
||||
const handleDeleteTable = useCallback(async () => {
|
||||
const confirmed = window.confirm("删除表格将同步移除 Supabase 记录,确认继续?");
|
||||
const confirmed = window.confirm("删除表格将同步移除在线表格记录,确认继续?");
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await deleteOnlineTable(tableId);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Loader2, Table, X, Zap } from "lucide-react";
|
||||
import { useConvexAuth, useMutation, useQuery } from "convex/react";
|
||||
import type { DocumentTable, TableRowData } from "@/types/online-table";
|
||||
import {
|
||||
DEFAULT_TABLE_COLUMNS,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
createDefaultTableSnapshot,
|
||||
saveOnlineTable,
|
||||
} from "@/lib/online-table";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
|
||||
import { extractRowsForPreview } from "@/components/online-table/utils";
|
||||
@@ -75,6 +78,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [lastSyncedAt, setLastSyncedAt] = useState<number | null>(null);
|
||||
const persistSnapshotRef = useRef<((reason: "auto" | "close") => Promise<void>) | null>(null);
|
||||
const lastLocalPersistAtRef = useRef<number>(0);
|
||||
const lastPointerDownInGridRef = useRef(false);
|
||||
const savingHintTimerRef = useRef<number | null>(null);
|
||||
const [showSavingHint, setShowSavingHint] = useState(false);
|
||||
@@ -82,6 +86,19 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||
|
||||
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 tableFromConvex = useQuery(
|
||||
api.tables.get,
|
||||
convexEnabled && userId && tableId ? { userId, tableId } : "skip",
|
||||
);
|
||||
const updateTable = useMutation(api.tables.update);
|
||||
|
||||
const startSavingHint = useCallback(() => {
|
||||
if (savingHintTimerRef.current !== null) return;
|
||||
savingHintTimerRef.current = window.setTimeout(() => {
|
||||
@@ -129,16 +146,36 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTable(tableId);
|
||||
hasInitializedRef.current = false;
|
||||
lastTableIdRef.current = tableId;
|
||||
}, [fetchTable, tableId]);
|
||||
}, [tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableData) {
|
||||
if (convexEnabled) {
|
||||
if (tableFromConvex === undefined) {
|
||||
setIsTableLoading(true);
|
||||
setTableError(null);
|
||||
return;
|
||||
}
|
||||
if (tableFromConvex === null) {
|
||||
setIsTableLoading(false);
|
||||
setTableError("无法加载表格数据,请稍后重试。");
|
||||
setTableData(null);
|
||||
return;
|
||||
}
|
||||
// 避免“本地保存 -> 订阅回推 -> 立刻重建 luckysheet”导致的闪烁/选区丢失
|
||||
if (Date.now() - lastLocalPersistAtRef.current < 1500) {
|
||||
setIsTableLoading(false);
|
||||
return;
|
||||
}
|
||||
setTableError(null);
|
||||
setTableData(tableFromConvex as unknown as DocumentTable);
|
||||
setIsTableLoading(false);
|
||||
return;
|
||||
}
|
||||
}, [tableData]);
|
||||
|
||||
fetchTable(tableId);
|
||||
}, [convexEnabled, fetchTable, tableFromConvex, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableData?.title !== undefined) {
|
||||
@@ -185,11 +222,22 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
rows,
|
||||
luckysheet: Array.isArray(luckysheetData) ? luckysheetData : luckysheetSheets,
|
||||
};
|
||||
await saveOnlineTable(tableId, {
|
||||
snapshot,
|
||||
rows,
|
||||
schema: tableData.schema,
|
||||
});
|
||||
lastLocalPersistAtRef.current = Date.now();
|
||||
if (convexEnabled && userId) {
|
||||
await updateTable({
|
||||
userId,
|
||||
tableId,
|
||||
snapshot,
|
||||
rows,
|
||||
schema: tableData.schema,
|
||||
});
|
||||
} else {
|
||||
await saveOnlineTable(tableId, {
|
||||
snapshot,
|
||||
rows,
|
||||
schema: tableData.schema,
|
||||
});
|
||||
}
|
||||
setHasPendingChanges(false);
|
||||
setLastSyncedAt(Date.now());
|
||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||
@@ -200,7 +248,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
stopSavingHint();
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [luckysheetSheets, startSavingHint, stopSavingHint, tableData, tableId]);
|
||||
}, [convexEnabled, luckysheetSheets, startSavingHint, stopSavingHint, tableData, tableId, updateTable, userId]);
|
||||
|
||||
const debouncedPersist = useDebouncedCallback(() => {
|
||||
void persistSnapshot("auto");
|
||||
@@ -443,6 +491,37 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
|
||||
const gridKey = tableData?.grid_key ?? tableId;
|
||||
const loadUrl = gridKey ? `/api/luckysheet/load?gridKey=${gridKey}` : "";
|
||||
let canceled = false;
|
||||
let resizeRafId: number | null = null;
|
||||
const resizeTimerIds: number[] = [];
|
||||
let unlockTimerId: number | null = null;
|
||||
|
||||
const safeResize = () => {
|
||||
if (canceled) return;
|
||||
const container = document.getElementById(LUCKY_SHEET_CONTAINER_ID);
|
||||
if (!container) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
if (rect.width < 10 || rect.height < 10) return;
|
||||
const instance = window.luckysheet as any;
|
||||
if (!instance || typeof instance.resize !== "function") return;
|
||||
try {
|
||||
instance.resize();
|
||||
} catch {
|
||||
// 忽略:Luckysheet 内部可能处于 destroy/create 过程中
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleResize = () => {
|
||||
if (canceled) return;
|
||||
resizeRafId = window.requestAnimationFrame(() => {
|
||||
safeResize();
|
||||
});
|
||||
// 多次兜底:避免首次打开时样式/布局尚未完全稳定
|
||||
resizeTimerIds.push(window.setTimeout(safeResize, 80));
|
||||
resizeTimerIds.push(window.setTimeout(safeResize, 240));
|
||||
resizeTimerIds.push(window.setTimeout(safeResize, 800));
|
||||
};
|
||||
|
||||
const options = {
|
||||
container: LUCKY_SHEET_CONTAINER_ID,
|
||||
title: tableData.title ?? tableId,
|
||||
@@ -476,7 +555,10 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
imageUrlHandle: (url: string) => url,
|
||||
hook: {
|
||||
workbookCreateAfter: () => {
|
||||
if (canceled) return;
|
||||
setIsTableLoading(false);
|
||||
// 仅在 Luckysheet 完成 DOM 构建后再调用 resize,避免触发其内部空引用(offsetHeight of null)
|
||||
scheduleResize();
|
||||
},
|
||||
updated: () => {
|
||||
if (isApplyingSnapshotRef.current) return;
|
||||
@@ -515,31 +597,21 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次加载时,资源/样式可能刚完成注入,强制触发一次 resize 让 Luckysheet 重新计算布局(避免工具栏/公式栏不显示)
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
(window.luckysheet as any)?.resize?.();
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
(window.luckysheet as any)?.resize?.();
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}, 80);
|
||||
});
|
||||
|
||||
// 等待首帧渲染完成再开放 updated 事件
|
||||
setTimeout(() => {
|
||||
unlockTimerId = window.setTimeout(() => {
|
||||
isApplyingSnapshotRef.current = false;
|
||||
}, 0);
|
||||
|
||||
const containerEl = containerRef.current;
|
||||
return () => {
|
||||
canceled = true;
|
||||
if (resizeRafId !== null) {
|
||||
cancelAnimationFrame(resizeRafId);
|
||||
}
|
||||
resizeTimerIds.forEach((id) => clearTimeout(id));
|
||||
if (unlockTimerId !== null) {
|
||||
clearTimeout(unlockTimerId);
|
||||
}
|
||||
if (window.luckysheet) {
|
||||
window.luckysheet?.destroy?.(LUCKY_SHEET_CONTAINER_ID);
|
||||
}
|
||||
@@ -570,8 +642,13 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
}
|
||||
setIsSavingTitle(true);
|
||||
try {
|
||||
const updated = await saveOnlineTable(tableId, { title: nextTitle });
|
||||
const finalTitle = updated.title ?? nextTitle;
|
||||
let finalTitle = nextTitle;
|
||||
if (convexEnabled && userId) {
|
||||
await updateTable({ userId, tableId, title: nextTitle });
|
||||
} else {
|
||||
const updated = await saveOnlineTable(tableId, { title: nextTitle });
|
||||
finalTitle = updated.title ?? nextTitle;
|
||||
}
|
||||
setTableData((prev) => (prev ? { ...prev, title: finalTitle } : prev));
|
||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||
} catch (error) {
|
||||
@@ -581,7 +658,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
setIsRenaming(false);
|
||||
setIsSavingTitle(false);
|
||||
}
|
||||
}, [renameValue, tableData, tableId]);
|
||||
}, [convexEnabled, renameValue, tableData, tableId, updateTable, userId]);
|
||||
|
||||
const statusText = saveError
|
||||
? saveError
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user