Pre Merge pull request !676 from O无忧O/dev1
|
|
@ -62,7 +62,7 @@
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<svg
|
||||||
|
width="200"
|
||||||
|
height="200"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#0D47A1;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="100" r="30" fill="url(#grad1)" />
|
||||||
|
<rect x="100" y="70" width="60" height="60" fill="url(#grad1)" />
|
||||||
|
<circle cx="170" cy="100" r="30" fill="url(#grad1)" />
|
||||||
|
<line x1="80" y1="100" x2="100" y2="100" stroke="#000" stroke-width="2" />
|
||||||
|
<line x1="160" y1="100" x2="170" y2="100" stroke="#000" stroke-width="2" />
|
||||||
|
<text x="50" y="100" text-anchor="middle" fill="#fff" font-size="12" dy=".3em">开始</text>
|
||||||
|
<text x="130" y="100" text-anchor="middle" fill="#fff" font-size="12" dy=".3em">过程</text>
|
||||||
|
<text x="170" y="100" text-anchor="middle" fill="#fff" font-size="12" dy=".3em">结束</text>
|
||||||
|
</svg>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
"lint:lint-staged": "lint-staged -c "
|
"lint:lint-staged": "lint-staged -c "
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.1.0",
|
|
||||||
"@form-create/designer": "^3.2.6",
|
"@form-create/designer": "^3.2.6",
|
||||||
"@form-create/element-ui": "^3.2.11",
|
"@form-create/element-ui": "^3.2.11",
|
||||||
"@iconify/iconify": "^3.1.1",
|
"@iconify/iconify": "^3.1.1",
|
||||||
|
|
@ -58,7 +57,9 @@
|
||||||
"markmap-toolbar": "^0.17.0",
|
"markmap-toolbar": "^0.17.0",
|
||||||
"markmap-view": "^0.16.0",
|
"markmap-view": "^0.16.0",
|
||||||
"min-dash": "^4.1.1",
|
"min-dash": "^4.1.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@maxgraph/core": "^0.14.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
|
|
|
||||||
12938
pnpm-lock.yaml
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
import request from '@/config/axios'
|
||||||
|
export const register = (data: any) => {
|
||||||
|
return request.post({
|
||||||
|
url: '/api/flow/register',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
|
|
||||||
export interface UserVO {
|
export interface UserVO {
|
||||||
id: number
|
id?: number
|
||||||
username: string
|
username: string
|
||||||
nickname: string
|
nickname: string
|
||||||
deptId: number
|
deptId: number
|
||||||
postIds: string[]
|
postIds: string[]
|
||||||
email: string
|
email: string
|
||||||
mobile: string
|
mobile: string
|
||||||
|
password: string
|
||||||
sex: number
|
sex: number
|
||||||
avatar: string
|
avatar: string
|
||||||
loginIp: string
|
loginIp: string
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="64" cy="64" r="64" fill="#E5E9F2"/>
|
||||||
|
<circle cx="64" cy="48" r="24" fill="#C0CCDA"/>
|
||||||
|
<path d="M64 80C42.464 80 24.572 92.436 16 110.444C24.572 121.564 43.36 128 64 128C84.64 128 103.428 121.564 112 110.444C103.428 92.436 85.536 80 64 80Z" fill="#C0CCDA"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 380 B |
|
|
@ -0,0 +1,51 @@
|
||||||
|
interface AccordionItem {
|
||||||
|
title: string;
|
||||||
|
type: 'form' | 'text';
|
||||||
|
content: FormItem[] | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormItem {
|
||||||
|
label: string;
|
||||||
|
type: 'text' | 'select';
|
||||||
|
value: string;
|
||||||
|
options?: { label: string; value: string; }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accordionData: AccordionItem[] = [
|
||||||
|
{
|
||||||
|
title: '基本设置',
|
||||||
|
type: 'form',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
label: '名称',
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '类型',
|
||||||
|
type: 'select',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: '类型1', value: 'type1' },
|
||||||
|
{ label: '类型2', value: 'type2' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '高级设置',
|
||||||
|
type: 'form',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
label: '配置项',
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '其他信息',
|
||||||
|
type: 'text',
|
||||||
|
content: '这是一些其他信息的描述文本'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "UML类图",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"type": "uml-class",
|
||||||
|
"name": "类",
|
||||||
|
"icon": "📦",
|
||||||
|
"template": {
|
||||||
|
"title": "类名",
|
||||||
|
"attributes": ["+ 属性1: string", "- 属性2: number"],
|
||||||
|
"methods": ["+ 方法1()", "# 方法2(param: string)"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "interface",
|
||||||
|
"name": "接口",
|
||||||
|
"icon": "🔌",
|
||||||
|
"template": {
|
||||||
|
"title": "接口名",
|
||||||
|
"methods": ["+ 方法1()", "+ 方法2()"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "思维导图",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"type": "topic",
|
||||||
|
"name": "主题",
|
||||||
|
"icon": "💡",
|
||||||
|
"template": {
|
||||||
|
"title": "主题"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "subtopic",
|
||||||
|
"name": "子主题",
|
||||||
|
"icon": "📝",
|
||||||
|
"template": {
|
||||||
|
"title": "子主题"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "网络拓扑",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"type": "server",
|
||||||
|
"name": "服务器",
|
||||||
|
"icon": "🖥️",
|
||||||
|
"template": {
|
||||||
|
"title": "Server",
|
||||||
|
"ip": "192.168.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "router",
|
||||||
|
"name": "路由器",
|
||||||
|
"icon": "📡",
|
||||||
|
"template": {
|
||||||
|
"title": "Router",
|
||||||
|
"ip": "192.168.1.254"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "业务流程图",
|
||||||
|
"preview": "business-process",
|
||||||
|
"modifiedDate": "2024-01-20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "思维导图",
|
||||||
|
"preview": "mind-map",
|
||||||
|
"modifiedDate": "2024-01-18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "UML类图",
|
||||||
|
"preview": "uml",
|
||||||
|
"modifiedDate": "2024-01-15"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "项目管理流程",
|
||||||
|
"description": "适用于项目规划和任务管理",
|
||||||
|
"preview": "business-process"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "组织架构图",
|
||||||
|
"description": "快速创建公司组织结构图",
|
||||||
|
"preview": "org-chart"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"communityItems": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "电商系统架构图",
|
||||||
|
"preview": "business-process",
|
||||||
|
"author": "张三",
|
||||||
|
"authorAvatar": "default-avatar",
|
||||||
|
"likes": 128,
|
||||||
|
"comments": 32
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deletedFiles": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "旧版流程图",
|
||||||
|
"preview": "business-process",
|
||||||
|
"deleteTime": "2024-03-10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<svg width="400" height="225" viewBox="0 0 400 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="400" height="225" fill="#F5F7FA"/>
|
||||||
|
<rect x="50" y="92.5" width="100" height="40" rx="4" fill="#409EFF"/>
|
||||||
|
<text x="100" y="117.5" text-anchor="middle" fill="white" font-size="14">开始</text>
|
||||||
|
<path d="M150 112.5H200" stroke="#409EFF" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="200" y="92.5" width="100" height="40" rx="4" fill="#409EFF"/>
|
||||||
|
<text x="250" y="117.5" text-anchor="middle" fill="white" font-size="14">处理</text>
|
||||||
|
<path d="M300 112.5H350" stroke="#409EFF" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
||||||
|
markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#409EFF"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 859 B |
|
|
@ -0,0 +1,81 @@
|
||||||
|
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- 渐变背景 -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#2c5edf;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#5b9fff;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<!-- 流程图连线动画 -->
|
||||||
|
<path id="flowPath1" d="M100,200 C200,200 300,300 400,300" />
|
||||||
|
<path id="flowPath2" d="M400,300 C500,300 600,200 700,200" />
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<rect width="100%" height="100%" fill="url(#skyGradient)" />
|
||||||
|
|
||||||
|
<!-- 装饰性圆点 -->
|
||||||
|
<g class="dots" fill="#ffffff" fill-opacity="0.1">
|
||||||
|
<circle cx="100" cy="100" r="2">
|
||||||
|
<animate attributeName="r" values="2;4;2" dur="3s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="300" cy="200" r="3">
|
||||||
|
<animate attributeName="r" values="3;5;3" dur="4s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="500" cy="150" r="2">
|
||||||
|
<animate attributeName="r" values="2;4;2" dur="3.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="700" cy="300" r="3">
|
||||||
|
<animate attributeName="r" values="3;5;3" dur="4.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 流程图节点和连线 -->
|
||||||
|
<g class="flow-elements">
|
||||||
|
<!-- 节点 -->
|
||||||
|
<rect x="80" y="180" width="40" height="40" rx="5" fill="#ffffff" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.4;0.2" dur="3s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="380" y="280" width="40" height="40" rx="5" fill="#ffffff" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.4;0.2" dur="3s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="680" y="180" width="40" height="40" rx="5" fill="#ffffff" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.4;0.2" dur="3s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
|
||||||
|
<!-- 动态连线 -->
|
||||||
|
<g stroke="#ffffff" stroke-width="2" stroke-opacity="0.3" fill="none">
|
||||||
|
<path d="">
|
||||||
|
<animate
|
||||||
|
attributeName="d"
|
||||||
|
values="M120,200 C200,200 300,300 380,300;M120,200 C200,200 300,290 380,300;M120,200 C200,200 300,300 380,300"
|
||||||
|
dur="4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</path>
|
||||||
|
<path d="">
|
||||||
|
<animate
|
||||||
|
attributeName="d"
|
||||||
|
values="M420,300 C500,300 600,200 680,200;M420,300 C500,310 600,200 680,200;M420,300 C500,300 600,200 680,200"
|
||||||
|
dur="4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 流动的点 -->
|
||||||
|
<circle r="3" fill="#ffffff">
|
||||||
|
<animateMotion
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
path="M120,200 C200,200 300,300 380,300"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
<circle r="3" fill="#ffffff">
|
||||||
|
<animateMotion
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
path="M420,300 C500,300 600,200 680,200"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -0,0 +1,17 @@
|
||||||
|
<svg width="400" height="225" viewBox="0 0 400 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="400" height="225" fill="#F5F7FA"/>
|
||||||
|
<circle cx="200" cy="112.5" r="40" fill="#409EFF"/>
|
||||||
|
<text x="200" y="117.5" text-anchor="middle" fill="white" font-size="14">核心主题</text>
|
||||||
|
<circle cx="100" cy="72.5" r="30" fill="#67C23A"/>
|
||||||
|
<text x="100" y="77.5" text-anchor="middle" fill="white" font-size="12">分支1</text>
|
||||||
|
<circle cx="100" cy="152.5" r="30" fill="#67C23A"/>
|
||||||
|
<text x="100" y="157.5" text-anchor="middle" fill="white" font-size="12">分支2</text>
|
||||||
|
<circle cx="300" cy="72.5" r="30" fill="#67C23A"/>
|
||||||
|
<text x="300" y="77.5" text-anchor="middle" fill="white" font-size="12">分支3</text>
|
||||||
|
<circle cx="300" cy="152.5" r="30" fill="#67C23A"/>
|
||||||
|
<text x="300" y="157.5" text-anchor="middle" fill="white" font-size="12">分支4</text>
|
||||||
|
<path d="M160 112.5L130 72.5" stroke="#67C23A" stroke-width="2"/>
|
||||||
|
<path d="M160 112.5L130 152.5" stroke="#67C23A" stroke-width="2"/>
|
||||||
|
<path d="M240 112.5L270 72.5" stroke="#67C23A" stroke-width="2"/>
|
||||||
|
<path d="M240 112.5L270 152.5" stroke="#67C23A" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg width="400" height="225" viewBox="0 0 400 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="400" height="225" fill="#F5F7FA"/>
|
||||||
|
<rect x="150" y="22.5" width="100" height="40" rx="4" fill="#409EFF"/>
|
||||||
|
<text x="200" y="47.5" text-anchor="middle" fill="white" font-size="14">CEO</text>
|
||||||
|
<rect x="50" y="92.5" width="100" height="40" rx="4" fill="#67C23A"/>
|
||||||
|
<text x="100" y="117.5" text-anchor="middle" fill="white" font-size="14">技术部</text>
|
||||||
|
<rect x="250" y="92.5" width="100" height="40" rx="4" fill="#67C23A"/>
|
||||||
|
<text x="300" y="117.5" text-anchor="middle" fill="white" font-size="14">市场部</text>
|
||||||
|
<path d="M200 62.5V82.5" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<path d="M100 82.5V92.5" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<path d="M300 82.5V92.5" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<path d="M100 82.5H300" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 895 B |
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="400" height="225" viewBox="0 0 400 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="400" height="225" fill="#F5F7FA"/>
|
||||||
|
<rect x="100" y="42.5" width="200" height="140" rx="4" fill="white" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<line x1="100" y1="82.5" x2="300" y2="82.5" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<line x1="100" y1="142.5" x2="300" y2="142.5" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<text x="200" y="67.5" text-anchor="middle" fill="#409EFF" font-size="14" font-weight="bold">Class</text>
|
||||||
|
<text x="110" y="107.5" fill="#666666" font-size="12">- attribute1: Type</text>
|
||||||
|
<text x="110" y="127.5" fill="#666666" font-size="12">- attribute2: Type</text>
|
||||||
|
<text x="110" y="167.5" fill="#666666" font-size="12">+ method(): ReturnType</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 787 B |
|
|
@ -0,0 +1,14 @@
|
||||||
|
<svg width="1440" height="800" viewBox="0 0 1440 800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0H1440V800H0V0Z" fill="url(#paint0_linear)"/>
|
||||||
|
<g opacity="0.1">
|
||||||
|
<circle cx="720" cy="400" r="300" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<circle cx="720" cy="400" r="200" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
<circle cx="720" cy="400" r="100" stroke="#409EFF" stroke-width="2"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="0" y1="0" x2="1440" y2="800" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#ECF5FF"/>
|
||||||
|
<stop offset="1" stop-color="#E4E7ED"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 644 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 2L28 15L15 28L2 15L15 2Z" stroke="#409EFF" stroke-width="0.5"/>
|
||||||
|
<circle cx="15" cy="15" r="2" fill="#409EFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 231 B |
|
|
@ -0,0 +1,43 @@
|
||||||
|
<svg width="800" height="600" viewBox="0 0 800 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- 背景装饰圆圈 -->
|
||||||
|
<circle cx="400" cy="300" r="250" stroke="white" stroke-width="2" stroke-opacity="0.1"/>
|
||||||
|
<circle cx="400" cy="300" r="200" stroke="white" stroke-width="2" stroke-opacity="0.1"/>
|
||||||
|
<circle cx="400" cy="300" r="150" stroke="white" stroke-width="2" stroke-opacity="0.1"/>
|
||||||
|
|
||||||
|
<!-- 流程图示意 -->
|
||||||
|
<g opacity="0.2">
|
||||||
|
<!-- 开始节点 -->
|
||||||
|
<circle cx="250" cy="200" r="30" fill="white"/>
|
||||||
|
|
||||||
|
<!-- 处理节点1 -->
|
||||||
|
<rect x="350" y="170" width="100" height="60" rx="4" fill="white"/>
|
||||||
|
|
||||||
|
<!-- 判断节点 -->
|
||||||
|
<path d="M500 200 L550 230 L500 260 L450 230 Z" fill="white"/>
|
||||||
|
|
||||||
|
<!-- 处理节点2 -->
|
||||||
|
<rect x="350" y="300" width="100" height="60" rx="4" fill="white"/>
|
||||||
|
|
||||||
|
<!-- 结束节点 -->
|
||||||
|
<circle cx="550" cy="330" r="30" fill="white"/>
|
||||||
|
|
||||||
|
<!-- 连接线 -->
|
||||||
|
<path d="M280 200 H350" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M450 200 H450" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M550 230 V330" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M500 230 H400 V300" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M450 330 H520" stroke="white" stroke-width="2"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 装饰点 -->
|
||||||
|
<circle cx="200" cy="150" r="4" fill="white" fill-opacity="0.3"/>
|
||||||
|
<circle cx="600" cy="450" r="4" fill="white" fill-opacity="0.3"/>
|
||||||
|
<circle cx="150" cy="400" r="4" fill="white" fill-opacity="0.3"/>
|
||||||
|
<circle cx="650" cy="200" r="4" fill="white" fill-opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- 装饰线 -->
|
||||||
|
<line x1="100" y1="100" x2="150" y2="150" stroke="white" stroke-opacity="0.1"/>
|
||||||
|
<line x1="700" y1="500" x2="650" y2="450" stroke="white" stroke-opacity="0.1"/>
|
||||||
|
<line x1="100" y1="500" x2="150" y2="450" stroke="white" stroke-opacity="0.1"/>
|
||||||
|
<line x1="700" y1="100" x2="650" y2="150" stroke="white" stroke-opacity="0.1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="4" y="4" width="24" height="24" rx="4" fill="#409EFF"/>
|
||||||
|
<path d="M10 16H22M16 10V22" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* 通用的文件列表横向布局样式 */
|
||||||
|
.file-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-card {
|
||||||
|
width: 280px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
|
|
@ -0,0 +1,70 @@
|
||||||
|
export const ToolbarIcons = {
|
||||||
|
grid: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M3 3h18v18H3z"/>
|
||||||
|
<path d="M3 9h18"/>
|
||||||
|
<path d="M3 15h18"/>
|
||||||
|
<path d="M9 3v18"/>
|
||||||
|
<path d="M15 3v18"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
export: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="7 10 12 15 17 10"/>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
undo: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M3 7v6h6"/>
|
||||||
|
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
redo: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M21 7v6h-6"/>
|
||||||
|
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
delete: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M3 6h18"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
|
||||||
|
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
fit: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M15 3h6v6"/>
|
||||||
|
<path d="M9 21H3v-6"/>
|
||||||
|
<path d="M21 3l-7 7"/>
|
||||||
|
<path d="M3 21l7-7"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
zoomOut: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
zoomIn: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
|
<line x1="11" y1="8" x2="11" y2="14"/>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
back: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M19 12H5"/>
|
||||||
|
<polyline points="12 19 5 12 12 5"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
curvedLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<path d="M3 12c4-6 14-6 18 0"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
orthogonalLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<polyline points="3 7 12 7 12 17 21 17"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
straightLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none">
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
export const ToolbarIcons = {
|
||||||
|
zoomIn: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
<line x1="11" y1="8" x2="11" y2="14"></line>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
zoomOut: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
fit: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M15 3h6v6M9 3H3v6M15 21h6v-6M9 21H3v-6"></path>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
delete: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
undo: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M3 7v6h6"></path>
|
||||||
|
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"></path>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
redo: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M21 7v6h-6"></path>
|
||||||
|
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"></path>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
straightLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
orthogonalLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<polyline points="5,12 12,12 12,19"></polyline>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
curvedLine: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M5 12c8-10 8 10 16 0"></path>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
autoLayout: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
export: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
back: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<div class="accordion">
|
||||||
|
<div v-for="(item, index) in items" :key="index" class="accordion-item">
|
||||||
|
<div class="accordion-header" @click="toggleItem(index)">
|
||||||
|
<span class="accordion-title">{{ item.title }}</span>
|
||||||
|
<span class="accordion-icon">{{ activeIndex === index ? '▼' : '▶' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-content" :class="{ 'active': activeIndex === index }">
|
||||||
|
<template v-if="item.type === 'form'">
|
||||||
|
<div v-for="(field, fieldIndex) in item.content" :key="fieldIndex" class="form-item">
|
||||||
|
<label>{{ field.label }}</label>
|
||||||
|
<input v-if="field.type === 'text'" type="text" v-model="field.value">
|
||||||
|
<select v-else-if="field.type === 'select'" v-model="field.value">
|
||||||
|
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ item.content }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
items: Array<{
|
||||||
|
title: string;
|
||||||
|
type?: 'text' | 'form';
|
||||||
|
content: any;
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeIndex = ref(0)
|
||||||
|
|
||||||
|
const toggleItem = (index: number) => {
|
||||||
|
activeIndex.value = activeIndex.value === index ? -1 : index
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.accordion {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content.active {
|
||||||
|
max-height: 500px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
min-width: 60px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input,
|
||||||
|
.form-item select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input:focus,
|
||||||
|
.form-item select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
<template>
|
||||||
|
<div class="flow-editor">
|
||||||
|
<!-- 顶部区域:菜单和工具栏 -->
|
||||||
|
<FlowHeader />
|
||||||
|
|
||||||
|
<!-- 主内容区域:左中右布局 -->
|
||||||
|
<div class="flow-content">
|
||||||
|
<!-- 左侧:节点库和风格库 -->
|
||||||
|
<div class="left-panel" :style="{ width: isLeftPanelVisible ? leftPanelWidth + 'px' : '0' }">
|
||||||
|
<div class="panel-content" v-show="isLeftPanelVisible">
|
||||||
|
<FlowAccordion />
|
||||||
|
</div>
|
||||||
|
<div class="resize-handle left" @mousedown="startResize('left', $event)">
|
||||||
|
<div class="panel-toggle" @click.stop="toggleLeftPanel">
|
||||||
|
<el-icon :class="{ 'is-collapsed': !isLeftPanelVisible }">
|
||||||
|
<CaretLeft />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间:画布 -->
|
||||||
|
<div class="center-panel">
|
||||||
|
<FlowCanvas />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:属性面板 -->
|
||||||
|
<div class="right-panel" :style="{ width: isRightPanelVisible ? rightPanelWidth + 'px' : '0' }">
|
||||||
|
<div class="resize-handle right" @mousedown="startResize('right', $event)">
|
||||||
|
<div class="panel-toggle" @click.stop="toggleRightPanel">
|
||||||
|
<el-icon :class="{ 'is-collapsed': !isRightPanelVisible }">
|
||||||
|
<CaretRight />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content" v-show="isRightPanelVisible">
|
||||||
|
<PropertyPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import FlowAccordion from './panel/FlowAccordion.vue'
|
||||||
|
import FlowCanvas from './canvas/FlowCanvas.vue'
|
||||||
|
import PropertyPanel from './property/PropertyPanel.vue'
|
||||||
|
import { CaretLeft, CaretRight } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FlowEditor'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 面板宽度状态
|
||||||
|
const leftPanelWidth = ref(280)
|
||||||
|
const rightPanelWidth = ref(300)
|
||||||
|
const minWidth = 200
|
||||||
|
const maxWidth = 500
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
const isResizing = ref(false)
|
||||||
|
const currentPanel = ref<'left' | 'right' | null>(null)
|
||||||
|
const startX = ref(0)
|
||||||
|
const startWidth = ref(0)
|
||||||
|
|
||||||
|
// 面板显示状态
|
||||||
|
const isLeftPanelVisible = ref(true)
|
||||||
|
const isRightPanelVisible = ref(true)
|
||||||
|
|
||||||
|
// 切换面板显示/隐藏
|
||||||
|
const toggleLeftPanel = () => {
|
||||||
|
isLeftPanelVisible.value = !isLeftPanelVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRightPanel = () => {
|
||||||
|
isRightPanelVisible.value = !isRightPanelVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始拖拽
|
||||||
|
const startResize = (panel: 'left' | 'right', event: MouseEvent) => {
|
||||||
|
isResizing.value = true
|
||||||
|
currentPanel.value = panel
|
||||||
|
startX.value = event.clientX
|
||||||
|
startWidth.value = panel === 'left' ? leftPanelWidth.value : rightPanelWidth.value
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResize)
|
||||||
|
document.addEventListener('mouseup', stopResize)
|
||||||
|
document.body.style.cursor = 'col-resize'
|
||||||
|
document.body.style.userSelect = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽过程
|
||||||
|
const handleResize = (event: MouseEvent) => {
|
||||||
|
if (!isResizing.value) return
|
||||||
|
|
||||||
|
const diff = event.clientX - startX.value
|
||||||
|
const newWidth = currentPanel.value === 'left'
|
||||||
|
? startWidth.value + diff
|
||||||
|
: startWidth.value - diff
|
||||||
|
|
||||||
|
// 限制最小和最大宽度
|
||||||
|
const clampedWidth = Math.min(Math.max(newWidth, minWidth), maxWidth)
|
||||||
|
|
||||||
|
if (currentPanel.value === 'left') {
|
||||||
|
leftPanelWidth.value = clampedWidth
|
||||||
|
} else {
|
||||||
|
rightPanelWidth.value = clampedWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束拖拽
|
||||||
|
const stopResize = () => {
|
||||||
|
isResizing.value = false
|
||||||
|
currentPanel.value = null
|
||||||
|
document.removeEventListener('mousemove', handleResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
document.body.style.userSelect = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', handleResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-editor {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border-left: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-panel {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
background: #f5f7fa;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
z-index: 9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.left {
|
||||||
|
right: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle.right {
|
||||||
|
left: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽时的遮罩,防止选中其他内容 */
|
||||||
|
.flow-editor.resizing::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle {
|
||||||
|
width: 16px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle.left {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-toggle.right {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-collapsed {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改过渡动画 */
|
||||||
|
.left-panel,
|
||||||
|
.right-panel {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle[draggable="true"] {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当面板打开时才显示拖拽光标 */
|
||||||
|
.left-panel:not([style*="width: 0"]) .resize-handle,
|
||||||
|
.right-panel:not([style*="width: 0"]) .resize-handle {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="flow-header">
|
||||||
|
<FlowMenu />
|
||||||
|
<FlowToolbar />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FlowMenu from './menu/FlowMenu.vue'
|
||||||
|
import FlowToolbar from './menu/FlowToolbar.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FlowHeader'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
flex-shrink: 0; /* 防止头部被压缩 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* SplitPane.css */
|
||||||
|
.split-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... 其他样式代码 ... */
|
||||||
|
|
||||||
|
.right-panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,399 @@
|
||||||
|
<template>
|
||||||
|
<div class="split-container">
|
||||||
|
<div class="toolbar">
|
||||||
|
<slot name="toolbar">默认工具栏</slot>
|
||||||
|
<div class="right-panel-trigger" v-if="!state.rightPanelVisible" @click="showRightPanel">
|
||||||
|
<span class="trigger-icon">⚙</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="left-panel" :style="{ width: state.leftPanelVisible ? state.leftPanelWidth + 'px' : '0' }">
|
||||||
|
<slot name="left">左侧面板</slot>
|
||||||
|
</div>
|
||||||
|
<div class="divider left-divider" @mousedown="leftDragHandler.startDrag">
|
||||||
|
<div class="toggle-button" @click.stop="toggleLeftPanel">
|
||||||
|
<span class="arrow-icon">
|
||||||
|
{{ state.leftPanelVisible ? '◀' : '▶' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center-panel">
|
||||||
|
<slot name="center">中间面板</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="!state.rightPanelPopup"
|
||||||
|
class="divider right-divider"
|
||||||
|
@mousedown="rightDragHandler.startDrag">
|
||||||
|
<div class="toggle-button" @click.stop="toggleRightPanel">
|
||||||
|
<span class="arrow-icon">
|
||||||
|
{{ state.rightPanelVisible ? '▶' : '◀' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="state.rightPanelVisible"
|
||||||
|
class="right-panel-container"
|
||||||
|
:class="{
|
||||||
|
'popup-mode': state.rightPanelPopup,
|
||||||
|
'docked-mode': !state.rightPanelPopup
|
||||||
|
}"
|
||||||
|
:style="rightPanelStyle">
|
||||||
|
<div class="right-panel-header"
|
||||||
|
@mousedown="handleDragStart"
|
||||||
|
:class="{ 'draggable': state.rightPanelPopup }">
|
||||||
|
<span class="panel-title">
|
||||||
|
<slot name="right-title">设置</slot>
|
||||||
|
</span>
|
||||||
|
<div class="header-actions">
|
||||||
|
<span class="mode-toggle" @click="toggleRightPanelMode">
|
||||||
|
{{ state.rightPanelDocked ? '⇱' : '⇲' }}
|
||||||
|
</span>
|
||||||
|
<span class="close-button" @click="hideRightPanel">✕</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-panel-content">
|
||||||
|
<slot name="right">右侧面板</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import { useDragHandler } from '../utils/dragHandler'
|
||||||
|
import './SplitPane.css' // 引入 CSS 文件
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
leftPanelWidth: 300,
|
||||||
|
rightPanelWidth: 400,
|
||||||
|
leftPanelVisible: true,
|
||||||
|
rightPanelVisible: false, // 默认隐藏
|
||||||
|
rightPanelPopup: false,
|
||||||
|
rightPanelDocked: true, // 新增:控制右侧面板是否吸附
|
||||||
|
// 添加右侧面板位置信息
|
||||||
|
rightPanelPosition: {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加拖动相关的状态
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragStartPosition = reactive({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
panelX: 0,
|
||||||
|
panelY: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加拖动处理函数
|
||||||
|
const handleDragStart = (e: MouseEvent) => {
|
||||||
|
if (!state.rightPanelPopup) return
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
dragStartPosition.x = e.clientX
|
||||||
|
dragStartPosition.y = e.clientY
|
||||||
|
dragStartPosition.panelX = state.rightPanelPosition.x
|
||||||
|
dragStartPosition.panelY = state.rightPanelPosition.y
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrag = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const deltaX = e.clientX - dragStartPosition.x
|
||||||
|
const deltaY = e.clientY - dragStartPosition.y
|
||||||
|
|
||||||
|
state.rightPanelPosition.x = dragStartPosition.panelX + deltaX
|
||||||
|
state.rightPanelPosition.y = dragStartPosition.panelY + deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件卸载时移除全局事件监听
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('mousemove', handleDrag)
|
||||||
|
window.addEventListener('mouseup', handleDragEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('mousemove', handleDrag)
|
||||||
|
window.removeEventListener('mouseup', handleDragEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleLeftPanel = () => {
|
||||||
|
state.leftPanelVisible = !state.leftPanelVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRightPanel = () => {
|
||||||
|
state.rightPanelVisible = !state.rightPanelVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRightPanelMode = () => {
|
||||||
|
state.rightPanelDocked = !state.rightPanelDocked
|
||||||
|
state.rightPanelPopup = !state.rightPanelDocked
|
||||||
|
if (!state.rightPanelDocked) {
|
||||||
|
// 切换到弹出模式时重置位置
|
||||||
|
state.rightPanelPosition.x = 0
|
||||||
|
state.rightPanelPosition.y = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftDragHandler = useDragHandler(state, 'left')
|
||||||
|
const rightDragHandler = useDragHandler(state, 'right')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示右侧面板。
|
||||||
|
* 此函数用于设置状态,使右侧面板弹出显示并且可见。
|
||||||
|
*/
|
||||||
|
const showRightPanel = () => {
|
||||||
|
state.rightPanelVisible = true
|
||||||
|
if (!state.rightPanelDocked) {
|
||||||
|
state.rightPanelPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏右侧面板的函数。
|
||||||
|
* 通过设置状态中的 rightPanelPopup 和 rightPanelVisible 为 false 来隐藏右侧面板。
|
||||||
|
*/
|
||||||
|
const hideRightPanel = () => {
|
||||||
|
if (state.rightPanelPopup) {
|
||||||
|
state.rightPanelPopup = false
|
||||||
|
state.rightPanelVisible = false
|
||||||
|
} else {
|
||||||
|
state.rightPanelVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加计算属性用于设置右侧面板样式
|
||||||
|
const rightPanelStyle = computed(() => {
|
||||||
|
if (!state.rightPanelPopup) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `translate(${state.rightPanelPosition.x}px, ${state.rightPanelPosition.y}px)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.split-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel, .right-panel {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.3s;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-panel {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #ffffff;
|
||||||
|
min-width: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 5px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider:hover {
|
||||||
|
background-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-divider .toggle-button {
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-divider .toggle-button {
|
||||||
|
border-radius: 25px 0 0 25px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:hover {
|
||||||
|
background-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
color: #606266;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-container.docked-mode {
|
||||||
|
width: v-bind('state.rightPanelWidth + "px"');
|
||||||
|
border-left: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-container.popup-mode {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
top: 60px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle:hover {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-divider {
|
||||||
|
width: 5px;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
cursor: ew-resize;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-divider .toggle-button {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
border-radius: 25px 0 0 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
left: -20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-trigger {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-trigger:hover {
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-header {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #909399;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<template>
|
||||||
|
<div class="flow-canvas-wrapper">
|
||||||
|
<div class="flow-canvas" ref="canvasRef" @dragover="handleDragOver" @drop="handleDrop"
|
||||||
|
:class="{ 'show-grid': showGrid }">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="graphOutlineRef" v-if="showGraphOutline" class="graph-outline" @mousedown="onMouseDown">
|
||||||
|
<div class="title-bar">
|
||||||
|
<span>缩略图</span>
|
||||||
|
<button @click="closeGraphOutline" class="close-button">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextEditToolbar :visible="showTextToolbar" :position="toolbarPosition" :currentStyle="currentNodeStyle" />
|
||||||
|
|
||||||
|
<TableEditor
|
||||||
|
:visible="showTableEditor"
|
||||||
|
@update:visible="showTableEditor = $event"
|
||||||
|
:initial-data="currentTableConfig"
|
||||||
|
@confirm="handleTableUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExportPreview
|
||||||
|
v-model:visible="showExportPreview"
|
||||||
|
:image-url="exportImageUrl"
|
||||||
|
@confirm="handleExportConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
|
import { Graph } from "@maxgraph/core";
|
||||||
|
import { flowEventBus } from "../../events";
|
||||||
|
import { WuFlowAction } from "../../utils/flow-utils";
|
||||||
|
import { CanvasEventHandler } from "../../handlers/CanvasEventHandler";
|
||||||
|
import TextEditToolbar from '../menu/TextEditToolbar.vue';
|
||||||
|
import TableEditor from '../property/TableEditor.vue';
|
||||||
|
import ExportPreview from '../property/ExportPreview.vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
interface FlowNode {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showGraphOutline = ref(true);
|
||||||
|
let isDragging = false;
|
||||||
|
let offsetX = ref(0);
|
||||||
|
let offsetY = ref(0);
|
||||||
|
let wuFlowAction: WuFlowAction | null = null;
|
||||||
|
const canvasRef = ref<HTMLElement>();
|
||||||
|
const graphOutlineRef = ref<HTMLElement>();
|
||||||
|
let graph: Graph | null = null;
|
||||||
|
|
||||||
|
// 添加网格显示状态
|
||||||
|
const showGrid = ref(true);
|
||||||
|
|
||||||
|
// 在 script setup 顶部其他 ref 变量附近添加
|
||||||
|
const showTextToolbar = ref(false);
|
||||||
|
const toolbarPosition = ref<{ left: string; top: string }>({ top: '0px', left: '0px' });
|
||||||
|
const currentNodeStyle = ref({
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fontSize: 14,
|
||||||
|
fontColor: '#000000',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
textDecoration: 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMouseMove = (event: MouseEvent) => {
|
||||||
|
if (isDragging && graphOutlineRef.value) {
|
||||||
|
graphOutlineRef.value.style.left = `${event.clientX - offsetX.value}px`;
|
||||||
|
graphOutlineRef.value.style.top = `${event.clientY - offsetY.value}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (event: MouseEvent) => {
|
||||||
|
if (graphOutlineRef.value) {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX.value = event.clientX - graphOutlineRef.value.offsetLeft;
|
||||||
|
offsetY.value = event.clientY - graphOutlineRef.value.offsetTop;
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging = false;
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeGraphOutline = () => {
|
||||||
|
showGraphOutline.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let eventHandler: CanvasEventHandler;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initFlowCanvas();
|
||||||
|
eventHandler = new CanvasEventHandler(wuFlowAction, graph, canvasRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
eventHandler?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const initFlowCanvas = () => {
|
||||||
|
wuFlowAction = new WuFlowAction(canvasRef.value, graphOutlineRef.value);
|
||||||
|
graph = wuFlowAction.getGraph();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
wuFlowAction?.insertVertex(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTableEditor = ref(false);
|
||||||
|
const currentTableConfig = ref(null);
|
||||||
|
const currentTableCell = ref(null);
|
||||||
|
|
||||||
|
// 监听表格编辑事件
|
||||||
|
flowEventBus.on('edit-table', ({ cell, config }) => {
|
||||||
|
currentTableCell.value = cell;
|
||||||
|
currentTableConfig.value = config;
|
||||||
|
showTableEditor.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理表格更新
|
||||||
|
const handleTableUpdate = ({ html, config }) => {
|
||||||
|
if (currentTableCell.value) {
|
||||||
|
flowEventBus.emit('table-updated', {
|
||||||
|
cell: currentTableCell.value,
|
||||||
|
html,
|
||||||
|
config
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showExportPreview = ref(false);
|
||||||
|
const exportImageUrl = ref('');
|
||||||
|
|
||||||
|
const handleExportConfirm = (imageUrl: string) => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = imageUrl;
|
||||||
|
link.download = 'flow.png';
|
||||||
|
link.click();
|
||||||
|
ElMessage({
|
||||||
|
message: '图片已开始下载',
|
||||||
|
type: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FlowCanvas'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '../../styles/flow-canvas.css';
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
export const FlowIcons = {
|
||||||
|
// 基础流程图标
|
||||||
|
Start: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
End: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
<circle cx="12" cy="12" r="5" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Process: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Decision: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 4L20 12L12 20L4 12L12 4Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Arrow: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12H20M20 12L14 6M20 12L14 18" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// UML节点图标
|
||||||
|
Class: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="3" y="3" width="18" height="18" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="15" x2="21" y2="15" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Interface: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="3" y="3" width="18" height="18" stroke-width="2" stroke-dasharray="4 2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Package: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M3 3H10V7H21V21H3V3Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Component: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M20 12L16 8L16 16L20 12Z" stroke-width="2"/>
|
||||||
|
<rect x="4" y="6" width="12" height="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Object: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Enumeration: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="3" y="3" width="18" height="18" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
<text x="7" y="15" font-size="8" fill="currentColor">enum</text>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Association: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Inheritance: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="2"/>
|
||||||
|
<path d="M14 6L20 12L14 18" fill="none" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Implementation: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="2" stroke-dasharray="4 2"/>
|
||||||
|
<path d="M14 6L20 12L14 18" fill="none" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Dependency: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="2" stroke-dasharray="4 2"/>
|
||||||
|
<path d="M14 6L20 12L14 18" fill="none" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 高级组件图标
|
||||||
|
SubProcess: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="8" x2="12" y2="16" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Parallel: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="9" x2="16" y2="9" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="15" x2="16" y2="15" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Timer: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
<path d="M12 8v4l3 3" stroke-width="2"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Menu Components
|
||||||
|
export { default as FlowToolbar } from './menu/FlowToolbar.vue';
|
||||||
|
export { default as TextEditToolbar } from './menu/TextEditToolbar.vue';
|
||||||
|
|
||||||
|
// Node Components
|
||||||
|
export { default as ShapeLibrary } from './node/ShapeLibrary.vue';
|
||||||
|
export * from './node/flowNodes';
|
||||||
|
|
||||||
|
// Property Components
|
||||||
|
export { default as StyleLibrary } from './property/StyleLibrary.vue';
|
||||||
|
export { default as TableEditor } from './property/TableEditor.vue';
|
||||||
|
export { default as ExportPreview } from './property/ExportPreview.vue';
|
||||||
|
|
||||||
|
// Canvas Components
|
||||||
|
export { default as FlowCanvas } from './canvas/FlowCanvas.vue';
|
||||||
|
|
||||||
|
// Panel Components
|
||||||
|
export { default as FlowAccordion } from './panel/FlowAccordion.vue';
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
<template>
|
||||||
|
<div class="flow-menu">
|
||||||
|
<el-menu mode="horizontal" :ellipsis="false">
|
||||||
|
<el-menu-item v-for="menu in menuConfig" :key="menu.key" :index="menu.key" @mouseenter="(e) => showSubmenu(menu.key, e)">
|
||||||
|
{{ menu.label }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 子菜单浮层 -->
|
||||||
|
<div v-if="activeSubmenu" class="submenu-popup" :style="submenuStyle" @mouseleave="hideSubmenu">
|
||||||
|
<template v-for="item in activeSubmenuItems" :key="item.key">
|
||||||
|
<el-divider v-if="item.type === 'divider'" />
|
||||||
|
<div v-else class="submenu-item" @click="handleMenuClick(item)">
|
||||||
|
<el-icon v-if="item.icon">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="menu-label">{{ item.label }}</span>
|
||||||
|
<span v-if="item.shortcut" class="shortcut">{{ item.shortcut }}</span>
|
||||||
|
<el-checkbox v-if="item.type === 'checkbox'" v-model="item.checked" @click.stop />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { menuConfig } from './menuConfig'
|
||||||
|
import { flowEventBus } from '../../events'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const activeSubmenu = ref(null)
|
||||||
|
const submenuPosition = ref({ x: 0, y: 0 })
|
||||||
|
const size = ref({ width: 1000, height: 800 })
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
const activeSubmenuItems = computed(() => {
|
||||||
|
if (!activeSubmenu.value) return []
|
||||||
|
return menuConfig.find(m => m.key === activeSubmenu.value)?.children || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const submenuStyle = computed(() => ({
|
||||||
|
left: submenuPosition.value.x + 'px',
|
||||||
|
top: submenuPosition.value.y + 'px'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const showSubmenu = (key: string, event: MouseEvent) => {
|
||||||
|
const menuEl = event.target as HTMLElement
|
||||||
|
const rect = menuEl.getBoundingClientRect()
|
||||||
|
submenuPosition.value = {
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.bottom
|
||||||
|
}
|
||||||
|
activeSubmenu.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideSubmenu = () => {
|
||||||
|
activeSubmenu.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuClick = (item) => {
|
||||||
|
if (item.type === 'checkbox') {
|
||||||
|
item.checked = !item.checked
|
||||||
|
|
||||||
|
// 处理复选框状态变化
|
||||||
|
switch (item.key) {
|
||||||
|
case 'showGrid':
|
||||||
|
flowEventBus.emit('toggle-grid', item.checked)
|
||||||
|
break
|
||||||
|
case 'showMinimap':
|
||||||
|
flowEventBus.emit('toggle-minimap', item.checked)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑菜单事件
|
||||||
|
switch (item.key) {
|
||||||
|
// 文件菜单
|
||||||
|
case 'new':
|
||||||
|
flowEventBus.emit('file-new')
|
||||||
|
break
|
||||||
|
case 'open':
|
||||||
|
flowEventBus.emit('file-open')
|
||||||
|
break
|
||||||
|
case 'save':
|
||||||
|
flowEventBus.emit('file-save')
|
||||||
|
break
|
||||||
|
case 'exportImage':
|
||||||
|
flowEventBus.emit('file-export-image')
|
||||||
|
break
|
||||||
|
case 'exportJson':
|
||||||
|
flowEventBus.emit('file-export-json')
|
||||||
|
break
|
||||||
|
|
||||||
|
// 编辑菜单
|
||||||
|
case 'undo':
|
||||||
|
flowEventBus.emit('undo')
|
||||||
|
break
|
||||||
|
case 'redo':
|
||||||
|
flowEventBus.emit('redo')
|
||||||
|
break
|
||||||
|
case 'cut':
|
||||||
|
flowEventBus.emit('cut')
|
||||||
|
break
|
||||||
|
case 'copy':
|
||||||
|
flowEventBus.emit('copy')
|
||||||
|
break
|
||||||
|
case 'paste':
|
||||||
|
flowEventBus.emit('paste')
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
flowEventBus.emit('delete')
|
||||||
|
break
|
||||||
|
case 'selectAll':
|
||||||
|
flowEventBus.emit('select-all')
|
||||||
|
break
|
||||||
|
case 'invertSelect':
|
||||||
|
flowEventBus.emit('invert-select')
|
||||||
|
break
|
||||||
|
case 'cancelSelect':
|
||||||
|
flowEventBus.emit('cancel-select')
|
||||||
|
break
|
||||||
|
|
||||||
|
// 视图菜单
|
||||||
|
case 'zoomIn':
|
||||||
|
flowEventBus.emit('zoom-in')
|
||||||
|
break
|
||||||
|
case 'zoomOut':
|
||||||
|
flowEventBus.emit('zoom-out')
|
||||||
|
break
|
||||||
|
case 'fit':
|
||||||
|
flowEventBus.emit('fit')
|
||||||
|
break
|
||||||
|
|
||||||
|
// 画布菜单
|
||||||
|
case 'backgroundColor':
|
||||||
|
ElMessageBox.prompt('请输入背景颜色', '设置背景颜色', {
|
||||||
|
inputPattern: /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/,
|
||||||
|
inputErrorMessage: '请输入有效的十六进制颜色代码',
|
||||||
|
inputValue: item.value
|
||||||
|
}).then(({ value }) => {
|
||||||
|
item.value = value;
|
||||||
|
flowEventBus.emit('change-background-color', value);
|
||||||
|
}).catch(() => { })
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gridSize':
|
||||||
|
ElMessageBox.prompt('请输入网格大小', '设置网格大小', {
|
||||||
|
inputPattern: /^\d+$/,
|
||||||
|
inputErrorMessage: '请输入有效的整数',
|
||||||
|
inputValue: item.value
|
||||||
|
}).then(({ value }) => {
|
||||||
|
item.value = Number(value);
|
||||||
|
flowEventBus.emit('change-grid-size', item.value);
|
||||||
|
}).catch(() => { })
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'canvasSize':
|
||||||
|
ElMessageBox.prompt('请输入画布大小 (宽x高)', '设置画布大小', {
|
||||||
|
inputPattern: /^\d+x\d+$/,
|
||||||
|
inputErrorMessage: '格式: 宽x高,例如: 1000x800',
|
||||||
|
inputValue: `${size.value.width}x${size.value.height}`
|
||||||
|
}).then(({ value }) => {
|
||||||
|
const [width, height] = value.split('x').map(Number)
|
||||||
|
size.value = { width, height }
|
||||||
|
if (item.value) {
|
||||||
|
item.value = size.value;
|
||||||
|
}
|
||||||
|
flowEventBus.emit('change-canvas-size', size.value);
|
||||||
|
}).catch(() => { })
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 排列菜单
|
||||||
|
case 'hierarchicalLayout':
|
||||||
|
flowEventBus.emit('hierarchical-layout');
|
||||||
|
break;
|
||||||
|
case 'circleLayout':
|
||||||
|
flowEventBus.emit('circle-layout');
|
||||||
|
break;
|
||||||
|
case 'organicLayout':
|
||||||
|
flowEventBus.emit('organic-layout');
|
||||||
|
break;
|
||||||
|
case 'bringToFront':
|
||||||
|
flowEventBus.emit('bring-to-front');
|
||||||
|
break;
|
||||||
|
case 'sendToBack':
|
||||||
|
flowEventBus.emit('send-to-back');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSubmenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加快捷键处理
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
const isCtrl = e.ctrlKey || e.metaKey
|
||||||
|
|
||||||
|
if (isCtrl) {
|
||||||
|
switch (e.key.toLowerCase()) {
|
||||||
|
case 'n':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('file-new')
|
||||||
|
break
|
||||||
|
case 'o':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('file-open')
|
||||||
|
break
|
||||||
|
case 's':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('file-save')
|
||||||
|
break
|
||||||
|
case 'z':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('undo')
|
||||||
|
break
|
||||||
|
case 'y':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('redo')
|
||||||
|
break
|
||||||
|
case 'x':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('cut')
|
||||||
|
break
|
||||||
|
case 'c':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('copy')
|
||||||
|
break
|
||||||
|
case 'v':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('paste')
|
||||||
|
break
|
||||||
|
case 'a':
|
||||||
|
e.preventDefault()
|
||||||
|
flowEventBus.emit('select-all')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Delete') {
|
||||||
|
flowEventBus.emit('delete')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-menu {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border-bottom: none !important;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-popup {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 0;
|
||||||
|
min-width: 150px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-divider {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,874 @@
|
||||||
|
<template>
|
||||||
|
<div class="flow-toolbar">
|
||||||
|
<!-- 导出组 -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button title="导出" @click="handleExport">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.export"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
|
||||||
|
<!-- 布局组 -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<el-dropdown @command="handleLayoutChange" trigger="click">
|
||||||
|
<button title="自动布局">
|
||||||
|
<span class="icon" v-html="currentLayoutIcon"></span>
|
||||||
|
</button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-for="layout in layouts" :key="layout.type" :command="layout.type">
|
||||||
|
<span class="layout-icon" v-html="layout.icon"></span>
|
||||||
|
<span class="layout-label">{{ layout.label }}</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
|
||||||
|
<!-- 线条样式组 -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button title="曲线" @click="handleEdgeStyle('curved')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.curvedLine"></span>
|
||||||
|
</button>
|
||||||
|
<button title="折线" @click="handleEdgeStyle('orthogonal')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.orthogonalLine"></span>
|
||||||
|
</button>
|
||||||
|
<button title="直线" @click="handleEdgeStyle('straight')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.straightLine"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
|
||||||
|
<!-- 编辑组 -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button title="重做" @click="handleRedo">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.redo"></span>
|
||||||
|
</button>
|
||||||
|
<button title="撤销" @click="handleUndo">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.undo"></span>
|
||||||
|
</button>
|
||||||
|
<button title="删除" @click="handleDelete">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
|
||||||
|
<!-- 视图组 -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button title="适应画布" @click="handleFit">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.fit"></span>
|
||||||
|
</button>
|
||||||
|
<button title="缩小" @click="handleZoom(0.8)">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.zoomOut"></span>
|
||||||
|
</button>
|
||||||
|
<button title="放大" @click="handleZoom(1.2)">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.zoomIn"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 返回按钮组,靠右对齐 -->
|
||||||
|
<div class="toolbar-spacer"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<router-link to="/" class="back-button" title="返回首页">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.back"></span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加背景颜色选择器 -->
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<button class="toolbar-btn" title="背景颜色" @click.stop="toggleColorPicker">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.backgroundColor"></span>
|
||||||
|
</button>
|
||||||
|
<!-- 颜色选择面板 -->
|
||||||
|
<div v-if="showColorPicker" class="color-picker-panel" @click.stop>
|
||||||
|
<div class="color-grid">
|
||||||
|
<div
|
||||||
|
v-for="color in colors"
|
||||||
|
:key="color"
|
||||||
|
class="color-item"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
@click="selectColor(color)"
|
||||||
|
>
|
||||||
|
<span v-if="selectedColor === color" class="color-check">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-color">
|
||||||
|
<input type="color" v-model="customColor" @change="selectColor(customColor)" />
|
||||||
|
<span>自定义颜色</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button
|
||||||
|
class="toolbar-btn"
|
||||||
|
:class="{ active: showGrid }"
|
||||||
|
title="显示网格"
|
||||||
|
@click="toggleGrid"
|
||||||
|
>
|
||||||
|
<span class="icon" v-html="ToolbarIcons.grid"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字体设置组 -->
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<el-dropdown @command="handleFontChange" trigger="click">
|
||||||
|
<button title="字体设置">
|
||||||
|
<span class="icon">🔤</span>
|
||||||
|
</button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="font in fonts"
|
||||||
|
:key="font.value"
|
||||||
|
:command="font.value"
|
||||||
|
:style="{ fontFamily: font.value }"
|
||||||
|
>
|
||||||
|
{{ font.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group" style="position: relative">
|
||||||
|
<button class="toolbar-btn" title="节点颜色" @click.stop="toggleNodeColorPicker">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.nodeColor"></span>
|
||||||
|
</button>
|
||||||
|
<!-- 颜色选择面板 -->
|
||||||
|
<div v-if="showNodeColorPicker" class="color-picker-panel" @click.stop>
|
||||||
|
<div class="color-grid">
|
||||||
|
<div
|
||||||
|
v-for="color in nodeColors"
|
||||||
|
:key="color"
|
||||||
|
class="color-item"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
@click.stop="setNodeColor(color)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-color">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="customNodeColor"
|
||||||
|
@change.stop="setNodeColor(customNodeColor)"
|
||||||
|
/>
|
||||||
|
<span>自定义颜色</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button
|
||||||
|
class="toolbar-btn"
|
||||||
|
title="样式刷"
|
||||||
|
:class="{ active: isStyleBrushActive }"
|
||||||
|
@click="toggleStyleBrush"
|
||||||
|
>
|
||||||
|
<span class="icon">🖌️</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tooltip content="插入图片" placement="bottom">
|
||||||
|
<el-button @click="handleInsertImage">
|
||||||
|
<span class="toolbar-icon" v-html="MenuIcons.image"></span>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入框 -->
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="imageInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="onImageSelected"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 边框设置组 -->
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<!-- 边框颜色 -->
|
||||||
|
<el-tooltip :content="hasSelectedNode ? '边框颜色' : '未选择节点'" placement="bottom">
|
||||||
|
<el-button
|
||||||
|
class="toolbar-btn"
|
||||||
|
@click.stop="toggleBorderColorPicker"
|
||||||
|
:disabled="!hasSelectedNode"
|
||||||
|
:class="{ disabled: !hasSelectedNode }"
|
||||||
|
>
|
||||||
|
<span class="icon" v-html="ToolbarIcons.borderColor"></span>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 边框样式 -->
|
||||||
|
<el-tooltip :content="hasSelectedNode ? '边框样式' : '未选择节点'" placement="bottom">
|
||||||
|
<el-dropdown @command="handleBorderStyle" trigger="click">
|
||||||
|
<button
|
||||||
|
class="toolbar-btn"
|
||||||
|
:disabled="!hasSelectedNode"
|
||||||
|
:class="{ disabled: !hasSelectedNode }"
|
||||||
|
>
|
||||||
|
<span class="icon" v-html="ToolbarIcons.borderStyle"></span>
|
||||||
|
</button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="solid">实线</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="dashed">虚线</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="dotted">点线</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 边框宽度 -->
|
||||||
|
<el-tooltip :content="hasSelectedNode ? '边框宽度' : '未选择节点'" placement="bottom">
|
||||||
|
<el-dropdown @command="handleBorderWidth" trigger="click">
|
||||||
|
<button
|
||||||
|
class="toolbar-btn"
|
||||||
|
:disabled="!hasSelectedNode"
|
||||||
|
:class="{ disabled: !hasSelectedNode }"
|
||||||
|
>
|
||||||
|
<span class="icon" v-html="ToolbarIcons.borderWidth"></span>
|
||||||
|
</button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-for="width in borderWidths" :key="width" :command="width">
|
||||||
|
{{ width }}px
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 边框颜色选择面板 -->
|
||||||
|
<div v-if="showBorderColorPicker" class="color-picker-panel" @click.stop>
|
||||||
|
<div class="color-grid">
|
||||||
|
<div
|
||||||
|
v-for="color in borderColors"
|
||||||
|
:key="color"
|
||||||
|
class="color-item"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
@click="setBorderColor(color)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-color">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="customBorderColor"
|
||||||
|
@change="setBorderColor(customBorderColor)"
|
||||||
|
/>
|
||||||
|
<span>自定义颜色</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对齐工具组 -->
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<el-tooltip content="左对齐" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('left')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignLeft"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="水平居中" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('center')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignCenter"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="右对齐" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('right')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignRight"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="顶部对齐" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('top')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignTop"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="垂直居中" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('middle')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignMiddle"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="底部对齐" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('bottom')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.alignBottom"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="水平等距分布" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('distributeHorizontally')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.distributeHorizontally"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="垂直等距分布" placement="bottom">
|
||||||
|
<button class="toolbar-btn" @click="handleAlign('distributeVertically')">
|
||||||
|
<span class="icon" v-html="ToolbarIcons.distributeVertically"></span>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { flowEventBus } from '../../events'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { FlowIcons, ToolbarIcons, MenuIcons } from '../../icons/index'
|
||||||
|
|
||||||
|
const handleZoom = (scale: number) => {
|
||||||
|
flowEventBus.emit('zoom', scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFit = () => {
|
||||||
|
flowEventBus.emit('fit')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
flowEventBus.emit('delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUndo = () => {
|
||||||
|
flowEventBus.emit('undo')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRedo = () => {
|
||||||
|
flowEventBus.emit('reDo')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdgeStyle = (style: string) => {
|
||||||
|
flowEventBus.emit('edge-style', style)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
flowEventBus.emit('export')
|
||||||
|
ElMessage({
|
||||||
|
message: '导出成功',
|
||||||
|
type: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 布局配置
|
||||||
|
const layouts = [
|
||||||
|
{
|
||||||
|
type: 'hierarchical',
|
||||||
|
label: '层次布局',
|
||||||
|
icon: '⚡' // 这里可以替换成实际的SVG图标
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'circle',
|
||||||
|
label: '圆形布局',
|
||||||
|
icon: '⭕'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'organic',
|
||||||
|
label: '有机布局',
|
||||||
|
icon: '🌿'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'stack',
|
||||||
|
label: '堆叠布局',
|
||||||
|
icon: '📚'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'parallel',
|
||||||
|
label: '平行边布局',
|
||||||
|
icon: '⫴'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'partition',
|
||||||
|
label: '分区布局',
|
||||||
|
icon: '🔲'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentLayout = ref('hierarchical')
|
||||||
|
|
||||||
|
const currentLayoutIcon = computed(() => {
|
||||||
|
const layout = layouts.find((l) => l.type === currentLayout.value)
|
||||||
|
return layout?.icon || layouts[0].icon
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLayoutChange = (type: string) => {
|
||||||
|
currentLayout.value = type
|
||||||
|
flowEventBus.emit('change-layout', type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showColorPicker = ref(false)
|
||||||
|
const selectedColor = ref('#ffffff')
|
||||||
|
const customColor = ref('#ffffff')
|
||||||
|
|
||||||
|
// 预定义的颜色列表
|
||||||
|
const colors = [
|
||||||
|
'#ffffff',
|
||||||
|
'#f0f0f0',
|
||||||
|
'#e8f4f9',
|
||||||
|
'#e3f3ff',
|
||||||
|
'#f9f0ff',
|
||||||
|
'#fff3e8',
|
||||||
|
'#f5fff0',
|
||||||
|
'#fff0f0'
|
||||||
|
]
|
||||||
|
|
||||||
|
const toggleColorPicker = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
showColorPicker.value = !showColorPicker.value
|
||||||
|
|
||||||
|
// 添加全局点击事件来关闭颜色选择器
|
||||||
|
if (showColorPicker.value) {
|
||||||
|
document.addEventListener('click', closeColorPicker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeColorPicker = () => {
|
||||||
|
showColorPicker.value = false
|
||||||
|
document.removeEventListener('click', closeColorPicker)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectColor = (color: string) => {
|
||||||
|
selectedColor.value = color
|
||||||
|
// 通过事件总线发送背景颜色变更事件
|
||||||
|
flowEventBus.emit('change-background', color)
|
||||||
|
showColorPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const showGrid = ref(true)
|
||||||
|
|
||||||
|
const toggleGrid = () => {
|
||||||
|
showGrid.value = !showGrid.value
|
||||||
|
flowEventBus.emit('toggle-grid', showGrid.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字体配置
|
||||||
|
const fonts = [
|
||||||
|
{ label: '默认字体', value: 'Arial' },
|
||||||
|
{ label: '微软雅黑', value: 'Microsoft YaHei' },
|
||||||
|
{ label: '宋体', value: 'SimSun' },
|
||||||
|
{ label: '黑体', value: 'SimHei' },
|
||||||
|
{ label: '楷体', value: 'KaiTi' },
|
||||||
|
{ label: 'Times New Roman', value: 'Times New Roman' },
|
||||||
|
{ label: 'Helvetica', value: 'Helvetica' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleFontChange = (fontFamily: string) => {
|
||||||
|
flowEventBus.emit('change-font', fontFamily)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNodeColorPicker = ref(false)
|
||||||
|
const customNodeColor = ref('#ffffff')
|
||||||
|
|
||||||
|
const nodeColors = [
|
||||||
|
'#67C23A',
|
||||||
|
'#F56C6C',
|
||||||
|
'#409EFF',
|
||||||
|
'#E6A23C',
|
||||||
|
'#909399',
|
||||||
|
'#95A5A6',
|
||||||
|
'#3498DB',
|
||||||
|
'#9B59B6',
|
||||||
|
'#F1C40F',
|
||||||
|
'#34495E',
|
||||||
|
'#1ABC9C',
|
||||||
|
'#7F8C8D',
|
||||||
|
'#D35400',
|
||||||
|
'#27AE60',
|
||||||
|
'#8E44AD'
|
||||||
|
]
|
||||||
|
|
||||||
|
const toggleNodeColorPicker = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
showNodeColorPicker.value = !showNodeColorPicker.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNodeColor = (color: string) => {
|
||||||
|
flowEventBus.emit('change-node-color', color)
|
||||||
|
showNodeColorPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击其他地方关闭颜色选择器
|
||||||
|
onMounted(() => {
|
||||||
|
const closeColorPicker = () => {
|
||||||
|
showNodeColorPicker.value = false
|
||||||
|
}
|
||||||
|
document.addEventListener('click', closeColorPicker)
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', closeColorPicker)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听节点选中状态变化
|
||||||
|
flowEventBus.on('selection-changed', (cells: any[]) => {
|
||||||
|
hasSelectedNode.value = cells?.some((cell) => cell.isVertex()) || false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
flowEventBus.off('selection-changed')
|
||||||
|
})
|
||||||
|
|
||||||
|
const isStyleBrushActive = ref(false)
|
||||||
|
let copiedStyle: any = null
|
||||||
|
|
||||||
|
const toggleStyleBrush = () => {
|
||||||
|
if (!isStyleBrushActive.value) {
|
||||||
|
// 获取当前选中节点的样式
|
||||||
|
flowEventBus.emit('get-selected-style', (style: any) => {
|
||||||
|
copiedStyle = style
|
||||||
|
isStyleBrushActive.value = true
|
||||||
|
flowEventBus.emit('activate-style-brush', style)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
isStyleBrushActive.value = false
|
||||||
|
copiedStyle = null
|
||||||
|
flowEventBus.emit('deactivate-style-brush')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听样式应用完成事件
|
||||||
|
flowEventBus.on('style-brush-applied', () => {
|
||||||
|
isStyleBrushActive.value = false
|
||||||
|
copiedStyle = null
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageInput = ref<HTMLInputElement>()
|
||||||
|
|
||||||
|
const handleInsertImage = () => {
|
||||||
|
imageInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImageSelected = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const imageUrl = event.target?.result as string
|
||||||
|
// 发送事件到 FlowCanvas 处理图片插入
|
||||||
|
flowEventBus.emit('insert-image', imageUrl)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
// 清空input,以便可以重复选择同一文件
|
||||||
|
target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边框相关数据
|
||||||
|
const borderWidths = [1, 2, 3, 4, 5, 6, 8, 10]
|
||||||
|
const borderColors = [
|
||||||
|
'#000000',
|
||||||
|
'#333333',
|
||||||
|
'#666666',
|
||||||
|
'#999999',
|
||||||
|
'#409EFF',
|
||||||
|
'#67C23A',
|
||||||
|
'#E6A23C',
|
||||||
|
'#F56C6C'
|
||||||
|
]
|
||||||
|
const showBorderColorPicker = ref(false)
|
||||||
|
const customBorderColor = ref('#000000')
|
||||||
|
|
||||||
|
// 边框相关方法
|
||||||
|
const handleBorderStyle = (style: string) => {
|
||||||
|
flowEventBus.emit('change-border-style', style)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBorderWidth = (width: number) => {
|
||||||
|
flowEventBus.emit('change-border-width', width)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBorderColorPicker = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
showBorderColorPicker.value = !showBorderColorPicker.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setBorderColor = (color: string) => {
|
||||||
|
flowEventBus.emit('change-border-color', color)
|
||||||
|
showBorderColorPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSelectedNode = ref(false)
|
||||||
|
|
||||||
|
// 对齐处理函数
|
||||||
|
const handleAlign = (type: string) => {
|
||||||
|
flowEventBus.emit('align-cells', type)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 2px;
|
||||||
|
background: #fff;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #dcdfe6;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon :deep(svg) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover .icon {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
color: #606266;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-top: 4px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-check {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color input {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item:hover) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color input {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-color span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加相对定位到按钮容器 */
|
||||||
|
.toolbar-btn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保颜色选择器的父容器也是相对定位 */
|
||||||
|
.toolbar-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.disabled:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用状态下的下拉菜单样式 */
|
||||||
|
.el-dropdown.is-disabled .toolbar-btn {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-edit-toolbar"
|
||||||
|
v-show="visible"
|
||||||
|
:style="position">
|
||||||
|
<!-- 字体选择 -->
|
||||||
|
<el-select v-model="localStyle.fontFamily"
|
||||||
|
size="small"
|
||||||
|
@change="handleStyleChange('fontFamily')">
|
||||||
|
<el-option v-for="font in fontFamilies"
|
||||||
|
:key="font"
|
||||||
|
:label="font"
|
||||||
|
:value="font"
|
||||||
|
:style="{ fontFamily: font }">
|
||||||
|
{{ font }}
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<!-- 字号选择 -->
|
||||||
|
<el-select v-model="localStyle.fontSize"
|
||||||
|
size="small"
|
||||||
|
@change="handleStyleChange('fontSize')">
|
||||||
|
<el-option v-for="size in fontSizes"
|
||||||
|
:key="size"
|
||||||
|
:label="`${size}px`"
|
||||||
|
:value="size">
|
||||||
|
{{ size }}px
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<!-- 文字颜色 -->
|
||||||
|
<el-color-picker v-model="localStyle.fontColor"
|
||||||
|
size="small"
|
||||||
|
@change="handleStyleChange('fontColor')" />
|
||||||
|
|
||||||
|
<!-- 加粗按钮 -->
|
||||||
|
<el-button size="small"
|
||||||
|
:type="localStyle.fontWeight === 'bold' ? 'primary' : 'default'"
|
||||||
|
@click="toggleFontWeight">
|
||||||
|
B
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 下划线按钮 -->
|
||||||
|
<el-button size="small"
|
||||||
|
:type="localStyle.textDecoration === 'underline' ? 'primary' : 'default'"
|
||||||
|
@click="toggleTextDecoration">
|
||||||
|
U
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { flowEventBus } from '../../events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextEditToolbar',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ top: '0px', left: '0px' })
|
||||||
|
},
|
||||||
|
currentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fontSize: 14,
|
||||||
|
fontColor: '#000000',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
textDecoration: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
localStyle: { ...this.currentStyle },
|
||||||
|
fontSizes: [12, 14, 16, 18, 20, 24, 28, 32],
|
||||||
|
fontFamilies: ['Arial', 'Times New Roman', '微软雅黑', '宋体', 'Helvetica']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentStyle: {
|
||||||
|
handler(newStyle) {
|
||||||
|
this.localStyle = { ...newStyle }
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleStyleChange(property) {
|
||||||
|
flowEventBus.emit('update-text-style', {
|
||||||
|
[property]: this.localStyle[property]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleFontWeight() {
|
||||||
|
this.localStyle.fontWeight =
|
||||||
|
this.localStyle.fontWeight === 'bold' ? 'normal' : 'bold'
|
||||||
|
this.handleStyleChange('fontWeight')
|
||||||
|
},
|
||||||
|
toggleTextDecoration() {
|
||||||
|
this.localStyle.textDecoration =
|
||||||
|
this.localStyle.textDecoration === 'underline' ? 'none' : 'underline'
|
||||||
|
this.handleStyleChange('textDecoration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-edit-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
export const menuConfig = [
|
||||||
|
{
|
||||||
|
key: 'file',
|
||||||
|
label: '文件',
|
||||||
|
children: [
|
||||||
|
{ key: 'new', label: '新建', icon: 'Document', shortcut: 'Ctrl+N' },
|
||||||
|
{ key: 'open', label: '打开', icon: 'FolderOpened', shortcut: 'Ctrl+O' },
|
||||||
|
{ key: 'save', label: '保存', icon: 'Download', shortcut: 'Ctrl+S' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'exportImage', label: '导出图片', icon: 'Picture', shortcut: 'Ctrl+E' },
|
||||||
|
{ key: 'exportJson', label: '导出JSON', icon: 'Document' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑',
|
||||||
|
children: [
|
||||||
|
{ key: 'undo', label: '撤销', icon: 'Back', shortcut: 'Ctrl+Z' },
|
||||||
|
{ key: 'redo', label: '重做', icon: 'Right', shortcut: 'Ctrl+Y' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'cut', label: '剪切', icon: 'Scissors', shortcut: 'Ctrl+X' },
|
||||||
|
{ key: 'copy', label: '复制', icon: 'Document', shortcut: 'Ctrl+C' },
|
||||||
|
{ key: 'paste', label: '粘贴', icon: 'DocumentCopy', shortcut: 'Ctrl+V' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'delete', label: '删除', icon: 'Delete', shortcut: 'Delete' },
|
||||||
|
{ key: 'selectAll', label: '全选', icon: 'Select', shortcut: 'Ctrl+A' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'select',
|
||||||
|
label: '选择',
|
||||||
|
children: [
|
||||||
|
{ key: 'selectAll', label: '全选', icon: 'Select' },
|
||||||
|
{ key: 'invertSelect', label: '反选', icon: 'RefreshRight' },
|
||||||
|
{ key: 'cancelSelect', label: '取消选择', icon: 'Close' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '视图',
|
||||||
|
key: 'view',
|
||||||
|
children: [
|
||||||
|
{ key: 'zoomIn', label: '放大', icon: 'ZoomIn' },
|
||||||
|
{ key: 'zoomOut', label: '缩小', icon: 'ZoomOut' },
|
||||||
|
{ key: 'fit', label: '适应画布', icon: 'FullScreen' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'showGrid', label: '显示网格', type: 'checkbox', checked: true },
|
||||||
|
{ key: 'showMinimap', label: '显示缩略图', type: 'checkbox', checked: true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '插入',
|
||||||
|
key: 'insert',
|
||||||
|
children: [
|
||||||
|
{ label: '图片', key: 'insertImage', icon: 'Picture' },
|
||||||
|
{ label: '表格', key: 'insertTable', icon: 'Grid' },
|
||||||
|
{ label: '文本', key: 'insertText', icon: 'EditPen' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'canvas',
|
||||||
|
label: '画布',
|
||||||
|
children: [
|
||||||
|
{ key: 'backgroundColor', label: '背景颜色', icon: 'Brush', type: 'color', value: '#ffffff' },
|
||||||
|
{ key: 'gridSize', label: '网格大小', icon: 'Grid', type: 'number', value: 10, min: 5, max: 50 },
|
||||||
|
{ key: 'canvasSize', label: '画布大小', icon: 'FullScreen', type: 'size',
|
||||||
|
value: { width: 1000, height: 800 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '排列',
|
||||||
|
key: 'arrange',
|
||||||
|
children: [
|
||||||
|
{ key: 'hierarchicalLayout', label: '层次布局', icon: 'Sort' },
|
||||||
|
{ key: 'circleLayout', label: '圆形布局', icon: 'CirclePlus' },
|
||||||
|
{ key: 'organicLayout', label: '有机布局', icon: 'Connection' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'bringToFront', label: '置于顶层', icon: 'TopRight' },
|
||||||
|
{ key: 'sendToBack', label: '置于底层', icon: 'BottomLeft' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
<template>
|
||||||
|
<div class="shape-library">
|
||||||
|
<div v-for="(category, index) in categories" :key="index" class="accordion-item">
|
||||||
|
<div class="accordion-header" @click="toggleCategory(index)">
|
||||||
|
<span class="accordion-title">{{ category.title }}</span>
|
||||||
|
<span class="accordion-icon">{{ activeIndex === index ? '▼' : '▶' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-content" :class="{ active: activeIndex === index }">
|
||||||
|
<div class="icon-grid">
|
||||||
|
<div
|
||||||
|
v-for="(item, itemIndex) in category.items"
|
||||||
|
:key="itemIndex"
|
||||||
|
class="icon-item"
|
||||||
|
:style="{ backgroundColor: item.backgroundColor }"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="handleDragStart($event, item)"
|
||||||
|
>
|
||||||
|
<div class="icon-wrapper" v-html="getIconSvg(item.icon)"></div>
|
||||||
|
<span class="icon-label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="more-nodes">
|
||||||
|
<button class="more-nodes-btn" @click="showMoreNodes">
|
||||||
|
<el-icon><CirclePlus /></el-icon>
|
||||||
|
更多节点
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NodeSelector v-model:visible="isDialogVisible" @select="handleNodeSelect" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { CirclePlus } from '@element-plus/icons-vue'
|
||||||
|
import { FlowIcons, ToolbarIcons, UMLIcons } from '../../icons/index'
|
||||||
|
import NodeSelector from '../property/NodeSelector.vue'
|
||||||
|
import flowNodes from './flowNodes'
|
||||||
|
import extraNodes from '@/assets/data/extra-nodes.json'
|
||||||
|
|
||||||
|
const StylePreviewSVG = `
|
||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="8" y="8" width="12" height="8" rx="2" fill="var(--node-color, #409EFF)" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<path d="M14 16L14 22" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<rect x="8" y="22" width="12" height="8" rx="2" fill="var(--node-color, #409EFF)" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<path d="M20 26L28 26" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<rect x="28" y="22" width="12" height="8" rx="2" fill="var(--node-color, #409EFF)" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<path d="M34 30L34 36" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
<rect x="28" y="36" width="12" height="8" rx="2" fill="var(--node-color, #409EFF)" stroke="var(--border-color, #409EFF)" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShapeLibrary',
|
||||||
|
components: {
|
||||||
|
NodeSelector,
|
||||||
|
CirclePlus
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeIndex: 0,
|
||||||
|
categories: flowNodes.categories,
|
||||||
|
isDialogVisible: false,
|
||||||
|
AllIcons: { ...FlowIcons, ...UMLIcons, ...ToolbarIcons }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleCategory(index: number) {
|
||||||
|
this.activeIndex = this.activeIndex === index ? -1 : index
|
||||||
|
},
|
||||||
|
getIconSvg(type: string) {
|
||||||
|
if (type.startsWith('style-')) {
|
||||||
|
// 为风格图标返回统一的流程图SVG,通过CSS变量控制颜色
|
||||||
|
const style = type.replace('style-', '')
|
||||||
|
const colors = {
|
||||||
|
default: { node: '#409EFF', border: '#409EFF' },
|
||||||
|
primary: { node: '#409EFF', border: '#409EFF' },
|
||||||
|
success: { node: '#67C23A', border: '#67C23A' },
|
||||||
|
warning: { node: '#E6A23C', border: '#E6A23C' },
|
||||||
|
danger: { node: '#F56C6C', border: '#F56C6C' }
|
||||||
|
// 可以添加更多风格的颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = colors[style] || colors.default
|
||||||
|
return StylePreviewSVG.replace(/var\(--node-color,[^)]+\)/g, color.node).replace(
|
||||||
|
/var\(--border-color,[^)]+\)/g,
|
||||||
|
color.border
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this.AllIcons[type as keyof typeof FlowIcons] || FlowIcons.Process
|
||||||
|
},
|
||||||
|
handleDragStart(event: DragEvent, item: any) {
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.setData('application/json', JSON.stringify(item))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMoreNodes() {
|
||||||
|
this.isDialogVisible = true
|
||||||
|
},
|
||||||
|
handleNodeSelect(nodes: any[]) {
|
||||||
|
// 按分类组织节点
|
||||||
|
const nodesByCategory = nodes.reduce((acc: any, node: any) => {
|
||||||
|
const category = extraNodes.categories.find((c) =>
|
||||||
|
c.nodes.some((n) => n.type === node.type)
|
||||||
|
)
|
||||||
|
if (category) {
|
||||||
|
if (!acc[category.name]) {
|
||||||
|
acc[category.name] = []
|
||||||
|
}
|
||||||
|
acc[category.name].push({
|
||||||
|
type: node.type,
|
||||||
|
label: node.name,
|
||||||
|
icon: node.icon,
|
||||||
|
backgroundColor: '#fff'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// 添加新分类到列表中
|
||||||
|
Object.entries(nodesByCategory).forEach(([categoryName, items]) => {
|
||||||
|
const existingCategory = this.categories.find((c) => c.title === categoryName)
|
||||||
|
if (existingCategory) {
|
||||||
|
existingCategory.items = [...existingCategory.items, ...(items as any[])]
|
||||||
|
} else {
|
||||||
|
this.categories.push({
|
||||||
|
title: categoryName,
|
||||||
|
items: items as any[]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.shape-library {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content.active {
|
||||||
|
max-height: 500px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: move;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item:hover {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-nodes {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-nodes-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-nodes-btn:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-nodes-btn .el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
import { FlowIcons } from '../icons';
|
||||||
|
|
||||||
|
interface FlowNode {
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
size?: [number, number] | null;
|
||||||
|
icon: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
style?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeCategory {
|
||||||
|
title: string;
|
||||||
|
items: FlowNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowCategories: NodeCategory[] = [
|
||||||
|
{
|
||||||
|
title: '基础流程',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'Start',
|
||||||
|
label: '开始',
|
||||||
|
icon: 'Start',
|
||||||
|
size: [50, 50],
|
||||||
|
backgroundColor: '#e8f5e9',
|
||||||
|
style: {
|
||||||
|
shape: 'ellipse',
|
||||||
|
fillColor: '#e8f5e9',
|
||||||
|
strokeColor: '#4caf50',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'End',
|
||||||
|
label: '结束',
|
||||||
|
icon: 'End',
|
||||||
|
size: [50, 50],
|
||||||
|
backgroundColor: '#ffebee',
|
||||||
|
style: {
|
||||||
|
shape: 'end',
|
||||||
|
fillColor: '#ffebee',
|
||||||
|
strokeColor: '#f44336',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Process',
|
||||||
|
label: '处理',
|
||||||
|
icon: 'Process',
|
||||||
|
backgroundColor: '#e3f2fd',
|
||||||
|
style: {
|
||||||
|
shape: 'rectangle',
|
||||||
|
fillColor: '#e3f2fd',
|
||||||
|
strokeColor: '#2196f3',
|
||||||
|
strokeWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Decision',
|
||||||
|
label: '判断',
|
||||||
|
icon: 'Decision',
|
||||||
|
backgroundColor: '#fff3e0',
|
||||||
|
style: {
|
||||||
|
shape: 'rhombus',
|
||||||
|
fillColor: '#fff3e0',
|
||||||
|
strokeColor: '#ff9800',
|
||||||
|
strokeWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Note',
|
||||||
|
label: '备注',
|
||||||
|
icon: 'Note',
|
||||||
|
size: [120, 80],
|
||||||
|
backgroundColor: '#fff9c4',
|
||||||
|
style: {
|
||||||
|
shape: 'note',
|
||||||
|
fillColor: '#fff9c4',
|
||||||
|
strokeColor: '#ffd54f',
|
||||||
|
strokeWidth: 1,
|
||||||
|
shadow: true,
|
||||||
|
rounded: true,
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
spacingTop: 5,
|
||||||
|
spacingLeft: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Text',
|
||||||
|
label: '文字',
|
||||||
|
icon: 'Text',
|
||||||
|
size: [100, 40],
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
style: {
|
||||||
|
shape: 'text',
|
||||||
|
strokeColor: 'none',
|
||||||
|
fillColor: 'none',
|
||||||
|
fontColor: '#333333',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
editable: true,
|
||||||
|
autosize: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '基础形状',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'Rectangle',
|
||||||
|
label: '矩形',
|
||||||
|
icon: 'Rectangle',
|
||||||
|
style: {
|
||||||
|
shape: 'rectangle',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Ellipse',
|
||||||
|
label: '圆形',
|
||||||
|
icon: 'Ellipse',
|
||||||
|
style: {
|
||||||
|
shape: 'ellipse',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Rhombus',
|
||||||
|
label: '菱形',
|
||||||
|
icon: 'Rhombus',
|
||||||
|
style: {
|
||||||
|
shape: 'rhombus',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Triangle',
|
||||||
|
label: '三角形',
|
||||||
|
icon: 'Triangle',
|
||||||
|
style: {
|
||||||
|
shape: 'triangle',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Hexagon',
|
||||||
|
label: '六边形',
|
||||||
|
icon: 'Hexagon',
|
||||||
|
style: {
|
||||||
|
shape: 'hexagon',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Star',
|
||||||
|
label: '五角星',
|
||||||
|
icon: 'Star',
|
||||||
|
style: {
|
||||||
|
shape: 'star',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UML图形',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'Class',
|
||||||
|
label: '类',
|
||||||
|
icon: 'ClassIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'umlClass',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
value: `<div style="padding: 8px">
|
||||||
|
<div style="border-bottom: 1px solid #333; font-weight: bold">类名</div>
|
||||||
|
<div style="border-bottom: 1px solid #333">+ 属性1: 类型</div>
|
||||||
|
<div>+ 方法1(): 返回类型</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Interface',
|
||||||
|
label: '接口',
|
||||||
|
icon: 'InterfaceIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'umlInterface',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
dashed: true,
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
value: `<div style="padding: 8px">
|
||||||
|
<div style="border-bottom: 1px solid #333; font-style: italic"><<interface>></div>
|
||||||
|
<div style="border-bottom: 1px solid #333; font-weight: bold">接口名</div>
|
||||||
|
<div>+ 方法1(): 返回类型</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Package',
|
||||||
|
label: '包',
|
||||||
|
icon: 'PackageIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'package',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Component',
|
||||||
|
label: '组件',
|
||||||
|
icon: 'ComponentIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'component',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
value: `<div style="padding: 8px">
|
||||||
|
<div style="font-style: italic"><<component>></div>
|
||||||
|
<div style="font-weight: bold">组件名</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Aggregation',
|
||||||
|
label: '聚合',
|
||||||
|
icon: 'AggregationIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'rhombus',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Composition',
|
||||||
|
label: '组合',
|
||||||
|
icon: 'CompositionIcon',
|
||||||
|
style: {
|
||||||
|
shape: 'rhombus',
|
||||||
|
fillColor: '#333333',
|
||||||
|
strokeColor: '#333333'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '泳道',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'VSwimlane',
|
||||||
|
label: '垂直泳道',
|
||||||
|
icon: 'VSwimlane',
|
||||||
|
size: [200, 400],
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
style: {
|
||||||
|
shape: 'swimlane',
|
||||||
|
vertical: true, // 垂直泳道(设置为 false 则为水平泳道)
|
||||||
|
fillColor: '#f5f5f5',
|
||||||
|
strokeColor: '#bdbdbd',
|
||||||
|
startSize: 40,
|
||||||
|
horizontal: false,
|
||||||
|
separatorColor: '#bdbdbd',
|
||||||
|
fontColor: '#333333',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'HSwimlane',
|
||||||
|
label: '水平泳道',
|
||||||
|
icon: 'HSwimlane',
|
||||||
|
size: [400, 200],
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
style: {
|
||||||
|
shape: 'swimlane',
|
||||||
|
vertical: false, // 垂直泳道(设置为 false 则为水平泳道)
|
||||||
|
fillColor: '#f5f5f5',
|
||||||
|
strokeColor: '#bdbdbd',
|
||||||
|
startSize: 40,
|
||||||
|
horizontal: true,
|
||||||
|
separatorColor: '#bdbdbd',
|
||||||
|
fontColor: '#333333',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '表格组件',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'Table',
|
||||||
|
label: '表格',
|
||||||
|
icon: 'TableIcon',
|
||||||
|
size: [300, 200],
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
style: {
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
strokeWidth: 1,
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
value: `
|
||||||
|
<table style="width:100%; border-collapse: collapse; font-size: 12px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">列1</th>
|
||||||
|
<th style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">列2</th>
|
||||||
|
<th style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">列3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据1</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据2</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据4</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据5</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">数据6</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`,
|
||||||
|
editable: true,
|
||||||
|
resizable: true,
|
||||||
|
autosize: true,
|
||||||
|
tableConfig: {
|
||||||
|
columns: [
|
||||||
|
{ title: '列1', key: 'col1', width: 100 },
|
||||||
|
{ title: '列2', key: 'col2', width: 100 },
|
||||||
|
{ title: '列3', key: 'col3', width: 100 }
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{ col1: '数据1', col2: '数据2', col3: '数据3' },
|
||||||
|
{ col1: '数据4', col2: '数据5', col3: '数据6' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'SimpleTable',
|
||||||
|
label: '简单表格',
|
||||||
|
icon: 'SimpleTableIcon',
|
||||||
|
size: [200, 120],
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
style: {
|
||||||
|
shape: 'table',
|
||||||
|
fillColor: '#ffffff',
|
||||||
|
strokeColor: '#333333',
|
||||||
|
strokeWidth: 1,
|
||||||
|
html: 1,
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
value: `
|
||||||
|
<table style="width:100%; border-collapse: collapse; font-size: 12px;">
|
||||||
|
<tr>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">名称</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">内容</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">说明</td>
|
||||||
|
<td style="border: 1px solid #333; padding: 4px;">描述</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`,
|
||||||
|
editable: true,
|
||||||
|
resizable: true,
|
||||||
|
autosize: true,
|
||||||
|
tableConfig: {
|
||||||
|
columns: [
|
||||||
|
{ title: '名称', key: 'name', width: 80 },
|
||||||
|
{ title: '内容', key: 'content', width: 120 }
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{ name: '名称', content: '内容' },
|
||||||
|
{ name: '说明', content: '描述' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
categories: flowCategories
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<el-tabs v-model="activeTab" class="flow-tabs" type="card">
|
||||||
|
<el-tab-pane label="图形库" name="shapes">
|
||||||
|
<ShapeLibrary />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="风格" name="styles">
|
||||||
|
<StyleLibrary />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import ShapeLibrary from '../node/ShapeLibrary.vue';
|
||||||
|
import StyleLibrary from '../property/StyleLibrary.vue';
|
||||||
|
|
||||||
|
const activeTab = ref('shapes');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-tabs {
|
||||||
|
margin: 3px;
|
||||||
|
background: #fff;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="预览导出图片"
|
||||||
|
width="800px"
|
||||||
|
:before-close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="preview-wrapper" :style="previewBackground">
|
||||||
|
<img :src="imageUrl" class="preview-image" />
|
||||||
|
</div>
|
||||||
|
<div class="background-selector">
|
||||||
|
<span>背景:</span>
|
||||||
|
<el-radio-group v-model="background" size="small">
|
||||||
|
<el-radio-button label="transparent">透明</el-radio-button>
|
||||||
|
<el-radio-button label="white">白色</el-radio-button>
|
||||||
|
<el-radio-button label="black">黑色</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">保存</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
imageUrl: string
|
||||||
|
visible: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
'update:visible': [value: boolean]
|
||||||
|
'confirm': [imageUrl: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value) => emits('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const background = ref('transparent')
|
||||||
|
|
||||||
|
const previewBackground = computed(() => {
|
||||||
|
switch (background.value) {
|
||||||
|
case 'transparent':
|
||||||
|
return {
|
||||||
|
background: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAADFJREFUOI1j/P///38GKgETlQwYNWDUABYGBgYGxv///zNSYsB/BgYGBioawEgNXwAAoBAP/gOzm/MAAAAASUVORK5CYII=)'
|
||||||
|
}
|
||||||
|
case 'white':
|
||||||
|
return { background: '#ffffff' }
|
||||||
|
case 'black':
|
||||||
|
return { background: '#000000' }
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emits('confirm', props.imageUrl)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-wrapper {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="visible" class="nodes-dialog-overlay" @click="handleClose">
|
||||||
|
<div class="nodes-dialog" @click.stop>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3>选择节点类型</h3>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn-confirm" @click="handleConfirm" :disabled="!hasSelection">
|
||||||
|
确定 ({{ selectedNodes.length }})
|
||||||
|
</button>
|
||||||
|
<button class="close-btn" @click="handleClose">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<!-- 左侧类型树 -->
|
||||||
|
<div class="category-tree">
|
||||||
|
<div
|
||||||
|
v-for="category in extraNodes.categories"
|
||||||
|
:key="category.name"
|
||||||
|
class="tree-item"
|
||||||
|
:class="{ active: selectedCategories.includes(category.name) }"
|
||||||
|
@click="toggleCategory(category.name)"
|
||||||
|
>
|
||||||
|
<span class="tree-icon">📁</span>
|
||||||
|
<span class="tree-label">{{ category.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧节点列表 -->
|
||||||
|
<div class="nodes-list">
|
||||||
|
<template v-if="currentNodes.length">
|
||||||
|
<div class="nodes-grid">
|
||||||
|
<div
|
||||||
|
v-for="node in currentNodes"
|
||||||
|
:key="node.type"
|
||||||
|
class="node-item"
|
||||||
|
:class="{ active: isNodeSelected(node) }"
|
||||||
|
@click="toggleNodeSelect(node)"
|
||||||
|
>
|
||||||
|
<span v-if="isNodeSelected(node)" class="check-icon">✓</span>
|
||||||
|
<span class="node-icon">{{ node.icon }}</span>
|
||||||
|
<span class="node-name">{{ node.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
请选择左侧节点类型
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, computed } from 'vue'
|
||||||
|
import extraNodes from '@/assets/data/extra-nodes.json'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'NodeSelector',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:visible', 'select'],
|
||||||
|
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const selectedCategories = ref<string[]>([])
|
||||||
|
const selectedNodes = ref<any[]>([])
|
||||||
|
|
||||||
|
const currentNodes = computed(() => {
|
||||||
|
if (selectedCategories.value.length === 0) return []
|
||||||
|
return extraNodes.categories
|
||||||
|
.filter(c => selectedCategories.value.includes(c.name))
|
||||||
|
.flatMap(c => c.nodes)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasSelection = computed(() => selectedNodes.value.length > 0)
|
||||||
|
|
||||||
|
const toggleCategory = (name: string) => {
|
||||||
|
const index = selectedCategories.value.indexOf(name)
|
||||||
|
if (index === -1) {
|
||||||
|
selectedCategories.value.push(name)
|
||||||
|
} else {
|
||||||
|
selectedCategories.value.splice(index, 1)
|
||||||
|
// 移除该分类下的所有已选节点
|
||||||
|
const categoryNodes = extraNodes.categories
|
||||||
|
.find(c => c.name === name)?.nodes || []
|
||||||
|
selectedNodes.value = selectedNodes.value.filter(node =>
|
||||||
|
!categoryNodes.some(n => n.type === node.type)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNodeSelected = (node: any) => {
|
||||||
|
return selectedNodes.value.some(n => n.type === node.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNodeSelect = (node: any) => {
|
||||||
|
const index = selectedNodes.value.findIndex(n => n.type === node.type)
|
||||||
|
if (index === -1) {
|
||||||
|
selectedNodes.value.push(node)
|
||||||
|
} else {
|
||||||
|
selectedNodes.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
selectedCategories.value = []
|
||||||
|
selectedNodes.value = []
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const selectedNodesWithData = selectedNodes.value.map(node => ({
|
||||||
|
...node,
|
||||||
|
type: node.type,
|
||||||
|
label: node.name,
|
||||||
|
icon: node.icon,
|
||||||
|
template: node.template
|
||||||
|
}))
|
||||||
|
|
||||||
|
emit('select', selectedNodesWithData)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedCategories,
|
||||||
|
selectedNodes,
|
||||||
|
currentNodes,
|
||||||
|
hasSelection,
|
||||||
|
extraNodes,
|
||||||
|
toggleCategory,
|
||||||
|
isNodeSelected,
|
||||||
|
toggleNodeSelect,
|
||||||
|
handleClose,
|
||||||
|
handleConfirm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nodes-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-dialog {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 800px;
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:disabled {
|
||||||
|
background: #a0cfff;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #909399;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tree {
|
||||||
|
width: 200px;
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-item {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-item:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-item.active {
|
||||||
|
border-color: #409eff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<template>
|
||||||
|
<div class="property-panel">
|
||||||
|
<div class="panel-header"> 属性面板 </div>
|
||||||
|
<div class="panel-content" v-if="selectedNode">
|
||||||
|
<!-- 基础属性 -->
|
||||||
|
<div class="property-section">
|
||||||
|
<div class="section-title">基础属性</div>
|
||||||
|
<el-form label-width="80px" size="small">
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-input v-model="selectedNode.label" @change="updateNodeProperty('label')" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="宽度">
|
||||||
|
<el-input-number v-model="selectedNode.width" @change="updateNodeProperty('width')" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="高度">
|
||||||
|
<el-input-number v-model="selectedNode.height" @change="updateNodeProperty('height')" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 样式属性 -->
|
||||||
|
<div class="property-section">
|
||||||
|
<div class="section-title">样式属性</div>
|
||||||
|
<el-form label-width="80px" size="small">
|
||||||
|
<el-form-item label="填充色">
|
||||||
|
<el-color-picker
|
||||||
|
v-model="selectedNode.fillColor"
|
||||||
|
@change="updateNodeProperty('fillColor')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="边框色">
|
||||||
|
<el-color-picker
|
||||||
|
v-model="selectedNode.strokeColor"
|
||||||
|
@change="updateNodeProperty('strokeColor')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="边框宽度">
|
||||||
|
<el-input-number
|
||||||
|
v-model="selectedNode.strokeWidth"
|
||||||
|
@change="updateNodeProperty('strokeWidth')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文字属性 -->
|
||||||
|
<div class="property-section">
|
||||||
|
<div class="section-title">文字属性</div>
|
||||||
|
<el-form label-width="80px" size="small">
|
||||||
|
<el-form-item label="字体">
|
||||||
|
<el-select v-model="selectedNode.fontFamily" @change="updateNodeProperty('fontFamily')">
|
||||||
|
<el-option label="Arial" value="Arial" />
|
||||||
|
<el-option label="微软雅黑" value="Microsoft YaHei" />
|
||||||
|
<el-option label="宋体" value="SimSun" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字号">
|
||||||
|
<el-input-number
|
||||||
|
v-model="selectedNode.fontSize"
|
||||||
|
@change="updateNodeProperty('fontSize')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字体颜色">
|
||||||
|
<el-color-picker
|
||||||
|
v-model="selectedNode.fontColor"
|
||||||
|
@change="updateNodeProperty('fontColor')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-empty" v-else> 请选择一个节点 </div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { flowEventBus } from '../../events'
|
||||||
|
import type { Cell } from '@maxgraph/core'
|
||||||
|
|
||||||
|
interface NodeProperties {
|
||||||
|
label: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
fillColor: string
|
||||||
|
strokeColor: string
|
||||||
|
strokeWidth: number
|
||||||
|
fontFamily: string
|
||||||
|
fontSize: number
|
||||||
|
fontColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PropertyPanel'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedNode = ref<NodeProperties | null>(null)
|
||||||
|
|
||||||
|
// 监听节点选择事件
|
||||||
|
onMounted(() => {
|
||||||
|
flowEventBus.on('node-selected', (nodes: [Cell]) => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let node: Cell = nodes[0]
|
||||||
|
selectedNode.value = {
|
||||||
|
label: node.getValue() || '',
|
||||||
|
width: node.geometry.width,
|
||||||
|
height: node.geometry.height,
|
||||||
|
fillColor: node.getStyle()['fillColor'] || '#ffffff',
|
||||||
|
strokeColor: node.getStyle()['strokeColor'] || '#000000',
|
||||||
|
strokeWidth: node.getStyle()['strokeWidth'] || 1,
|
||||||
|
fontFamily: node.getStyle()['fontFamily'] || 'Arial',
|
||||||
|
fontSize: node.getStyle()['fontSize'] || 14,
|
||||||
|
fontColor: node.getStyle()['fontColor'] || '#000000'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
flowEventBus.on('node-unselected', () => {
|
||||||
|
selectedNode.value = null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新节点属性
|
||||||
|
const updateNodeProperty = (property: string) => {
|
||||||
|
flowEventBus.emit('update-node-property', {
|
||||||
|
property,
|
||||||
|
value: selectedNode.value[property]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.property-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,379 @@
|
||||||
|
<template>
|
||||||
|
<div class="style-library">
|
||||||
|
<div class="style-list">
|
||||||
|
<div v-for="(style, index) in styles"
|
||||||
|
:key="index"
|
||||||
|
class="style-item"
|
||||||
|
@click="applyStyle(style)">
|
||||||
|
<div class="style-preview" :style="getPreviewStyle(style)">
|
||||||
|
<div class="style-name">{{ style.name }}</div>
|
||||||
|
<div class="preview-node start-node" :style="getStartEndStyle(style)">开始</div>
|
||||||
|
<div class="preview-node process-node" :style="style.nodeStyle">处理</div>
|
||||||
|
<div class="preview-node end-node" :style="getStartEndStyle(style)">结束</div>
|
||||||
|
<svg class="preview-edges" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<marker :id="'arrow-' + index"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
refX="9"
|
||||||
|
refY="5"
|
||||||
|
markerWidth="4"
|
||||||
|
markerHeight="4"
|
||||||
|
orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" :fill="style.edgeStyle.strokeColor"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<path :style="getEdgeStyle(style, index)" d="M56,40 L102,40" />
|
||||||
|
<path :style="getEdgeStyle(style, index)" d="M140,40 L194,40" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { flowEventBus } from '../../events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StyleLibrary',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
styles: [
|
||||||
|
{
|
||||||
|
name: '经典蓝',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#e3f2fd',
|
||||||
|
borderColor: '#2196f3',
|
||||||
|
color: '#1976d2'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#2196f3',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '商务灰',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
borderColor: '#9e9e9e',
|
||||||
|
color: '#616161'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#9e9e9e',
|
||||||
|
strokeWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '活力橙',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#fff3e0',
|
||||||
|
borderColor: '#ff9800',
|
||||||
|
color: '#f57c00'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#ff9800',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '清新绿',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#e8f5e9',
|
||||||
|
borderColor: '#4caf50',
|
||||||
|
color: '#2e7d32'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#4caf50',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '科技紫',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#f3e5f5',
|
||||||
|
borderColor: '#9c27b0',
|
||||||
|
color: '#6a1b9a'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#9c27b0',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '暗夜黑',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#424242',
|
||||||
|
borderColor: '#212121',
|
||||||
|
color: '#ffffff'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#757575',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '珊瑚红',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#ffebee',
|
||||||
|
borderColor: '#f44336',
|
||||||
|
color: '#c62828'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#f44336',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '海洋蓝',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#e0f7fa',
|
||||||
|
borderColor: '#00bcd4',
|
||||||
|
color: '#006064'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#00bcd4',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '柔和粉',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#fce4ec',
|
||||||
|
borderColor: '#e91e63',
|
||||||
|
color: '#ad1457'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#e91e63',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '极光绿',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#e8f5e9',
|
||||||
|
borderColor: '#4caf50',
|
||||||
|
color: '#2e7d32'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#9c27b0',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '日落橙',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#fff3e0',
|
||||||
|
borderColor: '#ff9800',
|
||||||
|
color: '#e65100'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#2196f3',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '深海蓝',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#e3f2fd',
|
||||||
|
borderColor: '#1976d2',
|
||||||
|
color: '#0d47a1'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#4caf50',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '霓虹紫',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#f3e5f5',
|
||||||
|
borderColor: '#9c27b0',
|
||||||
|
color: '#4a148c'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#ff9800',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '薄暮红',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#ffebee',
|
||||||
|
borderColor: '#f44336',
|
||||||
|
color: '#b71c1c'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#00bcd4',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '青玉白',
|
||||||
|
nodeStyle: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#00bcd4',
|
||||||
|
color: '#006064'
|
||||||
|
},
|
||||||
|
edgeStyle: {
|
||||||
|
strokeColor: '#ff4081',
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
applyStyle(style) {
|
||||||
|
flowEventBus.emit('apply-graph-style', style)
|
||||||
|
},
|
||||||
|
getPreviewStyle(style) {
|
||||||
|
return {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStartEndStyle(style) {
|
||||||
|
return {
|
||||||
|
...style.nodeStyle,
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
padding: '0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '12px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getEdgeStyle(style, index) {
|
||||||
|
return {
|
||||||
|
stroke: style.edgeStyle.strokeColor,
|
||||||
|
strokeWidth: style.edgeStyle.strokeWidth,
|
||||||
|
fill: 'none',
|
||||||
|
markerEnd: `url(#arrow-${index})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.style-library {
|
||||||
|
position: absolute; /* 添加绝对定位 */
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 280px; /* 设置固定宽度 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-left: 1px solid #e4e7ed; /* 添加左边框 */
|
||||||
|
z-index: 100; /* 确保在其他元素上层 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100%; /* 确保高度填满 */
|
||||||
|
box-sizing: border-box; /* 确保padding不会增加实际高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-item {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background-color: #fff; /* 添加白色背景 */
|
||||||
|
border-radius: 4px; /* 添加圆角 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-preview {
|
||||||
|
height: 60px;
|
||||||
|
position: relative;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-name {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9); /* 半透明背景 */
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-node {
|
||||||
|
position: relative;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 40px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-node {
|
||||||
|
margin-left: 20px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-node {
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-node {
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-edges {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 */
|
||||||
|
.style-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:modelValue="visible"
|
||||||
|
@update:modelValue="$emit('update:visible', $event)"
|
||||||
|
title="编辑表格"
|
||||||
|
width="800px"
|
||||||
|
:before-close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="table-editor">
|
||||||
|
<!-- 表格配置 -->
|
||||||
|
<div class="table-config">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button @click="addRow">添加行</el-button>
|
||||||
|
<el-button @click="addColumn">添加列</el-button>
|
||||||
|
<el-button @click="deleteRow" :disabled="!selectedCell.row">删除行</el-button>
|
||||||
|
<el-button @click="deleteColumn" :disabled="!selectedCell.col">删除列</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格预览和编辑 -->
|
||||||
|
<div class="table-preview">
|
||||||
|
<table class="editable-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(col, index) in tableData.columns"
|
||||||
|
:key="index"
|
||||||
|
@click="selectColumn(index)"
|
||||||
|
:class="{ selected: selectedCell.col === index }">
|
||||||
|
<el-input v-model="col.title" size="small" @change="updateTable" />
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, rowIndex) in tableData.data"
|
||||||
|
:key="rowIndex">
|
||||||
|
<td v-for="(col, colIndex) in tableData.columns"
|
||||||
|
:key="colIndex"
|
||||||
|
@click="selectCell(rowIndex, colIndex)"
|
||||||
|
:class="{ selected: selectedCell.row === rowIndex && selectedCell.col === colIndex }">
|
||||||
|
<el-input v-model="row[col.key]" size="small" @change="updateTable" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">确认</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TableEditor',
|
||||||
|
props: {
|
||||||
|
visible: Boolean,
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
columns: [],
|
||||||
|
data: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:visible', 'update:value', 'confirm'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tableData: {
|
||||||
|
columns: [],
|
||||||
|
data: []
|
||||||
|
},
|
||||||
|
selectedCell: {
|
||||||
|
row: null,
|
||||||
|
col: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
initialData: {
|
||||||
|
handler(val) {
|
||||||
|
this.tableData = JSON.parse(JSON.stringify(val))
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addRow() {
|
||||||
|
const newRow = {}
|
||||||
|
this.tableData.columns.forEach(col => {
|
||||||
|
newRow[col.key] = ''
|
||||||
|
})
|
||||||
|
this.tableData.data.push(newRow)
|
||||||
|
this.updateTable()
|
||||||
|
},
|
||||||
|
addColumn() {
|
||||||
|
const columnIndex = this.tableData.columns.length + 1
|
||||||
|
const key = `col${columnIndex}`
|
||||||
|
this.tableData.columns.push({
|
||||||
|
title: `列${columnIndex}`,
|
||||||
|
key,
|
||||||
|
width: 100
|
||||||
|
})
|
||||||
|
this.tableData.data.forEach(row => {
|
||||||
|
row[key] = ''
|
||||||
|
})
|
||||||
|
this.updateTable()
|
||||||
|
},
|
||||||
|
deleteRow() {
|
||||||
|
if (this.selectedCell.row !== null) {
|
||||||
|
this.tableData.data.splice(this.selectedCell.row, 1)
|
||||||
|
this.selectedCell.row = null
|
||||||
|
this.updateTable()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteColumn() {
|
||||||
|
if (this.selectedCell.col !== null) {
|
||||||
|
const key = this.tableData.columns[this.selectedCell.col].key
|
||||||
|
this.tableData.columns.splice(this.selectedCell.col, 1)
|
||||||
|
this.tableData.data.forEach(row => {
|
||||||
|
delete row[key]
|
||||||
|
})
|
||||||
|
this.selectedCell.col = null
|
||||||
|
this.updateTable()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectCell(row, col) {
|
||||||
|
this.selectedCell = { row, col }
|
||||||
|
},
|
||||||
|
selectColumn(col) {
|
||||||
|
this.selectedCell = { row: null, col }
|
||||||
|
},
|
||||||
|
updateTable() {
|
||||||
|
this.generateTableHtml()
|
||||||
|
},
|
||||||
|
generateTableHtml() {
|
||||||
|
const html = `
|
||||||
|
<table style="width:100%; border-collapse: collapse; font-size: 12px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
${this.tableData.columns.map(col =>
|
||||||
|
`<th style="border: 1px solid #333; padding: 4px; background-color: #f5f5f5;">${col.title}</th>`
|
||||||
|
).join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.tableData.data.map(row =>
|
||||||
|
`<tr>${this.tableData.columns.map(col =>
|
||||||
|
`<td style="border: 1px solid #333; padding: 4px;">${row[col.key]}</td>`
|
||||||
|
).join('')}</tr>`
|
||||||
|
).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
|
this.$emit('update:value', html)
|
||||||
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
},
|
||||||
|
handleConfirm() {
|
||||||
|
this.$emit('confirm', {
|
||||||
|
html: this.generateTableHtml(),
|
||||||
|
config: this.tableData
|
||||||
|
})
|
||||||
|
this.handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-editor {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-config {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-table th,
|
||||||
|
.editable-table td {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-table th {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-table .selected {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export const MenuIcons = {
|
||||||
|
delete: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`,
|
||||||
|
|
||||||
|
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`,
|
||||||
|
|
||||||
|
layers: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"/></svg>`,
|
||||||
|
|
||||||
|
moveUp: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>`,
|
||||||
|
|
||||||
|
moveDown: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>`,
|
||||||
|
|
||||||
|
group: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M3 5v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.11 0-2 .9-2 2zm12 4c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3zm-9 8c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6v-1z"/></svg>`,
|
||||||
|
|
||||||
|
ungroup: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>`,
|
||||||
|
|
||||||
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>`,
|
||||||
|
|
||||||
|
permission: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>`,
|
||||||
|
|
||||||
|
edit: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>`,
|
||||||
|
|
||||||
|
color: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
image: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||||
|
</svg>`
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import mitt from 'mitt'
|
||||||
|
|
||||||
|
type Events = {
|
||||||
|
[K in string]: unknown
|
||||||
|
} & {
|
||||||
|
zoom: number
|
||||||
|
fit: void
|
||||||
|
delete: void
|
||||||
|
export: void
|
||||||
|
undo: void
|
||||||
|
reDo: void
|
||||||
|
'edge-style': string
|
||||||
|
'auto-layout': void
|
||||||
|
'change-background': string
|
||||||
|
'toggle-grid': boolean
|
||||||
|
'change-layout': string
|
||||||
|
'change-font': string
|
||||||
|
'update-text-style': any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flowEventBus = mitt<Events>()
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ConstraintHandler } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class CustomConstraintHandler extends ConstraintHandler {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { RubberBandHandler } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class CustomRubberBandHandler extends RubberBandHandler {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { Graph, PopupMenuHandler } from "@maxgraph/core";
|
||||||
|
import { MenuIcons } from '../constants/menuIcons';
|
||||||
|
|
||||||
|
export class MyCustomPopupMenuHandler extends PopupMenuHandler {
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
super(graph);
|
||||||
|
// Configures automatic expand on mouseover
|
||||||
|
this.autoExpand = true; // TODO autoExpand is not working
|
||||||
|
|
||||||
|
this.factoryMethod = (menu, cell, evt) => {
|
||||||
|
// 只对节点显示菜单
|
||||||
|
if (cell && cell.isVertex()) {
|
||||||
|
menu.addItem('删除节点', null, () => {
|
||||||
|
this.graph.removeCells([cell]);
|
||||||
|
}, null, MenuIcons.delete);
|
||||||
|
|
||||||
|
menu.addItem('复制节点', null, () => {
|
||||||
|
const clone = this.graph.cloneCells([cell])[0];
|
||||||
|
const geo = clone.getGeometry();
|
||||||
|
geo.x += 50;
|
||||||
|
geo.y += 50;
|
||||||
|
this.graph.addCell(clone, this.graph.getDefaultParent(), null, null, null);
|
||||||
|
}, null, MenuIcons.copy);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 层级操作
|
||||||
|
const layerSubmenu = menu.addItem('层级操作', null, null, null, MenuIcons.layers);
|
||||||
|
menu.addItem('置于顶层', null, () => {
|
||||||
|
this.graph.moveCells([cell], 0, 0, true); // true 表示置于顶层
|
||||||
|
}, layerSubmenu, MenuIcons.moveUp);
|
||||||
|
menu.addItem('置于底层', null, () => {
|
||||||
|
this.graph.moveCells([cell], 0, 0, false); // false 表示置于底层
|
||||||
|
}, layerSubmenu, MenuIcons.moveDown);
|
||||||
|
menu.addItem('上移一层', null, () => {
|
||||||
|
this.graph.orderCells(true, [cell]); // true 表示下移一层
|
||||||
|
}, layerSubmenu, MenuIcons.moveUp);
|
||||||
|
menu.addItem('下移一层', null, () => {
|
||||||
|
this.graph.orderCells(false, [cell]); // true 表示下移一层
|
||||||
|
}, layerSubmenu, MenuIcons.moveDown);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 组合操作
|
||||||
|
const groupSubmenu = menu.addItem('组合操作', null, null, null, MenuIcons.group);
|
||||||
|
menu.addItem('组合', null, () => {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells.length > 1) {
|
||||||
|
this.graph.groupCells(null, 0, cells);
|
||||||
|
}
|
||||||
|
}, groupSubmenu, MenuIcons.group);
|
||||||
|
menu.addItem('解除组合', null, () => {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
this.graph.ungroupCells(cells);
|
||||||
|
}, groupSubmenu, MenuIcons.ungroup);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 锁定操作
|
||||||
|
// menu.addItem(cell.getAttribute('locked') ? '解除锁定' : '锁定', null, () => {
|
||||||
|
// this.graph.toggleCellStyles('locked', true, [cell]);
|
||||||
|
// }, null, cell.getAttribute('locked') ? '🔓' : '🔒');
|
||||||
|
|
||||||
|
// 权限操作
|
||||||
|
const permissionSubmenu = menu.addItem('权限设置', null, null, null, MenuIcons.lock);
|
||||||
|
menu.addItem('只读', null, () => {
|
||||||
|
cell.setAttribute('permission', 'readonly');
|
||||||
|
}, permissionSubmenu, MenuIcons.lock);
|
||||||
|
menu.addItem('可编辑', null, () => {
|
||||||
|
cell.setAttribute('permission', 'editable');
|
||||||
|
}, permissionSubmenu, MenuIcons.permission);
|
||||||
|
menu.addItem('完全控制', null, () => {
|
||||||
|
cell.setAttribute('permission', 'full');
|
||||||
|
}, permissionSubmenu, MenuIcons.permission);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 编辑操作
|
||||||
|
const editSubmenu = menu.addItem('编辑', null, null, null, MenuIcons.edit);
|
||||||
|
menu.addItem('修改文本', null, () => {
|
||||||
|
// 触发文本编辑
|
||||||
|
this.graph.startEditingAtCell(cell);
|
||||||
|
}, editSubmenu, MenuIcons.edit);
|
||||||
|
|
||||||
|
menu.addItem('修改颜色', null, () => {
|
||||||
|
// 这里可以触发颜色选择事件
|
||||||
|
console.log('修改颜色');
|
||||||
|
}, editSubmenu, MenuIcons.color);
|
||||||
|
|
||||||
|
// 在现有菜单项中添加插入图片选项
|
||||||
|
menu.addItem('插入图片', null, () => {
|
||||||
|
// 创建一个隐藏的文件输入框
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.style.display = 'none';
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
input.onchange = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const imageUrl = event.target?.result as string;
|
||||||
|
// 在当前位置插入图片
|
||||||
|
const geo = cell.getGeometry().clone();
|
||||||
|
const imageCell = this.graph.insertVertex(
|
||||||
|
this.graph.getDefaultParent(),
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
geo.x + 50,
|
||||||
|
geo.y + 50,
|
||||||
|
100, // 默认宽度
|
||||||
|
100, // 默认高度
|
||||||
|
{
|
||||||
|
shape: 'image',
|
||||||
|
image: imageUrl,
|
||||||
|
imageAlign: 'center',
|
||||||
|
imageVerticalAlign: 'middle',
|
||||||
|
resizable: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.graph.setSelectionCell(imageCell);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
document.body.removeChild(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}, null, MenuIcons.image);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重写创建菜单项的方法来添加自定义样式
|
||||||
|
protected createMenuItem(title: string, image: string | null, funct: (() => void) | null, parent: any, icon?: string): HTMLElement {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'custom-menu-item';
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
const iconSpan = document.createElement('span');
|
||||||
|
iconSpan.className = 'menu-icon';
|
||||||
|
iconSpan.innerHTML = icon;
|
||||||
|
item.appendChild(iconSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelSpan = document.createElement('span');
|
||||||
|
labelSpan.className = 'menu-label';
|
||||||
|
labelSpan.textContent = title;
|
||||||
|
item.appendChild(labelSpan);
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
item.classList.add('has-submenu');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funct) {
|
||||||
|
item.onclick = funct;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ConnectionHandler, Graph } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class WuConnectionHandler extends ConnectionHandler {
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
console.log("WuConnectionHandler constructor");
|
||||||
|
super(graph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Geometry, ConnectionConstraint, Point } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class WuCustomGeometryClass extends Geometry {
|
||||||
|
// Defines the default constraints for the vertices
|
||||||
|
constraints = [new ConnectionConstraint(new Point(0.25, 0), true), new ConnectionConstraint(new Point(0.5, 0), true), new ConnectionConstraint(new Point(0.75, 0), true), new ConnectionConstraint(new Point(0, 0.25), true), new ConnectionConstraint(new Point(0, 0.5), true), new ConnectionConstraint(new Point(0, 0.75), true), new ConnectionConstraint(new Point(1, 0.25), true), new ConnectionConstraint(new Point(1, 0.5), true), new ConnectionConstraint(new Point(1, 0.75), true), new ConnectionConstraint(new Point(0.25, 1), true), new ConnectionConstraint(new Point(0.5, 1), true), new ConnectionConstraint(new Point(0.75, 1), true)];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
import { Graph, GraphDataModel, CellState, GraphPluginConstructor, InternalEvent, Cell, RectangleShape } from "@maxgraph/core";
|
||||||
|
import { WuCustomGeometryClass } from "./WuCustomGeometryClass";
|
||||||
|
import { WuConnectionHandler } from "./WuConnectionHandler";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { flowEventBus } from '../events';
|
||||||
|
import { CustomPopupMenuHandler } from '../handlers/CustomPopupMenuHandler';
|
||||||
|
import { KeyHandler } from '../handlers/KeyHandler';
|
||||||
|
import { WuConstraintHandler } from "../handlers/WuConstraintHandler";
|
||||||
|
import { Codec, utils, ModelXmlSerializer } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class WuCustomGraph extends Graph {
|
||||||
|
private copiedStyle: any = null;
|
||||||
|
private isStyleBrushActive = false;
|
||||||
|
private keyHandler: KeyHandler;
|
||||||
|
|
||||||
|
constructor(container: HTMLElement, model: GraphDataModel, plugins: GraphPluginConstructor[]) {
|
||||||
|
super(container, model, plugins);
|
||||||
|
this.initDoubleClickHandler();
|
||||||
|
this.initClickHandler();
|
||||||
|
this.initStyleBrush();
|
||||||
|
this.initEdgeAnimation();
|
||||||
|
this.initConnectorShape();
|
||||||
|
this.initPopupMenu();
|
||||||
|
this.keyHandler = new KeyHandler(this);
|
||||||
|
|
||||||
|
// // 添加选中状态变化监听
|
||||||
|
this.addListener(InternalEvent.CLICK, (sender, evt) => {
|
||||||
|
const cells = this.getSelectionCells();
|
||||||
|
flowEventBus.emit('selection-changed', cells);
|
||||||
|
console.log('select....')
|
||||||
|
flowEventBus.emit('node-selected', cells);
|
||||||
|
});
|
||||||
|
|
||||||
|
flowEventBus.on('apply-graph-style', (style: any) => {
|
||||||
|
this.applyGraphStyle(style);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initTableEditor();
|
||||||
|
|
||||||
|
// 注册表格形状
|
||||||
|
this.getStylesheet().putCellStyle('table', {
|
||||||
|
shape: 'html',
|
||||||
|
whiteSpace: 'wrap',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
align: 'left',
|
||||||
|
overflow: 'visible'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听对齐事件
|
||||||
|
flowEventBus.on('align-cells', (type: string) => {
|
||||||
|
this.alignCells(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initDoubleClickHandler() {
|
||||||
|
// 监听双击事件
|
||||||
|
this.addListener(InternalEvent.CLICK, () => {
|
||||||
|
flowEventBus.emit('hide-text-toolbar');
|
||||||
|
});
|
||||||
|
this.addListener(InternalEvent.DOUBLE_CLICK, (sender: any, evt: any) => {
|
||||||
|
flowEventBus.emit('hide-text-toolbar');
|
||||||
|
const cell = evt.getProperty('cell');
|
||||||
|
if (cell && cell.isVertex()) {
|
||||||
|
// 获取节点的值(标签)
|
||||||
|
const label = cell.getValue() || '未命名节点';
|
||||||
|
// 显示提示信息
|
||||||
|
ElMessage({
|
||||||
|
message: `双击了节点: ${label}`,
|
||||||
|
type: 'info',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initClickHandler() {
|
||||||
|
this.addListener(InternalEvent.DOUBLE_CLICK, (sender: any, evt: any) => {
|
||||||
|
// 先隐藏工具栏
|
||||||
|
flowEventBus.emit('hide-text-toolbar');
|
||||||
|
const cell = evt.getProperty('cell');
|
||||||
|
if (cell && cell.isVertex()) {
|
||||||
|
// 获取点击位置
|
||||||
|
const x = evt.getProperty('event').clientX;
|
||||||
|
const y = evt.getProperty('event').clientY;
|
||||||
|
|
||||||
|
// 获取当前节点的样式
|
||||||
|
const style = cell.getStyle();
|
||||||
|
|
||||||
|
// 延迟一下再显示工具栏,避免闪烁
|
||||||
|
setTimeout(() => {
|
||||||
|
flowEventBus.emit('show-text-toolbar', {
|
||||||
|
position: { left: `${x}px`, top: `${y + 20}px` },
|
||||||
|
cell,
|
||||||
|
style: {
|
||||||
|
fontFamily: style.fontFamily || 'Arial',
|
||||||
|
fontSize: style.fontSize || 14,
|
||||||
|
fontColor: style.fontColor || '#000000',
|
||||||
|
fontWeight: style.fontWeight || 'normal',
|
||||||
|
textDecoration: style.textDecoration || 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initStyleBrush() {
|
||||||
|
flowEventBus.on('activate-style-brush', (style: any) => {
|
||||||
|
this.isStyleBrushActive = true;
|
||||||
|
this.copiedStyle = style;
|
||||||
|
});
|
||||||
|
|
||||||
|
flowEventBus.on('deactivate-style-brush', () => {
|
||||||
|
this.isStyleBrushActive = false;
|
||||||
|
this.copiedStyle = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addListener(InternalEvent.CLICK, (sender: any, evt: any) => {
|
||||||
|
if (this.isStyleBrushActive && this.copiedStyle) {
|
||||||
|
const cell = evt.getProperty('cell');
|
||||||
|
if (cell && cell.isVertex()) {
|
||||||
|
// 应用复制的样式
|
||||||
|
flowEventBus.emit('update-text-style', this.copiedStyle);
|
||||||
|
// 通知样式已应用
|
||||||
|
flowEventBus.emit('style-brush-applied');
|
||||||
|
this.isStyleBrushActive = false;
|
||||||
|
this.copiedStyle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private initEdgeAnimation() {
|
||||||
|
this.addListener(InternalEvent.CLICK, (sender: any, evt: any) => {
|
||||||
|
const cell = evt.getProperty('cell');
|
||||||
|
if (cell && cell.isEdge()) {
|
||||||
|
const state = this.view.getState(cell);
|
||||||
|
if (state) {
|
||||||
|
// 创建动画效果
|
||||||
|
const animate = () => {
|
||||||
|
state.shape.node.style.strokeDasharray = '5,5';
|
||||||
|
state.shape.node.style.animation = 'flowLine 1s linear infinite';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加动画样式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes flowLine {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 10;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// 应用动画
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// 3秒后移除动画
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state?.shape?.node) {
|
||||||
|
state.shape.node.style.strokeDasharray = '';
|
||||||
|
state.shape.node.style.animation = '';
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initConnectorShape() {
|
||||||
|
this.getStylesheet().putCellStyle('connector', {
|
||||||
|
shape: 'ellipse',
|
||||||
|
perimeter: 'ellipsePerimeter',
|
||||||
|
fillColor: '#fff',
|
||||||
|
strokeColor: '#5b8ffa',
|
||||||
|
strokeWidth: 2,
|
||||||
|
image: `data:image/svg+xml,${encodeURIComponent(`
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="white" stroke="#5b8ffa" stroke-width="2"/>
|
||||||
|
<circle cx="12" cy="12" r="4" fill="#5b8ffa"/>
|
||||||
|
</svg>
|
||||||
|
`)}`,
|
||||||
|
imageWidth: 24,
|
||||||
|
imageHeight: 24,
|
||||||
|
imageAlign: 'center',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initPopupMenu() {
|
||||||
|
new CustomPopupMenuHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initTableEditor() {
|
||||||
|
this.addListener(InternalEvent.DOUBLE_CLICK, (sender: any, evt: any) => {
|
||||||
|
const cell = evt.getProperty('cell');
|
||||||
|
if (cell && cell.isVertex() && cell.getStyle()['shape'] === 'table') {
|
||||||
|
const tableConfig = cell.getStyle()['tableConfig'];
|
||||||
|
flowEventBus.emit('edit-table', {
|
||||||
|
cell,
|
||||||
|
config: tableConfig
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
flowEventBus.on('table-updated', ({ cell, html, config }) => {
|
||||||
|
this.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
const style = { ...cell.getStyle() };
|
||||||
|
style.value = html;
|
||||||
|
style.tableConfig = config;
|
||||||
|
this.setCellStyle(style, cell);
|
||||||
|
} finally {
|
||||||
|
this.model.endUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllConnectionConstraints = (terminal: CellState | null, _source: boolean) => {
|
||||||
|
// Overridden to define per-geometry connection points
|
||||||
|
return (terminal?.cell?.geometry as WuCustomGeometryClass)?.constraints ?? null;
|
||||||
|
};
|
||||||
|
createConnectionHandler() {
|
||||||
|
const r = new WuConnectionHandler(this);
|
||||||
|
r.constraintHandler = new WuConstraintHandler(this);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStyleApply({ style, cells }: { style: any, cells: any[] | null }) {
|
||||||
|
const model = this.model;
|
||||||
|
model.beginUpdate();
|
||||||
|
console.log(model.cells);
|
||||||
|
try {
|
||||||
|
if (cells) {
|
||||||
|
// 应用到指定的节点
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
// 应用节点样式
|
||||||
|
this.setCellStyle({
|
||||||
|
fillColor: style.nodeStyle.backgroundColor,
|
||||||
|
strokeColor: style.nodeStyle.borderColor,
|
||||||
|
fontColor: style.nodeStyle.color
|
||||||
|
}, cell);
|
||||||
|
} else if (cell.isEdge()) {
|
||||||
|
// 应用边的样式
|
||||||
|
this.setCellStyle({
|
||||||
|
strokeColor: style.edgeStyle.strokeColor,
|
||||||
|
strokeWidth: style.edgeStyle.strokeWidth
|
||||||
|
}, cell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 应用到所有节点
|
||||||
|
const allCells = this.model.cells
|
||||||
|
Object.values(allCells).forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
this.setCellStyle({
|
||||||
|
fillColor: style.nodeStyle.backgroundColor,
|
||||||
|
strokeColor: style.nodeStyle.borderColor,
|
||||||
|
fontColor: style.nodeStyle.color
|
||||||
|
}, [cell]);
|
||||||
|
} else if (cell.isEdge()) {
|
||||||
|
this.setCellStyle({
|
||||||
|
strokeColor: style.edgeStyle.strokeColor,
|
||||||
|
strokeWidth: style.edgeStyle.strokeWidth
|
||||||
|
}, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyGraphStyle(style: any) {
|
||||||
|
let cells = this.getSelectionCells();
|
||||||
|
console.log(cells);
|
||||||
|
if (cells.length === 0) {
|
||||||
|
cells = Object.values(this.model.cells);
|
||||||
|
}
|
||||||
|
if (!cells.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const newStyle = {
|
||||||
|
...cell.getStyle(),
|
||||||
|
fillColor: style.nodeStyle.backgroundColor,
|
||||||
|
strokeColor: style.nodeStyle.borderColor,
|
||||||
|
fontColor: style.nodeStyle.color
|
||||||
|
};
|
||||||
|
this.setCellStyle(newStyle, [cell]);
|
||||||
|
} else if (cell.isEdge()) {
|
||||||
|
const newStyle = {
|
||||||
|
...cell.getStyle(),
|
||||||
|
strokeColor: style.edgeStyle.strokeColor,
|
||||||
|
strokeWidth: style.edgeStyle.strokeWidth
|
||||||
|
};
|
||||||
|
this.setCellStyle(newStyle, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphData() {
|
||||||
|
|
||||||
|
const serializer = new ModelXmlSerializer(this.model)
|
||||||
|
return serializer.export()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGraphData(data: string) {
|
||||||
|
const model = new GraphDataModel();
|
||||||
|
const serializer = new ModelXmlSerializer(model);
|
||||||
|
serializer.import(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphSvg() {
|
||||||
|
const scale = 1
|
||||||
|
const bounds = this.getGraphBounds()
|
||||||
|
|
||||||
|
return `<svg width="${bounds.width * scale}" height="${bounds.height * scale}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
${this.container.innerHTML}
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对齐功能
|
||||||
|
alignCells = (type: string) => {
|
||||||
|
const cells = this.getSelectionCells();
|
||||||
|
if (cells.length < 2) return;
|
||||||
|
|
||||||
|
this.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
const bounds = cells.map(cell => this.getBoundingBox([cell]));
|
||||||
|
const first = bounds[0];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'left':
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], first.x - bound.x, 0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'center':
|
||||||
|
const centerX = first.x + first.width / 2;
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], centerX - (bound.x + bound.width / 2), 0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], (first.x + first.width) - (bound.x + bound.width), 0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], 0, first.y - bound.y);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'middle':
|
||||||
|
const centerY = first.y + first.height / 2;
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], 0, centerY - (bound.y + bound.height / 2));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
bounds.slice(1).forEach((bound, i) => {
|
||||||
|
this.moveCells([cells[i + 1]], 0, (first.y + first.height) - (bound.y + bound.height));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'distributeHorizontally':
|
||||||
|
const sortedX = [...cells].sort((a, b) => this.getBoundingBox([a]).x - this.getBoundingBox([b]).x);
|
||||||
|
const totalWidth = this.getBoundingBox([sortedX[sortedX.length - 1]]).x - this.getBoundingBox([sortedX[0]]).x;
|
||||||
|
const spacing = totalWidth / (cells.length - 1);
|
||||||
|
sortedX.slice(1, -1).forEach((cell, i) => {
|
||||||
|
const targetX = this.getBoundingBox([sortedX[0]]).x + spacing * (i + 1);
|
||||||
|
const currentX = this.getBoundingBox([cell]).x;
|
||||||
|
this.moveCells([cell], targetX - currentX, 0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'distributeVertically':
|
||||||
|
const sortedY = [...cells].sort((a, b) => this.getBoundingBox([a]).y - this.getBoundingBox([b]).y);
|
||||||
|
const totalHeight = this.getBoundingBox([sortedY[sortedY.length - 1]]).y - this.getBoundingBox([sortedY[0]]).y;
|
||||||
|
const spacingY = totalHeight / (cells.length - 1);
|
||||||
|
sortedY.slice(1, -1).forEach((cell, i) => {
|
||||||
|
const targetY = this.getBoundingBox([sortedY[0]]).y + spacingY * (i + 1);
|
||||||
|
const currentY = this.getBoundingBox([cell]).y;
|
||||||
|
this.moveCells([cell], 0, targetY - currentY);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { SelectionHandler, Graph, Cell, InternalMouseEvent } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class WuSelectHander extends SelectionHandler {
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
console.log("WuSelectHander constructor");
|
||||||
|
super(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCellForEvent(cell: Cell, me: InternalMouseEvent): Cell {
|
||||||
|
console.log("WuSelectHander selectCellForEvent");
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,414 @@
|
||||||
|
import { Graph,ModelXmlSerializer} from "@maxgraph/core";
|
||||||
|
import { flowEventBus } from "../events";
|
||||||
|
import { WuFlowAction } from "../utils/flow-utils";
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { HierarchicalLayout, CircleLayout, FastOrganicLayout } from "@maxgraph/core";
|
||||||
|
|
||||||
|
export class CanvasEventHandler {
|
||||||
|
private showGrid = ref(true);
|
||||||
|
private showTextToolbar = ref(false);
|
||||||
|
private toolbarPosition = ref({ top: '0px', left: '0px' });
|
||||||
|
private currentNodeStyle = ref({
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fontSize: 14,
|
||||||
|
fontColor: '#000000',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
textDecoration: 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private wuFlowAction: WuFlowAction,
|
||||||
|
private graph: Graph | null,
|
||||||
|
private canvasRef: HTMLElement | undefined
|
||||||
|
) {
|
||||||
|
this.initEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initEvents() {
|
||||||
|
// 注册事件监听
|
||||||
|
flowEventBus.on("zoom", this.handleZoom);
|
||||||
|
flowEventBus.on("fit", this.handleFit);
|
||||||
|
flowEventBus.on("delete", this.handleDelete);
|
||||||
|
flowEventBus.on("export", this.handleExport);
|
||||||
|
flowEventBus.on("undo", this.handleUndo);
|
||||||
|
flowEventBus.on("reDo", this.handleReDo);
|
||||||
|
flowEventBus.on("edge-style", this.handleEdgeStyle);
|
||||||
|
flowEventBus.on("auto-layout", this.handleAutoLayout);
|
||||||
|
flowEventBus.on('change-background', this.handleBackgroundChange);
|
||||||
|
flowEventBus.on('toggle-grid', this.handleToggleGrid);
|
||||||
|
flowEventBus.on('change-layout', this.handleLayoutChange);
|
||||||
|
flowEventBus.on('change-font', this.handleFontChange);
|
||||||
|
flowEventBus.on('change-node-color', this.handleNodeColorChange);
|
||||||
|
flowEventBus.on('insert-image', this.handleInsertImage);
|
||||||
|
flowEventBus.on('change-border-color', this.handleBorderColor);
|
||||||
|
flowEventBus.on('change-border-style', this.handleBorderStyle);
|
||||||
|
flowEventBus.on('change-border-width', this.handleBorderWidth);
|
||||||
|
flowEventBus.on('file-new', this.handleNewFile);
|
||||||
|
flowEventBus.on('file-open', this.handleOpenFile);
|
||||||
|
flowEventBus.on('file-save', this.handleSaveFile);
|
||||||
|
flowEventBus.on('file-export-image', this.handleExportImage);
|
||||||
|
flowEventBus.on('file-export-json', this.handleExportJson);
|
||||||
|
flowEventBus.on('cut', this.handleCut);
|
||||||
|
flowEventBus.on('copy', this.handleCopy);
|
||||||
|
flowEventBus.on('paste', this.handlePaste);
|
||||||
|
flowEventBus.on('select-all', this.handleSelectAll);
|
||||||
|
flowEventBus.on('invert-select', this.handleInvertSelect);
|
||||||
|
flowEventBus.on('cancel-select', this.handleCancelSelect);
|
||||||
|
flowEventBus.on('zoom-in', this.handleZoomIn);
|
||||||
|
flowEventBus.on('zoom-out', this.handleZoomOut);
|
||||||
|
flowEventBus.on('toggle-grid', this.handleToggleGrid);
|
||||||
|
flowEventBus.on('toggle-minimap', this.handleToggleMinimap);
|
||||||
|
flowEventBus.on('change-background-color', this.handleBackgroundColor);
|
||||||
|
flowEventBus.on('change-grid-size', this.handleGridSize);
|
||||||
|
flowEventBus.on('change-canvas-size', this.handleCanvasSize);
|
||||||
|
flowEventBus.on('hierarchical-layout', this.handleHierarchicalLayout);
|
||||||
|
flowEventBus.on('circle-layout', this.handleCircleLayout);
|
||||||
|
flowEventBus.on('organic-layout', this.handleOrganicLayout);
|
||||||
|
flowEventBus.on('bring-to-front', this.handleBringToFront);
|
||||||
|
flowEventBus.on('send-to-back', this.handleSendToBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
// 清理所有事件监听
|
||||||
|
flowEventBus.off("zoom", this.handleZoom);
|
||||||
|
flowEventBus.off("fit", this.handleFit);
|
||||||
|
flowEventBus.off("delete", this.handleDelete);
|
||||||
|
flowEventBus.off("export", this.handleExport);
|
||||||
|
flowEventBus.off("undo", this.handleUndo);
|
||||||
|
flowEventBus.off("reDo", this.handleReDo);
|
||||||
|
flowEventBus.off("edge-style", this.handleEdgeStyle);
|
||||||
|
flowEventBus.off("auto-layout", this.handleAutoLayout);
|
||||||
|
flowEventBus.off('change-background', this.handleBackgroundChange);
|
||||||
|
flowEventBus.off('toggle-grid', this.handleToggleGrid);
|
||||||
|
flowEventBus.off('change-layout', this.handleLayoutChange);
|
||||||
|
flowEventBus.off('change-font', this.handleFontChange);
|
||||||
|
flowEventBus.off('change-node-color', this.handleNodeColorChange);
|
||||||
|
flowEventBus.off('insert-image', this.handleInsertImage);
|
||||||
|
flowEventBus.off('change-border-color', this.handleBorderColor);
|
||||||
|
flowEventBus.off('change-border-style', this.handleBorderStyle);
|
||||||
|
flowEventBus.off('change-border-width', this.handleBorderWidth);
|
||||||
|
flowEventBus.off('file-new', this.handleNewFile);
|
||||||
|
flowEventBus.off('file-open', this.handleOpenFile);
|
||||||
|
flowEventBus.off('file-save', this.handleSaveFile);
|
||||||
|
flowEventBus.off('file-export-image', this.handleExportImage);
|
||||||
|
flowEventBus.off('file-export-json', this.handleExportJson);
|
||||||
|
flowEventBus.off('cut', this.handleCut);
|
||||||
|
flowEventBus.off('copy', this.handleCopy);
|
||||||
|
flowEventBus.off('paste', this.handlePaste);
|
||||||
|
flowEventBus.off('select-all', this.handleSelectAll);
|
||||||
|
flowEventBus.off('invert-select', this.handleInvertSelect);
|
||||||
|
flowEventBus.off('cancel-select', this.handleCancelSelect);
|
||||||
|
flowEventBus.off('zoom-in', this.handleZoomIn);
|
||||||
|
flowEventBus.off('zoom-out', this.handleZoomOut);
|
||||||
|
flowEventBus.off('toggle-grid', this.handleToggleGrid);
|
||||||
|
flowEventBus.off('toggle-minimap', this.handleToggleMinimap);
|
||||||
|
flowEventBus.off('change-background-color', this.handleBackgroundColor);
|
||||||
|
flowEventBus.off('change-grid-size', this.handleGridSize);
|
||||||
|
flowEventBus.off('change-canvas-size', this.handleCanvasSize);
|
||||||
|
flowEventBus.off('hierarchical-layout', this.handleHierarchicalLayout);
|
||||||
|
flowEventBus.off('circle-layout', this.handleCircleLayout);
|
||||||
|
flowEventBus.off('organic-layout', this.handleOrganicLayout);
|
||||||
|
flowEventBus.off('bring-to-front', this.handleBringToFront);
|
||||||
|
flowEventBus.off('send-to-back', this.handleSendToBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleZoom = (scale: number) => {
|
||||||
|
this.graph?.zoomTo(scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleFit = () => {
|
||||||
|
this.graph?.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDelete = () => {
|
||||||
|
const cells = this.graph?.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph?.removeCells(cells);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleExport = () => {
|
||||||
|
const exportedXml = this.wuFlowAction.getXmlSerializer();
|
||||||
|
sessionStorage.setItem("flow-canvas-data", exportedXml);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleUndo = () => {
|
||||||
|
// 撤销操作
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleReDo = () => {
|
||||||
|
// 重做操作
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleEdgeStyle = (style: string) => {
|
||||||
|
this.wuFlowAction.getDefaultEdgeStyle().edgeStyle = style;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleAutoLayout = () => {
|
||||||
|
this.wuFlowAction.handleLayoutChange("hierarchical");
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBackgroundChange = (color: string) => {
|
||||||
|
if (this.canvasRef) {
|
||||||
|
this.canvasRef.style.backgroundColor = color;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleFontChange = (fontFamily: string) => {
|
||||||
|
this.wuFlowAction.changeFont(fontFamily);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleNodeColorChange = (color: string) => {
|
||||||
|
this.wuFlowAction.changeNodeColor(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleInsertImage = (imageUrl: string) => {
|
||||||
|
if (this.canvasRef) {
|
||||||
|
const container = this.canvasRef.getBoundingClientRect();
|
||||||
|
const x = container.width / 2;
|
||||||
|
const y = container.height / 2;
|
||||||
|
this.wuFlowAction.insertImageNode(x, y, imageUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBorderColor = (color: string) => {
|
||||||
|
this.wuFlowAction.changeBorderColor(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBorderStyle = (style: string) => {
|
||||||
|
this.wuFlowAction.changeBorderStyle(style);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBorderWidth = (width: number) => {
|
||||||
|
this.wuFlowAction.changeBorderWidth(width);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleToggleGrid = (show: boolean) => {
|
||||||
|
this.showGrid.value = show;
|
||||||
|
if (this.graph) {
|
||||||
|
this.graph.setGridEnabled(show);
|
||||||
|
this.graph.setGridVisible(show);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleLayoutChange = (type: string) => {
|
||||||
|
this.wuFlowAction.handleLayoutChange(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleNewFile = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
this.graph.removeCells(this.graph.getChildCells(this.graph.getDefaultParent()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleOpenFile = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = (e: Event) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target?.result as string);
|
||||||
|
this.wuFlowAction?.loadGraphData(data);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('文件格式错误');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSaveFile = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const data = this.wuFlowAction?.getXmlSerializer();
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'flow.json';
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleExportImage = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const bounds = this.graph.getGraphBounds();
|
||||||
|
const scale = 1;
|
||||||
|
canvas.width = bounds.width * scale;
|
||||||
|
canvas.height = bounds.height * scale;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
const svgString = this.wuFlowAction?.getGraphSvg();
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
flowEventBus.emit('export-preview', canvas.toDataURL('image/png'));
|
||||||
|
};
|
||||||
|
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleExportJson = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const data = this.wuFlowAction?.getXmlSerializer();
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'flow.json';
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCut = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.handleCopy();
|
||||||
|
this.graph.removeCells(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCopy = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
const encoder = new ModelXmlSerializer(this.graph.model);
|
||||||
|
const cellsXml = encoder.encode(cells);
|
||||||
|
navigator.clipboard.writeText(cellsXml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handlePaste = async () => {
|
||||||
|
if (this.graph) {
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
const decoder = new ModelXmlSerializer(this.graph.model);
|
||||||
|
const cells = decoder.decode(text);
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.importCells(cells);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('粘贴失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSelectAll = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const parent = this.graph.getDefaultParent();
|
||||||
|
const children = this.graph.getChildVertices(parent);
|
||||||
|
this.graph.setSelectionCells(children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleInvertSelect = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const parent = this.graph.getDefaultParent();
|
||||||
|
const allCells = this.graph.getChildVertices(parent);
|
||||||
|
const selectedCells = this.graph.getSelectionCells();
|
||||||
|
const invertedCells = allCells.filter(cell => !selectedCells.includes(cell));
|
||||||
|
this.graph.setSelectionCells(invertedCells);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCancelSelect = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
this.graph.clearSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleZoomIn = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const scale = this.graph.view.scale;
|
||||||
|
this.graph.zoomTo(scale * 1.2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleZoomOut = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const scale = this.graph.view.scale;
|
||||||
|
this.graph.zoomTo(scale * 0.8);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleToggleMinimap = (show: boolean) => {
|
||||||
|
flowEventBus.emit('show-graph-outline', show);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBackgroundColor = (color: string) => {
|
||||||
|
if (this.canvasRef) {
|
||||||
|
this.canvasRef.style.backgroundColor = color;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGridSize = (size: number) => {
|
||||||
|
if (this.graph) {
|
||||||
|
this.graph.gridSize = size;
|
||||||
|
// 刷新网格显示
|
||||||
|
const isGridVisible = this.showGrid.value;
|
||||||
|
this.graph.setGridEnabled(isGridVisible);
|
||||||
|
this.graph.setGridVisible(isGridVisible);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCanvasSize = (size: { width: number; height: number }) => {
|
||||||
|
if (this.canvasRef) {
|
||||||
|
this.canvasRef.style.width = `${size.width}px`;
|
||||||
|
this.canvasRef.style.height = `${size.height}px`;
|
||||||
|
// 通知图形需要重新布局
|
||||||
|
this.graph?.fit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleHierarchicalLayout = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const layout = new HierarchicalLayout(this.graph);
|
||||||
|
layout.execute(this.graph.getDefaultParent());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCircleLayout = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const layout = new CircleLayout(this.graph);
|
||||||
|
layout.execute(this.graph.getDefaultParent());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleOrganicLayout = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const layout = new FastOrganicLayout(this.graph);
|
||||||
|
layout.execute(this.graph.getDefaultParent());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBringToFront = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
this.graph.orderCells(true, cells);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSendToBack = () => {
|
||||||
|
if (this.graph) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
this.graph.orderCells(false, cells);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... 其他事件处理方法
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { MaxPopupMenu, Cell, Graph } from '@maxgraph/core';
|
||||||
|
import { flowEventBus } from '../events';
|
||||||
|
|
||||||
|
export class CustomPopupMenuHandler extends MaxPopupMenu {
|
||||||
|
|
||||||
|
|
||||||
|
protected addPopupMenuItems(menu: HTMLElement, cell: Cell, evt: MouseEvent): void {
|
||||||
|
if (cell && cell.isVertex()) {
|
||||||
|
// 删除选项
|
||||||
|
const deleteItem = document.createElement('div');
|
||||||
|
deleteItem.className = 'menu-item';
|
||||||
|
deleteItem.innerHTML = '删除';
|
||||||
|
deleteItem.onclick = () => {
|
||||||
|
flowEventBus.emit('delete');
|
||||||
|
this.hideMenu();
|
||||||
|
};
|
||||||
|
menu.appendChild(deleteItem);
|
||||||
|
|
||||||
|
// 复制选项
|
||||||
|
const copyItem = document.createElement('div');
|
||||||
|
copyItem.className = 'menu-item';
|
||||||
|
copyItem.innerHTML = '复制';
|
||||||
|
copyItem.onclick = () => {
|
||||||
|
const graph = this.getGraph();
|
||||||
|
if (graph) {
|
||||||
|
const clone = graph.cloneCells([cell])[0];
|
||||||
|
const geo = clone.getGeometry();
|
||||||
|
geo.x += 50;
|
||||||
|
geo.y += 50;
|
||||||
|
graph.addCell(clone);
|
||||||
|
}
|
||||||
|
this.hideMenu();
|
||||||
|
};
|
||||||
|
menu.appendChild(copyItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createMenu(): HTMLElement {
|
||||||
|
const menu = document.createElement('div');
|
||||||
|
menu.className = 'graph-context-menu';
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { Graph, Cell } from '@maxgraph/core';
|
||||||
|
|
||||||
|
export class KeyHandler {
|
||||||
|
private graph: Graph;
|
||||||
|
private copiedCells: Cell[] = [];
|
||||||
|
private pasteOffset = 30; // 粘贴时的偏移量
|
||||||
|
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
document.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown(event: KeyboardEvent) {
|
||||||
|
// Delete 键删除选中元素
|
||||||
|
if (event.key === 'Delete') {
|
||||||
|
this.handleDelete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + C 复制
|
||||||
|
if (event.ctrlKey && event.key === 'c') {
|
||||||
|
this.handleCopy(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + V 粘贴
|
||||||
|
if (event.ctrlKey && event.key === 'v') {
|
||||||
|
this.handlePaste(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + Z 撤销
|
||||||
|
if (event.ctrlKey && event.key === 'z') {
|
||||||
|
this.handleUndo(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl + Y 重做
|
||||||
|
if (event.ctrlKey && event.key === 'y') {
|
||||||
|
this.handleRedo(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDelete(event: KeyboardEvent) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells && cells.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
this.graph.removeCells(cells);
|
||||||
|
this.graph.clearSelection();
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCopy(event: KeyboardEvent) {
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells && cells.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
// 存储选中的单元格
|
||||||
|
this.copiedCells = cells;
|
||||||
|
|
||||||
|
// 可选:显示复制成功提示
|
||||||
|
console.log('已复制', cells.length, '个元素');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePaste(event: KeyboardEvent) {
|
||||||
|
if (this.copiedCells.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
// 克隆所有复制的单元格
|
||||||
|
const clones = this.graph.cloneCells(this.copiedCells);
|
||||||
|
|
||||||
|
// 为每个克隆的单元格添加偏移
|
||||||
|
clones.forEach(clone => {
|
||||||
|
if (clone.isVertex()) {
|
||||||
|
const geo = clone.getGeometry();
|
||||||
|
if (geo) {
|
||||||
|
geo.x += this.pasteOffset;
|
||||||
|
geo.y += this.pasteOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 插入克隆的单元格
|
||||||
|
const parent = this.graph.getDefaultParent();
|
||||||
|
this.graph.addCells(clones, parent);
|
||||||
|
|
||||||
|
// 选中新粘贴的单元格
|
||||||
|
this.graph.setSelectionCells(clones);
|
||||||
|
|
||||||
|
// 增加下次粘贴的偏移量
|
||||||
|
this.pasteOffset += 30;
|
||||||
|
// 如果偏移太大,重置
|
||||||
|
if (this.pasteOffset > 150) {
|
||||||
|
this.pasteOffset = 30;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUndo(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.graph.model.undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRedo(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.graph.model.redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { ConnectionHandler, CellState ,Graph} from '@maxgraph/core';
|
||||||
|
|
||||||
|
export class WuConnctionHandler extends ConnectionHandler {
|
||||||
|
// constructor(graph: Graph, options: any) {
|
||||||
|
// super(graph, options);
|
||||||
|
// }
|
||||||
|
isConnectableCell(cell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Special case: Snaps source of new connections to fixed points
|
||||||
|
* Without a connect preview in connectionHandler.createEdgeState mouseMove
|
||||||
|
* and getSourcePerimeterPoint should be overriden by setting sourceConstraint
|
||||||
|
* sourceConstraint to null in mouseMove and updating it and returning the
|
||||||
|
* nearest point (cp) in getSourcePerimeterPoint (see below)
|
||||||
|
*/
|
||||||
|
updateEdgeState(pt, constraint) {
|
||||||
|
if (pt != null && this.previous != null) {
|
||||||
|
const constraints = this.graph.getAllConnectionConstraints(this.previous, true) ;// .getAllConnectionConstraints(this.previous);
|
||||||
|
let nearestConstraint = null;
|
||||||
|
let dist = null;
|
||||||
|
for (let i = 0; i < constraints.length; i++) {
|
||||||
|
const cp = this.graph.getConnectionPoint(this.previous, constraints[i]);
|
||||||
|
if (cp != null) {
|
||||||
|
const tmp = (cp.x - pt.x) * (cp.x - pt.x) + (cp.y - pt.y) * (cp.y - pt.y);
|
||||||
|
if (dist == null || tmp < dist) {
|
||||||
|
nearestConstraint = constraints[i];
|
||||||
|
dist = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nearestConstraint != null) {
|
||||||
|
this.sourceConstraint = nearestConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case the edge style must be changed during the preview:
|
||||||
|
// this.edgeState.style.edgeStyle = 'orthogonalEdgeStyle';
|
||||||
|
// And to use the new edge style in the new edge inserted into the graph,
|
||||||
|
// update the cell style as follows:
|
||||||
|
// this.edgeState.cell.style = utils.setStyle(this.edgeState.cell.style, 'edgeStyle', this.edgeState.style.edgeStyle);
|
||||||
|
}
|
||||||
|
return super.updateEdgeState(pt, constraint);
|
||||||
|
}
|
||||||
|
createEdgeState(me) {
|
||||||
|
// Connect preview
|
||||||
|
const edge = this.graph.createEdge(null, null, null, null, null, {
|
||||||
|
edgeStyle: 'orthogonalEdgeStyle'
|
||||||
|
});
|
||||||
|
return new CellState(this.graph.view, edge, this.graph.getCellStyle(edge));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ConstraintHandler, Graph, mathUtils } from '@maxgraph/core';
|
||||||
|
|
||||||
|
export class WuConstraintHandler extends ConstraintHandler {
|
||||||
|
intersects(icon, point, source, existingEdge) {
|
||||||
|
return !source || existingEdge || mathUtils.intersects(icon.bounds, point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,417 @@
|
||||||
|
export const FlowIcons = {
|
||||||
|
// 基础流程图标
|
||||||
|
Start: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
End: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
<circle cx="12" cy="12" r="5" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Process: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Decision: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 4L20 12L12 20L4 12L12 4Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Arrow: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12H20M20 12L14 6M20 12L14 18" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 高级组件图标
|
||||||
|
SubProcess: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="8" x2="12" y2="16" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Parallel: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="9" x2="16" y2="9" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="15" x2="16" y2="15" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Timer: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke-width="2"/>
|
||||||
|
<path d="M12 8v4l3 3" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 基础形状节点图标
|
||||||
|
Rectangle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="6" width="16" height="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Ellipse: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<ellipse cx="12" cy="12" rx="8" ry="6" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
DoubleEllipse: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<ellipse cx="12" cy="12" rx="8" ry="6" stroke-width="2"/>
|
||||||
|
<ellipse cx="12" cy="12" rx="5" ry="3.5" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Rhombus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 4L20 12L12 20L4 12L12 4Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Line: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Image: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="4" width="16" height="16" stroke-width="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="2" stroke-width="2"/>
|
||||||
|
<path d="M4 19L8 15L12 19" stroke-width="2"/>
|
||||||
|
<path d="M12 19L16 15L20 19" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
ArrowConnector: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12H20M20 12L14 6M20 12L14 18" stroke-width="2"/>
|
||||||
|
<circle cx="4" cy="12" r="2" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Label: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 6h16M4 12h16M4 18h12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Cylinder: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M6 7C6 5.89543 8.68629 5 12 5C15.3137 5 18 5.89543 18 7V17C18 18.1046 15.3137 19 12 19C8.68629 19 6 18.1046 6 17V7Z" stroke-width="2"/>
|
||||||
|
<path d="M6 7C6 8.10457 8.68629 9 12 9C15.3137 9 18 8.10457 18 7" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Swimlane: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="4" width="16" height="16" stroke-width="2"/>
|
||||||
|
<line x1="4" y1="8" x2="20" y2="8" stroke-width="2"/>
|
||||||
|
<line x1="12" y1="8" x2="12" y2="20" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Connector: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="6" cy="12" r="2" stroke-width="2"/>
|
||||||
|
<circle cx="18" cy="12" r="2" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Actor: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="7" r="3" stroke-width="2"/>
|
||||||
|
<path d="M12 10v7M8 20h8M8 14l4-4l4 4" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Cloud: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M6.5 15.5C5.11929 15.5 4 14.3807 4 13C4 11.6193 5.11929 10.5 6.5 10.5C6.57089 10.5 6.64124 10.5032 6.71092 10.5097C7.06447 8.48561 8.85496 7 11 7C13.2091 7 15 8.79086 15 11C15 11.1839 14.9859 11.3643 14.9587 11.5402C15.4882 11.1963 16.1204 11 16.8 11C18.5673 11 20 12.4327 20 14.2C20 15.9673 18.5673 17.4 16.8 17.4H7.2C6.53726 17.4 6 16.8627 6 16.2C6 15.5373 6.53726 15 7.2 15H6.5Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Triangle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 4L20 18H4L12 4Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Hexagon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 4L19 8V16L12 20L5 16V8L12 4Z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// UML 图标
|
||||||
|
ClassIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
InterfaceIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
<line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
PackageIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M2,2 h8 v4 h12 v16 h-20 z" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
ComponentIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="4" y="4" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<rect x="1" y="8" width="6" height="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<rect x="1" y="13" width="6" height="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<rect x="17" y="8" width="6" height="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<rect x="17" y="13" width="6" height="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
AggregationIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M2,12 L12,2 L22,12 L12,22 Z" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
CompositionIcon: `<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M2,12 L12,2 L22,12 L12,22 Z" fill="currentColor" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Note: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke-width="2"/>
|
||||||
|
<path d="M14 2v6h6" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="13" x2="16" y2="13" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="17" x2="16" y2="17" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Text: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<text x="4" y="16" font-size="16" font-family="Arial" fill="currentColor">Aa</text>
|
||||||
|
<line x1="4" y1="19" x2="20" y2="19" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
Star: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 2l2.4 7.4h7.6l-6.2 4.5 2.4 7.4-6.2-4.5-6.2 4.5 2.4-7.4-6.2-4.5h7.6z" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
VSwimlane: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="2" y="2" width="20" height="20" stroke-width="2"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="22" stroke-width="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="22" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
HSwimlane: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="2" y="2" width="20" height="20" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="16" x2="22" y2="16" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
TableIcon: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="15" x2="21" y2="15" stroke-width="2"/>
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21" stroke-width="2"/>
|
||||||
|
<line x1="15" y1="3" x2="15" y2="21" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
SimpleTableIcon: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="2"/>
|
||||||
|
<line x1="3" y1="15" x2="21" y2="15" stroke-width="2"/>
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolbarIcons = {
|
||||||
|
// ... 其他图标 ...
|
||||||
|
|
||||||
|
// 背景颜色图标
|
||||||
|
backgroundColor: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" stroke-width="2"/>
|
||||||
|
<path d="M4 8h16" stroke-width="2"/>
|
||||||
|
<path d="M8 12h8" stroke-width="2" stroke-dasharray="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 节点颜色图标
|
||||||
|
nodeColor: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 2L2 12L12 22L22 12L12 2Z" stroke-width="2"/>
|
||||||
|
<circle cx="12" cy="12" r="4" stroke-width="2"/>
|
||||||
|
<path d="M12 6V8M12 16V18M6 12H8M16 12H18" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 网格图标
|
||||||
|
grid: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 4h16v16H4V4z" stroke-width="2"/>
|
||||||
|
<path d="M4 12h16M12 4v16" stroke-width="1" stroke-dasharray="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 导出图标
|
||||||
|
export: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 3v12M7 8l5-5 5 5" stroke-width="2"/>
|
||||||
|
<path d="M4 14v4a2 2 0 002 2h12a2 2 0 002-2v-4" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 撤销图标
|
||||||
|
undo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M3 7v6h6" stroke-width="2"/>
|
||||||
|
<path d="M3 13c0-4.97 4.03-9 9-9 4.97 0 9 4.03 9 9s-4.03 9-9 9" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 重做图标
|
||||||
|
redo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M21 7v6h-6" stroke-width="2"/>
|
||||||
|
<path d="M21 13c0-4.97-4.03-9-9-9-4.97 0-9 4.03-9 9s4.03 9 9 9" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 删除图标
|
||||||
|
delete: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 7h16M10 11v6M14 11v6" stroke-width="2"/>
|
||||||
|
<path d="M5 7l1 12a2 2 0 002 2h8a2 2 0 002-2l1-12" stroke-width="2"/>
|
||||||
|
<path d="M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 适应画布图标
|
||||||
|
fit: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 4h16v16H4V4z" stroke-width="2"/>
|
||||||
|
<path d="M9 9h6v6H9V9z" stroke-width="2"/>
|
||||||
|
<path d="M4 4l5 5M20 4l-5 5M4 20l5-5M20 20l-5-5" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 缩小图标
|
||||||
|
zoomOut: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="11" cy="11" r="7" stroke-width="2"/>
|
||||||
|
<path d="M16 16l4 4" stroke-width="2"/>
|
||||||
|
<path d="M8 11h6" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 放大图标
|
||||||
|
zoomIn: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="11" cy="11" r="7" stroke-width="2"/>
|
||||||
|
<path d="M16 16l4 4" stroke-width="2"/>
|
||||||
|
<path d="M8 11h6M11 8v6" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 返回图标
|
||||||
|
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M15 4l-8 8 8 8" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 直线图标
|
||||||
|
straightLine: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12h16" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 折线图标
|
||||||
|
orthogonalLine: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12h8v-6h8" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 曲线图标
|
||||||
|
curvedLine: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M4 12C8 12 16 4 20 12" stroke-width="2"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
borderColor: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path d="M17.75 7L14 3.25l-10 10V17h3.75l10-10zm2.96-2.96c.39-.39.39-1.02 0-1.41L18.37.29c-.39-.39-1.02-.39-1.41 0L15 2.25 18.75 6l1.96-1.96z"/>
|
||||||
|
<path d="M0 20h24v4H0z"/>
|
||||||
|
</svg>`,
|
||||||
|
borderStyle: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path d="M3 16h18v2H3zm0-5h18v2H3zm0-5h18v2H3z"/>
|
||||||
|
</svg>`,
|
||||||
|
borderWidth: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path d="M3 17h18v1H3z"/>
|
||||||
|
<path d="M3 12h18v2H3z"/>
|
||||||
|
<path d="M3 5h18v3H3z"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
// 对齐图标
|
||||||
|
alignLeft: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 128h64v768h-64z M256 320h512v128H256z M256 576h320v128H256z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
alignCenter: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M480 128h64v768h-64z M256 320h512v128H256z M352 576h320v128H352z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
alignRight: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M832 128h64v768h-64z M256 320h512v128H256z M448 576h320v128H448z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
alignTop: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 128h768v64H128z M320 256h128v512H320z M576 256h128v320H576z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
alignMiddle: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 480h768v64H128z M320 256h128v512H320z M576 352h128v320H576z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
alignBottom: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 832h768v64H128z M320 256h128v512H320z M576 448h128v320H576z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
distributeHorizontally: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 128h64v768h-64z M832 128h64v768h-64z M448 320h128v384H448z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
|
||||||
|
distributeVertically: `<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||||
|
<path d="M128 128h768v64H128z M128 832h768v64H128z M320 448h384v128H320z" fill="currentColor"/>
|
||||||
|
</svg>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MenuIcons = {
|
||||||
|
delete: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`,
|
||||||
|
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2v-4" stroke-width="2"/>
|
||||||
|
<path d="M8 12h11c1.1 0 2-.9 2-2v-4H8v4z"/></svg>`,
|
||||||
|
image: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||||
|
</svg>`
|
||||||
|
};
|
||||||
|
|
||||||
|
// UML 图标定义
|
||||||
|
export const UMLIcons = {
|
||||||
|
Class: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Interface: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<text x="6" y="6" font-size="6" fill="currentColor">«interface»</text>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Package: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M2,2 h6 v4 h14 v16 h-20 v-20" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Component: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="4" y="4" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<rect x="1" y="8" width="4" height="3" fill="currentColor"/>
|
||||||
|
<rect x="19" y="8" width="4" height="3" fill="currentColor"/>
|
||||||
|
<rect x="8" y="1" width="3" height="4" fill="currentColor"/>
|
||||||
|
<rect x="8" y="19" width="3" height="4" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Object: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<text x="6" y="6" font-size="6" font-style="italic" fill="currentColor">object</text>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Enumeration: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<line x1="2" y1="8" x2="22" y2="8" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<text x="6" y="6" font-size="6" fill="currentColor">«enumeration»</text>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Association: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<polygon points="18,8 22,12 18,16" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Inheritance: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<polygon points="16,8 22,12 16,16" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Implementation: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
<polygon points="16,8 22,12 16,16" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
|
Dependency: `
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2" stroke-dasharray="4"/>
|
||||||
|
<polygon points="18,8 22,12 18,16" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { App } from 'vue'
|
||||||
|
import SplitPane from './components/SplitPane.vue'
|
||||||
|
import FlowAccordion from './components/panel/FlowAccordion.vue'
|
||||||
|
import Accordion from './components/Accordion.vue'
|
||||||
|
import FlowCanvas from './components/canvas/FlowCanvas.vue'
|
||||||
|
import FlowToolbar from './components/menu/FlowToolbar.vue'
|
||||||
|
import { WuFlowAction } from './utils/flow-utils'
|
||||||
|
|
||||||
|
// 导出单个组件
|
||||||
|
export { SplitPane, FlowAccordion, Accordion, FlowCanvas, FlowToolbar, WuFlowAction }
|
||||||
|
|
||||||
|
// 导出插件安装函数
|
||||||
|
export default {
|
||||||
|
install: (app: App) => {
|
||||||
|
app.component('WuSplitPane', SplitPane)
|
||||||
|
app.component('WuFlowAccordion', FlowAccordion)
|
||||||
|
app.component('WuAccordion', Accordion)
|
||||||
|
app.component('WuFlowCanvas', FlowCanvas)
|
||||||
|
app.component('WuFlowToolbar', FlowToolbar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
.flow-canvas-wrapper {
|
||||||
|
padding: 6px;
|
||||||
|
height: calc(100% - 12px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas.style-brush-active {
|
||||||
|
cursor: copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-canvas.show-grid {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, #f0f0f0 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, #f0f0f0 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-image: linear-gradient(to right, #f0f0f0 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, #f0f0f0 1px, transparent 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elements {
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-bg {
|
||||||
|
fill: white;
|
||||||
|
stroke: #dcdfe6;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node:hover .node-bg {
|
||||||
|
stroke: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node :deep(svg) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
font-size: 12px;
|
||||||
|
fill: #303133;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-outline {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: move;
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 0px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-toolbar button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-toolbar button.active {
|
||||||
|
background: #e6f7ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-toolbar select {
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-toolbar input[type="color"] {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mxRubberband {
|
||||||
|
border-color: #b18426;
|
||||||
|
background: #db9b0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右键菜单样式 */
|
||||||
|
.mxPopupMenu {
|
||||||
|
position: absolute;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 5px 0;
|
||||||
|
min-width: 120px;
|
||||||
|
z-index: 9999;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:hover .menu-icon svg {
|
||||||
|
fill: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:active {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mxPopupMenu hr {
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
border: none;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
transform: scaleY(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子菜单样式 */
|
||||||
|
.custom-menu-item.has-submenu::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 2px solid #c0c4cc;
|
||||||
|
border-top: 2px solid #c0c4cc;
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #c0c4cc;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.disabled:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
interface DragState {
|
||||||
|
leftPanelWidth: number;
|
||||||
|
rightPanelWidth: number;
|
||||||
|
leftPanelVisible: boolean;
|
||||||
|
rightPanelVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDragHandler(state: DragState, type: 'left' | 'right') {
|
||||||
|
const minWidth = 0
|
||||||
|
const maxWidth = 800
|
||||||
|
let startX = 0
|
||||||
|
let startWidth = 0
|
||||||
|
let isDragging = false
|
||||||
|
|
||||||
|
const startDrag = (e: MouseEvent) => {
|
||||||
|
const isVisible = type === 'left' ? state.leftPanelVisible : state.rightPanelVisible
|
||||||
|
if (!isVisible) return
|
||||||
|
|
||||||
|
isDragging = true
|
||||||
|
startX = e.clientX
|
||||||
|
startWidth = type === 'left' ? state.leftPanelWidth : state.rightPanelWidth
|
||||||
|
|
||||||
|
document.body.style.userSelect = 'none'
|
||||||
|
document.addEventListener('mousemove', onDrag)
|
||||||
|
document.addEventListener('mouseup', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDrag = (e: MouseEvent) => {
|
||||||
|
if (!isDragging) return
|
||||||
|
|
||||||
|
const containerWidth = window.innerWidth
|
||||||
|
const diff = e.clientX - startX
|
||||||
|
|
||||||
|
if (type === 'left') {
|
||||||
|
// 左侧面板拖动限制
|
||||||
|
const maxAllowedWidth = containerWidth - (state.rightPanelVisible ? state.rightPanelWidth : 0) - 400 // 保留中间面板最小宽度
|
||||||
|
const newWidth = Math.min(
|
||||||
|
maxAllowedWidth,
|
||||||
|
Math.max(minWidth, startWidth + diff)
|
||||||
|
)
|
||||||
|
state.leftPanelWidth = newWidth
|
||||||
|
} else {
|
||||||
|
// 右侧面板拖动限制
|
||||||
|
const maxAllowedWidth = containerWidth - (state.leftPanelVisible ? state.leftPanelWidth : 0) - 400 // 保留中间面板最小宽度
|
||||||
|
const newWidth = Math.min(
|
||||||
|
maxAllowedWidth,
|
||||||
|
Math.max(minWidth, startWidth - diff)
|
||||||
|
)
|
||||||
|
state.rightPanelWidth = newWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
isDragging = false
|
||||||
|
document.body.style.userSelect = ''
|
||||||
|
document.removeEventListener('mousemove', onDrag)
|
||||||
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDrag,
|
||||||
|
onDrag,
|
||||||
|
stopDrag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Graph, EditorToolbar } from "mxgraph/javascript/mxClient";
|
||||||
|
|
||||||
|
export class flowToolsBars {
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
new EditorToolbar(graph.container, graph.editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
import {
|
||||||
|
Graph, HierarchicalLayout, Outline, InternalEvent,
|
||||||
|
GraphDataModel, ModelXmlSerializer, CircleLayout, FastOrganicLayout,
|
||||||
|
StackLayout, ParallelEdgeLayout, PartitionLayout, ConstraintHandler, getDefaultPlugins, CellStyle,Cell,
|
||||||
|
SelectionCellsHandler, SelectionHandler, GraphPluginConstructor, CellState, ConnectionHandler
|
||||||
|
} from "@maxgraph/core";
|
||||||
|
import { CustomRubberBandHandler } from "../flow/CustomRubberBandHandler";
|
||||||
|
import { WuCustomGraph } from "../flow/WuCustomGraph";
|
||||||
|
import { MyCustomPopupMenuHandler } from "../flow/MyCustomPopupMenuHandler";
|
||||||
|
import { WuSelectHander } from "../flow/WuSelectHander";
|
||||||
|
import { ShapeRegister } from './shape-register';
|
||||||
|
import { WuConnctionHandler } from "../handlers/WuConnctionHandler";
|
||||||
|
|
||||||
|
export class WuFlowAction {
|
||||||
|
private graph: WuCustomGraph;
|
||||||
|
private canvasRef: HTMLElement;
|
||||||
|
private static shapesRegistered = false; // 添加静态标志
|
||||||
|
|
||||||
|
constructor(container: HTMLElement, graphOutlineRef: HTMLElement) {
|
||||||
|
this.canvasRef = container;
|
||||||
|
// 只在第一次时注册自定义形状
|
||||||
|
if (!WuFlowAction.shapesRegistered) {
|
||||||
|
ShapeRegister.registerCustomShapes();
|
||||||
|
WuFlowAction.shapesRegistered = true;
|
||||||
|
}
|
||||||
|
this.init(container, graphOutlineRef)
|
||||||
|
}
|
||||||
|
init(container: HTMLElement, graphOutlineRef: HTMLElement) {
|
||||||
|
//
|
||||||
|
const plugins: GraphPluginConstructor[] = [...getDefaultPlugins(),MyCustomPopupMenuHandler,CustomRubberBandHandler,WuSelectHander,SelectionCellsHandler,SelectionHandler];
|
||||||
|
if (container) {
|
||||||
|
InternalEvent.disableContextMenu(container);
|
||||||
|
const flowCanvasData = sessionStorage.getItem("flow-canvas-data");
|
||||||
|
if (flowCanvasData) {
|
||||||
|
const model = new GraphDataModel();
|
||||||
|
const serializer = new ModelXmlSerializer(model);
|
||||||
|
serializer.import(flowCanvasData);
|
||||||
|
this.graph = new WuCustomGraph(container, model, plugins);
|
||||||
|
} else {
|
||||||
|
this.graph = new WuCustomGraph(container, undefined, plugins);
|
||||||
|
}
|
||||||
|
// 启用基本功能
|
||||||
|
this.graph.setEnabled(true);
|
||||||
|
// 启用连接功能
|
||||||
|
this.graph.setConnectable(true);
|
||||||
|
// 启用多选功能
|
||||||
|
this.graph.allowLoops = true;
|
||||||
|
// 允许从任意点开始连接
|
||||||
|
// 允许悬浮显示连接点
|
||||||
|
// 允许创建目标连接
|
||||||
|
|
||||||
|
// 允许连接到空白处创建新节点
|
||||||
|
this.graph.setAllowDanglingEdges(true);
|
||||||
|
// 设置默认边样式
|
||||||
|
this.graph.getStylesheet().getDefaultEdgeStyle().edgeStyle = 'orthogonalEdgeStyle';
|
||||||
|
this.graph.getStylesheet().getDefaultEdgeStyle().rounded = true;
|
||||||
|
this.graph.getStylesheet().getDefaultEdgeStyle().strokeColor = "#5b8ffa";
|
||||||
|
this.graph.getStylesheet().getDefaultEdgeStyle().strokeWidth = 2;
|
||||||
|
this.graph.setMultigraph(true);
|
||||||
|
this.graph.setDropEnabled(true);
|
||||||
|
// 启用选择功能
|
||||||
|
this.graph.setPanning(true); // 暂时禁用平移,防止与橡皮筋选择冲突
|
||||||
|
this.graph.swimlaneSelectionEnabled = true;
|
||||||
|
|
||||||
|
const outline = new Outline(this.graph, graphOutlineRef);
|
||||||
|
outline.border = 1;
|
||||||
|
outline.enabled = true;
|
||||||
|
if (graphOutlineRef) {
|
||||||
|
graphOutlineRef.style.right = "10px";
|
||||||
|
graphOutlineRef.style.top = "10px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.graph.setDropEnabled(true);
|
||||||
|
this.graph.addListener(InternalEvent.CELL_CONNECTED, (sender, event: InternalEvent) => {
|
||||||
|
console.log("WuFlowAction sender", sender, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleLayoutChange(type: string) {
|
||||||
|
const parent = this.graph.getDefaultParent()
|
||||||
|
this.graph.model.beginUpdate()
|
||||||
|
try {
|
||||||
|
let layout
|
||||||
|
switch (type) {
|
||||||
|
case 'hierarchical':
|
||||||
|
layout = new HierarchicalLayout(this.graph)
|
||||||
|
layout.orientation = 'north'
|
||||||
|
layout.interHierarchySpacing = 30
|
||||||
|
break
|
||||||
|
case 'circle':
|
||||||
|
layout = new CircleLayout(this.graph, 3)
|
||||||
|
break
|
||||||
|
case 'organic':
|
||||||
|
layout = new FastOrganicLayout(this.graph)
|
||||||
|
break
|
||||||
|
case 'stack':
|
||||||
|
layout = new StackLayout(this.graph)
|
||||||
|
break
|
||||||
|
case 'parallel':
|
||||||
|
layout = new ParallelEdgeLayout(this.graph)
|
||||||
|
break
|
||||||
|
case 'partition':
|
||||||
|
layout = new PartitionLayout(this.graph, true, 100, 120)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
layout = new HierarchicalLayout(this.graph)
|
||||||
|
}
|
||||||
|
layout.execute(parent)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeFont(fontFamily: string) {
|
||||||
|
if (!this.graph) return
|
||||||
|
const cells = this.graph.getSelectionCells()
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate()
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const style = { ...cell.getStyle() }
|
||||||
|
style.fontFamily = fontFamily
|
||||||
|
this.graph.setCellStyle(style, [cell])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getDefaultEdgeStyle() {
|
||||||
|
return this.graph.getStylesheet().getDefaultEdgeStyle()
|
||||||
|
}
|
||||||
|
getXmlSerializer() {
|
||||||
|
const model = this.graph.getDataModel()
|
||||||
|
const serializer = new ModelXmlSerializer(model)
|
||||||
|
return serializer.export()
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphSvg() {
|
||||||
|
return this.graph.getGraphSvg()
|
||||||
|
}
|
||||||
|
insertVertex(event: DragEvent) {
|
||||||
|
const jsonStr = event.dataTransfer?.getData("application/json");
|
||||||
|
if (jsonStr && this.graph) {
|
||||||
|
const item = JSON.parse(jsonStr);
|
||||||
|
const rect = this.canvasRef.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
this.graph.batchUpdate(() => {
|
||||||
|
const parent = this.graph.getDefaultParent();
|
||||||
|
// 获取节点尺寸,如果未定义则使用默认值
|
||||||
|
console.log('item', item );
|
||||||
|
const size = item?.size || [100, 50];
|
||||||
|
let style: CellStyle = {
|
||||||
|
fillColor: item.backgroundColor || "orange",
|
||||||
|
shape: item.shape ||item.style.shape || "ellipse",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
verticalLabelPosition: "middle",
|
||||||
|
editable: true,
|
||||||
|
movable: true,
|
||||||
|
resizable: true,
|
||||||
|
};
|
||||||
|
if (item.style) {
|
||||||
|
style = {
|
||||||
|
...item.style, editable: true,
|
||||||
|
movable: true,
|
||||||
|
resizable: true,
|
||||||
|
connectable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertex = this.graph.insertVertex({
|
||||||
|
parent,
|
||||||
|
position: [x, y],
|
||||||
|
size: size,
|
||||||
|
style: style,
|
||||||
|
value: item.label,
|
||||||
|
});
|
||||||
|
vertex.connectable = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraph() {
|
||||||
|
return this.graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAnimation(cell: CellState) {
|
||||||
|
const state = this.graph.view.getState(cell.cell);
|
||||||
|
state?.shape?.node.getElementsByTagName('path')[0].removeAttribute('visibility');
|
||||||
|
state?.shape?.node.getElementsByTagName('path')[0].setAttribute('stroke-width', '6');
|
||||||
|
state?.shape?.node.getElementsByTagName('path')[0].setAttribute('stroke', 'lightGray');
|
||||||
|
state?.shape?.node.getElementsByTagName('path')[1].setAttribute('class', 'flow');
|
||||||
|
}
|
||||||
|
|
||||||
|
changeNodeColor(color: string) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const style = { ...cell.getStyle() };
|
||||||
|
style.fillColor = color;
|
||||||
|
this.graph.setCellStyle(style, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedNodeStyle(styleProps: any) {
|
||||||
|
const graph = this.getGraph();
|
||||||
|
const cell = graph.getSelectionCell();
|
||||||
|
if (cell) {
|
||||||
|
const style = cell.getStyle();
|
||||||
|
const newStyle = { ...cell.getStyle(), ...styleProps };
|
||||||
|
graph.setCellStyle(newStyle, [cell]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTextStyle(styleProps: any) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
console.log("updateTextStyle1", styleProps);
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const style = { ...cell.getStyle() };
|
||||||
|
// 只更新变化的属性
|
||||||
|
Object.entries(styleProps).forEach(([key, value]) => {
|
||||||
|
if (style[key] !== value) {
|
||||||
|
style[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.graph.setCellStyle(style, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedNodeStyle() {
|
||||||
|
if (!this.graph) return null;
|
||||||
|
const cell = this.graph.getSelectionCell();
|
||||||
|
if (cell?.isVertex()) {
|
||||||
|
const style = cell.getStyle();
|
||||||
|
return {
|
||||||
|
fontFamily: style.fontFamily || 'Arial',
|
||||||
|
fontSize: style.fontSize || 14,
|
||||||
|
fontColor: style.fontColor || '#000000',
|
||||||
|
fontWeight: style['font-weight'] || 'normal',
|
||||||
|
textDecoration: style['text-decoration'] || 'none'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertImageNode(x: number, y: number, imageUrl: string) {
|
||||||
|
const graph = this.getGraph();
|
||||||
|
if (graph) {
|
||||||
|
const parent = graph.getDefaultParent();
|
||||||
|
graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
const vertex = graph.insertVertex(
|
||||||
|
parent,
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
x - 50, // 居中显示
|
||||||
|
y - 50,
|
||||||
|
100, // 默认宽度
|
||||||
|
100, // 默认高度
|
||||||
|
{
|
||||||
|
shape: 'image',
|
||||||
|
image: imageUrl,
|
||||||
|
imageAlign: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
resizable: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
graph.setSelectionCell(vertex);
|
||||||
|
} finally {
|
||||||
|
graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeBorderColor(color: string) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const style = { ...cell.getStyle() };
|
||||||
|
style.strokeColor = color;
|
||||||
|
this.graph.setCellStyle(style, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeBorderStyle(style: string) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const cellStyle = { ...cell.getStyle() };
|
||||||
|
cellStyle.dashed = style === 'dashed';
|
||||||
|
// cellStyle.dotted = style === 'dotted';
|
||||||
|
this.graph.setCellStyle(cellStyle, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeBorderWidth(width: number) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
const cells = this.graph.getSelectionCells();
|
||||||
|
if (cells?.length) {
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
try {
|
||||||
|
cells.forEach(cell => {
|
||||||
|
if (cell.isVertex()) {
|
||||||
|
const style = { ...cell.getStyle() };
|
||||||
|
style.strokeWidth = width;
|
||||||
|
this.graph.setCellStyle(style, [cell]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGraphData(data: any) {
|
||||||
|
if (!this.graph) return;
|
||||||
|
this.graph.model.beginUpdate();
|
||||||
|
this.graph.model.import(data);
|
||||||
|
this.graph.model.endUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
import { Graph, CellRenderer, Shape } from '@maxgraph/core';
|
||||||
|
import { FlowIcons, UMLIcons } from '../icons';
|
||||||
|
|
||||||
|
export class ShapeRegister {
|
||||||
|
static registerCustomShapes() {
|
||||||
|
// 注册基础流程图形状
|
||||||
|
CellRenderer.registerShape('start', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
// 绘制圆形
|
||||||
|
c.ellipse(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// 绘制内部小圆
|
||||||
|
const insetSize = Math.min(w, h) * 0.3;
|
||||||
|
c.setFillColor('#4caf50');
|
||||||
|
c.ellipse(x + (w-insetSize)/2, y + (h-insetSize)/2, insetSize, insetSize);
|
||||||
|
c.fill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CellRenderer.registerShape('end', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
// 绘制圆形
|
||||||
|
c.ellipse(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// 绘制内部圆环
|
||||||
|
const insetSize = Math.min(w, h) * 0.3;
|
||||||
|
c.setStrokeWidth(2);
|
||||||
|
c.setStrokeColor('#f44336');
|
||||||
|
c.ellipse(x + (w-insetSize)/2, y + (h-insetSize)/2, insetSize, insetSize);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 UML 图形状
|
||||||
|
Object.entries(UMLIcons).forEach(([name, svg]) => {
|
||||||
|
CellRenderer.registerShape(name.toLowerCase(), class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
c.rect(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
if (this.state.style.shape === name.toLowerCase()) {
|
||||||
|
const svgElement = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement;
|
||||||
|
const svgContent = svgElement.innerHTML;
|
||||||
|
c.setSvg(svgContent, x, y, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 UML 类图形状
|
||||||
|
CellRenderer.registerShape('umlClass', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
c.rect(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// 绘制分隔线
|
||||||
|
const headerHeight = h / 3;
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(x, y + headerHeight);
|
||||||
|
c.lineTo(x + w, y + headerHeight);
|
||||||
|
c.moveTo(x, y + headerHeight * 2);
|
||||||
|
c.lineTo(x + w, y + headerHeight * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 UML 接口形状
|
||||||
|
CellRenderer.registerShape('umlInterface', class extends Shape {
|
||||||
|
constructor(state: any) {
|
||||||
|
super(state);
|
||||||
|
this.isDashed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
c.setDashed(true, [2, 2]);
|
||||||
|
c.rect(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// 绘制分隔线
|
||||||
|
const headerHeight = h / 3;
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(x, y + headerHeight);
|
||||||
|
c.lineTo(x + w, y + headerHeight);
|
||||||
|
c.moveTo(x, y + headerHeight * 2);
|
||||||
|
c.lineTo(x + w, y + headerHeight * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册包形状
|
||||||
|
CellRenderer.registerShape('package', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
const tabWidth = w * 0.3;
|
||||||
|
const tabHeight = h * 0.2;
|
||||||
|
|
||||||
|
c.begin();
|
||||||
|
// 绘制标签部分
|
||||||
|
c.moveTo(x, y);
|
||||||
|
c.lineTo(x + tabWidth, y);
|
||||||
|
c.lineTo(x + tabWidth, y + tabHeight);
|
||||||
|
c.lineTo(x + w, y + tabHeight);
|
||||||
|
c.lineTo(x + w, y + h);
|
||||||
|
c.lineTo(x, y + h);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册组件形状
|
||||||
|
CellRenderer.registerShape('component', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
c.rect(x, y, w, h);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
const portSize = Math.min(w, h) * 0.2;
|
||||||
|
|
||||||
|
// 绘制端口
|
||||||
|
c.begin();
|
||||||
|
c.rect(x - portSize, y + h * 0.25, portSize, portSize);
|
||||||
|
c.rect(x - portSize, y + h * 0.6, portSize, portSize);
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册备注形状
|
||||||
|
CellRenderer.registerShape('note', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
const fold = Math.min(w, h) * 0.2; // 折角大小
|
||||||
|
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(x, y);
|
||||||
|
c.lineTo(x + w - fold, y);
|
||||||
|
c.lineTo(x + w, y + fold);
|
||||||
|
c.lineTo(x + w, y + h);
|
||||||
|
c.lineTo(x, y + h);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// 绘制折角
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(x + w - fold, y);
|
||||||
|
c.lineTo(x + w - fold, y + fold);
|
||||||
|
c.lineTo(x + w, y + fold);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册五角星形状
|
||||||
|
CellRenderer.registerShape('star', class extends Shape {
|
||||||
|
paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
const cx = x + w/2;
|
||||||
|
const cy = y + h/2;
|
||||||
|
const r = Math.min(w, h)/2;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
// 外部点
|
||||||
|
const angle = (i * 72 - 90) * Math.PI/180;
|
||||||
|
points.push([
|
||||||
|
cx + r * Math.cos(angle),
|
||||||
|
cy + r * Math.sin(angle)
|
||||||
|
]);
|
||||||
|
// 内部点
|
||||||
|
const innerAngle = ((i * 72 + 36) - 90) * Math.PI/180;
|
||||||
|
points.push([
|
||||||
|
cx + (r/2) * Math.cos(innerAngle),
|
||||||
|
cy + (r/2) * Math.sin(innerAngle)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(points[0][0], points[0][1]);
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
c.lineTo(points[i][0], points[i][1]);
|
||||||
|
}
|
||||||
|
c.lineTo(points[0][0], points[0][1]);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册表格形状
|
||||||
|
// graph.getStylesheet().putCellStyle('table', {
|
||||||
|
// shape: 'html',
|
||||||
|
// whiteSpace: 'wrap',
|
||||||
|
// verticalAlign: 'top',
|
||||||
|
// align: 'left',
|
||||||
|
// overflow: 'visible'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // 注册泳道形状
|
||||||
|
// CellRenderer.registerShape('swimlane', class extends Shape {
|
||||||
|
// paintBackground(c: any, x: number, y: number, w: number, h: number) {
|
||||||
|
// const style = this.state.style;
|
||||||
|
// const isHorizontal = style.horizontal;
|
||||||
|
// const startSize = style.startSize || 30;
|
||||||
|
|
||||||
|
// // 绘制外框
|
||||||
|
// c.begin();
|
||||||
|
// c.rect(x, y, w, h);
|
||||||
|
// c.fillAndStroke();
|
||||||
|
|
||||||
|
// // 绘制标题区域
|
||||||
|
// c.begin();
|
||||||
|
// if (isHorizontal) {
|
||||||
|
// c.rect(x, y, w, startSize);
|
||||||
|
// } else {
|
||||||
|
// c.rect(x, y, startSize, h);
|
||||||
|
// }
|
||||||
|
// c.fillAndStroke();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -200,103 +200,107 @@ const clear = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div>
|
||||||
:class="prefixCls"
|
<div
|
||||||
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
|
:class="prefixCls"
|
||||||
@click="drawer = true"
|
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
|
||||||
>
|
@click="drawer = true"
|
||||||
<Icon color="#fff" icon="ep:setting" />
|
>
|
||||||
</div>
|
<Icon color="#fff" icon="ep:setting" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
|
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<!-- 主题 -->
|
<!-- 主题 -->
|
||||||
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
|
|
||||||
<!-- 布局 -->
|
<!-- 布局 -->
|
||||||
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
||||||
<LayoutRadioPicker />
|
<LayoutRadioPicker />
|
||||||
|
|
||||||
<!-- 系统主题 -->
|
<!-- 系统主题 -->
|
||||||
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
|
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
|
||||||
<ColorRadioPicker
|
|
||||||
v-model="systemTheme"
|
|
||||||
:schema="[
|
|
||||||
'#409eff',
|
|
||||||
'#009688',
|
|
||||||
'#536dfe',
|
|
||||||
'#ff5c93',
|
|
||||||
'#ee4f12',
|
|
||||||
'#0096c7',
|
|
||||||
'#9c27b0',
|
|
||||||
'#ff9800'
|
|
||||||
]"
|
|
||||||
@change="setSystemTheme"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 头部主题 -->
|
|
||||||
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
|
|
||||||
<ColorRadioPicker
|
|
||||||
v-model="headerTheme"
|
|
||||||
:schema="[
|
|
||||||
'#fff',
|
|
||||||
'#151515',
|
|
||||||
'#5172dc',
|
|
||||||
'#e74c3c',
|
|
||||||
'#24292e',
|
|
||||||
'#394664',
|
|
||||||
'#009688',
|
|
||||||
'#383f45'
|
|
||||||
]"
|
|
||||||
@change="setHeaderTheme"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 菜单主题 -->
|
|
||||||
<template v-if="layout !== 'top'">
|
|
||||||
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
|
|
||||||
<ColorRadioPicker
|
<ColorRadioPicker
|
||||||
v-model="menuTheme"
|
v-model="systemTheme"
|
||||||
|
:schema="[
|
||||||
|
'#409eff',
|
||||||
|
'#009688',
|
||||||
|
'#536dfe',
|
||||||
|
'#ff5c93',
|
||||||
|
'#ee4f12',
|
||||||
|
'#0096c7',
|
||||||
|
'#9c27b0',
|
||||||
|
'#ff9800'
|
||||||
|
]"
|
||||||
|
@change="setSystemTheme"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 头部主题 -->
|
||||||
|
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
|
||||||
|
<ColorRadioPicker
|
||||||
|
v-model="headerTheme"
|
||||||
:schema="[
|
:schema="[
|
||||||
'#fff',
|
'#fff',
|
||||||
'#001529',
|
'#151515',
|
||||||
'#212121',
|
'#5172dc',
|
||||||
'#273352',
|
'#e74c3c',
|
||||||
'#191b24',
|
'#24292e',
|
||||||
'#383f45',
|
'#394664',
|
||||||
'#001628',
|
'#009688',
|
||||||
'#344058'
|
'#383f45'
|
||||||
]"
|
]"
|
||||||
@change="setMenuTheme"
|
@change="setHeaderTheme"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 界面显示 -->
|
<!-- 菜单主题 -->
|
||||||
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
<template v-if="layout !== 'top'">
|
||||||
<InterfaceDisplay />
|
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
|
||||||
|
<ColorRadioPicker
|
||||||
|
v-model="menuTheme"
|
||||||
|
:schema="[
|
||||||
|
'#fff',
|
||||||
|
'#1e293b', // 深靛蓝(现代科技感)
|
||||||
|
'#2c3e50', // 午夜蓝(优雅深邃)
|
||||||
|
'#252b3a', // 深蓝灰(专业稳重)
|
||||||
|
'#2b2d42', // 深藏青(高端大气)
|
||||||
|
'#1a237e', // 靛蓝(科技感强)
|
||||||
|
'#263238', // 蓝灰(沉稳现代)
|
||||||
|
'#1f2937' // 石墨蓝(简约时尚)
|
||||||
|
]"
|
||||||
|
@change="setMenuTheme"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElDivider />
|
<!-- 界面显示 -->
|
||||||
<div>
|
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
||||||
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
|
<InterfaceDisplay />
|
||||||
</div>
|
|
||||||
<div class="mt-5px">
|
<ElDivider />
|
||||||
<ElButton class="w-full" type="danger" @click="clear">
|
<div>
|
||||||
{{ t('setting.clearAndReset') }}
|
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
|
||||||
</ElButton>
|
</div>
|
||||||
</div>
|
<div class="mt-5px">
|
||||||
</ElDrawer>
|
<ElButton class="w-full" type="danger" @click="clear">
|
||||||
|
{{ t('setting.clearAndReset') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElDrawer>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$prefix-cls: #{$namespace}-setting;
|
$prefix-cls: #{$namespace}-setting;
|
||||||
|
|
||||||
.#{$prefix-cls} {
|
.#{$prefix-cls} {
|
||||||
|
z-index: 1200;
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px 0 0 6px;
|
||||||
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
|
|
||||||
|
/* 修正没有z-index会被表格层覆盖,值不要超过4000 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ import type { App } from 'vue'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import remainingRouter from './modules/remaining'
|
import remainingRouter from './modules/remaining'
|
||||||
|
import flowRouter from './modules/flowrouter'
|
||||||
|
|
||||||
// 创建路由实例
|
// 创建路由实例
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||||
strict: true,
|
strict: true,
|
||||||
routes: remainingRouter as RouteRecordRaw[],
|
routes: [...remainingRouter, ...flowRouter] as RouteRecordRaw[],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/flow',
|
||||||
|
name: 'Flow',
|
||||||
|
component: () => import('@/views/flow/Home.vue'),
|
||||||
|
children: [
|
||||||
|
{ path: '', redirect: '/flow/default' },
|
||||||
|
{
|
||||||
|
path: 'default',
|
||||||
|
name: 'default',
|
||||||
|
component: () => import('@/views/flow/HomeDefault.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pricing',
|
||||||
|
name: 'pricing',
|
||||||
|
component: () => import('@/views/flow/Pricing.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'contact',
|
||||||
|
name: 'contact',
|
||||||
|
component: () => import('@/views/flow/Contact.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'features',
|
||||||
|
name: 'features',
|
||||||
|
component: () => import('@/views/flow/Features.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/files',
|
||||||
|
name: 'files',
|
||||||
|
component: () => import('@/views/flow/FileManager.vue'),
|
||||||
|
children: [
|
||||||
|
{ path: '', redirect: '/files/my-files' },
|
||||||
|
{
|
||||||
|
path: 'my-files',
|
||||||
|
name: 'my-files',
|
||||||
|
component: () => import('@/views/flow/files/MyFiles.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'recommended',
|
||||||
|
name: 'recommended',
|
||||||
|
component: () => import('@/views/flow/files/Recommended.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'recent',
|
||||||
|
name: 'recent',
|
||||||
|
component: () => import('@/views/flow/files/Recent.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/flow/Register',
|
||||||
|
name: 'Register',
|
||||||
|
component: () => import('@/views/flow/Register.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/flow-editor',
|
||||||
|
name: 'FlowEditor',
|
||||||
|
component: () => import('@/components/wu-flow-lib/components/FlowEditor.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: () => import('@/views/flow/Register.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login2',
|
||||||
|
name: 'Login2',
|
||||||
|
component: () => import('@/views/flow/Login.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -53,7 +53,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/index',
|
redirect: '/flow',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
meta: {},
|
meta: {},
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@
|
||||||
|
|
||||||
--left-menu-min-width: 64px;
|
--left-menu-min-width: 64px;
|
||||||
|
|
||||||
--left-menu-bg-color: #001529;
|
--left-menu-bg-color: #1e293b;
|
||||||
|
|
||||||
--left-menu-bg-light-color: #0f2438;
|
--left-menu-bg-light-color: #2c3e50;
|
||||||
|
|
||||||
--left-menu-bg-active-color: var(--el-color-primary);
|
--left-menu-bg-active-color: var(--el-color-primary);
|
||||||
|
|
||||||
--left-menu-text-color: #bfcbd9;
|
--left-menu-text-color: #e2e8f0;
|
||||||
|
|
||||||
--left-menu-text-active-color: #fff;
|
--left-menu-text-active-color: #fff;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-table v-loading="loading" :data="list">
|
<el-table v-loading="loading" :data="list">
|
||||||
<el-table-column label="定义编号" align="center" prop="id" width="400" />
|
<el-table-column label="定义编号" align="center" prop="id" width="400" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
|
|
||||||
|
|
||||||
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
|
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
|
||||||
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
|
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
|
||||||
<el-tabs tab-position="left" v-model="categoryActive">
|
<el-tabs tab-position="left" v-model="categoryActive">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,86 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
<ContentWrap>
|
class="-mb-15px"
|
||||||
<!-- 搜索工作栏 -->
|
:model="queryParams"
|
||||||
<el-form
|
ref="queryFormRef"
|
||||||
class="-mb-15px"
|
:inline="true"
|
||||||
:model="queryParams"
|
label-width="68px"
|
||||||
ref="queryFormRef"
|
>
|
||||||
:inline="true"
|
<el-form-item label="发起人" prop="startUserId">
|
||||||
label-width="68px"
|
<el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
|
||||||
>
|
<el-option
|
||||||
<el-form-item label="发起人" prop="startUserId">
|
v-for="user in userList"
|
||||||
<el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
|
:key="user.id"
|
||||||
<el-option
|
:label="user.nickname"
|
||||||
v-for="user in userList"
|
:value="user.id"
|
||||||
:key="user.id"
|
|
||||||
:label="user.nickname"
|
|
||||||
:value="user.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="流程名称" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model="queryParams.name"
|
|
||||||
placeholder="请输入流程名称"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
class="!w-240px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
<el-form-item label="所属流程" prop="processDefinitionId">
|
</el-form-item>
|
||||||
<el-input
|
<el-form-item label="流程名称" prop="name">
|
||||||
v-model="queryParams.processDefinitionId"
|
<el-input
|
||||||
placeholder="请输入流程定义的编号"
|
v-model="queryParams.name"
|
||||||
clearable
|
placeholder="请输入流程名称"
|
||||||
@keyup.enter="handleQuery"
|
clearable
|
||||||
class="!w-240px"
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属流程" prop="processDefinitionId">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.processDefinitionId"
|
||||||
|
placeholder="请输入流程定义的编号"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="流程分类" prop="category">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.category"
|
||||||
|
placeholder="请选择流程分类"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categoryList"
|
||||||
|
:key="category.code"
|
||||||
|
:label="category.name"
|
||||||
|
:value="category.code"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
<el-form-item label="流程分类" prop="category">
|
</el-form-item>
|
||||||
<el-select
|
<el-form-item label="流程状态" prop="status">
|
||||||
v-model="queryParams.category"
|
<el-select
|
||||||
placeholder="请选择流程分类"
|
v-model="queryParams.status"
|
||||||
clearable
|
placeholder="请选择流程状态"
|
||||||
class="!w-240px"
|
clearable
|
||||||
>
|
class="!w-240px"
|
||||||
<el-option
|
>
|
||||||
v-for="category in categoryList"
|
<el-option
|
||||||
:key="category.code"
|
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
|
||||||
:label="category.name"
|
:key="dict.value"
|
||||||
:value="category.code"
|
:label="dict.label"
|
||||||
/>
|
:value="dict.value"
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="流程状态" prop="status">
|
|
||||||
<el-select
|
|
||||||
v-model="queryParams.status"
|
|
||||||
placeholder="请选择流程状态"
|
|
||||||
clearable
|
|
||||||
class="!w-240px"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
|
|
||||||
:key="dict.value"
|
|
||||||
:label="dict.label"
|
|
||||||
:value="dict.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="发起时间" prop="createTime">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="queryParams.createTime"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
type="daterange"
|
|
||||||
start-placeholder="开始日期"
|
|
||||||
end-placeholder="结束日期"
|
|
||||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
|
||||||
class="!w-240px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
<el-form-item label="发起时间" prop="createTime">
|
||||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
<el-date-picker
|
||||||
</el-form-item>
|
v-model="queryParams.createTime"
|
||||||
</el-form>
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
</ContentWrap>
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||||
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
|
|
||||||
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
|
|
||||||
<doc-alert
|
|
||||||
title="审批转办、委派、抄送"
|
|
||||||
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
|
|
||||||
/>
|
|
||||||
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
|
|
||||||
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
|
|
||||||
<doc-alert
|
|
||||||
title="审批转办、委派、抄送"
|
|
||||||
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
|
|
||||||
/>
|
|
||||||
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【通用】跟进记录、待办事项" url="https://doc.iocoder.cn/crm/follow-up/" />
|
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="4" class="min-w-[200px]">
|
<el-col :span="4" class="min-w-[200px]">
|
||||||
<div class="side-item-list">
|
<div class="side-item-list">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【线索】线索管理" url="https://doc.iocoder.cn/crm/clue/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-tabs>
|
<el-tabs>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
|
|
||||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="ERP 手册(功能开启)" url="https://doc.iocoder.cn/erp/build/" />
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- 销售/采购的全局统计 -->
|
<!-- 销售/采购的全局统计 -->
|
||||||
<el-row :gutter="16" class="row">
|
<el-row :gutter="16" class="row">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" />
|
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
|
|
||||||