From 47fc8928d8146861b73d90183a50354bfed898aa Mon Sep 17 00:00:00 2001 From: rookie4show Date: Tue, 17 Feb 2026 21:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=9F=A2=E9=87=8F?= =?UTF-8?q?=E8=8A=82=E7=82=B9=20MVP=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 NodeProperties 接口,添加 vector 字段定义 - 创建 VectorNode.vue 组件,使用 SVG Pattern 实现自动平铺 - 创建 VectorNodeModel.ts 数据模型,处理节点初始化和 resize - 创建 VectorPanel.vue 属性面板,支持图形类型、平铺尺寸、颜色等配置 - 在 FlowEditor.vue 中注册 vectorNode - 在 ComponentsPanel.vue 中添加到组件库 - 在 PropertyPanel.vue 中注册属性面板 功能特性: - 支持 5 种图形类型(矩形/椭圆/多边形/路径/自定义SVG) - 节点缩放时矢量图自动重复平铺 - 可调整平铺尺寸(10-500px) - 支持填充和描边颜色配置 - 实时预览,属性修改立即生效 --- src/components/flow/ComponentsPanel.vue | 18 ++ src/components/flow/FlowEditor.vue | 3 + src/components/flow/PropertyPanel.vue | 4 +- .../flow/nodes/common/VectorNode.vue | 106 +++++++++ .../flow/nodes/common/VectorNodeModel.ts | 44 ++++ src/components/flow/panels/VectorPanel.vue | 207 ++++++++++++++++++ src/stores/files.ts | 35 +++ src/ts/schema.ts | 12 +- 8 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 src/components/flow/nodes/common/VectorNode.vue create mode 100644 src/components/flow/nodes/common/VectorNodeModel.ts create mode 100644 src/components/flow/panels/VectorPanel.vue create mode 100644 src/stores/files.ts diff --git a/src/components/flow/ComponentsPanel.vue b/src/components/flow/ComponentsPanel.vue index dcbe388..524d394 100644 --- a/src/components/flow/ComponentsPanel.vue +++ b/src/components/flow/ComponentsPanel.vue @@ -51,6 +51,24 @@ const componentGroups = [ width: 200, height: 120 } + }, + { + id: 'vector', + name: '矢量图块', + type: 'vectorNode', + description: '可平铺的矢量图形,用于边框装饰', + data: { + vector: { + kind: 'rect', + tileWidth: 50, + tileHeight: 50, + fill: '#409EFF', + stroke: '#303133', + strokeWidth: 1 + }, + width: 200, + height: 200 + } } ] }, diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue index abda04c..4562ef4 100644 --- a/src/components/flow/FlowEditor.vue +++ b/src/components/flow/FlowEditor.vue @@ -74,6 +74,8 @@ import ImageNode from './nodes/common/ImageNode.vue'; import AssetSelectorNode from './nodes/common/AssetSelectorNode.vue'; import TextNode from './nodes/common/TextNode.vue'; import TextNodeModel from './nodes/common/TextNodeModel'; +import VectorNode from './nodes/common/VectorNode.vue'; +import VectorNodeModel from './nodes/common/VectorNodeModel'; import PropertyPanel from './PropertyPanel.vue'; import { useGlobalMessage } from '@/ts/useGlobalMessage'; import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow'; @@ -669,6 +671,7 @@ function registerNodes(lfInstance: LogicFlow) { register({ type: 'imageNode', component: ImageNode }, lfInstance); register({ type: 'assetSelector', component: AssetSelectorNode }, lfInstance); register({ type: 'textNode', component: TextNode, model: TextNodeModel }, lfInstance); + register({ type: 'vectorNode', component: VectorNode, model: VectorNodeModel }, lfInstance); } // 初始化 LogicFlow diff --git a/src/components/flow/PropertyPanel.vue b/src/components/flow/PropertyPanel.vue index 4e841ce..3c286af 100644 --- a/src/components/flow/PropertyPanel.vue +++ b/src/components/flow/PropertyPanel.vue @@ -5,6 +5,7 @@ import ImagePanel from './panels/ImagePanel.vue'; import TextPanel from './panels/TextPanel.vue'; import StylePanel from './panels/StylePanel.vue'; import AssetSelectorPanel from './panels/AssetSelectorPanel.vue'; +import VectorPanel from './panels/VectorPanel.vue'; import { ASSET_LIBRARIES } from '@/types/nodeTypes'; import { getLogicFlowInstance } from '@/ts/useLogicFlow'; @@ -33,7 +34,8 @@ const panelMap: Record = { propertySelect: PropertyRulePanel, imageNode: ImagePanel, textNode: TextPanel, - assetSelector: AssetSelectorPanel + assetSelector: AssetSelectorPanel, + vectorNode: VectorPanel }; const panelComponent = computed(() => panelMap[nodeType.value] || null); diff --git a/src/components/flow/nodes/common/VectorNode.vue b/src/components/flow/nodes/common/VectorNode.vue new file mode 100644 index 0000000..316e9b3 --- /dev/null +++ b/src/components/flow/nodes/common/VectorNode.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/components/flow/nodes/common/VectorNodeModel.ts b/src/components/flow/nodes/common/VectorNodeModel.ts new file mode 100644 index 0000000..38c1a2e --- /dev/null +++ b/src/components/flow/nodes/common/VectorNodeModel.ts @@ -0,0 +1,44 @@ +import { HtmlNodeModel } from '@logicflow/core'; + +class VectorNodeModel extends HtmlNodeModel { + initNodeData(data: any) { + super.initNodeData(data); + + // 从 properties 读取宽高 + if (data.properties?.width) { + this.width = data.properties.width; + } else { + this.width = 200; + } + + if (data.properties?.height) { + this.height = data.properties.height; + } else { + this.height = 200; + } + + // 初始化默认矢量配置 + if (!data.properties?.vector) { + this.setProperty('vector', { + kind: 'rect', + tileWidth: 50, + tileHeight: 50, + fill: '#409EFF', + stroke: '#303133', + strokeWidth: 1 + }); + } + } + + resize(deltaX: number, deltaY: number) { + const result = super.resize?.(deltaX, deltaY); + + // 持久化宽高到 properties + this.setProperty('width', this.width); + this.setProperty('height', this.height); + + return result; + } +} + +export default VectorNodeModel; diff --git a/src/components/flow/panels/VectorPanel.vue b/src/components/flow/panels/VectorPanel.vue new file mode 100644 index 0000000..c1c623c --- /dev/null +++ b/src/components/flow/panels/VectorPanel.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/src/stores/files.ts b/src/stores/files.ts new file mode 100644 index 0000000..f631de5 --- /dev/null +++ b/src/stores/files.ts @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia'; + +export const useFilesStore = defineStore('files', { + state: () => ({ + fileList: [{ label: 'File 1', name: 1, visible: false }, { label: 'File 2', name: 2, visible: false }], + activeFile: -1, + }), + getters: { + visibleFiles: (state) => state.fileList.filter(file => file.visible), + }, + actions: { + addFile(file: { label: string; name: number }) { + this.fileList.push({ ...file, visible: false }); + }, + setActiveFile(fileId: number) { + this.activeFile = fileId; + }, + setVisible(fileId: number, visibility: boolean) { + const file = this.fileList.find(file => file.name === fileId); + if (file) { + file.visible = visibility; + } + }, + closeTab(fileId: number) { + const file = this.fileList.find(file => file.name === fileId); + if (file) { + file.visible = false; + if (this.activeFile === fileId) { + const nextVisibleFile = this.visibleFiles[0]; + this.activeFile = nextVisibleFile ? nextVisibleFile.name : -1; + } + } + }, + }, +}); \ No newline at end of file diff --git a/src/ts/schema.ts b/src/ts/schema.ts index 0f9517d..abdb0e3 100644 --- a/src/ts/schema.ts +++ b/src/ts/schema.ts @@ -58,7 +58,17 @@ export interface NodeProperties { meta?: NodeMeta; image?: { url: string; fit?: 'fill'|'contain'|'cover' }; text?: { content: string; rich?: boolean }; - vector?: { kind: 'path'|'rect'|'ellipse'|'polygon'; path?: string; points?: Array<[number, number]> }; + vector?: { + kind: 'path' | 'rect' | 'ellipse' | 'polygon' | 'svg'; + svgContent?: string; + path?: string; + points?: Array<[number, number]>; + tileWidth: number; + tileHeight: number; + fill?: string; + stroke?: string; + strokeWidth?: number; + }; shikigami?: { name: string; avatar: string; rarity: string }; yuhun?: { name: string; type: string; avatar: string; shortName?: string }; property?: Record;