- 上卷鞍座=预备生产位(不生产);投入生产后离开鞍座、进入生产中并转入物料跟踪,鞍座随即空出
- 在线状态改为人工触发:移动到入口端才变在线,引擎不再自动上线(ensure_online 置空)
- 单卷在产:投入生产时若已有在产卷则拒绝
- 物料跟踪显示在产卷实时进度(客户端外推带头长度/进度条)
- 入口跟踪鞍座卡片改为预备生产展示(去掉进度)
- 计划管理新增删除按钮 + DELETE /plan/{id}(生产中不可删)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
273 lines
14 KiB
Vue
273 lines
14 KiB
Vue
<template>
|
||
<div class="entry-page">
|
||
|
||
<!-- ─── 入口跟踪:8 个设备工位 + 下方单个上卷鞍座 ─── -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
入口跟踪
|
||
<span class="ch-badge">{{ saddle ? '上卷鞍座:预备生产' : '上卷鞍座:空闲' }}</span>
|
||
<span style="margin-left:auto;display:flex;gap:8px;align-items:center;">
|
||
<button v-if="saddle" class="btn btn-primary" @click="commit(saddle)">投入生产</button>
|
||
<button class="btn btn-outline" @click="refreshAll">刷新</button>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="entry-grid">
|
||
<!-- 8 个设备工位 -->
|
||
<div v-for="(row, ri) in equipRows" :key="ri" class="entry-row">
|
||
<div
|
||
v-for="pos in row"
|
||
:key="pos"
|
||
:class="['pos-cell', { filled: !!occupantOf(pos) }]"
|
||
>
|
||
<div class="pos-title">{{ pos }}</div>
|
||
<table v-if="occupantOf(pos)" class="pos-table">
|
||
<tbody>
|
||
<tr><td class="k">冷卷号</td><td class="v">{{ occ(pos,'cold_coil_no') }}</td></tr>
|
||
<tr><td class="k">热卷号</td><td class="v">{{ occ(pos,'hot_coil_no') }}</td></tr>
|
||
<tr><td class="k">钢种</td><td class="v">{{ occ(pos,'steel_grade') }}</td></tr>
|
||
<tr><td class="k">来料厚[mm]</td><td class="v">{{ occNum(pos,'incoming_thickness',2) }}</td></tr>
|
||
<tr><td class="k">成品厚[mm]</td><td class="v">{{ occNum(pos,'product_thickness',2) }}</td></tr>
|
||
<tr><td class="k">成品宽[mm]</td><td class="v">{{ occNum(pos,'product_width',0) }}</td></tr>
|
||
<tr><td class="k">来料重[t]</td><td class="v">{{ occNum(pos,'incoming_weight',3) }}</td></tr>
|
||
<tr><td class="k">轧制模式</td><td class="v">{{ occ(pos,'rolling_mode') }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div v-else class="pos-empty">空闲</div>
|
||
<div v-if="occupantOf(pos)" class="pos-act"><span class="action-link" @click="openMove(occupantOf(pos))">移动</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下方:单个上卷鞍座(预备生产位;投入生产后离开鞍座进入物料跟踪) -->
|
||
<div :class="['pos-cell', 'saddle-cell', { filled: !!saddle }]">
|
||
<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>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── 入口队列表 ─── -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
入口队列
|
||
<span class="ch-badge">{{ queuePlans.length }} 条</span>
|
||
</div>
|
||
<div class="table-scroll">
|
||
<table class="data-table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>序号</th><th>冷卷号</th><th>热卷号</th><th>钢种</th>
|
||
<th>规格(厚×宽)</th><th>来料重量</th><th>轧制模式</th><th>状态</th><th>当前位置</th><th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(p, i) in queuePlans" :key="p.id">
|
||
<td class="td-num">{{ i + 1 }}</td>
|
||
<td class="td-num">{{ p.cold_coil_no || p.plan_no }}</td>
|
||
<td class="td-num">{{ p.hot_coil_no || '—' }}</td>
|
||
<td>{{ p.steel_grade || '—' }}</td>
|
||
<td class="td-num">{{ fmt(p.product_thickness, 2) }} × {{ fmt(p.product_width, 0) }}</td>
|
||
<td class="td-num">{{ fmt(p.incoming_weight, 2) }}</td>
|
||
<td>{{ p.rolling_mode || '—' }}</td>
|
||
<td><span :class="['badge', badgeOf(p.status)]">{{ statusLabel(p.status) }}</span></td>
|
||
<td class="td-muted">{{ p.position || '—' }}</td>
|
||
<td><span class="action-link" @click="openMove(p)">移动</span></td>
|
||
</tr>
|
||
<tr v-if="!queuePlans.length">
|
||
<td colspan="10" class="td-muted" style="text-align:center;padding:14px;">暂无在线/准备好的计划</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── 移动-位置选择弹窗 ─── -->
|
||
<div v-if="moveDialog.visible" class="modal-mask" @click.self="moveDialog.visible=false">
|
||
<div class="modal-box" style="width:520px;">
|
||
<div class="modal-header">
|
||
移动计划 — {{ moveDialog.plan && (moveDialog.plan.cold_coil_no || moveDialog.plan.plan_no) }}
|
||
<span class="modal-close" @click="moveDialog.visible=false">✕</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="kv-label" style="margin-bottom:8px;">选择目标位置(只有「上卷鞍座」会进入生产环节)</div>
|
||
<div class="pos-pick">
|
||
<span
|
||
v-for="pos in positions"
|
||
:key="pos"
|
||
:class="['pick-item', { active: moveDialog.target === pos, saddle: pos === SADDLE }]"
|
||
@click="moveDialog.target = pos"
|
||
>{{ pos }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" @click="moveDialog.visible=false">取消</button>
|
||
<button class="btn btn-primary" :disabled="!moveDialog.target || moving" @click="confirmMove">{{ moving ? '移动中...' : '确定' }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getPlans, getSaddle, movePlan, commitProducing } from '@/api'
|
||
|
||
const STATUS_LABEL = { ready: '准备好', online: '在线', producing: '生产中', produced: '生产完成' }
|
||
const STATUS_BADGE = { ready: 'badge-gray', online: 'badge-green', producing: 'badge-yellow', produced: 'badge-blue' }
|
||
const SADDLE = '上卷鞍座'
|
||
const EQUIP_ROWS = [
|
||
['1#上卷小车', '1#称重位', '1#地辊', '1#倒卷小车'],
|
||
['2#上卷小车', '2#称重位', '2#地辊', '2#倒卷小车'],
|
||
]
|
||
const POSITIONS = [...EQUIP_ROWS[0], ...EQUIP_ROWS[1], SADDLE]
|
||
const TARGET = 2000
|
||
|
||
export default {
|
||
name: 'EntryTracking',
|
||
data() {
|
||
return {
|
||
plans: [], saddle: null, TARGET, SADDLE,
|
||
positions: POSITIONS, equipRows: EQUIP_ROWS,
|
||
timer: null, fastTimer: null, moving: false,
|
||
moveDialog: { visible: false, plan: null, target: '' },
|
||
}
|
||
},
|
||
computed: {
|
||
queuePlans() {
|
||
return this.plans
|
||
.filter(p => (p.status === 'online' || p.status === 'ready') && p.on_saddle !== 1)
|
||
.sort((a, b) => (a.status === 'online' ? 0 : 1) - (b.status === 'online' ? 0 : 1))
|
||
},
|
||
},
|
||
created() {
|
||
this.refreshAll()
|
||
this.fastTimer = setInterval(this.fetchSaddle, 2000)
|
||
this.timer = setInterval(this.fetchPlans, 5000)
|
||
},
|
||
beforeDestroy() { clearInterval(this.timer); clearInterval(this.fastTimer) },
|
||
methods: {
|
||
refreshAll() { this.fetchPlans(); this.fetchSaddle() },
|
||
async fetchPlans() {
|
||
try {
|
||
const res = await getPlans({ page: 1, page_size: 100 })
|
||
this.plans = res.data.items || []
|
||
} catch (e) { /* ignore */ }
|
||
},
|
||
async fetchSaddle() {
|
||
try {
|
||
const res = await getSaddle()
|
||
this.saddle = res.data || null
|
||
} catch (e) { /* ignore */ }
|
||
},
|
||
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
|
||
statusLabel(s) { return STATUS_LABEL[s] || s || '—' },
|
||
badgeOf(s) { return STATUS_BADGE[s] || 'badge-gray' },
|
||
progPct(p) { return Math.max(0, Math.min(100, (p.run_length_m || 0) / TARGET * 100)) },
|
||
progColor(p) { return this.progPct(p) >= 100 ? 'var(--accent-green)' : 'var(--sms-teal)' },
|
||
occupantOf(pos) {
|
||
return this.plans.find(p => p.position === pos && p.on_saddle !== 1) || null
|
||
},
|
||
occ(pos, key) { const p = this.occupantOf(pos); return p ? (p[key] || '—') : '' },
|
||
occNum(pos, key, n) { const p = this.occupantOf(pos); return p && p[key] != null ? Number(p[key]).toFixed(n) : '' },
|
||
openMove(plan) { this.moveDialog = { visible: true, plan, target: plan.position || '' } },
|
||
async confirmMove() {
|
||
const { plan, target } = this.moveDialog
|
||
if (!target) return
|
||
this.moving = true
|
||
try {
|
||
await movePlan(plan.id, target)
|
||
this.$message.success(target === SADDLE ? '已移动到上卷鞍座' : `已移动到 ${target}`)
|
||
this.moveDialog.visible = false
|
||
this.refreshAll()
|
||
} catch (e) {
|
||
this.$message.error(e?.response?.data?.detail || '操作失败')
|
||
} finally { this.moving = false }
|
||
},
|
||
async commit(p) {
|
||
try {
|
||
await commitProducing(p.id)
|
||
this.$message.success('已投入生产,转入物料跟踪')
|
||
this.refreshAll()
|
||
} catch (e) {
|
||
this.$message.error(e?.response?.data?.detail || '操作失败')
|
||
}
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/assets/styles/variables';
|
||
|
||
.entry-page { display: flex; flex-direction: column; gap: 12px; }
|
||
|
||
// ── 入口设备网格 ──
|
||
.entry-grid { padding: 8px; display: flex; flex-direction: column; gap: 6px; overflow-x: auto; }
|
||
.entry-row { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 6px; }
|
||
|
||
.pos-cell {
|
||
background: $bg-panel; border: 1px solid $border; border-radius: 4px; padding: 4px 8px 4px;
|
||
height: 192px; overflow: hidden; display: flex; flex-direction: column;
|
||
&.filled { border-color: $sms-teal; background: rgba($sms-teal, .04); }
|
||
}
|
||
.pos-title {
|
||
text-align: center; font-size: 11px; font-weight: 700; color: $text-primary;
|
||
padding: 1px 0 3px; border-bottom: 1px dashed $border; margin-bottom: 3px; letter-spacing: .3px;
|
||
}
|
||
.pos-table {
|
||
width: 100%; border-collapse: collapse; font-size: 10.5px; line-height: 1.3;
|
||
td { padding: 0 0 1px; vertical-align: baseline; white-space: nowrap; }
|
||
td.k { color: $text-muted; text-align: left; font-size: 10px; padding-right: 6px; }
|
||
td.v { color: $sms-teal; text-align: right; font-family: $font-mono; font-weight: 600; width: 100%; }
|
||
}
|
||
.pos-act { margin-top: auto; text-align: right; padding-top: 3px; border-top: 1px dashed $border; flex-shrink: 0; }
|
||
.pos-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: $text-muted; font-size: 11px; }
|
||
|
||
// ── 下方单个上卷鞍座(全宽) ──
|
||
.saddle-cell {
|
||
height: 132px;
|
||
&.filled { border-color: $accent-yellow; background: rgba($accent-yellow, .05); }
|
||
}
|
||
.saddle-title { border-bottom-color: rgba($accent-yellow, .4); display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
.st-tag { font-size: 9px; color: $accent-yellow; border: 1px solid rgba($accent-yellow, .5); border-radius: 2px; padding: 0 5px; font-weight: 600; } }
|
||
.saddle-body { flex: 1; display: grid; grid-template-columns: 3fr 2fr; gap: 16px; align-items: center; }
|
||
.sb-info { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 18px;
|
||
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-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; } }
|
||
|
||
// ── 移动弹窗 ──
|
||
.pos-pick { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||
.pick-item {
|
||
padding: 9px 12px; font-size: 12px; text-align: center; border-radius: 6px; cursor: pointer;
|
||
border: 1px solid $border; color: $text-secondary; background: $bg-card;
|
||
&:hover { border-color: $sms-teal; color: $sms-teal; }
|
||
&.active { color: #fff; background: $sms-teal; border-color: $sms-teal; }
|
||
&.saddle { grid-column: 1 / -1; border-style: dashed; border-color: $accent-yellow; color: $accent-yellow;
|
||
&.active { color: #fff; background: $accent-yellow; border-style: solid; } }
|
||
}
|
||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
|
||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
|
||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
|
||
.modal-body { padding: 16px; overflow-y: auto; }
|
||
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
|
||
</style>
|