git save current version as 0.0.4
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import type { DocumentTable } from "@/types/online-table";
|
||||
import { deleteOnlineTable, getDocumentTable } from "@/lib/online-table";
|
||||
import { deleteOnlineTable, getDocumentTable, saveOnlineTable } from "@/lib/online-table";
|
||||
import { Loader2, Table as TableIcon, Maximize2, Trash2, RotateCw } from "lucide-react";
|
||||
|
||||
interface CompactTablePreviewProps {
|
||||
@@ -56,6 +56,13 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
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}`,
|
||||
@@ -83,6 +90,12 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
};
|
||||
}, [refresh, tableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (table?.title !== undefined) {
|
||||
setRenameValue(table.title ?? "");
|
||||
}
|
||||
}, [table?.title]);
|
||||
|
||||
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
@@ -105,8 +118,56 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
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 effectiveHeight = height ?? 520;
|
||||
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 (
|
||||
@@ -144,10 +205,48 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
onMouseUp={suppressEditorEvents}
|
||||
onMouseMove={suppressEditorEvents}
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-md">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 bg-white/90 px-4 py-2 backdrop-blur">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-700">{table.title}</p>
|
||||
<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">
|
||||
@@ -178,18 +277,23 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full bg-gray-50" style={{ height: effectiveHeight, minHeight: 320 }}>
|
||||
<iframe
|
||||
key={`${tableId}-${iframeVersion}`}
|
||||
src={iframeSrc}
|
||||
title={`online-table-${tableId}`}
|
||||
className="h-full w-full border-0"
|
||||
loading="lazy"
|
||||
onLoad={() => setIframeLoading(false)}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
<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 bg-white/90">
|
||||
<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>
|
||||
|
||||
@@ -78,6 +78,9 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
const lastPointerDownInGridRef = useRef(false);
|
||||
const savingHintTimerRef = useRef<number | null>(null);
|
||||
const [showSavingHint, setShowSavingHint] = useState(false);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||
|
||||
const startSavingHint = useCallback(() => {
|
||||
if (savingHintTimerRef.current !== null) return;
|
||||
@@ -137,6 +140,12 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
}
|
||||
}, [tableData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableData?.title !== undefined) {
|
||||
setRenameValue(tableData.title ?? "");
|
||||
}
|
||||
}, [tableData?.title]);
|
||||
|
||||
const luckysheetSheets = useMemo(() => {
|
||||
if (tableData?.snapshot?.luckysheet && Array.isArray(tableData.snapshot.luckysheet) && tableData.snapshot.luckysheet.length > 0) {
|
||||
return tableData.snapshot.luckysheet;
|
||||
@@ -530,6 +539,31 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
const showLoadingOverlay = !isLuckysheetReady || isTableLoading;
|
||||
const loadingMessage = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||
|
||||
const handleRenameSubmit = useCallback(async () => {
|
||||
if (!tableData) {
|
||||
setIsRenaming(false);
|
||||
return;
|
||||
}
|
||||
const nextTitle = (renameValue || "").trim() || "未命名表格";
|
||||
if (nextTitle === tableData.title) {
|
||||
setIsRenaming(false);
|
||||
return;
|
||||
}
|
||||
setIsSavingTitle(true);
|
||||
try {
|
||||
const updated = await saveOnlineTable(tableId, { title: nextTitle });
|
||||
const finalTitle = updated.title ?? nextTitle;
|
||||
setTableData((prev) => (prev ? { ...prev, title: finalTitle } : prev));
|
||||
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
|
||||
} catch (error) {
|
||||
console.error("重命名表格失败", error);
|
||||
setRenameValue(tableData.title ?? "");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setIsSavingTitle(false);
|
||||
}
|
||||
}, [renameValue, tableData, tableId]);
|
||||
|
||||
const statusText = saveError
|
||||
? saveError
|
||||
: isSaving && showSavingHint
|
||||
@@ -546,9 +580,38 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
||||
<header className="flex justify-between items-center p-3 border-b border-gray-200 shadow-sm">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Table className="h-5 w-5 text-blue-500" />
|
||||
<h1 className="text-lg font-bold">
|
||||
在线表格编辑 - {(tableData?.title ?? tableId).slice(0, 24)}
|
||||
</h1>
|
||||
{isRenaming ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="w-64 rounded border border-gray-200 px-2 py-1 text-lg font-semibold text-gray-800 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(tableData?.title ?? "");
|
||||
setIsRenaming(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-left text-lg font-bold text-gray-900 hover:text-emerald-600"
|
||||
onClick={() => setIsRenaming(true)}
|
||||
title="点击重命名表格"
|
||||
>
|
||||
<span className="truncate max-w-xs">
|
||||
在线表格编辑 - {(tableData?.title ?? tableId).slice(0, 24)}
|
||||
</span>
|
||||
{isSavingTitle && <Loader2 className="h-4 w-4 animate-spin text-gray-400" />}
|
||||
</button>
|
||||
)}
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
<span className="text-xs text-yellow-600 font-medium" title="协同模式">协同模式 (单用户模式)</span>
|
||||
</div>
|
||||
|
||||
@@ -121,6 +121,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
};
|
||||
}, [tableId, reloadVersion]);
|
||||
|
||||
|
||||
const allowInlineEdit = editable ?? embed;
|
||||
const focusLuckysheetEditor = useCallback(() => {
|
||||
const applyFocus = () => {
|
||||
@@ -360,15 +361,33 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||
|
||||
const embedContainerStyle = embed ? { minHeight: "100vh", height: "100vh" } : undefined;
|
||||
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 ? "w-full bg-transparent" : "min-h-screen w-full bg-white"}
|
||||
className={
|
||||
embed ? "h-full w-full bg-transparent overflow-hidden" : "min-h-screen w-full bg-white"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
className={embed ? "relative w-full" : "relative h-[calc(100vh-64px)] w-full"}
|
||||
className={
|
||||
embed ? "relative h-full w-full overflow-hidden" : "relative h-[calc(100vh-64px)] w-full"
|
||||
}
|
||||
style={embedContainerStyle}
|
||||
>
|
||||
<div
|
||||
@@ -412,3 +431,4 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
||||
};
|
||||
|
||||
export default HeadlessTableViewer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user