2025-12-01 11:56:31 +08:00
|
|
|
|
<template>
|
2025-12-01 17:33:20 +08:00
|
|
|
|
<div class="import-wizard-container">
|
|
|
|
|
|
<!-- 文件上传区域 -->
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<div class="file-upload-area" :class="{ disabled: importStatus === ImportStatus.PROCESSING }">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="upload"
|
|
|
|
|
|
class="upload-excel"
|
|
|
|
|
|
action=""
|
|
|
|
|
|
:auto-upload="false"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
:show-file-list="false"
|
2025-12-02 13:56:21 +08:00
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
|
accept=".xlsx,.xls"
|
|
|
|
|
|
:disabled="importStatus === ImportStatus.PROCESSING || validateLoading || importLoading"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
>
|
|
|
|
|
|
<el-button type="primary" icon="el-icon-upload2">选择Excel文件</el-button>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<!-- 操作按钮:新增loading和防呆 -->
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
icon="el-icon-check"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
@click="handleValidate"
|
2025-12-02 13:56:21 +08:00
|
|
|
|
v-if="file && importStatus === ImportStatus.IDLE"
|
|
|
|
|
|
:disabled="!file || validateLoading"
|
|
|
|
|
|
:loading="validateLoading"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
>
|
|
|
|
|
|
校验数据
|
|
|
|
|
|
</el-button>
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
icon="el-icon-circle-check"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
@click="startImport"
|
2025-12-02 13:56:21 +08:00
|
|
|
|
v-if="file && importStatus === ImportStatus.IDLE"
|
|
|
|
|
|
:disabled="!file || !isValidated || errorList.length > 0 || importLoading"
|
|
|
|
|
|
:loading="importLoading"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
>
|
|
|
|
|
|
开始导入
|
|
|
|
|
|
</el-button>
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
icon="el-icon-refresh"
|
|
|
|
|
|
@click="reset"
|
|
|
|
|
|
:disabled="importStatus === ImportStatus.PROCESSING || validateLoading || importLoading"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 校验错误提示 -->
|
|
|
|
|
|
<div v-if="errorList.length > 0" class="error-list">
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<el-alert title="数据校验失败" type="error" description="以下行数据不符合格式要求,请修正后重新导入:" show-icon />
|
2025-12-01 17:33:20 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 数据预览 -->
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<div v-if="tableData.length > 0 && importStatus === ImportStatus.IDLE" class="data-preview">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="数据预览"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:description="`共解析出 ${tableData.length} 条有效数据`"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:closable="false"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<el-table :data="tableData" border size="small" max-height="300" stripe>
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-for="column in TableColumnEnum"
|
|
|
|
|
|
:key="column.prop"
|
|
|
|
|
|
:prop="column.prop"
|
|
|
|
|
|
:label="column.label"
|
|
|
|
|
|
:width="column.width"
|
|
|
|
|
|
:min-width="column.minWidth"
|
|
|
|
|
|
/>
|
2025-12-01 17:33:20 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 导入进度展示 -->
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<div v-if="importStatus === ImportStatus.PROCESSING" class="import-progress">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="正在导入数据"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
:description="`当前进度:${progress}%`"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:closable="false"
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<el-progress :percentage="progress" status="success" />
|
|
|
|
|
|
<p class="progress-tip">已导入 {{ importedCount }} / {{ totalCount }} 条数据</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 导入完成提示 -->
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<div v-if="importStatus === ImportStatus.FINISHED" class="import-finished">
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="导入完成"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
:description="`共成功导入 ${importedCount} 条数据,总计 ${totalCount} 条`"
|
|
|
|
|
|
show-icon
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 导入失败提示 -->
|
2025-12-02 13:56:21 +08:00
|
|
|
|
<div v-if="importStatus === ImportStatus.ERROR" class="import-error">
|
|
|
|
|
|
<el-alert title="导入失败" type="error" :description="importErrorMsg" show-icon />
|
2025-12-01 17:33:20 +08:00
|
|
|
|
</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';
|
2025-12-02 13:56:21 +08:00
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'MaterialCoilImportWizard',
|
2025-12-02 13:56:21 +08:00
|
|
|
|
props: {
|
|
|
|
|
|
planId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-12-01 17:33:20 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 枚举挂载到实例,方便模板使用
|
|
|
|
|
|
ImportStatus,
|
|
|
|
|
|
TableColumnEnum,
|
|
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 文件对象
|
|
|
|
|
|
file: null,
|
|
|
|
|
|
// 解析后的原始数据
|
|
|
|
|
|
rawData: [],
|
|
|
|
|
|
// 格式化后用于展示/导入的数据
|
|
|
|
|
|
tableData: [],
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 校验错误列表(为空表示校验通过)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
errorList: [],
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 是否点击过校验按钮(新增状态)
|
|
|
|
|
|
isValidated: false,
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 导入进度(0-100)
|
|
|
|
|
|
progress: 0,
|
|
|
|
|
|
// 已导入条数
|
|
|
|
|
|
importedCount: 0,
|
|
|
|
|
|
// 总数据条数
|
|
|
|
|
|
totalCount: 0,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 导入状态
|
|
|
|
|
|
importStatus: ImportStatus.IDLE,
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 导入错误信息
|
|
|
|
|
|
importErrorMsg: '',
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 仓库名到仓库id的映射关系
|
|
|
|
|
|
warehouseMap: {},
|
|
|
|
|
|
// 防呆loading状态
|
|
|
|
|
|
validateLoading: false, // 校验按钮loading
|
|
|
|
|
|
importLoading: false // 导入按钮loading
|
2025-12-01 17:33:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/**
|
2025-12-02 13:56:21 +08:00
|
|
|
|
* 处理文件选择(新增防呆:操作中禁止切换文件)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
*/
|
|
|
|
|
|
handleFileChange(file) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 操作中禁止切换文件
|
|
|
|
|
|
if (this.validateLoading || this.importLoading || this.importStatus === ImportStatus.PROCESSING) {
|
|
|
|
|
|
this.$message.warning('当前有操作正在进行中,请完成后再选择新文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.file = file.raw;
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.isValidated = false; // 选择新文件后重置校验状态
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.readExcel();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有的仓库,建立仓库名到仓库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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 读取Excel文件内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
async readExcel() {
|
|
|
|
|
|
if (!this.file) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fileReader = new FileReader();
|
|
|
|
|
|
fileReader.readAsArrayBuffer(this.file);
|
2025-12-02 13:56:21 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
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]);
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 解析数据行(过滤空行)
|
|
|
|
|
|
this.rawData = jsonData.slice(1).filter(row => row.length > 0);
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 格式化数据
|
|
|
|
|
|
this.formatExcel();
|
|
|
|
|
|
|
|
|
|
|
|
this.$message.success(`成功解析Excel,共读取到 ${this.rawData.length} 条数据`);
|
|
|
|
|
|
} catch (error) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.handleError(`解析Excel失败:${error.message}`);
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.handleError(`读取文件失败:${error.message}`);
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 校验表头是否匹配
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateHeaders(headers) {
|
|
|
|
|
|
this.errorList = [];
|
2025-12-02 13:56:21 +08:00
|
|
|
|
const { REQUIRED_HEADERS } = SystemConstant;
|
|
|
|
|
|
|
|
|
|
|
|
// 校验表头数量
|
|
|
|
|
|
if (headers.length !== REQUIRED_HEADERS.length) {
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum: 1,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
errorMsg: ErrorType.HEADER_COUNT_ERROR.message(REQUIRED_HEADERS.length, headers.length)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 校验表头名称
|
2025-12-01 17:33:20 +08:00
|
|
|
|
headers.forEach((header, index) => {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
if (header !== REQUIRED_HEADERS[index]) {
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.errorList.push({
|
|
|
|
|
|
rowNum: 1,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
errorMsg: ErrorType.HEADER_NAME_ERROR.message(index, REQUIRED_HEADERS[index], header)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (this.errorList.length > 0) {
|
|
|
|
|
|
this.$message.error('Excel表头格式不符合要求,请检查!');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-02 13:56:21 +08:00
|
|
|
|
* 校验Excel数据格式(唯一校验入口,添加防重复点击)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
*/
|
2025-12-02 13:56:21 +08:00
|
|
|
|
async handleValidate() {
|
|
|
|
|
|
// 防重复点击校验
|
|
|
|
|
|
if (this.validateLoading) {
|
|
|
|
|
|
this.$message.warning('正在校验数据,请稍候...');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.file) {
|
|
|
|
|
|
this.$message.warning('请先选择Excel文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标记为已点击过校验
|
|
|
|
|
|
this.isValidated = true;
|
|
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
if (this.rawData.length === 0) {
|
|
|
|
|
|
this.$message.warning('暂无数据可校验');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.validateLoading = true; // 开启校验loading
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.errorList = [];
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 执行各类校验器
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 校验物料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)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
if (this.errorList.length > 0) {
|
|
|
|
|
|
this.$message.error(`数据校验失败,共发现${this.errorList.length}条错误`);
|
2025-12-01 17:33:20 +08:00
|
|
|
|
} else {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.$message.success('数据校验通过,可以开始导入');
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
2025-12-02 13:56:21 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.handleError(`校验数据时发生错误:${error.message}`);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.validateLoading = false; // 关闭校验loading
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化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) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
const { HEADER_MAP, REQUIRED_HEADERS } = SystemConstant;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
const rowObj = {};
|
2025-12-02 13:56:21 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 映射表头和字段
|
2025-12-02 13:56:21 +08:00
|
|
|
|
REQUIRED_HEADERS.forEach((header, index) => {
|
|
|
|
|
|
const field = HEADER_MAP[header];
|
2025-12-01 17:33:20 +08:00
|
|
|
|
rowObj[field] = row[index] ? row[index].toString().trim() : '';
|
|
|
|
|
|
});
|
2025-12-02 13:56:21 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 重量转数字
|
|
|
|
|
|
rowObj.weight = rowObj.weight ? Number(rowObj.weight) : 0;
|
|
|
|
|
|
// 增加行号
|
|
|
|
|
|
rowObj.rowNum = rowNum;
|
2025-12-02 13:56:21 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
return rowObj;
|
|
|
|
|
|
},
|
2025-12-01 11:56:31 +08:00
|
|
|
|
|
2025-12-01 17:33:20 +08:00
|
|
|
|
/**
|
2025-12-02 13:56:21 +08:00
|
|
|
|
* 开始导入数据(添加防重复点击和二次确认)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
*/
|
2025-12-02 13:56:21 +08:00
|
|
|
|
async startImport() {
|
|
|
|
|
|
// 防重复点击导入
|
|
|
|
|
|
if (this.importLoading) {
|
|
|
|
|
|
this.$message.warning('正在导入数据,请勿重复操作...');
|
2025-12-01 17:33:20 +08:00
|
|
|
|
return;
|
2025-12-01 11:56:31 +08:00
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
if (!this.file || this.tableData.length === 0) {
|
|
|
|
|
|
this.$message.warning('暂无数据可导入');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 二次确认,防止误操作
|
|
|
|
|
|
const confirm = await this.$confirm(
|
|
|
|
|
|
'确认导入已校验通过的数据?导入过程中请勿刷新页面或关闭浏览器!',
|
|
|
|
|
|
'导入确认',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确认导入',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
dangerouslyUseHTMLString: true
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
2025-12-02 13:56:21 +08:00
|
|
|
|
).catch(() => false);
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirm) return;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.importLoading = true; // 开启导入loading
|
2025-12-01 17:33:20 +08:00
|
|
|
|
// 初始化导入状态
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.importStatus = ImportStatus.PROCESSING;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.progress = 0;
|
|
|
|
|
|
this.importedCount = 0;
|
|
|
|
|
|
this.totalCount = this.tableData.length;
|
|
|
|
|
|
this.importErrorMsg = '';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.batchImport();
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.importStatus = ImportStatus.FINISHED;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.$message.success(`导入完成!共成功导入${this.importedCount}条数据`);
|
|
|
|
|
|
} catch (error) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.handleError(`导入失败:${error.message},已导入${this.importedCount}条数据`);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.importLoading = false; // 关闭导入loading
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量导入数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
async batchImport() {
|
|
|
|
|
|
for (let i = 0; i < this.tableData.length; i++) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 若导入已失败,终止后续操作
|
|
|
|
|
|
if (this.importStatus === ImportStatus.ERROR) break;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
|
|
|
|
|
const row = this.tableData[i];
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.importOneRecord(row);
|
|
|
|
|
|
this.importedCount++;
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 更新进度(避免进度跳动过大)
|
|
|
|
|
|
const currentProgress = Math.round(((i + 1) / this.totalCount) * 100);
|
|
|
|
|
|
this.progress = currentProgress;
|
|
|
|
|
|
// 给浏览器渲染时间,避免卡死
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
2025-12-01 17:33:20 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw new Error(`第${row.rowNum}行导入失败:${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导入单条记录
|
|
|
|
|
|
*/
|
|
|
|
|
|
async importOneRecord(row) {
|
|
|
|
|
|
try {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 1. 查找物料ID(再次校验,防止数据篡改)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
const itemId = await this._findItemId(row);
|
|
|
|
|
|
if (!itemId) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
throw new Error(ErrorType.ITEM_ID_NOT_FOUND.message(row.type, row.name, row.specification, row.material, row.surfaceTreatmentDesc, row.manufacturer, row.zincLayer));
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
const itemType = SystemConstant.ITEM_TYPE_MAP[row.type];
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 2. 插入钢卷数据
|
2025-12-01 17:33:20 +08:00
|
|
|
|
const coilParams = {
|
|
|
|
|
|
itemId,
|
|
|
|
|
|
itemType,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
materialType: row.type,
|
|
|
|
|
|
warehouseId: this.warehouseMap[row.logicWarehouse],
|
|
|
|
|
|
enterCoilNo: row.inboundCoilNo,
|
|
|
|
|
|
currentCoilNo: row.inboundCoilNo,
|
|
|
|
|
|
supplierCoilNo: row.factoryCoilNo,
|
|
|
|
|
|
grossWeight: row.weight,
|
|
|
|
|
|
netWeight: row.weight,
|
2025-12-01 17:33:20 +08:00
|
|
|
|
remark: row.remark,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
dataType: SystemConstant.COIL_DATA_TYPE
|
2025-12-01 17:33:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
const coilRes = await addMaterialCoil(coilParams);
|
2025-12-02 13:56:21 +08:00
|
|
|
|
if (coilRes.code !== 200) {
|
2025-12-01 17:33:20 +08:00
|
|
|
|
throw new Error(`钢卷数据插入失败:${coilRes.message || '接口返回异常'}`);
|
|
|
|
|
|
}
|
2025-12-02 13:56:21 +08:00
|
|
|
|
const coilId = coilRes.data?.coilId;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 3. 插入待处理操作
|
2025-12-01 17:33:20 +08:00
|
|
|
|
const actionParams = {
|
|
|
|
|
|
coilId,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
currentCoilNo: row.inboundCoilNo,
|
|
|
|
|
|
actionStatus: SystemConstant.PENDING_ACTION_STATUS,
|
2025-12-01 17:33:20 +08:00
|
|
|
|
itemType,
|
|
|
|
|
|
itemId,
|
2025-12-02 13:56:21 +08:00
|
|
|
|
actionType: SystemConstant.PENDING_ACTION_TYPE_RECEIVE,
|
|
|
|
|
|
operationType: SystemConstant.OPERATION_TYPE_IMPORT,
|
|
|
|
|
|
remark: `Excel导入:${row.inboundCoilNo}`,
|
|
|
|
|
|
warehouseId: this.planId
|
2025-12-01 17:33:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
const actionRes = await addPendingAction(actionParams);
|
2025-12-02 13:56:21 +08:00
|
|
|
|
if (actionRes.code !== 200) {
|
2025-12-01 17:33:20 +08:00
|
|
|
|
throw new Error(`待处理操作插入失败:${actionRes.message || '接口返回异常'}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw new Error(error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-02 13:56:21 +08:00
|
|
|
|
* 根据条件查找唯一的物料ID
|
2025-12-01 17:33:20 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async _findItemId(row) {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
const { ITEM_TYPE_MAP, ITEM_ID_FIELD } = SystemConstant;
|
|
|
|
|
|
const itemType = ITEM_TYPE_MAP[row.type];
|
|
|
|
|
|
if (!itemType) return null;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 构建查询参数
|
|
|
|
|
|
const queryParams = {
|
|
|
|
|
|
[row.type === '原料' ? 'rawMaterialName' : 'productName']: row.name,
|
|
|
|
|
|
specification: row.specification,
|
|
|
|
|
|
material: row.material,
|
|
|
|
|
|
manufacturer: row.manufacturer,
|
|
|
|
|
|
surfaceTreatmentDesc: row.surfaceTreatmentDesc,
|
|
|
|
|
|
zincLayer: row.zincLayer
|
|
|
|
|
|
};
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 执行查询
|
|
|
|
|
|
const res = itemType === 'raw_material'
|
|
|
|
|
|
? await listRawMaterial(queryParams)
|
|
|
|
|
|
: await listProduct(queryParams);
|
|
|
|
|
|
|
|
|
|
|
|
// 空值判断函数
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
2025-12-01 17:33:20 +08:00
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
// 必须返回唯一记录
|
|
|
|
|
|
if (matchedRecords.length !== 1) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 返回物料ID
|
|
|
|
|
|
return matchedRecords[0][ITEM_ID_FIELD[itemType]];
|
2025-12-01 17:33:20 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(`查询${row.type}信息失败:${error.message}`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-02 13:56:21 +08:00
|
|
|
|
* 重置所有状态(添加防呆和二次确认)
|
2025-12-01 17:33:20 +08:00
|
|
|
|
*/
|
2025-12-02 13:56:21 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置所有状态
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.file = null;
|
|
|
|
|
|
this.rawData = [];
|
|
|
|
|
|
this.tableData = [];
|
|
|
|
|
|
this.errorList = [];
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.isValidated = false;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.progress = 0;
|
|
|
|
|
|
this.importedCount = 0;
|
|
|
|
|
|
this.totalCount = 0;
|
2025-12-02 13:56:21 +08:00
|
|
|
|
this.importStatus = ImportStatus.IDLE;
|
2025-12-01 17:33:20 +08:00
|
|
|
|
this.importErrorMsg = '';
|
2025-12-02 13:56:21 +08:00
|
|
|
|
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;
|
2025-12-01 11:56:31 +08:00
|
|
|
|
}
|
2025-12-02 13:56:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
this.getWarehouseMap();
|
|
|
|
|
|
},
|
2025-12-01 17:33:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.import-wizard-container {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-upload-area {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
2025-12-02 13:56:21 +08:00
|
|
|
|
flex-wrap: wrap; /* 适配小屏幕 */
|
2025-12-01 17:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 13:56:21 +08:00
|
|
|
|
.import-finished,
|
|
|
|
|
|
.import-error {
|
2025-12-01 17:33:20 +08:00
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
2025-12-02 13:56:21 +08:00
|
|
|
|
|
|
|
|
|
|
/* 优化按钮间距,适配小屏幕 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.file-upload-area {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-01 17:33:20 +08:00
|
|
|
|
</style>
|