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:
2026-06-29 16:01:34 +08:00
parent 4567b87314
commit 649e667ad0
13 changed files with 139 additions and 40 deletions

View File

@@ -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)):
"""上卷鞍座当前计划(含实时速度/已生产长度),并推进联动。""" """上卷鞍座当前计划(含实时速度/已生产长度),并推进联动。"""

View File

@@ -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 新字段

View File

@@ -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="班次")

View File

@@ -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)

View File

@@ -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")
# 兼容旧字段 # 兼容旧字段

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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}")

View File

@@ -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')

View File

@@ -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) {

View File

@@ -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; }

View File

@@ -90,36 +90,22 @@
<!-- 异常管理 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">
<option value="">请选择钢卷</option>
<option v-for="c in coils" :key="c.id" :value="c.coil_no">{{ c.coil_no }} · {{ c.steel_grade || '' }}</option>
</select>
<input v-model="coilQuery.coil_no" class="kv-input" style="width:160px;" placeholder="搜索卷号" @keyup.enter="fetchCoils" />
<button class="btn btn-outline" @click="fetchCoils">刷新列表</button>
</div> </div>
<div class="sidebar-search">
<input v-model="coilQuery.coil_no" class="kv-input" placeholder="搜索卷号..." style="width:100%;" @keyup.enter="fetchCoils" />
</div>
<div class="cl-list">
<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 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>
<!-- 钢卷信息 --> <!-- 钢卷信息 -->
@@ -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,17 +215,35 @@
</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>
</div>
</div>
</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 v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false"> <div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
<div class="modal-box" style="width:560px;"> <div class="modal-box" style="width:560px;">
@@ -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()