@@ -1,15 +1,7 @@
< template >
< 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
ref = "upload"
class = "upload-excel"
@@ -18,18 +10,19 @@
:show-file-list = "false"
:on-change = "handleFileChange"
accept = ".xlsx,.xls"
: disabled = "importStatus === 'process ing' "
: disabled = "importStatus === ImportStatus.PROCESSING || validateLoading || importLoad ing"
>
< el-button type = "primary" icon = "el-icon-upload2" > 选择Excel文件 < / el-button >
< / el-upload >
<!-- 操作按钮 -- >
<!-- 操作按钮 : 新增loading和防呆 -- >
< el-button
type = "success"
icon = "el-icon-check"
@click ="handleValidate"
v-if = "file && importMode === 'validate' && importStatus === 'idle' "
: disabled= "!file"
v-if = "file && importStatus === ImportStatus.IDLE "
: disabled= "!file || validateLoading "
:loading = "validateLoading"
>
校验数据
< / el-button >
@@ -37,8 +30,9 @@
type = "warning"
icon = "el-icon-circle-check"
@click ="startImport"
v-if = "file && ( importMode === 'direct' || (importMode === 'validate' && errorList.length === 0)) && importStatus === 'idle' "
: disabled = "!file || (importMode === 'v alidate' && errorList.length > 0) "
v-if = "file && importStatus === ImportStatus.IDLE "
: disabled = "!file || !isV alidated || errorList.length > 0 || importLoading "
:loading = "importLoading"
>
开始导入
< / el-button >
@@ -46,7 +40,7 @@
type = "default"
icon = "el-icon-refresh"
@click ="reset"
: disabled = "importStatus === 'processing' "
: disabled = "importStatus === ImportStatus.PROCESSING || validateLoading || importLoading "
>
重置
< / el-button >
@@ -54,12 +48,7 @@
<!-- 校验错误提示 -- >
< div v-if = "errorList.length > 0" class="error-list" >
< el -alert
title = "数据校验失败"
type = "error"
description = "以下行数据不符合格式要求,请修正后重新导入:"
show -icon
/ >
< 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 = "错误信息" / >
@@ -67,7 +56,7 @@
< / 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
title = "数据预览"
type = "info"
@@ -76,23 +65,19 @@
: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-column
v-for = "column in TableColumnEnum"
:key = " column. prop"
:prop = "column.prop"
:label = "column.label"
:width = "column.width"
:min-width = "column.minWidth"
/ >
< / el-table >
< / div >
<!-- 导入进度展示 -- >
< div v-if = "importStatus === 'processing' " class="import-progress" >
< div v-if = "importStatus === ImportStatus.PROCESSING " class="import-progress" >
< el -alert
title = "正在导入数据"
type = "warning"
@@ -105,7 +90,7 @@
< / div >
<!-- 导入完成提示 -- >
< div v-if = "importStatus === 'finished' " class="import-finished" >
< div v-if = "importStatus === ImportStatus.FINISHED " class="import-finished" >
< el -alert
title = "导入完成"
type = "success"
@@ -115,13 +100,8 @@
< / div >
<!-- 导入失败提示 -- >
< div v-if = "importStatus === 'error' " class="import-error" >
< el -alert
title = "导入失败"
type = "error"
:description = "importErrorMsg"
show -icon
/ >
< div v-if = "importStatus === ImportStatus.ERROR " class="import-error" >
< el -alert title = "导入失败" type = "error" :description = "importErrorMsg" show -icon / >
< / div >
< / div >
< / template >
@@ -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-直接导入
i mportMode : 'validate' ,
// 枚举挂载到实例,方便模板使用
I mportStatus ,
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文件内容
*/
@@ -203,30 +339,24 @@ export default {
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 . e rror( ` 解析Excel失败: ${ error . message } ` ) ;
this . importStatus = 'error' ;
this . importErrorMsg = ` 解析Excel失败: ${ error . message } ` ;
this . handleE rror( ` 解析Excel失败: ${ error . message } ` ) ;
}
} ;
} catch ( error ) {
this . $message . e rror( ` 读取文件失败: ${ error . message } ` ) ;
this . importStatus = 'error' ;
this . importErrorMsg = ` 读取文件失败: ${ error . message } ` ;
this . handleE rror( ` 读取文件失败: ${ 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 v alidateExcel ( ) {
async handleV alidate( ) {
// 防重复点击校验
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 ++ ) {
cons t row = this . rawData [ i ] ;
const rowNum = i + 2 ; // 数据行从第二行开始,行号=索引+2
const rowObj = this . formatRowData ( row , rowNum ) ;
try {
// 逐行校验
for ( le t 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 : 't ype' , 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 ) ;
ErrorT ype. 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 && ! [ '原料' , '成品' ] . include s( 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 ( ` 数据校验失败,共发现 ${ thi s. 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 . p rogress = Math . round ( ( ( i + 1 ) / this . totalCount ) * 100 ) ;
// 更新进度(避免进度跳动过大)
const currentP rogress = 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 ,
logicWarehous e: row . logicWarehous e,
inboundCoilNo : row . inboundCoilNo ,
factory CoilNo: row . factory CoilNo,
weight : row . weight ,
materialTyp e: row . typ e,
warehouseId : this . warehouseMap [ row . logicWarehouse ] ,
enter CoilNo: row . inbound CoilNo,
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 ,
oper ationType: 'import' , // 操作类型:导入
remark : ` Excel导入: ${ row . inboundCoilNo } `
ac tionType : 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 ( ) ;
} ,
} ;
< / script >
@@ -563,18 +724,12 @@ export default {
border - radius : 4 px ;
}
. import - mode - selector {
margin - bottom : 20 px ;
padding : 10 px ;
background : # f5f7fa ;
border - radius : 4 px ;
}
. file - upload - area {
margin - bottom : 20 px ;
display : flex ;
align - items : center ;
gap : 10 px ;
flex - wrap : wrap ; /* 适配小屏幕 */
}
. file - upload - area . disabled {
@@ -603,7 +758,16 @@ export default {
font - size : 14 px ;
}
. import - finished , . import - error {
. import - finished ,
. import - error {
margin - bottom : 20 px ;
}
/* 优化按钮间距,适配小屏幕 */
@ media ( max - width : 768 px ) {
. file - upload - area {
flex - direction : column ;
align - items : flex - start ;
}
}
< / style >