fix(quality): 左侧列表加高 + 复选/单选主题样式 + 缺陷图片改附件上传预览 + 钢卷信息按L2精简

- 钢卷列表高度撑满;选中态/悬浮改主题红
- 复选框/单选框 accent-color 主题红、统一尺寸
- 缺陷图片改为附件上传(客户端压缩为base64)+ 缩略图 + 点击大图预览,去掉URL输入
- qc_defect.image_url 改 TEXT 以存图片
- 钢卷信息只保留 L2 实际有的字段(卷号/订单号/钢种/规格/厚宽/内径/毛净重/状态/备注)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 16:20:23 +08:00
parent 51a9715b2a
commit 1073379b09
3 changed files with 72 additions and 31 deletions

View File

@@ -70,6 +70,7 @@ async def _run_migrations(conn):
"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)",
"ALTER TABLE qc_defect ALTER COLUMN image_url TYPE TEXT",
# 状态列改为 VARCHAR 以适配新值
"ALTER TABLE production_plans ALTER COLUMN status TYPE VARCHAR(20) USING status::text",
# production_records 新字段

View File

@@ -58,7 +58,7 @@ class QcDefect(Base):
side_drive = 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(Text, nullable=True, comment="缺陷图片(URL或base64)")
# 兼容旧字段
production_line = Column(String(50), nullable=True)

View File

