feat(wms): 优化收货功能并重构导入组件

重构收货功能界面,将"确认收货"改为"签收",并调整按钮样式。新增收货弹窗功能,支持选择实际库区和填写备注。

重构导入组件,优化校验逻辑和错误处理,增加防呆机制和loading状态。将枚举和常量提取为模块级变量,提升代码可维护性。添加仓库映射关系,增强数据校验能力。

调整表格按钮的内边距,提升UI一致性
This commit is contained in:
砂糖
2025-12-02 13:56:21 +08:00
parent 40383a73b9
commit 51caea9e41
4 changed files with 538 additions and 292 deletions

View File

@@ -104,7 +104,7 @@
} }
.el-table__cell .el-button { .el-table__cell .el-button {
padding: 0 !important; padding: 2 !important;
} }
.el-table__cell + .el-button { .el-table__cell + .el-button {

View File

@@ -179,12 +179,11 @@
<el-tag v-else type="primary">未收货</el-tag> <el-tag v-else type="primary">未收货</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作"> <!-- <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button v-if="scope.row.actionStatus == 0 || scope.row.actionStatus == 1" type="primary" <el-button v-if="scope.row.actionStatus == 0 || scope.row.actionStatus == 1" type="primary" @click="openReceiptModal(scope.row)">签收</el-button>
size="mini" @click="openReceiptModal(scope.row)">确认收货</el-button>
</template> </template>
</el-table-column> </el-table-column> -->
</el-table> </el-table>
<Pagination v-show="total > 0" :total="total" :page.sync="pagination.pageNum" <Pagination v-show="total > 0" :total="total" :page.sync="pagination.pageNum"
@@ -213,7 +212,7 @@
</el-form> </el-form>
<template slot="footer" class="dialog-footer"> <template slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmReceipt" v-loading="buttonLoading">确认收货</el-button> <el-button type="primary" @click="confirmReceipt" v-loading="buttonLoading">签收</el-button>
<el-button @click="receiptModalVisible = false">取消</el-button> <el-button @click="receiptModalVisible = false">取消</el-button>
</template> </template>
</el-dialog> </el-dialog>

View File

@@ -1,52 +1,46 @@
<template> <template>
<div class="import-wizard-container"> <div class="import-wizard-container">
<!-- 导入模式选择 -->
<div class="import-mode-selector">
<el-radio-group v-model="importMode" @change="reset">
<el-radio label="validate">校验导入推荐</el-radio>
<el-radio label="direct">直接导入不校验</el-radio>
</el-radio-group>
</div>
<!-- 文件上传区域 --> <!-- 文件上传区域 -->
<div class="file-upload-area" :class="{ disabled: importStatus === 'processing' }"> <div class="file-upload-area" :class="{ disabled: importStatus === ImportStatus.PROCESSING }">
<el-upload <el-upload
ref="upload" ref="upload"
class="upload-excel" class="upload-excel"
action="" action=""
:auto-upload="false" :auto-upload="false"
:show-file-list="false" :show-file-list="false"
:on-change="handleFileChange" :on-change="handleFileChange"
accept=".xlsx,.xls" accept=".xlsx,.xls"
:disabled="importStatus === 'processing'" :disabled="importStatus === ImportStatus.PROCESSING || validateLoading || importLoading"
> >
<el-button type="primary" icon="el-icon-upload2">选择Excel文件</el-button> <el-button type="primary" icon="el-icon-upload2">选择Excel文件</el-button>
</el-upload> </el-upload>
<!-- 操作按钮 --> <!-- 操作按钮新增loading和防呆 -->
<el-button <el-button
type="success" type="success"
icon="el-icon-check" icon="el-icon-check"
@click="handleValidate" @click="handleValidate"
v-if="file && importMode === 'validate' && importStatus === 'idle'" v-if="file && importStatus === ImportStatus.IDLE"
:disabled="!file" :disabled="!file || validateLoading"
:loading="validateLoading"
> >
校验数据 校验数据
</el-button> </el-button>
<el-button <el-button
type="warning" type="warning"
icon="el-icon-circle-check" icon="el-icon-circle-check"
@click="startImport" @click="startImport"
v-if="file && (importMode === 'direct' || (importMode === 'validate' && errorList.length === 0)) && importStatus === 'idle'" v-if="file && importStatus === ImportStatus.IDLE"
:disabled="!file || (importMode === 'validate' && errorList.length > 0)" :disabled="!file || !isValidated || errorList.length > 0 || importLoading"
:loading="importLoading"
> >
开始导入 开始导入
</el-button> </el-button>
<el-button <el-button
type="default" type="default"
icon="el-icon-refresh" icon="el-icon-refresh"
@click="reset" @click="reset"
:disabled="importStatus === 'processing'" :disabled="importStatus === ImportStatus.PROCESSING || validateLoading || importLoading"
> >
重置 重置
</el-button> </el-button>
@@ -54,12 +48,7 @@
<!-- 校验错误提示 --> <!-- 校验错误提示 -->
<div v-if="errorList.length > 0" class="error-list"> <div v-if="errorList.length > 0" class="error-list">
<el-alert <el-alert title="数据校验失败" type="error" description="以下行数据不符合格式要求,请修正后重新导入:" show-icon />
title="数据校验失败"
type="error"
description="以下行数据不符合格式要求,请修正后重新导入:"
show-icon
/>
<el-table :data="errorList" border size="small" max-height="200"> <el-table :data="errorList" border size="small" max-height="200">
<el-table-column prop="rowNum" label="行号" width="80" /> <el-table-column prop="rowNum" label="行号" width="80" />
<el-table-column prop="errorMsg" label="错误信息" /> <el-table-column prop="errorMsg" label="错误信息" />
@@ -67,61 +56,52 @@
</div> </div>
<!-- 数据预览 --> <!-- 数据预览 -->
<div v-if="tableData.length > 0 && importStatus === 'idle'" class="data-preview"> <div v-if="tableData.length > 0 && importStatus === ImportStatus.IDLE" class="data-preview">
<el-alert <el-alert
title="数据预览" title="数据预览"
type="info" type="info"
:description="`共解析出 ${tableData.length} 条有效数据`" :description="`共解析出 ${tableData.length} 条有效数据`"
show-icon show-icon
:closable="false" :closable="false"
/> />
<el-table :data="tableData" border size="small" max-height="300" stripe> <el-table :data="tableData" border size="small" max-height="300" stripe>
<el-table-column prop="type" label="类型" width="80" /> <el-table-column
<el-table-column prop="logicWarehouse" label="逻辑库区" width="120" /> v-for="column in TableColumnEnum"
<el-table-column prop="inboundCoilNo" label="入场卷号" width="150" /> :key="column.prop"
<el-table-column prop="factoryCoilNo" label="厂家卷号" width="150" /> :prop="column.prop"
<el-table-column prop="weight" label="重量(吨)" width="120" /> :label="column.label"
<el-table-column prop="remark" label="备注" min-width="100" /> :width="column.width"
<el-table-column prop="name" label="名称" min-width="100" /> :min-width="column.minWidth"
<el-table-column prop="specification" label="规格" min-width="100" /> />
<el-table-column prop="material" label="材质" width="100" />
<el-table-column prop="manufacturer" label="厂家" min-width="100" />
<el-table-column prop="surfaceTreatmentDesc" label="表面处理" width="120" />
<el-table-column prop="zincLayer" label="锌层" width="100" />
</el-table> </el-table>
</div> </div>
<!-- 导入进度展示 --> <!-- 导入进度展示 -->
<div v-if="importStatus === 'processing'" class="import-progress"> <div v-if="importStatus === ImportStatus.PROCESSING" class="import-progress">
<el-alert <el-alert
title="正在导入数据" title="正在导入数据"
type="warning" type="warning"
:description="`当前进度:${progress}%`" :description="`当前进度:${progress}%`"
show-icon show-icon
:closable="false" :closable="false"
/> />
<el-progress :percentage="progress" status="success" /> <el-progress :percentage="progress" status="success" />
<p class="progress-tip">已导入 {{ importedCount }} / {{ totalCount }} 条数据</p> <p class="progress-tip">已导入 {{ importedCount }} / {{ totalCount }} 条数据</p>
</div> </div>
<!-- 导入完成提示 --> <!-- 导入完成提示 -->
<div v-if="importStatus === 'finished'" class="import-finished"> <div v-if="importStatus === ImportStatus.FINISHED" class="import-finished">
<el-alert <el-alert
title="导入完成" title="导入完成"
type="success" type="success"
:description="`共成功导入 ${importedCount} 条数据,总计 ${totalCount} 条`" :description="`共成功导入 ${importedCount} 条数据,总计 ${totalCount} 条`"
show-icon show-icon
/> />
</div> </div>
<!-- 导入失败提示 --> <!-- 导入失败提示 -->
<div v-if="importStatus === 'error'" class="import-error"> <div v-if="importStatus === ImportStatus.ERROR" class="import-error">
<el-alert <el-alert title="导入失败" type="error" :description="importErrorMsg" show-icon />
title="导入失败"
type="error"
:description="importErrorMsg"
show-icon
/>
</div> </div>
</div> </div>
</template> </template>
@@ -132,63 +112,219 @@ import { addMaterialCoil } from '@/api/wms/coil';
import { addPendingAction } from '@/api/wms/pendingAction'; import { addPendingAction } from '@/api/wms/pendingAction';
import { listRawMaterial } from '@/api/wms/rawMaterial'; import { listRawMaterial } from '@/api/wms/rawMaterial';
import { listProduct } from '@/api/wms/product'; import { listProduct } from '@/api/wms/product';
import { listWarehouse } from '@/api/wms/warehouse';
// ===================== 枚举定义 =====================
/**
* 导入状态枚举
*/
const ImportStatus = Object.freeze({
IDLE: 'idle', // 闲置
PROCESSING: 'processing', // 处理中
FINISHED: 'finished', // 完成
ERROR: 'error' // 失败
});
/**
* 错误类型枚举(含校验逻辑)
*/
const ErrorType = Object.freeze({
// 表头错误
HEADER_COUNT_ERROR: {
message: (required, actual) => `表头数量不匹配,要求${required}列,实际${actual}`
},
HEADER_NAME_ERROR: {
message: (index, required, actual) => `${index + 1}列表头错误,要求:"${required}",实际:"${actual}"`
},
// 字段校验错误
FIELD_REQUIRED: {
message: (label) => `${label}不能为空`,
validator: (rowObj, rowNum, errorList) => {
const requiredFields = [
{ key: 'type', label: '类型' },
{ key: 'logicWarehouse', label: '逻辑库区' },
{ key: 'inboundCoilNo', label: '入场卷号' },
{ key: 'name', label: '名称' },
{ key: 'specification', label: '规格' },
{ key: 'weight', label: '重量(吨)' }
];
requiredFields.forEach(field => {
const value = rowObj[field.key];
if (!value || (typeof value === 'string' && value.trim() === '')) {
errorList.push({
rowNum,
errorMsg: ErrorType.FIELD_REQUIRED.message(field.label)
});
}
});
}
},
// 类型错误
TYPE_ERROR: {
message: '类型只能是"原料"或"成品"',
validator: (rowObj, rowNum, errorList) => {
if (rowObj.type && !['原料', '成品'].includes(rowObj.type.trim())) {
errorList.push({
rowNum,
errorMsg: ErrorType.TYPE_ERROR.message
});
}
}
},
// 重量错误
WEIGHT_ERROR: {
message: '重量必须是大于0的数字',
validator: (rowObj, rowNum, errorList) => {
if (rowObj.weight) {
const weight = Number(rowObj.weight);
if (isNaN(weight) || weight <= 0) {
errorList.push({
rowNum,
errorMsg: ErrorType.WEIGHT_ERROR.message
});
}
}
}
},
// 仓库名不规范或不存在
WAREHOUSE_ERROR: {
message: (warehouseName) => `仓库名"${warehouseName}"不规范或不存在`,
validator: (rowObj, rowNum, errorList, warehouseMap) => {
const warehouseId = warehouseMap[rowObj.logicWarehouse];
if (!warehouseId) {
errorList.push({
rowNum,
errorMsg: ErrorType.WAREHOUSE_ERROR.message(rowObj.logicWarehouse)
});
}
}
},
// 物料ID不存在
ITEM_ID_NOT_FOUND: {
message: (type, name, spec, material, surfaceTreatmentDesc, manufacturer, zincLayer) => `未找到唯一匹配的${type}类型(名称:${name},规格:${spec},材质:${material || '无'},表面处理:${surfaceTreatmentDesc || '无'},厂家:${manufacturer || '无'},锌层:${zincLayer || '无'}`
}
});
/**
* 表格列配置枚举
*/
const TableColumnEnum = Object.freeze([
{ prop: 'type', label: '类型', width: 80 },
{ prop: 'logicWarehouse', label: '逻辑库区', width: 120 },
{ prop: 'inboundCoilNo', label: '入场卷号', width: 150 },
{ prop: 'factoryCoilNo', label: '厂家卷号', width: 150 },
{ prop: 'weight', label: '重量(吨)', width: 120 },
{ prop: 'remark', label: '备注', minWidth: 100 },
{ prop: 'name', label: '名称', minWidth: 100 },
{ prop: 'specification', label: '规格', minWidth: 100 },
{ prop: 'material', label: '材质', width: 100 },
{ prop: 'manufacturer', label: '厂家', minWidth: 100 },
{ prop: 'surfaceTreatmentDesc', label: '表面处理', width: 120 },
{ prop: 'zincLayer', label: '锌层', width: 100 }
]);
/**
* 系统常量枚举
*/
const SystemConstant = Object.freeze({
// Excel表头配置
REQUIRED_HEADERS: TableColumnEnum.map(col => col.label),
HEADER_MAP: TableColumnEnum.reduce((map, col) => {
map[col.label] = col.prop;
return map;
}, {}),
// 接口常量
COIL_DATA_TYPE: 10, // 钢卷数据类型
PENDING_ACTION_STATUS: 0, // 待处理状态
OPERATION_TYPE_IMPORT: 'import', // 操作类型-导入
PENDING_ACTION_TYPE_RECEIVE: 401, // 待处理类型-入库
// 物料类型映射
ITEM_TYPE_MAP: {
'原料': 'raw_material',
'成品': 'product'
},
// 物料ID字段映射
ITEM_ID_FIELD: {
'raw_material': 'rawMaterialId',
'product': 'productId'
}
});
export default { export default {
name: 'MaterialCoilImportWizard', name: 'MaterialCoilImportWizard',
props: {
planId: {
type: String,
default: 0
}
},
data() { data() {
return { return {
// 导入模式validate-校验导入 direct-直接导入 // 枚举挂载到实例,方便模板使用
importMode: 'validate', ImportStatus,
TableColumnEnum,
// 文件对象 // 文件对象
file: null, file: null,
// 解析后的原始数据 // 解析后的原始数据
rawData: [], rawData: [],
// 格式化后用于展示/导入的数据 // 格式化后用于展示/导入的数据
tableData: [], tableData: [],
// 校验错误列表 // 校验错误列表(为空表示校验通过)
errorList: [], errorList: [],
// 是否点击过校验按钮(新增状态)
isValidated: false,
// 导入进度0-100 // 导入进度0-100
progress: 0, progress: 0,
// 已导入条数 // 已导入条数
importedCount: 0, importedCount: 0,
// 总数据条数 // 总数据条数
totalCount: 0, totalCount: 0,
// 导入状态idle-闲置 processing-处理中 finished-完成 error-失败 // 导入状态
importStatus: 'idle', importStatus: ImportStatus.IDLE,
// 导入错误信息 // 导入错误信息
importErrorMsg: '', importErrorMsg: '',
// 规定的表头(顺序和名称必须匹配) // 仓库名到仓库id的映射关系
requiredHeaders: [ warehouseMap: {},
'类型', '逻辑库区', '入场卷号', '厂家卷号', '重量(吨)', '备注', // 防呆loading状态
'名称', '规格', '材质', '厂家', '表面处理', '锌层' validateLoading: false, // 校验按钮loading
], importLoading: false // 导入按钮loading
// 表头映射(中文表头 -> 字段名)
headerMap: {
'类型': 'type',
'逻辑库区': 'logicWarehouse',
'入场卷号': 'inboundCoilNo',
'厂家卷号': 'factoryCoilNo',
'重量(吨)': 'weight',
'备注': 'remark',
'名称': 'name',
'规格': 'specification',
'材质': 'material',
'厂家': 'manufacturer',
'表面处理': 'surfaceTreatmentDesc',
'锌层': 'zincLayer'
}
}; };
}, },
methods: { methods: {
/** /**
* 处理文件选择 * 处理文件选择(新增防呆:操作中禁止切换文件)
*/ */
handleFileChange(file) { handleFileChange(file) {
// 操作中禁止切换文件
if (this.validateLoading || this.importLoading || this.importStatus === ImportStatus.PROCESSING) {
this.$message.warning('当前有操作正在进行中,请完成后再选择新文件');
return;
}
this.file = file.raw; this.file = file.raw;
// 选择文件后自动读取Excel this.isValidated = false; // 选择文件后重置校验状态
this.readExcel(); this.readExcel();
}, },
/**
* 获取所有的仓库建立仓库名到仓库id的映射关系
*/
async getWarehouseMap() {
try {
const response = await listWarehouse({ pageNum: 1, pageSize: 1000 });
const map = {};
for (let item of response.data) {
map[item.warehouseName] = item.warehouseId;
}
console.log(map, response);
this.warehouseMap = map;
} catch (error) {
this.handleError(`获取仓库列表失败:${error.message}`);
}
},
/** /**
* 读取Excel文件内容 * 读取Excel文件内容
*/ */
@@ -198,35 +334,29 @@ export default {
try { try {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.readAsArrayBuffer(this.file); fileReader.readAsArrayBuffer(this.file);
fileReader.onload = async (e) => { fileReader.onload = async (e) => {
try { try {
const data = new Uint8Array(e.target.result); const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' }); const workbook = XLSX.read(data, { type: 'array' });
// 取第一个sheet
const sheetName = workbook.SheetNames[0]; const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName]; const worksheet = workbook.Sheets[sheetName];
// 解析为JSON跳过表头从第二行开始
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// 校验表头 // 校验表头
this.validateHeaders(jsonData[0]); this.validateHeaders(jsonData[0]);
// 解析数据行(从第二行开始 // 解析数据行(过滤空行
this.rawData = jsonData.slice(1).filter(row => row.length > 0); // 过滤空行 this.rawData = jsonData.slice(1).filter(row => row.length > 0);
// 格式化数据 // 格式化数据
this.formatExcel(); this.formatExcel();
this.$message.success(`成功解析Excel共读取到 ${this.rawData.length} 条数据`); this.$message.success(`成功解析Excel共读取到 ${this.rawData.length} 条数据`);
} catch (error) { } catch (error) {
this.$message.error(`解析Excel失败${error.message}`); this.handleError(`解析Excel失败${error.message}`);
this.importStatus = 'error';
this.importErrorMsg = `解析Excel失败${error.message}`;
} }
}; };
} catch (error) { } catch (error) {
this.$message.error(`读取文件失败:${error.message}`); this.handleError(`读取文件失败:${error.message}`);
this.importStatus = 'error';
this.importErrorMsg = `读取文件失败:${error.message}`;
} }
}, },
@@ -235,21 +365,22 @@ export default {
*/ */
validateHeaders(headers) { validateHeaders(headers) {
this.errorList = []; this.errorList = [];
// 校验表头数量和名称 const { REQUIRED_HEADERS } = SystemConstant;
if (headers.length !== this.requiredHeaders.length) {
// 校验表头数量
if (headers.length !== REQUIRED_HEADERS.length) {
this.errorList.push({ this.errorList.push({
rowNum: 1, rowNum: 1,
errorMsg: `表头数量不匹配,要求${this.requiredHeaders.length}列,实际${headers.length}` errorMsg: ErrorType.HEADER_COUNT_ERROR.message(REQUIRED_HEADERS.length, headers.length)
}); });
return;
} }
// 校验每列表头名称 // 校验表头名称
headers.forEach((header, index) => { headers.forEach((header, index) => {
if (header !== this.requiredHeaders[index]) { if (header !== REQUIRED_HEADERS[index]) {
this.errorList.push({ this.errorList.push({
rowNum: 1, rowNum: 1,
errorMsg: `${index + 1}列表头错误,要求:"${this.requiredHeaders[index]}",实际:"${header}"` errorMsg: ErrorType.HEADER_NAME_ERROR.message(index, REQUIRED_HEADERS[index], header)
}); });
} }
}); });
@@ -260,81 +391,64 @@ export default {
}, },
/** /**
* 校验Excel数据格式 * 校验Excel数据格式(唯一校验入口,添加防重复点击)
*/ */
async validateExcel() { 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) { if (this.rawData.length === 0) {
this.$message.warning('暂无数据可校验'); this.$message.warning('暂无数据可校验');
return; return;
} }
this.validateLoading = true; // 开启校验loading
this.errorList = []; this.errorList = [];
// 逐行校验 try {
for (let i = 0; i < this.rawData.length; i++) { // 逐行校验
const row = this.rawData[i]; for (let i = 0; i < this.rawData.length; i++) {
const rowNum = i + 2; // 数据行从第二行开始,行号=索引+2 const row = this.rawData[i];
const rowObj = this.formatRowData(row, rowNum); const rowNum = i + 2; // 数据行从第二行开始
const rowObj = this.formatRowData(row, rowNum);
// 1. 校验必填项 // 执行各类校验器
const requiredFields = [ ErrorType.FIELD_REQUIRED.validator(rowObj, rowNum, this.errorList);
{ key: 'type', label: '类型', value: rowObj.type }, ErrorType.TYPE_ERROR.validator(rowObj, rowNum, this.errorList);
{ key: 'logicWarehouse', label: '逻辑库区', value: rowObj.logicWarehouse }, ErrorType.WEIGHT_ERROR.validator(rowObj, rowNum, this.errorList);
{ key: 'inboundCoilNo', label: '入场卷号', value: rowObj.inboundCoilNo }, ErrorType.WAREHOUSE_ERROR.validator(rowObj, rowNum, this.errorList, this.warehouseMap);
{ key: 'name', label: '名称', value: rowObj.name },
{ key: 'specification', label: '规格', value: rowObj.specification },
];
// 检查必填项为空 // 校验物料ID是否存在仅当当前行无基础错误时
for (const field of requiredFields) { if (!this.errorList.some(err => err.rowNum === rowNum) && rowObj.type) {
if (!field.value || field.value.toString().trim() === '') { const itemId = await this._findItemId(rowObj);
this.errorList.push({ if (!itemId) {
rowNum, this.errorList.push({
errorMsg: `${field.label}不能为空` rowNum,
}); errorMsg: ErrorType.ITEM_ID_NOT_FOUND.message(rowObj.type, rowObj.name, rowObj.specification, rowObj.material, rowObj.surfaceTreatmentDesc, rowObj.manufacturer, rowObj.zincLayer)
});
}
} }
} }
// 2. 校验类型只能是“原料”或“成品” if (this.errorList.length > 0) {
if (rowObj.type && !['原料', '成品'].includes(rowObj.type.trim())) { this.$message.error(`数据校验失败,共发现${this.errorList.length}条错误`);
this.errorList.push({
rowNum,
errorMsg: '类型只能是"原料"或"成品"'
});
}
// 3. 校验重量是数字且大于0
if (rowObj.weight) {
const weight = Number(rowObj.weight);
if (isNaN(weight) || weight <= 0) {
this.errorList.push({
rowNum,
errorMsg: '重量必须是大于0的数字'
});
}
} else { } else {
this.errorList.push({ this.$message.success('数据校验通过,可以开始导入');
rowNum,
errorMsg: '重量不能为空'
});
} }
} catch (error) {
// 4. 预校验itemId是否存在仅校验模式 this.handleError(`校验数据时发生错误:${error.message}`);
if (rowObj.type && ['原料', '成品'].includes(rowObj.type.trim()) && !this.errorList.some(err => err.rowNum === rowNum)) { } finally {
const itemId = await this._findItemId(rowObj); this.validateLoading = false; // 关闭校验loading
if (!itemId) {
this.errorList.push({
rowNum,
errorMsg: `未找到唯一匹配的${rowObj.type}(名称:${rowObj.name},规格:${rowObj.specification}`
});
}
}
}
if (this.errorList.length > 0) {
this.$message.error(`数据校验失败,共发现${this.errorList.length}条错误`);
} else {
this.$message.success('数据校验通过,可以开始导入');
} }
}, },
@@ -345,7 +459,6 @@ export default {
this.tableData = []; this.tableData = [];
if (this.rawData.length === 0) return; if (this.rawData.length === 0) return;
// 逐行格式化
this.rawData.forEach((row, index) => { this.rawData.forEach((row, index) => {
const rowNum = index + 2; const rowNum = index + 2;
const rowObj = this.formatRowData(row, rowNum); const rowObj = this.formatRowData(row, rowNum);
@@ -357,51 +470,54 @@ export default {
* 格式化单行数据 * 格式化单行数据
*/ */
formatRowData(row, rowNum) { formatRowData(row, rowNum) {
const { HEADER_MAP, REQUIRED_HEADERS } = SystemConstant;
const rowObj = {}; const rowObj = {};
// 映射表头和字段 // 映射表头和字段
this.requiredHeaders.forEach((header, index) => { REQUIRED_HEADERS.forEach((header, index) => {
const field = this.headerMap[header]; const field = HEADER_MAP[header];
// 处理空值,统一转为字符串
rowObj[field] = row[index] ? row[index].toString().trim() : ''; rowObj[field] = row[index] ? row[index].toString().trim() : '';
}); });
// 重量转数字 // 重量转数字
rowObj.weight = rowObj.weight ? Number(rowObj.weight) : 0; rowObj.weight = rowObj.weight ? Number(rowObj.weight) : 0;
// 增加行号 // 增加行号
rowObj.rowNum = rowNum; rowObj.rowNum = rowNum;
return rowObj; return rowObj;
}, },
/** /**
* 处理校验按钮点击 * 开始导入数据(添加防重复点击和二次确认)
*/
async handleValidate() {
if (!this.file) {
this.$message.warning('请先选择Excel文件');
return;
}
await this.validateExcel();
},
/**
* 开始导入数据
*/ */
async startImport() { async startImport() {
// 防重复点击导入
if (this.importLoading) {
this.$message.warning('正在导入数据,请勿重复操作...');
return;
}
if (!this.file || this.tableData.length === 0) { if (!this.file || this.tableData.length === 0) {
this.$message.warning('暂无数据可导入'); this.$message.warning('暂无数据可导入');
return; return;
} }
// 直接导入模式下先快速校验基础格式 // 二次确认,防止误操作
if (this.importMode === 'direct') { const confirm = await this.$confirm(
await this.validateExcel(); '确认导入已校验通过的数据?导入过程中请勿刷新页面或关闭浏览器!',
if (this.errorList.length > 0) { '导入确认',
this.$message.error('直接导入模式下基础格式校验失败,请修正'); {
return; confirmButtonText: '确认导入',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
} }
} ).catch(() => false);
if (!confirm) return;
this.importLoading = true; // 开启导入loading
// 初始化导入状态 // 初始化导入状态
this.importStatus = 'processing'; this.importStatus = ImportStatus.PROCESSING;
this.progress = 0; this.progress = 0;
this.importedCount = 0; this.importedCount = 0;
this.totalCount = this.tableData.length; this.totalCount = this.tableData.length;
@@ -409,12 +525,12 @@ export default {
try { try {
await this.batchImport(); await this.batchImport();
this.importStatus = 'finished'; this.importStatus = ImportStatus.FINISHED;
this.$message.success(`导入完成!共成功导入${this.importedCount}条数据`); this.$message.success(`导入完成!共成功导入${this.importedCount}条数据`);
} catch (error) { } catch (error) {
this.importStatus = 'error'; this.handleError(`导入失败:${error.message},已导入${this.importedCount}条数据`);
this.importErrorMsg = `导入失败:${error.message},已导入${this.importedCount}条数据`; } finally {
this.$message.error(this.importErrorMsg); this.importLoading = false; // 关闭导入loading
} }
}, },
@@ -422,18 +538,20 @@ export default {
* 批量导入数据 * 批量导入数据
*/ */
async batchImport() { async batchImport() {
// 遍历所有数据行,逐个导入
for (let i = 0; i < this.tableData.length; i++) { for (let i = 0; i < this.tableData.length; i++) {
if (this.importStatus === 'error') break; // 发生错误则停止 // 若导入已失败,终止后续操作
if (this.importStatus === ImportStatus.ERROR) break;
const row = this.tableData[i]; const row = this.tableData[i];
try { try {
await this.importOneRecord(row); await this.importOneRecord(row);
this.importedCount++; this.importedCount++;
// 更新进度 // 更新进度(避免进度跳动过大)
this.progress = Math.round(((i + 1) / this.totalCount) * 100); const currentProgress = Math.round(((i + 1) / this.totalCount) * 100);
this.progress = currentProgress;
// 给浏览器渲染时间,避免卡死
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) { } catch (error) {
// 单条失败可选择继续或终止,这里选择终止
throw new Error(`${row.rowNum}行导入失败:${error.message}`); throw new Error(`${row.rowNum}行导入失败:${error.message}`);
} }
} }
@@ -444,42 +562,48 @@ export default {
*/ */
async importOneRecord(row) { async importOneRecord(row) {
try { try {
// 1. 查找itemId // 1. 查找物料ID再次校验防止数据篡改
const itemId = await this._findItemId(row); const itemId = await this._findItemId(row);
if (!itemId) { if (!itemId) {
throw new Error(`未找到唯一的${row.type}信息`); throw new Error(ErrorType.ITEM_ID_NOT_FOUND.message(row.type, row.name, row.specification, row.material, row.surfaceTreatmentDesc, row.manufacturer, row.zincLayer));
} }
const itemType = row.type === '原料' ? 'raw_material' : 'product'; const itemType = SystemConstant.ITEM_TYPE_MAP[row.type];
// 2. 插入钢卷数据dataType=10 // 2. 插入钢卷数据
const coilParams = { const coilParams = {
itemId, itemId,
itemType, itemType,
logicWarehouse: row.logicWarehouse, materialType: row.type,
inboundCoilNo: row.inboundCoilNo, warehouseId: this.warehouseMap[row.logicWarehouse],
factoryCoilNo: row.factoryCoilNo, enterCoilNo: row.inboundCoilNo,
weight: row.weight, currentCoilNo: row.inboundCoilNo,
supplierCoilNo: row.factoryCoilNo,
grossWeight: row.weight,
netWeight: row.weight,
remark: row.remark, remark: row.remark,
dataType: 10 // 钢卷数据类型固定为10 dataType: SystemConstant.COIL_DATA_TYPE
}; };
const coilRes = await addMaterialCoil(coilParams); const coilRes = await addMaterialCoil(coilParams);
if (!coilRes.success) { // 假设接口返回success标识 if (coilRes.code !== 200) {
throw new Error(`钢卷数据插入失败:${coilRes.message || '接口返回异常'}`); throw new Error(`钢卷数据插入失败:${coilRes.message || '接口返回异常'}`);
} }
const coilId = coilRes.data?.coilId; // 假设返回钢卷ID const coilId = coilRes.data?.coilId;
// 3. 插入待处理操作actionStatus=0 // 3. 插入待处理操作
const actionParams = { const actionParams = {
coilId, coilId,
actionStatus: 0, // 待处理状态固定为0 currentCoilNo: row.inboundCoilNo,
actionStatus: SystemConstant.PENDING_ACTION_STATUS,
itemType, itemType,
itemId, itemId,
operationType: 'import', // 操作类型:导入 actionType: SystemConstant.PENDING_ACTION_TYPE_RECEIVE,
remark: `Excel导入${row.inboundCoilNo}` operationType: SystemConstant.OPERATION_TYPE_IMPORT,
remark: `Excel导入${row.inboundCoilNo}`,
warehouseId: this.planId
}; };
const actionRes = await addPendingAction(actionParams); const actionRes = await addPendingAction(actionParams);
if (!actionRes.success) { if (actionRes.code !== 200) {
throw new Error(`待处理操作插入失败:${actionRes.message || '接口返回异常'}`); throw new Error(`待处理操作插入失败:${actionRes.message || '接口返回异常'}`);
} }
@@ -489,48 +613,46 @@ export default {
}, },
/** /**
* 根据条件查找唯一的itemId * 根据条件查找唯一的物料ID
*/ */
async _findItemId(row) { async _findItemId(row) {
const itemType = row.type === '原料' ? 'raw_material' : 'product'; const { ITEM_TYPE_MAP, ITEM_ID_FIELD } = SystemConstant;
let res = null; const itemType = ITEM_TYPE_MAP[row.type];
if (!itemType) return null;
try { try {
if (itemType === 'raw_material') { // 构建查询参数
res = await listRawMaterial({ const queryParams = {
rawMaterialName: row.name, [row.type === '原料' ? 'rawMaterialName' : 'productName']: row.name,
specification: row.specification, specification: row.specification,
material: row.material, material: row.material,
manufacturer: row.manufacturer, manufacturer: row.manufacturer,
surfaceTreatmentDesc: row.surfaceTreatmentDesc, surfaceTreatmentDesc: row.surfaceTreatmentDesc,
zincLayer: row.zincLayer zincLayer: row.zincLayer
}); };
} else {
res = await listProduct({
productName: row.name,
specification: row.specification,
material: row.material,
manufacturer: row.manufacturer,
surfaceTreatmentDesc: row.surfaceTreatmentDesc,
zincLayer: row.zincLayer
});
}
// 校验返回结果数量 // 执行查询
// 如果锌层,表面处理,厂家未传递,则需要从结果中取出这些值未空的记录 const res = itemType === 'raw_material'
const record = res.data.filter(item => ? await listRawMaterial(queryParams)
(!row.zincLayer || item.zincLayer === row.zincLayer) && : await listProduct(queryParams);
(!row.surfaceTreatmentDesc || item.surfaceTreatmentDesc === row.surfaceTreatmentDesc) &&
(!row.manufacturer || item.manufacturer === row.manufacturer)
);
if (record.length !== 1) {
return null;
}
// 返回对应ID // 空值判断函数
return itemType === 'raw_material' const isEmpty = (value) => value === null || value === undefined || value === '';
? res.data[0].rawMaterialId
: res.data[0].productId; // 筛选匹配记录
const matchedRecords = res.rows.filter(item => {
if (isEmpty(row.zincLayer) && !isEmpty(item.zincLayer)) return false;
if (isEmpty(row.surfaceTreatmentDesc) && !isEmpty(item.surfaceTreatmentDesc)) return false;
if (isEmpty(row.manufacturer) && !isEmpty(item.manufacturer)) return false;
if (isEmpty(row.material) && !isEmpty(item.material)) return false;
return true;
});
// 必须返回唯一记录
if (matchedRecords.length !== 1) return null;
// 返回物料ID
return matchedRecords[0][ITEM_ID_FIELD[itemType]];
} catch (error) { } catch (error) {
this.$message.error(`查询${row.type}信息失败:${error.message}`); this.$message.error(`查询${row.type}信息失败:${error.message}`);
return null; return null;
@@ -538,21 +660,60 @@ export default {
}, },
/** /**
* 重置所有状态 * 重置所有状态(添加防呆和二次确认)
*/ */
reset() { async reset() {
// 操作中禁止重置
if (this.validateLoading || this.importLoading || this.importStatus === ImportStatus.PROCESSING) {
this.$message.warning('当前有操作正在进行中,无法重置');
return;
}
// 有数据时二次确认,防止误清空
if (this.file || this.tableData.length > 0 || this.errorList.length > 0) {
const confirm = await this.$confirm(
'确认重置所有状态?已选择的文件、解析的数据和校验结果将被清空!',
'重置确认',
{
confirmButtonText: '确认重置',
cancelButtonText: '取消',
type: 'info'
}
).catch(() => false);
if (!confirm) return;
}
// 重置所有状态
this.file = null; this.file = null;
this.rawData = []; this.rawData = [];
this.tableData = []; this.tableData = [];
this.errorList = []; this.errorList = [];
this.isValidated = false;
this.progress = 0; this.progress = 0;
this.importedCount = 0; this.importedCount = 0;
this.totalCount = 0; this.totalCount = 0;
this.importStatus = 'idle'; this.importStatus = ImportStatus.IDLE;
this.importErrorMsg = ''; this.importErrorMsg = '';
this.$refs.upload?.clearFiles(); // 清空上传组件文件 this.$refs.upload?.clearFiles();
this.$message.success('已重置所有状态');
},
/**
* 统一错误处理确保loading状态关闭
*/
handleError(message) {
this.$message.error(message);
this.importStatus = ImportStatus.ERROR;
this.importErrorMsg = message;
// 异常场景下强制关闭loading避免界面卡死
this.validateLoading = false;
this.importLoading = false;
} }
} },
mounted() {
this.getWarehouseMap();
},
}; };
</script> </script>
@@ -563,18 +724,12 @@ export default {
border-radius: 4px; border-radius: 4px;
} }
.import-mode-selector {
margin-bottom: 20px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.file-upload-area { .file-upload-area {
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
flex-wrap: wrap; /* 适配小屏幕 */
} }
.file-upload-area.disabled { .file-upload-area.disabled {
@@ -603,7 +758,16 @@ export default {
font-size: 14px; font-size: 14px;
} }
.import-finished, .import-error { .import-finished,
.import-error {
margin-bottom: 20px; margin-bottom: 20px;
} }
/* 优化按钮间距,适配小屏幕 */
@media (max-width: 768px) {
.file-upload-area {
flex-direction: column;
align-items: flex-start;
}
}
</style> </style>

View File

@@ -81,6 +81,11 @@
<el-tag v-else type="primary">未收货</el-tag> <el-tag v-else type="primary">未收货</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button v-if="scope.row.actionStatus == 0 || scope.row.actionStatus == 1" type="primary" @click="openReceiptModal(scope.row)">签收</el-button>
</template>
</el-table-column>
<!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" <el-button size="mini" type="text" icon="el-icon-edit"
@@ -99,23 +104,51 @@
<!-- 先做导入功能通过一个钢卷的excel文件导入, 全屏弹窗 --> <!-- 先做导入功能通过一个钢卷的excel文件导入, 全屏弹窗 -->
<el-dialog title="导入收货计划" :visible.sync="importDialogVisible" width="80%" > <el-dialog v-if="selectedPlan" title="导入收货计划" :visible.sync="importDialogVisible" width="80%" >
<ImportGuide /> <ImportGuide :planId="selectedPlan.planId" />
</el-dialog>
<el-dialog title="确认收货" v-loading="loading" :visible.sync="receiptModalVisible" width="30%">
<el-form :model="coilInfo" ref="receiptFormRef" label-width="120px">
<el-form-item label="钢卷号" prop="currentCoilNo">
<el-input v-model="coilInfo.currentCoilNo" disabled placeholder="请输入钢卷号" />
</el-form-item>
<el-form-item label="净重" prop="netWeight">
<el-input v-model="coilInfo.netWeight" disabled placeholder="请输入净重" />
</el-form-item>
<el-form-item label="毛重" prop="grossWeight">
<el-input v-model="coilInfo.grossWeight" disabled placeholder="请输入毛重" />
</el-form-item>
<el-form-item label="实际库区" prop="actualWarehouseId">
<ActualWarehouseSelect v-model="coilInfo.actualWarehouseId" placeholder="请选择实际库区" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="coilInfo.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmReceipt" v-loading="buttonLoading">签收</el-button>
<el-button @click="receiptModalVisible = false">取消</el-button>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { listDeliveryPlan } from "@/api/wms/deliveryPlan"; // 导入收货计划API import { updateMaterialCoilSimple, getMaterialCoil } from '@/api/wms/coil'
import { listPendingAction } from '@/api/wms/pendingAction'; import { listPendingAction, updatePendingAction } from '@/api/wms/pendingAction'
import { listDeliveryPlan } from '@/api/wms/deliveryPlan'
import MemoInput from "@/components/MemoInput"; import MemoInput from "@/components/MemoInput";
import ImportGuide from "@/views/wms/receive/components/ImportGuide.vue"; import ImportGuide from "@/views/wms/receive/components/ImportGuide.vue";
import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect";
export default { export default {
name: "DeliveryWaybill", name: "DeliveryWaybill",
components: { components: {
MemoInput, MemoInput,
ImportGuide ImportGuide,
ActualWarehouseSelect
}, },
data() { data() {
return { return {
@@ -184,6 +217,16 @@ export default {
// 导入弹窗 // 导入弹窗
importDialogVisible: false, importDialogVisible: false,
// 确认收货表单参数
receiptModalVisible: false,
receiptForm: {
currentCoilNo: null,
netWeight: null,
grossWeight: null,
actualWarehouseId: null,
remark: null,
},
coilInfo: {},
}; };
}, },
created() { created() {
@@ -346,6 +389,46 @@ export default {
...this.queryParams ...this.queryParams
}, `deliveryWaybill_${new Date().getTime()}.xlsx`) }, `deliveryWaybill_${new Date().getTime()}.xlsx`)
}, },
// 打开收货弹窗
openReceiptModal(row) {
this.loading = true
// 打开确认收货弹窗
this.receiptModalVisible = true;
this.receiptForm = row;
// 根据钢卷id查询钢卷信息
getMaterialCoil(row.coilId).then(res => {
this.coilInfo = res.data;
this.loading = false
})
},
// 确认收货
confirmReceipt() {
// 二次确认
this.$modal.confirm("收货后刚钢卷会进入库存并占用所选的实际库区,是否继续?").then(() => {
this.buttonLoading = true;
// 更新操作记录状态 actionStatus: 2
updatePendingAction({
...this.receiptForm,
actionStatus: 2,
}).then(_ => {
this.$message.success("确认收货成功");
this.getList()
}).finally(() => {
this.buttonLoading = false;
});
// 更新钢卷状态为当前钢卷; dataType: 1
updateMaterialCoilSimple({
...this.coilInfo,
dataType: 1,
})
})
},
// 取消操作
cancel() {
this.form = {};
this.$refs.form.resetFields();
this.buttonLoading = false;
}
} }
}; };
</script> </script>