feat(workspace): integrate persisted canvas, element editing and session renewal

This commit is contained in:
2026-11-06 12:50:43 +08:00
parent 45e78f2e43
commit 373582906d
36 changed files with 1028 additions and 193 deletions
+10 -7
View File
@@ -10,15 +10,17 @@ import { NodeStyleToolbar } from "./node-style-toolbar";
import styles from "./canvas.module.css";
export const NodeEditing = createContext({
begin: () => {}, edit: (_id: string, _text: string) => {},
begin: () => {}, select: (_id: string) => {}, edit: (_id: string, _text: string) => {},
branch: (_id: string, _direction: NodeDirection, _point: XYPosition) => {},
transform: (_id: string, _transform: NodeTransform) => {},
style: (_id: string, _style: TextStyle, _fontSize?: number) => {},
rename: (_id: string, _title: string) => {},
frame: (_id: string, _patch: { title?: string; labelMode?: import("@/lib/canvas-elements").FrameLabelMode }) => {},
rotating: null as string | null, rotate: (_id: string | null) => {},
});
type Gesture = { node: CanvasNode; start: XYPosition; direction: "bottom-right" | "rotate" };
export function TransformableNode(props: NodeProps<CanvasNode> & { children: ReactNode; className?: string }) {
export function TransformableNode(props: NodeProps<CanvasNode> & { children: ReactNode; className?: string; hideStyleToolbar?: boolean }) {
const { id, data, selected, width, height, children, className = "" } = props;
const editing = useContext(NodeEditing);
const flow = useReactFlow<CanvasNode>();
@@ -26,7 +28,7 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
const frame = useRef<HTMLDivElement>(null);
const gesture = useRef<Gesture | null>(null);
const rotation = data.rotation || 0;
const plain = data.kind === "plain-text" || data.kind === "icon";
const plain = data.kind === "plain-text" || data.kind === "icon" || data.kind === "plain-image";
const capabilities = elementCapabilities(data.kind);
const rotating = capabilities.rotatable && editing.rotating === id && selected;
useEffect(() => { update(id); }, [id, rotation, width, height, selected, update]);
@@ -47,6 +49,7 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
const node = flow.getNode(id);
if (!node) return;
editing.begin();
if (!node.selected) editing.select(id);
gesture.current = { node, direction, start: flow.screenToFlowPosition({ x: event.clientX, y: event.clientY }) };
event.currentTarget.setPointerCapture(event.pointerId);
}
@@ -64,15 +67,15 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
}
return <>
<NodeStyleToolbar {...props} />
{!props.hideStyleToolbar && <NodeStyleToolbar {...props} />}
<div ref={frame} className={`${styles.transformFrame} ${selected ? styles.selectedFrame : ""} ${plain ? styles.plainFrame : ""} ${rotating ? styles.rotatingFrame + " nodrag nopan" : ""} ${className}`}
onPointerDownCapture={rotating ? (event) => start(event, "rotate") : undefined}
onPointerMove={move} onPointerUp={finish} onPointerCancel={finish}
onLostPointerCapture={() => { gesture.current = null; }}
style={{ width: width || (data.kind === "icon" ? 80 : plain ? 240 : 260), height: height || (data.kind === "icon" ? 80 : plain ? 72 : 200), transform: `rotate(${rotation}deg)` }}>
style={{ width: width || (data.kind === "frame" ? 640 : data.kind === "plain-image" ? 320 : data.kind === "icon" ? 80 : plain ? 240 : 260), height: height || (data.kind === "frame" ? 400 : data.kind === "plain-image" ? 240 : data.kind === "icon" ? 80 : plain ? 72 : 200), transform: `rotate(${rotation}deg)` }}>
{children}
{capabilities.resizable && <button type="button" className={`${styles.cornerResize} nodrag nopan`} tabIndex={selected && !rotating ? 0 : -1}
aria-hidden={!selected || rotating} aria-label="拖动右下角调整大小" title="拖动调整大小"
{capabilities.resizable && <button type="button" className={`${styles.cornerResize} nodrag nopan`} tabIndex={rotating ? -1 : 0}
aria-hidden={rotating} aria-label="拖动右下角调整大小" title="拖动调整大小"
onPointerDown={(event) => start(event, "bottom-right")} onPointerMove={move} onPointerUp={finish} onPointerCancel={finish}>
<Grip size={14} />
</button>}