git save current version as 0.0.4
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+23
-22
@@ -419,81 +419,82 @@
|
|||||||
|
|
||||||
.wolai-table-resize-handle {
|
.wolai-table-resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: rgba(15, 23, 42, 0.45);
|
background: rgba(37, 99, 235, 0.12);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
transition: opacity 0.15s ease, background 0.2s ease;
|
transition: opacity 0.15s ease, background 0.2s ease, transform 0.12s ease;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
|
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle.is-dragging {
|
.wolai-table-resize-handle.is-dragging {
|
||||||
background: rgba(37, 99, 235, 0.9);
|
background: rgba(37, 99, 235, 0.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--left,
|
.wolai-table-resize-handle--left,
|
||||||
.wolai-table-resize-handle--right {
|
.wolai-table-resize-handle--right {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 12px;
|
width: 10px;
|
||||||
height: 48px;
|
height: 34px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--left {
|
.wolai-table-resize-handle--left {
|
||||||
left: -6px;
|
left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--right {
|
.wolai-table-resize-handle--right {
|
||||||
right: -6px;
|
right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--top,
|
.wolai-table-resize-handle--top,
|
||||||
.wolai-table-resize-handle--bottom {
|
.wolai-table-resize-handle--bottom {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 48px;
|
width: 34px;
|
||||||
height: 12px;
|
height: 10px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--top {
|
.wolai-table-resize-handle--top {
|
||||||
top: -6px;
|
top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--bottom {
|
.wolai-table-resize-handle--bottom {
|
||||||
bottom: -6px;
|
bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--top-left,
|
.wolai-table-resize-handle--top-left,
|
||||||
.wolai-table-resize-handle--top-right,
|
.wolai-table-resize-handle--top-right,
|
||||||
.wolai-table-resize-handle--bottom-left,
|
.wolai-table-resize-handle--bottom-left,
|
||||||
.wolai-table-resize-handle--bottom-right {
|
.wolai-table-resize-handle--bottom-right {
|
||||||
width: 16px;
|
width: 12px;
|
||||||
height: 16px;
|
height: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--top-left {
|
.wolai-table-resize-handle--top-left {
|
||||||
top: -8px;
|
top: 6px;
|
||||||
left: -8px;
|
left: 6px;
|
||||||
cursor: nwse-resize;
|
cursor: nwse-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--bottom-right {
|
.wolai-table-resize-handle--bottom-right {
|
||||||
bottom: -8px;
|
bottom: 6px;
|
||||||
right: -8px;
|
right: 6px;
|
||||||
cursor: nwse-resize;
|
cursor: nwse-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--top-right {
|
.wolai-table-resize-handle--top-right {
|
||||||
top: -8px;
|
top: 6px;
|
||||||
right: -8px;
|
right: 6px;
|
||||||
cursor: nesw-resize;
|
cursor: nesw-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolai-table-resize-handle--bottom-left {
|
.wolai-table-resize-handle--bottom-left {
|
||||||
bottom: -8px;
|
bottom: 6px;
|
||||||
left: -8px;
|
left: 6px;
|
||||||
cursor: nesw-resize;
|
cursor: nesw-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
File diff suppressed because one or more lines are too long
@@ -79,6 +79,13 @@
|
|||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#__next {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
@@ -407,6 +414,96 @@
|
|||||||
.wolai-media__resize-handle.is-dragging {
|
.wolai-media__resize-handle.is-dragging {
|
||||||
background: rgba(37, 99, 235, 0.85);
|
background: rgba(37, 99, 235, 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.online-table-block {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-table-block:hover .wolai-table-resize-handle,
|
||||||
|
.online-table-block:focus-within .wolai-table-resize-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(37, 99, 235, 0.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: opacity 0.15s ease, background 0.2s ease, transform 0.12s ease;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 40;
|
||||||
|
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle.is-dragging {
|
||||||
|
background: rgba(37, 99, 235, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--left,
|
||||||
|
.wolai-table-resize-handle--right {
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 34px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--left {
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--right {
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--top,
|
||||||
|
.wolai-table-resize-handle--bottom {
|
||||||
|
left: 50%;
|
||||||
|
width: 34px;
|
||||||
|
height: 10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--top {
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--bottom {
|
||||||
|
bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--top-left,
|
||||||
|
.wolai-table-resize-handle--top-right,
|
||||||
|
.wolai-table-resize-handle--bottom-left,
|
||||||
|
.wolai-table-resize-handle--bottom-right {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--top-left {
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--bottom-right {
|
||||||
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--top-right {
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolai-table-resize-handle--bottom-left {
|
||||||
|
bottom: 6px;
|
||||||
|
left: 6px;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
/* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */
|
/* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */
|
||||||
.luckysheet-input-box,
|
.luckysheet-input-box,
|
||||||
#luckysheet-rich-text-editor {
|
#luckysheet-rich-text-editor {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function TableViewerPage({ params, searchParams }: TableVie
|
|||||||
const allowEditing = resolvedSearch?.readonly !== "1";
|
const allowEditing = resolvedSearch?.readonly !== "1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={embedMode ? "min-h-screen bg-transparent" : "min-h-screen bg-white"}>
|
<div className={embedMode ? "h-full w-full bg-transparent" : "min-h-screen bg-white"}>
|
||||||
<HeadlessTableViewer tableId={tableId} embed={embedMode} editable={allowEditing} />
|
<HeadlessTableViewer tableId={tableId} embed={embedMode} editable={allowEditing} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import type { DocumentTable } from "@/types/online-table";
|
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";
|
import { Loader2, Table as TableIcon, Maximize2, Trash2, RotateCw } from "lucide-react";
|
||||||
|
|
||||||
interface CompactTablePreviewProps {
|
interface CompactTablePreviewProps {
|
||||||
@@ -56,6 +56,13 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
|||||||
const { table, isLoading, refresh } = useTableData(tableId);
|
const { table, isLoading, refresh } = useTableData(tableId);
|
||||||
const [iframeVersion, setIframeVersion] = useState(0);
|
const [iframeVersion, setIframeVersion] = useState(0);
|
||||||
const [iframeLoading, setIframeLoading] = useState(true);
|
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(
|
const iframeSrc = useMemo(
|
||||||
() => `/tables/${tableId}/view?embed=1&v=${iframeVersion}`,
|
() => `/tables/${tableId}/view?embed=1&v=${iframeVersion}`,
|
||||||
@@ -83,6 +90,12 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
|||||||
};
|
};
|
||||||
}, [refresh, tableId]);
|
}, [refresh, tableId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (table?.title !== undefined) {
|
||||||
|
setRenameValue(table.title ?? "");
|
||||||
|
}
|
||||||
|
}, [table?.title]);
|
||||||
|
|
||||||
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
|
const suppressEditorEvents = useCallback((event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -105,8 +118,56 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
|||||||
setIframeLoading(true);
|
setIframeLoading(true);
|
||||||
refresh();
|
refresh();
|
||||||
}, [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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -144,10 +205,48 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
|||||||
onMouseUp={suppressEditorEvents}
|
onMouseUp={suppressEditorEvents}
|
||||||
onMouseMove={suppressEditorEvents}
|
onMouseMove={suppressEditorEvents}
|
||||||
>
|
>
|
||||||
<div className="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-md">
|
<div
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 bg-white/90 px-4 py-2 backdrop-blur">
|
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)]"
|
||||||
<div>
|
style={{
|
||||||
<p className="text-sm font-semibold text-gray-700">{table.title}</p>
|
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>
|
<p className="text-xs text-gray-400">嵌入视图 · 双击进入全屏编辑</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -178,18 +277,23 @@ const CompactTablePreview: React.FC<CompactTablePreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full bg-gray-50" style={{ height: effectiveHeight, minHeight: 320 }}>
|
<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
|
<iframe
|
||||||
key={`${tableId}-${iframeVersion}`}
|
key={`${tableId}-${iframeVersion}`}
|
||||||
src={iframeSrc}
|
src={iframeSrc}
|
||||||
title={`online-table-${tableId}`}
|
title={`online-table-${tableId}`}
|
||||||
className="h-full w-full border-0"
|
className="block h-full w-full border-0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onLoad={() => setIframeLoading(false)}
|
onLoad={() => setIframeLoading(false)}
|
||||||
allow="clipboard-read; clipboard-write"
|
allow="clipboard-read; clipboard-write"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{iframeLoading && (
|
{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" />
|
<Loader2 className="h-5 w-5 animate-spin text-gray-500" />
|
||||||
<span className="text-xs text-gray-500">正在载入 Luckysheet 视图...</span>
|
<span className="text-xs text-gray-500">正在载入 Luckysheet 视图...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
const lastPointerDownInGridRef = useRef(false);
|
const lastPointerDownInGridRef = useRef(false);
|
||||||
const savingHintTimerRef = useRef<number | null>(null);
|
const savingHintTimerRef = useRef<number | null>(null);
|
||||||
const [showSavingHint, setShowSavingHint] = useState(false);
|
const [showSavingHint, setShowSavingHint] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
const [isSavingTitle, setIsSavingTitle] = useState(false);
|
||||||
|
|
||||||
const startSavingHint = useCallback(() => {
|
const startSavingHint = useCallback(() => {
|
||||||
if (savingHintTimerRef.current !== null) return;
|
if (savingHintTimerRef.current !== null) return;
|
||||||
@@ -137,6 +140,12 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
}
|
}
|
||||||
}, [tableData]);
|
}, [tableData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableData?.title !== undefined) {
|
||||||
|
setRenameValue(tableData.title ?? "");
|
||||||
|
}
|
||||||
|
}, [tableData?.title]);
|
||||||
|
|
||||||
const luckysheetSheets = useMemo(() => {
|
const luckysheetSheets = useMemo(() => {
|
||||||
if (tableData?.snapshot?.luckysheet && Array.isArray(tableData.snapshot.luckysheet) && tableData.snapshot.luckysheet.length > 0) {
|
if (tableData?.snapshot?.luckysheet && Array.isArray(tableData.snapshot.luckysheet) && tableData.snapshot.luckysheet.length > 0) {
|
||||||
return tableData.snapshot.luckysheet;
|
return tableData.snapshot.luckysheet;
|
||||||
@@ -530,6 +539,31 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
const showLoadingOverlay = !isLuckysheetReady || isTableLoading;
|
const showLoadingOverlay = !isLuckysheetReady || isTableLoading;
|
||||||
const loadingMessage = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
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
|
const statusText = saveError
|
||||||
? saveError
|
? saveError
|
||||||
: isSaving && showSavingHint
|
: 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">
|
<header className="flex justify-between items-center p-3 border-b border-gray-200 shadow-sm">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Table className="h-5 w-5 text-blue-500" />
|
<Table className="h-5 w-5 text-blue-500" />
|
||||||
<h1 className="text-lg font-bold">
|
{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)}
|
在线表格编辑 - {(tableData?.title ?? tableId).slice(0, 24)}
|
||||||
</h1>
|
</span>
|
||||||
|
{isSavingTitle && <Loader2 className="h-4 w-4 animate-spin text-gray-400" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<Zap className="h-4 w-4 text-yellow-500" />
|
<Zap className="h-4 w-4 text-yellow-500" />
|
||||||
<span className="text-xs text-yellow-600 font-medium" title="协同模式">协同模式 (单用户模式)</span>
|
<span className="text-xs text-yellow-600 font-medium" title="协同模式">协同模式 (单用户模式)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
};
|
};
|
||||||
}, [tableId, reloadVersion]);
|
}, [tableId, reloadVersion]);
|
||||||
|
|
||||||
|
|
||||||
const allowInlineEdit = editable ?? embed;
|
const allowInlineEdit = editable ?? embed;
|
||||||
const focusLuckysheetEditor = useCallback(() => {
|
const focusLuckysheetEditor = useCallback(() => {
|
||||||
const applyFocus = () => {
|
const applyFocus = () => {
|
||||||
@@ -360,15 +361,33 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
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 (
|
return (
|
||||||
<div
|
<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}
|
style={embedContainerStyle}
|
||||||
>
|
>
|
||||||
<div
|
<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}
|
style={embedContainerStyle}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -412,3 +431,4 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default HeadlessTableViewer;
|
export default HeadlessTableViewer;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user