diff --git a/klp-ui/src/api/flow/countDiscrepancy.js b/klp-ui/src/api/flow/countDiscrepancy.js index 448c2fb24..2d8efd0aa 100644 --- a/klp-ui/src/api/flow/countDiscrepancy.js +++ b/klp-ui/src/api/flow/countDiscrepancy.js @@ -35,6 +35,15 @@ export function updateCountDiscrepancy(data) { }) } +// 批量新增盘库差异记录 +export function batchAddCountDiscrepancy(data) { + return request({ + url: '/flow/countDiscrepancy/batch', + method: 'post', + data: data + }) +} + // 删除盘库差异记录 export function delCountDiscrepancy(discrepancyId) { return request({ diff --git a/klp-ui/src/views/wms/coil/do/trace.vue b/klp-ui/src/views/wms/coil/do/trace.vue new file mode 100644 index 000000000..3e21d4e65 --- /dev/null +++ b/klp-ui/src/views/wms/coil/do/trace.vue @@ -0,0 +1,1610 @@ + + + + + \ No newline at end of file diff --git a/klp-ui/src/views/wms/post/InvCount/apply.vue b/klp-ui/src/views/wms/post/InvCount/apply.vue new file mode 100644 index 000000000..2d0129beb --- /dev/null +++ b/klp-ui/src/views/wms/post/InvCount/apply.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/klp-ui/src/views/wms/post/InvCount/approval.vue b/klp-ui/src/views/wms/post/InvCount/approval.vue index a12a0edb8..b40865379 100644 --- a/klp-ui/src/views/wms/post/InvCount/approval.vue +++ b/klp-ui/src/views/wms/post/InvCount/approval.vue @@ -1,139 +1,49 @@ diff --git a/klp-ui/src/views/wms/post/InvCount/components/WarehouseDetailPanel.vue b/klp-ui/src/views/wms/post/InvCount/components/WarehouseDetailPanel.vue new file mode 100644 index 000000000..2921fb5ab --- /dev/null +++ b/klp-ui/src/views/wms/post/InvCount/components/WarehouseDetailPanel.vue @@ -0,0 +1,606 @@ + + + + diff --git a/klp-ui/src/views/wms/post/InvCount/execute.vue b/klp-ui/src/views/wms/post/InvCount/execute.vue index ececc1d5a..015990298 100644 --- a/klp-ui/src/views/wms/post/InvCount/execute.vue +++ b/klp-ui/src/views/wms/post/InvCount/execute.vue @@ -6,162 +6,47 @@
- 盘库执行 - + 差异处理 +
- - - -
-
- +
-
-
- {{ item.planCode }} - {{ item.planName }} -
-
- 执行中 - 差异处理中 -
-
- -
-
-
- - 暂无执行中的盘库计划 +
{{ item.planCode }}{{ item.planName }}
+
差异处理中
+
暂无差异处理中的计划
- - + - - - - - -
将实盘Excel文件拖到此处,或点击上传
-
仅支持 .xlsx / .xls 格式
-
- -
- - - - - 盘盈 - 盘亏 - 状态不符 - 重量偏差 - - - {{ discForm.enterCoilNo }} - - - {{ discForm.discrepancyDetail }} - - - - - - - - - - - - - diff --git a/klp-ui/src/views/wms/post/InvCount/index.vue b/klp-ui/src/views/wms/post/InvCount/index.vue index 6b5b3c707..92c589554 100644 --- a/klp-ui/src/views/wms/post/InvCount/index.vue +++ b/klp-ui/src/views/wms/post/InvCount/index.vue @@ -12,7 +12,7 @@ - + @@ -36,7 +36,7 @@
草稿 待审批 - 执行中 + 差异处理中 差异处理中 已归档
@@ -81,7 +81,7 @@ Status / 状态: 草稿 待审批 - 执行中 + 差异处理中 差异处理中 已归档 @@ -106,7 +106,6 @@
提交审批 审批通过,开始盘库 - 核对并生成差异报告 归档封存
@@ -115,270 +114,17 @@
库区盘点明细 · Warehouse Count Details 绑定库区 - 全部生成快照
- -
-
-
- {{ wh.warehouseName || wh.actualWarehouseName || '库区' + (idx+1) }} - {{ wh.systemCoilCount || 0 }}卷 -
-
暂无绑定库区
-
-
-
+ - -
-
-
操作台
-
概览
-
快照
-
差异对比
-
差异明细
-
-
- - -
-
- -
-
-
-
-
生成快照
-
{{ (wh.snapshotCoilLogic || wh.snapshotCoilActual) ? '已生成' : '未生成' }}
-
- 生成 -
-
-
-
-
上传实盘
-
{{ wh.snapshotCoilStats ? '已上传' : '未上传' }}
-
- 上传 -
-
-
-
-
执行对比
-
{{ _diffDone[wh.relId] ? '已完成' : '未对比' }}
-
- 对比 -
-
-
-
-
保存结果
-
对比后可保存
-
- 保存 -
-
- -
-
对比结果摘要
-
-
盘亏:{{ diffMissing.length }}卷
-
盘盈:{{ diffExtra.length }}卷
-
不一致:{{ diffMismatch.length }}卷
-
-
-
-
- - -
- - {{ wh.warehouseName || '-' }} - {{ wh.actualWarehouseName || '-' }} - - 一致 - 不一致 - 未盘点 - - {{ parseTime(wh.ioStartTime, '{y}-{m}-{d} {h}:{i}') || '-' }} - {{ parseTime(wh.ioEndTime, '{y}-{m}-{d} {h}:{i}') || '-' }} - - 查看差异 ({{ (discrepancyMap[wh.relId] || []).length }}) - - {{ wh.systemCoilCount || 0 }} - {{ wh.systemTotalWeight || 0 }} - - 生成快照 - 上传Excel - 移除 - - {{ wh.actualCoilCount || 0 }} - {{ wh.actualTotalWeight || 0 }} - - -
- - -
- - - - 已生成 - 预览 - - 未生成 - - - - 已生成 - 预览 - - 未生成 - - - - 已生成 - 预览 - - 未生成 - - -
- - -
-
-
- - - - - - 重新对比 - - - 重量差异在此阈值内视为相同 -
- - -
-
- - 盘亏(快照有·实盘无) - {{ diffMissing.length }} -
- - - - - - - -
无盘亏卷
-
-
- -
-
- - 盘盈(实盘有·快照无) - {{ diffExtra.length }} -
- - - - - - - -
无盘盈卷
-
-
-
-
-
- - 字段不一致 - {{ diffMismatch.length }} -
- - - - - - - -
无不一致卷
-
-
- 请先生成快照并导入实盘Excel后进行对比 -
-
-
- - -
-
- - - - - - - - - - - - - - - - - - 暂无差异记录 -
-
- -
-
- - -
-
-
- } rawSheetRows - XLSX.utils.sheet_to_json 直接解析的结果 + * @returns {Array} 驼峰字段名数组 + */ +export function parseSnapshotExcel(rawSheetRows) { + return rawSheetRows.map(function(row) { + var newRow = {}; + Object.keys(SNAPSHOT_COL_MAP).forEach(function(zhKey) { + var camelKey = SNAPSHOT_COL_MAP[zhKey]; + if (row[zhKey] !== undefined) newRow[camelKey] = row[zhKey]; + }); + return newRow; + }); +} + +/** + * 将实盘Excel的原始行数据(中文表头)转为标准驼峰字段名对象数组 + * @param {Array} rawSheetRows - XLSX.utils.sheet_to_json 直接解析的结果 + * @returns {Array} 驼峰字段名数组 + */ +export function parsePhysicalExcel(rawSheetRows) { + return rawSheetRows.map(function(row) { + var newRow = {}; + Object.keys(PHYSICAL_HEADER_MAP).forEach(function(zhKey) { + var camelKey = PHYSICAL_HEADER_MAP[zhKey]; + if (row[zhKey] !== undefined) newRow[camelKey] = row[zhKey]; + }); + return newRow; + }); +} + +// ===== 下载函数 ===== + +/** + * 根据ossId从MinIO下载Excel并解析为标准驼峰字段名对象数组 + * @param {string} ossId - OSS文件ID + * @param {Object|null} colMap - 中文表头→驼峰映射表,传 null 则不转换 + * @returns {Promise>} + */ +export async function downloadAndParseExcel(ossId, colMap) { + var ossRes = await listByIds(ossId); + var ossList = ossRes.data || ossRes.rows || []; + if (ossList.length === 0) throw new Error('未找到文件'); + var url = ossList[0].url; + var resp = await fetch(url); + var blob = await resp.blob(); + var arrayBuf = await blob.arrayBuffer(); + var workbook = XLSX.read(new Uint8Array(arrayBuf), { type: 'array' }); + var sheet = workbook.Sheets[workbook.SheetNames[0]]; + var rows = XLSX.utils.sheet_to_json(sheet); + if (colMap) { + rows = rows.map(function(row) { + var newRow = {}; + Object.keys(colMap).forEach(function(zhKey) { + var camelKey = colMap[zhKey]; + if (row[zhKey] !== undefined) newRow[camelKey] = row[zhKey]; + }); + return newRow; + }); + } + return rows; +} + +// ===== 对比函数 ===== + +/** 构建复合键 enterCoilNo|currentCoilNo */ +function makeKey(item) { + return (item.enterCoilNo || '') + '|' + (item.currentCoilNo || ''); +} + +/** + * 对比快照数据与实盘数据,返回四种差异结果 + * @param {Array} snapRows - 快照标准化数据 + * @param {Array} physicalRows - 实盘标准化数据 + * @param {number} weightThreshold - 重量差异阈值(kg) + * @returns {{ missing: Array, extra: Array, mismatch: Array, weightSimilar: Array }} + */ +export function compareCoils(snapRows, physicalRows, weightThreshold) { + var snapMap = {}; + snapRows.forEach(function(item) { + var key = makeKey(item); + if (key !== '||') snapMap[key] = item; + }); + var physicalMap = {}; + physicalRows.forEach(function(item) { + var key = makeKey(item); + if (key !== '||') physicalMap[key] = item; + }); + + var result = { missing: [], extra: [], mismatch: [], weightSimilar: [] }; + + // 盘亏:快照有、实盘无 + Object.keys(snapMap).forEach(function(key) { + if (!physicalMap[key]) result.missing.push(snapMap[key]); + }); + // 盘盈:实盘有、快照无 + Object.keys(physicalMap).forEach(function(key) { + if (!snapMap[key]) result.extra.push(physicalMap[key]); + }); + // 字段不一致 & 重量不同 + Object.keys(snapMap).forEach(function(key) { + var snapItem = snapMap[key]; + var physItem = physicalMap[key]; + if (!snapItem || !physItem) return; + var realDiffs = []; + var weightDiffs = []; + COMPARE_FIELDS.forEach(function(f) { + var snapVal = snapItem[f.snapshotField]; + var physVal = physItem[f.physicalField]; + if (f.isNumber) { + var sn = Number(snapVal) || 0; + var pn = Number(physVal) || 0; + var absDiff = Math.abs(sn - pn); + if (absDiff > 0 && absDiff <= weightThreshold && f.label.indexOf('重量') >= 0) { + weightDiffs.push({ label: f.label, snapshotValue: snapVal, physicalValue: physVal }); + } else if (absDiff > weightThreshold) { + realDiffs.push({ label: f.label, snapshotValue: snapVal, physicalValue: physVal }); + } + } else { + if (String(snapVal || '').trim() !== String(physVal || '').trim()) { + realDiffs.push({ label: f.label, snapshotValue: snapVal, physicalValue: physVal }); + } + } + }); + if (weightDiffs.length > 0) { + result.weightSimilar.push({ + enterCoilNo: snapItem.enterCoilNo || physItem.enterCoilNo, + currentCoilNo: snapItem.currentCoilNo || physItem.currentCoilNo, + diffs: weightDiffs + }); + } + if (realDiffs.length > 0) { + result.mismatch.push({ + enterCoilNo: snapItem.enterCoilNo || physItem.enterCoilNo, + currentCoilNo: snapItem.currentCoilNo || physItem.currentCoilNo, + diffs: realDiffs + }); + } + }); + + return result; +} diff --git a/klp-ui/src/views/wms/post/flow.vue b/klp-ui/src/views/wms/post/flow.vue index 38d2125dd..3498884b8 100644 --- a/klp-ui/src/views/wms/post/flow.vue +++ b/klp-ui/src/views/wms/post/flow.vue @@ -13,6 +13,19 @@ +
+ + + 下载流程图 + + + + 下载 SVG + 下载 PNG + + +
+
@@ -110,80 +123,105 @@ graph TD inventoryCheck: ` graph TD - A["创建盘库单
填写盘库单基本信息
添加盘库计划"]:::c1 + A["创建盘库计划
填写基本信息"]:::c1 - A --> B["盘库计划
设定起止时间
选择库区类型"]:::c2 - B --> C{"库区类型?"}:::cdec + A --> B["创建计划明细
可创建多个明细"]:::c2 - C -->|逻辑库| D["获取库存快照
记录当前时间节点
库存情况"]:::c3 - C -->|物理库| E["获取库存快照
记录当前时间节点
库存情况"]:::c3 - E --> F["记录吞吐记录
额外记录物理库
出入库流水明细"]:::c4 + B --> C["选择库区
逻辑库 / 实际库
至少选一个"]:::c3 + C --> D["生成系统库存快照"]:::c4 + D --> E["上传实盘库存Excel"]:::c5 + E --> F["执行对比
快照 vs 实盘
自动计算差异"]:::c6 - D --> G["人工实地盘库
按盘库计划执行
录入实际库存数据"]:::c5 - F --> G + F --> G["查看差异明细
保存差异并填写处理方式"]:::c7 + G --> H["提交送审"]:::c8 - G --> H["系统自动对照
快照库存 vs 实际库存
逐项比对查找差异"]:::c6 + H --> I{"审批"}:::dec + I -->|不通过| J["退回修改"]:::c7 + J --> G + I -->|通过| K["开始处理差异
逐项执行处理方式"]:::c9 - H --> I["盘亏明细
系统有 实际无
库存缺失项"]:::loss - H --> J["盘盈明细
实际有 系统无
库存多出项"]:::gain - H --> K["明细差异
数据不一致
数量/规格偏差"]:::diff - - I --> L["生成盘库差异报告
汇总盘亏/盘盈/差异
存储差异记录"]:::c7 - J --> L - K --> L - - L --> M(["盘库单封存
流程结束"]):::cend + K --> L{"所有差异
处理完成?"}:::dec + L -->|否| K + L -->|是| M(["完结流程
盘库结束"]):::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 - [*] --> 创建排产单: 填写基本信息 - 创建排产单 --> 关联合同获取需求: 选择合同 - 关联合同获取需求 --> 选择需求合并明细: 选取需求
合并相同条目 - 选择需求合并明细 --> 提交审批 +graph TD + A["创建需求单
填写基本信息"]:::p1 - 提交审批 --> 审批驳回: 审批不通过 - 提交审批 --> 审批通过: 审批通过 + A --> B["选择合同
可选一个或多个合同"]:::p2 + B --> C["自动获取需求明细
从所选合同提取"]:::p3 + C --> D["调整需求明细
可编辑/修改/补充"]:::p4 - 审批驳回 --> 选择需求合并明细: 退回修改后重新提交 + D --> E["提交审批"]:::p5 + E --> F{"审批"}:::dec + F -->|不通过| G["退回修改"]:::p4 + G --> D + F -->|通过| H["转化为排产单"]:::p6 - 审批通过 --> 车间接收执行单: 转为执行单推送车间 + H --> I["排产单
可再次编辑"]:::p7 + I --> J["再次提交审批"]:::p5 + J --> K{"审批"}:::dec + K -->|不通过| L["退回修改"]:::p7 + L --> I + K -->|通过| M["提交给车间"]:::p8 - 车间接收执行单 --> 执行生产: 车间接收 - 车间接收执行单 --> 审批驳回: 车间打回拒绝 + M --> N["车间绑定钢卷
每个排产计划
绑定一个或多个钢卷"]:::p9 + N --> O["执行生产"]:::p10 + O --> P(["排产完结"]):::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 - [*] --> 创建维修计划: 点选异常巡检记录
绑定记录与异常设备 - 创建维修计划 --> 审批维修计划 +graph TD + A["创建维修计划"]:::e1 + A1["点选异常巡检记录
绑定记录与异常设备"]:::e1sub + A --> A1 - 审批维修计划 --> 审批驳回: 审批不通过 - 审批维修计划 --> 审批通过: 审批通过 + A1 --> B["提交审批"]:::e2 + B --> C{"审批"}:::edec + C -->|不通过| D["退回修改"]:::e1 + D --> A1 + C -->|通过| E["逐设备维修记录
逐一执行设备维修
记录维修过程与结果"]:::e3 - 审批驳回 --> 创建维修计划: 退回修改后重新提交 + E --> F{"全部设备
维修完成?"}:::edec + F -->|否| E + F -->|是| G(["流程结束"]):::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 { @@ -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; +}