Files
klp-oa/klp-ui/src/views/wms/post/aps/processes.vue
砂糖 2fde9ec993 feat: 实现产需单按工序步骤生成排产明细功能
1. 重构接收产需单接口,支持按配置工序步骤生成明细
2. 新增工艺、工艺步骤CRUD接口与管理页面
3. 新增工序选择组件
4. 优化产需单页面,增加历史记录功能
5. 为排产明细添加工序步骤名称展示
2026-07-04 15:43:05 +08:00

590 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>