feat(linkage): 鞍座改为预备生产位 + 在线改人工触发 + 计划删除

- 上卷鞍座=预备生产位(不生产);投入生产后离开鞍座、进入生产中并转入物料跟踪,鞍座随即空出
- 在线状态改为人工触发:移动到入口端才变在线,引擎不再自动上线(ensure_online 置空)
- 单卷在产:投入生产时若已有在产卷则拒绝
- 物料跟踪显示在产卷实时进度(客户端外推带头长度/进度条)
- 入口跟踪鞍座卡片改为预备生产展示(去掉进度)
- 计划管理新增删除按钮 + DELETE /plan/{id}(生产中不可删)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 15:20:16 +08:00
parent 1a6deea4bb
commit 62c484411e
6 changed files with 103 additions and 94 deletions

View File

@@ -19,6 +19,7 @@ export const updateProductionRecord = (id, data) => request.put(`/production/${i
export const getPlans = params => request.get('/plan/', { params })
export const createPlan = data => request.post('/plan/', data)
export const updatePlan = (id, data) => request.put(`/plan/${id}`, data)
export const deletePlan = id => request.delete(`/plan/${id}`)
export const confirmPlan = id => request.patch(`/plan/${id}/confirm`)
export const startProducing = id => request.patch(`/plan/${id}/start`) // 移动到上卷鞍座(兼容)
export const moveToSaddle = id => request.patch(`/plan/${id}/start`)

View File

@@ -5,9 +5,9 @@
<div class="card">
<div class="card-header">
入口跟踪
<span class="ch-badge">{{ saddle ? '生产' + statusLabel(saddle.status) : '鞍座空闲' }}</span>
<span class="ch-badge">{{ saddle ? '上卷鞍座:预备生产' : '上卷鞍座空闲' }}</span>
<span style="margin-left:auto;display:flex;gap:8px;align-items:center;">
<button v-if="saddle && saddle.status !== 'producing'" class="btn btn-primary" @click="commit(saddle)">投入生产</button>
<button v-if="saddle" class="btn btn-primary" @click="commit(saddle)">投入生产</button>
<button class="btn btn-outline" @click="refreshAll">刷新</button>
</span>
</div>
@@ -38,28 +38,24 @@
</div>
</div>
<!-- 下方单个上卷鞍座唯一进入生产的工位 -->
<!-- 下方单个上卷鞍座预备生产位投入生产后离开鞍座进入物料跟踪 -->
<div :class="['pos-cell', 'saddle-cell', { filled: !!saddle }]">
<div class="pos-title saddle-title">上卷鞍座<span class="st-tag">生产</span></div>
<div class="pos-title saddle-title">上卷鞍座<span class="st-tag">预备生产位</span></div>
<div v-if="saddle" class="saddle-body">
<div class="sb-info">
<span><i>冷卷号</i>{{ saddle.cold_coil_no || saddle.plan_no }}</span>
<span><i>热卷号</i>{{ saddle.hot_coil_no || '—' }}</span>
<span><i>钢种</i>{{ saddle.steel_grade || '—' }}</span>
<span><i>规格</i>{{ fmt(saddle.product_thickness, 2) }}×{{ fmt(saddle.product_width, 0) }}</span>
<span><i>来料重[t]</i>{{ fmt(saddle.incoming_weight, 3) }}</span>
<span><i>轧制模式</i>{{ saddle.rolling_mode || '—' }}</span>
<span><i>状态</i><b :class="badgeOf(saddle.status)" style="padding:0 6px;border-radius:2px;">{{ statusLabel(saddle.status) }}</b></span>
</div>
<div class="sb-run">
<div class="sb-metric"><span class="m-v">{{ fmt(saddle.run_speed, 0) }}</span><span class="m-u">m/min</span></div>
<div class="sb-metric"><span class="m-v">{{ fmt(saddle.run_length_m, 0) }}</span><span class="m-u">/ {{ TARGET }} m</span></div>
<div class="sb-prog">
<div class="prog-bar-wrap" style="height:9px;"><div class="prog-bar-fill" :style="{ width: progPct(saddle) + '%', background: progColor(saddle) }"></div></div>
<span class="sb-pct">{{ progPct(saddle).toFixed(1) }}%</span>
</div>
<div class="sb-stage">
<span class="badge badge-yellow">预备生产</span>
<span class="sb-hint">点击右上投入生产 进入生产中并转入物料跟踪</span>
</div>
</div>
<div v-else class="pos-empty">空闲 移动计划上卷鞍座开始生产</div>
<div v-else class="pos-empty">空闲 把在线计划移动到上卷鞍座预备生产</div>
</div>
</div>
</div>
@@ -203,8 +199,8 @@ export default {
async commit(p) {
try {
await commitProducing(p.id)
this.$message.success('已投入生产')
this.fetchSaddle()
this.$message.success('已投入生产,转入物料跟踪')
this.refreshAll()
} catch (e) {
this.$message.error(e?.response?.data?.detail || '操作失败')
}
@@ -252,12 +248,9 @@ export default {
span { font-size: 12px; color: $text-primary; font-family: $font-mono; font-weight: 600; display: flex; gap: 6px;
i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; min-width: 56px; } }
}
.sb-run { display: flex; align-items: center; gap: 16px; border-left: 1px solid $border; padding-left: 16px; }
.sb-metric { display: flex; flex-direction: column; line-height: 1.1;
.m-v { font-size: 22px; font-family: $font-mono; font-weight: 700; color: $sms-teal; }
.m-u { font-size: 10px; color: $text-muted; } }
.sb-prog { flex: 1; display: flex; flex-direction: column; gap: 5px;
.sb-pct { font-size: 11px; font-family: $font-mono; font-weight: 700; color: $sms-teal; text-align: right; } }
.sb-stage { display: flex; flex-direction: column; align-items: center; gap: 8px; justify-content: center;
border-left: 1px solid $border; padding-left: 16px;
.sb-hint { font-size: 11px; color: $text-muted; text-align: center; } }
.action-link { color: $accent-green; cursor: pointer; font-size: 12px; &:hover { text-decoration: underline; } }

View File

@@ -45,7 +45,12 @@
<span class="kv-label">冷卷号</span><span class="kv-value">{{ producingPlan.cold_coil_no || producingPlan.plan_no }}</span>
<span class="kv-label">钢种</span><span class="kv-value">{{ producingPlan.steel_grade || '—' }}</span>
<span class="kv-label">规格</span><span class="kv-value">{{ fmt(producingPlan.product_thickness) }}×{{ fmt(producingPlan.product_width, 0) }}</span>
<span class="kv-label">分卷</span><span class="kv-value">{{ producingPlan.split_count || 1 }}</span>
<span class="kv-label">线速度</span><span class="kv-value">{{ fmt(producingPlan.run_speed, 0) }}<span class="kv-unit"> m/min</span></span>
<span class="kv-label">带头</span><span class="kv-value">{{ fmt(prodLength, 0) }}<span class="kv-unit"> / 2000 m</span></span>
<div class="prog-bar-wrap" style="flex:1;min-width:120px;height:8px;">
<div class="prog-bar-fill" :style="{ width: prodPct + '%', background: prodPct >= 100 ? 'var(--accent-green)' : 'var(--sms-teal)' }"></div>
</div>
<span class="kv-value">{{ prodPct.toFixed(0) }}%</span>
</div>
<table class="data-table compact" v-if="onlinePlans.length">
<thead><tr><th>冷卷号</th><th>钢种</th><th>厚度</th><th>宽度</th><th>分卷</th><th>下达时间</th><th>操作</th></tr></thead>
@@ -380,6 +385,8 @@ export default {
data() {
return {
l1Online: false,
now: Date.now(),
prodBase: null, // { id, len, at, speed } 用于客户端平滑外推生产进度
current: { coil_no: '26053552', speed: 95.0 },
prev_coil_no: '26053551',
weld: { position: 0.08 },
@@ -428,8 +435,18 @@ export default {
}
},
computed: {
onlinePlans() { return this.plans.filter(p => p.status === 'online') },
onlinePlans() { return this.plans.filter(p => p.status === 'online' && p.on_saddle !== 1) },
producingPlan() { return this.plans.find(p => p.status === 'producing') || null },
prodLength() {
const p = this.producingPlan
if (!p) return 0
const b = this.prodBase
if (b && b.id === p.id) {
return Math.min(2000, b.len + b.speed / 60 * Math.max(0, (this.now - b.at) / 1000))
}
return p.run_length_m || 0
},
prodPct() { return this.producingPlan ? Math.max(0, Math.min(100, this.prodLength / 2000 * 100)) : 0 },
equipments() {
const n = EQUIPMENTS.length
const xStart = 50, xEnd = 1850
@@ -540,10 +557,10 @@ export default {
try {
const res = await getPlans({ page: 1, page_size: 50 })
this.plans = res.data.items || []
// 把生产中的卷号同步到产线显示
if (this.producingPlan && this.producingPlan.cold_coil_no) {
this.current.coil_no = this.producingPlan.cold_coil_no
}
// 把生产中的卷号同步到产线显示 + 记录进度外推基准
const pp = this.producingPlan
if (pp && pp.cold_coil_no) this.current.coil_no = pp.cold_coil_no
this.prodBase = pp ? { id: pp.id, len: pp.run_length_m || 0, at: Date.now(), speed: pp.run_speed || 0 } : null
} catch (e) { /* ignore */ }
},
async movePlan(p) {
@@ -623,6 +640,7 @@ export default {
return { coil, gap, speed, aux }
},
tick() {
this.now = Date.now()
this.weld.position = (this.weld.position + 0.012) % 1
// 新一卷开始时滚动卷号
if (this.weld.position < 0.012) {

View File

@@ -78,8 +78,10 @@
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td @click.stop>
<span class="action-link" @click="openDialog(row)">编辑</span>
<span v-if="row.status === 'online' || row.status === 'ready'"
<span v-if="row.status === 'ready' || row.status === 'online'"
class="action-link" style="color:var(--accent-green)" @click="openMove(row)">移动</span>
<span v-if="row.status !== 'producing'"
class="action-link" style="color:var(--accent-red)" @click="removeRow(row)">删除</span>
</td>
</tr>
<tr v-if="!tableData.length && !loading">
@@ -268,7 +270,7 @@
</template>
<script>
import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm, movePlan, getLastPlanTemplate } from '@/api'
import { getPlans, createPlan, updatePlan, deletePlan, confirmPlan as apiConfirm, movePlan, getLastPlanTemplate } from '@/api'
const SADDLE = '上卷鞍座'
const POSITIONS = [
@@ -381,6 +383,17 @@ export default {
this.$message.success('已上线')
this.fetchData()
},
async removeRow(row) {
if (!confirm(`确认删除计划 ${row.cold_coil_no || row.plan_no}`)) return
try {
await deletePlan(row.id)
this.$message.success('已删除')
if (this.selectedRow && this.selectedRow.id === row.id) this.selectedRow = null
this.fetchData()
} catch (e) {
this.$message.error(e?.response?.data?.detail || '删除失败')
}
},
openMove(row) {
this.moveDialog = { visible: true, plan: row, target: row.position || '' }
},