feat(linkage): 鞍座自动投入生产 + 实绩生产阶段数据 + 去掉口语化提示

- 产线空闲时自动把上卷鞍座预备卷投入生产;产线在产则等上一卷完成生成实绩后自动进入
- 引擎循环间隔降到5s,自动推进更及时
- 实绩页:ProductionRecordOut 增加生产阶段字段;点击行在下方展示生产阶段数据
- 移除入口跟踪/物料跟踪/成本页的口语化提示小字

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 15:30:30 +08:00
parent 62c484411e
commit 4567b87314
6 changed files with 67 additions and 9 deletions

View File

@@ -81,6 +81,19 @@ class ProductionRecordOut(BaseModel):
offline_time: Optional[datetime] = None offline_time: Optional[datetime] = None
status: Optional[str] = None status: Optional[str] = None
remark: Optional[str] = None remark: Optional[str] = None
# 生产阶段数据
shift_date: Optional[datetime] = None
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
process_length: Optional[float] = None
process_weight: Optional[float] = None
avg_speed: Optional[float] = None
max_speed: Optional[float] = None
acid_consumption: Optional[float] = None
inlet_thickness: Optional[float] = None
inlet_width: Optional[float] = None
quality_grade: Optional[str] = None
operator: Optional[str] = None
created_at: datetime created_at: datetime
class Config: class Config:

View File

@@ -212,13 +212,25 @@ async def detect_downtime(db: AsyncSession):
logger.info("自动检测到停机,已新增待补充停机记录") logger.info("自动检测到停机,已新增待补充停机记录")
async def auto_commit_saddle(db: AsyncSession):
"""产线空闲(无在产卷)且上卷鞍座有预备卷 → 自动投入生产(无需人工点击)。"""
res = await db.execute(select(ProductionPlan).where(ProductionPlan.status == "producing"))
if res.scalars().first() is not None:
return # 产线占用:鞍座预备卷等待上一卷生产完成
saddle = await _saddle_plan(db)
if saddle is None:
return
await commit_plan(db, saddle)
async def tick(db: AsyncSession): async def tick(db: AsyncSession):
"""引擎单步:推进在产卷 + 停机检测(在线为人工触发,不自动上线)""" """引擎单步:推进在产卷 → 产线空闲则自动投入鞍座预备卷 → 停机检测"""
await advance_production(db) await advance_production(db)
await auto_commit_saddle(db)
await detect_downtime(db) await detect_downtime(db)
async def run_engine_loop(interval_s: int = 15): async def run_engine_loop(interval_s: int = 5):
"""后台循环,使联动在无人查看时也能自动推进。""" """后台循环,使联动在无人查看时也能自动推进。"""
import asyncio import asyncio
from app.database import AsyncSessionLocal from app.database import AsyncSessionLocal

View File

@@ -20,7 +20,7 @@
<div class="card-header">{{ curMeta.item_name }}耗量统计</div> <div class="card-header">{{ curMeta.item_name }}耗量统计</div>
<div class="card-body"> <div class="card-body">
<v-chart v-if="records.length" class="cost-chart" :option="chartOption" autoresize /> <v-chart v-if="records.length" class="cost-chart" :option="chartOption" autoresize />
<div v-else class="empty-tip">暂无数据请调整时间范围或点击新增录入</div> <div v-else class="empty-tip">暂无数据</div>
</div> </div>
</div> </div>

View File

@@ -52,10 +52,9 @@
</div> </div>
<div class="sb-stage"> <div class="sb-stage">
<span class="badge badge-yellow">预备生产</span> <span class="badge badge-yellow">预备生产</span>
<span class="sb-hint">点击右上投入生产 进入生产中并转入物料跟踪</span>
</div> </div>
</div> </div>
<div v-else class="pos-empty">空闲 把在线计划移动到上卷鞍座预备生产</div> <div v-else class="pos-empty">空闲</div>
</div> </div>
</div> </div>
</div> </div>
@@ -103,7 +102,7 @@
<span class="modal-close" @click="moveDialog.visible=false"></span> <span class="modal-close" @click="moveDialog.visible=false"></span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="kv-label" style="margin-bottom:8px;">选择目标位置只有上卷鞍座会进入生产环节</div> <div class="kv-label" style="margin-bottom:8px;">选择目标位置</div>
<div class="pos-pick"> <div class="pos-pick">
<span <span
v-for="pos in positions" v-for="pos in positions"

View File

@@ -37,7 +37,6 @@
<div class="card-header"> <div class="card-header">
在线计划入口队列 在线计划入口队列
<span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span> <span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
<span style="margin-left:auto;font-size:11px;color:var(--text-muted);">点击移动把队列卷推到入口并开始生产</span>
</div> </div>
<div style="padding:8px 14px;"> <div style="padding:8px 14px;">
<div v-if="producingPlan" class="producing-row"> <div v-if="producingPlan" class="producing-row">

View File

