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

@@ -0,0 +1,589 @@
<template>
<div class="app-container" style="height: calc(100vh - 84px); display: flex;">
<!-- 左侧工艺列表 -->
<div class="left-panel" v-loading="processLoading"
style="width: 35%; border-right: 1px solid #e4e7ed; overflow-y: auto;">
<!-- 筛选区 -->
<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.processName" placeholder="工艺名称" clearable @keyup.enter.native="handleSearch"
style="width: 160px;" />
<el-button class="aps-btn-red" icon="el-icon-search" size="mini" @click="handleSearch">筛选</el-button>
<el-button icon="el-icon-refresh" size="mini" class="aps-btn-silver" @click="resetQuery">重置</el-button>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<el-button class="aps-btn-red" icon="el-icon-plus" size="mini" @click="handleAddProcess">新增工艺</el-button>
<span style="font-size: 12px; color: #909399;">
<span class="aps-total-count">{{ total }}</span>
</span>
</div>
</div>
</div>
<!-- 列表区域 -->
<div class="custom-list">
<div class="list-body">
<div v-for="item in processList" :key="item.processId" class="list-item"
:class="{ 'list-item-active': currentProcess && currentProcess.processId === item.processId }"
@click="handleProcessClick(item)">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-weight: bold; font-size: 14px;">{{ item.processName }}</div>
<div style="display: flex; gap: 4px;">
<el-button type="text" icon="el-icon-edit" size="mini" @click.stop="handleUpdateProcess(item)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click.stop="handleDeleteProcess(item)"></el-button>
</div>
</div>
<div style="font-size: 12px; color: #909399; margin-bottom: 4px;">
{{ item.processDesc || '暂无描述' }}
</div>
<div style="font-size: 12px; color: #909399;">
备注: {{ item.remark || '-' }}
</div>
</div>
<div v-if="processList.length === 0 && !processLoading"
style="padding: 40px; text-align: center; color: #909399;">
暂无工艺数据
</div>
</div>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getProcessList" style="padding: 10px; margin-bottom: 10px !important;" />
</div>
<!-- 右侧工艺步骤区域 -->
<div class="right-panel" v-if="currentProcess && currentProcess.processId"
style="flex: 1; display: flex; flex-direction: column;" v-loading="stepLoading">
<div class="detail-panel">
<!-- 工艺基本信息卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺基本信息</span>
</div>
<div class="detail-card-body">
<div class="form-grid-2">
<div class="form-field"><label>工艺名称</label>
<div class="field-value">{{ currentProcess.processName }}</div>
</div>
<div class="form-field"><label>工艺描述</label>
<div class="field-value">{{ currentProcess.processDesc || '-' }}</div>
</div>
<div class="form-field" style="grid-column:1/3;"><label>备注</label>
<div class="field-value">{{ currentProcess.remark || '-' }}</div>
</div>
</div>
</div>
</div>
<!-- 工艺步骤卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺步骤{{ stepList.length }} </span>
<el-button type="text" icon="el-icon-plus" size="mini" style="color: white;" @click="handleAddStep">新增步骤</el-button>
</div>
<div class="detail-card-body">
<div v-if="stepList.length > 0" class="process-timeline-wrap">
<el-timeline>
<el-timeline-item
v-for="(step, index) in stepList"
:key="step.stepId"
:timestamp="'步骤 ' + step.stepOrder"
placement="top"
:type="index === stepList.length - 1 ? 'success' : 'primary'"
:hollow="index !== stepList.length - 1"
size="large"
>
<div class="timeline-content">
<div class="timeline-header">
<span class="step-name">{{ step.stepName }}</span>
<div class="step-actions">
<el-button type="text" icon="el-icon-edit" size="mini" @click="handleUpdateStep(step)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click="handleDeleteStep(step)"></el-button>
</div>
</div>
<div v-if="step.remark" class="step-remark">{{ step.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
<el-empty v-else description="暂无工艺步骤" />
</div>
</div>
</div>
</div>
<div v-else style="flex: 1; display: flex; flex-direction: column;">
<el-empty description="选择工艺后查看步骤" />
</div>
<!-- 新增/编辑工艺弹窗 -->
<el-dialog :title="processTitle" :visible.sync="processOpen" width="500px" append-to-body>
<el-form ref="processForm" :model="processForm" :rules="processRules" label-width="80px">
<el-form-item label="工艺名称" prop="processName">
<el-input v-model="processForm.processName" placeholder="请输入工艺名称" />
</el-form-item>
<el-form-item label="工艺描述" prop="processDesc">
<el-input v-model="processForm.processDesc" type="textarea" :rows="3" placeholder="请输入工艺描述" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="processForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitProcessForm"> </el-button>
<el-button @click="cancelProcess"> </el-button>
</div>
</el-dialog>
<!-- 新增/编辑工艺步骤弹窗 -->
<el-dialog :title="stepTitle" :visible.sync="stepOpen" width="500px" append-to-body>
<el-form ref="stepForm" :model="stepForm" :rules="stepRules" label-width="80px">
<el-form-item label="步骤顺序" prop="stepOrder">
<el-input-number v-model="stepForm.stepOrder" :min="1" :max="99" controls-position="right" />
</el-form-item>
<el-form-item label="步骤名称" prop="stepName">
<el-input v-model="stepForm.stepName" placeholder="请输入步骤名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="stepForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitStepForm"> </el-button>
<el-button @click="cancelStep"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcess, getProcess, addProcess, updateProcess, delProcess } from '@/api/aps/process'
import { listProcessStep, addProcessStep, updateProcessStep, delProcessStep } from '@/api/aps/processStep'
export default {
name: 'ApsProcesses',
data() {
return {
// 工艺相关
processLoading: false,
processList: [],
total: 0,
currentProcess: null,
queryParams: {
processName: '',
pageNum: 1,
pageSize: 10
},
processTitle: '',
processOpen: false,
processForm: {},
processRules: {
processName: [
{ required: true, message: '工艺名称不能为空', trigger: 'blur' }
]
},
// 工艺步骤相关
stepLoading: false,
submitLoading: false,
stepList: [],
stepTitle: '',
stepOpen: false,
stepForm: {},
stepRules: {
stepOrder: [
{ required: true, message: '步骤顺序不能为空', trigger: 'blur' }
],
stepName: [
{ required: true, message: '步骤名称不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getProcessList()
},
methods: {
/** 查询工艺列表 */
getProcessList() {
this.processLoading = true
listProcess(this.queryParams).then(res => {
this.processList = res.rows || []
this.total = res.total || 0
}).catch(() => {
this.processList = []
this.total = 0
}).finally(() => {
this.processLoading = false
})
},
/** 搜索 */
handleSearch() {
this.queryParams.pageNum = 1
this.getProcessList()
},
/** 重置 */
resetQuery() {
this.queryParams.processName = ''
this.handleSearch()
},
/** 点击工艺 */
handleProcessClick(process) {
this.currentProcess = process
this.getStepList(process.processId)
},
/** 查询工艺步骤列表 */
getStepList(processId) {
this.stepLoading = true
listProcessStep({ processId }).then(res => {
this.stepList = res.rows || []
}).catch(() => {
this.stepList = []
}).finally(() => {
this.stepLoading = false
})
},
/** 新增工艺 */
handleAddProcess() {
this.resetProcessForm()
this.processTitle = '新增工艺'
this.processOpen = true
},
/** 修改工艺 */
handleUpdateProcess(row) {
this.resetProcessForm()
const processId = row.processId || this.currentProcess.processId
getProcess(processId).then(res => {
this.processForm = res.data
this.processTitle = '修改工艺'
this.processOpen = true
})
},
/** 提交工艺表单 */
submitProcessForm() {
this.$refs['processForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.processForm.processId != undefined) {
updateProcess(this.processForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
} else {
addProcess(this.processForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除工艺 */
handleDeleteProcess(row) {
const processIds = row.processId || this.currentProcess.processId
this.$modal.confirm('是否确认删除该工艺?').then(() => {
return delProcess(processIds)
}).then(() => {
this.getProcessList()
this.$modal.msgSuccess('删除成功')
this.currentProcess = null
this.stepList = []
}).catch(() => {})
},
/** 取消工艺弹窗 */
cancelProcess() {
this.processOpen = false
this.resetProcessForm()
},
/** 重置工艺表单 */
resetProcessForm() {
this.processForm = {
processId: undefined,
processName: undefined,
processDesc: undefined,
remark: undefined
}
this.resetForm('processForm')
},
/** 新增步骤 */
handleAddStep() {
this.resetStepForm()
this.stepForm.processId = this.currentProcess.processId
// 自动计算下一步骤顺序
if (this.stepList.length > 0) {
const maxOrder = Math.max(...this.stepList.map(s => s.stepOrder || 0))
this.stepForm.stepOrder = maxOrder + 1
} else {
this.stepForm.stepOrder = 1
}
this.stepTitle = '新增工艺步骤'
this.stepOpen = true
},
/** 修改步骤 */
handleUpdateStep(row) {
this.resetStepForm()
this.stepForm = { ...row }
this.stepTitle = '修改工艺步骤'
this.stepOpen = true
},
/** 提交步骤表单 */
submitStepForm() {
this.$refs['stepForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.stepForm.stepId != undefined) {
updateProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
} else {
addProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除步骤 */
handleDeleteStep(row) {
const stepIds = row.stepId
this.$modal.confirm('是否确认删除该工艺步骤?').then(() => {
return delProcessStep(stepIds)
}).then(() => {
this.getStepList(this.currentProcess.processId)
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 取消步骤弹窗 */
cancelStep() {
this.stepOpen = false
this.resetStepForm()
},
/** 重置步骤表单 */
resetStepForm() {
this.stepForm = {
stepId: undefined,
processId: undefined,
stepOrder: undefined,
stepName: undefined,
remark: undefined
}
this.resetForm('stepForm')
}
}
}
</script>
<style scoped lang="scss">
@import './scss/aps-theme.scss';
.app-container {
overflow: hidden;
padding: 0;
}
.left-panel {
height: calc(100vh - 84px);
box-sizing: border-box;
overflow-y: auto;
}
.right-panel {
height: calc(100vh - 84px);
overflow: hidden;
min-height: 0;
}
.custom-list {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.list-item {
padding: 12px;
border-bottom: 2px solid #dddddd;
cursor: pointer;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: $aps-silver-1;
}
.list-item-active {
background-color: $aps-red-1;
border-left: 3px solid $aps-red-2;
}
.aps-total-count {
color: $aps-red-2;
font-weight: bold;
}
.aps-btn-red {
@include aps-btn-red;
}
.aps-btn-silver {
@include aps-btn-silver;
}
.detail-panel {
flex: 1;
overflow-y: scroll;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
background: $aps-bg;
min-height: 0;
}
.detail-card {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
}
.detail-card-header {
background: linear-gradient(to right, $aps-red-2, $aps-red-3);
color: white;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-card-body {
padding: 14px;
}
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.form-field label {
font-size: 11px;
color: $aps-text-muted;
font-weight: 500;
}
.form-field .field-value {
font-size: 13px;
color: $aps-text;
padding: 4px 0;
border-bottom: 1px solid $aps-silver-mid;
}
.aps-product-table {
width: 100%;
::v-deep th {
background: $aps-silver-1 !important;
color: $aps-silver-5 !important;
font-weight: 600 !important;
}
}
// ====== 工艺步骤时间线 ======
.process-timeline-wrap {
padding: 20px 10px;
::v-deep .el-timeline {
padding-left: 10px;
}
::v-deep .el-timeline-item__wrapper {
padding-left: 20px;
}
::v-deep .el-timeline-item__timestamp {
font-size: 12px;
font-weight: 600;
color: $aps-text-muted;
}
::v-deep .el-timeline-item__tail {
border-left-color: $aps-silver-3;
}
::v-deep .el-timeline-item__node--primary {
background-color: $aps-red-2;
border-color: $aps-red-2;
}
::v-deep .el-timeline-item__node--success {
background-color: #67C23A;
border-color: #67C23A;
}
}
.timeline-content {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
padding: 12px 16px;
transition: box-shadow 0.2s, border-color 0.2s;
&:hover {
border-color: $aps-red-2;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-name {
font-size: 14px;
font-weight: 600;
color: $aps-text;
}
.step-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
.timeline-content:hover & {
opacity: 1;
}
}
.step-remark {
margin-top: 8px;
font-size: 12px;
color: $aps-text-muted;
padding-top: 8px;
border-top: 1px dashed $aps-silver-3;
}
</style>