2025-12-01 11:56:31 +08:00
|
|
|
|
<template>
|
2025-12-01 17:33:20 +08:00
|
|
|
|
<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' }">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="upload"
|
|
|
|
|
|
class="upload-excel"
|
|
|
|
|
|
action=""
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
|
accept=".xlsx,.xls"
|
|
|
|
|
|
:disabled="importStatus === 'processing'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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 && importMode === 'validate' && importStatus === 'idle'"
|
|
|
|
|
|
:disabled="!file"
|
|
|
|
|
|
>
|
|
|
|
|
|
校验数据
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
icon="el-icon-circle-check"
|
|
|
|
|
|
@click="startImport"
|
|
|
|
|
|
v-if="file && (importMode === 'direct' || (importMode === 'validate' && errorList.length === 0)) && importStatus === 'idle'"
|
|
|
|
|
|
:disabled="!file || (importMode === 'validate' && errorList.length > 0)"
|
|
|
|
|
|
>
|
|
|
|
|
|
开始导入
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
icon="el-icon-refresh"
|
|
|
|
|
|
@click="reset"
|
|
|
|
|
|
:disabled="importStatus === 'processing'"
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 校验错误提示 -->
|
|
|
|
|
|
<div v-if="errorList.length > 0" class="error-list">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="数据校验失败"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
description="以下行数据不符合格式要求,请修正后重新导入:"
|
|
|
|
|
|
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="type" label="类型" width="80" />
|
|
|
|
|
|
<el-table-column prop="logicWarehouse" label="逻辑库区" width="120" />
|
|
|
|
|
|
<el-table-column prop="inboundCoilNo" label="入场卷号" width="150" />
|
|
|
|
|
|
<el-table-column prop="factoryCoilNo" label="厂家卷号" width="150" />
|
|
|
|
|
|
<el-table-column prop="weight" label="重量(吨)" width="120" />
|
|
|
|
|
|
<el-table-column prop="remark" label="备注" min-width="100" />
|
|
|
|
|
|
<el-table-column prop="name" label="名称" min-width="100" />
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
2025-12-01 11:56:31 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-12-01 17:33:20 +08:00
|
|
|
|
import * as XLSX from 'xlsx';
|
|
|
|
|
|
import { addMaterialCoil } from '@/api/wms/coil';
|
|
|
|
|
|
import { addPendingAction } from '@/api/wms/pendingAction';
|
|
|
|
|
|
import { listRawMaterial } from '@/api/wms/rawMaterial';
|
|
|
|
|
|
import { listProduct } from '@/api/wms/product';
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'MaterialCoilImportWizard',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
// 导入模式:validate-校验导入 direct-直接导入
|
|
|
|
|
|
importMode: 'validate',
|
|
|
|
|
|
// 文件对象
|
|
|
|
|
|
file: null,
|
|
|
|
|
|
// 解析后的原始数据
|
|
|
|
|
|
rawData: [],
|
|
|
|
|
|
// 格式化后用于展示/导入的数据
|
|
|
|
|
|
tableData: [],
|
|
|
|
|
|
// 校验错误列表
|
|
|
|
|
|
errorList: [],
|
|
|
|
|
|
// 导入进度(0-100)
|
|
|
|
|
|
progress: 0,
|
|
|
|
|
|
// 已导入条数
|
|
|
|
|
|
importedCount: 0,
|
|
|
|
|
|
// 总数据条数
|
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
|
// 导入状态:idle-闲置 processing-处理中 finished-完成 error-失败
|
|
|
|
|
|
importStatus: 'idle',
|
|
|
|
|
|
// 导入错误信息
|
|
|
|
|
|
importErrorMsg: '',
|
|
|
|
|
|
// 规定的表头(顺序和名称必须匹配)
|
|
|
|
|
|
requiredHeaders: [
|
|
|
|
|
|
'类型', '逻辑库区', '入场卷号', '厂家卷号', '重量(吨)', '备注',
|
|
|
|
|
|
'名称', '规格', '材质', '厂家', '表面处理', '锌层'
|
|
|
|
|
|
],
|
|
|
|
|
|
// 表头映射(中文表头 -> 字段名)
|
|
|
|
|
|
headerMap: {
|
|
|
|
|
|
'类型': 'type',
|
|
|
|
|
|
'逻辑库区': 'logicWarehouse',
|
|
|
|
|
|
'入场卷号': 'inboundCoilNo',
|
|
|
|
|
|
'厂家卷号': 'factoryCoilNo',
|
|
|
|
|
|
'重量(吨)': 'weight',
|
|
|
|
|
|
'备注': 'remark',
|
|
|
|
|
|
'名称': 'name',
|
|
|
|
|
|
'规格': 'specification',
|
|
|
|
|
|
'材质': 'material',
|
|
|
|
|
|
'厂家': 'manufacturer',
|
|
|
|
|
|
'表面处理': 'surfaceTreatmentDesc',
|
|
|
|
|
|
'锌层': 'zincLayer'
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理文件选择
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleFileChange(file) {
|
|
|
|
|
|
this.file = file.raw;
|
|
|
|
|
|
// 选择文件后自动读取Excel
|
|
|
|
|
|
this.readExcel();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 读取Excel文件内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
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' });
|
|
|
|
|
|
// 取第一个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.formatExcel();
|
|
|
|
|
|
|
|
|
|
|
|
this.$message.success(`成功解析Excel,共读取到 ${this.rawData.length} 条数据`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(`解析Excel失败:${error.message}`);
|
|
|
|
|
|
this.importStatus = 'error';
|
|
|
|
|
|
this.importErrorMsg = `解析Excel失败:${error.message}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(`读取文件失败:${error.message}`);
|
|
|
|
|
|
this.importStatus = 'error';
|
|
|
|
|
|
this.importErrorMsg = `读取文件失败:${error.message}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 校验表头是否匹配
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateHeaders(headers) {
|
|
|
|
|
|
this.errorList = [];
|
|
|
|
|
|
// 校验表头数量和名称
|
|
|
|
|
|
if (headers.length !== this.requiredHeaders.length) {
|
|
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum: 1,
|
|
|
|
|
|
errorMsg: `表头数量不匹配,要求${this.requiredHeaders.length}列,实际${headers.length}列`
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 校验每列表头名称
|
|
|
|
|
|
headers.forEach((header, index) => {
|
|
|
|
|
|
if (header !== this.requiredHeaders[index]) {
|
|
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum: 1,
|
|
|
|
|
|
errorMsg: `第${index + 1}列表头错误,要求:"${this.requiredHeaders[index]}",实际:"${header}"`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (this.errorList.length > 0) {
|
|
|
|
|
|
this.$message.error('Excel表头格式不符合要求,请检查!');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 校验Excel数据格式
|
|
|
|
|
|
*/
|
|
|
|
|
|
async validateExcel() {
|
|
|
|
|
|
if (this.rawData.length === 0) {
|
|
|
|
|
|
this.$message.warning('暂无数据可校验');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 检查必填项为空
|
|
|
|
|
|
for (const field of requiredFields) {
|
|
|
|
|
|
if (!field.value || field.value.toString().trim() === '') {
|
|
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum,
|
|
|
|
|
|
errorMsg: `${field.label}不能为空`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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的数字'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum,
|
|
|
|
|
|
errorMsg: '重量不能为空'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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('数据校验通过,可以开始导入');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化Excel行数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
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 = {};
|
|
|
|
|
|
// 映射表头和字段
|
|
|
|
|
|
this.requiredHeaders.forEach((header, index) => {
|
|
|
|
|
|
const field = this.headerMap[header];
|
|
|
|
|
|
// 处理空值,统一转为字符串
|
|
|
|
|
|
rowObj[field] = row[index] ? row[index].toString().trim() : '';
|
|
|
|
|
|
});
|
|
|
|
|
|
// 重量转数字
|
|
|
|
|
|
rowObj.weight = rowObj.weight ? Number(rowObj.weight) : 0;
|
|
|
|
|
|
// 增加行号
|
|
|
|
|
|
rowObj.rowNum = rowNum;
|
|
|
|
|
|
return rowObj;
|
|
|
|
|
|
},
|
2025-12-01 11:56:31 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理校验按钮点击
|
|
|
|
|
|
*/
|
|
|
|
|
|
async handleValidate() {
|
|
|
|
|
|
if (!this.file) {
|
|
|
|
|
|
this.$message.warning('请先选择Excel文件');
|
|
|
|
|
|
return;
|
2025-12-01 11:56:31 +08:00
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
await this.validateExcel();
|
2025-12-01 11:56:31 +08:00
|
|
|
|
},
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 开始导入数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
async startImport() {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-12-01 11:56:31 +08:00
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化导入状态
|
|
|
|
|
|
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.importStatus = 'error';
|
|
|
|
|
|
this.importErrorMsg = `导入失败:${error.message},已导入${this.importedCount}条数据`;
|
|
|
|
|
|
this.$message.error(this.importErrorMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量导入数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
async batchImport() {
|
|
|
|
|
|
// 遍历所有数据行,逐个导入
|
|
|
|
|
|
for (let i = 0; i < this.tableData.length; i++) {
|
|
|
|
|
|
if (this.importStatus === 'error') break; // 发生错误则停止
|
|
|
|
|
|
|
|
|
|
|
|
const row = this.tableData[i];
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.importOneRecord(row);
|
|
|
|
|
|
this.importedCount++;
|
|
|
|
|
|
// 更新进度
|
|
|
|
|
|
this.progress = Math.round(((i + 1) / this.totalCount) * 100);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 单条失败可选择继续或终止,这里选择终止
|
|
|
|
|
|
throw new Error(`第${row.rowNum}行导入失败:${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导入单条记录
|
|
|
|
|
|
*/
|
|
|
|
|
|
async importOneRecord(row) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 查找itemId
|
|
|
|
|
|
const itemId = await this._findItemId(row);
|
|
|
|
|
|
if (!itemId) {
|
|
|
|
|
|
throw new Error(`未找到唯一的${row.type}信息`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const itemType = row.type === '原料' ? 'raw_material' : 'product';
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 插入钢卷数据(dataType=10)
|
|
|
|
|
|
const coilParams = {
|
|
|
|
|
|
itemId,
|
|
|
|
|
|
itemType,
|
|
|
|
|
|
logicWarehouse: row.logicWarehouse,
|
|
|
|
|
|
inboundCoilNo: row.inboundCoilNo,
|
|
|
|
|
|
factoryCoilNo: row.factoryCoilNo,
|
|
|
|
|
|
weight: row.weight,
|
|
|
|
|
|
remark: row.remark,
|
|
|
|
|
|
dataType: 10 // 钢卷数据类型固定为10
|
|
|
|
|
|
};
|
|
|
|
|
|
const coilRes = await addMaterialCoil(coilParams);
|
|
|
|
|
|
if (!coilRes.success) { // 假设接口返回success标识
|
|
|
|
|
|
throw new Error(`钢卷数据插入失败:${coilRes.message || '接口返回异常'}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const coilId = coilRes.data?.coilId; // 假设返回钢卷ID
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 插入待处理操作(actionStatus=0)
|
|
|
|
|
|
const actionParams = {
|
|
|
|
|
|
coilId,
|
|
|
|
|
|
actionStatus: 0, // 待处理状态固定为0
|
|
|
|
|
|
itemType,
|
|
|
|
|
|
itemId,
|
|
|
|
|
|
operationType: 'import', // 操作类型:导入
|
|
|
|
|
|
remark: `Excel导入:${row.inboundCoilNo}`
|
|
|
|
|
|
};
|
|
|
|
|
|
const actionRes = await addPendingAction(actionParams);
|
|
|
|
|
|
if (!actionRes.success) {
|
|
|
|
|
|
throw new Error(`待处理操作插入失败:${actionRes.message || '接口返回异常'}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw new Error(error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据条件查找唯一的itemId
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _findItemId(row) {
|
|
|
|
|
|
const itemType = row.type === '原料' ? 'raw_material' : 'product';
|
|
|
|
|
|
let res = 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 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回对应ID
|
|
|
|
|
|
return itemType === 'raw_material'
|
|
|
|
|
|
? res.data[0].rawMaterialId
|
|
|
|
|
|
: res.data[0].productId;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(`查询${row.type}信息失败:${error.message}`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置所有状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
reset() {
|
|
|
|
|
|
this.file = null;
|
|
|
|
|
|
this.rawData = [];
|
|
|
|
|
|
this.tableData = [];
|
|
|
|
|
|
this.errorList = [];
|
|
|
|
|
|
this.progress = 0;
|
|
|
|
|
|
this.importedCount = 0;
|
|
|
|
|
|
this.totalCount = 0;
|
|
|
|
|
|
this.importStatus = 'idle';
|
|
|
|
|
|
this.importErrorMsg = '';
|
|
|
|
|
|
this.$refs.upload?.clearFiles(); // 清空上传组件文件
|
2025-12-01 11:56:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.import-wizard-container {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-upload-area.disabled {
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|