feat(plugin): implement canvas node plugin system with dynamic registration and remote installation

This commit is contained in:
HouYunFei
2026-07-15 11:47:25 +08:00
parent f0db29d8e3
commit eef4d96787
51 changed files with 4933 additions and 72 deletions
+64
View File
@@ -0,0 +1,64 @@
// Markdown 节点:编辑与渲染 Markdown。marked 从 CDN 按需加载,不打进插件体积。
// styles.css 由 esbuild 以 text 方式打进 bundle,通过 plugin.css 自动注入。
import css from "./styles.css";
let markedPromise;
function loadMarked() {
if (!markedPromise) markedPromise = import("https://esm.sh/marked@14").then((mod) => mod.marked);
return markedPromise;
}
export default function markdownPlugin(runtime) {
const { React } = runtime;
const { useState, useEffect } = React;
function MarkdownContent({ ctx }) {
const [editing, setEditing] = useState(false);
const [html, setHtml] = useState("");
const value = ctx.node.metadata?.content || "";
useEffect(() => {
let alive = true;
loadMarked().then((marked) => {
if (alive) setHtml(marked.parse(value || "*双击右上角按钮编辑 Markdown*"));
});
return () => {
alive = false;
};
}, [value]);
const toggle = { position: "absolute", right: 8, top: 8, zIndex: 20, width: 32, height: 32, display: "grid", placeItems: "center", borderRadius: 8, border: `1px solid ${ctx.theme.node.stroke}`, background: `${ctx.theme.toolbar.panel}dd`, color: ctx.theme.node.text, cursor: "pointer" };
return (
<div data-canvas-no-zoom onMouseDown={(e) => e.stopPropagation()} style={{ position: "relative", height: "100%", width: "100%", display: "flex", flexDirection: "column" }}>
<button type="button" style={toggle} onClick={() => setEditing((v) => !v)} title={editing ? "预览" : "编辑"}>{editing ? "👁" : "✎"}</button>
{editing ? (
<textarea autoFocus value={value} placeholder="# 输入 Markdown" onChange={(e) => ctx.updateMetadata({ content: e.target.value })} onWheel={(e) => e.stopPropagation()} style={{ height: "100%", width: "100%", resize: "none", background: "transparent", padding: 16, fontFamily: "monospace", fontSize: 14, outline: "none", border: "none", color: ctx.theme.node.text }} />
) : (
<div className="cnv-md" onWheel={(e) => e.stopPropagation()} style={{ color: ctx.theme.node.text }} dangerouslySetInnerHTML={{ __html: html }} />
)}
</div>
);
}
return {
id: "markdown",
name: "Markdown 节点",
version: "1.0.0",
description: "在画布中编辑与渲染 Markdown",
css,
nodes: [
{
type: "markdown:doc",
title: "Markdown",
icon: "📝",
description: "编辑与渲染 Markdown",
defaultSize: { width: 360, height: 300 },
defaultMetadata: { content: "" },
minimapColor: "#6366f1",
resource: (node) => ({ kind: "text", text: node.metadata?.content }),
Content: MarkdownContent,
},
],
};
}
+59
View File
@@ -0,0 +1,59 @@
.cnv-md {
height: 100%;
width: 100%;
overflow: auto;
padding: 16px;
font-size: 14px;
line-height: 1.6;
}
.cnv-md h1,
.cnv-md h2,
.cnv-md h3 {
margin: 0.6em 0 0.3em;
font-weight: 600;
line-height: 1.3;
}
.cnv-md h1 {
font-size: 1.5em;
}
.cnv-md h2 {
font-size: 1.3em;
}
.cnv-md p {
margin: 0.5em 0;
}
.cnv-md a {
color: #6366f1;
text-decoration: underline;
}
.cnv-md code {
padding: 0.1em 0.35em;
border-radius: 4px;
background: rgba(120, 120, 120, 0.16);
font-family: monospace;
font-size: 0.9em;
}
.cnv-md pre {
padding: 12px;
border-radius: 8px;
background: rgba(120, 120, 120, 0.14);
overflow: auto;
}
.cnv-md pre code {
padding: 0;
background: transparent;
}
.cnv-md ul,
.cnv-md ol {
padding-left: 1.4em;
margin: 0.5em 0;
}
.cnv-md blockquote {
margin: 0.5em 0;
padding-left: 0.8em;
border-left: 3px solid rgba(120, 120, 120, 0.4);
opacity: 0.85;
}
.cnv-md img {
max-width: 100%;
}