feat: 实时数据持久化到计划/实绩 + 质量管理改版(钢卷信息+异常记录+继承)
- 在产计划持久化实时数据快照(run_data),物料跟踪每次轮询上报;生产完成写入实绩 process_data - 实绩页点击行展开生产阶段数据 + 实时数据快照(测点列表) - 质量管理默认进入异常管理,改为全宽:顶部选卷 + 钢卷信息(5列) + 异常记录表 - 异常记录新增「继承来源」列与「继承」按钮(从来源卷复制缺陷),进入即预置6行 - qc_defect 增加 inherit_source 字段 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func, desc
|
from sqlalchemy import select, func, desc
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -192,6 +192,18 @@ async def commit_producing(plan_id: int, db: AsyncSession = Depends(get_db), _ =
|
|||||||
return Response.ok(PlanOut.model_validate(plan))
|
return Response.ok(PlanOut.model_validate(plan))
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{plan_id}/runtime", response_model=Response[dict])
|
||||||
|
async def save_runtime(plan_id: int, body: dict = Body(...), db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
|
||||||
|
"""持久化在产卷的实时数据快照到对应计划。"""
|
||||||
|
result = await db.execute(select(ProductionPlan).where(ProductionPlan.id == plan_id))
|
||||||
|
plan = result.scalar_one_or_none()
|
||||||
|
if not plan:
|
||||||
|
raise HTTPException(status_code=404, detail="计划不存在")
|
||||||
|
if plan.status == "producing":
|
||||||
|
plan.run_data = body
|
||||||
|
return Response.ok({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/saddle/current", response_model=Response[Optional[PlanOut]])
|
@router.get("/saddle/current", response_model=Response[Optional[PlanOut]])
|
||||||
async def get_saddle(db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
|
async def get_saddle(db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)):
|
||||||
"""上卷鞍座当前计划(含实时速度/已生产长度),并推进联动。"""
|
"""上卷鞍座当前计划(含实时速度/已生产长度),并推进联动。"""
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ async def _run_migrations(conn):
|
|||||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_speed DOUBLE PRECISION DEFAULT 0",
|
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_speed DOUBLE PRECISION DEFAULT 0",
|
||||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_length_m DOUBLE PRECISION DEFAULT 0",
|
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_length_m DOUBLE PRECISION DEFAULT 0",
|
||||||
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS produced_at TIMESTAMP",
|
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS produced_at TIMESTAMP",
|
||||||
|
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_data JSONB",
|
||||||
|
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS process_data JSONB",
|
||||||
|
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS inherit_source VARCHAR(30)",
|
||||||
# 状态列改为 VARCHAR 以适配新值
|
# 状态列改为 VARCHAR 以适配新值
|
||||||
"ALTER TABLE production_plans ALTER COLUMN status TYPE VARCHAR(20) USING status::text",
|
"ALTER TABLE production_plans ALTER COLUMN status TYPE VARCHAR(20) USING status::text",
|
||||||
# production_records 新字段
|
# production_records 新字段
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class ProductionPlan(Base):
|
|||||||
run_speed = Column(Float, default=0, comment="当前线速度 m/min")
|
run_speed = Column(Float, default=0, comment="当前线速度 m/min")
|
||||||
run_length_m = Column(Float, default=0, comment="带头已生产长度 m")
|
run_length_m = Column(Float, default=0, comment="带头已生产长度 m")
|
||||||
produced_at = Column(DateTime, comment="生产完成时间")
|
produced_at = Column(DateTime, comment="生产完成时间")
|
||||||
|
run_data = Column(JSON, comment="生产期间实时数据快照")
|
||||||
|
|
||||||
# 兼容历史字段
|
# 兼容历史字段
|
||||||
shift = Column(String(10), comment="班次")
|
shift = Column(String(10), comment="班次")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey, func
|
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey, JSON, func
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ class ProductionRecord(Base):
|
|||||||
length_per_ton = Column(Float, comment="吨钢长度 m/t")
|
length_per_ton = Column(Float, comment="吨钢长度 m/t")
|
||||||
offline_time = Column(DateTime, comment="下线时间")
|
offline_time = Column(DateTime, comment="下线时间")
|
||||||
status = Column(String(20), default="UNWEIGH", comment="状态: UNWEIGH/PRODUCT")
|
status = Column(String(20), default="UNWEIGH", comment="状态: UNWEIGH/PRODUCT")
|
||||||
|
process_data = Column(JSON, comment="生产阶段实时数据快照")
|
||||||
|
|
||||||
# 兼容历史字段
|
# 兼容历史字段
|
||||||
shift_date = Column(DateTime)
|
shift_date = Column(DateTime)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class QcDefect(Base):
|
|||||||
side_middle = Column(Boolean, default=False, comment="中间")
|
side_middle = Column(Boolean, default=False, comment="中间")
|
||||||
side_drive = Column(Boolean, default=False, comment="驱动侧")
|
side_drive = Column(Boolean, default=False, comment="驱动侧")
|
||||||
is_main = Column(Boolean, default=False, comment="主缺陷")
|
is_main = Column(Boolean, default=False, comment="主缺陷")
|
||||||
|
inherit_source = Column(String(30), nullable=True, comment="继承来源卷号")
|
||||||
image_url = Column(String(255), nullable=True, comment="缺陷图片URL")
|
image_url = Column(String(255), nullable=True, comment="缺陷图片URL")
|
||||||
|
|
||||||
# 兼容旧字段
|
# 兼容旧字段
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class PlanOut(BaseModel):
|
|||||||
run_speed: Optional[float] = 0
|
run_speed: Optional[float] = 0
|
||||||
run_length_m: Optional[float] = 0
|
run_length_m: Optional[float] = 0
|
||||||
produced_at: Optional[datetime] = None
|
produced_at: Optional[datetime] = None
|
||||||
|
run_data: Optional[dict] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class ProductionRecordOut(BaseModel):
|
|||||||
inlet_width: Optional[float] = None
|
inlet_width: Optional[float] = None
|
||||||
quality_grade: Optional[str] = None
|
quality_grade: Optional[str] = None
|
||||||
operator: Optional[str] = None
|
operator: Optional[str] = None
|
||||||
|
process_data: Optional[dict] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class QcDefectBase(BaseModel):
|
|||||||
side_middle: Optional[bool] = False
|
side_middle: Optional[bool] = False
|
||||||
side_drive: Optional[bool] = False
|
side_drive: Optional[bool] = False
|
||||||
is_main: Optional[bool] = False
|
is_main: Optional[bool] = False
|
||||||
|
inherit_source: Optional[str] = None
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
defect_code: Optional[str] = None
|
defect_code: Optional[str] = None
|
||||||
defect_type: Optional[str] = None
|
defect_type: Optional[str] = None
|
||||||
@@ -130,6 +131,7 @@ class QcDefectUpdate(BaseModel):
|
|||||||
side_middle: Optional[bool] = None
|
side_middle: Optional[bool] = None
|
||||||
side_drive: Optional[bool] = None
|
side_drive: Optional[bool] = None
|
||||||
is_main: Optional[bool] = None
|
is_main: Optional[bool] = None
|
||||||
|
inherit_source: Optional[str] = None
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
defect_code: Optional[str] = None
|
defect_code: Optional[str] = None
|
||||||
defect_type: Optional[str] = None
|
defect_type: Optional[str] = None
|
||||||
@@ -156,6 +158,7 @@ class QcDefectOut(BaseModel):
|
|||||||
side_middle: Optional[bool] = None
|
side_middle: Optional[bool] = None
|
||||||
side_drive: Optional[bool] = None
|
side_drive: Optional[bool] = None
|
||||||
is_main: Optional[bool] = None
|
is_main: Optional[bool] = None
|
||||||
|
inherit_source: Optional[str] = None
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
production_line: Optional[str] = None
|
production_line: Optional[str] = None
|
||||||
position: Optional[str] = None
|
position: Optional[str] = None
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ async def _produce(db: AsyncSession, plan: ProductionPlan):
|
|||||||
inlet_width=plan.incoming_width,
|
inlet_width=plan.incoming_width,
|
||||||
quality_grade="A",
|
quality_grade="A",
|
||||||
operator="系统",
|
operator="系统",
|
||||||
|
process_data=plan.run_data,
|
||||||
)
|
)
|
||||||
db.add(rec)
|
db.add(rec)
|
||||||
logger.info(f"生产完成并产生实绩: {plan.cold_coil_no or plan.plan_no}")
|
logger.info(f"生产完成并产生实绩: {plan.cold_coil_no or plan.plan_no}")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const movePlan = (id, position) => request.patch(`/plan/${id}/move`, null
|
|||||||
export const getPositions = () => request.get('/plan/positions/all')
|
export const getPositions = () => request.get('/plan/positions/all')
|
||||||
export const commitProducing = id => request.patch(`/plan/${id}/commit`) // 投入生产
|
export const commitProducing = id => request.patch(`/plan/${id}/commit`) // 投入生产
|
||||||
export const getSaddle = () => request.get('/plan/saddle/current')
|
export const getSaddle = () => request.get('/plan/saddle/current')
|
||||||
|
export const saveRuntime = (id, data) => request.patch(`/plan/${id}/runtime`, data)
|
||||||
export const seedPlans = (count = 50) => request.post('/plan/seed', null, { params: { count } })
|
export const seedPlans = (count = 50) => request.post('/plan/seed', null, { params: { count } })
|
||||||
export const getLastPlanTemplate = () => request.get('/plan/last-template')
|
export const getLastPlanTemplate = () => request.get('/plan/last-template')
|
||||||
|
|
||||||
|
|||||||
@@ -337,7 +337,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPlans, startProducing } from '@/api'
|
import { getPlans, startProducing, saveRuntime } from '@/api'
|
||||||
function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
|
function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
|
||||||
function fix(v, n = 1) { return Number(v).toFixed(n) }
|
function fix(v, n = 1) { return Number(v).toFixed(n) }
|
||||||
|
|
||||||
@@ -560,6 +560,20 @@ export default {
|
|||||||
const pp = this.producingPlan
|
const pp = this.producingPlan
|
||||||
if (pp && pp.cold_coil_no) this.current.coil_no = pp.cold_coil_no
|
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
|
this.prodBase = pp ? { id: pp.id, len: pp.run_length_m || 0, at: Date.now(), speed: pp.run_speed || 0 } : null
|
||||||
|
// 持久化实时数据快照到在产计划
|
||||||
|
if (pp) this.persistRuntime(pp)
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
},
|
||||||
|
async persistRuntime(pp) {
|
||||||
|
try {
|
||||||
|
const snapshot = {
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
coil_no: pp.cold_coil_no || pp.plan_no,
|
||||||
|
line_speed: Number(this.current.speed.toFixed(1)),
|
||||||
|
weld_position: Number((this.weld.position * 100).toFixed(1)),
|
||||||
|
items: this.rtItems.map(it => ({ label: it.label, value: it.val, unit: it.unit })),
|
||||||
|
}
|
||||||
|
await saveRuntime(pp.id, snapshot)
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
},
|
},
|
||||||
async movePlan(p) {
|
async movePlan(p) {
|
||||||
|
|||||||
@@ -117,6 +117,20 @@
|
|||||||
<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">{{ 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 class="sg-row"><span class="sg-k">操作员</span><span class="sg-v">{{ selectedRow.operator || '—' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedRow.process_data && selectedRow.process_data.items && selectedRow.process_data.items.length">
|
||||||
|
<div class="card-header" style="border-top:1px solid var(--border);">
|
||||||
|
实时数据快照
|
||||||
|
<span class="ch-badge">{{ selectedRow.process_data.items.length }} 测点</span>
|
||||||
|
<span class="td-muted" style="margin-left:auto;font-size:11px;">采集 {{ fmtTime(selectedRow.process_data.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rt-grid">
|
||||||
|
<div v-for="(it, i) in selectedRow.process_data.items" :key="i" class="rt-cell">
|
||||||
|
<span class="rt-l">{{ it.label }}</span>
|
||||||
|
<span class="rt-v">{{ it.value }}<i>{{ it.unit }}</i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
@@ -354,6 +368,10 @@ export default {
|
|||||||
.sg-row { display: flex; align-items: center; gap: 10px; font-size: 12px; min-height: 22px; }
|
.sg-row { display: flex; align-items: center; gap: 10px; font-size: 12px; min-height: 22px; }
|
||||||
.sg-k { color: $text-muted; min-width: 64px; }
|
.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; } }
|
.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; } }
|
||||||
|
.rt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px 24px; padding: 10px 18px 16px; }
|
||||||
|
.rt-cell { display: flex; justify-content: space-between; gap: 10px; font-size: 11.5px; border-bottom: 1px dashed $border; padding: 2px 0; }
|
||||||
|
.rt-l { color: $text-secondary; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.rt-v { color: $sms-teal; font-family: $font-mono; font-weight: 600; white-space: nowrap; i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; margin-left: 3px; } }
|
||||||
.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; }
|
||||||
|
|||||||
@@ -90,38 +90,24 @@
|
|||||||
|
|
||||||
<!-- ═══════════════════════════════════════ 异常管理 Tab ═══════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════ 异常管理 Tab ═══════════════════════════════════════ -->
|
||||||
<template v-if="activeTab === 'abnormal'">
|
<template v-if="activeTab === 'abnormal'">
|
||||||
<div class="abn-layout">
|
<!-- 顶部:选择钢卷 -->
|
||||||
<!-- 左侧:钢卷列表 -->
|
<div class="card">
|
||||||
<div class="abn-sidebar">
|
<div class="card-body" style="padding:10px 14px;">
|
||||||
<div class="sidebar-header">
|
<div class="flex-row" style="gap:12px;flex-wrap:wrap;">
|
||||||
钢卷列表
|
<span class="kv-label">钢卷</span>
|
||||||
<span class="add-btn" title="刷新" @click="fetchCoils">⟳</span>
|
<select v-model="selectedCoilNo" class="kv-input" style="width:240px;" @change="onCoilChange">
|
||||||
</div>
|
<option value="">请选择钢卷</option>
|
||||||
<div class="sidebar-search">
|
<option v-for="c in coils" :key="c.id" :value="c.coil_no">{{ c.coil_no }} · {{ c.steel_grade || '—' }}</option>
|
||||||
<input v-model="coilQuery.coil_no" class="kv-input" placeholder="搜索卷号..." style="width:100%;" @keyup.enter="fetchCoils" />
|
</select>
|
||||||
</div>
|
<input v-model="coilQuery.coil_no" class="kv-input" style="width:160px;" placeholder="搜索卷号" @keyup.enter="fetchCoils" />
|
||||||
<div class="cl-list">
|
<button class="btn btn-outline" @click="fetchCoils">刷新列表</button>
|
||||||
<div
|
|
||||||
v-for="c in coils"
|
|
||||||
:key="c.id"
|
|
||||||
:class="['cl-item', { active: selectedCoil && selectedCoil.id === c.id }]"
|
|
||||||
@click="selectCoil(c)"
|
|
||||||
>
|
|
||||||
<div class="cl-name">{{ c.coil_no }}</div>
|
|
||||||
<div class="cl-meta">
|
|
||||||
<span class="td-muted" style="font-size:10px;">{{ c.steel_grade || '—' }}</span>
|
|
||||||
<span class="td-muted" style="font-size:10px;">{{ c.spec_thickness ? c.spec_thickness + '×' + (c.spec_width || '?') : '' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!coils.length" class="cl-empty">暂无钢卷</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:异常管理面板 -->
|
<div v-if="!selectedCoil" class="card"><div class="card-body"><div class="empty-tip">请选择钢卷</div></div></div>
|
||||||
<div class="abn-main">
|
|
||||||
<div v-if="!selectedCoil" class="empty-tip">请从左侧选择钢卷</div>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 钢卷信息 -->
|
<!-- 钢卷信息 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
@@ -167,9 +153,10 @@
|
|||||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
<span>异常记录 <span class="ch-badge">{{ defects.length }} 行</span></span>
|
<span>异常记录 <span class="ch-badge">{{ defects.length }} 行</span></span>
|
||||||
<div class="flex-row" style="gap:8px;">
|
<div class="flex-row" style="gap:8px;">
|
||||||
|
<button class="btn btn-primary" style="padding:2px 14px;font-size:11px;" :disabled="saving" @click="saveDefects">{{ saving ? '保存中...' : '保存' }}</button>
|
||||||
|
<button class="btn btn-outline" style="padding:2px 12px;font-size:11px;" @click="openInherit">继承</button>
|
||||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="addDefectRow">+ 新增行</button>
|
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="addDefectRow">+ 新增行</button>
|
||||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="loadDefects">⟳ 刷新</button>
|
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="loadDefects">⟳ 刷新</button>
|
||||||
<button class="btn btn-primary" style="padding:2px 14px;font-size:11px;" :disabled="saving" @click="saveDefects">{{ saving ? '保存中...' : '保存' }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
@@ -185,6 +172,7 @@
|
|||||||
<th style="width:180px;">断面位置</th>
|
<th style="width:180px;">断面位置</th>
|
||||||
<th style="width:160px;">缺陷代码</th>
|
<th style="width:160px;">缺陷代码</th>
|
||||||
<th style="width:110px;">程度</th>
|
<th style="width:110px;">程度</th>
|
||||||
|
<th style="width:90px;">继承来源</th>
|
||||||
<th style="width:60px;">主缺陷</th>
|
<th style="width:60px;">主缺陷</th>
|
||||||
<th style="min-width:120px;">缺陷图片</th>
|
<th style="min-width:120px;">缺陷图片</th>
|
||||||
<th style="width:60px;">操作</th>
|
<th style="width:60px;">操作</th>
|
||||||
@@ -218,6 +206,7 @@
|
|||||||
<input type="radio" :value="opt.value" v-model="d.degree" />{{ opt.label }}
|
<input type="radio" :value="opt.value" v-model="d.degree" />{{ opt.label }}
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="td-muted" style="text-align:center;">{{ d.inherit_source || '—' }}</td>
|
||||||
<td style="text-align:center;"><input type="checkbox" v-model="d.is_main" /></td>
|
<td style="text-align:center;"><input type="checkbox" v-model="d.is_main" /></td>
|
||||||
<td><input v-model="d.image_url" class="kv-input" style="width:100%;" placeholder="图片URL" /></td>
|
<td><input v-model="d.image_url" class="kv-input" style="width:100%;" placeholder="图片URL" /></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -226,16 +215,34 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!defects.length">
|
<tr v-if="!defects.length">
|
||||||
<td colspan="12" class="td-muted" style="text-align:center;padding:18px;">暂无异常,点击「+ 新增行」开始录入</td>
|
<td colspan="13" class="td-muted" style="text-align:center;padding:18px;">暂无数据</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ─── 继承来源选择弹窗 ─── -->
|
||||||
|
<div v-if="inheritDialog.visible" class="modal-mask" @click.self="inheritDialog.visible=false">
|
||||||
|
<div class="modal-box" style="width:420px;">
|
||||||
|
<div class="modal-header">继承缺陷 — 选择来源钢卷<span class="modal-close" @click="inheritDialog.visible=false">✕</span></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">来源钢卷</div>
|
||||||
|
<select v-model="inheritDialog.source" class="kv-input">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option v-for="c in coils" :key="c.id" :value="c.coil_no" v-show="c.coil_no !== selectedCoilNo">{{ c.coil_no }} · {{ c.steel_grade || '—' }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline" @click="inheritDialog.visible=false">取消</button>
|
||||||
|
<button class="btn btn-primary" @click="doInherit">确定继承</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<!-- ─── 新增/编辑任务弹窗 ─── -->
|
<!-- ─── 新增/编辑任务弹窗 ─── -->
|
||||||
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
|
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
|
||||||
@@ -344,6 +351,7 @@ function blankDefect() {
|
|||||||
defect_code: '',
|
defect_code: '',
|
||||||
degree: '',
|
degree: '',
|
||||||
is_main: false,
|
is_main: false,
|
||||||
|
inherit_source: '',
|
||||||
image_url: '',
|
image_url: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,8 +360,10 @@ export default {
|
|||||||
name: 'Quality',
|
name: 'Quality',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeTab: 'tasks',
|
activeTab: 'abnormal',
|
||||||
saving: false,
|
saving: false,
|
||||||
|
selectedCoilNo: '',
|
||||||
|
inheritDialog: { visible: false, source: '' },
|
||||||
|
|
||||||
// 任务
|
// 任务
|
||||||
taskData: [], taskTotal: 0,
|
taskData: [], taskTotal: 0,
|
||||||
@@ -377,7 +387,7 @@ export default {
|
|||||||
return '—'
|
return '—'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() { this.fetchTasks() },
|
created() { this.fetchCoils(); this.fetchTasks() },
|
||||||
methods: {
|
methods: {
|
||||||
// ── 任务 ──────────────────────────────────────
|
// ── 任务 ──────────────────────────────────────
|
||||||
async fetchTasks() {
|
async fetchTasks() {
|
||||||
@@ -435,8 +445,38 @@ export default {
|
|||||||
},
|
},
|
||||||
async selectCoil(c) {
|
async selectCoil(c) {
|
||||||
this.selectedCoil = c
|
this.selectedCoil = c
|
||||||
|
this.selectedCoilNo = c ? c.coil_no : ''
|
||||||
await this.loadDefects()
|
await this.loadDefects()
|
||||||
},
|
},
|
||||||
|
async onCoilChange() {
|
||||||
|
const c = this.coils.find(x => x.coil_no === this.selectedCoilNo)
|
||||||
|
if (c) await this.selectCoil(c)
|
||||||
|
else { this.selectedCoil = null; this.defects = [] }
|
||||||
|
},
|
||||||
|
openInherit() {
|
||||||
|
if (!this.selectedCoil) return
|
||||||
|
this.inheritDialog = { visible: true, source: '' }
|
||||||
|
},
|
||||||
|
async doInherit() {
|
||||||
|
const src = this.inheritDialog.source
|
||||||
|
if (!src) { this.$message.error('请选择继承来源钢卷'); return }
|
||||||
|
try {
|
||||||
|
const res = await getQcDefectsByCoil(src)
|
||||||
|
const rows = (res.data || []).map(d => ({
|
||||||
|
defect_desc: d.defect_desc || '',
|
||||||
|
start_position: d.start_position || 0,
|
||||||
|
end_position: d.end_position || 0,
|
||||||
|
upper_surface: !!d.upper_surface, lower_surface: !!d.lower_surface,
|
||||||
|
side_op: !!d.side_op, side_middle: !!d.side_middle, side_drive: !!d.side_drive,
|
||||||
|
defect_code: d.defect_code || '', degree: d.degree || '',
|
||||||
|
is_main: !!d.is_main, inherit_source: src, image_url: d.image_url || '',
|
||||||
|
}))
|
||||||
|
if (!rows.length) { this.$message.warning('来源钢卷无缺陷记录'); return }
|
||||||
|
this.defects = rows
|
||||||
|
this.inheritDialog.visible = false
|
||||||
|
this.$message.success(`已继承 ${rows.length} 条缺陷`)
|
||||||
|
} catch (e) { this.$message.error('继承失败') }
|
||||||
|
},
|
||||||
async reloadCoil() {
|
async reloadCoil() {
|
||||||
if (!this.selectedCoil) return
|
if (!this.selectedCoil) return
|
||||||
try {
|
try {
|
||||||
@@ -460,10 +500,11 @@ export default {
|
|||||||
defect_code: d.defect_code || '',
|
defect_code: d.defect_code || '',
|
||||||
degree: d.degree || '',
|
degree: d.degree || '',
|
||||||
is_main: !!d.is_main,
|
is_main: !!d.is_main,
|
||||||
|
inherit_source: d.inherit_source || '',
|
||||||
image_url: d.image_url || '',
|
image_url: d.image_url || '',
|
||||||
}))
|
}))
|
||||||
if (!this.defects.length) this.addDefectRow()
|
while (this.defects.length < 6) this.defects.push(blankDefect())
|
||||||
} catch (e) { this.defects = [blankDefect()] }
|
} catch (e) { this.defects = Array.from({ length: 6 }, () => blankDefect()) }
|
||||||
},
|
},
|
||||||
addDefectRow() { this.defects.push(blankDefect()) },
|
addDefectRow() { this.defects.push(blankDefect()) },
|
||||||
removeRow(i) { this.defects.splice(i, 1) },
|
removeRow(i) { this.defects.splice(i, 1) },
|
||||||
@@ -496,8 +537,9 @@ export default {
|
|||||||
defect_code: d.defect_code || null,
|
defect_code: d.defect_code || null,
|
||||||
degree: d.degree || null,
|
degree: d.degree || null,
|
||||||
is_main: !!d.is_main,
|
is_main: !!d.is_main,
|
||||||
|
inherit_source: d.inherit_source || null,
|
||||||
image_url: d.image_url || null,
|
image_url: d.image_url || null,
|
||||||
}))
|
})).filter(d => d.defect_desc || d.defect_code || d.start_position || d.end_position)
|
||||||
await bulkSaveQcDefects({ coil_no: this.selectedCoil.coil_no, defects: list })
|
await bulkSaveQcDefects({ coil_no: this.selectedCoil.coil_no, defects: list })
|
||||||
this.$message.success('保存成功')
|
this.$message.success('保存成功')
|
||||||
await this.loadDefects()
|
await this.loadDefects()
|
||||||
|
|||||||
Reference in New Issue
Block a user