feat: 多个页面功能优化与新增

1.  异议页面:新增状态为4时的导出PDF按钮,替换原有注释的打印按钮
2.  流程图页面:重构流程图组件,支持多流程切换、节点点击事件,优化主题配置和渲染逻辑
3.  钢卷待领页面:新增钢卷数据修正功能,新增操作列按钮和对应弹窗表单,注释绑定组件
4.  导出PDF弹窗:优化多钢卷数据展示,拆分合并附件排版,优化导出样式和分页逻辑
This commit is contained in:
2026-06-30 11:42:03 +08:00
parent 89773b273b
commit 81dec034b3
4 changed files with 873 additions and 330 deletions

View File

@@ -27,7 +27,7 @@
</div>
<div v-loading="loading" class="flow-content">
<div ref="diagram" class="flow-diagram" v-html="currentSvg" @click="onNodeClick"></div>
<div ref="diagram" class="flow-diagram" v-html="currentSvg"></div>
</div>
</div>
</template>
@@ -35,7 +35,7 @@
<script>
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
const theme = {
const MERMAID_THEME = {
...THEMES['zinc-light'],
padding: 24,
nodeSpacing: 28,
@@ -43,35 +43,75 @@ const theme = {
font: 'Inter, "Microsoft YaHei", sans-serif',
}
const diagrams = {
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/>形成最终处理方案<br/>指定执行部门"]:::approve
G --> H["<b>部门执行</b><br/>执行处理方案"]:::execute
H --> I["<b>返回执行结果</b><br/>提交执行报告"]:::result
I --> J(["<b>售后单封存</b><br/>流程结束"]):::end
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' },
]
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 execute fill:#fffbe6,stroke:#fadb14,color:#303133,stroke-width:2px
classDef result fill:#f6ffed,stroke:#52c41a,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
`,
const NODE_EVENT_CONFIG = {
steelFullChain: [
{ id: 'A', label: '销售部创建合同', handler: 'handleClick', params: { action: 'openContract' } },
{ id: 'B', label: '原料卷到货', handler: 'handleClick', params: { action: 'openRawMaterial' } },
{ id: 'C', label: '入库验收', handler: 'handleClick', params: { action: 'openWarehouse' } },
{ id: 'D', label: '登记原料库存', handler: 'handleClick', params: { action: 'openWarehouse' } },
{ id: 'E', label: '成品钢卷加工', handler: 'handleClick', params: { action: 'openProduction' } },
{ id: 'I', label: '登记质量缺陷', handler: 'handleClick', params: { action: 'openQuality' } },
{ id: 'J', label: '质量等级判定', handler: 'handleClick', params: { action: 'openQuality' } },
{ id: 'K', label: '单卷档案', handler: 'handleClick', params: { action: 'openArchive' } },
{ id: 'L', label: '编排发货计划', handler: 'handleClick', params: { action: 'openShipping' } },
{ id: 'M', label: '生成发货单', handler: 'handleClick', params: { action: 'openShipping' } },
{ 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: 'handleClick', params: { action: 'openAlert' } },
{ id: 'X', label: '生产数据报表统计', handler: 'handleClick', params: { action: 'openReport' } },
],
afterSales: [
{ id: 'A', label: '创建售后单', handler: 'handleClick', params: { action: 'openAfterSalesCreate' } },
{ id: 'C', label: '生产部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'production' } },
{ id: 'D', label: '质量部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'quality' } },
{ id: 'E', label: '销售部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'sales' } },
{ id: 'G', label: '售后负责人汇总', handler: 'handleClick', params: { action: 'openAfterSalesSummary' } },
],
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
@@ -121,38 +161,66 @@ graph TD
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["<b>上传实盘库存Excel</b>"]:::c5
E --> F["<b>执行对比</b><br/>快照 vs 实盘<br/>自动计算差异"]:::c6
C --> D["<b>提交审批</b>"]:::c4
F --> G["<b>查看差异明细</b><br/>保存差异并填写处理方式"]:::c7
G --> H["<b>提交送审</b>"]:::c8
D --> E{"审批"}:::dec
E -->|不通过| F["<b>退回修改</b>"]:::c5
F --> B
E -->|通过| G["<b>生成系统库存快照</b>"]:::c6
H --> I{"审批"}:::dec
I -->|不通过| J["<b>退回修改</b>"]:::c7
J --> G
I -->|通过| K["<b>开始处理差异</b><br/>逐项执行处理方式"]:::c9
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{"所有差异<br/>处理完成?"}:::dec
L -->|否| K
L -->|是| M(["<b>完结流程</b><br/>盘库结束"]):::cend
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:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef c6 fill:#fffbe6,stroke:#fadb14,color:#606266,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:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef c9 fill:#f6ffed,stroke:#52c41a,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
@@ -226,22 +294,42 @@ graph TD
export default {
name: 'FlowChart',
data() {
return {
loading: false,
activeTab: 'steelFullChain',
tabs: [
{ 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' },
],
tabs: TAB_LIST,
svgCache: {},
selectedNode: null,
downloadLoading: false,
}
},
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(() => {
@@ -249,22 +337,41 @@ export default {
})
},
},
computed: {
currentSvg() {
const code = diagrams[this.activeTab]
if (!code) return ''
if (!this.svgCache[this.activeTab]) {
try {
this.svgCache[this.activeTab] = renderMermaidSVG(code, 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]
},
},
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()
@@ -272,25 +379,30 @@ export default {
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)
@@ -308,10 +420,13 @@ export default {
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) {
@@ -320,12 +435,15 @@ export default {
}
}, '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
@@ -335,40 +453,10 @@ export default {
document.body.removeChild(a)
URL.revokeObjectURL(url)
},
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 groups = svg.querySelectorAll('g:not([class*="edge"])')
groups.forEach(g => {
const hasRect = g.querySelector('rect, path, ellipse, polygon')
const text = g.querySelector('text, span')
if (hasRect && text) {
g.style.cursor = 'pointer'
}
})
},
onNodeClick(e) {
let target = e.target
while (target && target !== e.currentTarget) {
if (target.tagName === 'g') {
const textEl = target.querySelector('text, span')
if (textEl) {
const label = textEl.textContent.replace(/\s+/g, ' ').trim()
if (label) {
this.selectedNode = label
this.$message({ message: label, type: 'info', duration: 1500 })
return
}
}
}
target = target.parentElement
}
handleClick({ id, label, params }) {
console.log('[FlowChart] Node clicked:', { id, label, params })
this.$message({ message: `${label}`, type: 'info', duration: 1500 })
},
},
}