feat: 新增盘库和维修计划初版

This commit is contained in:
2026-06-25 10:53:27 +08:00
parent e994afb97f
commit a9b4d5ddd6
17 changed files with 5303 additions and 2 deletions

View File

@@ -0,0 +1,661 @@
<template>
<div class="app-approval-wrapper">
<div class="app-container count-container">
<DragResizePanel :initialSize="280" :minSize="280" :maxSize="600">
<template #panelA>
<div class="left-panel">
<div class="panel-header">
<div class="header-title">
<i class="el-icon-s-check"></i>
<span>盘库审批</span>
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表"></el-button>
</div>
</div>
<div class="search-row">
<el-input v-model="queryParams.planCode" placeholder="搜索计划编号..." clearable prefix-icon="el-icon-search"
size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" />
</div>
<div v-loading="loading" class="list-body">
<div v-for="item in dataList" :key="item.planId" class="list-item"
:class="{ active: currentRow && currentRow.planId === item.planId }" @click="handleRowClick(item)">
<div class="item-main">
<span class="item-title">{{ item.planCode }}</span>
<span class="item-sub">{{ item.planName }}</span>
</div>
<div class="item-meta">
<el-tag size="mini">待审批</el-tag>
</div>
<div class="item-actions">
<el-button size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
<i class="el-icon-folder-opened"></i>
<span>暂无待审批盘库计划</span>
</div>
</div>
<div class="list-footer">
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
</div>
</template>
<template #panelB>
<div class="right-panel">
<div v-if="!currentRow" class="empty-tip">
<i class="el-icon-info"></i>
<span>请在左侧列表中选择一条盘库计划进行审批</span>
</div>
<div v-else v-loading="detailLoading" 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">Inventory Count Plan · Approval</div>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail" title="刷新详情">刷新</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Status / 状态</span>
<el-tag 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>盘库日期: {{ parseTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.deadlineTime"><i class="el-icon-time"></i>截止: {{ parseTime(currentRow.deadlineTime, '{y}-{m}-{d} {h}:{i}') }}</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>
<span v-if="currentRow.participantNames"><i class="el-icon-user"></i>参与人: {{ currentRow.participantNames }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<div class="section-title">
<span>审批操作 <span class="en-sub">· Approval Actions</span></span>
</div>
<div class="flow-actions">
<el-button type="danger" size="small" icon="el-icon-close" :loading="rejectLoading" @click="handleReject">驳回</el-button>
<el-button type="primary" size="small" icon="el-icon-check" :loading="approveLoading" @click="handleApprove">审批通过</el-button>
</div>
<el-divider />
<div class="section-title">
<span>库区盘点明细 <span class="en-sub">· Warehouse Count Details</span></span>
</div>
<el-table v-loading="warehouseLoading" :data="warehouseList" border size="small" style="width:100%" row-key="relId">
<el-table-column label="逻辑库区" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="出入库查询起始" align="center" width="160">
<template slot-scope="scope">{{ parseTime(scope.row.ioStartTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</template>
</el-table-column>
<el-table-column label="出入库查询截止" align="center" width="160">
<template slot-scope="scope">{{ parseTime(scope.row.ioEndTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</template>
</el-table-column>
</el-table>
<div class="section-gap" />
<!-- 差异列表审批人逐条标记需处理 -->
<div class="section-title">
<span>差异项标记 <span class="en-sub">· Mark Items for Resolution</span></span>
</div>
<el-table v-loading="discLoading" :data="discList" border size="small" style="width:100%">
<el-table-column label="差异类型" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.discrepancyType === 1" type="success" size="mini">盘盈</el-tag>
<el-tag v-else-if="scope.row.discrepancyType === 2" type="danger" size="mini">盘亏</el-tag>
<el-tag v-else-if="scope.row.discrepancyType === 3" type="warning" size="mini">状态不符</el-tag>
<el-tag v-else-if="scope.row.discrepancyType === 4" size="mini">重量偏差</el-tag>
</template>
</el-table-column>
<el-table-column label="钢卷号" align="center" prop="enterCoilNo" width="160" />
<el-table-column label="差异详情" align="center" prop="discrepancyDetail" min-width="200" show-overflow-tooltip />
<el-table-column label="需处理" align="center" width="80">
<template slot-scope="scope">
<el-tag v-if="scope.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="100">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditDiscMark(scope.row)">标记</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="discList.length === 0 && !discLoading" style="padding:12px 0;color:#909399;font-size:13px;">暂无差异记录</div>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
</div>
</template>
</DragResizePanel>
</div>
<!-- 标记需处理对话框 -->
<el-dialog title="标记差异项" :visible.sync="markDialogVisible" width="500px" append-to-body>
<el-form ref="markForm" :model="markForm" label-width="100px">
<el-form-item label="差异类型">
<el-tag v-if="markForm.discrepancyType === 1" type="success">盘盈</el-tag>
<el-tag v-else-if="markForm.discrepancyType === 2" type="danger">盘亏</el-tag>
<el-tag v-else-if="markForm.discrepancyType === 3" type="warning">状态不符</el-tag>
<el-tag v-else-if="markForm.discrepancyType === 4">重量偏差</el-tag>
</el-form-item>
<el-form-item label="钢卷号">{{ markForm.enterCoilNo }}</el-form-item>
<el-form-item label="差异详情">{{ markForm.discrepancyDetail }}</el-form-item>
<el-form-item label="需要处理">
<el-switch v-model="markForm.needResolve" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="审批意见">
<el-input v-model="markForm.approveRemark" type="textarea" :rows="3" placeholder="请输入审批意见" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button type="primary" @click="submitDiscMark"> </el-button>
<el-button @click="markDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listCountPlan, getCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import { listCountPlanWarehouse } from "@/api/flow/countPlanWarehouse";
import { listCountDiscrepancy, updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountApproval",
components: { DragResizePanel, CountFlowSection },
data() {
return {
loading: false,
detailLoading: false,
warehouseLoading: false,
approveLoading: false,
rejectLoading: false,
discLoading: false,
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
planCode: undefined,
planStatus: 1
},
dataList: [],
currentRow: null,
warehouseList: [],
discList: [],
markDialogVisible: false,
markForm: {
discrepancyId: null,
discrepancyType: null,
enterCoilNo: '',
discrepancyDetail: '',
needResolve: false,
approveRemark: ''
}
};
},
created() {
this.getList();
},
methods: {
parseTime,
getList() {
this.loading = true;
var self = this;
listCountPlan(this.queryParams).then(function(response) {
self.dataList = response.rows;
self.total = response.total;
self.loading = false;
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
handleRowClick(row) {
this.currentRow = row;
this.loadDetail(row.planId);
},
loadDetail(planId) {
this.detailLoading = true;
var self = this;
getCountPlan(planId).then(function(response) {
self.currentRow = response.data;
self.loadWarehouseList(planId);
}).finally(function() { self.detailLoading = false; });
},
loadWarehouseList(planId) {
this.warehouseLoading = true;
var self = this;
listCountPlanWarehouse({ planId: planId, pageNum: 1, pageSize: 999 }).then(function(r) {
self.warehouseList = r.rows || [];
self.loadDiscList(); // 仓库加载完成后加载差异
}).finally(function() { self.warehouseLoading = false; });
},
loadDiscList() {
this.discLoading = true;
this.discList = [];
var self = this;
if (this.warehouseList.length === 0) {
this.discLoading = false;
return;
}
var promises = this.warehouseList.map(function(wh) {
return listCountDiscrepancy({ relId: wh.relId, pageNum: 1, pageSize: 999 });
});
Promise.all(promises).then(function(results) {
var all = [];
results.forEach(function(r) { all = all.concat(r.rows || []); });
self.discList = all;
}).finally(function() { self.discLoading = false; });
},
handleRefreshDetail() {
if (this.currentRow && this.currentRow.planId) {
this.loadDetail(this.currentRow.planId);
}
},
handleEditDiscMark(row) {
this.markForm = {
discrepancyId: row.discrepancyId,
discrepancyType: row.discrepancyType,
enterCoilNo: row.enterCoilNo,
discrepancyDetail: row.discrepancyDetail,
needResolve: row.processStatus === 1,
approveRemark: row.remark || ''
};
this.markDialogVisible = true;
},
submitDiscMark() {
var self = this;
var newStatus = this.markForm.needResolve ? 1 : 0;
updateCountDiscrepancy({
discrepancyId: this.markForm.discrepancyId,
processStatus: newStatus,
remark: this.markForm.approveRemark
}).then(function() {
self.$modal.msgSuccess('标记已保存');
self.markDialogVisible = false;
self.loadDiscList();
}).catch(function() {
self.$modal.msgError('保存失败');
});
},
handleApprove() {
var self = this;
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的审批?通过后盘库计划将进入差异处理阶段。').then(function() {
self.approveLoading = true;
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
}).then(function() {
self.$modal.msgSuccess("审批通过,盘库计划已进入差异处理阶段");
self.approveLoading = false;
self.currentRow = null;
self.getList();
}).catch(function() { self.approveLoading = false; });
},
handleReject() {
var self = this;
this.$prompt('请输入驳回原因', '驳回审批', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputValidator: function(val) { return val ? true : '驳回原因不能为空'; }
}).then(function({ value }) {
self.rejectLoading = true;
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 0, remark: value });
}).then(function() {
self.$modal.msgSuccess("已驳回,盘库计划返回草稿状态");
self.rejectLoading = false;
self.currentRow = null;
self.getList();
}).catch(function() { self.rejectLoading = false; });
}
}
};
</script>
<style scoped>
.count-container {
height: calc(100vh - 84px);
}
/* ========== 左侧面板 ========== */
.left-panel {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
border-right: 1px solid #e4e7ed;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 8px;
background: #f5f7fa;
}
.header-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.header-title i {
color: #409eff;
font-size: 16px;
}
.search-row {
display: flex;
align-items: center;
gap: 6px;
padding: 0 14px 10px;
background: #f5f7fa;
}
.list-body {
flex: 1;
overflow-y: auto;
padding: 0 6px;
}
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
transition: all 0.15s;
}
.list-item:hover {
background: #ebeef5;
}
.list-item.active {
background: #d9ecff;
}
.list-item.active .item-title {
color: #409eff;
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.item-title {
font-size: 13px;
font-weight: 500;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-sub {
font-size: 12px;
color: #909399;
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
}
.item-actions {
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s;
}
.list-item:hover .item-actions {
opacity: 1;
}
.list-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #c0c4cc;
font-size: 13px;
gap: 8px;
}
.list-empty i {
font-size: 32px;
}
.list-footer {
border-top: 1px solid #e4e7ed;
padding: 2px 8px 0;
background: #f5f7fa;
}
/* ========== 右侧面板 — Word 文档风格 ========== */
.right-panel {
height: 100%;
overflow-y: auto;
padding: 12px 16px;
background: #faf8f5;
}
.right-panel .detail-content {
margin: 0 auto;
background: #ffffff;
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%;
}
.empty-tip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 14px;
gap: 8px;
}
.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-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
font-size: 24px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.3;
letter-spacing: 0.5px;
}
.doc-subtitle {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 12px;
font-weight: 400;
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-family: 'Georgia', 'Times New Roman', serif;
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-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 22px 0 12px 0;
padding: 0 0 10px 0;
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-family: 'Georgia', 'Times New Roman', serif;
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;
}
.right-panel .el-table {
border: 1px solid #e8e4de !important;
border-radius: 2px !important;
font-size: 12px !important;
}
.right-panel .el-table thead th {
background-color: #2c3e50 !important;
color: #ffffff !important;
font-weight: 600 !important;
font-size: 11px !important;
letter-spacing: 0.5px !important;
border-bottom: none !important;
font-family: 'Georgia', 'Times New Roman', serif;
}
.right-panel .el-table thead th .cell {
color: #ffffff !important;
}
.right-panel .el-table__body tr:hover > td {
background-color: #f7f5f0 !important;
}
.right-panel .el-table--border td {
border-right: 1px solid #f0ece6 !important;
}
.right-panel .el-table--border th {
border-right: 1px solid #3a5166 !important;
}
.right-panel .el-table td {
padding: 6px 4px !important;
color: #3a3a3a !important;
}
.right-panel .el-divider--horizontal {
margin: 8px 0 4px;
background-color: #e0dcd6;
}
.right-panel .el-tag {
border-radius: 2px;
font-family: 'Georgia', 'Times New Roman', serif;
letter-spacing: 0.3px;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div v-if="enabled" class="section-container">
<div class="section-title">
<i class="el-icon-s-order"></i>
<span>流程总览 <span class="en-sub">· Process Overview</span></span>
</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-circle-check" />
</el-steps>
<div class="current-status">
<span class="status-label">Current / 当前阶段</span>
<el-tag :type="tagType" size="small">{{ flowStatusText }}</el-tag>
</div>
</div>
</template>
<script>
export default {
name: 'CountFlowSection',
props: {
enabled: {
type: Boolean,
default: true
},
planStatus: {
type: [Number, String],
default: undefined
}
},
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)
*/
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;
return v;
},
flowStatusText() {
const map = {
0: '草稿',
1: '待审批',
2: '执行中',
3: '差异处理中',
4: '已归档'
};
return map[this.planStatus] || '未知';
},
tagType() {
const map = {
0: 'info',
1: '',
2: 'warning',
3: 'danger',
4: 'success'
};
return map[this.planStatus] || '';
}
}
}
</script>
<style scoped>
.section-container {
margin-bottom: 6px;
}
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 0 0 12px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
}
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
}
.flow-steps {
padding: 8px 0 4px;
}
.flow-steps >>> .el-step.is-wait .el-step__icon-inner,
.flow-steps >>> .el-step.is-wait .el-step__title {
color: #c0c4cc;
}
.flow-steps >>> .el-step.is-process .el-step__icon-inner,
.flow-steps >>> .el-step.is-process .el-step__title {
color: #409eff;
}
.flow-steps >>> .el-step.is-finish .el-step__icon-inner,
.flow-steps >>> .el-step.is-finish .el-step__title {
color: #67c23a;
}
.flow-steps >>> .el-step__description {
display: none;
}
.current-status {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
margin-top: 6px;
padding-top: 8px;
border-top: 1px dashed #e0dcd6;
}
.status-label {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
letter-spacing: 0.3px;
}
</style>

