feat: 实现矢量节点 MVP 功能

- 扩展 NodeProperties 接口,添加 vector 字段定义
- 创建 VectorNode.vue 组件,使用 SVG Pattern 实现自动平铺
- 创建 VectorNodeModel.ts 数据模型,处理节点初始化和 resize
- 创建 VectorPanel.vue 属性面板,支持图形类型、平铺尺寸、颜色等配置
- 在 FlowEditor.vue 中注册 vectorNode
- 在 ComponentsPanel.vue 中添加到组件库
- 在 PropertyPanel.vue 中注册属性面板

功能特性:
- 支持 5 种图形类型(矩形/椭圆/多边形/路径/自定义SVG)
- 节点缩放时矢量图自动重复平铺
- 可调整平铺尺寸(10-500px)
- 支持填充和描边颜色配置
- 实时预览,属性修改立即生效
This commit is contained in:
2026-02-17 21:50:24 +08:00
parent 3091ef063c
commit 47fc8928d8
8 changed files with 427 additions and 2 deletions

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useNodeAppearance } from '@/ts/useNodeAppearance';
const vectorConfig = ref({
kind: 'rect',
svgContent: '',
path: '',
tileWidth: 50,
tileHeight: 50,
fill: '#409EFF',
stroke: '#303133',
strokeWidth: 1
});
const nodeSize = ref({ width: 200, height: 200 });
const { containerStyle } = useNodeAppearance({
onPropsChange(props, node) {
// 同步矢量配置
if (props.vector) {
Object.assign(vectorConfig.value, props.vector);
}
// 同步节点尺寸
if (node) {
nodeSize.value.width = node.width;
nodeSize.value.height = node.height;
}
}
});
// 生成唯一的 pattern ID
const patternId = computed(() => `vector-pattern-${Math.random().toString(36).substr(2, 9)}`);
// 生成 SVG 内容
const svgContent = computed(() => {
const { kind, path, tileWidth, tileHeight, fill, stroke, strokeWidth } = vectorConfig.value;
let shapeElement = '';
switch (kind) {
case 'rect':
shapeElement = `<rect x="0" y="0" width="${tileWidth}" height="${tileHeight}"
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
break;
case 'ellipse':
shapeElement = `<ellipse cx="${tileWidth/2}" cy="${tileHeight/2}"
rx="${tileWidth/2 - strokeWidth}" ry="${tileHeight/2 - strokeWidth}"
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
break;
case 'path':
shapeElement = `<path d="${path || 'M 0 0'}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
break;
case 'svg':
shapeElement = vectorConfig.value.svgContent || '';
break;
case 'polygon':
// 默认三角形
const points = `0,${tileHeight} ${tileWidth/2},0 ${tileWidth},${tileHeight}`;
shapeElement = `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
break;
}
return `
<svg width="${nodeSize.value.width}" height="${nodeSize.value.height}"
xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="${patternId.value}"
x="0" y="0"
width="${tileWidth}"
height="${tileHeight}"
patternUnits="userSpaceOnUse">
${shapeElement}
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#${patternId.value})" />
</svg>
`;
});
</script>
<template>
<div class="vector-node" :style="containerStyle">
<div class="vector-content" v-html="svgContent"></div>
</div>
</template>
<style scoped>
.vector-node {
width: 100%;
height: 100%;
overflow: hidden;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.vector-content {
width: 100%;
height: 100%;
}
.vector-content :deep(svg) {
display: block;
width: 100%;
height: 100%;
}
</style>