feat: 新增盘库管理全流程功能模块

1.  新增批量新增盘库差异记录API
2.  新增盘库Excel对比工具函数
3.  新增盘库申请页面与库区明细组件
4.  优化流程图页面,新增流程图下载功能
5.  重构盘库主页面流程状态与操作逻辑
6.  新增多组件拆分与页面模块化改造
This commit is contained in:
2026-06-26 15:41:21 +08:00
parent dc47a91d0f
commit 39eaab139e
9 changed files with 2994 additions and 1613 deletions

View File

@@ -13,6 +13,19 @@
</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" @click="onNodeClick"></div>
</div>
@@ -110,80 +123,105 @@ graph TD
inventoryCheck: `
graph TD
A["<b>创建盘库</b><br/>填写盘库单基本信息<br/>添加盘库计划"]:::c1
A["<b>创建盘库计划</b><br/>填写基本信息"]:::c1
A --> B["<b>盘库计划</b><br/>设定起止时间<br/>选择库区类型"]:::c2
B --> C{"<b>库区类型?</b>"}:::cdec
A --> B["<b>创建计划明细</b><br/>可创建多个明细"]:::c2
C -->|逻辑库| D["<b>获取库存快照</b><br/>记录当前时间节点<br/>库存情况"]:::c3
C -->|物理库| E["<b>获取库存快照</b><br/>记录当前时间节点<br/>库存情况"]:::c3
E --> F["<b>记录吞吐记录</b><br/>额外记录物理库<br/>出入库流水明细"]:::c4
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
D --> G["<b>人工实地盘库</b><br/>按盘库计划执行<br/>录入实际库存数据"]:::c5
F --> G
F --> G["<b>查看差异明细</b><br/>保存差异并填写处理方式"]:::c7
G --> H["<b>提交送审</b>"]:::c8
G --> H["<b>系统自动对照</b><br/>快照库存 vs 实际库存<br/>逐项比对查找差异"]:::c6
H --> I{"审批"}:::dec
I -->|不通过| J["<b>退回修改</b>"]:::c7
J --> G
I -->|通过| K["<b>开始处理差异</b><br/>逐项执行处理方式"]:::c9
H --> I["<b>盘亏明细</b><br/>系统有 实际无<br/>库存缺失项"]:::loss
H --> J["<b>盘盈明细</b><br/>实际有 系统无<br/>库存多出项"]:::gain
H --> K["<b>明细差异</b><br/>数据不一致<br/>数量/规格偏差"]:::diff
I --> L["<b>生成盘库差异报告</b><br/>汇总盘亏/盘盈/差异<br/>存储差异记录"]:::c7
J --> L
K --> L
L --> M(["<b>盘库单封存</b><br/>流程结束"]):::cend
K --> L{"所有差异<br/>处理完成?"}:::dec
L -->|否| K
L -->|是| M(["<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 cdec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
classDef c3 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
classDef c4 fill:#fff7e6,stroke:#fa8c16,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:#303133,stroke-width:2px
classDef loss fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
classDef gain fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
classDef diff fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef c6 fill:#fffbe6,stroke:#fadb14,color:#606266,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 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: `
stateDiagram-v2
[*] --> 创建排产单: 填写基本信息
创建排产单 --> 关联合同获取需求: 选择合同
关联合同获取需求 --> 选择需求合并明细: 选取需求<br/>合并相同条目
选择需求合并明细 --> 提交审批
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: `
stateDiagram-v2
[*] --> 创建维修计划: 点选异常巡检记录<br/>绑定记录与异常设备
创建维修计划 --> 审批维修计划
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
审批通过 --> 逐设备维修记录: 逐一执行设备维修<br/>记录维修过程与结果
逐设备维修记录 --> 逐设备维修记录: 存在未维修设备
逐设备维修记录 --> [*]: 全部设备维修完成<br/>流程结束
`
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 {
@@ -201,6 +239,7 @@ export default {
],
svgCache: {},
selectedNode: null,
downloadLoading: false,
}
},
watch: {
@@ -226,6 +265,76 @@ export default {
},
},
methods: {
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)
},
switchTab(key) {
this.activeTab = key
this.selectedNode = null
@@ -331,4 +440,12 @@ export default {
.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;
}
</style>