chore: release 0.0.1
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Loader2, RotateCw } from "lucide-react";
|
||||
import type { DocumentTable } from "@/types/online-table";
|
||||
import {
|
||||
DEFAULT_TABLE_COLUMNS,
|
||||
DEFAULT_TABLE_ROWS,
|
||||
DEFAULT_TABLE_SCHEMA,
|
||||
createDefaultTableSnapshot,
|
||||
getDocumentTable,
|
||||
saveOnlineTable,
|
||||
} from "@/lib/online-table";
|
||||
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { extractRowsForPreview } from "@/components/online-table/utils";
|
||||
|
||||
interface HeadlessTableViewerProps {
|
||||
tableId: string;
|
||||
embed?: boolean;
|
||||
}
|
||||
|
||||
const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
|
||||
|
||||
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false }) => {
|
||||
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isLuckysheetReady = useLuckysheetLoader();
|
||||
const [table, setTable] = useState<DocumentTable | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reloadVersion, setReloadVersion] = useState(0);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
getDocumentTable(tableId)
|
||||
.then((data) => {
|
||||
if (!canceled) {
|
||||
setTable(data);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("加载表格失败", err);
|
||||
if (!canceled) {
|
||||
setTable(null);
|
||||
setError("无法加载表格数据");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!canceled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [tableId, reloadVersion]);
|
||||
|
||||
const persistSnapshot = useCallback(async () => {
|
||||
if (
|
||||
!embed ||
|
||||
!table ||
|
||||
!window.luckysheet ||
|
||||
typeof window.luckysheet.getluckysheetfile !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const luckysheetData = window.luckysheet.getluckysheetfile?.() ?? [];
|
||||
const rows = extractRowsForPreview(
|
||||
luckysheetData,
|
||||
(table.schema?.columns ?? []).map((item) => ({ id: item.id })),
|
||||
).filter((row) => row && typeof row === "object" && Object.keys(row).length > 0);
|
||||
const snapshot = {
|
||||
...(table.snapshot ?? {}),
|
||||
rows,
|
||||
luckysheet: Array.isArray(luckysheetData) ? luckysheetData : table.snapshot?.luckysheet ?? [],
|
||||
};
|
||||
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 {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [embed, table, tableId]);
|
||||
|
||||
const debouncedPersist = useDebouncedCallback(() => {
|
||||
void persistSnapshot();
|
||||
}, 1200);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedPersist.cancel();
|
||||
};
|
||||
}, [debouncedPersist]);
|
||||
|
||||
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: embed,
|
||||
allowUpdate: false,
|
||||
enableAddBackTop: false,
|
||||
enableAddRow: false,
|
||||
row: DEFAULT_TABLE_ROWS,
|
||||
column: DEFAULT_TABLE_COLUMNS,
|
||||
data: sheets,
|
||||
pointEdit: embed,
|
||||
pointEditZoom: window.devicePixelRatio ?? 1,
|
||||
pointEditUpdate: embed
|
||||
? () => {
|
||||
debouncedPersist();
|
||||
}
|
||||
: undefined,
|
||||
hook: embed
|
||||
? {
|
||||
updated: () => {
|
||||
debouncedPersist();
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (window.luckysheet && typeof window.luckysheet.destroy === "function") {
|
||||
window.luckysheet.destroy(containerId);
|
||||
}
|
||||
};
|
||||
}, [containerId, isLuckysheetReady, table, tableId]);
|
||||
|
||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||
|
||||
const embedContainerStyle = embed ? { minHeight: "100vh", height: "100vh" } : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={embed ? "w-full bg-transparent" : "min-h-screen w-full bg-white"}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
className={embed ? "relative w-full" : "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>
|
||||
)}
|
||||
{embed && (isSaving || 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>}
|
||||
{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;
|
||||
Reference in New Issue
Block a user