0.5 缩减重构
This commit is contained in:
@@ -60,19 +60,19 @@ const useTableData = (tableId: string) => {
|
||||
.then((data) => {
|
||||
if (!canceled) {
|
||||
setTable({ ...data, title: data.title || "未命名表格" });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load table:", error);
|
||||
if (!canceled) {
|
||||
setTable(null);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!canceled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load table:", error);
|
||||
if (!canceled) {
|
||||
setTable(null);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!canceled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
@@ -87,254 +87,254 @@ const CompactTablePreviewInner: React.FC<CompactTablePreviewProps> = ({
|
||||
onDelete,
|
||||
height,
|
||||
}) => {
|
||||
const { table, isLoading, refresh } = useTableData(tableId);
|
||||
const [iframeVersion, setIframeVersion] = useState(0);
|
||||
const [iframeLoading, setIframeLoading] = useState(true);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||
const fixedViewerHeight = 320; // 默认视窗高度
|
||||
const minEmbedHeight = 260;
|
||||
const maxEmbedHeight = 440;
|
||||
const rowHeight = 26; // 预估单行高度,便于动态收缩高度
|
||||
|
||||
const iframeSrc = useMemo(
|
||||
() => `/tables/${tableId}/view?embed=1&v=${iframeVersion}`,
|
||||
[tableId, iframeVersion],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSaved = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||
if (detail?.tableId === tableId) {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
const handleDeleted = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||
if (detail?.tableId === tableId) {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
window.addEventListener("online-table-saved", handleSaved as EventListener);
|
||||
window.addEventListener("online-table-deleted", handleDeleted as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener("online-table-saved", handleSaved as EventListener);
|
||||
window.removeEventListener("online-table-deleted", handleDeleted as EventListener);
|
||||
};
|
||||
}, [refresh, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (table?.title !== undefined) {
|
||||
setRenameValue(table.title ?? "");
|
||||
}
|
||||
}, [table?.title]);
|
||||
|
||||
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const { table, isLoading, refresh } = useTableData(tableId);
|
||||
const [iframeVersion, setIframeVersion] = useState(0);
|
||||
const [iframeLoading, setIframeLoading] = useState(true);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||
const fixedViewerHeight = 320; // 默认视窗高度
|
||||
const minEmbedHeight = 260;
|
||||
const maxEmbedHeight = 440;
|
||||
const rowHeight = 26; // 预估单行高度,便于动态收缩高度
|
||||
|
||||
const iframeSrc = useMemo(
|
||||
() => `/tables/${tableId}/view?embed=1&v=${iframeVersion}`,
|
||||
[tableId, iframeVersion],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSaved = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||
if (detail?.tableId === tableId) {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
const handleDeleted = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ tableId?: string }>).detail;
|
||||
if (detail?.tableId === tableId) {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
window.addEventListener("online-table-saved", handleSaved as EventListener);
|
||||
window.addEventListener("online-table-deleted", handleDeleted as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener("online-table-saved", handleSaved as EventListener);
|
||||
window.removeEventListener("online-table-deleted", handleDeleted as EventListener);
|
||||
};
|
||||
}, [refresh, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (table?.title !== undefined) {
|
||||
setRenameValue(table.title ?? "");
|
||||
}
|
||||
}, [table?.title]);
|
||||
|
||||
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleDeleteTable = useCallback(async () => {
|
||||
const confirmed = window.confirm("删除表格将同步移除在线表格记录,确认继续?");
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await deleteOnlineTable(tableId);
|
||||
window.dispatchEvent(new CustomEvent("online-table-deleted", { detail: { tableId } }));
|
||||
onDelete?.();
|
||||
} catch (error) {
|
||||
console.error("删除表格失败", error);
|
||||
window.alert("删除失败,请稍后重试");
|
||||
}
|
||||
}, [onDelete, tableId]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setIframeVersion((value) => value + 1);
|
||||
setIframeLoading(true);
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
const estimatedRows = useMemo(() => {
|
||||
const rowsBySnapshot =
|
||||
Array.isArray(table?.snapshot?.rows) && table.snapshot?.rows
|
||||
? table.snapshot.rows.length
|
||||
: 0;
|
||||
const celldata = table?.snapshot?.luckysheet?.[0]?.celldata;
|
||||
const rowsByCells =
|
||||
Array.isArray(celldata) && celldata.length > 0
|
||||
? Math.max(
|
||||
...celldata.map((cell) =>
|
||||
typeof cell?.r === "number" ? cell.r : -1,
|
||||
),
|
||||
) + 1
|
||||
: 0;
|
||||
const fallbackRows = 10;
|
||||
return Math.max(rowsBySnapshot, rowsByCells, fallbackRows);
|
||||
}, [table?.snapshot]);
|
||||
|
||||
const clampHeight = useCallback(
|
||||
(value: number) => Math.min(maxEmbedHeight, Math.max(minEmbedHeight, value)),
|
||||
[maxEmbedHeight, minEmbedHeight],
|
||||
);
|
||||
const autoHeight = clampHeight(estimatedRows * rowHeight);
|
||||
const effectiveHeight = clampHeight(height ?? autoHeight ?? fixedViewerHeight);
|
||||
const effectiveWidth: number | string = "100%";
|
||||
|
||||
const handleRenameSubmit = useCallback(async () => {
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
const nextTitle = (renameValue || "").trim() || "未命名表格";
|
||||
if (nextTitle === table.title) {
|
||||
setIsRenaming(false);
|
||||
return;
|
||||
}
|
||||
setIsSavingTitle(true);
|
||||
try {
|
||||
const updated = await saveOnlineTable(tableId, { title: nextTitle });
|
||||
const finalTitle = updated.title ?? nextTitle;
|
||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||
refresh();
|
||||
setRenameValue(finalTitle);
|
||||
} catch (error) {
|
||||
console.error("重命名表格失败", error);
|
||||
setRenameValue(table.title ?? "");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setIsSavingTitle(false);
|
||||
}
|
||||
}, [refresh, renameValue, table, tableId]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-20 items-center justify-center rounded-md border border-dashed bg-gray-50">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!table) {
|
||||
return (
|
||||
<div className="flex h-24 items-center justify-between rounded-md border border-red-200 bg-red-50 px-4 py-2 text-red-600">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<TableIcon className="h-5 w-5" />
|
||||
<span>表格加载失败或不存在</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 rounded-md border border-red-200 px-3 py-1 text-xs font-medium"
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
onDoubleClick={onFullScreen}
|
||||
contentEditable={false}
|
||||
onMouseDown={suppressEditorEvents}
|
||||
onMouseUp={suppressEditorEvents}
|
||||
onDelete?.();
|
||||
} catch (error) {
|
||||
console.error("删除表格失败", error);
|
||||
window.alert("删除失败,请稍后重试");
|
||||
}
|
||||
}, [onDelete, tableId]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setIframeVersion((value) => value + 1);
|
||||
setIframeLoading(true);
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
const estimatedRows = useMemo(() => {
|
||||
const rowsBySnapshot =
|
||||
Array.isArray(table?.snapshot?.rows) && table.snapshot?.rows
|
||||
? table.snapshot.rows.length
|
||||
: 0;
|
||||
const celldata = table?.snapshot?.luckysheet?.[0]?.celldata;
|
||||
const rowsByCells =
|
||||
Array.isArray(celldata) && celldata.length > 0
|
||||
? Math.max(
|
||||
...celldata.map((cell) =>
|
||||
typeof cell?.r === "number" ? cell.r : -1,
|
||||
),
|
||||
) + 1
|
||||
: 0;
|
||||
const fallbackRows = 10;
|
||||
return Math.max(rowsBySnapshot, rowsByCells, fallbackRows);
|
||||
}, [table?.snapshot]);
|
||||
|
||||
const clampHeight = useCallback(
|
||||
(value: number) => Math.min(maxEmbedHeight, Math.max(minEmbedHeight, value)),
|
||||
[maxEmbedHeight, minEmbedHeight],
|
||||
);
|
||||
const autoHeight = clampHeight(estimatedRows * rowHeight);
|
||||
const effectiveHeight = clampHeight(height ?? autoHeight ?? fixedViewerHeight);
|
||||
const effectiveWidth: number | string = "100%";
|
||||
|
||||
const handleRenameSubmit = useCallback(async () => {
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
const nextTitle = (renameValue || "").trim() || "未命名表格";
|
||||
if (nextTitle === table.title) {
|
||||
setIsRenaming(false);
|
||||
return;
|
||||
}
|
||||
setIsSavingTitle(true);
|
||||
try {
|
||||
const updated = await saveOnlineTable(tableId, { title: nextTitle });
|
||||
const finalTitle = updated.title ?? nextTitle;
|
||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||
refresh();
|
||||
setRenameValue(finalTitle);
|
||||
} catch (error) {
|
||||
console.error("重命名表格失败", error);
|
||||
setRenameValue(table.title ?? "");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setIsSavingTitle(false);
|
||||
}
|
||||
}, [refresh, renameValue, table, tableId]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-20 items-center justify-center rounded-md border border-dashed bg-gray-50">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!table) {
|
||||
return (
|
||||
<div className="flex h-24 items-center justify-between rounded-md border border-red-200 bg-red-50 px-4 py-2 text-red-600">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<TableIcon className="h-5 w-5" />
|
||||
<span>表格加载失败或不存在</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 rounded-md border border-red-200 px-3 py-1 text-xs font-medium"
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
onDoubleClick={onFullScreen}
|
||||
contentEditable={false}
|
||||
onMouseDown={suppressEditorEvents}
|
||||
onMouseUp={suppressEditorEvents}
|
||||
onMouseMove={suppressEditorEvents}
|
||||
>
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white/90 shadow-[0_10px_36px_rgba(15,23,42,0.05)] transition-all hover:shadow-[0_14px_44px_rgba(15,23,42,0.08)]"
|
||||
style={{
|
||||
width: effectiveWidth,
|
||||
maxWidth: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 bg-white/80 px-4 py-2 backdrop-blur-sm">
|
||||
<div className="flex flex-col">
|
||||
{isRenaming ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="w-48 rounded border border-gray-200 px-2 py-1 text-sm font-semibold text-gray-700 focus:border-emerald-500 focus:outline-none"
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onBlur={handleRenameSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
void handleRenameSubmit();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
setRenameValue(table.title ?? "");
|
||||
setIsRenaming(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-left text-sm font-semibold text-gray-700 hover:text-emerald-600"
|
||||
title="点击重命名表格"
|
||||
onClick={() => setIsRenaming(true)}
|
||||
>
|
||||
<span className="truncate max-w-xs">{table.title}</span>
|
||||
{isSavingTitle && <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-400" />}
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-400">嵌入视图 · 双击进入全屏编辑</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
||||
title="刷新嵌入视图"
|
||||
type="button"
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteTable}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-red-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
|
||||
title="删除表格"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onFullScreen}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
title="进入全屏编辑"
|
||||
type="button"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative w-full overflow-hidden bg-white select-none px-4 pb-4 pt-3"
|
||||
style={{ height: effectiveHeight, minHeight: minEmbedHeight }}
|
||||
>
|
||||
<div className="relative h-full w-full overflow-hidden rounded-xl border border-gray-100 bg-white">
|
||||
<iframe
|
||||
key={`${tableId}-${iframeVersion}`}
|
||||
src={iframeSrc}
|
||||
title={`online-table-${tableId}`}
|
||||
className="block h-full w-full border-0"
|
||||
loading="lazy"
|
||||
onLoad={() => setIframeLoading(false)}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
</div>
|
||||
{iframeLoading && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-white/90">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-500" />
|
||||
<span className="text-xs text-gray-500">正在载入 Luckysheet 视图...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white/90 shadow-[0_10px_36px_rgba(15,23,42,0.05)] transition-all hover:shadow-[0_14px_44px_rgba(15,23,42,0.08)]"
|
||||
style={{
|
||||
width: effectiveWidth,
|
||||
maxWidth: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 bg-white/80 px-4 py-2 backdrop-blur-sm">
|
||||
<div className="flex flex-col">
|
||||
{isRenaming ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="w-48 rounded border border-gray-200 px-2 py-1 text-sm font-semibold text-gray-700 focus:border-emerald-500 focus:outline-none"
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onBlur={handleRenameSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
void handleRenameSubmit();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
setRenameValue(table.title ?? "");
|
||||
setIsRenaming(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-left text-sm font-semibold text-gray-700 hover:text-emerald-600"
|
||||
title="点击重命名表格"
|
||||
onClick={() => setIsRenaming(true)}
|
||||
>
|
||||
<span className="truncate max-w-xs">{table.title}</span>
|
||||
{isSavingTitle && <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-400" />}
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-400">嵌入视图 · 双击进入全屏编辑</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
||||
title="刷新嵌入视图"
|
||||
type="button"
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteTable}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-red-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
|
||||
title="删除表格"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onFullScreen}
|
||||
className="rounded-md p-1.5 text-gray-400 ring-offset-white transition-colors hover:text-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
title="进入全屏编辑"
|
||||
type="button"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative w-full overflow-hidden bg-white select-none px-4 pb-4 pt-3"
|
||||
style={{ height: effectiveHeight, minHeight: minEmbedHeight }}
|
||||
>
|
||||
<div className="relative h-full w-full overflow-hidden rounded-xl border border-gray-100 bg-white">
|
||||
<iframe
|
||||
key={`${tableId}-${iframeVersion}`}
|
||||
src={iframeSrc}
|
||||
title={`online-table-${tableId}`}
|
||||
className="block h-full w-full border-0"
|
||||
loading="lazy"
|
||||
onLoad={() => setIframeLoading(false)}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
</div>
|
||||
{iframeLoading && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-white/90">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-500" />
|
||||
<span className="text-xs text-gray-500">正在载入 Luckysheet 视图...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,39 +17,39 @@ 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";
|
||||
|
||||
type LuckysheetSelection =
|
||||
| {
|
||||
row?: [number, number];
|
||||
column?: [number, number];
|
||||
row_focus?: number;
|
||||
column_focus?: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const isPrintableKey = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented) return false;
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
||||
if (event.key === "Enter" || event.key === "Tab" || event.key === "Escape") return false;
|
||||
if (event.key.length === 1) return true;
|
||||
return event.key === "Process" || event.key === "Unidentified";
|
||||
};
|
||||
|
||||
const isInlineEditorVisible = () => {
|
||||
const inputBox = document.getElementById("luckysheet-input-box");
|
||||
if (!inputBox) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(inputBox);
|
||||
return style.top !== "-10000px" && style.display !== "none";
|
||||
};
|
||||
|
||||
interface HeadlessTableViewerProps {
|
||||
tableId: string;
|
||||
embed?: boolean;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
|
||||
type LuckysheetSelection =
|
||||
| {
|
||||
row?: [number, number];
|
||||
column?: [number, number];
|
||||
row_focus?: number;
|
||||
column_focus?: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const isPrintableKey = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented) return false;
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
||||
if (event.key === "Enter" || event.key === "Tab" || event.key === "Escape") return false;
|
||||
if (event.key.length === 1) return true;
|
||||
return event.key === "Process" || event.key === "Unidentified";
|
||||
};
|
||||
|
||||
const isInlineEditorVisible = () => {
|
||||
const inputBox = document.getElementById("luckysheet-input-box");
|
||||
if (!inputBox) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(inputBox);
|
||||
return style.top !== "-10000px" && style.display !== "none";
|
||||
};
|
||||
|
||||
interface HeadlessTableViewerProps {
|
||||
tableId: string;
|
||||
embed?: boolean;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
|
||||
|
||||
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false, editable }) => {
|
||||
@@ -87,15 +87,15 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
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);
|
||||
}, 700);
|
||||
}, []);
|
||||
|
||||
const stopSavingHint = useCallback(() => {
|
||||
if (savingHintTimerRef.current !== null) {
|
||||
clearTimeout(savingHintTimerRef.current);
|
||||
savingHintTimerRef.current = null;
|
||||
}
|
||||
setShowSavingHint(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -154,71 +154,71 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
canceled = true;
|
||||
};
|
||||
}, [allowInlineEdit, convexEnabled, reloadVersion, tableFromConvex, tableId]);
|
||||
|
||||
|
||||
|
||||
|
||||
const focusLuckysheetEditor = useCallback(() => {
|
||||
const applyFocus = () => {
|
||||
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||
if (editor && typeof editor.focus === "function") {
|
||||
editor.focus();
|
||||
const selection = window.getSelection();
|
||||
if (selection && editor.childNodes.length > 0) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(editor);
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(() => {
|
||||
applyFocus();
|
||||
setTimeout(applyFocus, 0);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isSingleCellSelection = useCallback((range: LuckysheetSelection[] | undefined) => {
|
||||
if (!Array.isArray(range) || range.length !== 1) return false;
|
||||
const target = range[0];
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const rowRange = target.row ?? (typeof target.row_focus === "number" ? [target.row_focus, target.row_focus] : undefined);
|
||||
const columnRange = target.column ?? (typeof target.column_focus === "number" ? [target.column_focus, target.column_focus] : undefined);
|
||||
if (!rowRange || !columnRange) {
|
||||
return false;
|
||||
}
|
||||
return rowRange[0] === rowRange[1] && columnRange[0] === columnRange[1];
|
||||
}, []);
|
||||
|
||||
const tryEnterInlineEdit = useCallback(() => {
|
||||
if (!allowInlineEdit) {
|
||||
return false;
|
||||
}
|
||||
const luckysheetInstance = window.luckysheet;
|
||||
if (!luckysheetInstance || typeof luckysheetInstance.enterEditMode !== "function") {
|
||||
return false;
|
||||
}
|
||||
const selection = luckysheetInstance.getluckysheet_select_save?.();
|
||||
const normalized = Array.isArray(selection)
|
||||
? (selection as LuckysheetSelection[])
|
||||
: selection
|
||||
? [selection as LuckysheetSelection]
|
||||
: undefined;
|
||||
if (!isSingleCellSelection(normalized)) {
|
||||
return false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||
const selection = window.getSelection();
|
||||
if (selection && editor.childNodes.length > 0) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(editor);
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(() => {
|
||||
applyFocus();
|
||||
setTimeout(applyFocus, 0);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isSingleCellSelection = useCallback((range: LuckysheetSelection[] | undefined) => {
|
||||
if (!Array.isArray(range) || range.length !== 1) return false;
|
||||
const target = range[0];
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const rowRange = target.row ?? (typeof target.row_focus === "number" ? [target.row_focus, target.row_focus] : undefined);
|
||||
const columnRange = target.column ?? (typeof target.column_focus === "number" ? [target.column_focus, target.column_focus] : undefined);
|
||||
if (!rowRange || !columnRange) {
|
||||
return false;
|
||||
}
|
||||
return rowRange[0] === rowRange[1] && columnRange[0] === columnRange[1];
|
||||
}, []);
|
||||
|
||||
const tryEnterInlineEdit = useCallback(() => {
|
||||
if (!allowInlineEdit) {
|
||||
return false;
|
||||
}
|
||||
const luckysheetInstance = window.luckysheet;
|
||||
if (!luckysheetInstance || typeof luckysheetInstance.enterEditMode !== "function") {
|
||||
return false;
|
||||
}
|
||||
const selection = luckysheetInstance.getluckysheet_select_save?.();
|
||||
const normalized = Array.isArray(selection)
|
||||
? (selection as LuckysheetSelection[])
|
||||
: selection
|
||||
? [selection as LuckysheetSelection]
|
||||
: undefined;
|
||||
if (!isSingleCellSelection(normalized)) {
|
||||
return false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||
if (editor && document.activeElement === editor && isInlineEditorVisible()) {
|
||||
return;
|
||||
}
|
||||
luckysheetInstance.enterEditMode?.();
|
||||
focusLuckysheetEditor();
|
||||
}, 0);
|
||||
return true;
|
||||
}, [allowInlineEdit, focusLuckysheetEditor, isSingleCellSelection]);
|
||||
|
||||
return true;
|
||||
}, [allowInlineEdit, focusLuckysheetEditor, isSingleCellSelection]);
|
||||
|
||||
const persistSnapshot = useCallback(async () => {
|
||||
if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
||||
return;
|
||||
@@ -262,60 +262,60 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [allowInlineEdit, convexEnabled, startSavingHint, stopSavingHint, table, tableId, updateTable, userId]);
|
||||
|
||||
const debouncedPersist = useDebouncedCallback(() => {
|
||||
void persistSnapshot();
|
||||
}, 1200);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedPersist.cancel();
|
||||
stopSavingHint();
|
||||
};
|
||||
}, [debouncedPersist, stopSavingHint]);
|
||||
|
||||
|
||||
const debouncedPersist = useDebouncedCallback(() => {
|
||||
void persistSnapshot();
|
||||
}, 1200);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedPersist.cancel();
|
||||
stopSavingHint();
|
||||
};
|
||||
}, [debouncedPersist, stopSavingHint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableId) return;
|
||||
// Supabase 已移除:实时订阅由 Convex useQuery 承担(见上方 tableFromConvex)
|
||||
}, [tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLuckysheetReady || !table || !containerRef.current || !window.luckysheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.luckysheet.destroy === "function") {
|
||||
window.luckysheet.destroy(containerId);
|
||||
}
|
||||
containerRef.current.innerHTML = "";
|
||||
|
||||
const sheets =
|
||||
(table.snapshot?.luckysheet && Array.isArray(table.snapshot.luckysheet) && table.snapshot.luckysheet.length > 0)
|
||||
? table.snapshot.luckysheet
|
||||
: (createDefaultTableSnapshot(table.schema ?? DEFAULT_TABLE_SCHEMA).luckysheet ?? []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLuckysheetReady || !table || !containerRef.current || !window.luckysheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.luckysheet.destroy === "function") {
|
||||
window.luckysheet.destroy(containerId);
|
||||
}
|
||||
containerRef.current.innerHTML = "";
|
||||
|
||||
const sheets =
|
||||
(table.snapshot?.luckysheet && Array.isArray(table.snapshot.luckysheet) && table.snapshot.luckysheet.length > 0)
|
||||
? table.snapshot.luckysheet
|
||||
: (createDefaultTableSnapshot(table.schema ?? DEFAULT_TABLE_SCHEMA).luckysheet ?? []);
|
||||
|
||||
window.luckysheet?.create?.({
|
||||
container: containerId,
|
||||
title: table.title ?? tableId,
|
||||
lang: "zh",
|
||||
showinfobar: false,
|
||||
showtoolbar: false,
|
||||
showsheetbar: sheets.length > 1,
|
||||
showstatisticBar: false,
|
||||
allowEdit: allowInlineEdit,
|
||||
allowUpdate: false,
|
||||
enableAddBackTop: false,
|
||||
enableAddRow: false,
|
||||
row: DEFAULT_TABLE_ROWS,
|
||||
column: DEFAULT_TABLE_COLUMNS,
|
||||
data: sheets,
|
||||
pointEdit: allowInlineEdit,
|
||||
pointEditZoom: window.devicePixelRatio ?? 1,
|
||||
pointEditUpdate: allowInlineEdit
|
||||
? () => {
|
||||
debouncedPersist();
|
||||
}
|
||||
: undefined,
|
||||
showsheetbar: sheets.length > 1,
|
||||
showstatisticBar: false,
|
||||
allowEdit: allowInlineEdit,
|
||||
allowUpdate: false,
|
||||
enableAddBackTop: false,
|
||||
enableAddRow: false,
|
||||
row: DEFAULT_TABLE_ROWS,
|
||||
column: DEFAULT_TABLE_COLUMNS,
|
||||
data: sheets,
|
||||
pointEdit: allowInlineEdit,
|
||||
pointEditZoom: window.devicePixelRatio ?? 1,
|
||||
pointEditUpdate: allowInlineEdit
|
||||
? () => {
|
||||
debouncedPersist();
|
||||
}
|
||||
: undefined,
|
||||
hook: allowInlineEdit
|
||||
? {
|
||||
updated: () => {
|
||||
@@ -324,22 +324,22 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
}
|
||||
: undefined,
|
||||
} as any);
|
||||
|
||||
return () => {
|
||||
if (window.luckysheet && typeof window.luckysheet.destroy === "function") {
|
||||
window.luckysheet.destroy(containerId);
|
||||
}
|
||||
};
|
||||
}, [allowInlineEdit, containerId, debouncedPersist, isLuckysheetReady, table, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (window.luckysheet && typeof window.luckysheet.destroy === "function") {
|
||||
window.luckysheet.destroy(containerId);
|
||||
}
|
||||
};
|
||||
}, [allowInlineEdit, containerId, debouncedPersist, isLuckysheetReady, table, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const handlePointerUp: EventListener = (event) => {
|
||||
const target = event.target instanceof Node ? event.target : null;
|
||||
if (target && !container.contains(target)) {
|
||||
@@ -352,100 +352,100 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
return () => {
|
||||
events.forEach((eventName) => container.removeEventListener(eventName, handlePointerUp, true));
|
||||
};
|
||||
}, [allowInlineEdit, containerId, isLuckysheetReady, tryEnterInlineEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||
return;
|
||||
}
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!isPrintableKey(event)) {
|
||||
return;
|
||||
}
|
||||
tryEnterInlineEdit();
|
||||
};
|
||||
const handleCompositionStart = () => {
|
||||
tryEnterInlineEdit();
|
||||
};
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
window.addEventListener("compositionstart", handleCompositionStart, true);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
window.removeEventListener("compositionstart", handleCompositionStart, true);
|
||||
};
|
||||
}, [allowInlineEdit, isLuckysheetReady, tryEnterInlineEdit]);
|
||||
|
||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||
|
||||
const embedContainerStyle = embed
|
||||
? { height: "360px", minHeight: "360px", width: "100%", overflow: "hidden" as const }
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!embed) return;
|
||||
const prevDocOverflow = document.documentElement.style.overflow;
|
||||
const prevBodyOverflow = document.body.style.overflow;
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.documentElement.style.overflow = prevDocOverflow;
|
||||
document.body.style.overflow = prevBodyOverflow;
|
||||
};
|
||||
}, [embed]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
embed ? "h-full w-full bg-transparent overflow-hidden" : "min-h-screen w-full bg-white"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
embed ? "relative h-full w-full overflow-hidden" : "relative h-[calc(100vh-64px)] w-full"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
id={containerId}
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
style={{ display: isLuckysheetReady && !!table && !error ? "block" : "none" }}
|
||||
/>
|
||||
|
||||
{overlayVisible && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-white/90">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-500" />
|
||||
<span className="text-sm text-gray-500">{overlayText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !overlayVisible && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-white/95 text-red-500">
|
||||
<span className="text-sm">{error}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-md border border-red-300 px-3 py-1 text-sm"
|
||||
onClick={() => setReloadVersion((value) => value + 1)}
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{allowInlineEdit && ((isSaving && showSavingHint) || saveError) && (
|
||||
<div className="pointer-events-none absolute bottom-2 right-3 flex flex-col items-end text-xs">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeadlessTableViewer;
|
||||
|
||||
}, [allowInlineEdit, containerId, isLuckysheetReady, tryEnterInlineEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||
return;
|
||||
}
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!isPrintableKey(event)) {
|
||||
return;
|
||||
}
|
||||
tryEnterInlineEdit();
|
||||
};
|
||||
const handleCompositionStart = () => {
|
||||
tryEnterInlineEdit();
|
||||
};
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
window.addEventListener("compositionstart", handleCompositionStart, true);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
window.removeEventListener("compositionstart", handleCompositionStart, true);
|
||||
};
|
||||
}, [allowInlineEdit, isLuckysheetReady, tryEnterInlineEdit]);
|
||||
|
||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||
|
||||
const embedContainerStyle = embed
|
||||
? { height: "360px", minHeight: "360px", width: "100%", overflow: "hidden" as const }
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!embed) return;
|
||||
const prevDocOverflow = document.documentElement.style.overflow;
|
||||
const prevBodyOverflow = document.body.style.overflow;
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.documentElement.style.overflow = prevDocOverflow;
|
||||
document.body.style.overflow = prevBodyOverflow;
|
||||
};
|
||||
}, [embed]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
embed ? "h-full w-full bg-transparent overflow-hidden" : "min-h-screen w-full bg-white"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
embed ? "relative h-full w-full overflow-hidden" : "relative h-[calc(100vh-64px)] w-full"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
id={containerId}
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
style={{ display: isLuckysheetReady && !!table && !error ? "block" : "none" }}
|
||||
/>
|
||||
|
||||
{overlayVisible && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-white/90">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-500" />
|
||||
<span className="text-sm text-gray-500">{overlayText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !overlayVisible && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-white/95 text-red-500">
|
||||
<span className="text-sm">{error}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-md border border-red-300 px-3 py-1 text-sm"
|
||||
onClick={() => setReloadVersion((value) => value + 1)}
|
||||
>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{allowInlineEdit && ((isSaving && showSavingHint) || saveError) && (
|
||||
<div className="pointer-events-none absolute bottom-2 right-3 flex flex-col items-end text-xs">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeadlessTableViewer;
|
||||
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import type { TableRowData } from "@/types/online-table";
|
||||
import { DEFAULT_TABLE_COLUMNS } from "@/lib/online-table";
|
||||
|
||||
const pickCellValue = (cell: any) => {
|
||||
if (!cell) return undefined;
|
||||
if (cell.m != null) return cell.m;
|
||||
if (cell.v?.m != null) return cell.v.m;
|
||||
if (cell.v?.v != null) return cell.v.v;
|
||||
if (cell.v != null && typeof cell.v !== "object") return cell.v;
|
||||
if (cell.w != null) return cell.w;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const extractRowsForPreview = (
|
||||
luckysheetData: any,
|
||||
columns: Array<{ id: string }>,
|
||||
): TableRowData[] => {
|
||||
const sheet = Array.isArray(luckysheetData) ? luckysheetData[0] : null;
|
||||
if (!sheet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columnIds =
|
||||
columns.length > 0 ? columns.map((item) => item.id) : Array.from({ length: DEFAULT_TABLE_COLUMNS }, (_, idx) => `col${idx + 1}`);
|
||||
|
||||
const rows: TableRowData[] = [];
|
||||
const grid = Array.isArray(sheet.data) ? sheet.data : [];
|
||||
|
||||
grid.forEach((row: any[], rowIndex: number) => {
|
||||
if (!Array.isArray(row)) return;
|
||||
const rowObj: TableRowData = {};
|
||||
let hasValue = false;
|
||||
columnIds.forEach((colId, colIndex) => {
|
||||
const value = pickCellValue(row[colIndex]);
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
rowObj[colId] = value;
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
if (hasValue) {
|
||||
rows.push(rowObj);
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length === 0 && Array.isArray(sheet.celldata)) {
|
||||
const map = new Map<number, TableRowData>();
|
||||
sheet.celldata.forEach((cell: { r: number; c: number; v: unknown }) => {
|
||||
const value = pickCellValue(cell?.v ?? cell);
|
||||
if (value === undefined || value === null || value === "") return;
|
||||
const existing = map.get(cell.r) ?? {};
|
||||
existing[columnIds[cell.c] ?? `col${cell.c + 1}`] = value;
|
||||
map.set(cell.r, existing);
|
||||
});
|
||||
Array.from(map.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.forEach(([, row]) => rows.push(row));
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
import type { TableRowData } from "@/types/online-table";
|
||||
import { DEFAULT_TABLE_COLUMNS } from "@/lib/online-table";
|
||||
|
||||
const pickCellValue = (cell: any) => {
|
||||
if (!cell) return undefined;
|
||||
if (cell.m != null) return cell.m;
|
||||
if (cell.v?.m != null) return cell.v.m;
|
||||
if (cell.v?.v != null) return cell.v.v;
|
||||
if (cell.v != null && typeof cell.v !== "object") return cell.v;
|
||||
if (cell.w != null) return cell.w;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const extractRowsForPreview = (
|
||||
luckysheetData: any,
|
||||
columns: Array<{ id: string }>,
|
||||
): TableRowData[] => {
|
||||
const sheet = Array.isArray(luckysheetData) ? luckysheetData[0] : null;
|
||||
if (!sheet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columnIds =
|
||||
columns.length > 0 ? columns.map((item) => item.id) : Array.from({ length: DEFAULT_TABLE_COLUMNS }, (_, idx) => `col${idx + 1}`);
|
||||
|
||||
const rows: TableRowData[] = [];
|
||||
const grid = Array.isArray(sheet.data) ? sheet.data : [];
|
||||
|
||||
grid.forEach((row: any[], rowIndex: number) => {
|
||||
if (!Array.isArray(row)) return;
|
||||
const rowObj: TableRowData = {};
|
||||
let hasValue = false;
|
||||
columnIds.forEach((colId, colIndex) => {
|
||||
const value = pickCellValue(row[colIndex]);
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
rowObj[colId] = value;
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
if (hasValue) {
|
||||
rows.push(rowObj);
|
||||
}
|
||||
});
|
||||
|
||||
if (rows.length === 0 && Array.isArray(sheet.celldata)) {
|
||||
const map = new Map<number, TableRowData>();
|
||||
sheet.celldata.forEach((cell: { r: number; c: number; v: unknown }) => {
|
||||
const value = pickCellValue(cell?.v ?? cell);
|
||||
if (value === undefined || value === null || value === "") return;
|
||||
const existing = map.get(cell.r) ?? {};
|
||||
existing[columnIds[cell.c] ?? `col${cell.c + 1}`] = value;
|
||||
map.set(cell.r, existing);
|
||||
});
|
||||
Array.from(map.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.forEach(([, row]) => rows.push(row));
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user