Files
double-rack/ruoyi-ui/src/views/mill/plan.vue
wangyu ca5c21b15f fix(plan): 恢复生产计划原始布局,工艺方案改可选,新增快速新增方案入口
- 恢复上下分栏:上方轧制队列表格 + 左下轧制工艺 + 右下操作面板 + 底部带卷栏
- 钢卷新增/修改弹窗中工艺方案字段改为可选(clearable,无必填校验)
- 右侧操作面板增加「工艺方案」分组,提供「快速新增方案」按钮
  弹窗内可填写方案基本信息并录入道次参数,保存后自动刷新方案下拉列表

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 16:57:38 +08:00

615 lines
24 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="plan-page">
<!-- 上方轧制队列 -->
<div class="queue-section">
<div class="section-header"><span>轧制队列</span></div>
<el-table :data="planList" border size="mini" class="queue-table"
:row-class-name="queueRowClass"
@current-change="handleQueueSelect"
highlight-current-row
height="calc(50vh - 120px)">
<el-table-column label="序号" prop="sortNo" width="50" align="center" fixed />
<el-table-column label="钢卷编号" prop="inMatNo" width="110" />
<el-table-column label="合金牌号" prop="alloyNo" width="90" />
<el-table-column label="采料厚度(mm)" prop="inMatThick" width="105" align="right" />
<el-table-column label="成品厚度(mm)" prop="outThick" width="105" align="right" />
<el-table-column label="采料宽度(mm)" prop="inMatWidth" width="105" align="right" />
<el-table-column label="道次数" prop="passCount" width="65" align="center" />
<el-table-column label="采料长度(m)" prop="inMatLength" width="100" align="right" />
<el-table-column label="采料重量(t)" prop="inMatWeight" width="100" align="right" />
<el-table-column label="采料外径(mm)" prop="inMatOd" width="105" align="right" />
<el-table-column label="采料内径(mm)" prop="inMatId" width="105" align="right" />
<el-table-column label="生产状态" prop="prodStatus" width="80" align="center">
<template slot-scope="{ row }">
<span :class="prodStatusClass(row.prodStatus)">{{ prodStatusLabel(row.prodStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="工艺方案" prop="recipeNo" min-width="100" />
</el-table>
</div>
<!-- 下方 -->
<div class="bottom-section">
<!-- 左侧轧制工艺道次只读 -->
<div class="pass-panel">
<div class="section-header">
<span>轧制工艺</span>
<span v-if="selectedPlan && selectedPlan.recipeNo" class="recipe-tag">{{ selectedPlan.recipeNo }}</span>
</div>
<el-table :data="passList" border size="mini" class="pass-table"
:row-class-name="passRowClass"
height="calc(50vh - 130px)">
<el-table-column label="道次" prop="passNo" width="45" align="center" fixed />
<el-table-column label="入口厚(mm)" prop="inThick" width="85" align="right" />
<el-table-column label="出口厚(mm)" prop="outThick" width="85" align="right" />
<el-table-column label="轧制力(kN)" prop="rollForce" width="85" align="right" />
<el-table-column label="入口张力" prop="inTension" width="75" align="right" />
<el-table-column label="出口张力" prop="outTension" width="75" align="right" />
<el-table-column label="最高速度" prop="maxSpeed" width="75" align="right" />
<el-table-column label="入口单位张力" prop="inUnitTension" width="100" align="right" />
<el-table-column label="出口单位张力" prop="outUnitTension" width="100" align="right" />
<el-table-column label="压下量(mm)" prop="reduction" width="85" align="right">
<template slot-scope="{ row }"><span class="calc-val">{{ row.reduction }}</span></template>
</el-table-column>
<el-table-column label="总压下量(mm)" prop="totalReduction" width="100" align="right">
<template slot-scope="{ row }"><span class="calc-val">{{ row.totalReduction }}</span></template>
</el-table-column>
</el-table>
</div>
<!-- 右侧查询 + 操作 -->
<div class="op-panel">
<div class="section-header"><span>条件查询</span></div>
<div class="query-form">
<el-form size="mini" label-width="72px">
<el-form-item label="钢卷编号">
<el-input v-model="query.inMatNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="query.dateRange" type="daterange"
range-separator="~" start-placeholder="开始" end-placeholder="结束"
value-format="yyyy-MM-dd" style="width:100%" />
</el-form-item>
<el-form-item label=" " label-width="72px">
<el-checkbox v-model="query.hideFinished">不显示生产完成</el-checkbox>
</el-form-item>
<el-form-item label=" " label-width="72px">
<el-button size="mini" type="primary" icon="el-icon-search" @click="loadList">条件查询</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="resetQuery">全部显示</el-button>
</el-form-item>
</el-form>
</div>
<div class="section-header" style="margin-top:8px"><span>队列操作</span></div>
<div class="op-buttons">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="handleAdd">钢卷增加</el-button>
<el-button size="mini" icon="el-icon-edit" :disabled="!selectedPlan" @click="handleEdit">修改</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" :disabled="!selectedPlan" @click="handleDelete">删除</el-button>
<el-button size="mini" icon="el-icon-top" :disabled="!selectedPlan" @click="handleMoveUp">Up 上移</el-button>
<el-button size="mini" icon="el-icon-bottom" :disabled="!selectedPlan" @click="handleMoveDown">Down 下移</el-button>
</div>
<div class="section-header" style="margin-top:8px"><span>工艺方案</span></div>
<div class="op-buttons">
<el-button size="mini" icon="el-icon-document-add" @click="openQuickRecipe">快速新增方案</el-button>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="footer-bar">
<div class="coil-info-block">
<span class="coil-label">当前带卷</span>
<span class="coil-field">卷号<b>{{ currentCoil.inMatNo || '—' }}</b></span>
<span class="coil-field">采料厚度<b>{{ currentCoil.inMatThick || '—' }} mm</b></span>
<span class="coil-field">成品厚度<b>{{ currentCoil.outThick || '—' }} mm</b></span>
<span class="coil-field">宽度<b>{{ currentCoil.inMatWidth || '—' }} mm</b></span>
<span class="coil-field">合金号<b>{{ currentCoil.alloyNo || '—' }}</b></span>
</div>
<div class="coil-divider"></div>
<div class="coil-info-block">
<span class="coil-label next">下一带卷</span>
<span class="coil-field">卷号<b>{{ nextCoil.inMatNo || '—' }}</b></span>
<span class="coil-field">采料厚度<b>{{ nextCoil.inMatThick || '—' }} mm</b></span>
<span class="coil-field">成品厚度<b>{{ nextCoil.outThick || '—' }} mm</b></span>
<span class="coil-field">宽度<b>{{ nextCoil.inMatWidth || '—' }} mm</b></span>
<span class="coil-field">合金号<b>{{ nextCoil.alloyNo || '—' }}</b></span>
</div>
</div>
<!-- 钢卷 新增/修改 对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="640px"
:close-on-click-modal="false" append-to-body>
<el-form :model="form" :rules="rules" ref="formRef" size="mini" label-width="90px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="钢卷编号" prop="inMatNo">
<el-input v-model="form.inMatNo" :disabled="!isNew" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合金牌号" prop="alloyNo">
<el-input v-model="form.alloyNo" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="工艺方案">
<el-select v-model="form.recipeId" filterable clearable placeholder="可选,不绑定也可保存"
style="width:100%" @change="onRecipeChange">
<el-option v-for="r in recipeOptions" :key="r.id"
:label="`${r.recipeNo}${r.alloyNo}`" :value="r.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料厚度" prop="inMatThick">
<el-input v-model="form.inMatThick"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成品厚度" prop="outThick">
<el-input v-model="form.outThick"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料宽度">
<el-input v-model="form.inMatWidth"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料长度">
<el-input v-model="form.inMatLength"><template slot="append">m</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料重量">
<el-input v-model="form.inMatWeight"><template slot="append">t</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料外径">
<el-input v-model="form.inMatOd"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料内径">
<el-input v-model="form.inMatId"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input v-model="form.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button size="mini" @click="dialogVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="handleSave">确定</el-button>
</div>
</el-dialog>
<!-- 快速新增工艺方案 对话框 -->
<el-dialog title="快速新增工艺方案" :visible.sync="recipeDialogVisible" width="700px"
:close-on-click-modal="false" append-to-body>
<el-form :model="recipeForm" :rules="recipeRules" ref="recipeFormRef" size="mini" label-width="88px">
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="方案记录号" prop="recipeNo">
<el-input v-model="recipeForm.recipeNo" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合金号" prop="alloyNo">
<el-input v-model="recipeForm.alloyNo" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="原料厚度" prop="inThick">
<el-input v-model="recipeForm.inThick"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="成品厚度" prop="outThick">
<el-input v-model="recipeForm.outThick"><template slot="append">mm</template></el-input>
</el-form-item>
</el-col>
</el-row>
<!-- 道次列表 -->
<div class="recipe-pass-header">
<span>道次参数</span>
<div>
<el-button size="mini" icon="el-icon-plus" @click="addRecipePass">增加道次</el-button>
<el-button size="mini" icon="el-icon-minus" @click="removeRecipePass" :disabled="recipePassList.length===0">删除末道</el-button>
</div>
</div>
<el-table :data="recipePassList" border size="mini" max-height="280">
<el-table-column label="道次" prop="passNo" width="45" align="center" />
<el-table-column label="入口厚(mm)" width="90">
<template slot-scope="{ row }">
<el-input v-model="row.inThick" size="mini" @blur="calcRecipePass(row)" />
</template>
</el-table-column>
<el-table-column label="出口厚(mm)" width="90">
<template slot-scope="{ row }">
<el-input v-model="row.outThick" size="mini" @blur="calcRecipePass(row)" />
</template>
</el-table-column>
<el-table-column label="宽度(mm)" width="80">
<template slot-scope="{ row }"><el-input v-model="row.width" size="mini" /></template>
</el-table-column>
<el-table-column label="轧制力(kN)" width="85">
<template slot-scope="{ row }"><el-input v-model="row.rollForce" size="mini" /></template>
</el-table-column>
<el-table-column label="入口张力" width="80">
<template slot-scope="{ row }"><el-input v-model="row.inTension" size="mini" /></template>
</el-table-column>
<el-table-column label="出口张力" width="80">
<template slot-scope="{ row }"><el-input v-model="row.outTension" size="mini" /></template>
</el-table-column>
<el-table-column label="最高速度" width="80">
<template slot-scope="{ row }"><el-input v-model="row.maxSpeed" size="mini" /></template>
</el-table-column>
<el-table-column label="压下量" prop="reduction" width="75" align="right">
<template slot-scope="{ row }"><span class="calc-val">{{ row.reduction }}</span></template>
</el-table-column>
</el-table>
</el-form>
<div slot="footer">
<el-button size="mini" @click="recipeDialogVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="saveQuickRecipe">保存方案</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPlan, addPlan, updatePlan, delPlan, moveUpPlan, moveDownPlan } from '@/api/mill/plan'
import { listAllRecipe, getRecipeDetail, addRecipe } from '@/api/mill/recipe'
const emptyForm = () => ({
id: null, inMatNo: '', alloyNo: '', recipeId: null, recipeNo: '',
outThick: '', inMatThick: '', inMatWidth: '', inMatLength: '',
inMatWeight: '', inMatOd: '', inMatId: '', passCount: 0, remark: ''
})
const emptyPass = (no) => ({
passNo: no, inThick: '', outThick: '', width: '',
rollForce: '', inTension: '', outTension: '',
maxSpeed: '', inUnitTension: '', outUnitTension: '',
reduction: '', totalReduction: '', remark: ''
})
export default {
name: 'MillPlan',
data() {
return {
planList: [],
selectedPlan: null,
passList: [],
recipeOptions: [],
query: { inMatNo: '', dateRange: null, hideFinished: false },
// 钢卷对话框
dialogVisible: false,
isNew: true,
form: emptyForm(),
rules: {
inMatNo: [{ required: true, message: '请输入钢卷编号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金牌号', trigger: 'blur' }],
inMatThick: [{ required: true, message: '请输入采料厚度', trigger: 'blur' }],
outThick: [{ required: true, message: '请输入成品厚度', trigger: 'blur' }],
},
// 快速新增方案对话框
recipeDialogVisible: false,
recipeForm: { recipeNo: '', alloyNo: '', inThick: '', outThick: '', outWidth: '' },
recipePassList: [],
recipeRules: {
recipeNo: [{ required: true, message: '请输入方案记录号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金号', trigger: 'blur' }],
inThick: [{ required: true, message: '请输入原料厚度', trigger: 'blur' }],
outThick: [{ required: true, message: '请输入成品厚度', trigger: 'blur' }],
}
}
},
computed: {
dialogTitle() { return this.isNew ? '新增钢卷' : '修改钢卷' },
currentCoil() {
return this.planList.find(p => p.prodStatus === '1') || {}
},
nextCoil() {
const idx = this.planList.findIndex(p => p.prodStatus === '1')
if (idx >= 0 && idx + 1 < this.planList.length) return this.planList[idx + 1]
return this.planList.find(p => p.prodStatus === '0') || {}
}
},
mounted() {
this.loadList()
this.refreshRecipeOptions()
},
methods: {
refreshRecipeOptions() {
listAllRecipe({}).then(res => { this.recipeOptions = res.data || [] })
},
loadList() {
const params = { inMatNo: this.query.inMatNo }
if (this.query.hideFinished) params.hideProdStatus = '2'
if (this.query.dateRange && this.query.dateRange.length === 2) {
params.beginDate = this.query.dateRange[0]
params.endDate = this.query.dateRange[1]
}
listPlan(params).then(res => {
this.planList = res.data || []
})
},
resetQuery() {
this.query = { inMatNo: '', dateRange: null, hideFinished: false }
this.loadList()
},
handleQueueSelect(row) {
this.selectedPlan = row
if (!row || !row.recipeId) { this.passList = []; return }
getRecipeDetail(row.recipeId).then(res => {
this.passList = (res.data && res.data.passList) || []
})
},
handleAdd() {
this.isNew = true
this.form = emptyForm()
this.dialogVisible = true
},
handleEdit() {
this.isNew = false
this.form = { ...this.selectedPlan }
this.dialogVisible = true
},
handleSave() {
this.$refs.formRef.validate(valid => {
if (!valid) return
if (this.form.recipeId) {
const matched = this.recipeOptions.find(r => r.id === this.form.recipeId)
if (matched) { this.form.recipeNo = matched.recipeNo; this.form.passCount = matched.passCount }
}
const api = this.isNew ? addPlan : updatePlan
api(this.form).then(() => {
this.$message.success('保存成功')
this.dialogVisible = false
this.loadList()
})
})
},
handleDelete() {
this.$confirm(`确定删除钢卷「${this.selectedPlan.inMatNo}」?`, '提示', { type: 'warning' }).then(() => {
delPlan(this.selectedPlan.id).then(() => {
this.$message.success('删除成功')
this.selectedPlan = null
this.passList = []
this.loadList()
})
})
},
handleMoveUp() { moveUpPlan(this.selectedPlan.id).then(() => this.loadList()) },
handleMoveDown() { moveDownPlan(this.selectedPlan.id).then(() => this.loadList()) },
onRecipeChange(recipeId) {
if (!recipeId) return
const r = this.recipeOptions.find(x => x.id === recipeId)
if (r) {
if (!this.form.alloyNo) this.form.alloyNo = r.alloyNo
if (!this.form.inMatThick) this.form.inMatThick = r.inThick
if (!this.form.outThick) this.form.outThick = r.outThick
if (!this.form.inMatWidth) this.form.inMatWidth = r.outWidth
}
},
// 快速新增方案
openQuickRecipe() {
this.recipeForm = { recipeNo: '', alloyNo: '', inThick: '', outThick: '', outWidth: '' }
this.recipePassList = []
this.recipeDialogVisible = true
},
addRecipePass() {
this.recipePassList.push(emptyPass(this.recipePassList.length + 1))
},
removeRecipePass() {
if (this.recipePassList.length) this.recipePassList.pop()
},
calcRecipePass(row) {
const inT = parseFloat(row.inThick) || 0
const outT = parseFloat(row.outThick) || 0
row.reduction = outT > 0 ? (inT - outT).toFixed(3) : ''
},
saveQuickRecipe() {
this.$refs.recipeFormRef.validate(valid => {
if (!valid) return
const data = {
...this.recipeForm,
passCount: this.recipePassList.length,
passList: this.recipePassList
}
addRecipe(data).then(() => {
this.$message.success('工艺方案新增成功')
this.recipeDialogVisible = false
this.refreshRecipeOptions()
})
})
},
queueRowClass({ row }) {
if (row.prodStatus === '1') return 'row-rolling'
if (row.prodStatus === '0' && this.currentCoil.sortNo && row.sortNo === this.currentCoil.sortNo + 1) return 'row-next'
return ''
},
passRowClass({ rowIndex }) {
return rowIndex % 2 === 0 ? '' : 'alt-row'
},
prodStatusLabel(s) {
return { '0': '待轧', '1': '轧制中', '2': '已完成', '9': '异常' }[s] || s
},
prodStatusClass(s) {
return { '0': 'status-wait', '1': 'status-rolling', '2': 'status-done', '9': 'status-err' }[s] || ''
}
}
}
</script>
<style scoped lang="scss">
.plan-page {
display: flex;
flex-direction: column;
height: calc(100vh - 84px);
background: #f0f2f5;
padding: 8px 12px;
box-sizing: border-box;
gap: 8px;
}
.section-header {
background: #1c2b3a;
color: #ecf0f1;
padding: 6px 12px;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
border-radius: 3px 3px 0 0;
}
.recipe-tag {
font-size: 11px;
font-weight: 400;
color: #a9bcd0;
}
/* ── 上方队列 ── */
.queue-section {
background: #fff;
border: 1px solid #dde1e6;
border-radius: 3px;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.queue-table {
::v-deep .row-rolling td { background: #fef3e2 !important; }
::v-deep .row-next td { background: #fdfbe4 !important; }
::v-deep .el-table__row.current-row td { background: #e8f0fb !important; }
}
/* ── 下方区域 ── */
.bottom-section {
display: flex;
flex: 1;
gap: 8px;
overflow: hidden;
}
.pass-panel {
flex: 1;
background: #fff;
border: 1px solid #dde1e6;
border-radius: 3px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pass-table {
flex: 1;
::v-deep .el-table__row.alt-row td { background: #f7f9fc !important; }
}
.calc-val {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #1d4e89;
}
/* 右侧操作区 */
.op-panel {
width: 260px;
flex-shrink: 0;
background: #fff;
border: 1px solid #dde1e6;
border-radius: 3px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.query-form {
padding: 10px 12px 4px;
border-bottom: 1px solid #e4e7ed;
}
.op-buttons {
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
border-bottom: 1px solid #e4e7ed;
.el-button { width: 100%; justify-content: flex-start; }
}
/* ── 底部状态栏 ── */
.footer-bar {
background: #1c2b3a;
border-radius: 3px;
padding: 6px 16px;
display: flex;
align-items: center;
flex-shrink: 0;
font-size: 11px;
}
.coil-info-block {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
}
.coil-label {
color: #a9bcd0;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
&.next { color: #f0b429; }
}
.coil-field {
color: #bdc3c7;
white-space: nowrap;
b { color: #ecf0f1; }
}
.coil-divider {
width: 1px;
height: 20px;
background: #2e4057;
margin: 0 20px;
}
/* 状态标签 */
.status-wait { color: #7f8c8d; }
.status-rolling { color: #d68910; font-weight: 700; }
.status-done { color: #2471a3; }
.status-err { color: #c0392b; font-weight: 700; }
/* 快速新增方案弹窗内道次表头 */
.recipe-pass-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0 4px;
font-size: 12px;
font-weight: 700;
color: #1c2b3a;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 6px;
}
</style>