@@ -56,7 +56,9 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in tableData" :key="row.id"> <tr v-for="row in tableData" :key="row.id"
:class="{ 'row-selected': selectedRow && selectedRow.id === row.id }"
style="cursor:pointer;" @click="selectRow(row)">
<td class="td-num">{{ row.sub_coil_no || row.coil_no }}</td> <td class="td-num">{{ row.sub_coil_no || row.coil_no }}</td>
<td class="td-num">{{ row.hot_coil_no || '—' }}</td> <td class="td-num">{{ row.hot_coil_no || '—' }}</td>
<td>{{ row.shift || '—' }}</td> <td>{{ row.shift || '—' }}</td>
@@ -78,7 +80,7 @@
<td class="td-num">{{ fmt(row.length_per_ton) }}</td> <td class="td-num">{{ fmt(row.length_per_ton) }}</td>
<td class="td-muted">{{ fmtTime(row.offline_time) }}</td> <td class="td-muted">{{ fmtTime(row.offline_time) }}</td>
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td> <td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td> <td @click.stop>
<span class="action-link" @click="openDialog(row)">编辑</span> <span class="action-link" @click="openDialog(row)">编辑</span>
<span class="action-link" style="color:#1d8eff" @click="viewCert(row)">质保书</span> <span class="action-link" style="color:#1d8eff" @click="viewCert(row)">质保书</span>
</td> </td>
@@ -91,6 +93,32 @@
</div> </div>
</div> </div>
<!-- 生产阶段数据 -->
<div class="card" v-if="selectedRow">
<div class="card-header">生产阶段数据 {{ selectedRow.sub_coil_no || selectedRow.coil_no }}</div>
<div class="stage-grid">
<div class="sg-row"><span class="sg-k">班期</span><span class="sg-v">{{ fmtTime(selectedRow.shift_date) }}</span></div>
<div class="sg-row"><span class="sg-k">开始时间</span><span class="sg-v">{{ fmtTime(selectedRow.start_time) }}</span></div>
<div class="sg-row"><span class="sg-k">结束时间</span><span class="sg-v">{{ fmtTime(selectedRow.end_time) }}</span></div>
<div class="sg-row"><span class="sg-k">下线时间</span><span class="sg-v">{{ fmtTime(selectedRow.offline_time) }}</span></div>
<div class="sg-row"><span class="sg-k">过程长度</span><span class="sg-v">{{ fmt(selectedRow.process_length, 0) }} <i>m</i></span></div>
<div class="sg-row"><span class="sg-k">过程重量</span><span class="sg-v">{{ fmt(selectedRow.process_weight, 3) }} <i>t</i></span></div>
<div class="sg-row"><span class="sg-k">平均速度</span><span class="sg-v">{{ fmt(selectedRow.avg_speed, 0) }} <i>m/min</i></span></div>
<div class="sg-row"><span class="sg-k">最高速度</span><span class="sg-v">{{ fmt(selectedRow.max_speed, 0) }} <i>m/min</i></span></div>
<div class="sg-row"><span class="sg-k">入口厚度</span><span class="sg-v">{{ fmt(selectedRow.inlet_thickness) }} <i>mm</i></span></div>
<div class="sg-row"><span class="sg-k">入口宽度</span><span class="sg-v">{{ fmt(selectedRow.inlet_width, 0) }} <i>mm</i></span></div>
<div class="sg-row"><span class="sg-k">酸耗</span><span class="sg-v">{{ fmt(selectedRow.acid_consumption) }} <i>kg</i></span></div>
<div class="sg-row"><span class="sg-k">质量等级</span><span class="sg-v">{{ selectedRow.quality_grade || '—' }}</span></div>
<div class="sg-row"><span class="sg-k">成品长度</span><span class="sg-v">{{ fmt(selectedRow.product_length, 0) }} <i>m</i></span></div>
<div class="sg-row"><span class="sg-k">吨钢长度</span><span class="sg-v">{{ fmt(selectedRow.length_per_ton) }} <i>m/t</i></span></div>
<div class="sg-row"><span class="sg-k">成品质量</span><span class="sg-v">{{ fmt(selectedRow.product_quality) }} <i>%</i></span></div>
<div class="sg-row"><span class="sg-k">操作员</span><span class="sg-v">{{ selectedRow.operator || '—' }}</span></div>
</div>
</div>
<!-- 新增/编辑弹窗 --> <!-- 新增/编辑弹窗 -->
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false"> <div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:820px;"> <div class="modal-box" style="width:820px;">
@@ -258,6 +286,7 @@ export default {
query: { page: 1, page_size: 50, coil_no: '', shift: '', status: '', start_date: '', end_date: '' }, query: { page: 1, page_size: 50, coil_no: '', shift: '', status: '', start_date: '', end_date: '' },
dialogVisible: false, editRow: null, form: { status: 'UNWEIGH' }, dialogVisible: false, editRow: null, form: { status: 'UNWEIGH' },
certVisible: false, certRow: {}, certVisible: false, certRow: {},
selectedRow: null,
} }
}, },
computed: { computed: {
@@ -285,6 +314,7 @@ export default {
}, },
fmt(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' }, fmt(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' }, fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
selectRow(row) { this.selectedRow = this.selectedRow && this.selectedRow.id === row.id ? null : row },
statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' }, statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' },
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' }, statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
openDialog(row = null) { openDialog(row = null) {
@@ -319,6 +349,11 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/styles/variables'; @import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } } .action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
.row-selected { background: rgba($sms-teal, .1) !important; }
.stage-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px 24px; padding: 14px 18px; }
.sg-row { display: flex; align-items: center; gap: 10px; font-size: 12px; min-height: 22px; }
.sg-k { color: $text-muted; min-width: 64px; }
.sg-v { color: $text-primary; font-family: $font-mono; font-weight: 600; i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; font-size: 11px; margin-left: 2px; } }
.form-field { display: flex; flex-direction: column; gap: 5px; } .form-field { display: flex; flex-direction: column; gap: 5px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; } .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.data-table.compact th, .data-table.compact td { padding: 4px 6px; font-size: 11.5px; } .data-table.compact th, .data-table.compact td { padding: 4px 6px; font-size: 11.5px; }