diff --git a/plugins/canvas/registry/build.mjs b/plugins/canvas/registry/build.mjs index 29be68e..dc7a92e 100644 --- a/plugins/canvas/registry/build.mjs +++ b/plugins/canvas/registry/build.mjs @@ -19,7 +19,7 @@ const OFFICIAL = [ { id: "svg", dir: "svg", name: "SVG 节点", description: "编辑与渲染 SVG,可接收上游文本节点的 SVG 源码", icon: "🔷" }, { id: "html", dir: "html", name: "HTML 节点", description: "沙箱 iframe 渲染 HTML,支持 {{input}} 注入上游文本", icon: "🌐" }, { id: "panorama", dir: "panorama", name: "3D 全景节点", description: "查看 360° 等距柱状全景图,可从上游图片节点取图", icon: "🧭" }, - { id: "sticky-note", dir: "sticky-note", name: "便利贴节点", description: "可换色、可编辑、可衍生文本节点的便利贴", icon: "📌" }, + { id: "sticky-note", dir: "sticky-note", name: "便利贴节点", description: "可自选颜色、双击编辑、拖动即可移动的便利贴", icon: "📌" }, ]; // 读取插件 package.json 的 version 作为清单版本的唯一来源 diff --git a/plugins/canvas/sticky-note/package.json b/plugins/canvas/sticky-note/package.json index 72d6a62..8c84ac5 100644 --- a/plugins/canvas/sticky-note/package.json +++ b/plugins/canvas/sticky-note/package.json @@ -1,6 +1,6 @@ { "name": "canvas-plugin-sticky-note", - "version": "1.0.0", + "version": "1.1.0", "private": true, "type": "module", "description": "Infinite Canvas 便利贴节点插件", diff --git a/plugins/canvas/sticky-note/src/index.tsx b/plugins/canvas/sticky-note/src/index.tsx index f3eaebe..b3d049b 100644 --- a/plugins/canvas/sticky-note/src/index.tsx +++ b/plugins/canvas/sticky-note/src/index.tsx @@ -1,55 +1,139 @@ -// 便利贴节点:可换色、可编辑、可衍生文本节点(演示 ctx.applyOps 画布指令集)。 -import { definePlugin, useState } from "@infinite-canvas/plugin-sdk"; +// 便利贴节点:纯展示便利贴——整块可拖动、双击编辑、右上角自选颜色。 +// 不再声明 resource(避免宿主在右上角显示「文本N」资源角标),也不再衍生节点。 +import { definePlugin, useEffect, useRef, useState } from "@infinite-canvas/plugin-sdk"; import type { CanvasNodeContentProps } from "@infinite-canvas/plugin-sdk"; -const COLORS = ["#fde68a", "#fca5a5", "#a7f3d0", "#bfdbfe", "#ddd6fe"]; +// 预设便签色(点选切换),并额外提供自定义取色 +const PRESET_COLORS = ["#fde68a", "#fca5a5", "#fdba74", "#a7f3d0", "#bfdbfe", "#ddd6fe", "#f9a8d4", "#e7e5e4"]; +const DEFAULT_COLOR = PRESET_COLORS[0]; function StickyNoteContent({ ctx }: CanvasNodeContentProps) { const [editing, setEditing] = useState(false); - // pluginColor 是插件自定义 metadata 字段(非内置字段),读出为 unknown,按需断言 - const color = (ctx.node.metadata?.pluginColor as string | undefined) || COLORS[0]; - const content = ctx.node.metadata?.content || ""; + const [paletteOpen, setPaletteOpen] = useState(false); + // 取色时的本地预览色:仅本组件即时重渲染,避免每次都写宿主 store + const [draftColor, setDraftColor] = useState(null); + const rootRef = useRef(null); + const commitTimerRef = useRef(null); - const cycleColor = () => { - const next = COLORS[(COLORS.indexOf(color) + 1) % COLORS.length]; - ctx.updateMetadata({ pluginColor: next }); + // pluginColor 是插件自定义 metadata 字段(读出为 unknown,按需断言) + const committedColor = (ctx.node.metadata?.pluginColor as string | undefined) || DEFAULT_COLOR; + const color = draftColor ?? committedColor; + const content = (ctx.node.metadata?.content as string | undefined) || ""; + + // 点击便利贴外部时:退出编辑并收起调色板。 + // 监听 pointerdown 的 capture 阶段:宿主画布在 pointerdown 上 preventDefault(会抑制 + // 兼容 mousedown 事件),所以必须用 pointerdown;capture 阶段又能先于宿主的 stopPropagation + // 触发,避免「按 Esc 能退出、点别处退不出」。 + useEffect(() => { + if (!editing && !paletteOpen) return; + const onDocDown = (e: PointerEvent) => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) { + setEditing(false); + setPaletteOpen(false); + } + }; + document.addEventListener("pointerdown", onDocDown, true); + return () => document.removeEventListener("pointerdown", onDocDown, true); + }, [editing, paletteOpen]); + + const pickColor = (next: string) => ctx.updateMetadata({ pluginColor: next }); + + // 连续取色(系统取色器拖动时 onChange 高频触发)会不停调用 updateMetadata, + // 而宿主每次都会整表重建 + 全画布重渲染 + 持久化,导致卡顿。 + // 这里先本地预览(setDraftColor),再节流提交到宿主。 + const previewColor = (next: string) => { + setDraftColor(next); + if (commitTimerRef.current) clearTimeout(commitTimerRef.current); + commitTimerRef.current = window.setTimeout(() => { + commitTimerRef.current = null; + pickColor(next); + }, 150); }; - const spawnTextNode = () => { - const id = `sticky-${ctx.node.id}-${ctx.getNodes().length}`; - ctx.applyOps([ - { - type: "add_node", - id, - nodeType: "text", - title: "便利贴衍生", - x: ctx.node.position?.x ?? 0, - y: (ctx.node.position?.y ?? 0) + 260, - metadata: { content, status: "success" }, - }, - { type: "connect_nodes", fromNodeId: ctx.node.id, toNodeId: id }, - ]); + // 立即提交(点击预设色、取色器关闭时用):清掉待提交的节流并写入。 + const commitColor = (next: string) => { + if (commitTimerRef.current) { + clearTimeout(commitTimerRef.current); + commitTimerRef.current = null; + } + setDraftColor(next); + pickColor(next); }; - const btn = { width: 26, height: 26, borderRadius: 8, border: "none", cursor: "pointer", background: "rgba(0,0,0,.08)", fontSize: 13 } as const; + useEffect(() => () => { + if (commitTimerRef.current) clearTimeout(commitTimerRef.current); + }, []); + + // 宿主里的颜色一旦真正变化(提交完成 / 撤销重做),清掉本地预览,回到 store 为准。 + // 拖动取色期间 committedColor 不变,draftColor 得以保留,故不影响即时预览。 + useEffect(() => { + setDraftColor(null); + }, [committedColor]); + + // 交互控件上按下时阻止冒泡,避免误触发节点拖动/双击编辑 + const stop = (e: { stopPropagation: () => void }) => e.stopPropagation(); return ( -
e.stopPropagation()} style={{ position: "relative", height: "100%", width: "100%", display: "flex", flexDirection: "column", background: color, borderRadius: 16, padding: 12, boxSizing: "border-box" }}> -
- - - +
{ + e.stopPropagation(); + setEditing(true); + }} + > + {/* 右上角:当前颜色小圆点,点开后自选颜色(预设 + 自定义) */} +
+
+ ) : null}
+ + {/* 内容区:双击进入编辑;非编辑态整块可直接拖动移动节点 */} {editing ? ( -