diff --git a/wolai-frontend/src/app/globals.css b/wolai-frontend/src/app/globals.css
index 21318910..cf77e4ca 100644
--- a/wolai-frontend/src/app/globals.css
+++ b/wolai-frontend/src/app/globals.css
@@ -407,4 +407,9 @@
.wolai-media__resize-handle.is-dragging {
background: rgba(37, 99, 235, 0.85);
}
+ /* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */
+ .luckysheet-input-box,
+ #luckysheet-rich-text-editor {
+ z-index: 100 !important;
+ }
}
diff --git a/wolai-frontend/src/app/tables/[tableId]/view/page.tsx b/wolai-frontend/src/app/tables/[tableId]/view/page.tsx
index 54786c47..e9919ed5 100644
--- a/wolai-frontend/src/app/tables/[tableId]/view/page.tsx
+++ b/wolai-frontend/src/app/tables/[tableId]/view/page.tsx
@@ -13,11 +13,11 @@ export default async function TableViewerPage({ params, searchParams }: TableVie
const resolvedSearch = await searchParams;
const tableId = resolvedParams.tableId;
const embedMode = resolvedSearch?.embed === "1";
+ const allowEditing = resolvedSearch?.readonly !== "1";
return (
-
+
);
}
-
diff --git a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx
index cab51730..bda2a640 100644
--- a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx
+++ b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx
@@ -41,6 +41,15 @@ const isPrintableKey = (event: KeyboardEvent) => {
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";
+};
+
const isElementInsideEditorToolbar = (element: HTMLElement | null) => {
if (!element) return false;
if (element.closest(".luckysheet-wa-editor")) {
@@ -209,11 +218,23 @@ const FullScreenTableEditor: React.FC = ({ tableId,
}, [isLuckysheetReady]);
const focusLuckysheetEditor = useCallback(() => {
- requestAnimationFrame(() => {
+ 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);
});
}, []);
@@ -325,18 +346,53 @@ const FullScreenTableEditor: React.FC = ({ tableId,
if (!isSingleCellSelection(range)) {
return;
}
- setTimeout(() => {
- const editor = document.getElementById("luckysheet-rich-text-editor");
- if (editor && document.activeElement === editor) {
- return;
- }
- luckysheetInstance.enterEditMode();
- focusLuckysheetEditor();
- }, 0);
+ setTimeout(() => {
+ const editor = document.getElementById("luckysheet-rich-text-editor");
+ if (editor && document.activeElement === editor && isInlineEditorVisible()) {
+ return;
+ }
+ luckysheetInstance.enterEditMode();
+ focusLuckysheetEditor();
+ }, 0);
},
[focusLuckysheetEditor, isSingleCellSelection],
);
+ useEffect(() => {
+ if (!isLuckysheetReady) {
+ return;
+ }
+ const container = document.getElementById(LUCKY_SHEET_CONTAINER_ID);
+ if (!container) {
+ return;
+ }
+ const handlePointerUp = (event: PointerEvent | MouseEvent | TouchEvent) => {
+ const target = event.target instanceof Node ? event.target : null;
+ if (target && !container.contains(target)) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ const selection = window.luckysheet?.getluckysheet_select_save?.();
+ if (!selection) {
+ return;
+ }
+ const normalized = Array.isArray(selection)
+ ? (selection as LuckysheetSelection[])
+ : [selection as LuckysheetSelection];
+ tryEnterSingleClickEdit(normalized);
+ });
+ };
+ const events: Array = ["pointerup", "mouseup", "touchend"];
+ events.forEach((eventName) => {
+ container.addEventListener(eventName, handlePointerUp, true);
+ });
+ return () => {
+ events.forEach((eventName) => {
+ container.removeEventListener(eventName, handlePointerUp, true);
+ });
+ };
+ }, [isLuckysheetReady, tryEnterSingleClickEdit]);
+
// Luckysheet 初始化和清理
useEffect(() => {
if (!isLuckysheetReady || !tableData || !containerRef.current || !window.luckysheet) {
@@ -372,6 +428,8 @@ const FullScreenTableEditor: React.FC = ({ tableId,
allowUpdate: false,
gridKey,
loadUrl,
+ pointEdit: true,
+ pointEditZoom: typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1,
uploadImage: async (file: File) => {
const formData = new FormData();
formData.append("image", file);
diff --git a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx
index 29ff2be5..e6e035ca 100644
--- a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx
+++ b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx
@@ -15,14 +15,41 @@ import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoad
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;
}
const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
-const HeadlessTableViewer: React.FC = ({ tableId, embed = false }) => {
+const HeadlessTableViewer: React.FC = ({ tableId, embed = false, editable }) => {
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
const containerRef = useRef(null);
const isLuckysheetReady = useLuckysheetLoader();
@@ -62,13 +89,72 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe
};
}, [tableId, reloadVersion]);
+ const allowInlineEdit = editable ?? embed;
+ 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");
+ if (editor && document.activeElement === editor && isInlineEditorVisible()) {
+ return;
+ }
+ luckysheetInstance.enterEditMode();
+ focusLuckysheetEditor();
+ }, 0);
+ return true;
+ }, [allowInlineEdit, focusLuckysheetEditor, isSingleCellSelection]);
+
const persistSnapshot = useCallback(async () => {
- if (
- !embed ||
- !table ||
- !window.luckysheet ||
- typeof window.luckysheet.getluckysheetfile !== "function"
- ) {
+ if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
return;
}
setIsSaving(true);
@@ -97,7 +183,7 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe
} finally {
setIsSaving(false);
}
- }, [embed, table, tableId]);
+ }, [allowInlineEdit, table, tableId]);
const debouncedPersist = useDebouncedCallback(() => {
void persistSnapshot();
@@ -132,21 +218,21 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe
showtoolbar: false,
showsheetbar: sheets.length > 1,
showstatisticBar: false,
- allowEdit: embed,
+ allowEdit: allowInlineEdit,
allowUpdate: false,
enableAddBackTop: false,
enableAddRow: false,
row: DEFAULT_TABLE_ROWS,
column: DEFAULT_TABLE_COLUMNS,
data: sheets,
- pointEdit: embed,
+ pointEdit: allowInlineEdit,
pointEditZoom: window.devicePixelRatio ?? 1,
- pointEditUpdate: embed
+ pointEditUpdate: allowInlineEdit
? () => {
debouncedPersist();
}
: undefined,
- hook: embed
+ hook: allowInlineEdit
? {
updated: () => {
debouncedPersist();
@@ -160,7 +246,50 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe
window.luckysheet.destroy(containerId);
}
};
- }, [containerId, isLuckysheetReady, table, tableId]);
+ }, [allowInlineEdit, containerId, debouncedPersist, isLuckysheetReady, table, tableId]);
+
+ useEffect(() => {
+ if (!allowInlineEdit || !isLuckysheetReady) {
+ return;
+ }
+ const container = document.getElementById(containerId);
+ if (!container) {
+ return;
+ }
+ const handlePointerUp = (event: PointerEvent | MouseEvent | TouchEvent) => {
+ const target = event.target instanceof Node ? event.target : null;
+ if (target && !container.contains(target)) {
+ return;
+ }
+ tryEnterInlineEdit();
+ };
+ const events: Array = ["pointerup", "mouseup", "touchend"];
+ events.forEach((eventName) => container.addEventListener(eventName, handlePointerUp, true));
+ 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 资源..." : "正在加载表格数据...";
@@ -203,7 +332,7 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe
)}
- {embed && (isSaving || saveError) && (
+ {allowInlineEdit && (isSaving || saveError) && (
{isSaving && 同步中...}
{saveError && {saveError}}