+ title="请先下载模板并填写数据;导入时会先校验第一行栏位,顺序可以调整。"
+ style="margin-bottom: 12px;" />
+
-
保存
-
关闭
+
+
+
+ 导入结果:成功 {{ importLastResult.successCount || 0 }} 条,失败 {{ importLastResult.failCount || 0 }} 条
+
+
+
+ 第{{ item.rowNo }}行:{{ item.message }}
+
+
+ 仅展示前20条失败原因,请按失败提示分批修复后重试。
+
+
+
+
+
@@ -618,7 +629,6 @@ import {
applyFieldSchemaToColumns,
getAdvancedQueryFieldsFromSchema,
getEditableExtFieldsFromSchema,
- buildImportPreviewColumns,
normalizeSchemaRowValues,
buildExtValuesFromRow
} from './poOrderFieldSchemaHelper.js'
@@ -683,8 +693,8 @@ const ADD_SELECT_OPTIONS_FIELD_CODE_MAP = {
* - fieldSchemaFields/baseColumnList/columnList: 字段schema与表格列配置(动态列渲染核心)
* - dictSelectOptionsMap/addDictSelectOptionsMap: 字典下拉选项缓存(列表/新增分开缓存)
* - columnSelectOptions: 常用下拉配置集合(客户、买手、状态、是/否、运输方式等)
- * - uploadDialogVisible/uploadFile/uploadPreviewList/uploadColumnMapping/uploadPreviewColumns/uploadSkippedColumns:
- * 导入流程全量状态
+ * - importDialogVisible/importFile/importSubmitting/importLastResult:
+ * 导入流程状态(模板下载、导入执行、失败反馈)
* - addDialogVisible/addForm/addFormSchemaFields/addFormSaving:
* 新增主弹窗状态(主记录)
* - addDetailDialogVisible/addDetailDialogMode/addDetailForm/addDetailRows/addDetailCurrentDraftId/addDetailEditingDraftId:
@@ -697,7 +707,7 @@ const ADD_SELECT_OPTIONS_FIELD_CODE_MAP = {
* 行编辑阶段的辅助暂存(用于复杂选择组件联动)
*
* 二、模态框与子弹层说明
- * - uploadDialogVisible: PO订单导入(文件上传、预览、批量保存)
+ * - importDialogVisible: PO订单导入(动态模板 + 逐行导入)
* - addDialogVisible: 新增PO订单(主记录 + 明细草稿)
* - addDetailDialogVisible: 新增明细(新增/编辑/复制)
* - supplierPartDialogVisible: 供应商可供物料选择
@@ -744,8 +754,8 @@ const ADD_SELECT_OPTIONS_FIELD_CODE_MAP = {
* - getTransportOptions, handleSupplierChange
*
* I. 导入导出
- * - exportModal, handleFileChange, previewUpload, refreshUploadPreviewColumns
- * - confirmUpload, downloadTemplate, exportExcel
+ * - openImportDialog, handleImportFileChange, submitImportCreatePoOrder
+ * - downloadImportTemplate, resetImportDialog, exportExcel
*
* J. 新增流程(主记录 + 明细)
* - resolveAddOrderTypeByCustomer, openAddDialog, loadAddFieldSchema
@@ -809,14 +819,12 @@ export default {
displayOnlyColumnProps: ['shortOffice', 'shortNpc', 'item', 'categoryName', 'sku', 'planner'],
customEditColumnProps: [],
- // ===== 导入弹窗状态 =====
- uploadDialogVisible: false,
- uploadFileName: '',
- uploadFile: null,
- uploadPreviewList: [],
- uploadSkippedColumns: [],
- uploadColumnMapping: {},
- uploadPreviewColumns: [],
+ // ===== 导入弹窗状态(新导入:模板下载 + 直接执行)=====
+ importDialogVisible: false,
+ importFileName: '',
+ importFile: null,
+ importSubmitting: false,
+ importLastResult: null,
// ===== 新增主弹窗 + 明细弹窗状态 =====
addDialogVisible: false,
@@ -1986,15 +1994,6 @@ export default {
return '复制明细'
}
return '新增明细'
- },
- uploadColumnMappingEntries () {
- if (!this.uploadColumnMapping || typeof this.uploadColumnMapping !== 'object') {
- return []
- }
- return Object.keys(this.uploadColumnMapping).map(header => ({
- header,
- fieldCode: this.uploadColumnMapping[header]
- }))
}
},
created () {
@@ -3391,69 +3390,45 @@ export default {
})
},
- // ========== 上传功能 ==========
- // 打开上传弹窗
- exportModal() {
+ // ========== 导入功能(新实现) ==========
+ /**
+ * 打开导入弹窗并重置状态。
+ * 业务意图:
+ * 1) 每次打开都清空上一次文件与结果,避免误用历史数据;
+ * 2) 强制用户先选 customer,确保模板字段与导入权限与 customer 一致。
+ */
+ openImportDialog () {
if (!this.searchData.customer) {
this.$message.warning('请先选择 Customer')
return
}
- this.uploadDialogVisible = true
- this.uploadFileName = ''
- this.uploadFile = null
- this.uploadPreviewList = []
- this.uploadSkippedColumns = []
- this.uploadColumnMapping = {}
- this.uploadPreviewColumns = []
- },
- // 文件选择变化
- handleFileChange(file) {
- this.uploadFile = file.raw
- this.uploadFileName = file.name
- },
- // 预览上传数据
- previewUpload() {
- if (!this.uploadFile) {
- this.$message.warning('请先选择文件')
+ this.resetImportDialog()
+ this.importDialogVisible = true
+ },
+ /**
+ * 导入弹窗关闭后的统一清理。
+ * 边界处理:导入进行中禁止被重置,避免中途清空文件状态导致重复提交。
+ */
+ resetImportDialog () {
+ if (this.importSubmitting) {
return
}
- if (!this.searchData.customer) {
- this.$message.warning('请先选择 Customer')
- return
+ if (this.$refs.importUpload && this.$refs.importUpload.clearFiles) {
+ this.$refs.importUpload.clearFiles()
}
-
- const formData = new FormData()
- formData.append('file', this.uploadFile)
- formData.append('site', this.searchData.site || this.$store.state.user.site)
- formData.append('customer', this.searchData.customer)
-
- this.$http({
- url: this.$http.adornUrl('/pODetail/previewUpload'),
- method: 'post',
- data: formData
- }).then(({ data }) => {
- if (data && data.code === 0) {
- const preview = data.data || {}
- this.uploadPreviewList = preview.rows || []
- this.uploadSkippedColumns = preview.skippedColumns || []
- this.uploadColumnMapping = preview.columnMapping || {}
- this.refreshUploadPreviewColumns()
- this.$message.success('文件解析成功,请确认数据后点击保存')
- } else {
- this.$message.error(data.msg || '文件解析失败')
- }
- }).catch((error) => {
- console.error('文件解析失败:', error)
- const errorMsg = (error.response && error.response.data && error.response.data.msg) || error.message || '未知错误'
- this.$message.error('文件解析失败: ' + errorMsg)
- })
+ this.importFileName = ''
+ this.importFile = null
+ this.importLastResult = null
},
- refreshUploadPreviewColumns () {
- this.uploadPreviewColumns = buildImportPreviewColumns(
- this.uploadPreviewList,
- this.fieldSchemaFields,
- this.uploadColumnMapping
- )
+ /**
+ * 记录用户选择的导入文件。
+ * 输入:el-upload change 事件文件对象。
+ * 输出:仅保留当前一次选择,后续由“开始导入”统一提交。
+ */
+ handleImportFileChange (file) {
+ this.importFile = file && file.raw ? file.raw : null
+ this.importFileName = file && file.name ? file.name : ''
+ this.importLastResult = null
},
exportExcel () {
if (this.editMode) {
@@ -3499,55 +3474,15 @@ export default {
this.$message.error('导出失败: ' + error.message)
}
},
- // 确认上传
- confirmUpload() {
- if (this.uploadPreviewList.length === 0) {
- this.$message.warning('没有可保存的数据')
+ /**
+ * 下载导入模板。
+ * 业务规则:模板由后端按 site + customer 当前可见字段动态生成,第一行即校验基准栏位。
+ */
+ downloadImportTemplate () {
+ if (!this.searchData.customer) {
+ this.$message.warning('请先选择 Customer')
return
}
-
- this.$confirm(`确认保存 ${this.uploadPreviewList.length} 条数据吗?`, '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(() => {
- const formData = new FormData()
- formData.append('file', this.uploadFile)
- formData.append('site', this.searchData.site || this.$store.state.user.site)
- formData.append('customer', this.searchData.customer)
-
- this.$http({
- url: this.$http.adornUrl('/pODetail/batchSave'),
- method: 'post',
- data: formData
- }).then(({ data }) => {
- if (data && data.code === 0) {
- const result = data.data || {}
- const successCount = result.successCount || 0
- const failCount = result.failCount || 0
- let msg = `导入完成:成功 ${successCount} 条,失败 ${failCount} 条`
- if (result.rowErrors && result.rowErrors.length) {
- msg += ';' + result.rowErrors.slice(0, 3).map(e => `第${e.rowNo}行 ${e.message}`).join(';')
- }
- if (failCount > 0) {
- this.$message.warning(msg)
- } else {
- this.$message.success(msg)
- }
- this.uploadDialogVisible = false
- this.getDataList()
- } else {
- this.$message.error(data.msg || '数据保存失败')
- }
- }).catch((error) => {
- console.error('数据保存失败:', error)
- const errorMsg = (error.response && error.response.data && error.response.data.msg) || error.message || '未知错误'
- this.$message.error('数据保存失败: ' + errorMsg)
- })
- }).catch(() => {})
- },
- // 下载模板
- downloadTemplate() {
const loading = this.$loading({
lock: true,
text: '正在下载模板...',
@@ -3558,6 +3493,10 @@ export default {
this.$http({
url: this.$http.adornUrl('/pODetail/downloadTemplate'),
method: 'get',
+ params: {
+ site: this.searchData.site || this.$store.state.user.site,
+ customer: this.searchData.customer
+ },
responseType: 'blob'
}).then(response => {
loading.close()
@@ -3570,7 +3509,7 @@ export default {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
- link.download = 'PO订单导入模板.xlsx'
+ link.download = `PO订单导入模板_${this.searchData.customer}.xlsx`
document.body.appendChild(link)
link.click()
@@ -3585,6 +3524,65 @@ export default {
this.$message.error('模板下载失败,请联系管理员')
})
},
+ /**
+ * 执行导入(后端逐行创建 header + detail)。
+ * 关键约束:
+ * 1) 导入期间统一禁用弹窗按钮,避免重复发起;
+ * 2) 请求超时时间单独提升为 10 分钟,兼容大批量(数千行)导入;
+ * 3) 返回成功/失败条数与行级错误,便于用户按行修复。
+ */
+ submitImportCreatePoOrder () {
+ if (!this.importFile) {
+ this.$message.warning('请先选择导入文件')
+ return
+ }
+ if (!this.searchData.customer) {
+ this.$message.warning('请先选择 Customer')
+ return
+ }
+ if (this.importSubmitting) {
+ return
+ }
+ const formData = new FormData()
+ formData.append('file', this.importFile)
+ formData.append('site', this.searchData.site || this.$store.state.user.site)
+ formData.append('customer', this.searchData.customer)
+
+ this.importSubmitting = true
+ this.importLastResult = null
+ this.$http({
+ url: this.$http.adornUrl('/pODetail/importCreatePoOrder'),
+ method: 'post',
+ data: formData,
+ timeout: 10 * 60 * 1000
+ }).then(({ data }) => {
+ if (!(data && data.code === 0)) {
+ this.$message.error((data && data.msg) || '导入失败')
+ return
+ }
+ const result = data.data || {}
+ this.importLastResult = result
+ const successCount = Number(result.successCount || 0)
+ const failCount = Number(result.failCount || 0)
+ let msg = `导入完成:成功 ${successCount} 条,失败 ${failCount} 条`
+ if (result.rowErrors && result.rowErrors.length) {
+ msg += ';' + result.rowErrors.slice(0, 3).map(item => `第${item.rowNo}行 ${item.message}`).join(';')
+ }
+ if (failCount > 0) {
+ this.$message.warning(msg)
+ } else {
+ this.$message.success(msg)
+ }
+ if (successCount > 0) {
+ this.getDataList()
+ }
+ }).catch((error) => {
+ const errorMsg = (error.response && error.response.data && error.response.data.msg) || error.message || '未知错误'
+ this.$message.error('导入失败: ' + errorMsg)
+ }).finally(() => {
+ this.importSubmitting = false
+ })
+ },
// 打开供应商选择弹窗
openSupplierSelect(row) {
diff --git a/src/views/modules/order/poOrderFieldSchemaHelper.js b/src/views/modules/order/poOrderFieldSchemaHelper.js
index 4f72560..a274273 100644
--- a/src/views/modules/order/poOrderFieldSchemaHelper.js
+++ b/src/views/modules/order/poOrderFieldSchemaHelper.js
@@ -127,29 +127,6 @@ export function buildExtValuesFromRow (row, schemaFields) {
return values
}
-/** 根据 fieldSchema 构建导入预览列 */
-export function buildImportPreviewColumns (rows, schemaFields, columnMapping) {
- const propSet = new Set()
- if (columnMapping && typeof columnMapping === 'object') {
- Object.values(columnMapping).forEach(code => propSet.add(code))
- }
- if (Array.isArray(rows)) {
- rows.forEach(row => {
- if (row && typeof row === 'object') {
- Object.keys(row).forEach(k => propSet.add(k))
- }
- })
- }
- const schemaMap = buildSchemaFieldMap(schemaFields)
- return Array.from(propSet).map(fieldCode => {
- const field = schemaMap[fieldCode] || schemaMap[PoFieldCodeToProp(fieldCode)]
- return {
- prop: fieldCode,
- label: field ? field.fieldLabel : fieldCode
- }
- })
-}
-
function buildSchemaMap (schemaFields) {
const schemaMap = {}
schemaFields.forEach(f => {