Browse Source

input长度

master
常熟吴彦祖 1 month ago
parent
commit
a77e57993b
  1. 356
      src/views/modules/order/poOrder.vue
  2. 23
      src/views/modules/order/poOrderFieldSchemaHelper.js

356
src/views/modules/order/poOrder.vue

@ -50,7 +50,7 @@
</el-button>
<el-button v-if="editMode" style="margin-left: 2px" @click="cancelEditMode">取消</el-button>
<el-button type="primary" style="margin-left: 2px" @click="openAddDialog" v-if="!editMode">新增</el-button>
<!-- <el-button type="primary" style="margin-left: 2px" @click="exportModal">导入</el-button>-->
<el-button type="primary" style="margin-left: 2px" @click="openImportDialog" v-if="!editMode">导入</el-button>
</el-form-item>
</el-form>
<el-table
@ -237,64 +237,75 @@
</el-pagination>
<!--
模态框 #1:PO订单导入弹窗
用途:
1) 选择 Excel 文件;
2) 预览后端解析结果(含字段映射、跳过列提示);
3) 确认后批量保存。
模态框 #1:PO订单导入弹窗(按 customer 权限动态模板)
交互规则:
1) 模板第一行按当前 customer 可见字段顺序生成;
2) 上传后后端先校验第一行栏位,顺序允许调整,但栏位需匹配模板;
3) 导入执行中所有按钮与文件选择统一置灰,避免重复提交。
关联状态:
uploadDialogVisible / uploadFile / uploadPreviewList / uploadColumnMapping / uploadSkippedColumns
importDialogVisible / importFile / importSubmitting / importLastResult
-->
<el-dialog title="PO订单数据导入" v-drag :visible.sync="uploadDialogVisible" :height="height" width="800px">
<!-- 选择文件 -->
<div style="margin-bottom:15px;display:flex;align-items:center">
<span style="margin-right:10px">请选择文件</span>
<el-input v-model="uploadFileName" style="width:400px;margin-right:10px" readonly></el-input>
<el-upload ref="upload" action="/api/pODetail/upload"
:show-file-list="false" :on-change="handleFileChange"
:auto-upload="false">
<el-button type="primary">选择文件</el-button>
</el-upload>
<el-button type="success" style="margin-left:10px" @click="previewUpload">上传</el-button>
<el-button type="primary" @click="downloadTemplate" style="margin-left:10px">
<i class="el-icon-download"></i> 下载模板
</el-button>
</div>
<!-- 跳过列提示 -->
<el-dialog
title="PO订单导入"
v-drag
:visible.sync="importDialogVisible"
width="560px"
append-to-body
:close-on-click-modal="false"
:show-close="!importSubmitting"
:close-on-press-escape="!importSubmitting"
@closed="resetImportDialog">
<el-alert
v-if="uploadSkippedColumns.length"
type="warning"
type="info"
:closable="false"
show-icon
style="margin-bottom:10px"
:title="'已跳过列:' + uploadSkippedColumns.join('、')"
/>
<div v-if="uploadColumnMappingEntries.length" style="margin-bottom:10px;font-size:12px;color:#606266">
列映射:
<span v-for="item in uploadColumnMappingEntries" :key="item.header" style="margin-right:12px">
{{ item.header }} → {{ item.fieldCode }}
</span>
title="请先下载模板并填写数据;导入时会先校验第一行栏位,顺序可以调整。"
style="margin-bottom: 12px;" />
<div style="display:flex;align-items:center;">
<el-input
v-model="importFileName"
style="width:320px;margin-right:10px;"
:disabled="importSubmitting"
readonly
placeholder="请选择导入文件" />
<el-upload
ref="importUpload"
action="#"
:show-file-list="false"
:auto-upload="false"
:disabled="importSubmitting"
:on-change="handleImportFileChange">
<el-button type="primary" :disabled="importSubmitting">选择文件</el-button>
</el-upload>
</div>
<!-- 预览表格 -->
<el-table
:data="uploadPreviewList"
border
height="250"
style="width:100%;margin-top:10px"
>
<el-table-column
v-for="col in uploadPreviewColumns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
align="center"
min-width="100"
/>
</el-table>
<!-- 按钮 -->
<div style="margin-top:20px;text-align:center">
<el-button type="primary" @click="confirmUpload">保存</el-button>
<el-button @click="uploadDialogVisible=false" style="margin-right:30px">关闭</el-button>
<div v-if="importLastResult" style="margin-top: 14px; font-size: 13px; color: #606266;">
<div>
导入结果:成功 {{ importLastResult.successCount || 0 }} 条,失败 {{ importLastResult.failCount || 0 }} 条
</div>
<div
v-if="importLastResult.rowErrors && importLastResult.rowErrors.length"
style="margin-top: 8px; max-height: 160px; overflow: auto;">
<div
v-for="item in importLastResult.rowErrors.slice(0, 20)"
:key="`import-error-${item.rowNo}-${item.message}`"
style="line-height: 1.6;">
第{{ item.rowNo }}行:{{ item.message }}
</div>
<div v-if="importLastResult.rowErrors.length > 20" style="color:#909399;">
仅展示前20条失败原因,请按失败提示分批修复后重试。
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="downloadImportTemplate" :disabled="importSubmitting">下载模板</el-button>
<el-button
type="primary"
@click="submitImportCreatePoOrder"
:loading="importSubmitting"
:disabled="importSubmitting || !importFile">开始导入</el-button>
<el-button @click="importDialogVisible = false" :disabled="importSubmitting">关闭</el-button>
</div>
</el-dialog>
@ -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) {

23
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 => {

Loading…
Cancel
Save