Files
klp-oa/klp-ui/src/views/wms/receive/plan/index.vue
砂糖 903c354add feat(wms): 新增应收货物计划批量删除和清空功能,优化表格配置
1. 后端添加批量删除应收计划接口
2. 前端新增批量删除、清空按钮和多选功能
3. 优化表格高度和字段文案,调整分页查询大小
2026-05-22 13:02:53 +08:00

1310 lines
48 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">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="queryParams.planName" placeholder="请输入收货计划名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="计划日期" prop="planDate">
<el-date-picker clearable v-model="queryParams.planDate" type="date" value-format="yyyy-MM-dd"
placeholder="请选择计划日期">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
@click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-row :gutter="20" v-loading="loading">
<el-col :span="6" v-for="(row, index) in deliveryPlanList" :key="row.planId">
<el-card shadow="hover" class="delivery-plan-card">
<div class="card-header">
<el-checkbox v-model="row.selected" @change="handleCardSelectionChange(row)"></el-checkbox>
<div class="card-title">{{ row.planName }}</div>
</div>
<div class="card-content">
<div class="content-item">
<span class="label">计划日期</span>
<span>{{ parseTime(row.planDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="content-item">
<span class="label">备注</span>
<span>{{ row.remark || '-' }}</span>
</div>
<div class="content-item">
<span class="label">创建人</span>
<span>{{ row.createBy }}</span>
</div>
<div class="content-item">
<span class="label">更新时间</span>
<span>{{ parseTime(row.updateTime, '{y}-{m}-{d}') }}</span>
</div>
</div>
<div class="card-actions">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-upload2" @click="openImportDialog(row)">导入</el-button>
<el-button size="mini" type="text" icon="el-icon-notebook-2" @click="openDetailDialog(row)">明细</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(row)">删除</el-button>
</div>
</el-card>
</el-col>
</el-row>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 添加或修改收货计划对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="收货计划名称" prop="planName">
<el-input v-model="form.planName" placeholder="请输入收货计划名称" />
</el-form-item>
<el-form-item label="计划日期" prop="planDate">
<el-date-picker clearable v-model="form.planDate" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择计划日期">
</el-date-picker>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 待收货明细管理对话框 -->
<el-dialog title="待收货明细管理" :visible.sync="detailOpen" width="1400px" append-to-body @close="resetDetail">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="计划入库" name="plan">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini"
@click="openDetailAddDialog">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="detailMultiple"
@click="handleDetailBatchDelete">批量删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini"
@click="handleDetailClear">清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="default" plain icon="el-icon-refresh" size="mini"
@click="getDetailList">刷新</el-button>
</el-col>
</el-row>
<el-table v-loading="detailLoading" :data="detailList" border @selection-change="handleDetailSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="warehouseArea" label="逻辑库区" width="120" />
<el-table-column prop="lotNo" label="入场卷号" width="120" />
<el-table-column prop="supplierLotNo" label="厂家卷号" width="120" />
<el-table-column prop="productLotNo" label="成品卷号" width="120" />
<el-table-column prop="productionDate" label="生产/发货日期" width="150">
<template slot-scope="scope">
{{ parseTime(scope.row.productionDate, '{y}-{m}-{d}') }}
</template>
</el-table-column>
<el-table-column prop="weight" label="重量(kg)" width="100" />
<el-table-column prop="goodsName" label="名称" width="120" />
<el-table-column prop="spec" label="规格" width="120" />
<el-table-column prop="length" label="长度(mm)" width="100" />
<el-table-column prop="materialType" label="材质" width="100" />
<el-table-column prop="manufacturer" label="生产厂家" width="120" />
<el-table-column prop="surfaceTreatment" label="表面处理" width="100" />
<el-table-column prop="zincCoating" label="锌层" width="80" />
<el-table-column prop="teamGroup" label="班组" width="100" />
<el-table-column prop="temperRolling" label="调制度" width="100" />
<el-table-column prop="coatingType" label="镀层种类" width="100" />
<el-table-column prop="goodsType" label="类型" width="100" />
<el-table-column prop="receiveStatus" label="状态" width="80">
<template slot-scope="scope">
<el-tag
:type="scope.row.receiveStatus === '0' ? 'info' : scope.row.receiveStatus === '1' ? 'success' : 'danger'">
{{ scope.row.receiveStatus === '0' ? '待收货' : scope.row.receiveStatus === '1' ? '已收货' : '异常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openDetailEditDialog(scope.row)">修改</el-button>
<el-button size="mini" type="text" class="btn-danger"
@click="handleDetailDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="detailTotal > 0" :total="detailTotal" :page.sync="detailQueryParams.pageNum"
:limit.sync="detailQueryParams.pageSize" @pagination="getDetailList" />
</el-tab-pane>
<el-tab-pane label="实际入库表格" name="actual">
<el-table v-loading="actualLoading" :data="actualList" border height="400">
<el-table-column prop="enterCoilNo" label="入场卷号" />
<el-table-column prop="currentCoilNo" label="成品卷号" />
<el-table-column prop="itemName" label="名称" width="120" />
<el-table-column prop="specification" label="规格" width="120" />
<el-table-column prop="materialType" label="材质" width="100" />
<el-table-column prop="netWeight" label="重量(kg)" width="100" />
<el-table-column prop="manufacturer" label="生产厂家" width="120" />
<el-table-column prop="zincLayer" label="镀层质量" width="80" />
<el-table-column prop="warehouseName" label="库区" width="100" />
<el-table-column prop="qualityStatus" label="质量状态" width="100" />
<el-table-column prop="createTime" label="入库时间">
<template slot-scope="scope">
{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="差异对比" name="diff">
<div class="diff-config">
<el-form :model="diffConfig" inline>
<el-form-item label="重量差异阈值(kg)">
<el-input-number v-model="diffConfig.weightThreshold" :precision="2" :step="0.1" :min="0" style="width: 120px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="calculateDiff">重新计算差异</el-button>
</el-form-item>
</el-form>
<span class="config-tip">重量差异在此阈值内视为相同超出则标记为不一致</span>
</div>
<el-row :gutter="16" class="diff-row">
<el-col :span="12">
<div class="diff-section">
<div class="section-header">
<span class="section-icon missing-icon"></span>
<span class="section-title">计划有实际无少收</span>
<span class="section-count">{{ missingCoils.length }}</span>
</div>
<el-table v-loading="diffLoading" height="300" :data="missingCoils" border size="small" class="diff-table">
<el-table-column prop="lotNo" label="入场卷号" />
<el-table-column prop="goodsName" label="名称" width="100" />
<el-table-column prop="spec" label="规格" width="100" />
<el-table-column prop="materialType" label="材质" width="80" />
<el-table-column prop="weight" label="重量(kg)" width="90" />
</el-table>
<div v-if="missingCoils.length === 0" class="empty-tip">暂无少收卷</div>
</div>
</el-col>
<el-col :span="12">
<div class="diff-section">
<div class="section-header">
<span class="section-icon extra-icon"></span>
<span class="section-title">实际有计划无多收</span>
<span class="section-count">{{ extraCoils.length }}</span>
</div>
<el-table v-loading="diffLoading" height="300" :data="extraCoils" border size="small" class="diff-table">
<el-table-column prop="enterCoilNo" label="入场卷号" />
<el-table-column prop="itemName" label="名称" width="100" />
<el-table-column prop="specification" label="规格" width="100" />
<el-table-column prop="materialType" label="材质" width="80" />
<el-table-column prop="netWeight" label="重量(kg)" width="90" />
</el-table>
<div v-if="extraCoils.length === 0" class="empty-tip">暂无多收卷</div>
</div>
</el-col>
</el-row>
<div class="diff-section">
<div class="section-header">
<span class="section-icon diff-icon"></span>
<span class="section-title">字段不一致</span>
<span class="section-count">{{ diffCoils.length }}</span>
</div>
<el-table v-loading="diffLoading" height="300" :data="diffCoils" border size="small" class="diff-table">
<el-table-column prop="lotNo" label="入场卷号" width="120" />
<el-table-column label="字段差异" min-width="500">
<template #default="scope">
<div class="diff-content">
<div v-for="(diff, index) in scope.row.diffs" :key="index" class="diff-item">
<span class="field-label">{{ diff.fieldName }}</span>
<div class="value-row">
<span class="value-label">计划:</span>
<span class="plan-value">{{ diff.planValue }}</span>
</div>
<div class="value-row">
<span class="value-label">实际:</span>
<span class="actual-value">{{ diff.actualValue }}</span>
</div>
</div>
</div>
</template>
</el-table-column>
</el-table>
<div v-if="diffCoils.length === 0" class="empty-tip">暂无字段不一致的卷</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!-- 添加或修改待收货明细对话框 -->
<el-dialog :title="detailFormTitle" :visible.sync="detailFormOpen" width="600px" append-to-body
@close="resetDetailForm">
<el-form ref="detailForm" :model="detailForm" :rules="detailRules" label-width="100px">
<el-form-item label="逻辑库区" prop="warehouseArea">
<el-input v-model="detailForm.warehouseArea" placeholder="请输入逻辑库区" />
</el-form-item>
<el-form-item label="入场卷号" prop="lotNo">
<el-input v-model="detailForm.lotNo" placeholder="请输入入场卷号" />
</el-form-item>
<el-form-item label="厂家卷号" prop="supplierLotNo">
<el-input v-model="detailForm.supplierLotNo" placeholder="请输入厂家卷号" />
</el-form-item>
<el-form-item label="成品卷号" prop="productLotNo">
<el-input v-model="detailForm.productLotNo" placeholder="请输入成品卷号" />
</el-form-item>
<el-form-item label="生产/发货日期" prop="productionDate">
<el-date-picker clearable v-model="detailForm.productionDate" type="date" value-format="yyyy-MM-dd"
placeholder="请选择生产/发货日期" style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item label="重量(kg)" prop="weight">
<el-input-number v-model="detailForm.weight" :precision="2" :step="0.01" style="width: 100%;" />
</el-form-item>
<el-form-item label="名称" prop="goodsName">
<el-input v-model="detailForm.goodsName" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="规格" prop="spec">
<el-input v-model="detailForm.spec" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="长度(mm)" prop="length">
<el-input-number v-model="detailForm.length" :precision="2" :step="0.01" style="width: 100%;" />
</el-form-item>
<el-form-item label="材质" prop="materialType">
<el-input v-model="detailForm.materialType" placeholder="请输入材质" />
</el-form-item>
<el-form-item label="生产厂家" prop="manufacturer">
<el-input v-model="detailForm.manufacturer" placeholder="请输入生产厂家" />
</el-form-item>
<el-form-item label="表面处理" prop="surfaceTreatment">
<el-input v-model="detailForm.surfaceTreatment" placeholder="请输入表面处理" />
</el-form-item>
<el-form-item label="锌层" prop="zincCoating">
<el-input v-model="detailForm.zincCoating" placeholder="请输入锌层" />
</el-form-item>
<el-form-item label="班组" prop="teamGroup">
<el-input v-model="detailForm.teamGroup" placeholder="请输入班组" />
</el-form-item>
<el-form-item label="调制度" prop="temperRolling">
<el-input v-model="detailForm.temperRolling" placeholder="请输入调制度" />
</el-form-item>
<el-form-item label="镀层种类" prop="coatingType">
<el-input v-model="detailForm.coatingType" placeholder="请输入镀层种类" />
</el-form-item>
<el-form-item label="类型" prop="goodsType">
<el-input v-model="detailForm.goodsType" placeholder="请输入类型" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="detailButtonLoading" type="primary" @click="submitDetailForm"> </el-button>
<el-button @click="detailFormOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 待收货明细导入对话框 -->
<el-dialog title="待收货明细导入" :visible.sync="importOpen" width="1000px" append-to-body>
<div class="import-container">
<div class="file-upload-area" :class="{ disabled: importStatus === 'processing' }">
<el-upload ref="upload" class="upload-excel" action="" :auto-upload="false" :show-file-list="false"
:on-change="handleFileChange" accept=".xlsx,.xls"
:disabled="importStatus === 'processing' || validateLoading || importLoading">
<el-button type="primary" icon="el-icon-upload2">选择Excel文件</el-button>
</el-upload>
<el-button type="success" icon="el-icon-check" @click="handleValidate" v-if="file && importStatus === 'idle'"
:disabled="!file || validateLoading" :loading="validateLoading">
校验数据
</el-button>
<el-button type="warning" icon="el-icon-circle-check" @click="startImport"
v-if="file && importStatus === 'idle'"
:disabled="!file || !isValidated || errorList.length > 0 || importLoading" :loading="importLoading">
开始导入
</el-button>
<el-button type="default" icon="el-icon-refresh" @click="resetImport"
:disabled="importStatus === 'processing' || validateLoading || importLoading">
重置
</el-button>
</div>
<div class="template-download">
<el-link type="primary" icon="el-icon-download" @click="downloadTemplate">下载导入模板</el-link>
<span class="template-tip">请按照模板格式填写数据</span>
</div>
<div v-if="errorList.length > 0" class="error-list">
<el-alert title="数据校验失败" type="error" :description="`共发现${errorList.length}条错误,请修正后重新导入`" show-icon />
<el-table :data="errorList" border size="small" max-height="200">
<el-table-column prop="rowNum" label="行号" width="80" />
<el-table-column prop="errorMsg" label="错误信息" />
</el-table>
</div>
<div v-if="tableData.length > 0 && importStatus === 'idle'" class="data-preview">
<el-alert title="数据预览" type="info" :description="`共解析出 ${tableData.length} 条有效数据`" show-icon
:closable="false" />
<el-table :data="tableData" border size="small" max-height="300" stripe>
<el-table-column prop="warehouseArea" label="逻辑库区" width="100" />
<el-table-column prop="lotNo" label="入场卷号" width="100" />
<el-table-column prop="supplierLotNo" label="厂家卷号" width="100" />
<el-table-column prop="productLotNo" label="成品卷号" width="100" />
<el-table-column prop="productionDate" label="生产/发货日期" width="120" />
<el-table-column prop="weight" label="重量(kg)" width="100" />
<el-table-column prop="goodsName" label="名称" width="120" />
<el-table-column prop="spec" label="规格" width="100" />
<el-table-column prop="length" label="长度(mm)" width="100" />
<el-table-column prop="materialType" label="材质" width="100" />
<el-table-column prop="manufacturer" label="生产厂家" width="120" />
</el-table>
</div>
<div v-if="importStatus === 'processing'" class="import-progress">
<el-alert title="正在导入数据" type="warning" :description="`当前进度:${progress}%`" show-icon :closable="false" />
<el-progress :percentage="progress" status="success" />
<p class="progress-tip">已导入 {{ importedCount }} / {{ totalCount }} 条数据</p>
</div>
<div v-if="importStatus === 'finished'" class="import-finished">
<el-alert title="导入完成" type="success" :description="`共成功导入 ${importedCount} 条数据,总计 ${totalCount} 条`"
show-icon />
</div>
<div v-if="importStatus === 'error'" class="import-error">
<el-alert title="导入失败" type="error" :description="importErrorMsg" show-icon />
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { listDeliveryPlan, getDeliveryPlan, delDeliveryPlan, addDeliveryPlan, updateDeliveryPlan } from "@/api/wms/deliveryPlan";
import { listReceivePlan, getReceivePlan, delReceivePlan, addReceivePlan, updateReceivePlan, checkReceivePlan, delReceivePlanBatch } from "@/api/wms/receivePlan";
import { listCoilWithIds } from "@/api/wms/coil";
import { listPendingAction } from "@/api/wms/pendingAction";
const TEMPLATE_HEADERS = [
'逻辑库区', '入场卷号', '厂家卷号', '成品卷号', '生产/发货日期', '重量(kg)',
'名称', '规格', '长度(mm)', '材质', '生产厂家', '表面处理', '锌层',
'班组', '调制度', '镀层种类', '类型', '备注'
];
const HEADER_MAP = {
'逻辑库区': 'warehouseArea',
'入场卷号': 'lotNo',
'厂家卷号': 'supplierLotNo',
'成品卷号': 'productLotNo',
'生产/发货日期': 'productionDate',
'重量(kg)': 'weight',
'名称': 'goodsName',
'规格': 'spec',
'长度(mm)': 'length',
'材质': 'materialType',
'生产厂家': 'manufacturer',
'表面处理': 'surfaceTreatment',
'锌层': 'zincCoating',
'班组': 'teamGroup',
'调制度': 'temperRolling',
'镀层种类': 'coatingType',
'类型': 'goodsType',
'备注': 'remark'
};
export default {
name: "DeliveryPlan",
data() {
return {
buttonLoading: false,
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
deliveryPlanList: [],
title: "",
open: false,
queryParams: {
pageNum: 1,
pageSize: 10,
planName: undefined,
planDate: undefined,
planType: 1
},
form: {
planName: '',
planDate: '',
},
rules: {},
detailOpen: false,
detailLoading: false,
detailList: [],
detailTotal: 0,
currentPlanId: null,
detailQueryParams: {
pageNum: 1,
pageSize: 10,
planId: null
},
detailFormOpen: false,
detailFormTitle: '',
detailButtonLoading: false,
detailIds: [],
detailSingle: true,
detailMultiple: true,
detailForm: {},
detailRules: {
lotNo: [{ required: true, message: '入场卷号不能为空', trigger: 'blur' }]
},
activeTab: 'plan',
actualLoading: false,
actualList: [],
diffLoading: false,
missingCoils: [],
extraCoils: [],
diffCoils: [],
diffConfig: {
weightThreshold: 0.5
},
importOpen: false,
file: null,
rawData: [],
tableData: [],
errorList: [],
isValidated: false,
progress: 0,
importedCount: 0,
totalCount: 0,
importStatus: 'idle',
importErrorMsg: '',
validateLoading: false,
importLoading: false
};
},
created() {
this.getList();
},
methods: {
getList() {
this.loading = true;
listDeliveryPlan(this.queryParams).then(response => {
this.deliveryPlanList = response.rows.map(item => ({
...item,
selected: false
}));
this.total = response.total;
this.loading = false;
this.ids = [];
this.single = true;
this.multiple = true;
});
},
cancel() {
this.open = false;
this.reset();
},
reset() {
this.form = {
planId: undefined,
planName: undefined,
planDate: undefined,
planType: 1,
remark: undefined,
delFlag: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined
};
this.resetForm("form");
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
handleCardSelectionChange(row) {
this.ids = this.deliveryPlanList.filter(item => item.selected).map(item => item.planId);
this.single = this.ids.length !== 1;
this.multiple = !this.ids.length;
},
handleAdd() {
this.form.planDate = new Date().toLocaleString().substring(0, 19).replace(/\//g, '-').replace('T', ' ');
this.form.planName = new Date().toLocaleDateString().replace(/\//g, '-') + '收货计划';
this.form.planType = 1;
this.open = true;
this.title = "添加收货计划";
},
handleUpdate(row) {
this.loading = true;
this.reset();
const planId = row.planId || this.ids[0];
getDeliveryPlan(planId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改收货计划";
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.planId != null) {
updateDeliveryPlan(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addDeliveryPlan(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
handleDelete(row) {
const planIds = row.planId || this.ids;
this.$modal.confirm('是否确认删除收货计划编号为"' + planIds + '"的数据项?').then(() => {
this.loading = true;
return delDeliveryPlan(planIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
handleExport() {
this.download('wms/deliveryPlan/export', {
...this.queryParams
}, `deliveryPlan_${new Date().getTime()}.xlsx`);
},
openDetailDialog(row) {
this.currentPlanId = row.planId;
this.detailQueryParams.planId = row.planId;
this.detailOpen = true;
this.getDetailList();
},
resetDetail() {
this.detailList = [];
this.currentPlanId = null;
this.activeTab = 'plan';
this.actualList = [];
this.missingCoils = [];
this.extraCoils = [];
this.diffCoils = [];
},
handleTabClick(tab) {
if (tab.name === 'actual' && this.actualList.length === 0) {
this.getActualList();
} else if (tab.name === 'diff' && (this.missingCoils.length === 0 && this.extraCoils.length === 0 && this.diffCoils.length === 0)) {
this.calculateDiff();
}
},
getActualList() {
this.actualLoading = true;
listPendingAction({
warehouseId: this.currentPlanId,
actionType: 401,
pageSize: 99999,
pageNum: 1,
}).then(response => {
const coilIds = response.rows.map(item => item.coilId).join(',');
listCoilWithIds({ coilIds }).then(actualRes => {
this.actualList = actualRes.rows;
this.actualLoading = false;
});
}).catch(() => {
this.actualLoading = false;
});
},
calculateDiff() {
this.diffLoading = true;
Promise.all([
listReceivePlan({ pageNum: 1, pageSize: 99999, planId: this.currentPlanId }),
listPendingAction({
warehouseId: this.currentPlanId,
actionType: 401,
pageSize: 99999,
pageNum: 1,
}).then(response => {
const coilIds = response.rows.map(item => item.coilId).join(',');
return listCoilWithIds({ coilIds }).then(actualRes => {
this.actualLoading = false;
return actualRes;
});
})
]).then(([planRes, actualRes]) => {
const planList = planRes.rows;
const actualList = actualRes.rows;
const planLotNoMap = new Map();
planList.forEach(item => {
planLotNoMap.set(item.lotNo, item);
});
const actualLotNoMap = new Map();
actualList.forEach(item => {
actualLotNoMap.set(item.enterCoilNo, item);
});
this.missingCoils = planList.filter(item => !actualLotNoMap.has(item.lotNo));
this.extraCoils = actualList.filter(item => !planLotNoMap.has(item.enterCoilNo));
this.diffCoils = [];
const compareFields = [
{ planField: 'goodsName', actualField: 'itemName', fieldName: '名称' },
{ planField: 'spec', actualField: 'specification', fieldName: '规格' },
{ planField: 'materialType', actualField: 'material', fieldName: '材质' },
{ planField: 'weight', actualField: 'netWeight', fieldName: '重量', isNumber: true },
{ planField: 'manufacturer', actualField: 'manufacturer', fieldName: '生产厂家' },
{ planField: 'zincCoating', actualField: 'zincLayer', fieldName: '锌层' }
];
const diffMap = new Map();
planList.forEach(planItem => {
const actualItem = actualLotNoMap.get(planItem.lotNo);
if (actualItem) {
const diffs = [];
compareFields.forEach(field => {
let planValue = String(planItem[field.planField] || '');
let actualValue = String(actualItem[field.actualField] || '');
if (field.isNumber) {
planValue = Number(planValue);
actualValue = Number(actualValue);
}
let isDiff = false;
if (field.isNumber) {
isDiff = Math.abs(planValue - actualValue) > this.diffConfig.weightThreshold;
} else {
isDiff = planValue !== actualValue;
}
if (isDiff) {
diffs.push({
fieldName: field.fieldName,
planValue,
actualValue
});
}
});
if (diffs.length > 0) {
diffMap.set(planItem.lotNo, {
lotNo: planItem.lotNo,
diffs
});
}
}
});
this.diffCoils = Array.from(diffMap.values());
this.diffLoading = false;
}).catch(() => {
this.diffLoading = false;
});
},
getDetailList() {
this.detailLoading = true;
this.detailIds = [];
this.detailSingle = true;
this.detailMultiple = true;
listReceivePlan(this.detailQueryParams).then(response => {
this.detailList = response.rows;
this.detailTotal = response.total;
this.detailLoading = false;
});
},
openDetailAddDialog() {
this.detailForm = {
planId: this.currentPlanId,
receiveStatus: '0'
};
this.detailFormTitle = '新增待收货明细';
this.detailFormOpen = true;
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate();
});
},
openDetailEditDialog(row) {
this.detailForm = { ...row };
this.detailFormTitle = '修改待收货明细';
this.detailFormOpen = true;
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate();
});
},
resetDetailForm() {
this.detailForm = {};
},
submitDetailForm() {
this.$refs.detailForm.validate(valid => {
if (valid) {
this.detailButtonLoading = true;
if (this.detailForm.receiveId != null) {
updateReceivePlan(this.detailForm).then(response => {
this.$modal.msgSuccess("修改成功");
this.detailFormOpen = false;
this.getDetailList();
}).finally(() => {
this.detailButtonLoading = false;
});
} else {
addReceivePlan(this.detailForm).then(response => {
this.$modal.msgSuccess("新增成功");
this.detailFormOpen = false;
this.getDetailList();
}).finally(() => {
this.detailButtonLoading = false;
});
}
}
});
},
handleDetailDelete(row) {
this.$modal.confirm('是否确认删除该待收货明细?').then(() => {
return delReceivePlan(row.receiveId);
}).then(() => {
this.getDetailList();
this.$modal.msgSuccess("删除成功");
}).catch(() => { });
},
handleDetailSelectionChange(selection) {
this.detailIds = selection.map(item => item.receiveId);
this.detailSingle = this.detailIds.length !== 1;
this.detailMultiple = !this.detailIds.length;
},
handleDetailBatchDelete() {
const receiveIds = this.detailIds.join(',');
this.$modal.confirm('是否确认删除选中的' + this.detailIds.length + '条待收货明细?').then(() => {
this.detailLoading = true;
return delReceivePlan(receiveIds);
}).then(() => {
this.getDetailList();
this.$modal.msgSuccess("删除成功");
}).catch(() => { }).finally(() => {
this.detailLoading = false;
});
},
handleDetailClear() {
this.$modal.confirm('是否确认清空当前收货计划下的所有待收货明细?此操作不可恢复!').then(() => {
this.detailLoading = true;
return listReceivePlan({ pageNum: 1, pageSize: 99999, planId: this.currentPlanId });
}).then(response => {
const allIds = response.rows.map(item => item.receiveId);
if (allIds.length === 0) {
this.$modal.msgInfo('暂无数据可清空');
return;
}
return delReceivePlanBatch(allIds);
}).then(() => {
this.getDetailList();
this.$modal.msgSuccess("清空成功");
}).catch(() => { }).finally(() => {
this.detailLoading = false;
});
},
openImportDialog(row) {
this.currentPlanId = row.planId;
this.importOpen = true;
},
handleFileChange(file) {
if (this.validateLoading || this.importLoading || this.importStatus === 'processing') {
this.$message.warning('当前有操作正在进行中,请完成后再选择新文件');
return;
}
this.file = file.raw;
this.isValidated = false;
this.readExcel();
},
async readExcel() {
if (!this.file) return;
try {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(this.file);
fileReader.onload = async (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
this.validateHeaders(jsonData[0]);
this.rawData = jsonData.slice(1).filter(row => row.length > 0);
this.formatExcel();
this.$message.success(`成功解析Excel共读取到 ${this.rawData.length} 条数据`);
} catch (error) {
this.handleError(`解析Excel失败${error.message}`);
}
};
} catch (error) {
this.handleError(`读取文件失败:${error.message}`);
}
},
validateHeaders(headers) {
this.errorList = [];
if (!headers || headers.length < TEMPLATE_HEADERS.length) {
this.errorList.push({
rowNum: 1,
errorMsg: `表头数量不匹配,要求至少${TEMPLATE_HEADERS.length}`
});
return;
}
TEMPLATE_HEADERS.forEach((header, index) => {
if (headers[index] !== header) {
this.errorList.push({
rowNum: 1,
errorMsg: `${index + 1}列表头错误,要求:"${header}",实际:"${headers[index] || ''}"`
});
}
});
if (this.errorList.length > 0) {
this.$message.error('Excel表头格式不符合要求请检查');
}
},
async handleValidate() {
if (this.validateLoading) {
this.$message.warning('正在校验数据,请稍候...');
return;
}
if (!this.file) {
this.$message.warning('请先选择Excel文件');
return;
}
this.isValidated = true;
if (this.rawData.length === 0) {
this.$message.warning('暂无数据可校验');
return;
}
this.validateLoading = true;
this.errorList = [];
try {
for (let i = 0; i < this.rawData.length; i++) {
const row = this.rawData[i];
const rowNum = i + 2;
const rowObj = this.formatRowData(row, rowNum);
if (!rowObj.lotNo || rowObj.lotNo.trim() === '') {
this.errorList.push({ rowNum, errorMsg: '入场卷号不能为空' });
}
if (rowObj.weight && isNaN(Number(rowObj.weight))) {
this.errorList.push({ rowNum, errorMsg: '重量必须是数字' });
}
if (rowObj.length && isNaN(Number(rowObj.length))) {
this.errorList.push({ rowNum, errorMsg: '长度必须是数字' });
}
}
if (this.errorList.length > 0) {
this.$message.error(`数据校验失败,共发现${this.errorList.length}条错误`);
} else {
this.$message.success('数据校验通过,可以开始导入');
}
} catch (error) {
this.handleError(`校验数据时发生错误:${error.message}`);
} finally {
this.validateLoading = false;
}
},
formatExcel() {
this.tableData = [];
if (this.rawData.length === 0) return;
this.rawData.forEach((row, index) => {
const rowNum = index + 2;
const rowObj = this.formatRowData(row, rowNum);
this.tableData.push(rowObj);
});
},
formatRowData(row, rowNum) {
const rowObj = {};
TEMPLATE_HEADERS.forEach((header, index) => {
const field = HEADER_MAP[header];
rowObj[field] = row[index] != null ? row[index].toString().trim() : '';
});
rowObj.rowNum = rowNum;
return rowObj;
},
async startImport() {
if (this.importLoading) {
this.$message.warning('正在导入数据,请勿重复操作...');
return;
}
if (!this.file || this.tableData.length === 0) {
this.$message.warning('暂无数据可导入');
return;
}
const confirm = await this.$confirm(
'确认导入已校验通过的数据?导入过程中请勿刷新页面或关闭浏览器!',
'导入确认',
{
confirmButtonText: '确认导入',
cancelButtonText: '取消',
type: 'warning'
}
).catch(() => false);
if (!confirm) return;
this.importLoading = true;
this.importStatus = 'processing';
this.progress = 0;
this.importedCount = 0;
this.totalCount = this.tableData.length;
this.importErrorMsg = '';
try {
await this.batchImport();
this.importStatus = 'finished';
this.$message.success(`导入完成!共成功导入${this.importedCount}条数据`);
} catch (error) {
this.handleError(`导入失败:${error.message},已导入${this.importedCount}条数据`);
} finally {
this.importLoading = false;
}
},
async batchImport() {
for (let i = 0; i < this.tableData.length; i++) {
if (this.importStatus === 'error') break;
const row = this.tableData[i];
try {
await this.importOneRow(row);
this.importedCount++;
const currentProgress = Math.round(((i + 1) / this.totalCount) * 100);
this.progress = currentProgress;
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) {
throw new Error(`${row.rowNum}行导入失败:${error.message}`);
}
}
},
async importOneRow(row) {
const params = {
planId: this.currentPlanId,
warehouseArea: row.warehouseArea,
lotNo: row.lotNo,
supplierLotNo: row.supplierLotNo,
productLotNo: row.productLotNo,
productionDate: row.productionDate,
weight: row.weight ? Number(row.weight) : null,
receiveStatus: '0',
goodsName: row.goodsName,
spec: row.spec,
length: row.length ? Number(row.length) : null,
materialType: row.materialType,
manufacturer: row.manufacturer,
surfaceTreatment: row.surfaceTreatment,
zincCoating: row.zincCoating,
teamGroup: row.teamGroup,
temperRolling: row.temperRolling,
coatingType: row.coatingType,
goodsType: row.goodsType,
remark: row.remark
};
const res = await addReceivePlan(params);
if (res.code !== 200) {
throw new Error(res.msg || '接口返回异常');
}
},
resetImport() {
if (this.validateLoading || this.importLoading || this.importStatus === 'processing') {
this.$message.warning('当前有操作正在进行中,无法重置');
return;
}
if (this.file || this.tableData.length > 0 || this.errorList.length > 0) {
const confirm = this.$confirm(
'确认重置所有状态?已选择的文件、解析的数据和校验结果将被清空!',
'重置确认',
{
confirmButtonText: '确认重置',
cancelButtonText: '取消',
type: 'info'
}
).catch(() => false);
if (!confirm) return;
}
this.file = null;
this.rawData = [];
this.tableData = [];
this.errorList = [];
this.isValidated = false;
this.progress = 0;
this.importedCount = 0;
this.totalCount = 0;
this.importStatus = 'idle';
this.importErrorMsg = '';
this.$refs.upload?.clearFiles();
this.$message.success('已重置所有状态');
},
downloadTemplate() {
const templateData = [
TEMPLATE_HEADERS,
['A库区', 'LOT001', 'SUP001', 'PROD001', '2024-01-01', 1000.50, '钢板', '1.0*1000', 2000, 'Q235', '宝钢', '镀锌', '80', '甲班', '1.5', 'GI', '原料', '示例备注']
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(templateData);
ws['!cols'] = [
{ wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 15 }, { wch: 12 },
{ wch: 15 }, { wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 15 }, { wch: 12 },
{ wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 10 }, { wch: 15 }
];
XLSX.utils.book_append_sheet(wb, ws, '导入模板');
XLSX.writeFile(wb, '待收货明细导入模板.xlsx');
},
handleError(message) {
this.$message.error(message);
this.importStatus = 'error';
this.importErrorMsg = message;
this.validateLoading = false;
this.importLoading = false;
}
}
};
</script>
<style scoped>
.delivery-plan-card {
margin-bottom: 20px;
height: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.card-content {
margin-bottom: 20px;
}
.content-item {
margin-bottom: 10px;
display: flex;
align-items: flex-start;
}
.content-item:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
font-weight: 500;
color: #606266;
margin-right: 10px;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn-danger {
color: #f56c6c;
}
.import-container {
padding: 20px;
}
.file-upload-area {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.file-upload-area.disabled {
opacity: 0.6;
pointer-events: none;
}
.template-download {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.template-tip {
font-size: 12px;
color: #909399;
}
.error-list {
margin-bottom: 20px;
}
.data-preview {
margin-bottom: 20px;
}
.import-progress {
margin-bottom: 20px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.progress-tip {
margin: 10px 0 0 0;
color: #666;
font-size: 14px;
}
.import-finished,
.import-error {
margin-bottom: 20px;
}
.diff-config {
background: #fff;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
border: 1px solid #e4e7ed;
display: flex;
align-items: center;
gap: 16px;
}
.config-tip {
font-size: 12px;
color: #909399;
}
.diff-section {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e4e7ed;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.section-icon {
font-size: 10px;
margin-right: 8px;
}
.missing-icon {
color: #f56c6c;
}
.extra-icon {
color: #67c23a;
}
.diff-icon {
color: #e6a23c;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.section-count {
margin-left: auto;
background: #e4e7ed;
color: #606266;
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.diff-table {
background: #fff;
}
.diff-content {
padding: 8px 0;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.diff-item {
padding: 0;
display: flex;
gap: 20px;
background: #f8f9fa;
border-radius: 4px;
margin-bottom: 8px;
border-left: 3px solid #e6a23c;
}
.value-label {
color: #909399;
margin-right: 8px;
width: 36px;
}
.plan-value {
color: #606266;
font-family: monospace;
}
.actual-value {
color: #f56c6c;
font-family: monospace;
}
.empty-tip {
text-align: center;
padding: 20px;
color: #909399;
font-size: 14px;
}
</style>