@@ -131,32 +131,15 @@
<div class="card-body">
<div class="kv-grid">
<div class="kv-cell"><span class="kv-label">入场卷号</span><span class="kv-value">{{ selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">当前卷</span><span class="kv-value">{{ selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">厂家原料号</span><span class="kv-value">{{ selectedCoil.order_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">逻辑库位</span><span class="kv-value">酸连轧原料库</span></div>
<div class="kv-cell"><span class="kv-label">实际库区</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">班组</span><span class="kv-value">{{ selectedCoil.shift || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">材料类型</span><span class="kv-value">原料</span></div>
<div class="kv-cell"><span class="kv-label">物料名</span><span class="kv-value">热轧卷板</span></div>
<div class="kv-cell"><span class="kv-label">规格</span><span class="kv-value">{{ specStr }}</span></div>
<div class="kv-cell"><span class="kv-label">材质</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">厂家</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">镀层质量</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">表面处理</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">质量状态</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">切边要求</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">原料材质</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">包装要求</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">实测厚度[mm]</span><span class="kv-value">{{ selectedCoil.target_thickness || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">实测宽度[mm]</span><span class="kv-value">{{ selectedCoil.target_width || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">长度[m]</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">订单</span><span class="kv-value">{{ selectedCoil.order_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">钢种</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">规格[mm]</span><span class="kv-value">{{ specStr }}</span></div>
<div class="kv-cell"><span class="kv-label">目标厚度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.target_thickness, 2) }}</span></div>
<div class="kv-cell"><span class="kv-label">目标宽度[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.target_width, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">内径[mm]</span><span class="kv-value">{{ fmtNum(selectedCoil.inner_diameter, 0) }}</span></div>
<div class="kv-cell"><span class="kv-label">毛重[t]</span><span class="kv-value">{{ weightT(selectedCoil.gross_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">净重[t]</span><span class="kv-value">{{ weightT(selectedCoil.net_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">生产开始</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">生产结束</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">调制度</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">镀层种类</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">钢卷表面处理</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">状态</span><span class="kv-value">{{ coilStatusLabel(selectedCoil.status) }}</span></div>
<div class="kv-cell"><span class="kv-label">备注</span><span class="kv-value">{{ selectedCoil.remark || '—' }}</span></div>
</div>
</div>
@@ -222,7 +205,16 @@
</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><input v-model="d.image_url" class="kv-input" style="width:100%;" placeholder="图片URL" /></td>
<td>
<div v-if="d.image_url" class="img-thumb">
<img :src="d.image_url" @click="imgPreview = d.image_url" />
<span class="img-del" @click="d.image_url = ''"></span>
</div>
<label v-else class="img-up">
<input type="file" accept="image/*" style="display:none;" @change="onPickImage(d, $event)" />
上传
</label>
</td>
<td>
<span class="action-link" @click="clearRow(d)">清空</span>
<span class="action-link" style="color:#da3633;" @click="removeRow(idx)">删除</span>
@@ -240,6 +232,11 @@
</div>
</template>
<!-- 图片预览 -->
<div v-if="imgPreview" class="img-mask" @click="imgPreview = ''">
<img :src="imgPreview" class="img-full" />
</div>
<!-- 继承来源选择弹窗 -->
<div v-if="inheritDialog.visible" class="modal-mask" @click.self="inheritDialog.visible=false">
<div class="modal-box" style="width:420px;">
@@ -380,6 +377,7 @@ export default {
saving: false,
selectedCoilNo: '',
inheritDialog: { visible: false, source: '' },
imgPreview: '',
// 任务
taskData: [], taskTotal: 0,
@@ -564,8 +562,33 @@ export default {
} finally { this.saving = false }
},
onPickImage(d, e) {
const file = e.target.files && e.target.files[0]
e.target.value = ''
if (!file) return
const reader = new FileReader()
reader.onload = () => {
const img = new Image()
img.onload = () => {
const max = 640
let { width, height } = img
if (width > max || height > max) {
const r = Math.min(max / width, max / height)
width = Math.round(width * r); height = Math.round(height * r)
}
const canvas = document.createElement('canvas')
canvas.width = width; canvas.height = height
canvas.getContext('2d').drawImage(img, 0, 0, width, height)
d.image_url = canvas.toDataURL('image/jpeg', 0.8)
}
img.src = reader.result
}
reader.readAsDataURL(file)
},
// ── 工具 ─────────────────────────────────────
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
fmtNum(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
coilStatusLabel(s) { return ({ waiting: '等待入线', on_line: '在线处理', finished: '处理完成', abnormal: '异常' })[s] || s || '—' },
taskStatusLabel(s) { return TASK_STATUS[s]?.label || s },
taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' },
weightT(kg) { return kg ? (kg / 1000).toFixed(3) : '—' },
@@ -598,7 +621,7 @@ export default {
.abn-sidebar {
width: 220px; flex-shrink: 0;
background: $bg-card; border: 1px solid $border; border-radius: 6px;
display: flex; flex-direction: column; max-height: calc(100vh - 180px);
display: flex; flex-direction: column; height: calc(100vh - 120px);
}
.sidebar-header {
padding: 10px 12px; font-size: 12px; font-weight: 600; color: $sms-highlight;
@@ -609,8 +632,8 @@ export default {
.add-btn { cursor: pointer; color: $sms-highlight; font-size: 14px; &:hover { opacity: .7; } }
.cl-list { flex: 1; overflow-y: auto; }
.cl-item { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid rgba($border, .5);
&:hover { background: rgba(255,255,255,.03); }
&.active { background: rgba(0,200,255,.08); border-left: 3px solid $sms-highlight; }
&:hover { background: rgba($sms-teal, .05); }
&.active { background: rgba($sms-teal, .08); border-left: 3px solid $sms-teal; }
}
.cl-name { font-size: 12px; color: $text-primary; margin-bottom: 3px; }
.cl-meta { display: flex; gap: 6px; }
@@ -634,10 +657,27 @@ export default {
.abn-table { table-layout: auto; font-size: 12px; }
.abn-table th, .abn-table td { padding: 6px 6px; vertical-align: middle; }
.abn-table .kv-input { font-size: 12px; padding: 3px 6px; }
.abn-table .ck { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; margin-right: 6px; cursor: pointer; }
.abn-table .rd { display: block; font-size: 11px; line-height: 1.6; cursor: pointer; }
.abn-table .ck { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; margin-right: 6px; cursor: pointer; }
.abn-table .rd { display: flex; align-items: center; gap: 4px; font-size: 11px; line-height: 1.7; cursor: pointer; }
.abn-table .all-link { display: block; font-size: 10px; color: $sms-highlight; cursor: pointer; margin-top: 2px;
&:hover { text-decoration: underline; } }
.abn-table input[type="checkbox"], .abn-table input[type="radio"] {
accent-color: $sms-teal; width: 14px; height: 14px; margin: 0; cursor: pointer; flex-shrink: 0;
}
/* 缺陷图片:上传/预览 */
.img-up {
display: inline-block; padding: 4px 10px; font-size: 11px; cursor: pointer;
border: 1px dashed $border-light; border-radius: 4px; color: $text-secondary; background: $bg-input;
&:hover { border-color: $sms-teal; color: $sms-teal; }
}
.img-thumb { position: relative; display: inline-block;
img { width: 56px; height: 42px; object-fit: cover; border: 1px solid $border; border-radius: 4px; cursor: zoom-in; display: block; }
.img-del { position: absolute; top: -7px; right: -7px; width: 16px; height: 16px; line-height: 15px; text-align: center;
background: $accent-red; color: #fff; border-radius: 50%; font-size: 10px; cursor: pointer; }
}
.img-mask { position: fixed; inset: 0; background: rgba(0,0,0,.8); display: flex; align-items: center; justify-content: center; z-index: 10000; cursor: zoom-out; }
.img-full { max-width: 88vw; max-height: 88vh; border-radius: 4px; box-shadow: 0 8px 40px rgba(0,0,0,.5); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
.form-field { display: flex; flex-direction: column; gap: 5px; }