From 51caea9e417526c861d7e52e2dccd302cf827e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= Date: Tue, 2 Dec 2025 13:56:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(wms):=20=E4=BC=98=E5=8C=96=E6=94=B6?= =?UTF-8?q?=E8=B4=A7=E5=8A=9F=E8=83=BD=E5=B9=B6=E9=87=8D=E6=9E=84=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构收货功能界面,将"确认收货"改为"签收",并调整按钮样式。新增收货弹窗功能,支持选择实际库区和填写备注。 重构导入组件,优化校验逻辑和错误处理,增加防呆机制和loading状态。将枚举和常量提取为模块级变量,提升代码可维护性。添加仓库映射关系,增强数据校验能力。 调整表格按钮的内边距,提升UI一致性 --- klp-ui/src/assets/styles/btn.scss | 2 +- klp-ui/src/views/wms/coil/do/warehousing.vue | 9 +- .../wms/receive/components/ImportGuide.vue | 726 +++++++++++------- klp-ui/src/views/wms/receive/detail/index.vue | 93 ++- 4 files changed, 538 insertions(+), 292 deletions(-) diff --git a/klp-ui/src/assets/styles/btn.scss b/klp-ui/src/assets/styles/btn.scss index 49a40e86..16a3e5bd 100644 --- a/klp-ui/src/assets/styles/btn.scss +++ b/klp-ui/src/assets/styles/btn.scss @@ -104,7 +104,7 @@ } .el-table__cell .el-button { - padding: 0 !important; + padding: 2 !important; } .el-table__cell + .el-button { diff --git a/klp-ui/src/views/wms/coil/do/warehousing.vue b/klp-ui/src/views/wms/coil/do/warehousing.vue index fc0755b4..bc4b24b2 100644 --- a/klp-ui/src/views/wms/coil/do/warehousing.vue +++ b/klp-ui/src/views/wms/coil/do/warehousing.vue @@ -179,12 +179,11 @@ 未收货 - + diff --git a/klp-ui/src/views/wms/receive/components/ImportGuide.vue b/klp-ui/src/views/wms/receive/components/ImportGuide.vue index 0cbb112b..5de48a34 100644 --- a/klp-ui/src/views/wms/receive/components/ImportGuide.vue +++ b/klp-ui/src/views/wms/receive/components/ImportGuide.vue @@ -1,52 +1,46 @@ @@ -132,63 +112,219 @@ import { addMaterialCoil } from '@/api/wms/coil'; import { addPendingAction } from '@/api/wms/pendingAction'; import { listRawMaterial } from '@/api/wms/rawMaterial'; 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 { name: 'MaterialCoilImportWizard', + props: { + planId: { + type: String, + default: 0 + } + }, data() { return { - // 导入模式:validate-校验导入 direct-直接导入 - importMode: 'validate', + // 枚举挂载到实例,方便模板使用 + ImportStatus, + TableColumnEnum, + // 文件对象 file: null, // 解析后的原始数据 rawData: [], // 格式化后用于展示/导入的数据 tableData: [], - // 校验错误列表 + // 校验错误列表(为空表示校验通过) errorList: [], + // 是否点击过校验按钮(新增状态) + isValidated: false, // 导入进度(0-100) progress: 0, // 已导入条数 importedCount: 0, // 总数据条数 totalCount: 0, - // 导入状态:idle-闲置 processing-处理中 finished-完成 error-失败 - importStatus: 'idle', + // 导入状态 + importStatus: ImportStatus.IDLE, // 导入错误信息 importErrorMsg: '', - // 规定的表头(顺序和名称必须匹配) - requiredHeaders: [ - '类型', '逻辑库区', '入场卷号', '厂家卷号', '重量(吨)', '备注', - '名称', '规格', '材质', '厂家', '表面处理', '锌层' - ], - // 表头映射(中文表头 -> 字段名) - headerMap: { - '类型': 'type', - '逻辑库区': 'logicWarehouse', - '入场卷号': 'inboundCoilNo', - '厂家卷号': 'factoryCoilNo', - '重量(吨)': 'weight', - '备注': 'remark', - '名称': 'name', - '规格': 'specification', - '材质': 'material', - '厂家': 'manufacturer', - '表面处理': 'surfaceTreatmentDesc', - '锌层': 'zincLayer' - } + // 仓库名到仓库id的映射关系 + warehouseMap: {}, + // 防呆loading状态 + validateLoading: false, // 校验按钮loading + importLoading: false // 导入按钮loading }; }, methods: { /** - * 处理文件选择 + * 处理文件选择(新增防呆:操作中禁止切换文件) */ handleFileChange(file) { + // 操作中禁止切换文件 + if (this.validateLoading || this.importLoading || this.importStatus === ImportStatus.PROCESSING) { + this.$message.warning('当前有操作正在进行中,请完成后再选择新文件'); + return; + } this.file = file.raw; - // 选择文件后自动读取Excel + this.isValidated = false; // 选择新文件后重置校验状态 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文件内容 */ @@ -198,35 +334,29 @@ export default { 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' }); - // 取第一个sheet const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; - // 解析为JSON(跳过表头,从第二行开始) 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.rawData = jsonData.slice(1).filter(row => row.length > 0); // 格式化数据 this.formatExcel(); this.$message.success(`成功解析Excel,共读取到 ${this.rawData.length} 条数据`); } catch (error) { - this.$message.error(`解析Excel失败:${error.message}`); - this.importStatus = 'error'; - this.importErrorMsg = `解析Excel失败:${error.message}`; + this.handleError(`解析Excel失败:${error.message}`); } }; } catch (error) { - this.$message.error(`读取文件失败:${error.message}`); - this.importStatus = 'error'; - this.importErrorMsg = `读取文件失败:${error.message}`; + this.handleError(`读取文件失败:${error.message}`); } }, @@ -235,21 +365,22 @@ export default { */ validateHeaders(headers) { this.errorList = []; - // 校验表头数量和名称 - if (headers.length !== this.requiredHeaders.length) { + const { REQUIRED_HEADERS } = SystemConstant; + + // 校验表头数量 + if (headers.length !== REQUIRED_HEADERS.length) { this.errorList.push({ rowNum: 1, - errorMsg: `表头数量不匹配,要求${this.requiredHeaders.length}列,实际${headers.length}列` + errorMsg: ErrorType.HEADER_COUNT_ERROR.message(REQUIRED_HEADERS.length, headers.length) }); - return; } - // 校验每列表头名称 + // 校验表头名称 headers.forEach((header, index) => { - if (header !== this.requiredHeaders[index]) { + if (header !== REQUIRED_HEADERS[index]) { this.errorList.push({ 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) { this.$message.warning('暂无数据可校验'); return; } + this.validateLoading = true; // 开启校验loading this.errorList = []; - // 逐行校验 - for (let i = 0; i < this.rawData.length; i++) { - const row = this.rawData[i]; - const rowNum = i + 2; // 数据行从第二行开始,行号=索引+2 - const rowObj = this.formatRowData(row, rowNum); + 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); - // 1. 校验必填项 - const requiredFields = [ - { key: 'type', label: '类型', value: rowObj.type }, - { key: 'logicWarehouse', label: '逻辑库区', value: rowObj.logicWarehouse }, - { key: 'inboundCoilNo', label: '入场卷号', value: rowObj.inboundCoilNo }, - { key: 'name', label: '名称', value: rowObj.name }, - { key: 'specification', label: '规格', value: rowObj.specification }, - ]; + // 执行各类校验器 + ErrorType.FIELD_REQUIRED.validator(rowObj, rowNum, this.errorList); + ErrorType.TYPE_ERROR.validator(rowObj, rowNum, this.errorList); + ErrorType.WEIGHT_ERROR.validator(rowObj, rowNum, this.errorList); + ErrorType.WAREHOUSE_ERROR.validator(rowObj, rowNum, this.errorList, this.warehouseMap); - // 检查必填项为空 - for (const field of requiredFields) { - if (!field.value || field.value.toString().trim() === '') { - this.errorList.push({ - rowNum, - errorMsg: `${field.label}不能为空` - }); + // 校验物料ID是否存在(仅当当前行无基础错误时) + if (!this.errorList.some(err => err.rowNum === rowNum) && rowObj.type) { + const itemId = await this._findItemId(rowObj); + if (!itemId) { + this.errorList.push({ + rowNum, + errorMsg: ErrorType.ITEM_ID_NOT_FOUND.message(rowObj.type, rowObj.name, rowObj.specification, rowObj.material, rowObj.surfaceTreatmentDesc, rowObj.manufacturer, rowObj.zincLayer) + }); + } } } - // 2. 校验类型只能是“原料”或“成品” - if (rowObj.type && !['原料', '成品'].includes(rowObj.type.trim())) { - 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的数字' - }); - } + if (this.errorList.length > 0) { + this.$message.error(`数据校验失败,共发现${this.errorList.length}条错误`); } else { - this.errorList.push({ - rowNum, - errorMsg: '重量不能为空' - }); + this.$message.success('数据校验通过,可以开始导入'); } - - // 4. 预校验itemId是否存在(仅校验模式) - if (rowObj.type && ['原料', '成品'].includes(rowObj.type.trim()) && !this.errorList.some(err => err.rowNum === rowNum)) { - const itemId = await this._findItemId(rowObj); - 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('数据校验通过,可以开始导入'); + } catch (error) { + this.handleError(`校验数据时发生错误:${error.message}`); + } finally { + this.validateLoading = false; // 关闭校验loading } }, @@ -345,7 +459,6 @@ export default { this.tableData = []; if (this.rawData.length === 0) return; - // 逐行格式化 this.rawData.forEach((row, index) => { const rowNum = index + 2; const rowObj = this.formatRowData(row, rowNum); @@ -357,51 +470,54 @@ export default { * 格式化单行数据 */ formatRowData(row, rowNum) { + const { HEADER_MAP, REQUIRED_HEADERS } = SystemConstant; const rowObj = {}; + // 映射表头和字段 - this.requiredHeaders.forEach((header, index) => { - const field = this.headerMap[header]; - // 处理空值,统一转为字符串 + REQUIRED_HEADERS.forEach((header, index) => { + const field = HEADER_MAP[header]; rowObj[field] = row[index] ? row[index].toString().trim() : ''; }); + // 重量转数字 rowObj.weight = rowObj.weight ? Number(rowObj.weight) : 0; // 增加行号 rowObj.rowNum = rowNum; + return rowObj; }, /** - * 处理校验按钮点击 - */ - async handleValidate() { - if (!this.file) { - this.$message.warning('请先选择Excel文件'); - return; - } - await this.validateExcel(); - }, - - /** - * 开始导入数据 + * 开始导入数据(添加防重复点击和二次确认) */ async startImport() { + // 防重复点击导入 + if (this.importLoading) { + this.$message.warning('正在导入数据,请勿重复操作...'); + return; + } if (!this.file || this.tableData.length === 0) { this.$message.warning('暂无数据可导入'); return; } - // 直接导入模式下先快速校验基础格式 - if (this.importMode === 'direct') { - await this.validateExcel(); - if (this.errorList.length > 0) { - this.$message.error('直接导入模式下基础格式校验失败,请修正'); - return; + // 二次确认,防止误操作 + const confirm = await this.$confirm( + '确认导入已校验通过的数据?导入过程中请勿刷新页面或关闭浏览器!', + '导入确认', + { + 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.importedCount = 0; this.totalCount = this.tableData.length; @@ -409,12 +525,12 @@ export default { try { await this.batchImport(); - this.importStatus = 'finished'; + this.importStatus = ImportStatus.FINISHED; this.$message.success(`导入完成!共成功导入${this.importedCount}条数据`); } catch (error) { - this.importStatus = 'error'; - this.importErrorMsg = `导入失败:${error.message},已导入${this.importedCount}条数据`; - this.$message.error(this.importErrorMsg); + this.handleError(`导入失败:${error.message},已导入${this.importedCount}条数据`); + } finally { + this.importLoading = false; // 关闭导入loading } }, @@ -422,18 +538,20 @@ export default { * 批量导入数据 */ async batchImport() { - // 遍历所有数据行,逐个导入 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]; try { await this.importOneRecord(row); 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) { - // 单条失败可选择继续或终止,这里选择终止 throw new Error(`第${row.rowNum}行导入失败:${error.message}`); } } @@ -444,42 +562,48 @@ export default { */ async importOneRecord(row) { try { - // 1. 查找itemId + // 1. 查找物料ID(再次校验,防止数据篡改) const itemId = await this._findItemId(row); 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 = { itemId, itemType, - logicWarehouse: row.logicWarehouse, - inboundCoilNo: row.inboundCoilNo, - factoryCoilNo: row.factoryCoilNo, - weight: row.weight, + materialType: row.type, + warehouseId: this.warehouseMap[row.logicWarehouse], + enterCoilNo: row.inboundCoilNo, + currentCoilNo: row.inboundCoilNo, + supplierCoilNo: row.factoryCoilNo, + grossWeight: row.weight, + netWeight: row.weight, remark: row.remark, - dataType: 10 // 钢卷数据类型固定为10 + dataType: SystemConstant.COIL_DATA_TYPE }; const coilRes = await addMaterialCoil(coilParams); - if (!coilRes.success) { // 假设接口返回success标识 + if (coilRes.code !== 200) { throw new Error(`钢卷数据插入失败:${coilRes.message || '接口返回异常'}`); } - const coilId = coilRes.data?.coilId; // 假设返回钢卷ID + const coilId = coilRes.data?.coilId; - // 3. 插入待处理操作(actionStatus=0) + // 3. 插入待处理操作 const actionParams = { coilId, - actionStatus: 0, // 待处理状态固定为0 + currentCoilNo: row.inboundCoilNo, + actionStatus: SystemConstant.PENDING_ACTION_STATUS, itemType, itemId, - operationType: 'import', // 操作类型:导入 - remark: `Excel导入:${row.inboundCoilNo}` + actionType: SystemConstant.PENDING_ACTION_TYPE_RECEIVE, + operationType: SystemConstant.OPERATION_TYPE_IMPORT, + remark: `Excel导入:${row.inboundCoilNo}`, + warehouseId: this.planId }; const actionRes = await addPendingAction(actionParams); - if (!actionRes.success) { + if (actionRes.code !== 200) { throw new Error(`待处理操作插入失败:${actionRes.message || '接口返回异常'}`); } @@ -489,48 +613,46 @@ export default { }, /** - * 根据条件查找唯一的itemId + * 根据条件查找唯一的物料ID */ async _findItemId(row) { - const itemType = row.type === '原料' ? 'raw_material' : 'product'; - let res = null; + const { ITEM_TYPE_MAP, ITEM_ID_FIELD } = SystemConstant; + const itemType = ITEM_TYPE_MAP[row.type]; + if (!itemType) return null; try { - if (itemType === 'raw_material') { - res = await listRawMaterial({ - rawMaterialName: row.name, - specification: row.specification, - material: row.material, - manufacturer: row.manufacturer, - surfaceTreatmentDesc: row.surfaceTreatmentDesc, - 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 queryParams = { + [row.type === '原料' ? 'rawMaterialName' : 'productName']: row.name, + specification: row.specification, + material: row.material, + manufacturer: row.manufacturer, + surfaceTreatmentDesc: row.surfaceTreatmentDesc, + zincLayer: row.zincLayer + }; - // 校验返回结果数量 - // 如果锌层,表面处理,厂家未传递,则需要从结果中取出这些值未空的记录 - const record = res.data.filter(item => - (!row.zincLayer || item.zincLayer === row.zincLayer) && - (!row.surfaceTreatmentDesc || item.surfaceTreatmentDesc === row.surfaceTreatmentDesc) && - (!row.manufacturer || item.manufacturer === row.manufacturer) - ); - if (record.length !== 1) { - return null; - } + // 执行查询 + const res = itemType === 'raw_material' + ? await listRawMaterial(queryParams) + : await listProduct(queryParams); - // 返回对应ID - return itemType === 'raw_material' - ? res.data[0].rawMaterialId - : res.data[0].productId; + // 空值判断函数 + const isEmpty = (value) => value === null || value === undefined || value === ''; + + // 筛选匹配记录 + 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) { this.$message.error(`查询${row.type}信息失败:${error.message}`); 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.rawData = []; this.tableData = []; this.errorList = []; + this.isValidated = false; this.progress = 0; this.importedCount = 0; this.totalCount = 0; - this.importStatus = 'idle'; + this.importStatus = ImportStatus.IDLE; 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(); + }, }; @@ -563,18 +724,12 @@ export default { border-radius: 4px; } -.import-mode-selector { - margin-bottom: 20px; - padding: 10px; - background: #f5f7fa; - border-radius: 4px; -} - .file-upload-area { margin-bottom: 20px; display: flex; align-items: center; gap: 10px; + flex-wrap: wrap; /* 适配小屏幕 */ } .file-upload-area.disabled { @@ -603,7 +758,16 @@ export default { font-size: 14px; } -.import-finished, .import-error { +.import-finished, +.import-error { margin-bottom: 20px; } + +/* 优化按钮间距,适配小屏幕 */ +@media (max-width: 768px) { + .file-upload-area { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/klp-ui/src/views/wms/receive/detail/index.vue b/klp-ui/src/views/wms/receive/detail/index.vue index 2ca1a28c..8eeaeb16 100644 --- a/klp-ui/src/views/wms/receive/detail/index.vue +++ b/klp-ui/src/views/wms/receive/detail/index.vue @@ -81,6 +81,11 @@ 未收货 + + + - - + + + + + + + + + + + + + + + + + + + + + + + +