Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
2026-05-29 16:10:09 +08:00
2 changed files with 602 additions and 1 deletions

View File

@@ -26,6 +26,15 @@ export function addPlanDetail(data) {
})
}
// 批量新增排产单明细
export function addPlanDetailBatch(data) {
return request({
url: '/aps/planDetail/batch',
method: 'post',
data: data
})
}
// 修改排产单明细
export function updatePlanDetail(data) {
return request({

View File

@@ -10,6 +10,7 @@
<div>
<el-button type="primary" plain @click="handleAdd">新增明细</el-button>
<el-button type="success" plain @click="handleBatchAdd">批量新增</el-button>
<el-button type="info" plain @click="handleImport">导入</el-button>
<el-button type="danger" plain @click="handleBatchDelete"
:disabled="selectedRows.length === 0">批量删除</el-button>
<el-button type="warning" plain @click="handleBatchTransfer"
@@ -60,6 +61,11 @@
</el-table-column>
<!-- 订单信息 -->
<el-table-column label="排产日期" align="center" prop="detailDate" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.detailDate" style="background-color: #fff3e6;" />
</template>
</el-table-column>
<el-table-column label="订单号" align="center" prop="orderCode" width="200">
<template slot-scope="scope">
<el-input v-model="scope.row.orderCode" style="background-color: #fff3e6;">
@@ -491,16 +497,169 @@
:loading="buttonLoading">确认新增</el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog title="导入排产单明细" :visible.sync="importDialogVisible" width="900px" append-to-body
:close-on-click-modal="false" @close="resetImportState">
<div class="import-container">
<el-steps :active="importStep" align-center finish-status="success">
<el-step title="下载模板" description="下载并填写Excel模板" />
<el-step title="上传文件" description="选择填好的文件" />
<el-step title="校验数据" description="验证数据完整性" />
<el-step title="开始导入" description="预览并确认导入" />
</el-steps>
<div class="import-step-content">
<!-- Step 0: 下载模板 -->
<div v-show="importStep === 0" class="step-body">
<div class="step-download">
<i class="el-icon-download step-icon"></i>
<p>请先下载Excel模板按格式填写排产单明细数据</p>
<el-button type="primary" icon="el-icon-download" @click="downloadTemplate">下载导入模板</el-button>
</div>
</div>
<!-- Step 1: 上传文件 -->
<div v-show="importStep === 1" class="step-body">
<div class="step-upload">
<el-upload ref="importUpload" class="custom-upload" drag :auto-upload="false" :show-file-list="false"
:on-change="handleImportFileChange" accept=".xlsx,.xls">
<div class="upload-zone-body">
<div class="upload-zone-icon">
<i class="el-icon-upload2"></i>
</div>
<p class="upload-zone-title">将文件拖到此处<span>点击选择</span></p>
<p class="upload-zone-tip">支持 .xlsx / .xls 格式</p>
</div>
</el-upload>
<div v-if="importFile" class="parse-result" :class="errorList.length > 0 ? 'parse-error' : 'parse-ok'">
<div class="parse-result-header">
<i :class="errorList.length > 0 ? 'el-icon-warning' : 'el-icon-circle-check'"></i>
<span class="parse-result-filename">{{ importFile.name }}</span>
<span class="parse-result-count">解析 {{ rawData.length }} 条数据</span>
</div>
<div v-if="errorList.length > 0" class="error-list">
<el-table :data="errorList" border max-height="150">
<el-table-column prop="rowNum" label="行号" width="80" />
<el-table-column prop="errorMsg" label="错误信息" />
</el-table>
</div>
</div>
</div>
</div>
<!-- Step 2: 校验数据 -->
<div v-show="importStep === 2" class="step-body">
<div class="step-validate">
<p class="step-desc">点击下方按钮校验数据完整性已解析 {{ rawData.length }} 条数据</p>
<el-button type="success" icon="el-icon-check" @click="handleValidateData"
:loading="validateLoading" size="medium" style="margin-bottom: 16px">
校验数据
</el-button>
<div v-if="errorList.length > 0" class="error-list">
<el-alert :title="'校验失败,共 ' + errorList.length + ' 条错误'" type="error" show-icon :closable="false" style="margin-bottom: 10px" />
<el-table :data="errorList" border max-height="200">
<el-table-column prop="rowNum" label="行号" width="80" />
<el-table-column prop="errorMsg" label="错误信息" />
</el-table>
</div>
<div v-if="isValidated && errorList.length === 0" class="validate-ok">
<el-alert title="数据校验通过!" type="success" show-icon :closable="false" />
</div>
</div>
</div>
<!-- Step 3: 预览并导入 -->
<div v-show="importStep === 3" class="step-body">
<div class="step-import-container">
<div class="data-preview">
<div class="section-title">数据预览 {{ tableData.length }} </div>
<el-table :data="tableData" border max-height="280" style="margin-top: 10px">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="detailDate" label="排产日期" width="100" />
<el-table-column prop="contractCode" label="合同号" width="120" />
<el-table-column prop="deliveryDate" label="交货期" width="100" />
<el-table-column prop="customerName" label="客户名称" width="120" />
<el-table-column prop="productName" label="产品名称" width="100" show-overflow-tooltip />
<el-table-column prop="usageReq" label="客户用途" width="100" show-overflow-tooltip />
<el-table-column prop="productMaterial" label="材质" width="80" />
<el-table-column prop="rollingThick" label="成品厚度" width="80" />
<el-table-column prop="productWidth" label="成品宽度" width="80" />
<el-table-column prop="planWeight" label="数量(吨)" width="90" />
<el-table-column prop="surfaceTreatment" label="表面处理" width="80" />
<el-table-column prop="productPackaging" label="包装要求" width="80" />
<el-table-column prop="remark" label="其他要求" width="120" show-overflow-tooltip />
</el-table>
</div>
<div v-if="importStatus === 'processing'" class="import-progress">
<el-progress :percentage="importProgress" />
<p>正在导入{{ importedCount }} / {{ totalCount }}</p>
</div>
<div v-if="importStatus === 'finished'" class="import-result">
<el-alert :title="'导入完成!共成功导入 ' + importedCount + ' 条数据'" type="success" show-icon :closable="false" />
</div>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button v-if="importStep > 0" @click="importStep--" :disabled="importStatus === 'processing'">上一步</el-button>
<el-button @click="importDialogVisible = false">关闭</el-button>
<el-button v-if="importStep === 0" type="primary" @click="downloadTemplate">下载模板</el-button>
<el-button v-if="importStep === 0" type="primary" @click="importStep = 1">已下载下一步</el-button>
<el-button v-if="importStep === 1" type="primary" @click="goValidateStep" :disabled="!importFile || rawData.length === 0">
下一步
</el-button>
<el-button v-if="importStep === 2" type="primary" @click="goImportStep" :disabled="!isValidated || errorList.length > 0">
下一步
</el-button>
<el-button v-if="importStep === 3 && importStatus !== 'finished'" type="primary" @click="startImport"
:loading="importLoading">
开始导入
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { updatePlanDetail, listPlanDetail, addPlanDetail, delPlanDetail } from "@/api/aps/planDetail";
import * as XLSX from 'xlsx'
import { updatePlanDetail, listPlanDetail, addPlanDetail, addPlanDetailBatch, delPlanDetail } from "@/api/aps/planDetail";
import { getPlanSheet, listPlanSheet } from "@/api/aps/planSheet";
import { listOrder } from '@/api/crm/order';
import { listOrderItem } from '@/api/crm/orderItem'
import PlanSheetList from "@/views/aps/planSheet/PlanSheetList.vue";
const TEMPLATE_HEADERS = [
'排产日期', '合同号', '交货期', '业务员', '客户名称', '产品名称',
'客户用途', '材质', '成品厚度', '成品宽度', '数量(吨)',
'厚度范围', '宽度范围', '表面质量', '表面处理', '包装要求',
'切边要求', '其他要求'
]
const HEADER_MAP = {
'排产日期': 'detailDate',
'合同号': 'contractCode',
'交货期': 'deliveryDate',
'业务员': 'salesman',
'客户名称': 'customerName',
'产品名称': 'productName',
'客户用途': 'usageReq',
'材质': 'productMaterial',
'成品厚度': 'rollingThick',
'成品宽度': 'productWidth',
'数量(吨)': 'planWeight',
'厚度范围': 'markCoatThick',
'宽度范围': 'productEdgeReq',
'表面质量': 'coatingG',
'表面处理': 'surfaceTreatment',
'包装要求': 'productPackaging',
'切边要求': 'widthReq',
'其他要求': 'remark'
}
export default {
name: "PlanSheet",
dicts: ['sys_lines'],
@@ -625,6 +784,20 @@ export default {
selectedRows: [],
// 是否批量操作(批量转单)
isBatchTransfer: false,
// 导入对话框
importDialogVisible: false,
importStep: 0,
importFile: null,
rawData: [],
tableData: [],
errorList: [],
isValidated: false,
importProgress: 0,
importedCount: 0,
totalCount: 0,
importStatus: 'idle',
validateLoading: false,
importLoading: false,
};
},
created() {
@@ -1166,6 +1339,218 @@ export default {
this.$message.error('批量新增失败');
});
});
},
// 打开导入对话框
handleImport() {
this.importDialogVisible = true
this.resetImportState()
},
// 重置导入状态
resetImportState() {
this.importStep = 0
this.importFile = null
this.rawData = []
this.tableData = []
this.errorList = []
this.isValidated = false
this.importProgress = 0
this.importedCount = 0
this.totalCount = 0
this.importStatus = 'idle'
this.validateLoading = false
this.importLoading = false
this.$nextTick(() => {
if (this.$refs.importUpload) {
this.$refs.importUpload.clearFiles()
}
})
},
// 跳转到校验步骤
goValidateStep() {
if (!this.importFile || this.rawData.length === 0) return
this.importStep = 2
},
// 跳转到导入步骤
goImportStep() {
if (!this.isValidated || this.errorList.length > 0) return
this.importStep = 3
},
// 下载导入模板
downloadTemplate() {
const templateData = [
TEMPLATE_HEADERS,
['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
]
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet(templateData)
ws['!cols'] = TEMPLATE_HEADERS.map(() => ({ wch: 14 }))
XLSX.utils.book_append_sheet(wb, ws, '排产单明细')
XLSX.writeFile(wb, '排产单明细导入模板.xlsx')
},
// 导入文件选择变化
handleImportFileChange(file) {
this.isValidated = false
this.errorList = []
this.tableData = []
this.importStatus = 'idle'
this.importFile = file.raw
this.readExcel()
},
// 读取Excel文件内容
readExcel() {
if (!this.importFile) return
try {
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(this.importFile)
fileReader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
this.validateHeaders(jsonData[0])
this.rawData = jsonData.slice(1).filter(row => row.some(cell => cell !== undefined && cell !== null && cell !== ''))
this.formatExcel()
if (this.errorList.length === 0) {
this.importStatus = 'parsed'
this.$message.success(`成功解析Excel共读取到 ${this.rawData.length} 条数据`)
}
} catch (error) {
this.errorList = [{ rowNum: 0, errorMsg: '解析Excel失败' + error.message }]
}
}
} catch (error) {
this.errorList = [{ rowNum: 0, errorMsg: '读取文件失败:' + error.message }]
}
},
// 校验表头
validateHeaders(headers) {
if (headers.length !== TEMPLATE_HEADERS.length) {
this.errorList.push({
rowNum: 1,
errorMsg: `表头列数不匹配,要求${TEMPLATE_HEADERS.length}列,实际${headers.length}`
})
return
}
headers.forEach((header, index) => {
if (String(header || '').trim() !== TEMPLATE_HEADERS[index]) {
this.errorList.push({
rowNum: 1,
errorMsg: `${index + 1}列表头错误,要求:"${TEMPLATE_HEADERS[index]}",实际:"${header}"`
})
}
})
if (this.errorList.length > 0) {
this.$message.error('Excel表头格式不符合要求请检查')
}
},
// 格式化Excel数据
formatExcel() {
this.tableData = []
if (this.rawData.length === 0) return
const currentMaxSeqNo = this.planDetailList.length > 0
? Math.max(...this.planDetailList.map(item => parseInt(item.bizSeqNo) || 0))
: 0
this.rawData.forEach((row, index) => {
const rowObj = { bizSeqNo: currentMaxSeqNo + index + 1 }
TEMPLATE_HEADERS.forEach((header, colIndex) => {
const field = HEADER_MAP[header]
const cellValue = row[colIndex]
if (cellValue !== undefined && cellValue !== null && cellValue !== '') {
rowObj[field] = String(cellValue).trim()
}
})
this.tableData.push(rowObj)
})
},
// 校验数据
handleValidateData() {
if (this.validateLoading) return
if (this.rawData.length === 0) {
this.$message.warning('暂无数据可校验')
return
}
this.validateLoading = true
this.errorList = []
try {
this.tableData.forEach((row, i) => {
const rowNum = i + 2
if (!row.contractCode || row.contractCode.trim() === '') {
this.errorList.push({ rowNum, errorMsg: '合同号不能为空' })
}
if (!row.customerName || row.customerName.trim() === '') {
this.errorList.push({ rowNum, errorMsg: '客户名称不能为空' })
}
if (!row.productName || row.productName.trim() === '') {
this.errorList.push({ rowNum, errorMsg: '产品名称不能为空' })
}
if (!row.productMaterial || row.productMaterial.trim() === '') {
this.errorList.push({ rowNum, errorMsg: '材质不能为空' })
}
if (row.planWeight && isNaN(Number(row.planWeight))) {
this.errorList.push({ rowNum, errorMsg: '数量(吨)必须是数字' })
}
})
this.isValidated = true
if (this.errorList.length > 0) {
this.$message.error(`数据校验失败,共 ${this.errorList.length} 条错误`)
} else {
this.$message.success('数据校验通过,可以开始导入')
this.importStep = 3
}
} catch (error) {
this.errorList.push({ rowNum: 0, errorMsg: '校验数据时发生错误:' + error.message })
} finally {
this.validateLoading = false
}
},
// 开始导入
startImport() {
if (this.importLoading) return
if (!this.isValidated) {
this.$message.warning('请先校验数据')
return
}
if (this.errorList.length > 0) {
this.$message.warning('请先修正校验错误后再导入')
return
}
if (this.tableData.length === 0) {
this.$message.warning('暂无数据可导入')
return
}
this.$confirm(`确认导入 ${this.tableData.length} 条排产单明细?`, '导入确认', {
confirmButtonText: '确认导入',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.doImport()
}).catch(() => {})
},
// 执行导入
doImport() {
this.importLoading = true
this.importStatus = 'processing'
this.totalCount = this.tableData.length
this.importedCount = 0
this.importProgress = 0
const boList = this.tableData.map(row => ({
...row,
planSheetId: this.currentPlanSheetId
}))
addPlanDetailBatch(boList).then(() => {
this.importedCount = this.totalCount
this.importProgress = 100
this.importStatus = 'finished'
this.$message.success(`导入完成!共成功导入 ${this.importedCount} 条数据`)
this.getList()
}).catch(error => {
this.importStatus = 'idle'
this.$message.error('导入失败:' + (error.message || '未知错误'))
}).finally(() => {
this.importLoading = false
})
}
}
};
@@ -1259,4 +1644,211 @@ export default {
margin-bottom: 15px;
color: #303133;
}
.import-container {
padding: 10px 0;
}
.import-step-content {
margin-top: 24px;
min-height: 320px;
}
.step-body {
padding: 20px 0;
}
.step-download {
text-align: center;
padding: 40px 0;
}
.step-download .step-icon {
font-size: 48px;
color: #409eff;
margin-bottom: 16px;
}
.step-download p {
font-size: 14px;
color: #606266;
margin-bottom: 20px;
}
.step-upload {
display: flex;
flex-direction: column;
align-items: center;
}
.custom-upload {
width: 100%;
}
.custom-upload ::v-deep .el-upload {
width: 100%;
}
.custom-upload ::v-deep .el-upload-dragger {
width: 100%;
height: auto;
padding: 40px 20px;
border: 2px dashed #dcdfe6;
border-radius: 8px;
background: #fafbfc;
transition: all .3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.custom-upload ::v-deep .el-upload-dragger:hover {
border-color: #409eff;
background: #ecf5ff;
}
.custom-upload ::v-deep .el-upload-dragger.is-dragover {
border-color: #409eff;
background: #ecf5ff;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.15);
}
.upload-zone-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.upload-zone-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.upload-zone-icon i {
font-size: 28px;
color: #409eff;
}
.upload-zone-title {
font-size: 15px;
color: #606266;
margin: 0;
}
.upload-zone-title span {
color: #409eff;
cursor: pointer;
}
.upload-zone-tip {
font-size: 12px;
color: #c0c4cc;
margin: 0;
}
.parse-result {
width: 100%;
margin-top: 16px;
border-radius: 8px;
overflow: hidden;
}
.parse-result-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
font-size: 13px;
}
.parse-result-header i {
font-size: 18px;
}
.parse-result-filename {
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.parse-result-count {
white-space: nowrap;
font-weight: 500;
}
.parse-result.parse-ok {
border: 1px solid #b7eb8f;
}
.parse-result.parse-ok .parse-result-header {
background: #f6ffed;
color: #52c41a;
}
.parse-result.parse-ok i {
color: #52c41a;
}
.parse-result.parse-error {
border: 1px solid #ffa39e;
}
.parse-result.parse-error .parse-result-header {
background: #fff1f0;
color: #f5222d;
}
.parse-result.parse-error i {
color: #f5222d;
}
.step-validate {
padding: 0 40px;
text-align: center;
}
.step-validate .step-desc {
font-size: 14px;
color: #606266;
margin-bottom: 16px;
}
.validate-ok {
margin-top: 16px;
}
.step-import-container {
padding: 0;
}
.error-list {
margin-top: 16px;
}
.data-preview {
margin-top: 0;
}
.import-progress {
margin-top: 20px;
text-align: center;
}
.import-progress p {
margin-top: 8px;
color: #606266;
}
.import-result {
margin-top: 20px;
}
</style>