Pre Merge pull request !676 from O无忧O/dev1
|
|
@ -62,7 +62,7 @@
|
|||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"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 "
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"@form-create/designer": "^3.2.6",
|
||||
"@form-create/element-ui": "^3.2.11",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
|
|
@ -58,7 +57,9 @@
|
|||
"markmap-toolbar": "^0.17.0",
|
||||
"markmap-view": "^0.16.0",
|
||||
"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",
|
||||
"pinia": "^2.1.7",
|
||||
"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'
|
||||
|
||||
export interface UserVO {
|
||||
id: number
|
||||
id?: number
|
||||
username: string
|
||||
nickname: string
|
||||
deptId: number
|
||||
postIds: string[]
|
||||
email: string
|
||||
mobile: string
|
||||
password: string
|
||||
sex: number
|
||||
avatar: 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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
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>
|
||||
<div>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
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>
|
||||
|
||||
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
|
||||
<template #header>
|
||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||
</template>
|
||||
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
|
||||
<template #header>
|
||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||
</template>
|
||||
|
||||
<div class="text-center">
|
||||
<!-- 主题 -->
|
||||
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
||||
<ThemeSwitch />
|
||||
<div class="text-center">
|
||||
<!-- 主题 -->
|
||||
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
||||
<ThemeSwitch />
|
||||
|
||||
<!-- 布局 -->
|
||||
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
||||
<LayoutRadioPicker />
|
||||
<!-- 布局 -->
|
||||
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
||||
<LayoutRadioPicker />
|
||||
|
||||
<!-- 系统主题 -->
|
||||
<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>
|
||||
<!-- 系统主题 -->
|
||||
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
|
||||
<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="[
|
||||
'#fff',
|
||||
'#001529',
|
||||
'#212121',
|
||||
'#273352',
|
||||
'#191b24',
|
||||
'#383f45',
|
||||
'#001628',
|
||||
'#344058'
|
||||
'#151515',
|
||||
'#5172dc',
|
||||
'#e74c3c',
|
||||
'#24292e',
|
||||
'#394664',
|
||||
'#009688',
|
||||
'#383f45'
|
||||
]"
|
||||
@change="setMenuTheme"
|
||||
@change="setHeaderTheme"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 界面显示 -->
|
||||
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
||||
<InterfaceDisplay />
|
||||
<!-- 菜单主题 -->
|
||||
<template v-if="layout !== 'top'">
|
||||
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="menuTheme"
|
||||
:schema="[
|
||||
'#fff',
|
||||
'#1e293b', // 深靛蓝(现代科技感)
|
||||
'#2c3e50', // 午夜蓝(优雅深邃)
|
||||
'#252b3a', // 深蓝灰(专业稳重)
|
||||
'#2b2d42', // 深藏青(高端大气)
|
||||
'#1a237e', // 靛蓝(科技感强)
|
||||
'#263238', // 蓝灰(沉稳现代)
|
||||
'#1f2937' // 石墨蓝(简约时尚)
|
||||
]"
|
||||
@change="setMenuTheme"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
<div>
|
||||
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<ElButton class="w-full" type="danger" @click="clear">
|
||||
{{ t('setting.clearAndReset') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
<!-- 界面显示 -->
|
||||
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
||||
<InterfaceDisplay />
|
||||
|
||||
<ElDivider />
|
||||
<div>
|
||||
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<ElButton class="w-full" type="danger" @click="clear">
|
||||
{{ t('setting.clearAndReset') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}-setting;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
z-index: 1200;
|
||||
border-radius: 6px 0 0 6px;
|
||||
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
|
||||
|
||||
/* 修正没有z-index会被表格层覆盖,值不要超过4000 */
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import type { App } from 'vue'
|
|||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import remainingRouter from './modules/remaining'
|
||||
import flowRouter from './modules/flowrouter'
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||
strict: true,
|
||||
routes: remainingRouter as RouteRecordRaw[],
|
||||
routes: [...remainingRouter, ...flowRouter] as RouteRecordRaw[],
|
||||
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: '/',
|
||||
component: Layout,
|
||||
redirect: '/index',
|
||||
redirect: '/flow',
|
||||
name: 'Home',
|
||||
meta: {},
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
--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-text-color: #bfcbd9;
|
||||
--left-menu-text-color: #e2e8f0;
|
||||
|
||||
--left-menu-text-active-color: #fff;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="定义编号" align="center" prop="id" width="400" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
|
||||
|
||||
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
|
||||
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
|
||||
<el-tabs tab-position="left" v-model="categoryActive">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,90 +1,86 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="发起人" prop="startUserId">
|
||||
<el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
: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
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="发起人" prop="startUserId">
|
||||
<el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.nickname"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属流程" prop="processDefinitionId">
|
||||
<el-input
|
||||
v-model="queryParams.processDefinitionId"
|
||||
placeholder="请输入流程定义的编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
</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-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-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-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-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-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>
|
||||
</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-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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
<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>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
<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>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【通用】跟进记录、待办事项" url="https://doc.iocoder.cn/crm/follow-up/" />
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4" class="min-w-[200px]">
|
||||
<div class="side-item-list">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【线索】线索管理" url="https://doc.iocoder.cn/crm/clue/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-tabs>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
|
||||
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="ERP 手册(功能开启)" url="https://doc.iocoder.cn/erp/build/" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<!-- 销售/采购的全局统计 -->
|
||||
<el-row :gutter="16" class="row">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||