|
|
|
@ -66,9 +66,18 @@ |
|
|
|
选择导入文件 |
|
|
|
</el-button> |
|
|
|
</el-upload> |
|
|
|
<el-button |
|
|
|
icon="el-icon-download" |
|
|
|
:loading="exporting" |
|
|
|
:disabled="previewLoading || saving || exporting || !previewRows.length" |
|
|
|
@click="handleExportPreview"> |
|
|
|
导出校验结果 |
|
|
|
</el-button> |
|
|
|
<span class="file-name" :title="fileName">{{ fileName || '未选择文件' }}</span> |
|
|
|
<span v-if="hasInvalidPreviewRows" class="invalid-hint">存在校验失败行,请先导出并修正后再保存</span> |
|
|
|
<span class="preview-count"> |
|
|
|
已读取 {{ previewRows.length }} 行<span v-if="previewRows.length > previewDisplayLimit">,表格显示前{{ previewDisplayLimit }}行</span> |
|
|
|
<span v-if="invalidPreviewCount">,其中 {{ invalidPreviewCount }} 行校验失败</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
@ -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后重新预览`) |
|
|
|
if (!this.previewRows.length) { |
|
|
|
this.$message.warning('请先选择导入文件并完成预览') |
|
|
|
return |
|
|
|
} |
|
|
|
this.performPoOrderImport() |
|
|
|
// 三个导入统一:有黄标行一律禁止保存,避免把失败数据写入系统。 |
|
|
|
if (this.hasInvalidPreviewRows) { |
|
|
|
this.$message.warning(`有${this.invalidPreviewCount}行校验未通过,请修正Excel后重新预览。可先导出校验结果`) |
|
|
|
return |
|
|
|
} |
|
|
|
if (!this.previewRows.length) { |
|
|
|
this.$message.warning('没有可保存的导入数据') |
|
|
|
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 `<Column ss:AutoFitWidth="0" ss:Width="${width}"/>` |
|
|
|
}).join('') |
|
|
|
const headerRow = '<Row ss:StyleID="Header">' + columns.map(column => { |
|
|
|
return `<Cell><Data ss:Type="String">${this.escapeExcelXml(column.label)}</Data></Cell>` |
|
|
|
}).join('') + '</Row>' |
|
|
|
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 `<Cell><Data ss:Type="String">${this.escapeExcelXml(value)}</Data></Cell>` |
|
|
|
}).join('') |
|
|
|
return `<Row${styleAttr}>${cells}</Row>` |
|
|
|
}).join('') |
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?> |
|
|
|
<?mso-application progid="Excel.Sheet"?> |
|
|
|
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" |
|
|
|
xmlns:o="urn:schemas-microsoft-com:office:office" |
|
|
|
xmlns:x="urn:schemas-microsoft-com:office:excel" |
|
|
|
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"> |
|
|
|
<Styles> |
|
|
|
<Style ss:ID="Default" ss:Name="Normal"> |
|
|
|
<Alignment ss:Vertical="Center" ss:WrapText="1"/> |
|
|
|
</Style> |
|
|
|
<Style ss:ID="Header"> |
|
|
|
<Font ss:Bold="1"/> |
|
|
|
<Interior ss:Color="#D9D9D9" ss:Pattern="Solid"/> |
|
|
|
</Style> |
|
|
|
<Style ss:ID="Warn"> |
|
|
|
<Interior ss:Color="#FFF7D6" ss:Pattern="Solid"/> |
|
|
|
</Style> |
|
|
|
</Styles> |
|
|
|
<Worksheet ss:Name="校验结果"> |
|
|
|
<Table>${headerCells}${headerRow}${dataRows}</Table> |
|
|
|
</Worksheet> |
|
|
|
</Workbook>` |
|
|
|
}, |
|
|
|
/** |
|
|
|
* 导出单元格文本。空值输出空串,避免 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, '>') |
|
|
|
.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; |
|
|
|
|