feat(mill): 完成工艺管理与生产计划全栈业务模块
- 新增三张业务表 SQL:mill_process_recipe / mill_process_pass / mill_production_plan - 后端:Domain + Mapper + MyBatis XML + Service + Controller(工艺方案 & 生产计划) - 生产计划支持队列排序(sortNo)、上移/下移、软删除 - 工艺方案支持道次批量保存、事务管理 - 前端:工艺管理页(左侧方案列表 + 右侧表单 + 道次内联表格) - 前端:生产计划页(轧制队列 + 轧制工艺展示 + 操作面板 + 底部带卷状态栏) - 注册 /mill/process 与 /mill/plan 前端路由 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
432
ruoyi-ui/src/views/mill/process.vue
Normal file
432
ruoyi-ui/src/views/mill/process.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="process-page">
|
||||
|
||||
<!-- 左侧:方案列表 -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<span>工艺方案</span>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchKey" placeholder="方案号/合金号" size="mini" clearable
|
||||
prefix-icon="el-icon-search" @input="loadList" />
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="recipe-list">
|
||||
<div v-for="r in recipeList" :key="r.id"
|
||||
:class="['recipe-item', { active: selectedId === r.id }]"
|
||||
@click="handleSelect(r)">
|
||||
<div class="recipe-item__no">{{ r.recipeNo }}</div>
|
||||
<div class="recipe-item__info">
|
||||
<span>{{ r.alloyNo }}</span>
|
||||
<span>{{ r.inThick }} → {{ r.outThick }} mm</span>
|
||||
<span>{{ r.passCount }} 道次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recipeList.length === 0" class="empty-tip">暂无方案</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:方案详情 -->
|
||||
<div class="right-panel">
|
||||
<div v-if="!form.id && !isNew" class="no-select">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>请在左侧选择工艺方案</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="detail-header">
|
||||
<span>{{ isNew ? '新建工艺方案' : form.recipeNo }}</span>
|
||||
<div class="btn-group">
|
||||
<el-button size="mini" type="primary" icon="el-icon-check" @click="handleSave">保存</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="handleReset">重置</el-button>
|
||||
<el-button v-if="!isNew" size="mini" type="danger" icon="el-icon-delete" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方案基本信息 -->
|
||||
<el-form :model="form" :rules="rules" ref="formRef" size="mini" label-width="88px" class="recipe-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="方案记录号" prop="recipeNo">
|
||||
<el-input v-model="form.recipeNo" :disabled="!isNew" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-form-item label="合金号" prop="alloyNo">
|
||||
<el-input v-model="form.alloyNo" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="原料厚度" prop="inThick">
|
||||
<el-input v-model="form.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="form.outThick"><template slot="append">mm</template></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="成品宽度" prop="outWidth">
|
||||
<el-input v-model="form.outWidth"><template slot="append">mm</template></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<!-- 道次详情 -->
|
||||
<div class="pass-section">
|
||||
<div class="pass-header">
|
||||
<span>道次参数</span>
|
||||
<div class="btn-group">
|
||||
<el-button size="mini" icon="el-icon-plus" @click="addPass">增加道次</el-button>
|
||||
<el-button size="mini" icon="el-icon-minus" @click="removeLastPass" :disabled="passList.length===0">删除末道</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="passList" border size="mini" class="pass-table"
|
||||
:row-class-name="passRowClass" height="calc(100vh - 340px)">
|
||||
<el-table-column label="道次" prop="passNo" width="50" align="center" fixed>
|
||||
<template slot-scope="{ row }">
|
||||
<b>{{ row.passNo }}</b>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入口厚度(mm)" width="100" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.inThick" size="mini" @blur="calcPass(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出口厚度(mm)" width="100" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.outThick" size="mini" @blur="calcPass(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="宽度(mm)" width="90" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.width" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="轧制力(kN)" width="90" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.rollForce" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入口张力(kN)" width="95" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.inTension" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出口张力(kN)" width="95" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.outTension" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高速度(m/min)" width="110" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.maxSpeed" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入口单位张力" width="100" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.inUnitTension" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出口单位张力" width="100" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.outUnitTension" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="压下量(mm)" prop="reduction" width="90" 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-column label="备注" min-width="100">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.remark" size="mini" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listRecipe, getRecipeDetail, addRecipe, updateRecipe, delRecipe } from '@/api/mill/recipe'
|
||||
|
||||
const emptyPass = (no) => ({
|
||||
passNo: no, inThick: '', outThick: '', width: '',
|
||||
rollForce: '', inTension: '', outTension: '',
|
||||
maxSpeed: '', inUnitTension: '', outUnitTension: '',
|
||||
reduction: '', totalReduction: '', remark: ''
|
||||
})
|
||||
|
||||
const emptyForm = () => ({
|
||||
id: null, recipeNo: '', alloyNo: '', passCount: 0,
|
||||
inThick: '', outThick: '', outWidth: '', status: '0', remark: ''
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'MillProcess',
|
||||
data() {
|
||||
return {
|
||||
searchKey: '',
|
||||
recipeList: [],
|
||||
selectedId: null,
|
||||
isNew: false,
|
||||
form: emptyForm(),
|
||||
passList: [],
|
||||
rules: {
|
||||
recipeNo: [{ required: true, message: '请输入方案记录号', trigger: 'blur' }],
|
||||
alloyNo: [{ required: true, message: '请输入合金号', trigger: 'blur' }],
|
||||
inThick: [{ required: true, message: '请输入原料厚度', trigger: 'blur' }],
|
||||
outThick: [{ required: true, message: '请输入成品厚度', trigger: 'blur' }],
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() { this.loadList() },
|
||||
methods: {
|
||||
loadList() {
|
||||
listRecipe({ recipeNo: this.searchKey, alloyNo: this.searchKey }).then(res => {
|
||||
this.recipeList = res.rows || []
|
||||
})
|
||||
},
|
||||
handleSelect(r) {
|
||||
this.selectedId = r.id
|
||||
this.isNew = false
|
||||
getRecipeDetail(r.id).then(res => {
|
||||
this.form = { ...res.data }
|
||||
this.passList = res.data.passList || []
|
||||
})
|
||||
},
|
||||
handleAdd() {
|
||||
this.isNew = true
|
||||
this.selectedId = null
|
||||
this.form = emptyForm()
|
||||
this.passList = []
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.formRef.validate(valid => {
|
||||
if (!valid) return
|
||||
this.form.passList = this.passList
|
||||
this.form.passCount = this.passList.length
|
||||
const api = this.isNew ? addRecipe : updateRecipe
|
||||
api(this.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.loadList()
|
||||
this.isNew = false
|
||||
})
|
||||
})
|
||||
},
|
||||
handleReset() {
|
||||
if (this.isNew) { this.form = emptyForm(); this.passList = [] }
|
||||
else this.handleSelect({ id: this.selectedId })
|
||||
},
|
||||
handleDelete() {
|
||||
this.$confirm('确定删除该方案?', '提示', { type: 'warning' }).then(() => {
|
||||
delRecipe([this.form.id]).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.form = emptyForm(); this.passList = []
|
||||
this.selectedId = null; this.isNew = false
|
||||
this.loadList()
|
||||
})
|
||||
})
|
||||
},
|
||||
addPass() {
|
||||
this.passList.push(emptyPass(this.passList.length + 1))
|
||||
},
|
||||
removeLastPass() {
|
||||
if (this.passList.length) this.passList.pop()
|
||||
},
|
||||
calcPass(row) {
|
||||
const inT = parseFloat(row.inThick) || 0
|
||||
const outT = parseFloat(row.outThick) || 0
|
||||
row.reduction = outT > 0 ? (inT - outT).toFixed(3) : ''
|
||||
// 累计总压下量
|
||||
let total = 0
|
||||
const base = parseFloat(this.form.inThick) || 0
|
||||
for (const p of this.passList) {
|
||||
const pOut = parseFloat(p.outThick) || 0
|
||||
if (base > 0 && pOut > 0) {
|
||||
total = parseFloat((base - pOut).toFixed(3))
|
||||
p.totalReduction = total
|
||||
}
|
||||
}
|
||||
},
|
||||
passRowClass({ rowIndex }) {
|
||||
return rowIndex % 2 === 0 ? '' : 'alt-row'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.process-page {
|
||||
display: flex;
|
||||
height: calc(100vh - 84px);
|
||||
gap: 0;
|
||||
background: #f0f2f5;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── 左侧面板 ── */
|
||||
.left-panel {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #dde1e6;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: #1c2b3a;
|
||||
color: #ecf0f1;
|
||||
padding: 7px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recipe-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
|
||||
&:hover { background: #f7f9fc; }
|
||||
&.active { background: #e8f0fb; border-left: 3px solid #1d4e89; }
|
||||
|
||||
&__no {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #1c2b3a;
|
||||
}
|
||||
&__info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: #7f8c8d;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #bdc3c7;
|
||||
padding: 24px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── 右侧面板 ── */
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #dde1e6;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #bdc3c7;
|
||||
font-size: 13px;
|
||||
i { font-size: 48px; margin-bottom: 10px; }
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #1c2b3a;
|
||||
color: #ecf0f1;
|
||||
padding: 7px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-group { display: flex; gap: 6px; }
|
||||
|
||||
.recipe-form {
|
||||
padding: 10px 12px 4px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
/* ── 道次区域 ── */
|
||||
.pass-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pass-header {
|
||||
padding: 6px 12px;
|
||||
background: #f7f9fc;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #1c2b3a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pass-table {
|
||||
flex: 1;
|
||||
|
||||
::v-deep .el-table__row.alt-row td {
|
||||
background: #f7f9fc !important;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
text-align: right;
|
||||
padding: 0 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.calc-val {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #1d4e89;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user