feat(linkage): 鞍座自动投入生产 + 实绩生产阶段数据 + 去掉口语化提示
- 产线空闲时自动把上卷鞍座预备卷投入生产;产线在产则等上一卷完成生成实绩后自动进入 - 引擎循环间隔降到5s,自动推进更及时 - 实绩页:ProductionRecordOut 增加生产阶段字段;点击行在下方展示生产阶段数据 - 移除入口跟踪/物料跟踪/成本页的口语化提示小字 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user