2026-06-26 15:41:21 +08:00
< template >
< div class = "wh-panel" v-loading = "whLoading" >
< div v-if = "warehouseList.length === 0 && !whLoading" class="wh-empty" > 暂无绑定库区 < / div >
< div v-if = "warehouseList.length > 0" class="doc-tabs" >
< div class = "doc-tabs-header" >
< div v-for = "(wh, idx) in warehouseList" :key="wh.relId" class="doc-tab-item"
: class = "{ active: activeIdx === idx }" @ click = "switchWh(idx)" >
< span class = "doc-tab-label" > { { wh . warehouseName || wh . actualWarehouseName || '库区' + ( idx + 1 ) } } < / span >
< span class = "doc-tab-badge" > { { wh . systemCoilCount || 0 } } 卷 < / span >
< / div >
< / div >
< div class = "doc-tabs-content" >
< div v-for = "(wh, idx) in warehouseList" :key="wh.relId" v-show="activeIdx === idx" >
2026-06-27 11:15:13 +08:00
<!-- === === 盘库执行中 ( 2 ) : 操作台 === === -- >
< template v-if = "planStatus === 2" >
2026-06-26 15:41:21 +08:00
< div class = "console-section" >
< div class = "console-steps" >
< div class = "console-step" : class = "{ done: wh.snapshotCoilLogic }" >
< span class = "step-icon" > < i class = "el-icon-camera" > < / i > < / span >
< span class = "step-info" >
< span class = "step-title" > 生成快照 < / span >
< span class = "step-desc" > { { wh . snapshotCoilLogic ? '已生成' : '未生成' } }
< el-button v-if = "wh.snapshotCoilLogic" type="text" size="mini" icon="el-icon-view" @click.stop="showPreview(wh.snapshotCoilLogic)" > 预览 < / el -button >
< / span >
< span v-if = "wh.snapshotCoilLogic && wh.systemCoilCount != null" class="step-stats" > 系统 : < b > { { wh . systemCoilCount } } < / b > 卷 | 总重 : < b > { { wh . systemTotalWeight } } < / b > kg < / span >
< / span >
< el-button size = "mini" type = "primary" plain :loading = "genLoading" @click ="doGenSnapshot(wh)" > 生成 < / el -button >
< / div >
< div class = "console-step" : class = "{ done: wh.snapshotCoilStats }" >
< span class = "step-icon" > < i class = "el-icon-upload2" > < / i > < / span >
< span class = "step-info" >
< span class = "step-title" > 上传实盘 < / span >
< span class = "step-desc" > { { wh . snapshotCoilStats ? '已上传' : '未上传' } }
< el-button v-if = "wh.snapshotCoilStats" type="text" size="mini" icon="el-icon-view" @click.stop="showPreview(wh.snapshotCoilStats)" > 预览 < / el -button >
< / span >
< span v-if = "wh.snapshotCoilStats && wh.actualCoilCount != null" class="step-stats" > 实盘 : < b > { { wh . actualCoilCount } } < / b > 卷 | 总重 : < b > { { wh . actualTotalWeight } } < / b > kg < / span >
< / span >
< el-button size = "mini" type = "primary" plain @click ="openImport(wh)" > 上传 < / el -button >
< / div >
< div class = "console-step" : class = "{ done: diffDoneMap[wh.relId] }" >
< span class = "step-icon" > < i class = "el-icon-s-data" > < / i > < / span >
< span class = "step-info" >
< span class = "step-title" > 执行对比 < / span >
< span class = "step-desc" > { { diffDoneMap [ wh . relId ] ? '已完成' : '未对比' } }
< span style = "color:#909399;font-size:10px;" > 阈值 < / span >
< el-input-number v-model = "diffThreshold" size="mini" style="width:90px;" :controls="false" / >
< span style = "color:#909399;font-size:10px;" > T < / span >
< / span >
< / span >
< el-button size = "mini" type = "primary" plain :loading = "diffLoading" @click ="doCompare(wh)" > 对比 < / el -button >
< / div >
< div class = "console-step" >
< span class = "step-icon" > < i class = "el-icon-document-checked" > < / i > < / span >
< span class = "step-info" >
< span class = "step-title" > 查看结果 < / span >
< span class = "step-desc" > 对比后可查看并保存 < / span >
< / span >
< el-button size = "mini" type = "success" plain :disabled = "!diffDoneMap[wh.relId]" @click ="openDiffDialog(wh)" > 查看结果 < / el -button >
< / div >
< / div >
< / div >
<!-- 对比结果摘要 -- >
< div v-if = "diffDoneMap[wh.relId]" class="wh-diff-summary" >
< div class = "result-header" > 对比结果摘要 < / div >
< div class = "result-grid" >
< div class = "result-item miss" > 盘亏 : { { diffResult . missing . length } } 卷 < / div >
< div class = "result-item extra" > 盘盈 : { { diffResult . extra . length } } 卷 < / div >
< div class = "result-item diff" > 不一致 : { { diffResult . mismatch . length } } 卷 < / div >
< div class = "result-item weight-similar" > 重量不同 : { { diffResult . weightSimilar . length } } 卷 < / div >
< / div >
< / div >
< div style = "text-align:right;margin-bottom:8px;" >
2026-06-27 11:15:13 +08:00
< el-button type = "primary" size = "small" icon = "el-icon-s-promotion" @click ="$emit('submit-disc-approval')" > 提交差异审批 < / el -button >
2026-06-26 15:41:21 +08:00
< / div >
< / template >
2026-06-27 11:15:13 +08:00
<!-- === === 草稿 ( 0 ) / 计划待审批 ( 1 ) : 库区清单 === === -- >
< template v-if = "planStatus === 0 || planStatus === 1" >
< div class = "wh-detail-list" >
< div class = "wh-detail-row" >
< span class = "wh-detail-label" > 逻辑库区 < / span >
< span class = "wh-detail-value" > { { wh . warehouseName || '-' } } < / span >
< / div >
< div class = "wh-detail-row" >
< span class = "wh-detail-label" > 实际库区 < / span >
< span class = "wh-detail-value" > { { wh . actualWarehouseName || '-' } } < / span >
< / div >
< div class = "wh-detail-row" >
< span class = "wh-detail-label" > 出入库查询起始 < / span >
< span class = "wh-detail-value" > { { wh . ioStartTime ? formatTime ( wh . ioStartTime ) : '-' } } < / span >
< / div >
< div class = "wh-detail-row" >
< span class = "wh-detail-label" > 出入库查询截止 < / span >
< span class = "wh-detail-value" > { { wh . ioEndTime ? formatTime ( wh . ioEndTime ) : '-' } } < / span >
< / div >
< / div >
< / template >
<!-- === === 概览卡片 ( 状态 2 / 3 / 4 / 5 ) === === -- >
< div v-if = "planStatus >= 2" class="wh-overview" >
2026-06-26 15:41:21 +08:00
< div class = "wh-box" >
< div class = "wh-box-title" > 盘点范围 < / div >
< div class = "wh-box-body" >
< div v-if = "wh.warehouseName" > 逻辑库区 : {{ wh.warehouseName }} < / div >
< div v-if = "wh.actualWarehouseName" > 实际库区 : {{ wh.actualWarehouseName }} < / div >
< div v-if = "wh.ioStartTime" > {{ formatTime ( wh.ioStartTime ) }} ~ {{ formatTime ( wh.ioEndTime ) | | ' ' }} < / div >
< div v-if = "!wh.warehouseName && !wh.actualWarehouseName && !wh.ioStartTime" style="color:#909399;" > 无 < / div >
< / div >
< / div >
< div class = "wh-box" >
< div class = "wh-box-title" > 系统快照
< el-button v-if = "wh.snapshotCoilLogic" type="text" size="mini" icon="el-icon-view" @click="showPreview(wh.snapshotCoilLogic)" > 预览 < / el -button >
< / div >
< div class = "wh-box-body" >
< div v-if = "wh.snapshotCoilLogic" > 系统 : < b > { { wh . systemCoilCount || 0 } } < / b > 卷 | 总重 : < b > { { wh . systemTotalWeight || 0 } } < / b > kg < / div >
< div v-else style = "color:#909399;" > 未生成 < / div >
< / div >
< / div >
< div class = "wh-box" >
< div class = "wh-box-title" > 实盘数据
< el-button v-if = "wh.snapshotCoilStats" type="text" size="mini" icon="el-icon-view" @click="showPreview(wh.snapshotCoilStats)" > 预览 < / el -button >
< / div >
< div class = "wh-box-body" >
< div v-if = "wh.snapshotCoilStats" > 实盘 : < b > { { wh . actualCoilCount || 0 } } < / b > 卷 | 总重 : < b > { { wh . actualTotalWeight || 0 } } < / b > kg < / div >
< div v-else style = "color:#909399;" > 未上传 < / div >
< / div >
< / div >
< / div >
2026-06-27 11:15:13 +08:00
<!-- === == 计划待审批 ( 1 ) 操作 === == -- >
2026-06-26 15:41:21 +08:00
< div v-if = "planStatus === 1" style="text-align:right;margin-bottom:8px;" >
< el-button size = "small" type = "danger" icon = "el-icon-close" @click ="$emit('reject')" > 驳回 < / el -button >
< el-button size = "small" type = "primary" icon = "el-icon-check" @click ="$emit('approve')" > 审批通过 < / el -button >
< / div >
2026-06-27 11:15:13 +08:00
<!-- === == 差异审批中 ( 3 ) 操作 === == -- >
2026-06-26 15:41:21 +08:00
< div v-if = "planStatus === 3" style="text-align:right;margin-bottom:8px;" >
2026-06-27 11:15:13 +08:00
< el-button size = "small" type = "danger" icon = "el-icon-close" @click ="$emit('reject')" > 驳回 < / el -button >
< el-button size = "small" type = "primary" icon = "el-icon-check" @click ="$emit('diff-approve')" > 审批通过 < / el -button >
< / div >
<!-- === == 差异处理中 ( 4 ) 操作 === == -- >
< div v-if = "planStatus === 4" style="text-align:right;margin-bottom:8px;" >
2026-06-26 15:41:21 +08:00
< el-button size = "small" type = "success" icon = "el-icon-circle-check" @click ="$emit('archive')" > 归档封存 < / el -button >
< / div >
2026-06-27 11:15:13 +08:00
<!-- === === 差异明细表格 ( 状态 2 + ) === === -- >
< template v-if = "planStatus >= 2" >
2026-06-26 15:41:21 +08:00
< div class = "section-title" > 差异明细 < / div >
2026-06-27 11:15:13 +08:00
< template v-if = "planStatus === 2" >
2026-06-26 15:41:21 +08:00
< div style = "display:flex;justify-content:flex-end;margin-bottom:4px;" >
< el-button size = "mini" type = "danger" plain icon = "el-icon-delete" : disabled = "discSelected.length === 0" @click ="batchDelDisc(wh)" > 批量删除 < / el -button >
< / div >
< el-table v-loading = "discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300"
@ selection - change = "function(s) { discSelected = s; }" >
< el-table-column type = "selection" width = "36" / >
< el-table-column label = "类型" align = "center" width = "80" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.discrepancyType === 1" type="success" size="mini" > 盘盈 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 2" type="danger" size="mini" > 盘亏 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 3" type="warning" size="mini" > 不符 < / el -tag >
< el-tag v-else size = "mini" > 偏差 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "钢卷号" align = "center" prop = "enterCoilNo" width = "140" / >
< el-table-column label = "差异详情" align = "center" prop = "discrepancyDetail" min -width = " 140 " show -overflow -tooltip / >
< el-table-column label = "原因分析" align = "center" min -width = " 140 " >
< template slot -scope = " ds " > < el-input v-model = "ds.row.reasonAnalysis" size="mini" placeholder="原因分析" @blur="saveDiscRow(ds.row)" / > < / template >
< / el-table-column >
< el-table-column label = "处理建议" align = "center" min -width = " 140 " >
< template slot -scope = " ds " > < el-input v-model = "ds.row.processSuggestion" size="mini" placeholder="处理建议" @blur="saveDiscRow(ds.row)" / > < / template >
< / el-table-column >
< el-table-column label = "" align = "center" width = "50" >
< template slot -scope = " ds " > < el-button size = "mini" type = "text" icon = "el-icon-delete" style = "color:#f56c6c;" @click ="delOneDisc(ds.row, wh.relId)" / > < / template >
< / el-table-column >
< / el-table >
< / template >
< template v-else-if = "planStatus === 1" >
< el-table v-loading = "discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300" >
< el-table-column label = "类型" align = "center" width = "80" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.discrepancyType === 1" type="success" size="mini" > 盘盈 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 2" type="danger" size="mini" > 盘亏 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 3" type="warning" size="mini" > 不符 < / el -tag >
< el-tag v-else size = "mini" > 偏差 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "钢卷号" align = "center" prop = "enterCoilNo" width = "140" / >
< el-table-column label = "差异详情" align = "center" prop = "discrepancyDetail" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "原因分析" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . reasonAnalysis || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理建议" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . processSuggestion || '-' } } < / template >
< / el-table-column >
2026-06-27 11:15:13 +08:00
< / el-table >
< / template >
< template v-else-if = "planStatus === 3" >
< el-table v-loading = "discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300" >
< el-table-column label = "类型" align = "center" width = "80" >
2026-06-26 15:41:21 +08:00
< template slot -scope = " ds " >
2026-06-27 11:15:13 +08:00
< el-tag v-if = "ds.row.discrepancyType === 1" type="success" size="mini" > 盘盈 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 2" type="danger" size="mini" > 盘亏 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 3" type="warning" size="mini" > 不符 < / el -tag >
< el-tag v-else size = "mini" > 偏差 < / el-tag >
2026-06-26 15:41:21 +08:00
< / template >
< / el-table-column >
2026-06-27 11:15:13 +08:00
< el-table-column label = "钢卷号" align = "center" prop = "enterCoilNo" width = "140" / >
< el-table-column label = "差异详情" align = "center" prop = "discrepancyDetail" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "原因分析" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . reasonAnalysis || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理建议" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . processSuggestion || '-' } } < / template >
2026-06-26 15:41:21 +08:00
< / el-table-column >
< / el-table >
< / template >
2026-06-27 11:15:13 +08:00
< template v-else-if = "planStatus === 4" >
2026-06-26 15:41:21 +08:00
< el-table v-loading = "discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300" >
< el-table-column label = "类型" align = "center" width = "80" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.discrepancyType === 1" type="success" size="mini" > 盘盈 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 2" type="danger" size="mini" > 盘亏 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 3" type="warning" size="mini" > 不符 < / el -tag >
< el-tag v-else size = "mini" > 偏差 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "钢卷号" align = "center" prop = "enterCoilNo" width = "140" / >
< el-table-column label = "差异详情" align = "center" prop = "discrepancyDetail" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "原因分析" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . reasonAnalysis || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理建议" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . processSuggestion || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理结果" align = "center" min -width = " 150 " >
< template slot -scope = " ds " >
< el-input v-model = "ds.row._processResult" size="mini" placeholder="选填" :disabled="ds.row.processStatus === 2" / >
< / template >
< / el-table-column >
< el-table-column label = "状态" align = "center" width = "90" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.processStatus === 2" type="success" size="mini" > 已处理 < / el -tag >
< el-tag v-else type = "info" size = "mini" > 待处理 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "操作" align = "center" width = "90" >
< template slot -scope = " ds " >
< el-button v-if = "ds.row.processStatus !== 2" size="mini" type="primary" @click="$emit('process-disc', { row: ds.row, relId: wh.relId })" > 处理 < / el -button >
< span v-else style = "color:#67c23a;font-size:12px;" > 已完成 < / span >
< / template >
< / el-table-column >
< / el-table >
< / template >
2026-06-27 11:15:13 +08:00
< template v-else-if = "planStatus === 5" >
2026-06-26 15:41:21 +08:00
< el-table v-loading = "discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300" >
< el-table-column label = "类型" align = "center" width = "80" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.discrepancyType === 1" type="success" size="mini" > 盘盈 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 2" type="danger" size="mini" > 盘亏 < / el -tag >
< el-tag v-else-if = "ds.row.discrepancyType === 3" type="warning" size="mini" > 不符 < / el -tag >
< el-tag v-else size = "mini" > 偏差 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "钢卷号" align = "center" prop = "enterCoilNo" width = "140" / >
< el-table-column label = "差异详情" align = "center" prop = "discrepancyDetail" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "原因分析" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . reasonAnalysis || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理建议" align = "center" min -width = " 100 " >
< template slot -scope = " ds " > { { ds . row . processSuggestion || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理结果" align = "center" min -width = " 120 " >
< template slot -scope = " ds " > { { ds . row . processResult || '-' } } < / template >
< / el-table-column >
< el-table-column label = "处理状态" align = "center" width = "90" >
< template slot -scope = " ds " >
< el-tag v-if = "ds.row.processStatus === 2" type="success" size="mini" > 已处理 < / el -tag >
< el-tag v-else type = "info" size = "mini" > 未处理 < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "处理人" align = "center" prop = "processUserName" width = "90" / >
< el-table-column label = "处理时间" align = "center" width = "150" >
< template slot -scope = " ds " > { { ds . row . processTime || '-' } } < / template >
< / el-table-column >
< / el-table >
< / template >
< div v-if = "!(discMap[wh.relId] && discMap[wh.relId].length > 0) && !discLoadingMap[wh.relId]" class="wh-empty-tip" > 暂无差异记录 < / div >
< div v-else style = "text-align:right;padding:4px 0;" >
< el-pagination background layout = "prev, pager, next, total" : total = "discTotalMap[wh.relId] || 0" :page-size = "discPageSize" : current -page = " discPageMap [ wh.relId ] | | 1 " @ current -change = " function ( p ) { loadDisc ( wh.relId , p ) ; } " small / >
< / div >
2026-06-27 11:15:13 +08:00
< / template >
2026-06-26 15:41:21 +08:00
< / div >
< / div >
< / div >
<!-- 导入实盘Excel对话框 -- >
< el-dialog title = "导入实盘Excel" :visible.sync = "importOpen" width = "1000px" append -to -body @close ="resetImport" >
< div >
< div style = "display:flex;align-items:center;gap:10px;margin-bottom:14px;" >
< el-upload ref = "impUpload" action = "" :auto-upload = "false" :show-file-list = "false" :on-change = "onImpFile" accept = ".xlsx,.xls" > < el-button type = "primary" icon = "el-icon-upload2" > 选择Excel < / el-button > < / el-upload >
< span v-if = "impFile" style="color:#67c23a;font-size:13px;"><i class="el-icon-success" > < / i > {{ impFile.name }} < / span >
< el-button type = "warning" icon = "el-icon-upload" : disabled = "!impFile || impErrors.length > 0 || importing" :loading = "importing" @click ="doImport" > 上传 < / el -button >
< el-button type = "default" icon = "el-icon-refresh" :disabled = "importing" @click ="resetImport" > 重置 < / el -button >
< / div >
< div style = "margin-bottom:10px;" > < el-link type = "primary" icon = "el-icon-download" @click ="downloadTpl" > 下载模板 < / el -link > < / div >
< div v-if = "impErrors.length > 0" style="margin-top:10px;"><el-alert title="校验失败" type="error" :description="'共' + impErrors.length + '条错误'" show -icon / > < / div >
< div v-if = "impPreview.length > 0 && impErrors.length === 0" style="margin-top:10px;" >
< el-alert title = "校验通过" type = "success" : description = "'共 ' + impPreview.length + ' 条数据' " show -icon :closable = "false" / >
< el-table :data = "impPreview" border size = "small" max -height = " 300 " stripe style = "margin-top:8px;" >
< el-table-column prop = "enterCoilNo" label = "入场钢卷号" width = "140" / >
< el-table-column prop = "currentCoilNo" label = "当前钢卷号" width = "140" / >
< el-table-column prop = "netWeight" label = "重量(kg)" width = "100" / >
< el-table-column prop = "specification" label = "规格" width = "120" / >
< el-table-column prop = "material" label = "材质" width = "100" / >
< el-table-column prop = "manufacturer" label = "生产厂家" width = "120" / >
< el-table-column prop = "qualityStatus" label = "品质" width = "100" / >
< el-table-column prop = "actualWarehouseName" label = "实际库区" width = "100" / >
< el-table-column prop = "length" label = "长度" width = "80" / >
< / el-table >
< / div >
< / div >
< / el-dialog >
<!-- 预览弹窗 -- >
< el-dialog title = "文件预览" :visible.sync = "previewOpen" width = "80%" top = "5vh" append -to -body :before-close = "function(){previewOpen=false;previewUrl='';}" >
< div style = "height:75vh;" > < vue-office-excel v-if = "previewUrl" :src="previewUrl" @render-error="function(){ $message.error('预览失败'); }" / > < span v-else > 加载中... < / span > < / div >
< / el-dialog >
<!-- 对比结果详情弹窗 -- >
< el-dialog title = "对比结果详情" :visible.sync = "diffDialogOpen" width = "90%" top = "3vh" append -to -body >
< div v-if = "diffDialogWh" >
< div style = "display:flex;justify-content:flex-end;gap:8px;margin-bottom:10px;" >
< el-button type = "primary" size = "small" icon = "el-icon-check" :loading = "saving" @click ="doSaveResult(diffDialogWh)" > 保存结果 < / el -button >
< / div >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:10px;" >
< div class = "diff-block" >
< div class = "diff-block-title" style = "color:#f56c6c;" > 盘亏 ( { { diffResult . missing . length } } ) < / div >
< el-table :data = "diffResult.missing" border size = "small" height = "220" >
< el-table-column prop = "enterCoilNo" label = "入场钢卷号" width = "130" / >
< el-table-column prop = "currentCoilNo" label = "当前钢卷号" width = "130" / >
< el-table-column prop = "netWeight" label = "重量(kg)" width = "100" / >
< el-table-column prop = "specification" label = "规格" width = "120" / >
< el-table-column prop = "material" label = "材质" width = "100" / >
< / el-table >
< / div >
< div class = "diff-block" >
< div class = "diff-block-title" style = "color:#67c23a;" > 盘盈 ( { { diffResult . extra . length } } ) < / div >
< el-table :data = "diffResult.extra" border size = "small" height = "220" >
< el-table-column prop = "enterCoilNo" label = "入场钢卷号" width = "130" / >
< el-table-column prop = "currentCoilNo" label = "当前钢卷号" width = "130" / >
< el-table-column prop = "netWeight" label = "重量(kg)" width = "100" / >
< el-table-column prop = "specification" label = "规格" width = "120" / >
< el-table-column prop = "material" label = "材质" width = "100" / >
< / el-table >
< / div >
< div class = "diff-block" >
< div class = "diff-block-title" style = "color:#e6a23c;" > 字段不一致 ( { { diffResult . mismatch . length } } ) < / div >
< el-table :data = "diffResult.mismatch" border size = "small" height = "220" >
< el-table-column prop = "enterCoilNo" label = "入场钢卷号" width = "130" / >
< el-table-column prop = "currentCoilNo" label = "当前钢卷号" width = "130" / >
< el-table-column label = "差异" min -width = " 200 " >
< template slot -scope = " ds " > < span v-for = "(d, i) in ds.row.diffs" :key="i" > {{ d.label }} : 快照 [ {{ d.snapshotValue }} ] 实盘 [ {{ d.physicalValue }} ] < br / > < / span > < / template >
< / el-table-column >
< / el-table >
< / div >
< div class = "diff-block" >
< div class = "diff-block-title" style = "color:#409eff;" > 重量不同 ( 阈值内 , { { diffResult . weightSimilar . length } } ) < / div >
< el-table :data = "diffResult.weightSimilar" border size = "small" height = "220" >
< el-table-column prop = "enterCoilNo" label = "入场钢卷号" width = "130" / >
< el-table-column prop = "currentCoilNo" label = "当前钢卷号" width = "130" / >
< el-table-column label = "差异" min -width = " 200 " >
< template slot -scope = " ds " > < span v-for = "(d, i) in ds.row.diffs" :key="i" > {{ d.label }} : 快照 [ {{ d.snapshotValue }} ] 实盘 [ {{ d.physicalValue }} ] < br / > < / span > < / template >
< / el-table-column >
< / el-table >
< / div >
< / div >
< / div >
< / el-dialog >
< / div >
< / template >
< script >
import { listCountPlanWarehouse } from "@/api/flow/countPlanWarehouse" ;
import { listCountDiscrepancy , updateCountDiscrepancy , batchAddCountDiscrepancy , delCountDiscrepancy } from "@/api/flow/countDiscrepancy" ;
import { exportCoilWithAll } from "@/api/wms/coil" ;
import { uploadFile , listByIds } from "@/api/system/oss" ;
import { updateCountPlanWarehouse } from "@/api/flow/countPlanWarehouse" ;
import { parseTime } from '@/utils/klp' ;
import * as XLSX from 'xlsx' ;
import { downloadAndParseExcel , SNAPSHOT _COL _MAP , PHYSICAL _HEADER _MAP , compareCoils } from '../utils/compare.js' ;
import VueOfficeExcel from '@vue-office/excel' ;
import '@vue-office/excel/lib/index.css' ;
const IMP _TEMPLATE = [ '入场钢卷号' , '当前钢卷号' , '重量(kg)' , '规格' , '材质' , '生产厂家' , '品质' , '实际库区' , '长度' , '备注' ] ;
export default {
name : 'WarehouseDetailPanel' ,
components : { VueOfficeExcel } ,
props : {
planId : { type : [ Number , String ] , required : true } ,
planStatus : { type : Number , default : 0 }
} ,
data ( ) {
return {
whLoading : false , warehouseList : [ ] , activeIdx : 0 ,
discMap : { } , discLoadingMap : { } , discPageMap : { } , discTotalMap : { } ,
discPageSize : 20 ,
genLoading : false ,
importOpen : false , importWh : null , impFile : null , impErrors : [ ] , impPreview : [ ] , importing : false ,
previewOpen : false , previewUrl : '' ,
diffThreshold : 0.5 , diffLoading : false , diffDoneMap : { } ,
diffResult : { missing : [ ] , extra : [ ] , mismatch : [ ] , weightSimilar : [ ] } ,
diffDialogOpen : false , diffDialogWh : null , saving : false ,
discSelected : [ ]
} ;
} ,
mounted ( ) { this . loadWarehouses ( ) ; } ,
watch : { planId ( ) { this . activeIdx = 0 ; this . loadWarehouses ( ) ; } } ,
methods : {
formatTime ( val ) { if ( ! val ) return '' ; return parseTime ( val , '{y}-{m}-{d} {h}:{i}' ) ; } ,
loadWarehouses ( ) {
if ( ! this . planId ) return ;
this . whLoading = true ; var self = this ;
listCountPlanWarehouse ( { planId : this . planId , pageNum : 1 , pageSize : 999 } ) . then ( function ( r ) {
self . warehouseList = r . rows || [ ] ;
self . $emit ( 'loaded' , self . warehouseList ) ;
if ( self . warehouseList . length > 0 ) self . loadDisc ( self . warehouseList [ 0 ] . relId ) ;
} ) . finally ( function ( ) { self . whLoading = false ; } ) ;
} ,
loadDisc ( relId , pageNum ) {
pageNum = pageNum || this . discPageMap [ relId ] || 1 ;
this . $set ( this . discPageMap , relId , pageNum ) ;
this . $set ( this . discLoadingMap , relId , true ) ; var self = this ;
listCountDiscrepancy ( { relId , pageNum : pageNum , pageSize : this . discPageSize } ) . then ( function ( r ) {
self . $set ( self . discMap , relId , ( r . rows || [ ] ) . map ( function ( item ) {
if ( item . _processResult === undefined ) item . _processResult = item . processResult || '' ;
2026-06-27 11:15:13 +08:00
if ( item . _needResolve === undefined ) item . _needResolve = item . processStatus === 1 ;
if ( item . _approveRemark === undefined ) item . _approveRemark = item . remark || '' ;
2026-06-26 15:41:21 +08:00
return item ;
} ) ) ;
2026-06-27 11:15:13 +08:00
self . $set ( self . discTotalMap , relId , r . total || 0 ) ;
2026-06-26 15:41:21 +08:00
} ) . finally ( function ( ) { self . $set ( self . discLoadingMap , relId , false ) ; } ) ;
} ,
loadAllDisc ( ) { var self = this ; this . warehouseList . forEach ( function ( wh ) { self . loadDisc ( wh . relId ) ; } ) ; } ,
switchWh ( idx ) { this . activeIdx = idx ; var wh = this . warehouseList [ idx ] ; if ( wh ) this . loadDisc ( wh . relId ) ; } ,
refreshOneDisc ( relId ) { this . loadDisc ( relId ) ; } ,
getActiveWarehouse ( ) { return this . warehouseList [ this . activeIdx ] || null ; } ,
refreshAll ( ) { this . activeIdx = 0 ; this . loadWarehouses ( ) ; } ,
// ===== 快照生成 =====
async doGenSnapshot ( wh ) {
if ( ! wh ) return ;
var p = { dataType : 1 , status : 0 } ;
if ( wh . warehouseId ) p . warehouseId = String ( wh . warehouseId ) ;
if ( wh . actualWarehouseId ) p . actualWarehouseId = String ( wh . actualWarehouseId ) ;
if ( ! p . warehouseId && ! p . actualWarehouseId ) { this . $message . warning ( '无库区条件' ) ; return ; }
this . genLoading = true ;
try {
var resp = await exportCoilWithAll ( p ) ;
var label = wh . warehouseName || wh . actualWarehouseName || '' ;
var file = new File ( [ new Blob ( [ resp ] ) ] , label + '_快照.xlsx' , { type : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } ) ;
var up = await uploadFile ( file ) ;
await updateCountPlanWarehouse ( { relId : wh . relId , snapshotCoilLogic : up . data . ossId } ) ;
var arr = await resp . arrayBuffer ( ) ;
var wb = XLSX . read ( new Uint8Array ( arr ) , { type : 'array' } ) ;
var rows = XLSX . utils . sheet _to _json ( wb . Sheets [ wb . SheetNames [ 0 ] ] ) ;
var cnt = rows . length , tw = 0 ;
rows . forEach ( function ( r ) { tw += Number ( r [ '重量' ] || r [ '重量(kg)' ] || 0 ) ; } ) ;
await updateCountPlanWarehouse ( { relId : wh . relId , systemCoilCount : cnt , systemTotalWeight : Math . round ( tw * 100 ) / 100 } ) ;
this . $message . success ( '快照已生成' ) ;
this . refreshAll ( ) ;
} catch ( e ) { this . $message . error ( '生成失败' ) ; }
finally { this . genLoading = false ; }
} ,
// ===== 实盘导入 =====
openImport ( wh ) { this . importWh = wh ; this . resetImport ( ) ; this . importOpen = true ; } ,
onImpFile ( file ) {
if ( this . importing ) return ;
this . impFile = file . raw ; this . impErrors = [ ] ; this . impPreview = [ ] ;
var self = this ;
var fr = new FileReader ( ) ;
fr . readAsArrayBuffer ( file . raw ) ;
fr . onload = function ( e ) {
try {
var wb = XLSX . read ( new Uint8Array ( e . target . result ) , { type : 'array' } ) ;
var data = XLSX . utils . sheet _to _json ( wb . Sheets [ wb . SheetNames [ 0 ] ] , { header : 1 } ) ;
var h = data [ 0 ] ;
for ( var i = 0 ; i < IMP _TEMPLATE . length ; i ++ ) { if ( h [ i ] !== IMP _TEMPLATE [ i ] ) self . impErrors . push ( { rowNum : 1 , errorMsg : '表头第' + ( i + 1 ) + '列应为' + IMP _TEMPLATE [ i ] } ) ; }
data . slice ( 1 ) . filter ( function ( r ) { return r . length > 0 ; } ) . forEach ( function ( r , i ) {
if ( ! r [ 0 ] || ! String ( r [ 0 ] ) . trim ( ) ) self . impErrors . push ( { rowNum : i + 2 , errorMsg : '入场钢卷号为空' } ) ;
if ( r [ 2 ] && isNaN ( Number ( r [ 2 ] ) ) ) self . impErrors . push ( { rowNum : i + 2 , errorMsg : '重量非数字' } ) ;
self . impPreview . push ( {
enterCoilNo : String ( r [ 0 ] || '' ) . trim ( ) , currentCoilNo : String ( r [ 1 ] || '' ) . trim ( ) , netWeight : r [ 2 ] ,
specification : r [ 3 ] , material : r [ 4 ] , manufacturer : r [ 5 ] , qualityStatus : r [ 6 ] ,
actualWarehouseName : r [ 7 ] , length : r [ 8 ]
} ) ;
} ) ;
if ( self . impErrors . length === 0 ) self . $message . success ( '校验通过,共 ' + self . impPreview . length + ' 条' ) ;
} catch ( ex ) { self . impErrors . push ( { rowNum : 0 , errorMsg : ex . message } ) ; }
} ;
} ,
async doImport ( ) {
if ( this . importing || this . impErrors . length > 0 || ! this . impFile ) return ;
this . importing = true ; var self = this ;
try {
var up = await uploadFile ( this . impFile ) ;
var cnt = 0 , tw = 0 ;
var fr2 = new FileReader ( ) ;
fr2 . readAsArrayBuffer ( this . impFile ) ;
await new Promise ( function ( resolve ) {
fr2 . onload = function ( e ) {
var wb = XLSX . read ( new Uint8Array ( e . target . result ) , { type : 'array' } ) ;
var data = XLSX . utils . sheet _to _json ( wb . Sheets [ wb . SheetNames [ 0 ] ] , { header : 1 } ) ;
data . slice ( 1 ) . filter ( function ( r ) { return r . length > 0 ; } ) . forEach ( function ( r ) { cnt ++ ; tw += Number ( r [ 2 ] ) || 0 ; } ) ;
resolve ( ) ;
} ;
} ) ;
await updateCountPlanWarehouse ( { relId : self . importWh . relId , snapshotCoilStats : up . data . ossId , actualCoilCount : cnt , actualTotalWeight : Math . round ( tw * 100 ) / 100 } ) ;
self . $message . success ( '上传成功' ) ;
self . importOpen = false ;
self . refreshAll ( ) ;
} catch ( e ) { self . $message . error ( '上传失败' ) ; }
finally { self . importing = false ; }
} ,
resetImport ( ) { this . impFile = null ; this . impErrors = [ ] ; this . impPreview = [ ] ; this . importing = false ; if ( this . $refs . impUpload ) this . $refs . impUpload . clearFiles ( ) ; } ,
downloadTpl ( ) {
var data = [ IMP _TEMPLATE , [ 'LOT001' , 'COIL001' , 1000.5 , '1.0*1000' , 'Q235' , '宝钢' , '正品' , 'A库区' , 2000 , '示例' ] ] ;
var wb = XLSX . utils . book _new ( ) ;
XLSX . utils . book _append _sheet ( wb , XLSX . utils . aoa _to _sheet ( data ) , '模板' ) ;
XLSX . writeFile ( wb , '盘库实盘导入模板.xlsx' ) ;
} ,
// ===== 预览 =====
showPreview ( ossId ) {
var self = this ; this . previewUrl = '' ; this . previewOpen = true ;
listByIds ( ossId ) . then ( function ( r ) { var list = r . data || r . rows || [ ] ; if ( list . length > 0 ) self . previewUrl = list [ 0 ] . url ; else { self . $message . error ( '未找到' ) ; self . previewOpen = false ; } } ) . catch ( function ( ) { self . $message . error ( '失败' ) ; self . previewOpen = false ; } ) ;
} ,
// ===== 差异保存 =====
saveDiscRow ( row ) {
if ( ! row || ! row . discrepancyId ) return ;
updateCountDiscrepancy ( { discrepancyId : row . discrepancyId , reasonAnalysis : row . reasonAnalysis , processSuggestion : row . processSuggestion } ) . catch ( function ( ) { } ) ;
} ,
2026-06-27 11:15:13 +08:00
saveDiscMark ( row ) {
if ( ! row || ! row . discrepancyId ) return ;
updateCountDiscrepancy ( { discrepancyId : row . discrepancyId , processStatus : row . _needResolve ? 1 : 0 , remark : row . _approveRemark } ) . catch ( function ( ) { } ) ;
} ,
2026-06-26 15:41:21 +08:00
// ===== 对比 =====
async doCompare ( wh ) {
if ( ! wh . snapshotCoilLogic ) { this . $message . warning ( '请先生成快照' ) ; return ; }
if ( ! wh . snapshotCoilStats ) { this . $message . warning ( '请先上传实盘' ) ; return ; }
this . diffLoading = true ; this . diffResult = { missing : [ ] , extra : [ ] , mismatch : [ ] , weightSimilar : [ ] } ;
try {
var snapRows = await downloadAndParseExcel ( wh . snapshotCoilLogic , SNAPSHOT _COL _MAP ) ;
var physRows = await downloadAndParseExcel ( wh . snapshotCoilStats , PHYSICAL _HEADER _MAP ) ;
this . diffResult = compareCoils ( snapRows , physRows , this . diffThreshold ) ;
this . $set ( this . diffDoneMap , wh . relId , true ) ;
} catch ( e ) { this . $message . error ( '对比失败:' + ( e . message || '' ) ) ; }
finally { this . diffLoading = false ; }
} ,
openDiffDialog ( wh ) {
if ( ! this . diffResult . missing . length && ! this . diffResult . extra . length && ! this . diffResult . mismatch . length && ! this . diffResult . weightSimilar . length ) {
this . $message . warning ( '无对比结果' ) ; return ;
}
this . diffDialogWh = wh ;
this . diffDialogOpen = true ;
} ,
async doSaveResult ( wh ) {
var r = this . diffResult ;
if ( ! r . missing . length && ! r . extra . length && ! r . mismatch . length && ! r . weightSimilar . length ) { this . $message . warning ( '无对比结果' ) ; return ; }
var bos = [ ] ;
r . missing . forEach ( function ( item ) { bos . push ( { relId : wh . relId , discrepancyType : 2 , enterCoilNo : item . enterCoilNo || '' , currentCoilNo : item . currentCoilNo || '' , discrepancyDetail : '盘亏' } ) ; } ) ;
r . extra . forEach ( function ( item ) { bos . push ( { relId : wh . relId , discrepancyType : 1 , enterCoilNo : item . enterCoilNo || '' , currentCoilNo : item . currentCoilNo || '' , discrepancyDetail : '盘盈' } ) ; } ) ;
r . mismatch . forEach ( function ( item ) { var d = ( item . diffs || [ ] ) . map ( function ( x ) { return x . label + ': 快照[' + ( x . snapshotValue ? ? '' ) + '] 实盘[' + ( x . physicalValue ? ? '' ) + ']' ; } ) . join ( '; ' ) ; bos . push ( { relId : wh . relId , discrepancyType : 4 , enterCoilNo : item . enterCoilNo || '' , currentCoilNo : item . currentCoilNo || '' , discrepancyDetail : '不一致:' + d } ) ; } ) ;
r . weightSimilar . forEach ( function ( item ) { var d = ( item . diffs || [ ] ) . map ( function ( x ) { return x . label + ': 快照[' + ( x . snapshotValue ? ? '' ) + '] 实盘[' + ( x . physicalValue ? ? '' ) + ']' ; } ) . join ( '; ' ) ; bos . push ( { relId : wh . relId , discrepancyType : 4 , enterCoilNo : item . enterCoilNo || '' , currentCoilNo : item . currentCoilNo || '' , discrepancyDetail : '重量不同(阈值内): ' + d } ) ; } ) ;
this . saving = true ;
try {
await batchAddCountDiscrepancy ( bos ) ;
this . $message . success ( '已保存 ' + bos . length + ' 条差异记录' ) ;
this . diffDialogOpen = false ;
this . loadDisc ( wh . relId ) ;
} catch ( e ) { this . $message . error ( '保存失败' ) ; }
finally { this . saving = false ; }
} ,
// 删除单条差异
delOneDisc ( row , relId ) {
var self = this ;
this . $modal . confirm ( '确认删除该差异记录?' ) . then ( function ( ) {
return delCountDiscrepancy ( row . discrepancyId ) ;
} ) . then ( function ( ) { self . $message . success ( '已删除' ) ; self . loadDisc ( relId ) ; } ) . catch ( function ( ) { } ) ;
} ,
// 批量删除差异
batchDelDisc ( wh ) {
var self = this ;
if ( ! this . discSelected || this . discSelected . length === 0 ) { this . $message . warning ( '请至少选择一条' ) ; return ; }
this . $modal . confirm ( '确认删除选中的 ' + this . discSelected . length + ' 条差异记录?' ) . then ( function ( ) {
var ids = self . discSelected . map ( function ( row ) { return row . discrepancyId ; } ) . join ( ',' ) ;
return delCountDiscrepancy ( ids ) ;
} ) . then ( function ( ) { self . $message . success ( '批量删除成功' ) ; self . discSelected = [ ] ; self . loadDisc ( wh . relId ) ; } ) . catch ( function ( ) { } ) ;
} ,
}
} ;
< / script >
< style scoped >
. wh - panel { }
. wh - empty { padding : 16 px 0 ; color : # 909399 ; font - size : 13 px ; }
2026-06-27 11:15:13 +08:00
. wh - detail - list { margin : 8 px 0 16 px ; border : 1 px solid # e8e4de ; border - radius : 2 px ; overflow : hidden ; }
. wh - detail - row { display : flex ; padding : 8 px 14 px ; font - size : 13 px ; border - bottom : 1 px solid # f0ece6 ; }
. wh - detail - row : last - child { border - bottom : none ; }
. wh - detail - label { width : 120 px ; flex - shrink : 0 ; color : # 606266 ; font - weight : 500 ; }
. wh - detail - value { color : # 303133 ; }
2026-06-26 15:41:21 +08:00
. doc - tabs - header { display : flex ; border - bottom : 1 px solid # d4d0c8 ; margin - bottom : 12 px ; }
. doc - tab - item { padding : 8 px 16 px ; cursor : pointer ; color : # 8 c8c8c ; font - size : 13 px ; font - weight : 500 ; border - bottom : 2 px solid transparent ; margin - bottom : - 1 px ; transition : color 0.2 s , border - color 0.2 s ; display : flex ; align - items : center ; gap : 6 px ; user - select : none ; }
. doc - tab - item : hover { color : # 1 a3c6e ; }
. doc - tab - item . active { color : # 1 a3c6e ; font - weight : 600 ; border - bottom - color : # 1 a3c6e ; }
. doc - tab - label { white - space : nowrap ; }
. doc - tab - badge { font - size : 11 px ; color : # 909399 ; }
. doc - tab - item . active . doc - tab - badge { color : # 1 a3c6e ; }
. wh - overview { display : flex ; gap : 12 px ; margin - bottom : 14 px ; }
. wh - box { flex : 1 ; border : 1 px solid # e8e4de ; border - radius : 2 px ; overflow : hidden ; }
. wh - box - title { padding : 6 px 10 px ; background : # f7f5f0 ; font - size : 12 px ; font - weight : 600 ; color : # 1 a1a1a ; border - bottom : 1 px solid # e8e4de ; display : flex ; align - items : center ; justify - content : space - between ; }
. wh - box - body { padding : 8 px 10 px ; font - size : 12 px ; color : # 606266 ; line - height : 1.8 ; }
. wh - box - body b { color : # 1 a3c6e ; }
. section - title { font - size : 14 px ; font - weight : 700 ; color : # 1 a1a1a ; margin : 16 px 0 10 px ; padding - bottom : 8 px ; border - bottom : 1 px solid # d4d0c8 ; letter - spacing : 0.3 px ; }
. wh - empty - tip { padding : 8 px 0 ; color : # 909399 ; font - size : 13 px ; }
. console - section { margin : 12 px 0 ; }
. console - steps { display : flex ; flex - direction : column ; gap : 6 px ; }
. console - step { display : flex ; align - items : center ; gap : 10 px ; padding : 8 px 12 px ; background : # faf8f5 ; border : 1 px solid # e8e4de ; border - radius : 2 px ; }
. console - step . done { background : # f0f9eb ; border - color : # c2e7b0 ; }
. step - icon { width : 28 px ; height : 28 px ; border - radius : 50 % ; background : # e4e7ed ; display : flex ; align - items : center ; justify - content : center ; font - size : 14 px ; color : # 909399 ; flex - shrink : 0 ; }
. console - step . done . step - icon { background : # 67 c23a ; color : # fff ; }
. step - info { flex : 1 ; display : flex ; flex - direction : column ; }
. step - title { font - size : 13 px ; font - weight : 600 ; color : # 303133 ; }
. step - desc { font - size : 11 px ; color : # 909399 ; margin - top : 1 px ; }
. step - stats { font - size : 11 px ; color : # 606266 ; margin - top : 2 px ; }
. step - stats b { color : # 1 a3c6e ; }
. wh - diff - summary { margin : 10 px 0 ; padding : 12 px ; background : # faf8f5 ; border : 1 px solid # e8e4de ; border - radius : 2 px ; }
. result - header { font - size : 13 px ; font - weight : 600 ; color : # 1 a1a1a ; margin - bottom : 8 px ; padding - bottom : 6 px ; border - bottom : 1 px solid # e0dcd6 ; }
. result - grid { display : grid ; grid - template - columns : 1 fr 1 fr 1 fr 1 fr ; gap : 8 px ; }
. result - item { padding : 8 px ; border - radius : 2 px ; font - size : 12 px ; font - weight : 500 ; text - align : center ; }
. result - item . miss { background : # fef0f0 ; color : # f56c6c ; }
. result - item . extra { background : # f0f9eb ; color : # 67 c23a ; }
. result - item . diff { background : # fdf6ec ; color : # e6a23c ; }
. result - item . weight - similar { background : # f0f5ff ; color : # 409 eff ; }
. diff - block { margin - bottom : 10 px ; }
. diff - block - title { font - size : 13 px ; font - weight : 600 ; padding : 4 px 0 ; border - bottom : 1 px solid # e4e7ed ; margin - bottom : 4 px ; }
< / style >