Files
klp-oa/klp-ui/src/views/wms/post/InvCount/execute.vue

760 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>