"use client"; import { BlockNoteEditor, Block } from "@blocknote/core"; import { createReactBlockSpec } from "@blocknote/react"; import React, { useCallback, useMemo, useState } from "react"; import type { CustomBlockSchema } from "../schema"; import CompactTablePreview from "@/components/online-table/CompactTablePreview"; import { useEditorBridgeStore } from "@/store/editor-bridge"; const DEFAULT_WIDTH = 960; const DEFAULT_HEIGHT = 520; const MIN_WIDTH = 420; const MAX_WIDTH = 1400; const MIN_HEIGHT = 320; const MAX_HEIGHT = 900; type ResizeHandle = | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right"; const handleMapping: Record< ResizeHandle, { horizontal?: "left" | "right"; vertical?: "top" | "bottom" } > = { left: { horizontal: "left" }, right: { horizontal: "right" }, top: { vertical: "top" }, bottom: { vertical: "bottom" }, "top-left": { horizontal: "left", vertical: "top" }, "top-right": { horizontal: "right", vertical: "top" }, "bottom-left": { horizontal: "left", vertical: "bottom" }, "bottom-right": { horizontal: "right", vertical: "bottom" }, }; // 占位符组件:在紧凑模式下渲染表格块 const OnlineTableBlockComponent = ({ block, editor, }: any) => { const { tableId } = block.props; const openTableFullScreen = useEditorBridgeStore((state) => state.bridge?.openTableFullScreen); const storedWidth = typeof block.props.width === "number" ? block.props.width : undefined; const storedHeight = typeof block.props.height === "number" ? block.props.height : undefined; const [activeHandle, setActiveHandle] = useState(null); const [draftSize, setDraftSize] = useState({ width: storedWidth ?? DEFAULT_WIDTH, height: storedHeight ?? DEFAULT_HEIGHT, }); const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); const committedSize = useMemo( () => ({ width: clamp(storedWidth ?? DEFAULT_WIDTH, MIN_WIDTH, MAX_WIDTH), height: clamp(storedHeight ?? DEFAULT_HEIGHT, MIN_HEIGHT, MAX_HEIGHT), }), [storedHeight, storedWidth], ); const commitSize = useCallback( (next: { width: number; height: number }) => { setDraftSize(next); editor.updateBlock(block, { props: { ...block.props, width: next.width, height: next.height, }, }); }, [block, editor], ); // 阶段三:实现双击/按钮进入全屏编辑 const handleFullScreen = () => { if (openTableFullScreen) { openTableFullScreen(tableId); } else { console.error("Editor bridge not ready or openTableFullScreen missing."); } }; const handleDelete = useCallback(() => { editor.removeBlocks([block.id]); }, [block.id, editor]); const startResize = useCallback( (handle: ResizeHandle) => (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); const startX = event.clientX; const startY = event.clientY; const startWidth = committedSize.width; const startHeight = committedSize.height; let nextWidth = startWidth; let nextHeight = startHeight; const axes = handleMapping[handle]; setActiveHandle(handle); setDraftSize({ width: startWidth, height: startHeight }); document.body.style.userSelect = "none"; const cursor = axes.horizontal && axes.vertical ? axes.horizontal === "left" ? axes.vertical === "top" ? "nwse-resize" : "nesw-resize" : axes.vertical === "top" ? "nesw-resize" : "nwse-resize" : axes.horizontal ? "ew-resize" : "ns-resize"; document.body.style.cursor = cursor; const handleMove = (moveEvent: MouseEvent) => { const deltaX = moveEvent.clientX - startX; const deltaY = moveEvent.clientY - startY; if (axes.horizontal === "left") { nextWidth = clamp(startWidth - deltaX, MIN_WIDTH, MAX_WIDTH); } else if (axes.horizontal === "right") { nextWidth = clamp(startWidth + deltaX, MIN_WIDTH, MAX_WIDTH); } else { nextWidth = startWidth; } if (axes.vertical === "top") { nextHeight = clamp(startHeight - deltaY, MIN_HEIGHT, MAX_HEIGHT); } else if (axes.vertical === "bottom") { nextHeight = clamp(startHeight + deltaY, MIN_HEIGHT, MAX_HEIGHT); } else { nextHeight = startHeight; } setDraftSize({ width: nextWidth, height: nextHeight, }); }; const handleUp = () => { window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); document.body.style.userSelect = ""; document.body.style.cursor = ""; setActiveHandle(null); commitSize({ width: Math.round(nextWidth), height: Math.round(nextHeight), }); }; window.addEventListener("mousemove", handleMove); window.addEventListener("mouseup", handleUp); }, [commitSize, committedSize.height, committedSize.width], ); const size = useMemo(() => { const src = activeHandle ? draftSize : committedSize; return { width: clamp(src.width, MIN_WIDTH, MAX_WIDTH), height: clamp(src.height, MIN_HEIGHT, MAX_HEIGHT), }; }, [activeHandle, committedSize, draftSize]); const handleClass = (handle: ResizeHandle) => `wolai-table-resize-handle wolai-table-resize-handle--${handle} ${ activeHandle === handle ? "is-dragging" : "" }`; return (
); }; // Block Spec 定义 export const onlineTableBlock = createReactBlockSpec( { type: "onlineTable", propSchema: { tableId: { default: "new-table-id" }, // 应该在创建时被覆盖 title: { default: "未命名表格" }, width: { default: DEFAULT_WIDTH }, height: { default: DEFAULT_HEIGHT }, }, content: "inline", // 允许内联内容,但通常表格块不会有太多内联内容 }, { render: (props) => , } );