refactor(盘库流程): 重构盘库流程页面与组件,完善排产明细功能

1.  重构盘库流程的步骤与状态映射,调整流程节点顺序与名称
2.  拆分通用盘库详情组件PlanDetailPanel,复用各流程页面
3.  新增计划审批、盘库执行页面,完善差异审批页面
4.  为排产单明细添加增删改查API与前端操作功能
5.  为排产日期添加格式化注解,完善参数接收格式
This commit is contained in:
2026-06-27 11:15:13 +08:00
parent 097d45b420
commit b94b7823e5
13 changed files with 1167 additions and 558 deletions

View File

@@ -6,9 +6,10 @@
</div>
<el-steps :active="activeStep" align-center class="flow-steps">
<el-step title="创建计划" icon="el-icon-document" />
<el-step title="快照与对比" icon="el-icon-camera" />
<el-step title="提交审批" icon="el-icon-s-promotion" />
<el-step title="处理差异" icon="el-icon-warning" />
<el-step title="计划审批" icon="el-icon-s-promotion" />
<el-step title="盘库执行" icon="el-icon-camera" />
<el-step title="差异审批" icon="el-icon-s-check" />
<el-step title="差异处理" icon="el-icon-warning" />
<el-step title="完成归档" icon="el-icon-circle-check" />
</el-steps>
<div class="current-status">
@@ -34,28 +35,28 @@ export default {
computed: {
/**
* el-steps active 从 0 开始。
* 步骤0=创建计划, 1=快照与对比, 2=提交审批, 3=处理差异, 4=完成归档
* status 0=草稿 -> active=0 (创建计划)
* status 1=待审批 -> active=2 (提交审批)
* status 2=执行中 -> active=3 (处理差异)
* status 3=差异处理 -> active=3 (处理差异)
* status 4=已归档 -> active=5 (全部finish)
* 步骤0=创建计划, 1=计划审批, 2=盘库执行, 3=差异审批, 4=差异处理, 5=完成归档
* status 0=草稿 -> active=0
* status 1=计划待审批 -> active=1
* status 2=盘库执行中 -> active=2
* status 3=差异审批中 -> active=3
* status 4=差异处理中 -> active=4
* status 5=已归档 -> active=6 (全部finish)
*/
activeStep() {
if (this.planStatus == null) return -1;
const v = Number(this.planStatus);
if (v >= 4) return 5;
if (v === 1) return 2;
if (v === 2 || v === 3) return 3;
if (v >= 5) return 6;
return v;
},
flowStatusText() {
const map = {
0: '草稿',
1: '待审批',
2: '执行中',
3: '差异处理中',
4: '已归档'
1: '计划待审批',
2: '盘库执行中',
3: '差异审批中',
4: '差异处理中',
5: '已归档'
};
return map[this.planStatus] || '未知';
},
@@ -64,8 +65,9 @@ export default {
0: 'info',
1: '',
2: 'warning',
3: 'danger',
4: 'success'
3: '',
4: 'danger',
5: 'success'
};
return map[this.planStatus] || '';
}

View File

@@ -0,0 +1,127 @@
<template>
<div v-if="!currentRow" class="empty-tip"><i class="el-icon-info"></i><span>{{ emptyText }}</span></div>
<div v-else v-loading="loading" class="detail-content">
<div class="doc-header">
<div class="doc-header-top">
<div class="doc-title-group">
<div class="doc-title">{{ currentRow.planCode }}</div>
<div class="doc-subtitle">{{ subtitle }}</div>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="refresh">刷新</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-edit" @click="$emit('edit-plan', currentRow)">编辑</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-delete" @click="$emit('delete-plan', currentRow)">删除</el-button>
<el-button v-if="currentRow.planStatus === 4" size="mini" type="success" icon="el-icon-circle-check" @click="$emit('archive')">归档封存</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Status</span>
<el-tag v-if="currentRow.planStatus === 0" type="info" size="small">草稿</el-tag>
<el-tag v-else-if="currentRow.planStatus === 1" size="small">计划待审批</el-tag>
<el-tag v-else-if="currentRow.planStatus === 2" type="warning" size="small">盘库执行中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 3" size="small">差异审批中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 4" type="danger" size="small">差异处理中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 5" type="success" size="small">已归档</el-tag>
</div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countDate"><i class="el-icon-date"></i>{{ formatTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>{{ currentRow.countUserName }}</span>
<span v-if="currentRow.principalUserName"><i class="el-icon-s-custom"></i>{{ currentRow.principalUserName }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<!-- 流程操作按钮 -->
<div v-if="currentRow.planStatus === 0" class="section-title">
<span>流程操作 <span class="en-sub">· Actions</span></span>
</div>
<div class="flow-actions" v-if="currentRow.planStatus === 0">
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-approval')">提交审批</el-button>
</div>
<el-divider v-if="currentRow.planStatus === 0" />
<div class="section-title">库区盘点明细 <span class="en-sub">· Warehouses</span>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="primary" plain icon="el-icon-plus" style="margin-left:8px;" @click="$emit('bind-warehouse')">绑定库区</el-button>
</div>
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="currentRow.planStatus"
@approve="$emit('approve')"
@diff-approve="$emit('diff-approve')"
@reject="$emit('reject')"
@archive="$emit('archive')"
@process-disc="$emit('process-disc', $event)"
@submit-disc-approval="$emit('submit-disc-approval')"
/>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
</template>
<script>
import { getCountPlan } from "@/api/flow/countPlan";
import CountFlowSection from "./CountFlowSection.vue";
import WarehouseDetailPanel from "./WarehouseDetailPanel.vue";
import { parseTime } from '@/utils/klp';
export default {
name: 'PlanDetailPanel',
components: { CountFlowSection, WarehouseDetailPanel },
props: {
planId: { type: [Number, String], default: null },
emptyText: { type: String, default: '请在左侧列表中选择一条盘库计划' },
subtitle: { type: String, default: 'Inventory Count Plan' }
},
data() {
return { loading: false, currentRow: null };
},
watch: {
planId: {
immediate: true,
handler(val) {
if (!val) { this.currentRow = null; return; }
this.loadDetail(val);
}
}
},
methods: {
formatTime(val, fmt) { if (!val) return ''; return parseTime(val, fmt); },
loadDetail(planId) {
this.loading = true; var self = this;
getCountPlan(planId).then(function(r) { self.currentRow = r.data; self.$emit('loaded', r.data); }).finally(function() { self.loading = false; });
},
refresh() { if (this.currentRow) this.loadDetail(this.currentRow.planId); if (this.$refs.whPanel) this.$refs.whPanel.refreshAll(); }
}
};
</script>
<style scoped>
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
.detail-content { margin: 0 auto; background: #fff; padding: 28px 32px 36px; box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 2px 12px rgba(0,0,0,0.04); min-height: 100%; }
.doc-header { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 2px solid #1a3c6e; }
.doc-header-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.doc-title-group { flex: 1; min-width: 0; }
.doc-title { font-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.3; letter-spacing: 0.5px; }
.doc-subtitle { font-size: 12px; color: #8c8c8c; font-style: italic; letter-spacing: 0.8px; margin-top: 2px; }
.doc-header-right { flex-shrink: 0; }
.doc-status-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.doc-status-label { font-size: 11px; color: #8c8c8c; letter-spacing: 0.3px; }
.detail-meta { display: flex; flex-wrap: wrap; gap: 16px; font-size: 12px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e0dcd6; }
.detail-meta span { display: inline-flex; align-items: center; gap: 4px; }
.detail-meta i { font-size: 13px; }
.section-title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin: 22px 0 12px; padding: 0 0 10px; border-bottom: 1px solid #d4d0c8; display: flex; align-items: center; gap: 10px; letter-spacing: 0.3px; }
.section-title:first-child { margin-top: 0; }
.section-title .en-sub { font-size: 11px; font-weight: 400; color: #8c8c8c; letter-spacing: 0.5px; font-style: italic; }
.flow-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.remark-content { padding: 12px 16px; background: #faf8f5; border: 1px solid #e8e4de; border-radius: 2px; font-size: 13px; line-height: 1.8; color: #1a1a1a; }
.section-gap { height: 16px; }
</style>

View File

@@ -13,8 +13,8 @@
<div class="doc-tabs-content">
<div v-for="(wh, idx) in warehouseList" :key="wh.relId" v-show="activeIdx === idx">
<!-- ====== 草稿 (0)操作台 ====== -->
<template v-if="planStatus === 0">
<!-- ====== 盘库执行中 (2)操作台 ====== -->
<template v-if="planStatus === 2">
<div class="console-section">
<div class="console-steps">
<div class="console-step" :class="{ done: wh.snapshotCoilLogic }">
@@ -73,12 +73,34 @@
</div>
<div style="text-align:right;margin-bottom:8px;">
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-approval')">提交审批</el-button>
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-disc-approval')">提交差异审批</el-button>
</div>
</template>
<!-- ====== 概览卡片状态 0/1/4 ====== -->
<div v-if="planStatus !== 3" class="wh-overview">
<!-- ====== 草稿 (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">
<div class="wh-box">
<div class="wh-box-title">盘点范围</div>
<div class="wh-box-body">
@@ -108,19 +130,28 @@
</div>
</div>
<!-- ===== 待审批操作 ===== -->
<!-- ===== 计划待审批 (1) 操作 ===== -->
<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>
<!-- ===== 差异审批中 (3) 操作 ===== -->
<div v-if="planStatus === 3" 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('diff-approve')">审批通过</el-button>
</div>
<!-- ===== 差异处理中 (4) 操作 ===== -->
<div v-if="planStatus === 4" style="text-align:right;margin-bottom:8px;">
<el-button size="small" type="success" icon="el-icon-circle-check" @click="$emit('archive')">归档封存</el-button>
</div>
<!-- ====== 差异明细表格 ====== -->
<!-- ====== 差异明细表格状态 2+ ====== -->
<template v-if="planStatus >= 2">
<div class="section-title">差异明细</div>
<template v-if="planStatus === 0">
<template v-if="planStatus === 2">
<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>
@@ -167,21 +198,31 @@
<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" width="80">
<template slot-scope="ds">
<el-tag v-if="ds.row.processStatus === 1" type="warning" 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="80">
<template slot-scope="ds">
<el-button size="mini" type="text" icon="el-icon-edit" @click="$emit('mark-disc', ds.row)">标记</el-button>
</template>
</el-table-column>
</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">
<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>
</template>
<template v-else-if="planStatus === 4">
<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">
@@ -219,7 +260,7 @@
</el-table>
</template>
<template v-else-if="planStatus === 4">
<template v-else-if="planStatus === 5">
<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">
@@ -257,6 +298,7 @@
<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>
</template>
</div>
</div>
</div>
@@ -402,8 +444,11 @@ export default {
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 || '';
if (item._needResolve === undefined) item._needResolve = item.processStatus === 1;
if (item._approveRemark === undefined) item._approveRemark = item.remark || '';
return item;
}));
self.$set(self.discTotalMap, relId, r.total || 0);
}).finally(function() { self.$set(self.discLoadingMap, relId, false); });
},
loadAllDisc() { var self = this; this.warehouseList.forEach(function(wh) { self.loadDisc(wh.relId); }); },
@@ -507,6 +552,10 @@ export default {
if (!row || !row.discrepancyId) return;
updateCountDiscrepancy({ discrepancyId: row.discrepancyId, reasonAnalysis: row.reasonAnalysis, processSuggestion: row.processSuggestion }).catch(function() {});
},
saveDiscMark(row) {
if (!row || !row.discrepancyId) return;
updateCountDiscrepancy({ discrepancyId: row.discrepancyId, processStatus: row._needResolve ? 1 : 0, remark: row._approveRemark }).catch(function() {});
},
// ===== 对比 =====
async doCompare(wh) {
@@ -568,6 +617,11 @@ export default {
<style scoped>
.wh-panel { }
.wh-empty { padding: 16px 0; color: #909399; font-size: 13px; }
.wh-detail-list { margin: 8px 0 16px; border: 1px solid #e8e4de; border-radius: 2px; overflow: hidden; }
.wh-detail-row { display: flex; padding: 8px 14px; font-size: 13px; border-bottom: 1px solid #f0ece6; }
.wh-detail-row:last-child { border-bottom: none; }
.wh-detail-label { width: 120px; flex-shrink: 0; color: #606266; font-weight: 500; }
.wh-detail-value { color: #303133; }
.doc-tabs-header { display: flex; border-bottom: 1px solid #d4d0c8; margin-bottom: 12px; }
.doc-tab-item { padding: 8px 16px; cursor: pointer; color: #8c8c8c; font-size: 13px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.2s, border-color 0.2s; display: flex; align-items: center; gap: 6px; user-select: none; }
.doc-tab-item:hover { color: #1a3c6e; }