From a77e57993b5e5034937018602f914a4b33542660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=B8=E7=86=9F=E5=90=B4=E5=BD=A6=E7=A5=96?= Date: Wed, 2 Sep 2026 09:26:17 +0800 Subject: [PATCH] =?UTF-8?q?input=E9=95=BF=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/modules/order/poOrder.vue | 356 +++++++++--------- .../modules/order/poOrderFieldSchemaHelper.js | 23 -- 2 files changed, 177 insertions(+), 202 deletions(-) diff --git a/src/views/modules/order/poOrder.vue b/src/views/modules/order/poOrder.vue index 540d589..4f2d599 100644 --- a/src/views/modules/order/poOrder.vue +++ b/src/views/modules/order/poOrder.vue @@ -50,7 +50,7 @@ 取消 新增 - + 导入 - - -
- 请选择文件 - - - 选择文件 - - 上传 - - 下载模板 - -
- + -
- 列映射: - - {{ item.header }} → {{ item.fieldCode }} - + 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 => {