diff --git a/src/views/modules/order/poOrderStatusOptions.js b/src/views/modules/order/poOrderStatusOptions.js index 06b86a8..04e8281 100644 --- a/src/views/modules/order/poOrderStatusOptions.js +++ b/src/views/modules/order/poOrderStatusOptions.js @@ -9,6 +9,7 @@ export const PO_ORDER_STATUS_OPTIONS = [ { label: '验货完成', value: '验货完成' }, { label: '部分发货', value: '部分发货' }, { label: '已发货', value: '已发货' }, + { label: '完全发货', value: '完全发货' }, { label: '已取消', value: '已取消' }, { label: '已关闭', value: '已关闭' } ] diff --git a/src/views/modules/sys/initialImport.vue b/src/views/modules/sys/initialImport.vue index 4c55d3b..6d1c230 100644 --- a/src/views/modules/sys/initialImport.vue +++ b/src/views/modules/sys/initialImport.vue @@ -66,9 +66,18 @@ 选择导入文件 + + 导出校验结果 + {{ fileName || '未选择文件' }} + 存在校验失败行,请先导出并修正后再保存 已读取 {{ previewRows.length }} 行,表格显示前{{ previewDisplayLimit }}行 + ,其中 {{ invalidPreviewCount }} 行校验失败 @@ -163,6 +172,7 @@ export default { previewLoading: false, saving: false, templateDownloading: false, + exporting: false, fileName: '', importFile: null, poCustomer: '', @@ -208,10 +218,28 @@ export default { return this.activeImportType === 'poOrder' }, activeColumns () { + let columns if (this.isPoOrderImport) { - return this.poOrderColumns - } - return this.activeImportType === 'partSku' ? this.partSkuColumns : this.supplierPriceColumns + columns = this.poOrderColumns + } else if (this.activeImportType === 'partSku') { + columns = this.partSkuColumns + } else { + columns = this.supplierPriceColumns + } + // 三个导入预览都把错误信息放在最后一列,和导出文件保持一致。 + const withoutError = (columns || []).filter(column => column.prop !== 'validationMessage') + return withoutError.concat([{ + prop: 'validationMessage', + label: '错误信息', + width: 280, + align: 'left' + }]) + }, + invalidPreviewCount () { + return this.previewRows.filter(row => this.isPreviewRowInvalid(row)).length + }, + hasInvalidPreviewRows () { + return this.invalidPreviewCount > 0 }, previewDisplayRows () { return this.previewRows.slice(0, this.previewDisplayLimit) @@ -226,16 +254,20 @@ export default { if (this.activeImportType === 'poOrder') { return '94%' } - return this.activeImportType === 'partSku' ? '680px' : '94%' + return this.activeImportType === 'partSku' ? '860px' : '94%' }, saveButtonText () { return '保存' }, saveDisabled () { + // 任意导入类型只要存在黄标行,就不允许继续保存。 + if (this.previewLoading || this.saving || !this.previewRows.length || this.hasInvalidPreviewRows) { + return true + } if (this.isPoOrderImport) { - return this.previewLoading || this.saving || !this.previewRows.length || !this.poCustomer + return !this.poCustomer } - return this.previewLoading || this.saving || !this.previewRows.length + return false } }, methods: { @@ -333,6 +365,7 @@ export default { : previewSupplierPrice(rawFile, site) }, buildPoOrderColumns (columnMapping) { + // 预览表按模板字段展示;物料编码、供应商编码分别由 Item、Vendor Name 解析得到。 const columns = Object.keys(columnMapping).map(prop => ({ prop, label: columnMapping[prop], @@ -341,8 +374,7 @@ export default { })) columns.push( { prop: 'part_no', label: '物料编码', width: 140, align: 'left' }, - { prop: 'supplier_no', label: '供应商编码', width: 140, align: 'left' }, - { prop: 'validationMessage', label: '校验结果', width: 220, align: 'left' } + { prop: 'supplier_no', label: '供应商编码', width: 140, align: 'left' } ) return columns }, @@ -418,17 +450,17 @@ export default { if (!this.ensureInitialImportAvailable()) { return } - if (this.isPoOrderImport) { - const invalidRows = this.previewRows.filter(row => !row || row.valid === false) - if (invalidRows.length) { - this.$message.warning(`有${invalidRows.length}行校验未通过,请修正Excel后重新预览`) - return - } - this.performPoOrderImport() + if (!this.previewRows.length) { + this.$message.warning('请先选择导入文件并完成预览') return } - if (!this.previewRows.length) { - this.$message.warning('没有可保存的导入数据') + // 三个导入统一:有黄标行一律禁止保存,避免把失败数据写入系统。 + if (this.hasInvalidPreviewRows) { + this.$message.warning(`有${this.invalidPreviewCount}行校验未通过,请修正Excel后重新预览。可先导出校验结果`) + return + } + if (this.isPoOrderImport) { + this.performPoOrderImport() return } const site = this.$store.state.user.site @@ -436,21 +468,6 @@ export default { this.$message.error('当前登录用户没有Site') return } - const invalidCount = this.previewRows.filter(row => row && row.valid === false).length - if (invalidCount > 0) { - this.$confirm( - `存在${invalidCount}行淡黄色数据。如果继续保存,无效数据行将跳过,是否继续?`, - '数据校验提示', - { - confirmButtonText: '继续保存', - cancelButtonText: '取消', - type: 'warning' - } - ).then(() => { - this.performSave(site) - }).catch(() => {}) - return - } this.performSave(site) }, performSave (site) { @@ -531,8 +548,10 @@ export default { if (this.missingItems.length) { messages.push(`以下Item不存在:${this.missingItems.join('、')}`) } + // PO 导入按 Vendor Name 查供应商;价格期初仍按 ShortNPC 提示。 if ((this.activeImportType === 'supplierPrice' || this.isPoOrderImport) && this.missingSuppliers.length) { - messages.push(`以下ShortNPC不存在:${this.missingSuppliers.join('、')}`) + const supplierLabel = this.isPoOrderImport ? 'Vendor Name' : 'ShortNPC' + messages.push(`以下${supplierLabel}不存在:${this.missingSuppliers.join('、')}`) } const content = this.$createElement( 'div', @@ -545,7 +564,128 @@ export default { }).catch(() => {}) }, previewRowClassName ({ row }) { - return row && row.valid === false ? 'initial-import-warning-row' : '' + return this.isPreviewRowInvalid(row) ? 'initial-import-warning-row' : '' + }, + /** + * 预览行是否校验失败。只认 valid === false,与页面黄标规则一致。 + */ + isPreviewRowInvalid (row) { + return !!(row && row.valid === false) + }, + /** + * 导出当前预览数据及校验结果。 + * 触发条件:已完成预览且存在数据行。 + * 结果:下载 Excel,失败行整行标黄,最后一列为错误信息。 + */ + handleExportPreview () { + if (!this.previewRows.length) { + this.$message.warning('请先选择导入文件并完成预览') + return + } + this.exporting = true + try { + const columns = this.buildPreviewExportColumns() + const xml = this.buildPreviewExportXml(columns, this.previewRows) + const blob = new Blob(['\ufeff' + xml], { type: 'application/vnd.ms-excel;charset=utf-8' }) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = this.resolvePreviewExportFileName() + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + this.$message.success('校验结果已导出') + } catch (error) { + this.$message.error((error && error.message) || '导出失败,请稍后重试') + } finally { + this.exporting = false + } + }, + /** + * 导出列与预览表一致:PO 额外带 Excel 行号,最后一列固定为错误信息。 + */ + buildPreviewExportColumns () { + const columns = [] + if (this.isPoOrderImport) { + columns.push({ prop: 'excelRowNo', label: 'Excel行号' }) + } + return columns.concat(this.activeColumns || []) + }, + resolvePreviewExportFileName () { + const now = new Date() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hour = String(now.getHours()).padStart(2, '0') + const minute = String(now.getMinutes()).padStart(2, '0') + const second = String(now.getSeconds()).padStart(2, '0') + return `${this.dialogTitle}校验结果_${now.getFullYear()}${month}${day}${hour}${minute}${second}.xls` + }, + /** + * 生成带行底色的 SpreadsheetML,Excel 打开后失败行仍为黄色。 + * 入参 columns 为导出列;rows 为当前全部预览行(不只表格显示的前 500 行)。 + */ + buildPreviewExportXml (columns, rows) { + const headerCells = columns.map(column => { + const width = column.prop === 'validationMessage' ? 280 : 80 + return `` + }).join('') + const headerRow = '' + columns.map(column => { + return `${this.escapeExcelXml(column.label)}` + }).join('') + '' + const dataRows = (rows || []).map(row => { + const styleAttr = this.isPreviewRowInvalid(row) ? ' ss:StyleID="Warn"' : '' + const cells = columns.map(column => { + const value = this.formatExportCellValue(row, column.prop) + return `${this.escapeExcelXml(value)}` + }).join('') + return `${cells}` + }).join('') + return ` + + + + + + + + + ${headerCells}${headerRow}${dataRows}
+
+
` + }, + /** + * 导出单元格文本。空值输出空串,避免 Excel 出现 undefined。 + */ + formatExportCellValue (row, prop) { + if (!row || !prop) { + return '' + } + const value = row[prop] + if (value == null || value === '') { + return '' + } + return String(value) + }, + /** + * SpreadsheetML 特殊字符转义,防止错误信息里的符号破坏 XML。 + */ + escapeExcelXml (value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') }, handleDialogClose (done) { if (!this.saving) { @@ -564,6 +704,7 @@ export default { this.previewLoading = false this.saving = false this.templateDownloading = false + this.exporting = false this.previewRows = [] this.missingItems = [] this.missingSuppliers = [] @@ -626,6 +767,11 @@ export default { white-space: nowrap; } +.invalid-hint { + color: #e6a23c; + font-size: 13px; +} + .preview-count { margin-left: auto; color: #606266;