1. 新增弹窗组件容器,实现节点点击后动态加载对应页面组件 2. 替换原有硬编码的跳转逻辑,统一使用handleOpen方法处理节点点击 3. 注释暂时不需要的流程节点配置,简化当前可用流程 4. 新增弹窗相关状态与样式,优化弹窗展示效果
612 lines
23 KiB
Vue
612 lines
23 KiB
Vue
<template>
|
|
<div class="app-container flow-page">
|
|
<div class="flow-tabs">
|
|
<span
|
|
v-for="tab in tabs"
|
|
:key="tab.key"
|
|
class="flow-tab-item"
|
|
:class="{ active: activeTab === tab.key }"
|
|
@click="switchTab(tab.key)"
|
|
>
|
|
<i :class="tab.icon"></i>
|
|
{{ tab.label }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flow-toolbar">
|
|
<el-dropdown trigger="click" @command="handleDownload">
|
|
<el-button size="small" type="primary" plain>
|
|
<i class="el-icon-download"></i> 下载流程图
|
|
<i class="el-icon-arrow-down el-icon--right"></i>
|
|
</el-button>
|
|
<el-dropdown-menu slot="dropdown">
|
|
<el-dropdown-item command="svg">下载 SVG</el-dropdown-item>
|
|
<el-dropdown-item command="png">下载 PNG</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</el-dropdown>
|
|
</div>
|
|
|
|
<div v-loading="loading" class="flow-content">
|
|
<div ref="diagram" class="flow-diagram" v-html="currentSvg"></div>
|
|
</div>
|
|
|
|
<el-dialog
|
|
:title="dialogTitle"
|
|
:visible.sync="dialogVisible"
|
|
:width="dialogWidth"
|
|
top="5vh"
|
|
fullscreen
|
|
:close-on-click-modal="false"
|
|
@closed="handleDialogClosed"
|
|
>
|
|
<div v-loading="dialogLoading" class="flow-dialog-body">
|
|
<component
|
|
v-if="dynamicComponent && !dialogLoading"
|
|
:is="dynamicComponent"
|
|
:key="dialogKey"
|
|
/>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
|
|
|
|
const MERMAID_THEME = {
|
|
...THEMES['zinc-light'],
|
|
padding: 24,
|
|
nodeSpacing: 28,
|
|
layerSpacing: 44,
|
|
font: 'Inter, "Microsoft YaHei", sans-serif',
|
|
}
|
|
|
|
const TAB_LIST = [
|
|
{ key: 'steelFullChain', label: '生产全链路流程', icon: 'el-icon-s-operation' },
|
|
{ key: 'afterSales', label: '售后处理流程', icon: 'el-icon-s-claim' },
|
|
{ key: 'inventoryCheck', label: '盘库流程', icon: 'el-icon-s-check' },
|
|
{ key: 'productionSchedule', label: '排产流程', icon: 'el-icon-s-order' },
|
|
// { key: 'equipmentRepair', label: '设备维修流程', icon: 'el-icon-s-tools' },
|
|
]
|
|
|
|
const NODE_EVENT_CONFIG = {
|
|
steelFullChain: [
|
|
{ id: 'A', label: '销售部创建合同', handler: 'handleOpen', params: { componentPath: 'crm/contract/index' } },
|
|
{ id: 'B', label: '原料卷到货', handler: 'handleOpen', params: { componentPath: 'wms/receive/plan/index' } },
|
|
{ id: 'C', label: '入库验收', handler: 'handleOpen', params: { componentPath: 'wms/coil/do/warehousing' } },
|
|
{ id: 'D', label: '登记原料库存', handler: 'handleOpen', params: { componentPath: 'wms/coil/do/warehousing' } },
|
|
// { id: 'E', label: '成品钢卷加工', handler: 'handleClick', params: { action: 'openProduction' } },
|
|
{ id: 'I', label: '登记质量缺陷', handler: 'handleOpen', params: { componentPath: 'wms/coil/abnormal/extend' } },
|
|
{ id: 'J', label: '质量等级判定', handler: 'handleOpen', params: { componentPath: 'wms/coil/abnormal/extend' } },
|
|
// { id: 'K', label: '单卷档案', handler: 'handleClick', params: { action: 'openArchive' } },
|
|
{ id: 'L', label: '编排发货计划', handler: 'handleOpen', params: { componentPath: 'wms/delivery/waybill/index' } },
|
|
{ id: 'M', label: '生成发货单', handler: 'handleOpen', params: { componentPath: 'wms/delivery/bills/index' } },
|
|
// { id: 'N', label: '发货质量校验', handler: 'handleClick', params: { action: 'openQuality' } },
|
|
// { id: 'O', label: '出库发货', handler: 'handleClick', params: { action: 'openShipping' } },
|
|
// { id: 'P', label: '禁止出库', handler: 'handleClick', params: { action: 'openShipping' } },
|
|
// { id: 'Q', label: '钢卷库存管理', handler: 'handleClick', params: { action: 'openInventory' } },
|
|
// { id: 'V', label: '生产过程数据异常检测', handler: 'handleClick', params: { action: 'openAlert' } },
|
|
{ id: 'W', label: '自动触发告警', handler: 'handleOpen', params: { componentPath: 'wms/coil/materialWarning/index' } },
|
|
// { id: 'X', label: '生产数据报表统计', handler: 'handleClick', params: { action: 'openReport' } },
|
|
],
|
|
afterSales: [
|
|
{ id: 'A', label: '创建售后单', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/index' } },
|
|
// { id: 'C', label: '生产部出具处理意见', handler: 'handleOpen', params: { action: 'openAfterSalesDept', dept: 'production' } },
|
|
// { id: 'D', label: '质量部出具处理意见', handler: 'handleOpen', params: { action: 'openAfterSalesDept', dept: 'quality' } },
|
|
// { id: 'E', label: '销售部出具处理意见', handler: 'handleOpen', params: { action: 'openAfterSalesDept', dept: 'sales' } },
|
|
{ id: 'G', label: '售后负责人汇总', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/summary' } },
|
|
],
|
|
inventoryCheck: [
|
|
{ id: 'A', label: '创建盘库计划', handler: 'handleClick', params: { action: 'openInventoryPlan' } },
|
|
{ id: 'B', label: '创建计划明细', handler: 'handleClick', params: { action: 'openInventoryPlanDetail' } },
|
|
{ id: 'C', label: '选择库区', handler: 'handleClick', params: { action: 'openInventoryPlanDetail' } },
|
|
{ id: 'D', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'G', label: '生成系统库存快照', handler: 'handleClick', params: { action: 'openInventorySnapshot' } },
|
|
{ id: 'H', label: '上传实盘库存Excel', handler: 'handleClick', params: { action: 'openInventoryUpload' } },
|
|
{ id: 'I', label: '执行对比', handler: 'handleClick', params: { action: 'openInventoryCompare' } },
|
|
{ id: 'J', label: '查看差异明细', handler: 'handleClick', params: { action: 'openInventoryDiff' } },
|
|
{ id: 'K', label: '再次提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'N', label: '执行处理差异', handler: 'handleClick', params: { action: 'openInventoryExecute' } },
|
|
],
|
|
productionSchedule: [
|
|
{ id: 'A', label: '创建需求单', handler: 'handleClick', params: { action: 'openScheduleCreate' } },
|
|
{ id: 'B', label: '选择合同', handler: 'handleClick', params: { action: 'openScheduleContract' } },
|
|
{ id: 'C', label: '自动获取需求明细', handler: 'handleClick', params: { action: 'openScheduleDetail' } },
|
|
{ id: 'D', label: '调整需求明细', handler: 'handleClick', params: { action: 'openScheduleDetail' } },
|
|
{ id: 'E', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'H', label: '转化为排产单', handler: 'handleClick', params: { action: 'openScheduleConvert' } },
|
|
{ id: 'I', label: '排产单', handler: 'handleClick', params: { action: 'openScheduleEdit' } },
|
|
{ id: 'J', label: '再次提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'M', label: '提交给车间', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'N', label: '车间绑定钢卷', handler: 'handleClick', params: { action: 'openScheduleBind' } },
|
|
{ id: 'O', label: '执行生产', handler: 'handleClick', params: { action: 'openScheduleExecute' } },
|
|
],
|
|
equipmentRepair: [
|
|
{ id: 'A', label: '创建维修计划', handler: 'handleClick', params: { action: 'openRepairCreate' } },
|
|
{ id: 'A1', label: '点选异常巡检记录', handler: 'handleClick', params: { action: 'openRepairSelect' } },
|
|
{ id: 'B', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
|
|
{ id: 'E', label: '逐设备维修记录', handler: 'handleClick', params: { action: 'openRepairExecute' } },
|
|
],
|
|
}
|
|
|
|
const DIAGRAMS = {
|
|
steelFullChain: `
|
|
graph TD
|
|
A["<b>销售部创建合同</b><br/>录入产品所需内容<br/>规格/数量/技术标准"]:::s1
|
|
|
|
A --> B["<b>原料卷到货</b>"]:::s2
|
|
B --> C["<b>入库验收</b>"]:::s2
|
|
C --> D["<b>登记原料库存</b>"]:::s2
|
|
|
|
D --> E["<b>成品钢卷加工</b>"]:::s3
|
|
|
|
E --> F["加工前质检"]:::s3qc
|
|
E --> G["加工中质检"]:::s3qc
|
|
E --> H["加工完工质检"]:::s3qc
|
|
|
|
F --> I["<b>登记质量缺陷</b><br/>各工序均可录入"]:::s3
|
|
G --> I
|
|
H --> I
|
|
|
|
I --> J["<b>质量等级判定</b><br/>依据质检标准"]:::s3
|
|
J --> K["<b>单卷档案</b><br/>缺陷记录与等级<br/>全程绑定留存"]:::s3
|
|
|
|
K --> L["<b>编排发货计划</b><br/>基于合同信息"]:::s4
|
|
L --> M["<b>生成发货单</b>"]:::s4
|
|
M --> N{"<b>发货质量校验</b><br/>缺陷/等级检查"}:::dec
|
|
N -->|达标| O["<b>出库发货</b>"]:::s4
|
|
N -->|不达标| P["<b>禁止出库</b>"]:::s5
|
|
|
|
K --> Q["<b>钢卷库存管理</b>"]:::s4
|
|
Q --> R["<b>逻辑库</b><br/>按功能/用途分类"]:::s4
|
|
Q --> S["<b>物理库</b><br/>实际存放场地"]:::s4
|
|
R --> T["<b>跨库/跨区调拨</b>"]:::s4
|
|
S --> T
|
|
T --> U["<b>实时更新库存数据</b>"]:::s4
|
|
|
|
E --> V{"<b>生产过程<br/>数据异常检测</b>"}:::dec
|
|
V -->|异常| W["<b>自动触发告警</b><br/>推送异常信息至<br/>业务人员核查处置"]:::s5
|
|
|
|
K --> X["<b>生产数据报表统计</b><br/>原料/加工/质检<br/>库存/发货全周期汇总"]:::s4
|
|
|
|
classDef s1 fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
|
|
classDef s2 fill:#e6fffa,stroke:#13c2c2,color:#303133,stroke-width:2px
|
|
classDef s3 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
|
|
classDef s3qc fill:#fffbe6,stroke:#fadb14,color:#303133,stroke-width:2px
|
|
classDef s4 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef s5 fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
|
|
classDef dec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
|
|
linkStyle default stroke:#bfbfbf,stroke-width:2px
|
|
`,
|
|
|
|
afterSales: `
|
|
graph TD
|
|
A["<b>创建售后单</b><br/>填写基本信息<br/>选择需售后处理的钢卷"]:::step
|
|
A --> B["<b>多部门并行处理</b>"]:::fork
|
|
B --> C["<b>生产部</b><br/>出具处理意见"]:::dept1
|
|
B --> D["<b>质量部</b><br/>出具处理意见"]:::dept2
|
|
B --> E["<b>销售部</b><br/>出具处理意见"]:::dept3
|
|
C --> F{"三个部门<br/>全部提交?"}:::decision
|
|
D --> F
|
|
E --> F
|
|
F -->|已全部提交| G["<b>售后负责人</b><br/>汇总各部门意见<br/>直接办结归档"]:::approve
|
|
G --> J(["<b>售后单封存</b><br/>流程结束"]):::end
|
|
|
|
classDef step fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
|
|
classDef fork fill:#f0f5ff,stroke:#409eff,color:#303133,stroke-width:2px
|
|
classDef dept1 fill:#e6fffa,stroke:#00b4a0,color:#303133,stroke-width:2px
|
|
classDef dept2 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
|
|
classDef dept3 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
|
|
classDef decision fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
|
|
classDef approve fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
|
|
classDef end fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
|
|
linkStyle default stroke:#bfbfbf,stroke-width:2px
|
|
`,
|
|
|
|
inventoryCheck: `
|
|
graph TD
|
|
A["<b>创建盘库计划</b><br/>填写基本信息"]:::c1
|
|
A --> B["<b>创建计划明细</b><br/>可创建多个明细"]:::c2
|
|
B --> C["<b>选择库区</b><br/>逻辑库 / 实际库<br/>至少选一个"]:::c3
|
|
C --> D["<b>提交审批</b>"]:::c4
|
|
|
|
D --> E{"审批"}:::dec
|
|
E -->|不通过| F["<b>退回修改</b>"]:::c5
|
|
F --> B
|
|
E -->|通过| G["<b>生成系统库存快照</b>"]:::c6
|
|
|
|
G --> H["<b>上传实盘库存Excel</b>"]:::c7
|
|
H --> I["<b>执行对比</b><br/>快照 vs 实盘<br/>自动计算差异"]:::c8
|
|
I --> J["<b>查看差异明细</b><br/>保存差异并填写处理方式"]:::c9
|
|
J --> K["<b>再次提交审批</b>"]:::c10
|
|
|
|
K --> L{"审批"}:::dec
|
|
L -->|不通过| M["<b>退回修改</b>"]:::c11
|
|
M --> J
|
|
L -->|通过| N["<b>执行处理差异</b><br/>逐项执行并填写执行结果"]:::c12
|
|
|
|
N --> O(["<b>归档办结</b><br/>盘库结束"]):::cend
|
|
|
|
classDef c1 fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
|
|
classDef c2 fill:#e6fffa,stroke:#13c2c2,color:#303133,stroke-width:2px
|
|
classDef c3 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
|
|
classDef c4 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef c5 fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
|
|
classDef c6 fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
|
|
classDef c7 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef c8 fill:#fffbe6,stroke:#fadb14,color:#606266,stroke-width:2px
|
|
classDef c9 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
|
|
classDef c10 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef c11 fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
|
|
classDef c12 fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
|
|
classDef dec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
|
|
classDef cend fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
|
|
linkStyle default stroke:#bfbfbf,stroke-width:2px
|
|
`,
|
|
|
|
productionSchedule: `
|
|
graph TD
|
|
A["<b>创建需求单</b><br/>填写基本信息"]:::p1
|
|
|
|
A --> B["<b>选择合同</b><br/>可选一个或多个合同"]:::p2
|
|
B --> C["<b>自动获取需求明细</b><br/>从所选合同提取"]:::p3
|
|
C --> D["<b>调整需求明细</b><br/>可编辑/修改/补充"]:::p4
|
|
|
|
D --> E["<b>提交审批</b>"]:::p5
|
|
E --> F{"审批"}:::dec
|
|
F -->|不通过| G["<b>退回修改</b>"]:::p4
|
|
G --> D
|
|
F -->|通过| H["<b>转化为排产单</b>"]:::p6
|
|
|
|
H --> I["<b>排产单</b><br/>可再次编辑"]:::p7
|
|
I --> J["<b>再次提交审批</b>"]:::p5
|
|
J --> K{"审批"}:::dec
|
|
K -->|不通过| L["<b>退回修改</b>"]:::p7
|
|
L --> I
|
|
K -->|通过| M["<b>提交给车间</b>"]:::p8
|
|
|
|
M --> N["<b>车间绑定钢卷</b><br/>每个排产计划<br/>绑定一个或多个钢卷"]:::p9
|
|
N --> O["<b>执行生产</b>"]:::p10
|
|
O --> P(["<b>排产完结</b>"]):::pend
|
|
|
|
classDef p1 fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
|
|
classDef p2 fill:#e6fffa,stroke:#13c2c2,color:#303133,stroke-width:2px
|
|
classDef p3 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
|
|
classDef p4 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef p5 fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
|
|
classDef p6 fill:#fffbe6,stroke:#fadb14,color:#606266,stroke-width:2px
|
|
classDef p7 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
|
|
classDef p8 fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
|
|
classDef p9 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef p10 fill:#fffbe6,stroke:#fadb14,color:#303133,stroke-width:2px
|
|
classDef dec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
|
|
classDef pend fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
|
|
linkStyle default stroke:#bfbfbf,stroke-width:2px
|
|
`,
|
|
|
|
equipmentRepair: `
|
|
graph TD
|
|
A["<b>创建维修计划</b>"]:::e1
|
|
A1["点选异常巡检记录<br/>绑定记录与异常设备"]:::e1sub
|
|
A --> A1
|
|
|
|
A1 --> B["<b>提交审批</b>"]:::e2
|
|
B --> C{"审批"}:::edec
|
|
C -->|不通过| D["<b>退回修改</b>"]:::e1
|
|
D --> A1
|
|
C -->|通过| E["<b>逐设备维修记录</b><br/>逐一执行设备维修<br/>记录维修过程与结果"]:::e3
|
|
|
|
E --> F{"全部设备<br/>维修完成?"}:::edec
|
|
F -->|否| E
|
|
F -->|是| G(["<b>流程结束</b>"]):::eend
|
|
|
|
classDef e1 fill:#e6fffa,stroke:#13c2c2,color:#303133,stroke-width:2px
|
|
classDef e1sub fill:#e6fffa,stroke:#13c2c2,color:#606266,stroke-width:1px,stroke-dasharray:3 3
|
|
classDef e2 fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
|
|
classDef e3 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
|
|
classDef edec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
|
|
classDef eend fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
|
|
linkStyle default stroke:#bfbfbf,stroke-width:2px
|
|
`,
|
|
}
|
|
|
|
|
|
export default {
|
|
name: 'FlowChart',
|
|
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
activeTab: 'steelFullChain',
|
|
tabs: TAB_LIST,
|
|
svgCache: {},
|
|
selectedNode: null,
|
|
downloadLoading: false,
|
|
dialogVisible: false,
|
|
dialogTitle: '',
|
|
dialogWidth: '70%',
|
|
dialogLoading: false,
|
|
dynamicComponent: null,
|
|
dialogKey: 0,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
currentSvg() {
|
|
const code = DIAGRAMS[this.activeTab]
|
|
if (!code) return ''
|
|
|
|
if (!this.svgCache[this.activeTab]) {
|
|
try {
|
|
this.svgCache[this.activeTab] = renderMermaidSVG(code, MERMAID_THEME)
|
|
} catch (e) {
|
|
console.error('Mermaid render error:', e)
|
|
return '<p style="color:#f5222d;text-align:center;">流程图渲染失败: ' + e.message + '</p>'
|
|
}
|
|
}
|
|
|
|
return this.svgCache[this.activeTab]
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.$nextTick(() => {
|
|
this.bindNodeEvents()
|
|
})
|
|
},
|
|
|
|
watch: {
|
|
currentSvg() {
|
|
this.$nextTick(() => {
|
|
this.bindNodeEvents()
|
|
})
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
switchTab(key) {
|
|
this.activeTab = key
|
|
this.selectedNode = null
|
|
},
|
|
|
|
bindNodeEvents() {
|
|
const el = this.$refs.diagram
|
|
if (!el) return
|
|
|
|
const svg = el.querySelector('svg')
|
|
if (!svg) return
|
|
|
|
const nodeConfigs = NODE_EVENT_CONFIG[this.activeTab] || []
|
|
nodeConfigs.forEach(config => {
|
|
const node = svg.querySelector(`g[data-id="${config.id}"]`)
|
|
if (!node) return
|
|
|
|
node.style.cursor = 'pointer'
|
|
node.addEventListener('click', (e) => {
|
|
e.stopPropagation()
|
|
const textEl = node.querySelector('text, span')
|
|
const label = textEl ? textEl.textContent.replace(/\s+/g, ' ').trim() : config.label
|
|
this.selectedNode = { dataId: config.id, label, handler: config.handler, params: config.params }
|
|
|
|
if (typeof this[config.handler] === 'function') {
|
|
this[config.handler]({ id: config.id, label, node, params: config.params })
|
|
} else {
|
|
console.warn(`[FlowChart] Handler "${config.handler}" not found`)
|
|
}
|
|
})
|
|
})
|
|
},
|
|
|
|
handleDownload(format) {
|
|
if (format === 'svg') {
|
|
this.downloadSvg()
|
|
} else if (format === 'png') {
|
|
this.downloadPng()
|
|
}
|
|
},
|
|
|
|
downloadSvg() {
|
|
const svgEl = this.$refs.diagram?.querySelector('svg')
|
|
if (!svgEl) {
|
|
this.$message.warning('流程图尚未渲染完成')
|
|
return
|
|
}
|
|
|
|
const clone = svgEl.cloneNode(true)
|
|
const serializer = new XMLSerializer()
|
|
const source = serializer.serializeToString(clone)
|
|
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' })
|
|
|
|
this.triggerDownload(URL.createObjectURL(blob), `${this.activeTab}.svg`)
|
|
this.$message.success('SVG 已下载')
|
|
},
|
|
|
|
downloadPng() {
|
|
const svgEl = this.$refs.diagram?.querySelector('svg')
|
|
if (!svgEl) {
|
|
this.$message.warning('流程图尚未渲染完成')
|
|
return
|
|
}
|
|
|
|
this.downloadLoading = true
|
|
|
|
const clone = svgEl.cloneNode(true)
|
|
const serializer = new XMLSerializer()
|
|
let source = serializer.serializeToString(clone)
|
|
source = source.replace(/<\/?foreignObject[^>]*>/gi, '').replace(/<\/?style[^>]*>/gi, '')
|
|
|
|
const svgBlob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' })
|
|
const url = URL.createObjectURL(svgBlob)
|
|
|
|
const img = new Image()
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas')
|
|
const rect = svgEl.getBoundingClientRect()
|
|
const scale = 2
|
|
canvas.width = rect.width * scale
|
|
canvas.height = rect.height * scale
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.scale(scale, scale)
|
|
ctx.drawImage(img, 0, 0, rect.width, rect.height)
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
canvas.toBlob(blob => {
|
|
this.downloadLoading = false
|
|
if (blob) {
|
|
this.triggerDownload(URL.createObjectURL(blob), `${this.activeTab}.png`)
|
|
this.$message.success('PNG 已下载')
|
|
}
|
|
}, 'image/png')
|
|
}
|
|
|
|
img.onerror = () => {
|
|
this.downloadLoading = false
|
|
this.$message.error('PNG 导出失败')
|
|
}
|
|
|
|
img.src = url
|
|
},
|
|
|
|
triggerDownload(url, filename) {
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = filename
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
URL.revokeObjectURL(url)
|
|
},
|
|
|
|
handleClick({ id, label, params }) {
|
|
console.log('[FlowChart] Node clicked:', { id, label, params })
|
|
this.$message({ message: `${label}`, type: 'info', duration: 1500 })
|
|
},
|
|
|
|
handleOpen({ id, label, params }) {
|
|
const { componentPath, dialogWidth = '70%' } = params || {}
|
|
if (!componentPath) {
|
|
this.$message.warning('组件路径不能为空')
|
|
return
|
|
}
|
|
|
|
this.dialogVisible = true
|
|
this.dialogTitle = label
|
|
this.dialogWidth = dialogWidth
|
|
this.dialogLoading = true
|
|
this.dynamicComponent = null
|
|
|
|
this.resolveImport(componentPath)
|
|
.then(module => {
|
|
this.dynamicComponent = module.default || module
|
|
})
|
|
.catch(err => {
|
|
console.error('[FlowChart] Component load error:', err)
|
|
this.$message.error('组件加载失败: ' + (err.message || '未知错误'))
|
|
this.dialogVisible = false
|
|
})
|
|
.finally(() => {
|
|
this.dialogLoading = false
|
|
})
|
|
},
|
|
|
|
resolveImport(componentPath) {
|
|
// 与 src/store/modules/permission.js 的 loadView 同款模式
|
|
// componentPath 相对于 @/views/,如 'crm/order/index'
|
|
if (process.env.NODE_ENV === 'development') {
|
|
return new Promise((resolve, reject) => {
|
|
require([`@/views/${componentPath}`], resolve, reject)
|
|
})
|
|
}
|
|
return import(`@/views/${componentPath}`)
|
|
},
|
|
|
|
handleDialogClosed() {
|
|
this.dynamicComponent = null
|
|
this.dialogKey++
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.flow-page {
|
|
padding: 0;
|
|
}
|
|
|
|
.flow-tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 8px 12px;
|
|
background: #fafafa;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
}
|
|
|
|
.flow-tab-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 5px 16px;
|
|
font-size: 13px;
|
|
color: #606266;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
|
|
.flow-tab-item:hover {
|
|
color: #409eff;
|
|
background: rgba(64, 158, 255, 0.06);
|
|
}
|
|
|
|
.flow-tab-item.active {
|
|
color: #409eff;
|
|
background: #e6f0fd;
|
|
}
|
|
|
|
.flow-tab-item i {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.flow-content {
|
|
min-height: 480px;
|
|
background: #fff;
|
|
overflow: auto;
|
|
}
|
|
|
|
.flow-diagram {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 16px;
|
|
}
|
|
|
|
.flow-diagram :deep(svg) {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.flow-diagram :deep(svg g) {
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.flow-diagram :deep(svg g:hover) {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.flow-toolbar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: 6px 12px;
|
|
background: #fafafa;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
}
|
|
|
|
.flow-dialog-body {
|
|
min-height: 300px;
|
|
}
|
|
</style>
|