View File

@@ -0,0 +1,759 @@
<template>
<div class="app-container count-container">
<DragResizePanel :initialSize="280" :minSize="280" :maxSize="600">
<template #panelA>
<div class="left-panel">
<div class="panel-header">
<div class="header-title">
<i class="el-icon-s-data"></i>
<span>盘库执行</span>
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表"></el-button>
</div>
<el-select v-model="queryParams.planStatus" placeholder="执行状态" clearable size="mini" @change="handleQuery" class="header-filter">
<el-option label="执行中" :value="2" />
<el-option label="差异处理中" :value="3" />
</el-select>
</div>
<div class="search-row">
<el-input v-model="queryParams.planCode" placeholder="搜索计划编号..." clearable prefix-icon="el-icon-search"
size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" />
</div>
<div v-loading="loading" class="list-body">
<div v-for="item in dataList" :key="item.planId" class="list-item"
:class="{ active: currentRow && currentRow.planId === item.planId }" @click="handleRowClick(item)">
<div class="item-main">
<span class="item-title">{{ item.planCode }}</span>
<span class="item-sub">{{ item.planName }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.planStatus === 2" type="warning" size="mini">执行中</el-tag>
<el-tag v-else-if="item.planStatus === 3" type="danger" size="mini">差异处理中</el-tag>
</div>
<div class="item-actions">
<el-button size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
<i class="el-icon-folder-opened"></i>
<span>暂无执行中的盘库计划</span>
</div>
</div>
<div class="list-footer">
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
</div>
</template>
<template #panelB>
<div class="right-panel">
<div v-if="!currentRow" class="empty-tip">
<i class="el-icon-info"></i>
<span>请在左侧列表中选择一条盘库计划执行操作</span>
</div>
<div v-else v-loading="detailLoading" 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">Inventory Count Plan · Execution</div>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail" title="刷新详情">刷新</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Status / 状态</span>
<el-tag v-if="currentRow.planStatus === 2" type="warning" size="small">执行中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 3" type="danger" 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>盘库日期: {{ parseTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.deadlineTime"><i class="el-icon-time"></i>截止: {{ parseTime(currentRow.deadlineTime, '{y}-{m}-{d} {h}:{i}') }}</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>
<span v-if="currentRow.participantNames"><i class="el-icon-user"></i>参与人: {{ currentRow.participantNames }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<div class="section-title">
<span>执行操作 <span class="en-sub">· Execution Actions</span></span>
</div>
<div class="flow-actions">
<el-button v-if="currentRow.planStatus === 2" type="primary" size="small" icon="el-icon-upload2" @click="handleUploadForPlan">上传实盘Excel</el-button>
<el-button v-if="currentRow.planStatus === 2" type="warning" size="small" icon="el-icon-s-data" :loading="generateLoading" @click="handleGenerateDiscrepancy">核对并生成差异报告</el-button>
<el-button v-if="currentRow.planStatus === 3" type="success" size="small" icon="el-icon-circle-check" :loading="archiveLoading" @click="handleArchive">归档封存</el-button>
</div>
<el-divider />
<div class="section-title">
<span>库区盘点明细 <span class="en-sub">· Warehouse Count Details</span></span>
</div>
<el-table v-loading="warehouseLoading" :data="warehouseList" border size="small" style="width:100%" row-key="relId">
<el-table-column type="expand" width="40">
<template slot-scope="scope">
<div v-loading="discrepancyLoadingMap[scope.row.relId]" style="padding: 8px 16px;">
<el-table v-if="discrepancyMap[scope.row.relId] && discrepancyMap[scope.row.relId].length > 0"
:data="discrepancyMap[scope.row.relId]" border size="small">
<el-table-column label="差异类型" align="center" width="100">
<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-if="ds.row.discrepancyType === 4" size="mini">重量偏差</el-tag>
</template>
</el-table-column>
<el-table-column label="钢卷号" align="center" prop="enterCoilNo" width="160" />
<el-table-column label="差异详情" align="center" prop="discrepancyDetail" min-width="200" show-overflow-tooltip />
<el-table-column label="原因分析" align="center" prop="reasonAnalysis" min-width="150" show-overflow-tooltip />
<el-table-column label="处理建议" align="center" prop="processSuggestion" min-width="150" show-overflow-tooltip />
<el-table-column label="处理结果" align="center" prop="processResult" min-width="150" show-overflow-tooltip />
<el-table-column label="处理状态" align="center" width="100">
<template slot-scope="ds">
<el-tag v-if="ds.row.processStatus === 0" type="info" size="mini">待处理</el-tag>
<el-tag v-else-if="ds.row.processStatus === 1" type="warning" size="mini">处理中</el-tag>
<el-tag v-else-if="ds.row.processStatus === 2" type="success" size="mini">已处理</el-tag>
</template>
</el-table-column>
<el-table-column label="处理人" align="center" prop="processUserName" width="100" />
<el-table-column label="操作" align="center" width="100" v-if="currentRow.planStatus === 3">
<template slot-scope="ds">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditDiscrepancy(ds.row)">处理</el-button>
</template>
</el-table-column>
</el-table>
<span v-else style="color:#909399;font-size:13px;">暂无差异记录</span>
</div>
</template>
</el-table-column>
<el-table-column label="逻辑库区" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="出入库查询起始" align="center" width="140">
<template slot-scope="scope">{{ parseTime(scope.row.ioStartTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</template>
</el-table-column>
<el-table-column label="出入库查询截止" align="center" width="140">
<template slot-scope="scope">{{ parseTime(scope.row.ioEndTime, '{y}-{m}-{d} {h}:{i}') || '-' }}</template>
</el-table-column>
<el-table-column label="系统钢卷数量" align="center" prop="systemCoilCount" width="100" />
<el-table-column label="系统总重量(kg)" align="center" prop="systemTotalWeight" width="110" />
<el-table-column label="实盘钢卷数量" align="center" prop="actualCoilCount" width="100" />
<el-table-column label="实盘总重量(kg)" align="center" prop="actualTotalWeight" width="110" />
<el-table-column label="账实一致" align="center" width="80">
<template slot-scope="scope">
<el-tag v-if="scope.row.isConsistent === 1" type="success" size="mini">一致</el-tag>
<el-tag v-else-if="scope.row.isConsistent === 0" type="danger" size="mini">不一致</el-tag>
<span v-else style="color:#909399">未盘点</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120" fixed="right">
<template slot-scope="scope">
<el-button v-if="currentRow.planStatus === 2" size="mini" type="text" icon="el-icon-upload2" @click="handleUploadExcel(scope.row)">上传Excel</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleViewDiscrepancy(scope.row)">差异</el-button>
</template>
</el-table-column>
</el-table>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
</div>
</template>
</DragResizePanel>
<el-dialog title="上传实盘Excel" :visible.sync="uploadDialogVisible" width="500px" append-to-body>
<el-upload
ref="upload"
class="upload-demo"
drag
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:data="{ relId: uploadingRelId }"
:file-list="uploadFileList"
:auto-upload="false"
accept=".xlsx,.xls">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将实盘Excel文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">仅支持 .xlsx / .xls 格式</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitUpload"> </el-button>
</div>
</el-dialog>
<el-dialog title="差异处理" :visible.sync="discDialogVisible" width="600px" append-to-body>
<el-form ref="discForm" :model="discForm" label-width="100px">
<el-form-item label="差异类型">
<el-tag v-if="discForm.discrepancyType === 1" type="success">盘盈</el-tag>
<el-tag v-else-if="discForm.discrepancyType === 2" type="danger">盘亏</el-tag>
<el-tag v-else-if="discForm.discrepancyType === 3" type="warning">状态不符</el-tag>
<el-tag v-else-if="discForm.discrepancyType === 4">重量偏差</el-tag>
</el-form-item>
<el-form-item label="钢卷号">
<span>{{ discForm.enterCoilNo }}</span>
</el-form-item>
<el-form-item label="差异详情">
<span>{{ discForm.discrepancyDetail }}</span>
</el-form-item>
<el-form-item label="原因分析" prop="reasonAnalysis">
<el-input v-model="discForm.reasonAnalysis" type="textarea" :rows="2" placeholder="请输入原因分析" />
</el-form-item>
<el-form-item label="处理建议" prop="processSuggestion">
<el-input v-model="discForm.processSuggestion" type="textarea" :rows="2" placeholder="请输入处理建议" />
</el-form-item>
<el-form-item label="处理结果" prop="processResult">
<el-input v-model="discForm.processResult" type="textarea" :rows="2" placeholder="请输入处理结果" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="discDialogVisible = false"> </el-button>
<el-button :loading="discButtonLoading" type="primary" @click="submitDiscrepancyForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listCountPlan, getCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import { listCountPlanWarehouse } from "@/api/flow/countPlanWarehouse";
import { listCountDiscrepancy, updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import { getToken } from '@/utils/auth'
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountExecute",
components: { DragResizePanel, CountFlowSection },
data() {
return {
loading: false,
detailLoading: false,
warehouseLoading: false,
generateLoading: false,
archiveLoading: false,
discButtonLoading: false,
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
planCode: undefined,
planStatus: undefined
},
dataList: [],
currentRow: null,
warehouseList: [],
discrepancyMap: {},
discrepancyLoadingMap: {},
// upload
uploadDialogVisible: false,
uploadFileList: [],
uploadingRelId: null,
// discrepancy dialog
discDialogVisible: false,
discForm: {}
};
},
computed: {
uploadAction() {
return process.env.VUE_APP_BASE_API + "/flow/countPlan/uploadExcel";
},
uploadHeaders() {
return { Authorization: "Bearer " + getToken() };
}
},
created() {
this.getList();
},
methods: {
parseTime,
getList() {
this.loading = true;
var self = this;
listCountPlan(this.queryParams).then(function(response) {
self.dataList = response.rows;
self.total = response.total;
self.loading = false;
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
handleRowClick(row) {
this.currentRow = row;
this.loadDetail(row.planId);
},
loadDetail(planId) {
this.detailLoading = true;
var self = this;
getCountPlan(planId).then(function(response) {
self.currentRow = response.data;
self.loadWarehouseList(planId);
}).finally(function() { self.detailLoading = false; });
},
loadWarehouseList(planId) {
this.warehouseLoading = true;
var self = this;
listCountPlanWarehouse({ planId: planId, pageNum: 1, pageSize: 999 }).then(function(r) {
self.warehouseList = r.rows || [];
}).finally(function() { self.warehouseLoading = false; });
},
handleRefreshDetail() {
if (this.currentRow && this.currentRow.planId) {
this.loadDetail(this.currentRow.planId);
}
},
handleViewDiscrepancy(row) {
var relId = row.relId;
if (this.discrepancyMap[relId]) {
return;
}
this.$set(this.discrepancyLoadingMap, relId, true);
var self = this;
listCountDiscrepancy({ relId: relId, pageNum: 1, pageSize: 999 }).then(function(r) {
self.$set(self.discrepancyMap, relId, r.rows || []);
}).finally(function() {
self.$set(self.discrepancyLoadingMap, relId, false);
});
},
// ---- Upload ----
handleUploadExcel(row) {
this.uploadingRelId = row.relId;
this.uploadFileList = [];
this.uploadDialogVisible = true;
},
handleUploadForPlan() {
if (this.warehouseList.length === 0) {
this.$modal.msgWarning("无关联库区,无需上传");
return;
}
this.handleUploadExcel(this.warehouseList[0]);
},
beforeUpload(file) {
var isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
if (!isExcel) {
this.$modal.msgError("仅支持 .xlsx / .xls 格式文件");
return false;
}
return true;
},
submitUpload() {
this.$refs.upload.submit();
},
handleUploadSuccess(response, file, fileList) {
if (response.code === 200) {
this.$modal.msgSuccess("Excel上传成功");
this.uploadDialogVisible = false;
this.loadWarehouseList(this.currentRow.planId);
} else {
this.$modal.msgError(response.msg || "上传失败");
}
},
handleUploadError(err, file, fileList) {
this.$modal.msgError("文件上传失败,请重试");
},
// ---- Workflow actions ----
handleGenerateDiscrepancy() {
var self = this;
this.$modal.confirm('确认将系统库存快照与实盘Excel数据进行核对系统将自动逐项比对并找出差异。').then(function() {
self.generateLoading = true;
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
}).then(function() {
self.$modal.msgSuccess("差异报告已生成");
self.generateLoading = false;
self.loadDetail(self.currentRow.planId);
self.getList();
}).catch(function() { self.generateLoading = false; });
},
handleArchive() {
var self = this;
this.$modal.confirm('确认将盘库计划"' + this.currentRow.planCode + '"归档封存?归档后数据将不可修改。').then(function() {
self.archiveLoading = true;
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 4 });
}).then(function() {
self.$modal.msgSuccess("盘库计划已归档");
self.archiveLoading = false;
self.currentRow = null;
self.getList();
}).catch(function() { self.archiveLoading = false; });
},
// ---- Discrepancy handling ----
handleEditDiscrepancy(row) {
this.discForm = Object.assign({}, row);
this.discDialogVisible = true;
},
submitDiscrepancyForm() {
var self = this;
this.discButtonLoading = true;
updateCountDiscrepancy({
discrepancyId: this.discForm.discrepancyId,
reasonAnalysis: this.discForm.reasonAnalysis,
processSuggestion: this.discForm.processSuggestion,
processResult: this.discForm.processResult,
processStatus: this.discForm.processResult ? 2 : this.discForm.processStatus
}).then(function() {
self.$modal.msgSuccess("差异处理保存成功");
self.discDialogVisible = false;
var relId = self.discForm.relId;
if (relId) {
self.$set(self.discrepancyMap, relId, undefined);
self.handleViewDiscrepancy({ relId: relId });
}
}).finally(function() { self.discButtonLoading = false; });
}
}
};
</script>
<style scoped>
.count-container {
height: calc(100vh - 84px);
}
/* ========== 左侧面板 ========== */
.left-panel {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
border-right: 1px solid #e4e7ed;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 8px;
background: #f5f7fa;
}
.header-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.header-title i {
color: #409eff;
font-size: 16px;
}
.header-filter {
width: 130px;
}
.search-row {
display: flex;
align-items: center;
gap: 6px;
padding: 0 14px 10px;
background: #f5f7fa;
}
.list-body {
flex: 1;
overflow-y: auto;
padding: 0 6px;
}
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
transition: all 0.15s;
}
.list-item:hover {
background: #ebeef5;
}
.list-item.active {
background: #d9ecff;
}
.list-item.active .item-title {
color: #409eff;
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.item-title {
font-size: 13px;
font-weight: 500;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-sub {
font-size: 12px;
color: #909399;
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
}
.item-actions {
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s;
}
.list-item:hover .item-actions {
opacity: 1;
}
.list-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #c0c4cc;
font-size: 13px;
gap: 8px;
}
.list-empty i {
font-size: 32px;
}
.list-footer {
border-top: 1px solid #e4e7ed;
padding: 2px 8px 0;
background: #f5f7fa;
}
/* ========== 右侧面板 — Word 文档风格 ========== */
.right-panel {
height: 100%;
overflow-y: auto;
padding: 12px 16px;
background: #faf8f5;
}
.right-panel .detail-content {
margin: 0 auto;
background: #ffffff;
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%;
}
.empty-tip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 14px;
gap: 8px;
}
.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-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
font-size: 24px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.3;
letter-spacing: 0.5px;
}
.doc-subtitle {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 12px;
font-weight: 400;
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-family: 'Georgia', 'Times New Roman', serif;
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-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 22px 0 12px 0;
padding: 0 0 10px 0;
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-family: 'Georgia', 'Times New Roman', serif;
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;
}
.right-panel .el-table {
border: 1px solid #e8e4de !important;
border-radius: 2px !important;
font-size: 12px !important;
}
.right-panel .el-table thead th {
background-color: #2c3e50 !important;
color: #ffffff !important;
font-weight: 600 !important;
font-size: 11px !important;
letter-spacing: 0.5px !important;
border-bottom: none !important;
font-family: 'Georgia', 'Times New Roman', serif;
}
.right-panel .el-table thead th .cell {
color: #ffffff !important;
}
.right-panel .el-table__body tr:hover > td {
background-color: #f7f5f0 !important;
}
.right-panel .el-table--border td {
border-right: 1px solid #f0ece6 !important;
}
.right-panel .el-table--border th {
border-right: 1px solid #3a5166 !important;
}
.right-panel .el-table td {
padding: 6px 4px !important;
color: #3a3a3a !important;
}
.right-panel .el-divider--horizontal {
margin: 8px 0 4px;
background-color: #e0dcd6;
}
.right-panel .el-tag {
border-radius: 2px;
font-family: 'Georgia', 'Times New Roman', serif;
letter-spacing: 0.3px;
}
.upload-demo {
text-align: center;
}
</style>

File diff suppressed because it is too large Load Diff