Files
klp-oa/klp-ui/src/views/wms/receive/plan/index.vue

1310 lines
48 KiB
Vue
Raw Normal View History

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