diff --git a/backend/app/database.py b/backend/app/database.py
index f5adbbf..166f905 100644
--- a/backend/app/database.py
+++ b/backend/app/database.py
@@ -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 新字段
diff --git a/backend/app/models/quality.py b/backend/app/models/quality.py
index fbd1b93..9a0f97d 100644
--- a/backend/app/models/quality.py
+++ b/backend/app/models/quality.py
@@ -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)
diff --git a/frontend/src/views/Quality.vue b/frontend/src/views/Quality.vue
index ba2fd82..bdbee22 100644
--- a/frontend/src/views/Quality.vue
+++ b/frontend/src/views/Quality.vue
@@ -131,32 +131,15 @@
入场卷号{{ selectedCoil.coil_no || '—' }}
-
当前卷号{{ selectedCoil.coil_no || '—' }}
-
厂家原料号{{ selectedCoil.order_no || '—' }}
-
逻辑库位酸连轧原料库
-
实际库区—
-
班组{{ selectedCoil.shift || '—' }}
-
材料类型原料
-
物料名热轧卷板
-
规格{{ specStr }}
-
材质{{ selectedCoil.steel_grade || '—' }}
-
厂家—
-
镀层质量—
-
表面处理—
-
质量状态—
-
切边要求—
-
原料材质{{ selectedCoil.steel_grade || '—' }}
-
包装要求—
-
实测厚度[mm]{{ selectedCoil.target_thickness || '—' }}
-
实测宽度[mm]{{ selectedCoil.target_width || '—' }}
-
长度[m]—
+
订单号{{ selectedCoil.order_no || '—' }}
+
钢种{{ selectedCoil.steel_grade || '—' }}
+
规格[mm]{{ specStr }}
+
目标厚度[mm]{{ fmtNum(selectedCoil.target_thickness, 2) }}
+
目标宽度[mm]{{ fmtNum(selectedCoil.target_width, 0) }}
+
内径[mm]{{ fmtNum(selectedCoil.inner_diameter, 0) }}
毛重[t]{{ weightT(selectedCoil.gross_weight) }}
净重[t]{{ weightT(selectedCoil.net_weight) }}
-
生产开始—
-
生产结束—
-
调制度—
-
镀层种类—
-
钢卷表面处理—
+
状态{{ coilStatusLabel(selectedCoil.status) }}
备注{{ selectedCoil.remark || '—' }}
@@ -222,7 +205,16 @@
{{ d.inherit_source || '—' }} |
|
- |
+
+
+ ![]()
+ ✕
+
+
+ |
清空
删除
@@ -240,6 +232,11 @@
+
+
+ ![]()
+
+
@@ -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; }
|