From 9e64df5e337e4db107b6025ba523af5431208c8d Mon Sep 17 00:00:00 2001 From: rookie4show Date: Sun, 28 Dec 2025 16:30:09 +0800 Subject: [PATCH] docs: update progress for minimap control and toolbar toggles --- docs/1management/plan.md | 4 +- docs/2design/StyleAndAppearance.md | 114 +++++++++++++++++++++++++++++ src/App.vue | 11 ++- src/components/Toolbar.vue | 66 +++++++++++++++++ src/components/flow/FlowEditor.vue | 41 +++++++++-- src/ts/useCanvasSettings.ts | 14 ++++ 6 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 docs/2design/StyleAndAppearance.md create mode 100644 src/ts/useCanvasSettings.ts diff --git a/docs/1management/plan.md b/docs/1management/plan.md index 40b6c32..b487ead 100644 --- a/docs/1management/plan.md +++ b/docs/1management/plan.md @@ -10,10 +10,10 @@ - DnD 接入:由组件库触发拖拽放置 - 右键菜单:节点置顶/置底与删除、边删除、画布添加节点(基于 LogicFlow Menu + `setElementZIndex`) - 多选/框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(SelectionSelect + snapline + 自定义对齐分布指令) + - 扩展与控制:接入 MiniMap + Control 插件;吸附/对齐线/框选开关共享到 Toolbar 与 FlowEditor;新增清空画布入口 - 未完成: - 右键菜单层级命令:已接通置顶/置底,单步前移/后移(`bringForward`/`sendBackward`)未实现 - 撤销重做、组合/锁定/隐藏、快捷键(Del/Ctrl+C/V、方向键微移、Ctrl+Z/Y) - - MiniMap/控制条/Snapshot 等扩展能力 ## 2. 左侧组件库(Palette) — 完成度:60% - 已完成: @@ -109,7 +109,7 @@ 4) 多选/对齐/吸附:框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(FlowEditor/extension)。已完成 5) 快捷键与微调:Del 删除、方向键微移、Ctrl+C/V 复制粘贴、Ctrl+G/U 组/解组(简单组:父 meta id + 同步移动)、锁定/隐藏(`properties.locked`/`visible`)。 6) 样式模型补齐:统一 `properties.style` 字段并在 PropertyPanel 全量编辑(填充/描边/圆角/阴影/透明度/文字对齐/行高/字重)。【已完成】 - 7) 扩展与控制:接入 MiniMap/Control/Snapshot;Toolbar 增加吸附/对齐开关与清空画布。 + 7) 扩展与控制:接入 MiniMap/Control/Snapshot;Toolbar 增加吸附/对齐开关与清空画布。【已完成:MiniMap + Control 接入;Toolbar/FlowEditor 共享开关 + 清空画布;Snapshot 已有】 8) 矢量节点 MVP:`vectorNode`(SVG path/rect/ellipse/polygon),属性面板支持 path/stroke/fill/strokeWidth;新增 SVG 导入弹窗。 9) 资源与导出增强:图片资源选择/上传弹窗(base64 或预留 `assetId`),导出 SVG/PDF(PDF 可延后)。 10) 历史与撤销重做:抽象 Action + HistoryService,记录增删改/移动/层级;Ctrl+Z/Y。 diff --git a/docs/2design/StyleAndAppearance.md b/docs/2design/StyleAndAppearance.md new file mode 100644 index 0000000..313a7f8 --- /dev/null +++ b/docs/2design/StyleAndAppearance.md @@ -0,0 +1,114 @@ +# 样式与节点结构说明(v1) + +## 背景 +- 目标:统一节点的样式字段与渲染消费路径,避免各节点分散管理填充/描边/阴影/文字等样式。 +- 数据载体:沿用 LogicFlow 的 `GraphData`,仅约定 `node.properties` 内的业务字段与样式字段。 + +## 样式模型:`properties.style` +位于每个节点 `properties.style`,是尺寸与外观的单一事实来源;渲染时会同步 `style.width/height` 到节点的 `width/height`。 + +```ts +interface NodeStyle { + // 尺寸与变换 + width: number; // px,必填 + height: number; // px,必填 + rotate?: number; // deg,逆时针,围绕节点中心 + + // 形状 + fill?: string; // 背景填充色 + stroke?: string; // 描边色 + strokeWidth?: number; // ≥0,px + radius?: number | [number, number, number, number]; // 圆角(rect 生效) + opacity?: number; // 0..1 + + // 阴影 + shadow?: { color?: string; blur?: number; offsetX?: number; offsetY?: number }; + + // 文本样式(text 节点或带文本的节点) + textStyle?: { + color?: string; + fontFamily?: string; + fontSize?: number; // px + fontWeight?: number | string; + lineHeight?: number; // 推荐 1..3 + align?: 'left' | 'center' | 'right'; + verticalAlign?: 'top' | 'middle' | 'bottom'; + letterSpacing?: number; // px + padding?: [number, number, number, number]; + background?: string; + }; +} +``` + +默认值参考 `src/ts/schema.ts:DefaultNodeStyle`: +- `width=180, height=120, rotate=0` +- `fill='#ffffff', stroke='#dcdfe6', strokeWidth=1, radius=4, opacity=1` +- `shadow={ color:'rgba(0,0,0,0.1)', blur:4, offsetX:0, offsetY:2 }` +- `textStyle={ color:'#303133', fontFamily:'system-ui', fontSize:14, fontWeight:400, lineHeight:1.4, align:'left', verticalAlign:'top', letterSpacing:0, padding:[8,8,8,8] }` + +## 归一化与工具(`src/ts/nodeStyle.ts`) +- `normalizeNodeStyle(style, sizeFallback)`:补全缺省,数值钳制(opacity ∈[0,1]),将散落的 width/height 收敛到 style。 +- `normalizePropertiesWithStyle(props)`:返回带规范化 style 的 props,并镜像 `width/height`。 +- `toContainerStyle` / `toTextStyle`:将 `NodeStyle` 转为 Vue 行内样式(背景、描边、圆角、阴影、透明度 / 文字)。 +- `styleEquals`:归一化后比较,避免重复 setProperties。 + +## 节点元信息:`properties.meta` +```ts +interface NodeMeta { + z?: number; // 层级 + locked?: boolean; // 锁定 + visible?: boolean; // 可见 + groupId?: string; // 分组 + name?: string; + createdAt?: number; + updatedAt?: number; +} +``` +FlowEditor 在渲染/normalize 时会补齐 `meta.visible=true`、`meta.locked=false` 并应用到节点模型(可见性、可拖拽等)。 + +## 监听与渲染路径 +1) **归一化入口**:`FlowEditor.normalizeNodeModel` 在节点创建/属性变更/渲染后执行: + - 归一化 `properties.style` 和 `meta`,必要时回写 `lf.setProperties`。 + - 同步 `style.width/height` 到节点模型,保证渲染尺寸一致。 +2) **事件监听**:节点组件通过 composable 监听 `NODE_PROPERTIES_CHANGE`,实时更新样式与自定义数据。 + +## 复用方案:`useNodeAppearance`(`src/ts/useNodeAppearance.ts`) +作用:在节点组件中统一获取样式和属性变更。 +```ts +const { containerStyle, textStyle } = useNodeAppearance({ + onPropsChange(props, node) { + // 可在这里同步业务字段,例如 image url、shikigami 数据等 + } +}); +``` +内部行为: +- 注入 `getNode`/`getGraph`,挂载时读取当前节点 props。 +- 监听 `NODE_PROPERTIES_CHANGE`,调用 `normalizeNodeStyle`,输出 `containerStyle`/`textStyle`。 +- 自动解绑监听,减少重复代码。 + +已接入的节点组件: +- 图片节点 `src/components/flow/nodes/common/ImageNode.vue` +- 式神节点 `.../yys/ShikigamiSelectNode.vue` +- 御魂节点 `.../yys/YuhunSelectNode.vue` +- 属性节点 `.../yys/PropertySelectNode.vue` + +## 编辑入口:PropertyPanel +- 样式面板 `src/components/flow/panels/StylePanel.vue` 写入 `properties.style`: + - 填充色、描边色/线宽、圆角、阴影(色/模糊/偏移)、透明度 + - 文本对齐、行高、字重 +- 图片面板 `ImagePanel.vue` 写入宽高/fit/url,并保持与 `style.width/height` 同步。 +- 其他面板(式神/御魂/属性)仅写业务字段,样式统一由 `useNodeAppearance` 消费。 + +## 持久化与 schema +- 顶层导出结构:`RootDocument`(见 `docs/2design/DataModel.md`),包含 `schemaVersion`。 +- 保存/导入路径:`useFilesStore` 写出/读入 `schemaVersion`;缺省数据走 `migrateToV1` 将散落的宽高/可见/锁定补齐到 `properties.style/meta`。 + +## 校验建议 +- width/height > 0;strokeWidth ≥ 0;radius ≥ 0 或四元组合法;opacity ∈ [0,1]。 +- 文本:fontSize > 0,lineHeight 合理(1~3),padding 四元组 ≥ 0。 +- 圆角仅对 rect 类节点生效;其他 kind 忽略 radius。 + +## 后续扩展建议 +- 文本节点接入富文本:在 `textStyle` 扩展行高倍数、字间距、背景/描边。 +- 叠加高级效果:blendMode/filter;保持向后兼容,新增字段时通过 `schemaVersion` 控制迁移。 +- 矢量节点:在 `vector` 节点中消费同一套 `style`,并在编辑面板复用 `StylePanel`。 diff --git a/src/App.vue b/src/App.vue index 0e33a80..920e3de 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,6 +21,13 @@ const toolbarHeight = 48; // 工具栏的高度 const windowHeight = ref(window.innerHeight); const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`); +const normalizeGraphData = (data: any) => { + if (data && Array.isArray((data as any).nodes) && Array.isArray((data as any).edges)) { + return data; + } + return { nodes: [], edges: [] }; +}; + const handleTabsEdit = ( targetName: string | undefined, action: 'remove' | 'add' @@ -56,7 +63,7 @@ watch( if (logicFlowInstance && currentTab?.graphRawData) { try { - logicFlowInstance.render(currentTab.graphRawData); + logicFlowInstance.render(normalizeGraphData(currentTab.graphRawData)); logicFlowInstance.zoom( currentTab.transform?.SCALE_X ?? 1, [currentTab.transform?.TRANSLATE_X ?? 0, currentTab.transform?.TRANSLATE_Y ?? 0] @@ -79,7 +86,7 @@ watch( if (logicFlowInstance && currentTab?.graphRawData) { try { - logicFlowInstance.render(currentTab.graphRawData); + logicFlowInstance.render(normalizeGraphData(currentTab.graphRawData)); logicFlowInstance.zoom( currentTab.transform?.SCALE_X ?? 1, [currentTab.transform?.TRANSLATE_X ?? 0, currentTab.transform?.TRANSLATE_Y ?? 0] diff --git a/src/components/Toolbar.vue b/src/components/Toolbar.vue index 30ea7fb..681c1ff 100644 --- a/src/components/Toolbar.vue +++ b/src/components/Toolbar.vue @@ -9,6 +9,30 @@ {{ t('updateLog') }} {{ t('feedback') }} 重置工作区 + 清空画布 + +
+ + +
@@ -84,9 +108,11 @@ import { useFilesStore } from "@/ts/useStore"; import { ElMessageBox } from "element-plus"; import { useGlobalMessage } from "@/ts/useGlobalMessage"; import { getLogicFlowInstance } from "@/ts/useLogicFlow"; +import { useCanvasSettings } from '@/ts/useCanvasSettings'; const filesStore = useFilesStore(); const { showMessage } = useGlobalMessage(); +const { selectionEnabled, snapGridEnabled, snaplineEnabled } = useCanvasSettings(); // 获取当前的 i18n 实例 const {t} = useI18n(); @@ -228,6 +254,39 @@ const handleResetWorkspace = () => { }); }; +const handleClearCanvas = () => { + ElMessageBox.confirm('仅清空当前画布,不影响其他文件,确定继续?', '提示', { + confirmButtonText: '清空', + cancelButtonText: '取消', + type: 'warning', + }).then(() => { + const lfInstance = getLogicFlowInstance(); + const activeId = filesStore.activeFileId; + const activeFile = filesStore.getTab(activeId); + + if (lfInstance) { + lfInstance.clearData(); + lfInstance.render({ nodes: [], edges: [] }); + lfInstance.zoom(1, [0, 0]); + } + + if (activeFile) { + activeFile.graphRawData = { nodes: [], edges: [] }; + activeFile.transform = { + SCALE_X: 1, + SCALE_Y: 1, + TRANSLATE_X: 0, + TRANSLATE_Y: 0 + }; + filesStore.updateTab(activeId); + } + + showMessage('success', '当前画布已清空'); + }).catch(() => { + // 用户取消 + }); +}; + const watermark = reactive({ text: localStorage.getItem('watermark.text') || '示例水印', fontSize: Number(localStorage.getItem('watermark.fontSize')) || 30, @@ -363,6 +422,13 @@ const handleClose = (done) => { z-index: 100; } +.toolbar-controls { + display: flex; + align-items: center; + gap: 12px; + margin-top: 6px; +} + .title { flex-grow: 1; text-align: center; diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue index 8e91bb9..37ae949 100644 --- a/src/components/flow/FlowEditor.vue +++ b/src/components/flow/FlowEditor.vue @@ -1,7 +1,7 @@