完成排产(测试过了)

This commit is contained in:
2026-03-08 16:02:44 +08:00
parent b660ddcc3e
commit 7736ac3311
125 changed files with 10418 additions and 15 deletions

View File

@@ -0,0 +1,486 @@
<template>
<div class="app-container line-capability-page" v-loading="loading">
<el-row :gutter="12" class="mb12">
<el-col :span="6"><div class="kpi-card"><div class="k">能力规则</div><div class="v">{{ total }}</div></div></el-col>
<el-col :span="6"><div class="kpi-card"><div class="k">启用规则</div><div class="v">{{ enabledCount }}</div></div></el-col>
<el-col :span="6"><div class="kpi-card"><div class="k">产线数量</div><div class="v">{{ lineTotal }}</div></div></el-col>
<el-col :span="6"><div class="kpi-card"><div class="k">平均每小时产能</div><div class="v">{{ avgCapacityText }}</div></div></el-col>
</el-row>
<el-card shadow="never" class="mb12">
<div slot="header" class="card-title">产线管理</div>
<el-row :gutter="10" class="mb8">
<el-col :span="7">
<el-input v-model="lineQuery.keyword" size="small" clearable placeholder="搜索产线名称/编号" @keyup.enter.native="loadLineList">
<el-button slot="append" icon="el-icon-search" @click="loadLineList" />
</el-input>
</el-col>
<el-col :span="17" class="tr">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openLineForm()">新增产线</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="loadLineList">刷新</el-button>
</el-col>
</el-row>
<KLPTable :data="lineList" max-height="260" @row-click="onLineRowClick">
<el-table-column label="产线名称" prop="lineName" min-width="140" />
<el-table-column label="产线编号" prop="lineCode" min-width="120" />
<el-table-column label="日产能" prop="capacity" width="110" />
<el-table-column label="单位" prop="unit" width="90" />
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click.stop="openLineForm(scope.row)">修改</el-button>
<el-button size="mini" type="text" @click.stop="removeLine(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
</el-card>
<el-card shadow="never">
<div slot="header" class="card-title">产线能力设置</div>
<el-form :inline="true" size="small" class="mb8">
<el-form-item label="产线">
<el-select v-model="queryParams.lineId" clearable filterable placeholder="全部产线" style="width: 220px" @change="handleQuery">
<el-option v-for="line in lineList" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.isEnabled" clearable placeholder="全部" style="width: 120px" @change="handleQuery">
<el-option :value="1" label="启用" />
<el-option :value="0" label="停用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openCapabilityForm">新增能力</el-button>
<el-button size="mini" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<KLPTable :data="lineCapabilityList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" />
<el-table-column label="产线" prop="lineName" min-width="130" />
<el-table-column label="产品" min-width="210" show-overflow-tooltip>
<template slot-scope="scope">{{ formatProduct(scope.row) }}</template>
</el-table-column>
<el-table-column label="工序" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">{{ formatProcess(scope.row) }}</template>
</el-table-column>
<el-table-column label="每小时产能" prop="capacityPerHour" width="120" />
<el-table-column label="准备时长(分钟)" prop="setupMinutes" width="130" />
<el-table-column label="优先级" prop="priority" width="90" />
<el-table-column label="状态" prop="isEnabledName" width="90" />
<el-table-column label="操作" width="170" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openCapabilityForm(scope.row)">修改</el-button>
<el-button size="mini" type="text" @click="removeCapability(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<el-dialog :title="lineDialog.title" :visible.sync="lineDialog.visible" width="560px" append-to-body>
<el-form ref="lineFormRef" :model="lineDialog.form" label-width="110px" :rules="lineRules">
<el-form-item label="产线名称" prop="lineName"><el-input v-model="lineDialog.form.lineName" /></el-form-item>
<el-form-item label="产线编号" prop="lineCode"><el-input v-model="lineDialog.form.lineCode" /></el-form-item>
<el-form-item label="日产能"><el-input-number v-model="lineDialog.form.capacity" :min="0" :step="1" style="width: 100%" /></el-form-item>
<el-form-item label="单位"><el-input v-model="lineDialog.form.unit" /></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="buttonLoading" @click="submitLine">确定</el-button>
<el-button @click="lineDialog.visible = false">取消</el-button>
</div>
</el-dialog>
<el-dialog :title="title" :visible.sync="open" width="720px" append-to-body>
<el-form ref="capabilityFormRef" :model="form" :rules="rules" label-width="130px">
<el-form-item label="产线" prop="lineId">
<el-select v-model="form.lineId" filterable placeholder="请选择产线" style="width: 100%" @change="onFormLineChange">
<el-option v-for="line in lineList" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
</el-select>
</el-form-item>
<el-form-item label="产品">
<el-input :value="form.productDisplay" readonly style="width: calc(100% - 120px); margin-right: 8px" />
<el-button @click="openProductSelector">选择</el-button>
<el-button type="text" @click="clearProduct">清空</el-button>
</el-form-item>
<el-form-item label="工序">
<el-input :value="form.processDisplay" readonly style="width: calc(100% - 120px); margin-right: 8px" />
<el-button @click="openProcessSelector">选择</el-button>
<el-button type="text" @click="clearProcess">清空</el-button>
</el-form-item>
<el-form-item label="每小时产能" prop="capacityPerHour"><el-input-number v-model="form.capacityPerHour" :min="0" :precision="2" :step="0.1" style="width: 100%" /></el-form-item>
<el-form-item label="准备时长(分钟)"><el-input-number v-model="form.setupMinutes" :min="0" :step="1" style="width: 100%" /></el-form-item>
<el-form-item label="优先级"><el-input-number v-model="form.priority" :min="1" :step="1" style="width: 100%" /></el-form-item>
<el-form-item label="是否启用"><el-radio-group v-model="form.isEnabled"><el-radio :label="1">启用</el-radio><el-radio :label="0">停用</el-radio></el-radio-group></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="buttonLoading" @click="submitForm">确定</el-button>
<el-button @click="open = false">取消</el-button>
</div>
</el-dialog>
<el-dialog title="选择产品" :visible.sync="productSelector.visible" width="980px" append-to-body>
<el-form :inline="true" size="small">
<el-form-item><el-input v-model="productSelector.keyword" placeholder="产品名称/编号" @keyup.enter.native="fetchProductOptions" /></el-form-item>
<el-form-item><el-button type="primary" size="mini" @click="fetchProductOptions">搜索</el-button></el-form-item>
</el-form>
<KLPTable :data="productSelector.list" max-height="360">
<el-table-column prop="productName" label="产品名称" min-width="150" />
<el-table-column prop="productCode" label="产品编号" min-width="120" />
<el-table-column prop="specification" label="规格" min-width="120" />
<el-table-column prop="material" label="材质" min-width="100" />
<el-table-column label="操作" width="90" fixed="right">
<template slot-scope="scope"><el-button size="mini" type="text" @click="selectProduct(scope.row)">选择</el-button></template>
</el-table-column>
</KLPTable>
<pagination v-show="productSelector.total > 0" :total="productSelector.total" :page.sync="productSelector.pageNum" :limit.sync="productSelector.pageSize" @pagination="fetchProductOptions" />
</el-dialog>
<el-dialog title="选择工序" :visible.sync="processSelector.visible" width="900px" append-to-body>
<el-form :inline="true" size="small">
<el-form-item><el-input v-model="processSelector.keyword" placeholder="工序名称/编码" @keyup.enter.native="fetchProcessOptions" /></el-form-item>
<el-form-item><el-button type="primary" size="mini" @click="fetchProcessOptions">搜索</el-button></el-form-item>
</el-form>
<KLPTable :data="processSelector.list" max-height="360">
<el-table-column prop="processName" label="工序名称" min-width="150" />
<el-table-column prop="processCode" label="工序编码" min-width="120" />
<el-table-column prop="standardTime" label="标准工时" width="110" />
<el-table-column label="操作" width="90" fixed="right">
<template slot-scope="scope"><el-button size="mini" type="text" @click="selectProcess(scope.row)">选择</el-button></template>
</el-table-column>
</KLPTable>
<pagination v-show="processSelector.total > 0" :total="processSelector.total" :page.sync="processSelector.pageNum" :limit.sync="processSelector.pageSize" @pagination="fetchProcessOptions" />
</el-dialog>
</div>
</template>
<script>
import { listLineCapability, getLineCapability, delLineCapability, addLineCapability, updateLineCapability } from '@/api/aps/aps'
import { listProductionLine, addProductionLine, updateProductionLine, delProductionLine } from '@/api/wms/productionLine'
import { listProduct } from '@/api/wms/product'
import { listProcesse } from '@/api/wms/craft'
export default {
name: 'ApsLineCapability',
data() {
return {
loading: false,
buttonLoading: false,
total: 0,
lineTotal: 0,
ids: [],
lineCapabilityList: [],
lineList: [],
lineQuery: { keyword: '' },
queryParams: { pageNum: 1, pageSize: 20, lineId: undefined, isEnabled: undefined },
open: false,
title: '',
form: {},
lineDialog: { visible: false, title: '', form: {} },
productSelector: { visible: false, keyword: '', pageNum: 1, pageSize: 10, total: 0, list: [] },
processSelector: { visible: false, keyword: '', pageNum: 1, pageSize: 10, total: 0, list: [] },
rules: {
lineId: [{ required: true, message: '请选择产线', trigger: 'change' }],
capacityPerHour: [{ required: true, message: '请输入每小时产能', trigger: 'blur' }]
},
lineRules: {
lineName: [{ required: true, message: '请输入产线名称', trigger: 'blur' }],
lineCode: [{ required: true, message: '请输入产线编号', trigger: 'blur' }]
}
}
},
computed: {
enabledCount() {
return (this.lineCapabilityList || []).filter(i => Number(i.isEnabled) === 1).length
},
avgCapacityText() {
if (!this.lineCapabilityList.length) return '-'
const sum = this.lineCapabilityList.reduce((s, i) => s + Number(i.capacityPerHour || 0), 0)
return (sum / this.lineCapabilityList.length).toFixed(2)
}
},
created() {
this.resetFormData()
this.loadLineList().then(() => this.getList())
},
methods: {
formatProduct(row) {
if (!row.productName) return '通用'
return `${row.productName}${row.productCode ? '' + row.productCode + '' : ''}`
},
formatProcess(row) {
if (!row.processName) return '通用'
return `${row.processName}${row.processCode ? '' + row.processCode + '' : ''}`
},
resetFormData() {
this.form = {
capabilityId: undefined,
lineId: undefined,
productId: undefined,
processId: undefined,
productDisplay: '',
processDisplay: '',
capacityPerHour: undefined,
setupMinutes: 0,
priority: 999,
isEnabled: 1,
remark: ''
}
},
async loadLineList() {
const res = await listProductionLine({ pageNum: 1, pageSize: 500, lineName: this.lineQuery.keyword || undefined })
this.lineList = res.rows || []
this.lineTotal = res.total || this.lineList.length
},
onLineRowClick(row) {
this.queryParams.lineId = row.lineId
this.handleQuery()
},
getList() {
this.loading = true
listLineCapability(this.queryParams).then(res => {
this.lineCapabilityList = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams = { pageNum: 1, pageSize: 20, lineId: undefined, isEnabled: undefined }
this.getList()
},
handleSelectionChange(selection) {
this.ids = (selection || []).map(i => i.capabilityId)
},
openCapabilityForm(row) {
this.resetFormData()
if (!row) {
this.title = '新增产线能力'
if (this.queryParams.lineId) this.form.lineId = this.queryParams.lineId
this.open = true
return
}
this.loading = true
getLineCapability(row.capabilityId).then(res => {
const d = res.data || {}
this.form = {
...this.form,
...d,
productDisplay: d.productName ? `${d.productName}${d.productCode ? '' + d.productCode + '' : ''}` : '',
processDisplay: d.processName ? `${d.processName}${d.processCode ? '' + d.processCode + '' : ''}` : ''
}
this.title = '修改产线能力'
this.open = true
}).finally(() => { this.loading = false })
},
onFormLineChange(lineId) {
const line = (this.lineList || []).find(i => String(i.lineId) === String(lineId))
const cap = Number((line && line.capacity) || 0)
if (cap > 0 && (!this.form.capacityPerHour || Number(this.form.capacityPerHour) <= 0)) {
this.form.capacityPerHour = Number((cap / 24).toFixed(2))
}
},
submitForm() {
this.$refs.capabilityFormRef.validate(valid => {
if (!valid) return
this.buttonLoading = true
const payload = {
capabilityId: this.form.capabilityId,
lineId: this.form.lineId,
productId: this.form.productId,
processId: this.form.processId,
capacityPerHour: this.form.capacityPerHour,
setupMinutes: this.form.setupMinutes,
priority: this.form.priority,
isEnabled: this.form.isEnabled,
remark: this.form.remark
}
const req = payload.capabilityId ? updateLineCapability(payload) : addLineCapability(payload)
req.then(() => {
this.$modal.msgSuccess(payload.capabilityId ? '修改成功' : '新增成功')
this.open = false
this.getList()
}).finally(() => { this.buttonLoading = false })
})
},
removeCapability(row) {
this.$modal.confirm('确认删除该能力规则吗?').then(() => delLineCapability(row.capabilityId)).then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
})
},
openLineForm(row) {
this.lineDialog.visible = true
this.lineDialog.title = row ? '修改产线' : '新增产线'
this.lineDialog.form = row ? { ...row } : { lineName: '', lineCode: '', capacity: 0, unit: '' }
},
submitLine() {
this.$refs.lineFormRef.validate(valid => {
if (!valid) return
const payload = { ...this.lineDialog.form }
this.buttonLoading = true
const req = payload.lineId ? updateProductionLine(payload) : addProductionLine(payload)
req.then(() => {
this.$modal.msgSuccess(payload.lineId ? '产线修改成功' : '产线新增成功')
this.lineDialog.visible = false
return this.loadLineList()
}).then(() => this.getList()).finally(() => { this.buttonLoading = false })
})
},
removeLine(row) {
this.$modal.confirm('确认删除该产线吗?').then(() => delProductionLine(row.lineId)).then(() => {
this.$modal.msgSuccess('删除成功')
return this.loadLineList()
}).then(() => this.getList())
},
openProductSelector() {
this.productSelector.visible = true
this.productSelector.pageNum = 1
this.fetchProductOptions()
},
fetchProductOptions() {
listProduct({
pageNum: this.productSelector.pageNum,
pageSize: this.productSelector.pageSize,
productName: this.productSelector.keyword || undefined,
productCode: this.productSelector.keyword || undefined
}).then(res => {
this.productSelector.list = res.rows || []
this.productSelector.total = res.total || 0
})
},
selectProduct(row) {
this.form.productId = row.productId
this.form.productDisplay = `${row.productName || ''}${row.productCode ? '' + row.productCode + '' : ''}`
this.productSelector.visible = false
},
clearProduct() {
this.form.productId = undefined
this.form.productDisplay = ''
},
openProcessSelector() {
this.processSelector.visible = true
this.processSelector.pageNum = 1
this.fetchProcessOptions()
},
fetchProcessOptions() {
listProcesse({
pageNum: this.processSelector.pageNum,
pageSize: this.processSelector.pageSize,
processName: this.processSelector.keyword || undefined,
processCode: this.processSelector.keyword || undefined
}).then(res => {
this.processSelector.list = res.rows || []
this.processSelector.total = res.total || 0
})
},
selectProcess(row) {
this.form.processId = row.processId
this.form.processDisplay = `${row.processName || ''}${row.processCode ? '' + row.processCode + '' : ''}`
this.processSelector.visible = false
},
clearProcess() {
this.form.processId = undefined
this.form.processDisplay = ''
}
}
}
</script>
<style scoped lang="scss">
.line-capability-page {
padding: 12px;
background: transparent;
}
.line-capability-page > .el-row,
.line-capability-page > .el-card {
background: #fff;
}
.mb12 { margin-bottom: 12px; }
.mb8 { margin-bottom: 8px; }
.tr { text-align: right; }
.kpi-card {
border: 1px solid #e9edf5;
border-radius: 10px;
background: #fff;
padding: 12px;
box-shadow: 0 1px 4px rgba(30, 41, 59, 0.04);
}
.kpi-card .k {
font-size: 12px;
color: #7c8798;
}
.kpi-card .v {
margin-top: 8px;
font-size: 24px;
line-height: 1;
font-weight: 700;
color: #1f2d3d;
}
.card-title {
font-weight: 700;
color: #344054;
font-size: 14px;
position: relative;
padding-left: 10px;
}
.card-title::before {
content: '';
position: absolute;
left: 0;
top: 3px;
width: 3px;
height: 14px;
border-radius: 2px;
background: #4f8ff7;
}
::v-deep .el-card {
border: 1px solid #e9edf5;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(30, 41, 59, 0.04);
}
::v-deep .el-card__header {
padding: 10px 14px;
border-bottom: 1px solid #eef2f8;
background: #fff;
}
::v-deep .el-card__body {
padding: 12px 14px;
}
::v-deep .el-table,
::v-deep .el-table::before,
::v-deep .el-table__fixed::before,
::v-deep .el-table__fixed-right::before {
border-color: #edf1f7;
}
::v-deep .el-table th.is-leaf,
::v-deep .el-table td {
border-bottom: 1px solid #f1f4f9;
}
::v-deep .el-table th {
background: #fff;
color: #5d6b82;
font-weight: 600;
}
::v-deep .el-table tbody tr:hover > td {
background: #f9fbff !important;
}
</style>