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:
@@ -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 新字段
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user