feat: 实现产需单按工序步骤生成排产明细功能

1. 重构接收产需单接口,支持按配置工序步骤生成明细
2. 新增工艺、工艺步骤CRUD接口与管理页面
3. 新增工序选择组件
4. 优化产需单页面,增加历史记录功能
5. 为排产明细添加工序步骤名称展示
This commit is contained in:
2026-07-04 15:43:05 +08:00
parent ce09ac9da3
commit 2fde9ec993
12 changed files with 1529 additions and 427 deletions

View File

@@ -7,7 +7,7 @@
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch"
<el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch"
style="width: 130px;" size="small" />
<el-input v-model="queryParams.customerName" placeholder="客户名" clearable
@keyup.enter.native="handleSearch" style="width: 130px;" size="small" />
@@ -58,15 +58,19 @@
<div class="detail-card-header">
<span>产需单信息</span>
<div style="display:flex; gap:6px;">
<button class="header-btn" :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3" @click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn" style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);" @click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
<button class="header-btn"
:disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3"
@click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn"
style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);"
@click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
</div>
</div>
<div class="detail-card-body">
<table class="req-info-table">
<tbody>
<tr>
<td class="req-td-label">产单号</td>
<td class="req-td-label">单号</td>
<td class="req-td-value">{{ currentReq.scheduleNo }}</td>
<td class="req-td-label">生产日期</td>
<td class="req-td-value">{{ currentReq.prodDate }}</td>
@@ -75,7 +79,7 @@
<td class="req-td-label">排产状态</td>
<td class="req-td-value"><span class="aps-status-tag"
:class="'status-' + (currentReq.scheduleStatus || 0)">{{ statusMap[currentReq.scheduleStatus] ||
'未知' }}</span></td>
'未知' }}</span></td>
<td class="req-td-label">业务员</td>
<td class="req-td-value">{{ currentReq.businessUser }}</td>
</tr>
@@ -139,7 +143,8 @@
</tr>
<tr v-if="currentReq.returnReason">
<td class="req-td-label" style="color:#e74c3c;">驳回原因</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{ currentReq.returnReason }}</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{
currentReq.returnReason }}</td>
</tr>
</tbody>
</table>
@@ -190,8 +195,10 @@
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;" @click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;" @click="handleDetailDelete(scope.row)">删除</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;"
@click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;"
@click="handleDetailDelete(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
@@ -204,13 +211,15 @@
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px" append-to-body
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" append-to-body
:close-on-click-modal="false">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="110px" size="small">
<!-- 第一行必填项 -->
<div style="display: flex; gap: 16px;">
<div style="flex: 1; min-width: 0;">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="80px" size="small">
<!-- 第一行必填项 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产单号" prop="scheduleNo">
<el-form-item label="产单号" prop="scheduleNo">
<el-input v-model="reqForm.scheduleNo" placeholder="自动生成或手动填写" />
</el-form-item>
</el-col>
@@ -282,19 +291,42 @@
<el-form-item label="交货重量偏差" prop="weightDeviation">
<el-input v-model="reqForm.weightDeviation" />
</el-form-item>
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="付款情况说明" prop="paymentDesc">
<el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="reqForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
</div>
<!-- 右侧历史记录 -->
<div style="width: 200px; flex-shrink: 0;" v-if="!reqForm.scheduleId && scheduleNoHistory.length > 0">
<div style="font-size: 13px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; padding-left: 8px; border-left: 3px solid #ff4d4f;">
历史记录
</div>
<div style="border: 1px solid #e4e7ed; border-radius: 4px; max-height: 450px; overflow-y: auto;">
<div v-for="item in scheduleNoHistory" :key="item.scheduleNo" class="history-item"
@click="useHistoryRecord(item)"
style="padding: 8px 10px; border-bottom: 1px solid #ecf0f1; cursor: pointer; font-size: 12px; transition: background 0.15s;">
<div style="font-weight: 500; color: #2c3e50;">{{ item.scheduleNo }}</div>
<div style="color: #7f8c8d; margin-top: 2px;">{{ item.customerName || '-' }}</div>
<div style="color: #b0b0b0; font-size: 11px;">{{ item.prodDate || '-' }}</div>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button :loading="btnLoading" type="danger" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
@@ -376,7 +408,8 @@
<el-input v-model="detailForm.material" placeholder="请输入材质" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false"
style="width:100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" />
@@ -458,9 +491,10 @@ export default {
dialogVisible: false,
dialogTitle: '新增产需单',
btnLoading: false,
scheduleNoHistory: [],
reqForm: this.getEmptyForm(),
reqRules: {
scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }],
scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }],
prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }]
}
}
@@ -564,6 +598,17 @@ export default {
handleAdd() {
this.resetForm()
this.reqForm.scheduleNo = this.generateScheduleNo()
this.reqForm.prodDate = this.getTodayStr()
const lastForm = this.getLastFormData()
if (lastForm) {
Object.keys(lastForm).forEach(key => {
if (key !== 'scheduleId' && key !== 'scheduleNo' && key !== 'prodDate' && lastForm[key] !== undefined && lastForm[key] !== '' && lastForm[key] !== null) {
this.reqForm[key] = lastForm[key]
}
})
}
this.scheduleNoHistory = this.getScheduleNoHistory()
this.dialogTitle = '新增产需单'
this.dialogVisible = true
this.$nextTick(() => {
@@ -586,9 +631,14 @@ export default {
this.$refs.reqForm.validate(valid => {
if (!valid) return
this.btnLoading = true
const action = this.reqForm.scheduleId ? updateRequirement : addRequirement
const isNew = !this.reqForm.scheduleId
const action = isNew ? addRequirement : updateRequirement
action(this.reqForm).then(() => {
this.$modal.msgSuccess(this.reqForm.scheduleId ? '修改成功' : '新增成功')
this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功')
if (isNew) {
this.saveHistoryRecord(this.reqForm)
this.saveLastFormData(this.reqForm)
}
this.dialogVisible = false
this.getList()
}).catch(() => {
@@ -615,6 +665,71 @@ export default {
}).catch(() => { })
},
getTodayStr() {
return new Date().toISOString().split('T')[0]
},
generateScheduleNo() {
const now = new Date()
const dateStr = now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0')
const prefix = 'PCXD' + dateStr
const existingNums = this.reqList
.filter(item => item.scheduleNo && item.scheduleNo.startsWith(prefix))
.map(item => {
const seq = parseInt(item.scheduleNo.substring(prefix.length), 10)
return isNaN(seq) ? 0 : seq
})
const maxSeq = existingNums.length > 0 ? Math.max(...existingNums) : 0
const newSeq = (maxSeq + 1).toString().padStart(3, '0')
return prefix + newSeq
},
getScheduleNoHistory() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_history') || '[]')
} catch (e) {
return []
}
},
saveHistoryRecord(formData) {
if (!formData.scheduleNo) return
const history = this.getScheduleNoHistory()
const idx = history.findIndex(item => item.scheduleNo === formData.scheduleNo)
if (idx >= 0) history.splice(idx, 1)
history.unshift({ ...formData })
delete history[0].scheduleId
if (history.length > 20) history.pop()
localStorage.setItem('aps_requirement_history', JSON.stringify(history))
},
useHistoryRecord(item) {
Object.keys(item).forEach(key => {
if (key !== 'scheduleId' && item[key] !== undefined) {
this.reqForm[key] = item[key]
}
})
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
},
getLastFormData() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_last_form') || 'null')
} catch (e) {
return null
}
},
saveLastFormData(formData) {
const data = { ...formData }
delete data.scheduleId
localStorage.setItem('aps_requirement_last_form', JSON.stringify(data))
},
openBindDialog() {
this.bindDialogVisible = true
this.selectedBindOrders = []
@@ -1223,4 +1338,8 @@ export default {
.aps-bind-dialog ::v-deep .el-dialog__body {
padding: 16px 20px 0 20px;
}
.history-item:hover {
background: #fdecea;
}
</style>