1完成酸轧轧辊调整

2完成双机架工艺规格串联
3完成双机架计划串联
4完成双机架wip快捷录入检索
5完成双机架实绩串联
This commit is contained in:
2026-05-19 17:13:37 +08:00
parent 417783e64a
commit 53a180787b
46 changed files with 5592 additions and 231 deletions

View File

@@ -0,0 +1,468 @@
<template>
<div class="actual-page">
<div class="body-wrap">
<!-- 实绩表格 -->
<div class="table-card">
<div class="card-header">
<span class="card-title">双机架实绩</span>
<el-radio-group v-model="query.planStatus" size="mini" @change="onStatusChange">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="0">准备</el-radio-button>
<el-radio-button label="1">上线</el-radio-button>
<el-radio-button label="2">生产中</el-radio-button>
<el-radio-button label="3">生产完成</el-radio-button>
</el-radio-group>
<el-button size="mini" icon="el-icon-refresh" style="margin-left:8px" @click="loadList">刷新</el-button>
</div>
<el-table
v-loading="loading"
:data="list"
size="mini"
border
highlight-current-row
:height="tableHeight"
style="width:100%"
@row-click="handleRowClick"
>
<el-table-column type="index" width="40" align="center" />
<el-table-column label="计划号" prop="planNo" width="150" show-overflow-tooltip />
<el-table-column label="入场钢卷号" prop="enterCoilNo" min-width="120" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.enterCoilNo || row.inMatNo || '—' }}</template>
</el-table-column>
<el-table-column label="出口钢卷号" prop="currentCoilNo" min-width="120" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.currentCoilNo || '—' }}</template>
</el-table-column>
<el-table-column label="合金牌号" prop="alloyNo" width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.alloyNo || '—' }}</template>
</el-table-column>
<el-table-column label="计划状态" width="80" align="center">
<template slot-scope="{ row }">
<el-tag :type="planStatusType(row.planStatus)" size="mini">
{{ planStatusLabel(row.planStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="生产状态" width="80" align="center">
<template slot-scope="{ row }">
<el-tag :type="prodStatusType(row.prodStatus)" size="mini" effect="plain">
{{ row.prodStatus || '—' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="来料厚(mm)" width="82" align="right">
<template slot-scope="{ row }">{{ row.inMatThick || '—' }}</template>
</el-table-column>
<el-table-column label="来料宽(mm)" width="82" align="right">
<template slot-scope="{ row }">{{ row.inMatWidth || '—' }}</template>
</el-table-column>
<el-table-column label="来料重(t)" width="76" align="right">
<template slot-scope="{ row }">{{ row.inMatWeight || '—' }}</template>
</el-table-column>
<el-table-column label="成品厚(mm)" width="82" align="right">
<template slot-scope="{ row }">
<span class="accent-val">{{ row.outThick || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="道次数" prop="passCount" width="60" align="center">
<template slot-scope="{ row }">{{ row.passCount || '—' }}</template>
</el-table-column>
<el-table-column label="工艺方案" prop="recipeNo" width="110" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.recipeNo || '—' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="148">
<template slot-scope="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column label="创建人" prop="createBy" width="80" align="center">
<template slot-scope="{ row }">{{ row.createBy || '—' }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-pagination
small
layout="total, sizes, prev, pager, next"
:page-sizes="[20, 50, 100]"
:total="total"
:page-size="query.pageSize"
:current-page="query.pageNum"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 右侧面板 -->
<div class="side-panel">
<!-- 查找 -->
<div class="panel-block">
<div class="panel-title">查找</div>
<div class="filter-item">
<span class="filter-label">钢卷号</span>
<el-input
v-model="query.inMatNo"
size="mini" clearable
placeholder="入场/出口钢卷号"
style="width:100%"
/>
</div>
<div class="filter-item">
<span class="filter-label">开始时间</span>
<el-date-picker
v-model="query.createStartTime"
type="datetime"
size="mini"
style="width:100%"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="创建开始时间"
/>
</div>
<div class="filter-item">
<span class="filter-label">结束时间</span>
<el-date-picker
v-model="query.createEndTime"
type="datetime"
size="mini"
style="width:100%"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="创建结束时间"
/>
</div>
<div class="filter-actions">
<el-button type="primary" size="mini" :loading="loading" @click="handleSearch">查找</el-button>
<el-button size="mini" @click="handleReset">重置</el-button>
</div>
</div>
<!-- 选中行详情 -->
<div v-if="selectedRow" class="panel-block detail-block">
<div class="panel-title">计划详情</div>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-key">计划号</span>
<span class="detail-val">{{ selectedRow.planNo }}</span>
</div>
<div class="detail-row">
<span class="detail-key">入场卷号</span>
<span class="detail-val">{{ selectedRow.enterCoilNo || selectedRow.inMatNo || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">出口卷号</span>
<span class="detail-val">{{ selectedRow.currentCoilNo || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">合金牌号</span>
<span class="detail-val">{{ selectedRow.alloyNo || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">来料厚</span>
<span class="detail-val accent">{{ selectedRow.inMatThick ? selectedRow.inMatThick + ' mm' : '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">来料宽</span>
<span class="detail-val accent">{{ selectedRow.inMatWidth ? selectedRow.inMatWidth + ' mm' : '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">来料重</span>
<span class="detail-val accent">{{ selectedRow.inMatWeight ? selectedRow.inMatWeight + ' t' : '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">来料长</span>
<span class="detail-val">{{ selectedRow.inMatLength ? selectedRow.inMatLength + ' m' : '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">成品厚</span>
<span class="detail-val accent">{{ selectedRow.outThick ? selectedRow.outThick + ' mm' : '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">道次数</span>
<span class="detail-val">{{ selectedRow.passCount || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">工艺方案</span>
<span class="detail-val">{{ selectedRow.recipeNo || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">计划状态</span>
<span class="detail-val">
<el-tag :type="planStatusType(selectedRow.planStatus)" size="mini">
{{ planStatusLabel(selectedRow.planStatus) }}
</el-tag>
</span>
</div>
<div class="detail-row">
<span class="detail-key">生产状态</span>
<span class="detail-val">
<el-tag :type="prodStatusType(selectedRow.prodStatus)" size="mini" effect="plain">
{{ selectedRow.prodStatus || '—' }}
</el-tag>
</span>
</div>
<div class="detail-row">
<span class="detail-key">创建人</span>
<span class="detail-val">{{ selectedRow.createBy || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-key">创建时间</span>
<span class="detail-val">{{ formatDate(selectedRow.createTime) }}</span>
</div>
<div v-if="selectedRow.remark" class="detail-row">
<span class="detail-key">备注</span>
<span class="detail-val">{{ selectedRow.remark }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { listDrActualPage } from '@/api/wms/drMill'
const PLAN_STATUS = {
'0': { label: '准备', type: 'info' },
'1': { label: '上线', type: 'primary' },
'2': { label: '生产中', type: 'warning' },
'3': { label: '生产完成', type: 'success' },
}
const PROD_STATUS_TYPE = {
Idle: 'info',
Rolling: 'warning',
NextCoil: 'primary',
Done: 'success',
Error: 'danger',
}
export default {
name: 'DrActual',
data() {
return {
loading: false,
list: [],
total: 0,
selectedRow: null,
query: {
pageNum: 1,
pageSize: 50,
planStatus: '',
inMatNo: '',
createStartTime: '',
createEndTime: '',
},
}
},
computed: {
tableHeight() { return 'calc(100vh - 210px)' }
},
created() { this.loadList() },
methods: {
loadList() {
this.loading = true
const params = {
...this.query,
planStatus: this.query.planStatus || undefined,
inMatNo: this.query.inMatNo || undefined,
createStartTime: this.query.createStartTime || undefined,
createEndTime: this.query.createEndTime || undefined,
}
listDrActualPage(params)
.then(res => {
this.list = res.rows || []
this.total = res.total || 0
})
.finally(() => { this.loading = false })
},
handlePageChange(page) {
this.query.pageNum = page
this.loadList()
},
handleSizeChange(size) {
this.query.pageSize = size
this.query.pageNum = 1
this.loadList()
},
onStatusChange() {
this.query.pageNum = 1
this.loadList()
},
handleRowClick(row) {
this.selectedRow = (this.selectedRow && this.selectedRow.planId === row.planId) ? null : row
},
handleSearch() {
this.query.pageNum = 1
this.loadList()
},
handleReset() {
this.query.planStatus = ''
this.query.inMatNo = ''
this.query.createStartTime = ''
this.query.createEndTime = ''
this.query.pageNum = 1
this.selectedRow = null
this.loadList()
},
planStatusLabel(s) { return (PLAN_STATUS[s] || { label: s || '—' }).label },
planStatusType(s) { return (PLAN_STATUS[s] || { type: '' }).type },
prodStatusType(s) { return PROD_STATUS_TYPE[s] || 'info' },
formatDate(v) {
if (!v) return '—'
return String(v).replace('T', ' ').substring(0, 19)
},
}
}
</script>
<style scoped lang="scss">
.actual-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
padding: 10px 12px;
box-sizing: border-box;
overflow: hidden;
}
.body-wrap {
flex: 1;
min-height: 0;
display: flex;
gap: 8px;
overflow: hidden;
}
/* ── 表格卡片 ── */
.table-card {
flex: 1;
min-width: 0;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.card-header {
display: flex;
align-items: center;
padding: 10px 14px;
border-bottom: 1px solid #e4e7ed;
flex-shrink: 0;
gap: 8px;
}
.card-title {
font-size: 13px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
.table-footer {
display: flex;
justify-content: flex-end;
padding: 6px 10px;
border-top: 1px solid #e4e7ed;
background: #fafafa;
flex-shrink: 0;
}
.accent-val {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #409eff;
}
/* ── 侧边面板 ── */
.side-panel {
width: 220px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
.panel-block {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: #303133;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
margin-bottom: 10px;
}
.filter-item { margin-bottom: 10px; }
.filter-label {
display: block;
font-size: 11px;
color: #909399;
margin-bottom: 4px;
}
.filter-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
/* ── 详情 ── */
.detail-grid {
display: flex;
flex-direction: column;
gap: 6px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4px;
}
.detail-key {
font-size: 11px;
color: #909399;
flex-shrink: 0;
padding-top: 2px;
}
.detail-val {
font-size: 12px;
color: #303133;
text-align: right;
word-break: break-all;
&.accent {
color: #409eff;
font-weight: 600;
font-family: 'Courier New', monospace;
}
}
</style>

View File

@@ -0,0 +1,516 @@
<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="240">
<el-table-column label="序号" prop="sortNo" width="50" align="center" fixed />
<el-table-column label="入场钢卷号" prop="enterCoilNo" width="115" />
<el-table-column label="当前钢卷号" prop="currentCoilNo" width="115" />
<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="采料重量(t)" prop="inMatWeight" width="100" align="right" />
<el-table-column label="道次数" prop="passCount" width="65" align="center" />
<el-table-column label="生产状态" prop="prodStatus" width="85" 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">
<template v-if="!selectedPlan">
<div class="section-header"><span>轧制工艺</span></div>
<div class="pass-empty">请在上方选择一条计划</div>
</template>
<template v-else-if="selectedPlan.recipeId && !passEditMode">
<div class="section-header">
<span>轧制工艺</span>
<div style="display:flex;align-items:center;gap:8px">
<span class="recipe-tag">{{ selectedPlan.recipeNo }}</span>
<span v-if="boundVersionCode" class="recipe-tag" style="color:#409eff;background:#ecf5ff">
{{ boundVersionCode }}
</span>
<el-button size="mini" icon="el-icon-edit" @click="enterEditMode">修改绑定</el-button>
</div>
</div>
<el-table :data="passList" border size="mini" class="pass-table"
:row-class-name="passRowClass" height="300">
<el-table-column label="道次" prop="passNo" width="45" align="center" fixed />
<el-table-column label="入口厚(mm)" prop="inThick" width="90" align="right" />
<el-table-column label="出口厚(mm)" prop="outThick" width="90" align="right" />
<el-table-column label="轧制力(kN)" prop="rollForce" width="85" align="right" />
<el-table-column label="入口张力(kN)" prop="inTension" width="90" align="right" />
<el-table-column label="出口张力(kN)" prop="outTension" width="90" align="right" />
<el-table-column label="最高速度" prop="maxSpeed" width="80" 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>
</template>
<template v-else-if="!selectedPlan.recipeId && !passEditMode">
<div class="section-header"><span>轧制工艺</span></div>
<div class="pass-empty">
<span>该计划尚未绑定工艺参数</span>
<div style="margin-top:12px">
<el-button size="mini" type="primary" icon="el-icon-document-copy" @click="openSelectRecipe">选择已有方案</el-button>
</div>
</div>
</template>
</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=" " 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" icon="el-icon-document" :disabled="!selectedPlan" @click="handleFinish">完成</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>
</div>
<!-- 新增/修改对话框 -->
<el-dialog :title="planDialogTitle" :visible.sync="planDialogVisible" width="680px"
:close-on-click-modal="false" append-to-body>
<el-form :model="form" :rules="rules" ref="formRef" size="mini" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="form.enterCoilNo"
@keyup.enter.native="onCoilNoInput" @blur="onCoilNoInput" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="当前钢卷号">
<el-input v-model="form.currentCoilNo" clearable />
</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="12">
<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.recipeId"
:label="`${r.recipeNo}${r.alloyNo}`" :value="r.recipeId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料厚度">
<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="成品厚度">
<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.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.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.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button size="mini" @click="planDialogVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="handleSave">确定</el-button>
</div>
</el-dialog>
<!-- 选择工艺方案 + 版本 -->
<el-dialog title="绑定工艺方案" :visible.sync="selectRecipeVisible" width="520px"
:close-on-click-modal="false" append-to-body>
<el-form size="small" label-width="72px">
<el-form-item label="工艺方案">
<el-select v-model="selectRecipeId" filterable clearable
placeholder="输入方案号或合金号搜索" style="width:100%"
@change="onSelectRecipeChange">
<el-option v-for="r in recipeOptions" :key="r.recipeId"
:label="`${r.recipeNo}${r.alloyNo}${r.inThick}→${r.outThick}mm`"
:value="r.recipeId" />
</el-select>
</el-form-item>
<el-form-item label="版本">
<el-select v-model="selectVersionId" placeholder="请先选择方案" style="width:100%"
:disabled="!selectRecipeId || !selectVersionList.length">
<el-option v-for="v in selectVersionList" :key="v.versionId"
:label="`${v.versionCode}${v.isActive === 1 ? '(当前激活)' : v.status === '1' ? '(已发布)' : '(草稿)'}`"
:value="v.versionId" />
</el-select>
<div v-if="selectRecipeId && !selectVersionList.length" style="font-size:12px;color:#f56c6c;margin-top:4px">
该方案暂无版本请先在规程页新增版本
</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="mini" @click="selectRecipeVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="confirmSelectRecipe"
:disabled="!selectVersionId">确定绑定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDrPlan, addDrPlan, updateDrPlan, delDrPlan, moveUpDrPlan, moveDownDrPlan, finishDrPlan,
listDrRecipe, getDrRecipeDetail,
listDrRecipeVersions, getDrRecipeVersionDetail,
queryCoilByNo } from '@/api/wms/drMill'
const emptyForm = () => ({
planId: null, planNo: '', enterCoilNo: '', currentCoilNo: '', inMatNo: '',
alloyNo: '', recipeId: null, recipeNo: '',
outThick: '', inMatThick: '', inMatWidth: '', inMatLength: '',
inMatWeight: '', inMatOd: '', inMatId: '', passCount: 0, remark: ''
})
export default {
name: 'DrPlanTab',
data() {
return {
planList: [],
selectedPlan: null,
passList: [],
recipeOptions: [],
query: { inMatNo: '', hideFinished: false },
passEditMode: false,
planDialogVisible: false,
isNew: true,
form: emptyForm(),
rules: {
enterCoilNo: [{ required: true, message: '请输入入场钢卷号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金牌号', trigger: 'blur' }],
},
selectRecipeVisible: false,
selectRecipeId: null,
selectVersionList: [], // 选方案后加载的版本列表
selectVersionId: null, // 用户选中的版本ID
boundVersionCode: '', // 当前计划绑定的版本号(展示用)
}
},
computed: {
planDialogTitle() { return this.isNew ? '新增双机架计划' : '修改双机架计划' }
},
mounted() {
this.loadList()
this.refreshRecipeOptions()
},
methods: {
refreshRecipeOptions() {
listDrRecipe({}).then(res => { this.recipeOptions = res.data || [] })
},
loadList() {
const params = { inMatNo: this.query.inMatNo }
if (this.query.hideFinished) params.planStatus = '0'
listDrPlan(params).then(res => { this.planList = res.data || [] })
},
resetQuery() {
this.query = { inMatNo: '', hideFinished: false }
this.loadList()
},
handleQueueSelect(row) {
this.selectedPlan = row
this.passList = []
this.boundVersionCode = ''
if (!row) return
if (row.versionId) {
// 优先用版本ID精确加载道次
getDrRecipeVersionDetail(row.versionId).then(res => {
this.passList = (res.data && res.data.passList) || []
this.boundVersionCode = res.data ? res.data.versionCode : ''
})
} else if (row.recipeId) {
// 兼容旧数据:取激活版本道次(后端 selectDetailById 已处理)
getDrRecipeDetail(row.recipeId).then(res => {
this.passList = (res.data && res.data.passList) || []
})
}
},
// 入场钢卷号变化 → 查 WMS 自动填入
onCoilNoInput() {
const v = (this.form.enterCoilNo || '').trim()
if (!v) return
this.form.inMatNo = v
queryCoilByNo(v).then(res => {
const c = res.data
if (!c) return
if (c.actualThickness && !this.form.inMatThick) this.form.inMatThick = c.actualThickness
if (c.actualWidth && !this.form.inMatWidth) this.form.inMatWidth = String(c.actualWidth)
if (c.netWeight && !this.form.inMatWeight) this.form.inMatWeight = String(c.netWeight)
if (c.length && !this.form.inMatLength) this.form.inMatLength = String(c.length)
if (c.currentCoilNo && !this.form.currentCoilNo) this.form.currentCoilNo = c.currentCoilNo
this.$message.success('已从 WMS 自动填入钢卷数据')
}).catch(() => {})
},
openSelectRecipe() {
this.selectRecipeId = null
this.selectVersionId = null
this.selectVersionList = []
this.selectRecipeVisible = true
},
onSelectRecipeChange(recipeId) {
this.selectVersionId = null
this.selectVersionList = []
if (!recipeId) return
listDrRecipeVersions(recipeId).then(res => {
this.selectVersionList = res.data || []
// 默认选中激活版本
const active = this.selectVersionList.find(v => v.isActive === 1)
if (active) this.selectVersionId = active.versionId
else if (this.selectVersionList.length) this.selectVersionId = this.selectVersionList[0].versionId
})
},
confirmSelectRecipe() {
if (!this.selectRecipeId) { this.$message.warning('请选择方案'); return }
if (!this.selectVersionId) { this.$message.warning('请选择版本'); return }
const r = this.recipeOptions.find(x => x.recipeId === this.selectRecipeId)
const v = this.selectVersionList.find(x => x.versionId === this.selectVersionId)
updateDrPlan({
...this.selectedPlan,
recipeId: this.selectRecipeId,
recipeNo: r ? r.recipeNo : '',
versionId: this.selectVersionId,
passCount: r ? r.passCount : 0,
}).then(() => {
this.$message.success('方案绑定成功')
this.selectRecipeVisible = false
this.selectedPlan = {
...this.selectedPlan,
recipeId: this.selectRecipeId,
recipeNo: r ? r.recipeNo : '',
versionId: this.selectVersionId,
}
getDrRecipeVersionDetail(this.selectVersionId).then(res => {
this.passList = (res.data && res.data.passList) || []
this.boundVersionCode = res.data ? res.data.versionCode : ''
})
this.loadList()
})
},
enterEditMode() { this.openSelectRecipe() },
onRecipeChange(recipeId) {
if (!recipeId) return
const r = this.recipeOptions.find(x => x.recipeId === 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
}
},
handleAdd() { this.isNew = true; this.form = emptyForm(); this.planDialogVisible = true },
handleEdit() { this.isNew = false; this.form = { ...this.selectedPlan }; this.planDialogVisible = true },
handleSave() {
this.$refs.formRef.validate(valid => {
if (!valid) return
if (this.form.recipeId) {
const r = this.recipeOptions.find(x => x.recipeId === this.form.recipeId)
if (r) { this.form.recipeNo = r.recipeNo; this.form.passCount = r.passCount }
}
const api = this.isNew ? addDrPlan : updateDrPlan
api(this.form).then(() => {
this.$message.success('保存成功')
this.planDialogVisible = false
this.loadList()
})
})
},
handleDelete() {
this.$confirm(`确定删除该计划?`, '提示', { type: 'warning' }).then(() => {
delDrPlan(this.selectedPlan.planId).then(() => {
this.$message.success('删除成功')
this.selectedPlan = null; this.passList = []
this.loadList()
})
})
},
handleFinish() {
finishDrPlan(this.selectedPlan.planId).then(() => {
this.$message.success('计划已完成')
this.loadList()
})
},
handleMoveUp() { moveUpDrPlan(this.selectedPlan.planId).then(() => this.loadList()) },
handleMoveDown() { moveDownDrPlan(this.selectedPlan.planId).then(() => this.loadList()) },
queueRowClass({ row }) {
if (row.prodStatus === 'Rolling') return 'row-rolling'
return ''
},
passRowClass({ rowIndex }) { return rowIndex % 2 === 0 ? '' : 'alt-row' },
prodStatusLabel(s) {
return { Idle: '待轧', Rolling: '轧制中', NextCoil: '下一卷', Done: '已完成', Error: '异常' }[s] || s
},
prodStatusClass(s) {
return { Idle: 'status-wait', Rolling: 'status-rolling', Done: 'status-done', Error: 'status-err' }[s] || ''
}
}
}
</script>
<style scoped lang="scss">
.plan-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
padding: 8px 12px;
box-sizing: border-box;
gap: 8px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
font-size: 13px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
.recipe-tag {
font-size: 12px;
font-weight: 400;
color: #909399;
background: #f0f2f5;
padding: 2px 8px;
border-radius: 2px;
}
.queue-section {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
flex-shrink: 0;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.queue-table {
::v-deep .row-rolling td { background: #fef3e2 !important; }
::v-deep .el-table__row.current-row td { background: #ecf5ff !important; }
}
.bottom-section { display: flex; flex: 1; gap: 8px; overflow: hidden; min-height: 0; }
.pass-panel {
flex: 1;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.pass-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
font-size: 13px;
}
.pass-table {
flex: 1;
::v-deep .el-table__row.alt-row td { background: #fafafa !important; }
}
.calc-val { font-family: 'Courier New', monospace; font-weight: 600; color: #409eff; }
.op-panel {
width: 260px;
flex-shrink: 0;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow-y: auto;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.query-form { padding: 10px 12px 4px; border-bottom: 1px solid #e4e7ed; }
.op-buttons {
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
.el-button { width: 100%; justify-content: flex-start; }
}
.status-wait { color: #909399; }
.status-rolling { color: #e6a23c; font-weight: 700; }
.status-done { color: #409eff; }
.status-err { color: #f56c6c; font-weight: 700; }
</style>

View File

@@ -0,0 +1,485 @@
<template>
<div class="process-page">
<!-- 左栏方案列表 -->
<div class="col-panel left-panel">
<div class="panel-header">
<span>工艺方案</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAddRecipe">新增</el-button>
</div>
<div class="search-bar">
<el-input v-model="searchKey" placeholder="方案号/合金号" size="mini" clearable
prefix-icon="el-icon-search" @input="loadRecipeList" />
</div>
<div class="item-list">
<div v-for="r in recipeList" :key="r.recipeId"
:class="['list-item', { active: selectedRecipeId === r.recipeId }]"
@click="handleSelectRecipe(r)">
<div class="item-main">{{ r.recipeNo }}</div>
<div class="item-sub">{{ r.alloyNo }} · {{ r.inThick }}{{ r.outThick }}mm</div>
</div>
<div v-if="!recipeList.length" class="empty-tip">暂无方案</div>
</div>
</div>
<!-- 中栏版本列表 -->
<div class="col-panel mid-panel">
<div class="panel-header">
<span>版本列表</span>
<el-button type="primary" size="mini" icon="el-icon-plus"
:disabled="!selectedRecipeId" @click="handleAddVersion">新增版本</el-button>
</div>
<div class="item-list">
<div v-for="v in versionList" :key="v.versionId"
:class="['list-item', { active: selectedVersionId === v.versionId }]"
@click="handleSelectVersion(v)">
<div class="item-main">
{{ v.versionCode }}
<el-tag v-if="v.isActive === 1" size="mini" type="success" style="margin-left:4px">激活</el-tag>
<el-tag v-else-if="v.status === '1'" size="mini" type="info" style="margin-left:4px">已发布</el-tag>
<el-tag v-else size="mini" style="margin-left:4px">草稿</el-tag>
</div>
<div class="item-sub">{{ v.remark }}</div>
<div class="item-actions">
<span v-if="v.isActive !== 1" class="action-link" @click.stop="handleActivate(v)">激活</span>
<span class="action-link danger" @click.stop="handleDeleteVersion(v)">删除</span>
</div>
</div>
<div v-if="selectedRecipeId && !versionList.length" class="empty-tip">暂无版本请新增</div>
<div v-if="!selectedRecipeId" class="empty-tip">请先选择方案</div>
</div>
</div>
<!-- 右栏版本详情 + 道次 -->
<div class="right-panel">
<div v-if="!versionForm.versionId && !isNewVersion" class="no-select">
<i class="el-icon-document"></i>
<p>请选择或新建版本</p>
</div>
<template v-else>
<!-- 版本基本信息 -->
<div class="detail-header">
<span>{{ isNewVersion ? '新建版本' : versionForm.versionCode }}</span>
<div class="btn-group">
<el-button size="mini" type="primary" icon="el-icon-check" @click="handleSaveVersion">保存</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="handleResetVersion">重置</el-button>
</div>
</div>
<el-form :model="versionForm" ref="versionFormRef" size="mini" label-width="76px"
class="version-form" :rules="versionRules">
<el-row :gutter="16">
<el-col :span="5">
<el-form-item label="版本号" prop="versionCode">
<el-input v-model="versionForm.versionCode" placeholder="如 V1.0" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="状态">
<el-select v-model="versionForm.status" style="width:100%">
<el-option label="草稿" value="0" />
<el-option label="已发布" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="备注">
<el-input v-model="versionForm.remark" clearable />
</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">删除末道</el-button>
</div>
</div>
<el-table :data="passList" border size="mini" class="pass-table"
:row-class-name="passRowClass" height="calc(100vh - 310px)">
<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)" width="90" align="right">
<template slot-scope="{ row }"><span class="calc-val">{{ row.reduction }}</span></template>
</el-table-column>
<el-table-column label="总压下量(mm)" 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>
<!-- 新增/编辑方案对话框 -->
<el-dialog :title="recipeDialogTitle" :visible.sync="recipeDialogVisible" width="520px" append-to-body>
<el-form :model="recipeForm" :rules="recipeRules" ref="recipeFormRef" size="small" label-width="88px">
<el-form-item label="方案记录号" prop="recipeNo">
<el-input v-model="recipeForm.recipeNo" :disabled="!!recipeForm.recipeId" />
</el-form-item>
<el-form-item label="合金号" prop="alloyNo">
<el-input v-model="recipeForm.alloyNo" />
</el-form-item>
<el-form-item label="原料厚度">
<el-input v-model="recipeForm.inThick"><template slot="append">mm</template></el-input>
</el-form-item>
<el-form-item label="成品厚度">
<el-input v-model="recipeForm.outThick"><template slot="append">mm</template></el-input>
</el-form-item>
<el-form-item label="成品宽度">
<el-input v-model="recipeForm.outWidth"><template slot="append">mm</template></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="recipeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRecipe">确定</el-button>
<el-button v-if="recipeForm.recipeId" type="danger" @click="handleDeleteRecipe">删除</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listDrRecipe, addDrRecipe, updateDrRecipe, delDrRecipe,
listDrRecipeVersions, getDrRecipeVersionDetail,
addDrRecipeVersion, updateDrRecipeVersion,
activateDrRecipeVersion, delDrRecipeVersion
} from '@/api/wms/drMill'
const emptyPass = (no) => ({
passNo: no, inThick: '', outThick: '', width: '',
rollForce: '', inTension: '', outTension: '',
maxSpeed: '', inUnitTension: '', outUnitTension: '',
reduction: '', totalReduction: '', remark: ''
})
const emptyVersionForm = () => ({
versionId: null, recipeId: null, versionCode: '', status: '0', remark: ''
})
export default {
name: 'DrProcessTab',
data() {
return {
// 方案
searchKey: '',
recipeList: [],
selectedRecipeId: null,
recipeDialogVisible: false,
recipeDialogTitle: '新增工艺方案',
recipeForm: {},
recipeRules: {
recipeNo: [{ required: true, message: '请输入方案记录号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金号', trigger: 'blur' }],
},
// 版本
versionList: [],
selectedVersionId: null,
isNewVersion: false,
versionForm: emptyVersionForm(),
versionRules: {
versionCode: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
},
// 道次
passList: [],
}
},
mounted() { this.loadRecipeList() },
methods: {
// ── 方案 ──────────────────────────────────────────────────
loadRecipeList() {
listDrRecipe({ recipeNo: this.searchKey, alloyNo: this.searchKey }).then(res => {
this.recipeList = res.data || []
})
},
handleSelectRecipe(r) {
this.selectedRecipeId = r.recipeId
this.isNewVersion = false
this.selectedVersionId = null
this.versionForm = emptyVersionForm()
this.passList = []
this.loadVersionList(r.recipeId)
},
handleAddRecipe() {
this.recipeForm = { recipeNo: '', alloyNo: '', inThick: '', outThick: '', outWidth: '', status: '0' }
this.recipeDialogTitle = '新增工艺方案'
this.recipeDialogVisible = true
},
handleEditRecipe(r) {
this.recipeForm = { ...r }
this.recipeDialogTitle = '编辑工艺方案'
this.recipeDialogVisible = true
},
submitRecipe() {
this.$refs.recipeFormRef.validate(valid => {
if (!valid) return
const api = this.recipeForm.recipeId ? updateDrRecipe : addDrRecipe
api(this.recipeForm).then(() => {
this.$message.success('保存成功')
this.recipeDialogVisible = false
this.loadRecipeList()
})
})
},
handleDeleteRecipe() {
this.$confirm('确定删除该方案及所有版本?', '提示', { type: 'warning' }).then(() => {
delDrRecipe([this.recipeForm.recipeId]).then(() => {
this.$message.success('删除成功')
this.recipeDialogVisible = false
this.selectedRecipeId = null
this.versionList = []
this.versionForm = emptyVersionForm()
this.passList = []
this.loadRecipeList()
})
})
},
// ── 版本 ──────────────────────────────────────────────────
loadVersionList(recipeId) {
listDrRecipeVersions(recipeId).then(res => {
this.versionList = res.data || []
})
},
handleSelectVersion(v) {
this.selectedVersionId = v.versionId
this.isNewVersion = false
getDrRecipeVersionDetail(v.versionId).then(res => {
this.versionForm = { ...res.data }
this.passList = (res.data.passList || []).map(p => ({ ...p }))
})
},
handleAddVersion() {
this.isNewVersion = true
this.selectedVersionId = null
this.versionForm = { ...emptyVersionForm(), recipeId: this.selectedRecipeId }
this.passList = []
},
handleSaveVersion() {
this.$refs.versionFormRef.validate(valid => {
if (!valid) return
this.versionForm.passList = this.passList
const api = this.isNewVersion ? addDrRecipeVersion : updateDrRecipeVersion
api(this.versionForm).then(res => {
this.$message.success('保存成功')
this.isNewVersion = false
if (res.data) this.versionForm.versionId = res.data
this.loadVersionList(this.selectedRecipeId)
})
})
},
handleResetVersion() {
if (this.isNewVersion) {
this.versionForm = { ...emptyVersionForm(), recipeId: this.selectedRecipeId }
this.passList = []
} else {
this.handleSelectVersion({ versionId: this.selectedVersionId })
}
},
handleActivate(v) {
this.$confirm(`确定激活版本 ${v.versionCode}`, '提示', { type: 'warning' }).then(() => {
activateDrRecipeVersion(v.versionId).then(() => {
this.$message.success('激活成功')
this.loadVersionList(this.selectedRecipeId)
if (this.selectedVersionId === v.versionId) {
this.versionForm.isActive = 1
}
})
})
},
handleDeleteVersion(v) {
this.$confirm(`确定删除版本 ${v.versionCode}`, '提示', { type: 'warning' }).then(() => {
delDrRecipeVersion(v.versionId).then(() => {
this.$message.success('删除成功')
if (this.selectedVersionId === v.versionId) {
this.selectedVersionId = null
this.versionForm = emptyVersionForm()
this.passList = []
}
this.loadVersionList(this.selectedRecipeId)
})
})
},
// ── 道次 ──────────────────────────────────────────────────
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) : ''
const base = parseFloat(this.passList[0] && this.passList[0].inThick) || 0
for (const p of this.passList) {
const pOut = parseFloat(p.outThick) || 0
if (base > 0 && pOut > 0) p.totalReduction = parseFloat((base - pOut).toFixed(3))
}
},
passRowClass({ rowIndex }) { return rowIndex % 2 === 0 ? '' : 'alt-row' }
}
}
</script>
<style scoped lang="scss">
.process-page {
display: flex;
height: 100%;
gap: 0;
background: #f5f7fa;
padding: 10px 12px;
box-sizing: border-box;
overflow: hidden;
}
.col-panel {
flex-shrink: 0;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
margin-right: 8px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.left-panel { width: 200px; }
.mid-panel { width: 220px; }
.panel-header {
background: #fff;
border-bottom: 1px solid #e4e7ed;
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
.search-bar { padding: 6px 8px; border-bottom: 1px solid #e4e7ed; flex-shrink: 0; }
.item-list { flex: 1; overflow-y: auto; }
.list-item {
padding: 8px 12px;
border-bottom: 1px solid #f0f2f5;
cursor: pointer;
transition: background .15s;
&:hover { background: #f5f7fa; }
&.active {
background: #ecf5ff;
border-left: 3px solid #409eff;
}
.item-main { font-size: 13px; font-weight: 600; color: #303133; }
.item-sub { font-size: 12px; color: #909399; margin-top: 2px; }
.item-actions {
display: flex; gap: 8px; margin-top: 4px;
.action-link {
font-size: 12px; color: #409eff; cursor: pointer;
&:hover { text-decoration: underline; }
&.danger { color: #f56c6c; }
}
}
}
.empty-tip { text-align: center; color: #c0c4cc; padding: 24px 0; font-size: 13px; }
.right-panel {
flex: 1;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.no-select {
flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; color: #c0c4cc; font-size: 13px;
i { font-size: 48px; margin-bottom: 10px; }
}
.detail-header {
background: #fff;
border-bottom: 1px solid #e4e7ed;
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
.btn-group { display: flex; gap: 6px; }
.version-form { padding: 8px 12px 4px; flex-shrink: 0; border-bottom: 1px solid #e4e7ed; }
.pass-section { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.pass-header {
padding: 8px 16px;
background: #fafafa;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
.pass-table {
flex: 1;
::v-deep .el-table__row.alt-row td { background: #fafafa !important; }
::v-deep .el-input__inner { text-align: right; padding: 0 4px !important; }
}
.calc-val { font-family: 'Courier New', monospace; font-weight: 600; color: #409eff; }
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div class="dr-container">
<div class="dr-sidebar">
<el-menu :default-active="activeMenu" class="sidebar-menu" @select="handleMenuSelect">
<el-menu-item index="plan">
<i class="el-icon-s-order"></i>
<span slot="title">计划</span>
</el-menu-item>
<el-menu-item index="processSpec">
<i class="el-icon-s-tools"></i>
<span slot="title">规程</span>
</el-menu-item>
<el-menu-item index="actual">
<i class="el-icon-s-data"></i>
<span slot="title">实绩</span>
</el-menu-item>
</el-menu>
</div>
<div style="flex:1;overflow:hidden;">
<component :is="currentComponent" />
</div>
</div>
</template>
<script>
import Plan from './components/Plan.vue'
import ProcessSpec from './components/ProcessSpec.vue'
import Actual from './components/Actual.vue'
export default {
name: 'DrSystem',
components: { Plan, ProcessSpec, Actual },
data() {
return { activeMenu: 'plan' }
},
computed: {
currentComponent() {
return { plan: 'Plan', processSpec: 'ProcessSpec', actual: 'Actual' }[this.activeMenu]
}
},
methods: {
handleMenuSelect(index) { this.activeMenu = index }
}
}
</script>
<style scoped lang="scss">
.dr-container {
display: flex;
width: 100%;
height: calc(100vh - 84px);
background-color: #f5f7fa;
}
.dr-sidebar {
width: 100px;
background-color: #fff;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px rgba(0,0,0,.05);
.sidebar-menu {
border: none;
background-color: transparent;
flex: 1;
overflow-y: auto;
border-right: none;
::v-deep .el-menu-item {
color: #606266;
padding: 0 20px;
font-size: 14px;
i {
font-size: 18px;
margin-right: 8px;
}
&:hover { background-color: #ecf5ff; color: #409eff; }
&.is-active {
background-color: #ecf5ff;
color: #409eff;
border-right: 3px solid #409eff;
}
}
}
}
</style>

View File

@@ -0,0 +1,355 @@
<template>
<div class="roll-config-view">
<div class="toolbar">
<span class="page-title"><i class="el-icon-s-tools" /> 酸轧配辊</span>
<div class="toolbar-right">
<span v-if="lastRefresh" class="refresh-time">上次刷新{{ lastRefresh }}</span>
<el-button size="small" @click="handleCheck">
<i class="el-icon-search" /> 缺辊检查
</el-button>
<el-button size="small" type="primary" :loading="loading" @click="loadData">
<i class="el-icon-refresh" /> 刷新
</el-button>
</div>
</div>
<div class="table-wrap" v-loading="loading">
<table class="roll-table">
<thead>
<tr>
<th rowspan="2" class="th-stand">机架</th>
<th rowspan="2" class="th-pos">位置</th>
<th colspan="4" class="th-section th-standby">&nbsp;</th>
<th colspan="9" class="th-section th-online">在线辊</th>
<th colspan="4" class="th-section th-standard">换辊参考</th>
</tr>
<tr>
<!-- 备辊 -->
<th class="th-sub">辊号</th>
<th class="th-sub">外径(mm)</th>
<th class="th-sub">凸度</th>
<th class="th-sub">粗糙度</th>
<!-- 在线辊 -->
<th class="th-sub">辊号</th>
<th class="th-sub">/</th>
<th class="th-sub">辊型</th>
<th class="th-sub">直径(mm)</th>
<th class="th-sub">凸度</th>
<th class="th-sub">粗糙度</th>
<th class="th-sub">本次长度</th>
<th class="th-sub">本次重量</th>
<th class="th-sub">安装时间</th>
<!-- 换辊参考 -->
<th class="th-sub">本次长度</th>
<th class="th-sub">累计长度</th>
<th class="th-sub">本次重量</th>
<th class="th-sub">累计重量</th>
</tr>
</thead>
<tbody>
<template v-for="(stand, si) in tableData">
<tr
v-for="(pos, pi) in stand.positions"
:key="stand.id + '-' + pi"
:class="rowCls(pos, pi)"
>
<td v-if="pi === 0" :rowspan="stand.positions.length" class="td-stand">
{{ stand.name }}
</td>
<td class="td-pos" :class="posTypeCls(pos)">{{ pos.label }}</td>
<!-- 备辊 -->
<td class="td-standby">{{ sv(pos.standby, 'rollid') }}</td>
<td class="td-standby">{{ nv(pos.standby, 'diameter') }}</td>
<td class="td-standby">{{ nv(pos.standby, 'crown') }}</td>
<td class="td-standby">{{ nv(pos.standby, 'rough') }}</td>
<!-- 在线辊 -->
<td class="td-online td-bold">{{ sv(pos.online, 'rollid') }}</td>
<td class="td-online">{{ dispPos(pos.online && pos.online.position) }}</td>
<td class="td-online">
<span :class="typeChipCls(pos.online && pos.online.type)">
{{ dispType(pos.online && pos.online.type) }}
</span>
</td>
<td class="td-online">{{ nv(pos.online, 'diameter') }}</td>
<td class="td-online">{{ nv(pos.online, 'crown') }}</td>
<td class="td-online">{{ nv(pos.online, 'rough') }}</td>
<td class="td-online">{{ iv(pos.online, 'rolled_length') }}</td>
<td class="td-online">{{ iv(pos.online, 'rolled_weight') }}</td>
<td class="td-online td-time">{{ dv(pos.online, 'instal_time') }}</td>
<!-- 换辊参考 -->
<td class="td-std">{{ iv(pos.online, 'rolled_length') }}</td>
<td class="td-std">{{ iv(pos.online, 'total_rolled_length') }}</td>
<td class="td-std">{{ iv(pos.online, 'rolled_weight') }}</td>
<td class="td-std">{{ iv(pos.online, 'total_rolled_weight') }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { getRollData } from '@/api/l2/timing'
// 酸轧PLTCM六机架
const STAND_NAMES = ['1#机架', '2#机架', '3#机架', '4#机架', '5#机架', '6#机架']
const POS_NORM = {
TOP: '上', UPPER: '上', UP: '上', '上': '上',
BOTTOM: '下', LOWER: '下', DOWN: '下', '下': '下'
}
const TYPE_NORM = {
WORK: '工作辊', WORK_ROLL: '工作辊', WR: '工作辊', '工作辊': '工作辊',
BACKUP: '支撑辊', BUR: '支撑辊', SUPPORT: '支撑辊', BACK_UP: '支撑辊', '支撑辊': '支撑辊',
INTERMEDIATE: '中间辊', IMR: '中间辊', INTER: '中间辊', '中间辊': '中间辊'
}
function normPos(v) { return POS_NORM[String(v || '').toUpperCase()] || String(v || '').trim() }
function normType(v) { return TYPE_NORM[String(v || '').toUpperCase()] || String(v || '').trim() }
// 酸轧六辊轧机UCM辊系排列支撑辊 + 中间辊 + 工作辊
const POSITIONS = [
{ label: '上支撑辊', position: '上', type: '支撑辊', group: 'bur' },
{ label: '上中间辊', position: '上', type: '中间辊', group: 'imr' },
{ label: '上工作辊', position: '上', type: '工作辊', group: 'wr' },
{ label: '下工作辊', position: '下', type: '工作辊', group: 'wr' },
{ label: '下中间辊', position: '下', type: '中间辊', group: 'imr' },
{ label: '下支撑辊', position: '下', type: '支撑辊', group: 'bur' }
]
function fmtDateStr(val) {
if (!val) return '—'
const s = String(val)
const m = s.match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/)
if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`
const d = new Date(val)
if (isNaN(d.getTime())) return s.substring(0, 16)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
export default {
name: 'AcidRollConfigPage',
data() {
return {
loading: false,
lastRefresh: '',
tableData: []
}
},
created() {
this.loadData()
},
methods: {
async loadData() {
this.loading = true
try {
const res = await getRollData()
const rows = res?.data?.rows || []
this.buildTableData(rows)
const now = new Date()
const pad = n => String(n).padStart(2, '0')
this.lastRefresh = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
} finally {
this.loading = false
}
},
buildTableData(rows) {
this.tableData = STAND_NAMES.map((name, idx) => {
const standId = idx + 1
const standRows = rows.filter(r => Number(r.standid) === standId)
const positions = POSITIONS.map(p => {
const matches = standRows
.filter(r => normPos(r.position) === p.position && normType(r.type) === p.type)
.sort((a, b) => {
const ta = a.instal_time ? new Date(a.instal_time).getTime() : 0
const tb = b.instal_time ? new Date(b.instal_time).getTime() : 0
return ta - tb
})
return { ...p, online: matches[0] || null, standby: matches[1] || null }
})
return { id: standId, name, positions }
})
},
rowCls(pos, pi) {
const cls = []
if (!pos.online) cls.push('row-empty')
if (pi === 2 || pi === 3) cls.push('row-wr') // 工作辊行分隔线
return cls.join(' ')
},
posTypeCls(pos) {
const map = { bur: 'pos-bur', imr: 'pos-imr', wr: 'pos-wr' }
return map[pos.group] || ''
},
typeChipCls(type) {
const t = normType(type)
if (t === '工作辊') return 'chip chip-wr'
if (t === '中间辊') return 'chip chip-imr'
if (t === '支撑辊') return 'chip chip-bur'
return ''
},
sv(row, key) {
if (!row) return '—'
const v = row[key]
return v != null && v !== '' ? String(v) : '—'
},
nv(row, key) {
if (!row) return '—'
const v = row[key]
if (v == null || v === '') return '—'
const n = parseFloat(v)
return isNaN(n) ? String(v) : n.toFixed(2)
},
iv(row, key) {
if (!row) return '—'
const v = row[key]
if (v == null || v === '') return '—'
const n = parseFloat(v)
return isNaN(n) ? String(v) : Math.round(n).toLocaleString()
},
dv(row, key) { return row ? fmtDateStr(row[key]) : '—' },
dispPos(v) { return POS_NORM[String(v || '').toUpperCase()] || v || '—' },
dispType(v) { return TYPE_NORM[String(v || '').toUpperCase()] || v || '—' },
handleCheck() {
const missing = []
this.tableData.forEach(stand => {
stand.positions.forEach(pos => {
if (pos.online && !pos.standby) missing.push(`${stand.name} ${pos.label}`)
})
})
if (!missing.length) {
this.$message.success('检查通过,各机架备辊配置正常')
} else {
this.$alert(missing.join('\n'), '以下位置缺少备辊', { type: 'warning', confirmButtonText: '知道了' })
}
}
}
}
</script>
<style scoped>
.roll-config-view {
padding: 12px 16px;
background: #fff;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* ── 工具栏 ── */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
flex-shrink: 0;
}
.page-title {
font-size: 15px;
font-weight: 700;
color: #303133;
display: flex;
align-items: center;
gap: 6px;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.refresh-time {
font-size: 12px;
color: #909399;
}
/* ── 表格容器 ── */
.table-wrap {
flex: 1;
overflow: auto;
border: 1px solid #dcdfe6;
border-radius: 4px;
min-height: 0;
}
/* ── 轧辊表 ── */
.roll-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
color: #303133;
white-space: nowrap;
}
.roll-table th,
.roll-table td {
border: 1px solid #e4e7ed;
padding: 5px 9px;
text-align: center;
}
.roll-table thead tr:first-child th {
background: #eef1f6;
font-weight: 600;
font-size: 13px;
}
.th-section { border-bottom: 2px solid #b8c2cc !important; }
.th-standby { background: #f5f7fa !important; color: #606266; }
.th-online { background: #e8f4ff !important; color: #1a5276; }
.th-standard{ background: #fdf6ec !important; color: #7b5c1e; }
.th-stand, .th-pos { background: #eef1f6; font-weight: 600; }
.th-sub {
background: #fafafa;
font-weight: normal;
font-size: 11px;
color: #606266;
}
/* 机架单元格(竖向文字) */
.td-stand {
font-weight: 700;
font-size: 13px;
background: #f0f3f8;
writing-mode: vertical-rl;
text-orientation: mixed;
letter-spacing: 3px;
width: 30px;
min-width: 30px;
}
/* 辊位名称列 */
.td-pos {
font-weight: 500;
width: 64px;
text-align: left;
padding-left: 8px;
}
.pos-bur { background: #f5f7fa; color: #5a6a7e; }
.pos-imr { background: #f0f9eb; color: #3a7a2a; }
.pos-wr { background: #e8f4ff; color: #1a5276; font-weight: 600; }
/* 工作辊行加粗上边框 */
.row-wr td { border-top: 2px solid #c8d8ea !important; }
/* 辊型 chip */
.chip {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
}
.chip-bur { background: #f5f7fa; color: #5a6a7e; border: 1px solid #dcdfe6; }
.chip-imr { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
.chip-wr { background: #e8f4ff; color: #1a5276; border: 1px solid #a0c4e8; }
.td-bold { font-weight: 600; }
.td-standby { color: #606266; }
.td-online { color: #303133; }
.td-std { color: #909399; }
.td-time { font-size: 11px; color: #909399; min-width: 110px; }
.row-empty td { color: #c0c4cc; background: #fafafa; }
.roll-table tbody tr:hover td { background: #f0f7ff; }
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div class="roll-history-view">
<!-- 筛选栏 -->
<div class="filter-bar">
<span class="page-title"><i class="el-icon-time" /> 酸轧换辊历史</span>
<div class="filter-right">
<el-form :inline="true" :model="query" size="small" style="margin:0">
<el-form-item label="轧辊号">
<el-input
v-model="query.rollId"
placeholder="输入轧辊号"
clearable
style="width:160px"
@keyup.enter.native="handleSearch"
/>
</el-form-item>
<el-form-item label="机架">
<el-select v-model="query.standId" placeholder="全部" clearable style="width:110px">
<el-option v-for="s in standOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="辊型">
<el-select v-model="query.rollType" placeholder="全部" clearable style="width:100px">
<el-option label="工作辊" value="WORK" />
<el-option label="中间辊" value="INTERMEDIATE" />
<el-option label="支撑辊" value="BACKUP" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 数据表 -->
<el-table
:data="rows"
size="mini"
border
stripe
:height="tableHeight"
v-loading="loading"
highlight-current-row
>
<el-table-column type="index" width="48" label="序" fixed align="center" />
<el-table-column prop="rollid" label="轧辊号" width="120" fixed show-overflow-tooltip />
<el-table-column label="机架" width="72" align="center">
<template slot-scope="{ row }">{{ standName(row.standid) }}</template>
</el-table-column>
<el-table-column label="位置" width="52" align="center">
<template slot-scope="{ row }">{{ dispPos(row.position) }}</template>
</el-table-column>
<el-table-column label="辊型" width="80" align="center">
<template slot-scope="{ row }">
<span :class="typeChipCls(row.type)">{{ dispType(row.type) }}</span>
</template>
</el-table-column>
<el-table-column prop="diameter" label="直径(mm)" width="90" align="right" :formatter="fmtNum" />
<el-table-column prop="crown" label="凸度" width="72" align="right" :formatter="fmtNum" />
<el-table-column prop="rough" label="粗糙度" width="72" align="right" :formatter="fmtNum" />
<el-table-column prop="rolled_length" label="本次长度" width="90" align="right" :formatter="fmtInt" />
<el-table-column prop="rolled_weight" label="本次重量" width="90" align="right" :formatter="fmtInt" />
<el-table-column prop="total_rolled_length" label="累计长度" width="90" align="right" :formatter="fmtInt" />
<el-table-column prop="total_rolled_weight" label="累计重量" width="90" align="right" :formatter="fmtInt" />
<el-table-column prop="grind_count" label="磨削次数" width="80" align="center" />
<el-table-column label="安装时间" width="140" align="center">
<template slot-scope="{ row }">{{ fmtDate(row.instal_time) }}</template>
</el-table-column>
<el-table-column label="卸辊时间" width="140" align="center">
<template slot-scope="{ row }">{{ fmtDate(row.deinstal_time) }}</template>
</el-table-column>
<el-table-column label="换辊时间" width="140" align="center">
<template slot-scope="{ row }">{{ fmtDate(row.change_time) }}</template>
</el-table-column>
<el-table-column prop="shift" label="班次" width="52" align="center" />
<el-table-column prop="crew" label="班组" width="52" align="center" />
</el-table>
<!-- 分页 -->
<div class="pagination-bar">
<el-pagination
small
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
:page-size="pagination.pageSize"
:page-sizes="[50, 100, 200]"
:current-page="pagination.page"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import { getRollHistoryList, getRollHistoryCount } from '@/api/l2/timing'
// 酸轧六机架
const STAND_NAMES = ['1#机架', '2#机架', '3#机架', '4#机架', '5#机架', '6#机架']
const POS_NORM = {
TOP: '上', UPPER: '上', UP: '上', '上': '上',
BOTTOM: '下', LOWER: '下', DOWN: '下', '下': '下'
}
const TYPE_NORM = {
WORK: '工作辊', WORK_ROLL: '工作辊', WR: '工作辊', '工作辊': '工作辊',
BACKUP: '支撑辊', BUR: '支撑辊', SUPPORT: '支撑辊', BACK_UP: '支撑辊', '支撑辊': '支撑辊',
INTERMEDIATE: '中间辊', IMR: '中间辊', INTER: '中间辊', '中间辊': '中间辊'
}
function fmtDateStr(val) {
if (!val) return '—'
const s = String(val)
const m = s.match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/)
if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`
const d = new Date(val)
if (isNaN(d.getTime())) return s.substring(0, 16)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
export default {
name: 'AcidRollHistoryPage',
data() {
return {
loading: false,
query: { rollId: '', standId: null, rollType: '' },
rows: [],
pagination: { page: 1, pageSize: 50, total: 0 },
tableHeight: 'calc(100vh - 172px)',
standOptions: STAND_NAMES.map((label, i) => ({ label, value: i + 1 }))
}
},
created() {
this.loadCount()
this.loadRows()
},
methods: {
async loadCount() {
try {
const res = await getRollHistoryCount(
this.query.rollId || undefined,
this.query.standId || undefined
)
this.pagination.total = res?.data?.total ?? 0
} catch (_) {}
},
async loadRows() {
this.loading = true
try {
const { page, pageSize } = this.pagination
const res = await getRollHistoryList(
page, pageSize,
this.query.rollId || undefined,
this.query.standId || undefined
)
let rows = res?.data?.rows || []
// 前端按辊型过滤(接口暂不支持辊型参数)
if (this.query.rollType) {
const norm = TYPE_NORM[this.query.rollType] || this.query.rollType
rows = rows.filter(r => {
const t = String(r.type || '').toUpperCase()
return TYPE_NORM[t] === norm || String(r.type || '') === norm
})
}
this.rows = rows
} finally {
this.loading = false
}
},
handleSearch() {
this.pagination.page = 1
this.loadCount()
this.loadRows()
},
handleReset() {
this.query = { rollId: '', standId: null, rollType: '' }
this.pagination.page = 1
this.loadCount()
this.loadRows()
},
handlePageChange(page) {
this.pagination.page = page
this.loadRows()
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.page = 1
this.loadCount()
this.loadRows()
},
standName(val) {
const idx = Number(val) - 1
return STAND_NAMES[idx] || (val != null ? String(val) : '—')
},
dispPos(v) { return POS_NORM[String(v || '').toUpperCase()] || v || '—' },
dispType(v) { return TYPE_NORM[String(v || '').toUpperCase()] || v || '—' },
typeChipCls(type) {
const t = String(type || '').toUpperCase()
const norm = TYPE_NORM[t] || ''
if (norm === '工作辊') return 'chip chip-wr'
if (norm === '中间辊') return 'chip chip-imr'
if (norm === '支撑辊') return 'chip chip-bur'
return ''
},
fmtNum(row, col, val) {
if (val == null || val === '') return '—'
const n = parseFloat(val)
return isNaN(n) ? String(val) : n.toFixed(2)
},
fmtInt(row, col, val) {
if (val == null || val === '') return '—'
const n = parseFloat(val)
return isNaN(n) ? String(val) : Math.round(n).toLocaleString()
},
fmtDate(val) { return fmtDateStr(val) }
}
}
</script>
<style scoped>
.roll-history-view {
padding: 12px 16px;
background: #fff;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* ── 筛选栏 ── */
.filter-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
flex-shrink: 0;
flex-wrap: wrap;
gap: 8px;
}
.page-title {
font-size: 15px;
font-weight: 700;
color: #303133;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.filter-right { display: flex; align-items: center; }
/* ── 辊型 chip ── */
.chip {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
}
.chip-bur { background: #f5f7fa; color: #5a6a7e; border: 1px solid #dcdfe6; }
.chip-imr { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
.chip-wr { background: #e8f4ff; color: #1a5276; border: 1px solid #a0c4e8; }
/* ── 分页 ── */
.pagination-bar {
display: flex;
justify-content: flex-end;
padding: 8px 0 0;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<div class="app-container acid-op-page">
<el-row :gutter="16">
<!-- 左侧操作表单 + 历史记录 -->
<el-col :span="15">
<div class="op-card">
<div class="op-header">
<span class="op-title">双机架工序录入</span>
<el-tag size="mini" type="info" style="margin-left:8px">actionType = 504</el-tag>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="form.enterCoilNo" placeholder="回车自动查询"
clearable @keyup.enter.native="onEnterCoilInput" @blur="onEnterCoilInput" @clear="clearCoilInfo" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出口钢卷号" prop="currentCoilNo">
<el-input v-model="form.currentCoilNo" placeholder="请输入出口钢卷号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left" style="margin:8px 0 12px">出口实绩</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="出口厚度(mm)" prop="exitThickness">
<el-input-number v-model="form.exitThickness" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出口宽度(mm)" prop="exitWidth">
<el-input-number v-model="form.exitWidth" :precision="1" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出口长度(m)">
<el-input-number v-model="form.exitLength" :precision="1" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="入口重量(t)">
<el-input-number v-model="form.entryWeight" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出口重量(t)">
<el-input-number v-model="form.exitWeight" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="工艺编码">
<el-input v-model="form.processCode" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="班组">
<el-select v-model="form.team" placeholder="请选择" style="width:100%" clearable>
<el-option label="甲班" value="甲" />
<el-option label="乙班" value="乙" />
<el-option label="丙班" value="丙" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注">
<el-input v-model="form.remark" clearable />
</el-form-item>
</el-col>
</el-row>
<div style="text-align:right;padding-top:4px">
<el-button @click="resetForm">重置</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">
新增录入同步创建计划
</el-button>
</div>
</el-form>
</div>
<!-- 历史 -->
<div class="op-card" style="margin-top:12px">
<div class="op-header">
<span class="op-title">最近录入记录</span>
<el-button size="mini" icon="el-icon-refresh" style="margin-left:auto" @click="loadHistory">刷新</el-button>
</div>
<el-table v-loading="historyLoading" :data="historyList" size="mini" border style="width:100%">
<el-table-column prop="enterCoilNo" label="入场钢卷号" min-width="120" show-overflow-tooltip />
<el-table-column prop="currentCoilNo" label="出口钢卷号" min-width="120" show-overflow-tooltip />
<el-table-column prop="exitThickness" label="出口厚(mm)" width="90" align="right" />
<el-table-column prop="exitWidth" label="出口宽(mm)" width="90" align="right" />
<el-table-column prop="exitLength" label="长度(m)" width="80" align="right" />
<el-table-column prop="entryWeight" label="入口重(t)" width="80" align="right" />
<el-table-column prop="team" label="班组" width="60" align="center" />
<el-table-column prop="createTime" label="录入时间" width="150" />
</el-table>
<pagination v-show="historyTotal > 0" :total="historyTotal"
:page.sync="historyQuery.pageNum" :limit.sync="historyQuery.pageSize"
@pagination="loadHistory" style="margin-top:6px" />
</div>
</el-col>
<!-- 右侧WMS 钢卷信息 -->
<el-col :span="9">
<div class="op-card">
<div class="op-header"><span class="op-title">WMS 钢卷信息</span></div>
<template v-if="coilInfo">
<el-descriptions :column="1" size="mini" border style="margin-top:8px">
<el-descriptions-item label="入场钢卷号">{{ coilInfo.enterCoilNo }}</el-descriptions-item>
<el-descriptions-item label="当前钢卷号">{{ coilInfo.currentCoilNo }}</el-descriptions-item>
<el-descriptions-item label="实际厚度">{{ coilInfo.actualThickness }} mm</el-descriptions-item>
<el-descriptions-item label="实际宽度">{{ coilInfo.actualWidth }} mm</el-descriptions-item>
<el-descriptions-item label="净重">{{ coilInfo.netWeight }} t</el-descriptions-item>
<el-descriptions-item label="长度">{{ coilInfo.length }} m</el-descriptions-item>
<el-descriptions-item label="质量状态">{{ coilInfo.qualityStatus }}</el-descriptions-item>
<el-descriptions-item label="物料类型">{{ coilInfo.materialType }}</el-descriptions-item>
</el-descriptions>
<el-button size="mini" type="primary" style="margin-top:8px" @click="applyCoilFill">
写入表单
</el-button>
</template>
<div v-else style="text-align:center;color:#aaa;padding:40px 0">
输入入场钢卷号后自动查询
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { addCoilWarehouseOperationLog, listCoilWarehouseOperationLog } from '@/api/wms/coilWarehouseOperationLog'
import { queryCoilByNo, addDrPlan } from '@/api/wms/drMill'
export default {
name: 'DrNormal',
data() {
return {
form: this.defaultForm(),
rules: {
enterCoilNo: [{ required: true, message: '入场钢卷号不能为空', trigger: 'blur' }],
},
submitting: false,
coilInfo: null,
historyLoading: false,
historyList: [],
historyTotal: 0,
historyQuery: { pageNum: 1, pageSize: 10, actionType: 504 },
}
},
created() { this.loadHistory() },
methods: {
defaultForm() {
return {
enterCoilNo: '', currentCoilNo: '',
exitThickness: undefined, exitWidth: undefined, exitLength: undefined,
entryWeight: undefined, exitWeight: undefined,
processCode: '', team: undefined, remark: '',
actionType: 504,
}
},
onEnterCoilInput() {
const v = (this.form.enterCoilNo || '').trim()
if (!v) return
queryCoilByNo(v).then(res => {
this.coilInfo = res.data || null
}).catch(() => { this.coilInfo = null })
},
clearCoilInfo() { this.coilInfo = null },
applyCoilFill() {
if (!this.coilInfo) return
if (this.coilInfo.actualThickness) this.form.exitThickness = parseFloat(this.coilInfo.actualThickness)
if (this.coilInfo.actualWidth) this.form.exitWidth = parseFloat(this.coilInfo.actualWidth)
if (this.coilInfo.netWeight) this.form.entryWeight = parseFloat(this.coilInfo.netWeight)
if (this.coilInfo.length) this.form.exitLength = parseFloat(this.coilInfo.length)
this.$message.success('钢卷数据已写入表单')
},
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.submitting = true
addCoilWarehouseOperationLog({ ...this.form }).then(() => {
// 同步创建双机架生产计划
return addDrPlan({
inMatNo: this.form.enterCoilNo,
enterCoilNo: this.form.enterCoilNo,
currentCoilNo: this.form.currentCoilNo,
inMatThick: this.coilInfo ? this.coilInfo.actualThickness : undefined,
inMatWidth: this.coilInfo ? this.coilInfo.actualWidth : undefined,
inMatWeight: this.coilInfo ? this.coilInfo.netWeight : undefined,
inMatLength: this.coilInfo ? this.coilInfo.length : undefined,
outThick: this.form.exitThickness,
remark: this.form.remark,
})
}).then(() => {
this.$modal.msgSuccess('录入成功,已同步创建双机架计划')
this.resetForm()
this.loadHistory()
}).catch(err => {
this.$modal.msgError('操作失败: ' + (err.message || ''))
}).finally(() => { this.submitting = false })
})
},
resetForm() {
this.form = this.defaultForm()
this.coilInfo = null
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
loadHistory() {
this.historyLoading = true
listCoilWarehouseOperationLog(this.historyQuery).then(res => {
this.historyList = res.rows || []
this.historyTotal = res.total || 0
}).finally(() => { this.historyLoading = false })
},
}
}
</script>
<style scoped>
.acid-op-page { background: #f5f7fa; }
.op-card { background: #fff; border: 1px solid #e4e7ed; border-radius: 4px; padding: 16px; }
.op-header { display: flex; align-items: center; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 2px solid #409eff; }
.op-title { font-size: 15px; font-weight: 600; color: #303133; }
</style>

View File

@@ -0,0 +1,231 @@
<template>
<div class="app-container acid-op-page">
<el-row :gutter="16">
<el-col :span="15">
<div class="op-card">
<div class="op-header">
<span class="op-title">双机架修复录入</span>
<el-tag size="mini" type="warning" style="margin-left:8px">actionType = 524</el-tag>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="form.enterCoilNo" placeholder="回车自动查询"
clearable @keyup.enter.native="onEnterCoilInput" @blur="onEnterCoilInput" @clear="clearCoilInfo" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出口钢卷号">
<el-input v-model="form.currentCoilNo" placeholder="请输入出口钢卷号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left" style="margin:8px 0 12px">修复参数</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="修复后厚度(mm)">
<el-input-number v-model="form.exitThickness" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="修复后宽度(mm)">
<el-input-number v-model="form.exitWidth" :precision="1" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="修复后长度(m)">
<el-input-number v-model="form.exitLength" :precision="1" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="入口重量(t)">
<el-input-number v-model="form.entryWeight" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="修复后重量(t)">
<el-input-number v-model="form.exitWeight" :precision="3" :min="0"
:controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="班组">
<el-select v-model="form.team" placeholder="请选择" style="width:100%" clearable>
<el-option label="甲班" value="甲" />
<el-option label="乙班" value="乙" />
<el-option label="丙班" value="丙" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" clearable />
</el-form-item>
</el-col>
</el-row>
<div style="text-align:right;padding-top:4px">
<el-button @click="resetForm">重置</el-button>
<el-button type="warning" :loading="submitting" @click="submitForm">
新增修复录入同步创建计划
</el-button>
</div>
</el-form>
</div>
<div class="op-card" style="margin-top:12px">
<div class="op-header">
<span class="op-title">最近修复记录</span>
<el-button size="mini" icon="el-icon-refresh" style="margin-left:auto" @click="loadHistory">刷新</el-button>
</div>
<el-table v-loading="historyLoading" :data="historyList" size="mini" border style="width:100%">
<el-table-column prop="enterCoilNo" label="入场钢卷号" min-width="120" show-overflow-tooltip />
<el-table-column prop="currentCoilNo" label="出口钢卷号" min-width="120" show-overflow-tooltip />
<el-table-column prop="exitThickness" label="修复后厚(mm)" width="95" align="right" />
<el-table-column prop="exitWidth" label="修复后宽(mm)" width="95" align="right" />
<el-table-column prop="exitLength" label="长度(m)" width="80" align="right" />
<el-table-column prop="team" label="班组" width="60" align="center" />
<el-table-column prop="createTime" label="录入时间" width="150" />
</el-table>
<pagination v-show="historyTotal > 0" :total="historyTotal"
:page.sync="historyQuery.pageNum" :limit.sync="historyQuery.pageSize"
@pagination="loadHistory" style="margin-top:6px" />
</div>
</el-col>
<el-col :span="9">
<div class="op-card">
<div class="op-header"><span class="op-title">WMS 钢卷信息</span></div>
<template v-if="coilInfo">
<el-descriptions :column="1" size="mini" border style="margin-top:8px">
<el-descriptions-item label="入场钢卷号">{{ coilInfo.enterCoilNo }}</el-descriptions-item>
<el-descriptions-item label="当前钢卷号">{{ coilInfo.currentCoilNo }}</el-descriptions-item>
<el-descriptions-item label="实际厚度">{{ coilInfo.actualThickness }} mm</el-descriptions-item>
<el-descriptions-item label="实际宽度">{{ coilInfo.actualWidth }} mm</el-descriptions-item>
<el-descriptions-item label="净重">{{ coilInfo.netWeight }} t</el-descriptions-item>
<el-descriptions-item label="长度">{{ coilInfo.length }} m</el-descriptions-item>
<el-descriptions-item label="质量状态">{{ coilInfo.qualityStatus }}</el-descriptions-item>
</el-descriptions>
<el-button size="mini" type="primary" style="margin-top:8px" @click="applyCoilFill">
写入表单
</el-button>
</template>
<div v-else style="text-align:center;color:#aaa;padding:40px 0">
输入入场钢卷号后自动查询
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { addCoilWarehouseOperationLog, listCoilWarehouseOperationLog } from '@/api/wms/coilWarehouseOperationLog'
import { queryCoilByNo, addDrPlan } from '@/api/wms/drMill'
export default {
name: 'DrRepair',
data() {
return {
form: this.defaultForm(),
rules: {
enterCoilNo: [{ required: true, message: '入场钢卷号不能为空', trigger: 'blur' }],
},
submitting: false,
coilInfo: null,
historyLoading: false,
historyList: [],
historyTotal: 0,
historyQuery: { pageNum: 1, pageSize: 10, actionType: 524 },
}
},
created() { this.loadHistory() },
methods: {
defaultForm() {
return {
enterCoilNo: '', currentCoilNo: '',
exitThickness: undefined, exitWidth: undefined, exitLength: undefined,
entryWeight: undefined, exitWeight: undefined,
team: undefined, remark: '',
actionType: 524,
}
},
onEnterCoilInput() {
const v = (this.form.enterCoilNo || '').trim()
if (!v) return
queryCoilByNo(v).then(res => {
this.coilInfo = res.data || null
}).catch(() => { this.coilInfo = null })
},
clearCoilInfo() { this.coilInfo = null },
applyCoilFill() {
if (!this.coilInfo) return
if (this.coilInfo.actualThickness) this.form.exitThickness = parseFloat(this.coilInfo.actualThickness)
if (this.coilInfo.actualWidth) this.form.exitWidth = parseFloat(this.coilInfo.actualWidth)
if (this.coilInfo.netWeight) this.form.entryWeight = parseFloat(this.coilInfo.netWeight)
if (this.coilInfo.length) this.form.exitLength = parseFloat(this.coilInfo.length)
this.$message.success('钢卷数据已写入表单')
},
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.submitting = true
addCoilWarehouseOperationLog({ ...this.form }).then(() => {
return addDrPlan({
inMatNo: this.form.enterCoilNo,
enterCoilNo: this.form.enterCoilNo,
currentCoilNo: this.form.currentCoilNo,
inMatThick: this.coilInfo ? this.coilInfo.actualThickness : undefined,
inMatWidth: this.coilInfo ? this.coilInfo.actualWidth : undefined,
inMatWeight: this.coilInfo ? this.coilInfo.netWeight : undefined,
inMatLength: this.coilInfo ? this.coilInfo.length : undefined,
outThick: this.form.exitThickness,
remark: `[修复] ${this.form.remark || ''}`,
})
}).then(() => {
this.$modal.msgSuccess('修复录入成功,已同步创建双机架计划')
this.resetForm()
this.loadHistory()
}).catch(err => {
this.$modal.msgError('操作失败: ' + (err.message || ''))
}).finally(() => { this.submitting = false })
})
},
resetForm() {
this.form = this.defaultForm()
this.coilInfo = null
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
loadHistory() {
this.historyLoading = true
listCoilWarehouseOperationLog(this.historyQuery).then(res => {
this.historyList = res.rows || []
this.historyTotal = res.total || 0
}).finally(() => { this.historyLoading = false })
},
}
}
</script>
<style scoped>
.acid-op-page { background: #f5f7fa; }
.op-card { background: #fff; border: 1px solid #e4e7ed; border-radius: 4px; padding: 16px; }
.op-header { display: flex; align-items: center; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 2px solid #e6a23c; }
.op-title { font-size: 15px; font-weight: 600; color: #303133; }
</style>

View File

@@ -0,0 +1,213 @@
<template>
<div class="dr-panel">
<!-- 计划概览 -->
<div class="panel-block">
<div class="pb-header">
<span class="pb-title">双机架计划</span>
<el-tag v-if="plan" size="mini" type="success" style="margin-left:6px">已找到</el-tag>
<el-tag v-else-if="loading" size="mini" type="info" style="margin-left:6px">查询中</el-tag>
<el-tag v-else size="mini" type="warning" style="margin-left:6px">未找到</el-tag>
</div>
<template v-if="plan">
<div class="plan-grid">
<div class="pg-item">
<span class="pg-label">计划号</span>
<span class="pg-value plan-no">{{ plan.planNo }}</span>
</div>
<div class="pg-item">
<span class="pg-label">合金牌号</span>
<span class="pg-value">{{ plan.alloyNo || '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">采料厚度</span>
<span class="pg-value">{{ plan.inMatThick != null ? plan.inMatThick + ' mm' : '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">成品厚度</span>
<span class="pg-value accent">{{ plan.outThick != null ? plan.outThick + ' mm' : '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">采料宽度</span>
<span class="pg-value">{{ plan.inMatWidth != null ? plan.inMatWidth + ' mm' : '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">采料重量</span>
<span class="pg-value">{{ plan.inMatWeight != null ? plan.inMatWeight + ' t' : '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">采料长度</span>
<span class="pg-value">{{ plan.inMatLength != null ? plan.inMatLength + ' m' : '—' }}</span>
</div>
<div class="pg-item">
<span class="pg-label">绑定方案</span>
<span class="pg-value">{{ plan.recipeNo || '未绑定' }}</span>
</div>
</div>
<!-- 写入整条计划数据 -->
<el-button type="primary" size="small" style="width:100%;margin-top:10px"
icon="el-icon-download" @click="fillFromPlan">
写入计划数据重量/宽度/长度/厚度
</el-button>
</template>
<div v-else-if="loading" class="pb-empty"><i class="el-icon-loading" /></div>
<div v-else class="pb-empty">未找到关联的双机架计划</div>
</div>
<!-- 道次列表 -->
<div class="panel-block" style="margin-top:12px" v-if="plan && passList.length > 0">
<div class="pb-header">
<span class="pb-title">道次明细</span>
<span class="pb-sub"> {{ passList.length }} 道次 · 点击行快捷写入成品厚度</span>
</div>
<el-table :data="passList" size="mini" border style="width:100%" max-height="300"
highlight-current-row @row-click="fillFromPass">
<el-table-column label="道次" prop="passNo" width="44" align="center" />
<el-table-column label="入口厚(mm)" prop="inThick" width="78" align="right">
<template slot-scope="{ row }">{{ row.inThick != null ? row.inThick : '—' }}</template>
</el-table-column>
<el-table-column label="出口厚(mm)" width="78" align="right">
<template slot-scope="{ row }">
<span class="pass-out-thick">{{ row.outThick != null ? row.outThick : '—' }}</span>
</template>
</el-table-column>
<el-table-column label="轧制力(kN)" prop="rollForce" min-width="72" align="right">
<template slot-scope="{ row }">{{ row.rollForce != null ? row.rollForce : '—' }}</template>
</el-table-column>
<el-table-column label="" width="46" fixed="right" align="center">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="fillFromPass(row)">写入</el-button>
</template>
</el-table-column>
</el-table>
<div class="pass-hint"> 通常选最后一道次的出口厚度作为成品实测厚度</div>
</div>
<!-- 无道次提示 -->
<div class="panel-block" style="margin-top:12px"
v-else-if="plan && plan.recipeId && passList.length === 0">
<div class="pb-empty">该方案暂无道次数据</div>
</div>
</div>
</template>
<script>
import { getDrPlanByActionId } from '@/api/wms/drMill'
export default {
name: 'DrMatchPanel',
props: {
actionId: { type: [String, Number], default: null }
},
data() {
return {
loading: false,
plan: null,
}
},
computed: {
passList() {
return (this.plan && this.plan.passList) || []
}
},
watch: {
actionId(val) {
if (val) this.loadPlan(val)
}
},
created() {
if (this.actionId) this.loadPlan(this.actionId)
},
methods: {
loadPlan(actionId) {
this.plan = null
this.loading = true
getDrPlanByActionId(actionId)
.then(res => { this.plan = res.data || null })
.catch(() => {})
.finally(() => { this.loading = false })
},
/** 写入计划整体数据:重量/宽度/长度/成品厚度 */
fillFromPlan() {
const p = this.plan
this.$emit('fill', {
outThick: p.outThick,
inMatWidth: p.inMatWidth,
inMatWeight: p.inMatWeight,
inMatLength: p.inMatLength,
})
this.$message.success('已写入计划数据')
},
/** 写入某道次的出口厚度作为成品厚度 */
fillFromPass(pass) {
this.$emit('fill', {
outThick: pass.outThick,
inMatWidth: this.plan && this.plan.inMatWidth,
inMatWeight: this.plan && this.plan.inMatWeight,
inMatLength: this.plan && this.plan.inMatLength,
})
this.$message.success(`已写入第 ${pass.passNo} 道次厚度`)
},
}
}
</script>
<style scoped>
.dr-panel { font-size: 13px; }
.panel-block {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
}
.pb-header {
display: flex;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
.pb-title { font-weight: 600; color: #303133; font-size: 13px; }
.pb-sub { font-size: 11px; color: #909399; margin-left: 8px; }
.plan-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 12px;
}
.pg-item { display: flex; flex-direction: column; }
.pg-label { font-size: 11px; color: #909399; }
.pg-value { font-size: 13px; color: #303133; font-weight: 500; }
.pg-value.accent { color: #409eff; }
.plan-no { font-family: 'Courier New', monospace; font-size: 12px; color: #606266; }
.pb-empty {
text-align: center;
color: #c0c4cc;
padding: 20px 0;
font-size: 12px;
}
.pass-out-thick {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #409eff;
}
.pass-hint {
font-size: 11px;
color: #909399;
margin-top: 6px;
text-align: center;
}
</style>

View File

@@ -7,7 +7,7 @@
<span>钢卷信息更新</span>
</div>
<div class="header-actions">
</div>
</div> -->
@@ -26,9 +26,10 @@
<!-- 主内容区 - 左右布局 -->
<div class="content-wrapper">
<!-- 左侧L2 匹配面板 -->
<!-- 左侧快捷写入面板DR操作用双机架计划面板其他用L2匹配 -->
<div>
<l2-match-panel :hot-coil-id="l2HotCoilId" @fill="applyL2Fill" />
<dr-match-panel v-if="isDrAction" :action-id="actionId" @fill="applyDrFill" />
<l2-match-panel v-else :hot-coil-id="l2HotCoilId" @fill="applyL2Fill" />
</div>
<!-- 右侧更新表单 -->
<div>
@@ -326,7 +327,8 @@ import AbnormalForm from './components/AbnormalForm';
import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import { addCoilContractRel } from "@/api/wms/coilContractRel";
import ContractSelect from "@/components/KLPService/ContractSelect";
import L2MatchPanel from './panels/L2MatchPanel.vue';
import L2MatchPanel from './panels/L2MatchPanel.vue'
import DrMatchPanel from './panels/DrMatchPanel.vue';
export default {
@@ -340,6 +342,7 @@ export default {
AbnormalForm,
ContractSelect,
L2MatchPanel,
DrMatchPanel,
},
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() {
@@ -429,6 +432,7 @@ export default {
],
},
actionId: null,
actionType: null, // 待操作类型504/524 = 双机架
acidPrefill: {
visible: false,
type: 'info',
@@ -462,6 +466,10 @@ export default {
l2HotCoilId() {
return (this.currentInfo && this.currentInfo.enterCoilNo) || ''
},
/** 是否双机架工序actionType 504=正常 / 524=修复) */
isDrAction() {
return this.actionType === 504 || this.actionType === 524
},
// 动态显示标签
getItemLabel() {
if (this.updateForm.materialType === '成品') {
@@ -490,6 +498,11 @@ export default {
// 填写生产开始时间
this.$set(this.updateForm, 'productionStartTime', pendingActionRes.data.createTime)
// 记录操作类型(用于判断是否双机架工序)
if (pendingActionRes.data && pendingActionRes.data.actionType != null) {
this.actionType = pendingActionRes.data.actionType
}
if (coilId) {
await this.loadCoilInfo(coilId);
}
@@ -523,6 +536,20 @@ export default {
}
},
methods: {
/** 双机架计划/道次快捷写入 */
applyDrFill(data) {
if (data.outThick != null) this.$set(this.updateForm, 'actualThickness', parseFloat(data.outThick))
if (data.inMatWidth != null) this.$set(this.updateForm, 'actualWidth', parseFloat(data.inMatWidth))
if (data.inMatWeight != null) {
this.$set(this.updateForm, 'netWeight', parseFloat(data.inMatWeight))
this.$set(this.updateForm, 'grossWeight', parseFloat(data.inMatWeight))
}
if (data.inMatLength != null) {
this.$set(this.updateForm, 'actualLength', parseFloat(data.inMatLength))
this.$set(this.updateForm, 'length', parseFloat(data.inMatLength))
}
},
applyL2Fill(data) {
if (data.entry_weight != null) {
const w = parseFloat(data.entry_weight)

View File

@@ -0,0 +1,385 @@
<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="enterCoilNo" width="115" />
<el-table-column label="当前钢卷号" prop="currentCoilNo" width="115" />
<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="采料重量(t)" prop="inMatWeight" width="100" align="right" />
<el-table-column label="道次数" prop="passCount" width="65" align="center" />
<el-table-column label="生产状态" prop="prodStatus" width="85" 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">
<template v-if="!selectedPlan">
<div class="section-header"><span>轧制工艺</span></div>
<div class="pass-empty">请在上方选择一条计划</div>
</template>
<template v-else-if="selectedPlan.recipeId && !passEditMode">
<div class="section-header">
<span>轧制工艺</span>
<div style="display:flex;align-items:center;gap:8px">
<span class="recipe-tag">{{ selectedPlan.recipeNo }}</span>
<el-button size="mini" icon="el-icon-edit" @click="enterEditMode">修改绑定</el-button>
</div>
</div>
<el-table :data="passList" border size="mini" class="pass-table"
:row-class-name="passRowClass" height="calc(50vh - 160px)">
<el-table-column label="道次" prop="passNo" width="45" align="center" fixed />
<el-table-column label="入口厚(mm)" prop="inThick" width="90" align="right" />
<el-table-column label="出口厚(mm)" prop="outThick" width="90" align="right" />
<el-table-column label="轧制力(kN)" prop="rollForce" width="85" align="right" />
<el-table-column label="入口张力(kN)" prop="inTension" width="90" align="right" />
<el-table-column label="出口张力(kN)" prop="outTension" width="90" align="right" />
<el-table-column label="最高速度" prop="maxSpeed" width="80" 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>
</template>
<template v-else-if="!selectedPlan.recipeId && !passEditMode">
<div class="section-header"><span>轧制工艺</span></div>
<div class="pass-empty">
<span>该计划尚未绑定工艺参数</span>
<div style="margin-top:12px">
<el-button size="mini" type="primary" icon="el-icon-document-copy" @click="openSelectRecipe">选择已有方案</el-button>
</div>
</div>
</template>
</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=" " 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" icon="el-icon-document" :disabled="!selectedPlan" @click="handleFinish">完成</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>
</div>
<!-- 新增/修改对话框 -->
<el-dialog :title="planDialogTitle" :visible.sync="planDialogVisible" width="680px"
:close-on-click-modal="false" append-to-body>
<el-form :model="form" :rules="rules" ref="formRef" size="mini" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="form.enterCoilNo"
@keyup.enter.native="onCoilNoInput" @blur="onCoilNoInput" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="当前钢卷号">
<el-input v-model="form.currentCoilNo" clearable />
</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="12">
<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.recipeId"
:label="`${r.recipeNo}${r.alloyNo}`" :value="r.recipeId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采料厚度">
<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="成品厚度">
<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.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.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.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button size="mini" @click="planDialogVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="handleSave">确定</el-button>
</div>
</el-dialog>
<!-- 选择方案对话框 -->
<el-dialog title="选择工艺方案" :visible.sync="selectRecipeVisible" width="480px"
:close-on-click-modal="false" append-to-body>
<el-select v-model="selectRecipeId" filterable clearable
placeholder="输入方案号或合金号搜索" style="width:100%">
<el-option v-for="r in recipeOptions" :key="r.recipeId"
:label="`${r.recipeNo}${r.alloyNo}${r.inThick}→${r.outThick}mm`"
:value="r.recipeId" />
</el-select>
<div slot="footer">
<el-button size="mini" @click="selectRecipeVisible = false">取消</el-button>
<el-button size="mini" type="primary" @click="confirmSelectRecipe">确定绑定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDrPlan, addDrPlan, updateDrPlan, delDrPlan, moveUpDrPlan, moveDownDrPlan, finishDrPlan,
listDrRecipe, getDrRecipeDetail, queryCoilByNo } from '@/api/wms/drMill'
const emptyForm = () => ({
planId: null, planNo: '', enterCoilNo: '', currentCoilNo: '', inMatNo: '',
alloyNo: '', recipeId: null, recipeNo: '',
outThick: '', inMatThick: '', inMatWidth: '', inMatLength: '',
inMatWeight: '', inMatOd: '', inMatId: '', passCount: 0, remark: ''
})
export default {
name: 'DrPlan',
data() {
return {
planList: [],
selectedPlan: null,
passList: [],
recipeOptions: [],
query: { inMatNo: '', hideFinished: false },
passEditMode: false,
planDialogVisible: false,
isNew: true,
form: emptyForm(),
rules: {
enterCoilNo: [{ required: true, message: '请输入入场钢卷号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金牌号', trigger: 'blur' }],
},
selectRecipeVisible: false,
selectRecipeId: null,
}
},
computed: {
planDialogTitle() { return this.isNew ? '新增双机架计划' : '修改双机架计划' }
},
mounted() {
this.loadList()
this.refreshRecipeOptions()
},
methods: {
refreshRecipeOptions() {
listDrRecipe({}).then(res => { this.recipeOptions = res.data || [] })
},
loadList() {
const params = { inMatNo: this.query.inMatNo }
if (this.query.hideFinished) params.planStatus = '0'
listDrPlan(params).then(res => { this.planList = res.data || [] })
},
resetQuery() {
this.query = { inMatNo: '', hideFinished: false }
this.loadList()
},
handleQueueSelect(row) {
this.selectedPlan = row
this.passList = []
if (!row) return
if (row.recipeId) {
getDrRecipeDetail(row.recipeId).then(res => {
this.passList = (res.data && res.data.passList) || []
})
}
},
// 入场钢卷号变化 → 查 WMS 自动填入
onCoilNoInput() {
const v = (this.form.enterCoilNo || '').trim()
if (!v) return
this.form.inMatNo = v
queryCoilByNo(v).then(res => {
const c = res.data
if (!c) return
if (c.actualThickness && !this.form.inMatThick) this.form.inMatThick = c.actualThickness
if (c.actualWidth && !this.form.inMatWidth) this.form.inMatWidth = String(c.actualWidth)
if (c.netWeight && !this.form.inMatWeight) this.form.inMatWeight = String(c.netWeight)
if (c.length && !this.form.inMatLength) this.form.inMatLength = String(c.length)
if (c.currentCoilNo && !this.form.currentCoilNo) this.form.currentCoilNo = c.currentCoilNo
this.$message.success('已从 WMS 自动填入钢卷数据')
}).catch(() => {})
},
openSelectRecipe() {
this.selectRecipeId = null
this.selectRecipeVisible = true
},
confirmSelectRecipe() {
if (!this.selectRecipeId) { this.$message.warning('请选择方案'); return }
const r = this.recipeOptions.find(x => x.recipeId === this.selectRecipeId)
updateDrPlan({ ...this.selectedPlan, recipeId: this.selectRecipeId, recipeNo: r ? r.recipeNo : '', passCount: r ? r.passCount : 0 })
.then(() => {
this.$message.success('方案绑定成功')
this.selectRecipeVisible = false
this.selectedPlan = { ...this.selectedPlan, recipeId: this.selectRecipeId, recipeNo: r ? r.recipeNo : '' }
getDrRecipeDetail(this.selectRecipeId).then(res => {
this.passList = (res.data && res.data.passList) || []
})
this.loadList()
})
},
enterEditMode() { this.openSelectRecipe() },
onRecipeChange(recipeId) {
if (!recipeId) return
const r = this.recipeOptions.find(x => x.recipeId === 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
}
},
handleAdd() { this.isNew = true; this.form = emptyForm(); this.planDialogVisible = true },
handleEdit() { this.isNew = false; this.form = { ...this.selectedPlan }; this.planDialogVisible = true },
handleSave() {
this.$refs.formRef.validate(valid => {
if (!valid) return
if (this.form.recipeId) {
const r = this.recipeOptions.find(x => x.recipeId === this.form.recipeId)
if (r) { this.form.recipeNo = r.recipeNo; this.form.passCount = r.passCount }
}
const api = this.isNew ? addDrPlan : updateDrPlan
api(this.form).then(() => {
this.$message.success('保存成功')
this.planDialogVisible = false
this.loadList()
})
})
},
handleDelete() {
this.$confirm(`确定删除该计划?`, '提示', { type: 'warning' }).then(() => {
delDrPlan(this.selectedPlan.planId).then(() => {
this.$message.success('删除成功')
this.selectedPlan = null; this.passList = []
this.loadList()
})
})
},
handleFinish() {
finishDrPlan(this.selectedPlan.planId).then(() => {
this.$message.success('计划已完成')
this.loadList()
})
},
handleMoveUp() { moveUpDrPlan(this.selectedPlan.planId).then(() => this.loadList()) },
handleMoveDown() { moveDownDrPlan(this.selectedPlan.planId).then(() => this.loadList()) },
queueRowClass({ row }) {
if (row.prodStatus === 'Rolling') return 'row-rolling'
return ''
},
passRowClass({ rowIndex }) { return rowIndex % 2 === 0 ? '' : 'alt-row' },
prodStatusLabel(s) {
return { Idle: '待轧', Rolling: '轧制中', NextCoil: '下一卷', Done: '已完成', Error: '异常' }[s] || s
},
prodStatusClass(s) {
return { Idle: 'status-wait', Rolling: 'status-rolling', Done: 'status-done', Error: '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; margin-right: 4px; }
.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 .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-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; font-size: 12px; }
.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; .el-button { width: 100%; justify-content: flex-start; } }
.status-wait { color: #7f8c8d; }
.status-rolling { color: #d68910; font-weight: 700; }
.status-done { color: #2471a3; }
.status-err { color: #c0392b; font-weight: 700; }
</style>

View File

@@ -0,0 +1,461 @@
<template>
<div class="process-page">
<!-- 左栏方案列表 -->
<div class="col-panel left-panel">
<div class="panel-header">
<span>工艺方案</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAddRecipe">新增</el-button>
</div>
<div class="search-bar">
<el-input v-model="searchKey" placeholder="方案号/合金号" size="mini" clearable
prefix-icon="el-icon-search" @input="loadRecipeList" />
</div>
<div class="item-list">
<div v-for="r in recipeList" :key="r.recipeId"
:class="['list-item', { active: selectedRecipeId === r.recipeId }]"
@click="handleSelectRecipe(r)">
<div class="item-main">{{ r.recipeNo }}</div>
<div class="item-sub">{{ r.alloyNo }} · {{ r.inThick }}{{ r.outThick }}mm</div>
<div class="item-actions">
<span class="action-link" @click.stop="handleEditRecipe(r)">编辑</span>
<span class="action-link danger" @click.stop="handleDeleteRecipeFromList(r)">删除</span>
</div>
</div>
<div v-if="!recipeList.length" class="empty-tip">暂无方案</div>
</div>
</div>
<!-- 中栏版本列表 -->
<div class="col-panel mid-panel">
<div class="panel-header">
<span>版本列表</span>
<el-button type="primary" size="mini" icon="el-icon-plus"
:disabled="!selectedRecipeId" @click="handleAddVersion">新增版本</el-button>
</div>
<div class="item-list">
<div v-for="v in versionList" :key="v.versionId"
:class="['list-item', { active: selectedVersionId === v.versionId }]"
@click="handleSelectVersion(v)">
<div class="item-main">
{{ v.versionCode }}
<el-tag v-if="v.isActive === 1" size="mini" type="success" style="margin-left:4px">激活</el-tag>
<el-tag v-else-if="v.status === '1'" size="mini" type="info" style="margin-left:4px">已发布</el-tag>
<el-tag v-else size="mini" style="margin-left:4px">草稿</el-tag>
</div>
<div class="item-sub">{{ v.remark }}</div>
<div class="item-actions">
<span v-if="v.isActive !== 1" class="action-link" @click.stop="handleActivate(v)">激活</span>
<span class="action-link danger" @click.stop="handleDeleteVersion(v)">删除</span>
</div>
</div>
<div v-if="selectedRecipeId && !versionList.length" class="empty-tip">暂无版本请新增</div>
<div v-if="!selectedRecipeId" class="empty-tip">请先选择方案</div>
</div>
</div>
<!-- 右栏版本详情 + 道次 -->
<div class="right-panel">
<div v-if="!versionForm.versionId && !isNewVersion" class="no-select">
<i class="el-icon-document"></i>
<p>请选择或新建版本</p>
</div>
<template v-else>
<!-- 版本基本信息 -->
<div class="detail-header">
<span>{{ isNewVersion ? '新建版本' : versionForm.versionCode }}</span>
<div class="btn-group">
<el-button size="mini" type="primary" icon="el-icon-check" @click="handleSaveVersion">保存</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="handleResetVersion">重置</el-button>
</div>
</div>
<el-form :model="versionForm" ref="versionFormRef" size="mini" label-width="76px"
class="version-form" :rules="versionRules">
<el-row :gutter="16">
<el-col :span="5">
<el-form-item label="版本号" prop="versionCode">
<el-input v-model="versionForm.versionCode" placeholder="如 V1.0" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="状态">
<el-select v-model="versionForm.status" style="width:100%">
<el-option label="草稿" value="0" />
<el-option label="已发布" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="备注">
<el-input v-model="versionForm.remark" clearable />
</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">删除末道</el-button>
</div>
</div>
<el-table :data="passList" border size="mini" class="pass-table"
:row-class-name="passRowClass" height="calc(100vh - 310px)">
<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)" width="90" align="right">
<template slot-scope="{ row }"><span class="calc-val">{{ row.reduction }}</span></template>
</el-table-column>
<el-table-column label="总压下量(mm)" 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>
<!-- 新增/编辑方案对话框 -->
<el-dialog :title="recipeDialogTitle" :visible.sync="recipeDialogVisible" width="520px" append-to-body>
<el-form :model="recipeForm" :rules="recipeRules" ref="recipeFormRef" size="small" label-width="88px">
<el-form-item label="方案记录号" prop="recipeNo">
<el-input v-model="recipeForm.recipeNo" :disabled="!!recipeForm.recipeId" />
</el-form-item>
<el-form-item label="合金号" prop="alloyNo">
<el-input v-model="recipeForm.alloyNo" />
</el-form-item>
<el-form-item label="原料厚度">
<el-input v-model="recipeForm.inThick"><template slot="append">mm</template></el-input>
</el-form-item>
<el-form-item label="成品厚度">
<el-input v-model="recipeForm.outThick"><template slot="append">mm</template></el-input>
</el-form-item>
<el-form-item label="成品宽度">
<el-input v-model="recipeForm.outWidth"><template slot="append">mm</template></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="recipeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRecipe">确定</el-button>
<el-button v-if="recipeForm.recipeId" type="danger" @click="handleDeleteRecipe">删除</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listDrRecipe, addDrRecipe, updateDrRecipe, delDrRecipe,
listDrRecipeVersions, getDrRecipeVersionDetail,
addDrRecipeVersion, updateDrRecipeVersion,
activateDrRecipeVersion, delDrRecipeVersion
} from '@/api/wms/drMill'
const emptyPass = (no) => ({
passNo: no, inThick: '', outThick: '', width: '',
rollForce: '', inTension: '', outTension: '',
maxSpeed: '', inUnitTension: '', outUnitTension: '',
reduction: '', totalReduction: '', remark: ''
})
const emptyVersionForm = () => ({
versionId: null, recipeId: null, versionCode: '', status: '0', remark: ''
})
export default {
name: 'DrProcess',
data() {
return {
// 方案
searchKey: '',
recipeList: [],
selectedRecipeId: null,
recipeDialogVisible: false,
recipeDialogTitle: '新增工艺方案',
recipeForm: {},
recipeRules: {
recipeNo: [{ required: true, message: '请输入方案记录号', trigger: 'blur' }],
alloyNo: [{ required: true, message: '请输入合金号', trigger: 'blur' }],
},
// 版本
versionList: [],
selectedVersionId: null,
isNewVersion: false,
versionForm: emptyVersionForm(),
versionRules: {
versionCode: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
},
// 道次
passList: [],
}
},
mounted() { this.loadRecipeList() },
methods: {
// ── 方案 ──────────────────────────────────────────────────
loadRecipeList() {
listDrRecipe({ recipeNo: this.searchKey, alloyNo: this.searchKey }).then(res => {
this.recipeList = res.data || []
})
},
handleSelectRecipe(r) {
this.selectedRecipeId = r.recipeId
this.isNewVersion = false
this.selectedVersionId = null
this.versionForm = emptyVersionForm()
this.passList = []
this.loadVersionList(r.recipeId)
},
handleAddRecipe() {
this.recipeForm = { recipeNo: '', alloyNo: '', inThick: '', outThick: '', outWidth: '', status: '0' }
this.recipeDialogTitle = '新增工艺方案'
this.recipeDialogVisible = true
},
handleEditRecipe(r) {
this.recipeForm = { ...r }
this.recipeDialogTitle = '编辑工艺方案'
this.recipeDialogVisible = true
},
submitRecipe() {
this.$refs.recipeFormRef.validate(valid => {
if (!valid) return
const api = this.recipeForm.recipeId ? updateDrRecipe : addDrRecipe
api(this.recipeForm).then(() => {
this.$message.success('保存成功')
this.recipeDialogVisible = false
this.loadRecipeList()
})
})
},
handleDeleteRecipe() {
this.$confirm('确定删除该方案及所有版本?', '提示', { type: 'warning' }).then(() => {
delDrRecipe([this.recipeForm.recipeId]).then(() => {
this.$message.success('删除成功')
this.recipeDialogVisible = false
this.selectedRecipeId = null
this.versionList = []
this.versionForm = emptyVersionForm()
this.passList = []
this.loadRecipeList()
})
})
},
handleDeleteRecipeFromList(r) {
this.$confirm(`确定删除方案「${r.recipeNo}」及其所有版本?`, '提示', { type: 'warning' }).then(() => {
delDrRecipe([r.recipeId]).then(() => {
this.$message.success('删除成功')
if (this.selectedRecipeId === r.recipeId) {
this.selectedRecipeId = null
this.versionList = []
this.versionForm = emptyVersionForm()
this.passList = []
}
this.loadRecipeList()
})
})
},
// ── 版本 ──────────────────────────────────────────────────
loadVersionList(recipeId) {
listDrRecipeVersions(recipeId).then(res => {
this.versionList = res.data || []
})
},
handleSelectVersion(v) {
this.selectedVersionId = v.versionId
this.isNewVersion = false
getDrRecipeVersionDetail(v.versionId).then(res => {
this.versionForm = { ...res.data }
this.passList = (res.data.passList || []).map(p => ({ ...p }))
})
},
handleAddVersion() {
this.isNewVersion = true
this.selectedVersionId = null
this.versionForm = { ...emptyVersionForm(), recipeId: this.selectedRecipeId }
this.passList = []
},
handleSaveVersion() {
this.$refs.versionFormRef.validate(valid => {
if (!valid) return
this.versionForm.passList = this.passList
const api = this.isNewVersion ? addDrRecipeVersion : updateDrRecipeVersion
api(this.versionForm).then(res => {
this.$message.success('保存成功')
this.isNewVersion = false
if (res.data) this.versionForm.versionId = res.data
this.loadVersionList(this.selectedRecipeId)
})
})
},
handleResetVersion() {
if (this.isNewVersion) {
this.versionForm = { ...emptyVersionForm(), recipeId: this.selectedRecipeId }
this.passList = []
} else {
this.handleSelectVersion({ versionId: this.selectedVersionId })
}
},
handleActivate(v) {
this.$confirm(`确定激活版本 ${v.versionCode}`, '提示', { type: 'warning' }).then(() => {
activateDrRecipeVersion(v.versionId).then(() => {
this.$message.success('激活成功')
this.loadVersionList(this.selectedRecipeId)
if (this.selectedVersionId === v.versionId) {
this.versionForm.isActive = 1
}
})
})
},
handleDeleteVersion(v) {
this.$confirm(`确定删除版本 ${v.versionCode}`, '提示', { type: 'warning' }).then(() => {
delDrRecipeVersion(v.versionId).then(() => {
this.$message.success('删除成功')
if (this.selectedVersionId === v.versionId) {
this.selectedVersionId = null
this.versionForm = emptyVersionForm()
this.passList = []
}
this.loadVersionList(this.selectedRecipeId)
})
})
},
// ── 道次 ──────────────────────────────────────────────────
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) : ''
const base = parseFloat(this.passList[0] && this.passList[0].inThick) || 0
for (const p of this.passList) {
const pOut = parseFloat(p.outThick) || 0
if (base > 0 && pOut > 0) p.totalReduction = parseFloat((base - pOut).toFixed(3))
}
},
passRowClass({ rowIndex }) { return rowIndex % 2 === 0 ? '' : 'alt-row' }
}
}
</script>
<style scoped lang="scss">
.process-page {
display: flex; height: calc(100vh - 84px); gap: 0;
background: #f5f7fa; padding: 10px 12px; box-sizing: border-box;
}
.col-panel {
flex-shrink: 0; background: #fff; border: 1px solid #e4e7ed;
border-radius: 3px; display: flex; flex-direction: column;
margin-right: 8px; overflow: hidden;
}
.left-panel { width: 200px; }
.mid-panel { width: 200px; }
.panel-header {
background: #fff; color: #303133; padding: 7px 10px;
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; font-weight: 700; flex-shrink: 0;
border-bottom: 1px solid #e4e7ed;
}
.search-bar { padding: 6px 8px; border-bottom: 1px solid #e4e7ed; flex-shrink: 0; }
.item-list { flex: 1; overflow-y: auto; }
.list-item {
padding: 7px 10px; border-bottom: 1px solid #f5f7fa;
cursor: pointer; transition: background .15s;
&:hover { background: #f5f7fa; }
&.active { background: #ecf5ff; border-left: 3px solid #409eff; }
.item-main { font-size: 12px; font-weight: 700; color: #303133; }
.item-sub { font-size: 11px; color: #909399; margin-top: 2px; }
.item-actions {
display: flex; gap: 8px; margin-top: 3px;
.action-link { font-size: 11px; color: #409eff; cursor: pointer;
&:hover { text-decoration: underline; }
&.danger { color: #f56c6c; }
}
}
}
.empty-tip { text-align: center; color: #c0c4cc; padding: 24px 0; font-size: 12px; }
.right-panel {
flex: 1; background: #fff; border: 1px solid #e4e7ed;
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: #c0c4cc; font-size: 13px;
i { font-size: 48px; margin-bottom: 10px; }
}
.detail-header {
background: #fff; color: #303133; padding: 7px 12px;
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; font-weight: 700; flex-shrink: 0;
border-bottom: 1px solid #e4e7ed;
}
.btn-group { display: flex; gap: 6px; }
.version-form { padding: 8px 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: #f5f7fa; border-bottom: 1px solid #e4e7ed;
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; font-weight: 700; color: #303133; flex-shrink: 0;
}
.pass-table {
flex: 1;
::v-deep .el-table__row.alt-row td { background: #f5f7fa !important; }
::v-deep .el-input__inner { text-align: right; padding: 0 4px !important; }
}
.calc-val { font-family: 'Courier New', monospace; font-weight: 600; color: #409eff; }
</style>

View File

@@ -65,7 +65,7 @@
/>
</el-select>
</template>
<span class="search-label">点位名称</span>
<span class="search-label">参数名称</span>
<el-input
v-model="filterName"
placeholder="请输入"
@@ -80,8 +80,13 @@
size="mini"
type="primary"
icon="el-icon-plus"
@click="openParamDialog()"
>新建参数</el-button>
<el-button
size="mini"
icon="el-icon-setting"
@click="openPlanDialog()"
>新建方案点位</el-button>
>管理点位</el-button>
<el-button
size="mini"
type="success"
@@ -90,134 +95,42 @@
>模板导入</el-button>
</div>
<!-- 方案点位 -->
<!-- 参数平铺 -->
<el-table
v-loading="planLoading"
:data="filteredPlans"
v-loading="planLoading || allParamLoading"
:data="filteredFlatRows"
size="small"
highlight-current-row
@current-change="onPlanSelect"
border
>
<el-table-column label="序号" type="index" align="center" />
<el-table-column label="父级名称" show-overflow-tooltip>
<template slot-scope="{ row }">{{ segLabel(row.segmentType) }} {{ row.segmentName || '—' }}</template>
</el-table-column>
<el-table-column label="点位名称" prop="pointName" show-overflow-tooltip />
<el-table-column label="点位编码" prop="pointCode" show-overflow-tooltip />
<el-table-column label="操作" align="right">
<el-table-column label="" width="68" align="center" fixed>
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openPlanDialog(row)">编辑</el-button>
<el-button type="text" size="mini" @click.stop="openParamDialog(row)">参数</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removePlan(row)">删除</el-button>
<span :class="['seg-chip', 'seg-' + (row._segmentType || '').toLowerCase()]">{{ row._segmentLabel }}</span>
</template>
</el-table-column>
<el-table-column label="参数名称" prop="paramName" min-width="130" show-overflow-tooltip />
<el-table-column label="编码" prop="paramCode" width="115" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" align="center" width="62" />
<el-table-column label="设定值" prop="targetValue" align="right" width="82" />
<el-table-column label="下限" prop="lowerLimit" align="right" width="72" />
<el-table-column label="上限" prop="upperLimit" align="right" width="72" />
<el-table-column label="实际状态" align="center" width="90">
<template slot-scope="{ row }">
<span v-if="paramAnomalyMap[row.paramCode]" class="anomaly-badge">
<i class="el-icon-warning-outline" /> 异常
</span>
<span v-else-if="row.upperLimit != null || row.lowerLimit != null" class="normal-badge">
<i class="el-icon-circle-check" /> 正常
</span>
<span v-else class="no-data-badge"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="right" width="100" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="editParamFromFlat(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="deleteParamFromFlat(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 方案参数面板 -->
<template v-if="selectedPlan">
<div class="param-header">
<span>{{ selectedPlan.pointName || selectedPlan.pointCode }} 参数</span>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openParamDialog()">新建参数</el-button>
</div>
<el-table v-loading="paramLoading" :data="paramList" size="small">
<el-table-column label="参数编码" prop="paramCode" width="110" show-overflow-tooltip />
<el-table-column label="参数名称" prop="paramName" show-overflow-tooltip />
<el-table-column label="实际值ID" prop="actualSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.actualSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="L1设定值ID" prop="presetSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.presetSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="设定值" prop="targetValue" align="right" width="80" />
<el-table-column label="最小值" prop="lowerLimit" align="right" width="80" />
<el-table-column label="最大值" prop="upperLimit" align="right" width="80" />
<el-table-column label="单位" prop="unit" align="center" width="60" />
<el-table-column label="更新时间" align="center" width="136">
<template slot-scope="{ row }">{{ (row.updateTime || row.createTime || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="实际状态" align="center" width="90">
<template slot-scope="{ row }">
<span v-if="paramAnomalyMap[row.paramCode]" class="anomaly-badge">
<i class="el-icon-warning-outline" /> 异常
</span>
<span v-else-if="row.upperLimit != null || row.lowerLimit != null" class="normal-badge">
<i class="el-icon-circle-check" /> 正常
</span>
<span v-else class="no-data-badge"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="openParamDialog(null, row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="removeParam(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 偏差分析区块 -->
<template v-if="planAnomalies.length">
<div class="anomaly-section-header">
<i class="el-icon-warning" style="color:#E6A23C;margin-right:4px" />
实际生产偏差分析
<el-tag type="warning" size="mini" effect="plain" style="margin-left:8px">{{ planAnomalies.length }} 项异常</el-tag>
<el-button type="text" size="mini" style="margin-left:auto" @click="anomalyExpanded = !anomalyExpanded">
{{ anomalyExpanded ? '收起' : '展开' }}
</el-button>
</div>
<div v-show="anomalyExpanded">
<el-table :data="planAnomalies" size="small" border>
<el-table-column label="参数" prop="paramName" width="110" show-overflow-tooltip />
<el-table-column label="规程设定值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedTarget != null ? row.storedTarget : '—' }}</template>
</el-table-column>
<el-table-column label="规程最大值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedUpper != null ? row.storedUpper : '—' }}</template>
</el-table-column>
<el-table-column label="规程最小值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedLower != null ? row.storedLower : '—' }}</template>
</el-table-column>
<el-table-column label="实际最大值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH') ? 'val-over' : ''">
{{ row.actualMax != null ? row.actualMax : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="实际最小值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH') ? 'val-under' : ''">
{{ row.actualMin != null ? row.actualMin : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="最大偏差" align="right" width="88">
<template slot-scope="{ row }">
<span v-if="row.deviationMax != null" class="val-over">+{{ row.deviationMax }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="最小偏差" align="right" width="88">
<template slot-scope="{ row }">
<span v-if="row.deviationMin != null" class="val-under">{{ row.deviationMin }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="来源钢卷" prop="coilId" show-overflow-tooltip width="120" />
<el-table-column label="检测时间" prop="detectedAt" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ (row.detectedAt || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="异常类型" align="center" width="120">
<template slot-scope="{ row }">
<el-tag v-if="row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH'" size="mini" type="danger" style="margin:1px">超上限</el-tag>
<el-tag v-if="row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH'" size="mini" type="warning" style="margin:1px">低于下限</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 对比图 -->
<div ref="anomalyChart" class="anomaly-chart" />
</div>
</template>
</template>
</div>
</div>
@@ -361,6 +274,16 @@
<!-- 方案参数 dialog -->
<el-dialog :title="paramTitle" :visible.sync="paramOpen" width="480px" append-to-body @close="paramForm = {}">
<el-form ref="paramFormRef" :model="paramForm" :rules="paramRules" label-width="90px" size="small">
<el-form-item v-if="!paramForm.paramId" label="所属点位" prop="planId">
<el-select v-model="paramForm.planId" style="width:100%" placeholder="请选择点位">
<el-option
v-for="plan in planList"
:key="plan.planId"
:label="segLabel(plan.segmentType) + ' ' + plan.pointName"
:value="plan.planId"
/>
</el-select>
</el-form-item>
<el-form-item label="参数编码" prop="paramCode">
<el-input v-model="paramForm.paramCode" maxlength="64" />
</el-form-item>
@@ -488,7 +411,7 @@ const TEMPLATE_HEADERS = ['段类型', '段名称', '点位名称', '参数名
/** 表头字段映射 */
const HEADER_MAP = {
'段类型': 'segmentType',
'段名称': 'segmentName',
'段名称': 'segmentName',
'点位名称': 'pointName',
'参数名称': 'paramName',
'设定值': 'targetValue',
@@ -514,9 +437,8 @@ export default {
appliedFilterName: '',
planList: [],
planLoading: false,
selectedPlan: null,
paramList: [],
paramLoading: false,
allParamList: [],
allParamLoading: false,
planOpen: false,
planTitle: '',
planSubmitLoading: false,
@@ -531,6 +453,7 @@ export default {
paramSubmitLoading: false,
paramForm: {},
paramRules: {
planId: [{ required: true, message: '请选择所属点位', trigger: 'change' }],
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
},
@@ -632,12 +555,6 @@ export default {
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
return [...SEGMENT_FORM_OPTIONS, ...extra]
},
/** 当前选中点位下的异常条目 */
planAnomalies() {
if (!this.selectedPlan) return []
return this.allAnomalies.filter(a => a.paramCode === this.selectedPlan.pointCode ||
this.paramList.some(p => p.paramCode === a.paramCode))
},
/** paramCode → anomaly 的快速索引(用于状态列) */
paramAnomalyMap() {
const map = {}
@@ -648,45 +565,42 @@ export default {
if (!this.coilAnomalyCoilId) return []
return this.allAnomalies.filter(a => a.coilId === this.coilAnomalyCoilId)
},
filteredPlans() {
/** 所有 plan 的参数平铺成一行,带 plan 的段/点位信息 */
flatRows() {
const SEG_LABELS = { INLET: '入口段', PROCESS: '工艺段', OUTLET: '出口段' }
const planMap = {}
for (const plan of this.planList) planMap[plan.planId] = plan
const rows = []
for (const param of this.allParamList) {
const plan = planMap[param.planId]
if (!plan) continue
rows.push({
...param,
_planId: plan.planId,
_segmentType: plan.segmentType,
_segmentLabel: SEG_LABELS[plan.segmentType] || plan.segmentName || plan.segmentType,
_pointName: plan.pointName,
_sortOrder: plan.sortOrder || 0
})
}
rows.sort((a, b) => {
const s = (a._sortOrder || 0) - (b._sortOrder || 0)
return s !== 0 ? s : (a.paramId || 0) - (b.paramId || 0)
})
return rows
},
filteredFlatRows() {
const type = this.activeSegmentType
const sub = this.activeSegmentName
return this.planList.filter(p => {
const typeOk =
type === '' || type === undefined || type === null
? true
: String(p.segmentType) === String(type)
let nameOkSeg = true
if (typeOk && type !== '' && type !== undefined && type !== null) {
if (sub === '' || sub === undefined || sub === null) {
nameOkSeg = true
} else if (sub === '__EMPTY__') {
const sn = p.segmentName != null ? String(p.segmentName).trim() : ''
nameOkSeg = !sn
} else {
const sn = p.segmentName != null ? String(p.segmentName).trim() : ''
nameOkSeg = sn === String(sub).trim()
}
}
const nameOk = !this.appliedFilterName || (p.pointName || '').includes(this.appliedFilterName)
return typeOk && nameOkSeg && nameOk
const name = this.appliedFilterName
return this.flatRows.filter(r => {
const typeOk = !type || String(r._segmentType) === String(type)
const nameOk = !name || (r.paramName || '').includes(name) || (r.paramCode || '').includes(name)
return typeOk && nameOk
})
}
},
watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } },
planAnomalies: {
handler(list) {
if (list.length && this.anomalyExpanded) {
this.$nextTick(() => this.renderAnomalyChart())
}
}
},
anomalyExpanded(val) {
if (val && this.planAnomalies.length) {
this.$nextTick(() => this.renderAnomalyChart())
}
},
segmentTypeTabOptions: {
handler() {
const a = this.activeSegmentType
@@ -852,22 +766,26 @@ export default {
},
loadPlans() {
this.planLoading = true
this.selectedPlan = null
this.paramList = []
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
this.planList = res.rows || []
this.loadAllParams()
}).catch(e => console.error(e)).finally(() => { this.planLoading = false })
},
loadParams(planId) {
this.paramLoading = true
listProcessPlanParam({ planId, pageNum: 1, pageSize: 500 }).then(res => {
this.paramList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.paramLoading = false })
},
onPlanSelect(row) {
if (!row) return
this.selectedPlan = row
this.loadParams(row.planId)
async loadAllParams() {
if (!this.planList.length) { this.allParamList = []; return }
this.allParamLoading = true
try {
const results = await Promise.all(
this.planList.map(plan =>
listProcessPlanParam({ planId: plan.planId, pageNum: 1, pageSize: 500 })
.then(res => res.rows || [])
.catch(() => [])
)
)
this.allParamList = results.flat()
} finally {
this.allParamLoading = false
}
},
applyFilter() { this.appliedFilterName = this.filterName },
resetFilter() {
@@ -921,28 +839,33 @@ export default {
}).catch(() => {})
},
openParamDialog(planRow, paramRow) {
const targetPlan = planRow || this.selectedPlan
if (!targetPlan) { this.$message.warning('请先选择一个方案点位'); return }
if (!this.selectedPlan || this.selectedPlan.planId !== targetPlan.planId) {
this.selectedPlan = targetPlan
this.loadParams(targetPlan.planId)
}
this.paramForm = paramRow
? { ...paramRow }
: {
planId: targetPlan.planId,
paramCode: undefined,
paramName: undefined,
planId: planRow ? planRow.planId : undefined,
paramCode: undefined,
paramName: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
unit: undefined,
remark: undefined
lowerLimit: undefined,
upperLimit: undefined,
unit: undefined,
remark: undefined
}
this.paramTitle = paramRow ? '编辑方案参数' : '新建方案参数'
this.paramOpen = true
this.$nextTick(() => this.$refs.paramFormRef && this.$refs.paramFormRef.clearValidate())
},
editParamFromFlat(row) {
this.openParamDialog(null, row)
},
deleteParamFromFlat(row) {
this.$modal.confirm('确认删除该参数?').then(() => {
return delProcessPlanParam(row.paramId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadAllParams()
}).catch(() => {})
},
submitParam() {
this.$refs.paramFormRef.validate(ok => {
if (!ok) return
@@ -951,18 +874,10 @@ export default {
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.paramOpen = false
this.loadParams(this.selectedPlan.planId)
this.loadAllParams()
}).catch(e => console.error(e)).finally(() => { this.paramSubmitLoading = false })
})
},
removeParam(row) {
this.$modal.confirm('确认删除该参数?').then(() => {
return delProcessPlanParam(row.paramId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadParams(this.selectedPlan.planId)
}).catch(() => {})
},
// ===================== 导入相关方法 =====================
/**
* 打开导入对话框
@@ -1218,7 +1133,7 @@ export default {
async batchImport() {
// <20>照点位名称分组每个点位创建一条记录然后添加多个参数
const pointGroups = {}
// 分组处理
this.tableData.forEach(row => {
const pointKey = `${row.segmentType}_${row.segmentName}_${row.pointName}`
@@ -1249,12 +1164,12 @@ export default {
try {
await this.importOnePoint(group)
this.importedCount += group.params.length
// 更新进度
index += group.params.length
const currentProgress = Math.round((index / this.totalCount) * 100)
this.progress = currentProgress
await new Promise(resolve => setTimeout(resolve, 50))
} catch (error) {
throw new Error(`导入点位"${group.pointName}"失败:${error.message}`)
@@ -1275,7 +1190,7 @@ export default {
pointCode: group.pointCode,
sortOrder: 0
}
const planRes = await addProcessPlan(planParams)
if (planRes.code !== 200) {
throw new Error(`点位创建失败:${planRes.msg || '接口返回异常'}`)
@@ -1293,7 +1208,7 @@ export default {
lowerLimit: param.lowerLimit ? Number(param.lowerLimit) : null,
unit: param.unit
}
const paramRes = await addProcessPlanParam(paramParams)
if (paramRes.code !== 200) {
throw new Error(`参数"${param.paramName}"创建失败:${paramRes.msg || '接口返回异常'}`)
@@ -1354,7 +1269,7 @@ export default {
// 创建工作簿
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet(templateData)
// 设置列宽
ws['!cols'] = [
{ wch: 10 }, // 段类型
@@ -1366,7 +1281,7 @@ export default {
{ wch: 10 }, // 下限
{ wch: 10 } // 单位
]
XLSX.utils.book_append_sheet(wb, ws, '导入模板')
XLSX.writeFile(wb, '方案点位导入模板.xlsx')
},
@@ -1558,6 +1473,19 @@ export default {
.btn-danger { color: #f56c6c; }
/* ── 段 chip ── */
.seg-chip {
display: inline-block;
padding: 2px 7px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.seg-inlet { background: #ecf5ff; color: #3a6ea8; border: 1px solid #b3d8ff; }
.seg-process { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
.seg-outlet { background: #fdf6ec; color: #a86a00; border: 1px solid #f5dab1; }
/* ── 偏差分析 ── */
.anomaly-section-header {
display: flex;