feat: 重构质量管理和设备巡检模块
质量管理: 由平铺记录改为任务制工作流(qc_task/qc_task_item/qc_defect三表) 设备巡检: 由点位+记录改为巡检模板制(eqp_checklist/item/record/detail四表) 前端: Quality.vue 支持任务列表+检验项详情+缺陷记录双Tab 前端: Inspection.vue 支持模板管理+项目维护+巡检记录+明细查看 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,13 +52,24 @@ export const calibrateQuality = data => request.post('/prediction/calibr
|
||||
export const resetCalibration = model => request.post(`/prediction/calibration/reset/${model}`)
|
||||
|
||||
// 设备巡检
|
||||
export const getInspectionLocations = () => request.get('/inspection/locations')
|
||||
export const createInspectionLocation = data => request.post('/inspection/locations', data)
|
||||
export const getChecklists = params => request.get('/inspection/checklists', { params })
|
||||
export const createChecklist = data => request.post('/inspection/checklists', data)
|
||||
export const updateChecklist = (id, data) => request.put(`/inspection/checklists/${id}`, data)
|
||||
export const getChecklistItems = id => request.get(`/inspection/checklists/${id}/items`)
|
||||
export const createChecklistItem = (checklistId, data) => request.post('/inspection/checklist-items', data, { params: { checklist_id: checklistId } })
|
||||
export const getInspectionRecords = params => request.get('/inspection/records', { params })
|
||||
export const createInspectionRecord = data => request.post('/inspection/records', data)
|
||||
export const getInspectionRecordDetails = id => request.get(`/inspection/records/${id}/details`)
|
||||
|
||||
// 质量管理
|
||||
export const getQualityList = params => request.get('/quality/', { params })
|
||||
export const createQuality = data => request.post('/quality/', data)
|
||||
export const updateQuality = (id, data) => request.put(`/quality/${id}`, data)
|
||||
export const getQualitySummary = () => request.get('/quality/summary')
|
||||
export const getQcTasks = params => request.get('/quality/tasks', { params })
|
||||
export const createQcTask = data => request.post('/quality/tasks', data)
|
||||
export const updateQcTask = (id, data) => request.put(`/quality/tasks/${id}`, data)
|
||||
export const deleteQcTask = id => request.delete(`/quality/tasks/${id}`)
|
||||
export const getQcTaskItems = taskId => request.get(`/quality/tasks/${taskId}/items`)
|
||||
export const createQcTaskItem = data => request.post('/quality/task-items', data)
|
||||
export const updateQcTaskItem = (id, data) => request.put(`/quality/task-items/${id}`, data)
|
||||
export const getQcDefects = params => request.get('/quality/defects', { params })
|
||||
export const createQcDefect = data => request.post('/quality/defects', data)
|
||||
export const updateQcDefect = (id, data) => request.put(`/quality/defects/${id}`, data)
|
||||
export const deleteQcDefect = id => request.delete(`/quality/defects/${id}`)
|
||||
|
||||
@@ -1,168 +1,374 @@
|
||||
<template>
|
||||
<div class="insp-layout">
|
||||
<!-- ─── 左侧:巡检模板列表 ─── -->
|
||||
<div class="insp-sidebar">
|
||||
<div class="sidebar-header">
|
||||
巡检点位
|
||||
<span class="add-btn" @click="openLocDialog()">+</span>
|
||||
巡检模板
|
||||
<span class="add-btn" @click="openChecklistDialog()">+</span>
|
||||
</div>
|
||||
<div class="loc-list">
|
||||
<div class="cl-list">
|
||||
<div
|
||||
v-for="loc in locations"
|
||||
:key="loc.id"
|
||||
:class="['loc-item', { active: selectedLoc && selectedLoc.id === loc.id }]"
|
||||
@click="selectLocation(loc)"
|
||||
v-for="cl in checklists"
|
||||
:key="cl.id"
|
||||
:class="['cl-item', { active: selectedCl && selectedCl.id === cl.id }]"
|
||||
@click="selectChecklist(cl)"
|
||||
>
|
||||
<div class="loc-code">{{ loc.code }}</div>
|
||||
<div class="loc-name">{{ loc.name }}</div>
|
||||
<div class="cl-name">{{ cl.name }}</div>
|
||||
<div class="cl-meta">
|
||||
<span class="td-muted" style="font-size:10px;">{{ cl.equipment_name || cl.equipment_code || '通用' }}</span>
|
||||
<span :class="['badge', periodBadge(cl.period)]" style="font-size:9px;padding:1px 5px;">{{ periodLabel(cl.period) }}</span>
|
||||
<span :class="['badge', cl.is_active ? 'badge-green' : 'badge-gray']" style="font-size:9px;padding:1px 5px;">{{ cl.is_active ? '启用' : '停用' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!locations.length" class="loc-empty">暂无点位</div>
|
||||
<div v-if="!checklists.length" class="cl-empty">暂无模板</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 右侧主区域 ─── -->
|
||||
<div class="insp-main">
|
||||
<div v-if="!selectedLoc" class="empty-tip">请选择点位</div>
|
||||
<div v-if="!selectedCl" class="empty-tip">请选择巡检模板</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 模板详情卡 -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span>{{ selectedLoc.name }} <span class="td-muted" style="font-size:11px;">{{ selectedLoc.code }}</span></span>
|
||||
<button class="btn btn-primary" @click="openRecordDialog(null)">录入巡检</button>
|
||||
<span>{{ selectedCl.name }}</span>
|
||||
<div class="flex-row" style="gap:8px;">
|
||||
<span :class="['badge', cl_is_active_badge]">{{ selectedCl.is_active ? '启用中' : '已停用' }}</span>
|
||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openEditChecklistDialog()">编辑</button>
|
||||
<button class="btn btn-primary" style="padding:2px 10px;font-size:11px;" @click="openInspectDialog()">开始巡检</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flex-row" style="gap:24px;flex-wrap:wrap;">
|
||||
<div class="flex-row"><span class="kv-label">设备编号</span><span class="kv-value">{{ selectedCl.equipment_code || '—' }}</span></div>
|
||||
<div class="flex-row"><span class="kv-label">设备名称</span><span class="kv-value">{{ selectedCl.equipment_name || '—' }}</span></div>
|
||||
<div class="flex-row"><span class="kv-label">巡检周期</span><span class="kv-value">{{ periodLabel(selectedCl.period) }}</span></div>
|
||||
<div class="flex-row"><span class="kv-label">描述</span><span class="kv-value">{{ selectedCl.description || '—' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模板项目 -->
|
||||
<div class="card">
|
||||
<div class="card-header">关联设备</div>
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
巡检项目
|
||||
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openItemDialog()">+ 添加项目</button>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备编号</th><th>设备名称</th><th>类别</th><th>状态</th><th>操作</th>
|
||||
<th style="width:50px;">序号</th>
|
||||
<th>项目名称</th>
|
||||
<th>检验标准</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="eq in locationEquipments" :key="eq.id">
|
||||
<td class="td-num">{{ eq.code }}</td>
|
||||
<td>{{ eq.name }}</td>
|
||||
<td>{{ eq.category || '—' }}</td>
|
||||
<td><span :class="['badge', eqStatusBadge(eq.status)]">{{ eqStatusLabel(eq.status) }}</span></td>
|
||||
<td><span class="action-link" @click="openRecordDialog(eq)">录入</span></td>
|
||||
<tr v-for="item in clItems" :key="item.id">
|
||||
<td class="td-num">{{ item.sort_order }}</td>
|
||||
<td>{{ item.item_name }}</td>
|
||||
<td class="td-muted">{{ item.item_standard || '—' }}</td>
|
||||
</tr>
|
||||
<tr v-if="!locationEquipments.length">
|
||||
<td colspan="5" class="td-muted" style="text-align:center;padding:16px;">该点位暂无关联设备</td>
|
||||
<tr v-if="!clItems.length">
|
||||
<td colspan="3" class="td-muted" style="text-align:center;padding:16px;">暂无巡检项目</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 巡检记录 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
巡检历史
|
||||
<span class="ch-badge">{{ records.length }} 条</span>
|
||||
巡检记录
|
||||
<span class="ch-badge">共 {{ recordTotal }} 条</span>
|
||||
</div>
|
||||
<div class="table-scroll" v-loading="loadingRecords">
|
||||
<!-- 记录过滤 -->
|
||||
<div class="card-body" style="padding:8px 14px;border-bottom:1px solid var(--border, #30363d);">
|
||||
<div class="flex-row" style="gap:10px;flex-wrap:wrap;">
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">状态</span>
|
||||
<select v-model="recordQuery.status" class="kv-input" style="width:90px;">
|
||||
<option value="">全部</option>
|
||||
<option value="ok">正常</option>
|
||||
<option value="issue">有问题</option>
|
||||
<option value="urgent">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">日期</span>
|
||||
<input v-model="recordQuery.start_date" type="date" class="kv-input" style="width:130px;" />
|
||||
<span class="kv-label">~</span>
|
||||
<input v-model="recordQuery.end_date" type="date" class="kv-input" style="width:130px;" />
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="fetchRecords">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th><th>设备</th><th>二维码</th><th>结果</th><th>巡检人</th><th>备注</th>
|
||||
<th>巡检时间</th>
|
||||
<th>巡检人</th>
|
||||
<th>状态</th>
|
||||
<th>总体结果</th>
|
||||
<th>备注</th>
|
||||
<th>明细</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in records" :key="r.id">
|
||||
<td class="td-muted">{{ fmtTime(r.created_at) }}</td>
|
||||
<td>{{ r.equipment_name || r.equipment_code || '—' }}</td>
|
||||
<td class="td-num">{{ r.scan_code || '—' }}</td>
|
||||
<td><span :class="['badge', resultBadge(r.result)]">{{ resultLabel(r.result) }}</span></td>
|
||||
<td class="td-muted">{{ fmtTime(r.inspect_time) }}</td>
|
||||
<td>{{ r.inspector }}</td>
|
||||
<td>{{ r.notes || '—' }}</td>
|
||||
<td><span :class="['badge', recordStatusBadge(r.status)]">{{ recordStatusLabel(r.status) }}</span></td>
|
||||
<td>
|
||||
<span v-if="r.overall_result" :class="['badge', r.overall_result === 'pass' ? 'badge-green' : 'badge-red']">
|
||||
{{ r.overall_result === 'pass' ? '通过' : '不通过' }}
|
||||
</span>
|
||||
<span v-else class="td-muted">—</span>
|
||||
</td>
|
||||
<td class="td-muted">{{ r.remark || '—' }}</td>
|
||||
<td><span class="action-link" @click="viewDetails(r)">查看明细</span></td>
|
||||
</tr>
|
||||
<tr v-if="!records.length && !loadingRecords">
|
||||
<tr v-if="!records.length">
|
||||
<td colspan="6" class="td-muted" style="text-align:center;padding:16px;">暂无巡检记录</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body" style="padding:8px 14px;" v-if="recordTotal > recordQuery.page_size">
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-outline" :disabled="recordQuery.page <= 1" @click="recordQuery.page--; fetchRecords()">上一页</button>
|
||||
<span class="kv-label">第 {{ recordQuery.page }} / {{ Math.ceil(recordTotal / recordQuery.page_size) }} 页</span>
|
||||
<button class="btn btn-outline" :disabled="recordQuery.page >= Math.ceil(recordTotal / recordQuery.page_size)" @click="recordQuery.page++; fetchRecords()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="recordDialogVisible" class="modal-mask" @click.self="recordDialogVisible=false">
|
||||
<div class="modal-box" style="width:460px;">
|
||||
<!-- ─── 新增模板弹窗 ─── -->
|
||||
<div v-if="clDialogVisible" class="modal-mask" @click.self="clDialogVisible = false">
|
||||
<div class="modal-box" style="width:480px;">
|
||||
<div class="modal-header">
|
||||
录入巡检
|
||||
<span class="modal-close" @click="recordDialogVisible=false">✕</span>
|
||||
新增巡检模板
|
||||
<span class="modal-close" @click="clDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">点位</div>
|
||||
<input class="kv-input" :value="selectedLoc.name" disabled />
|
||||
<div class="kv-label">模板名称 *</div>
|
||||
<input v-model="clForm.name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">设备编号</div>
|
||||
<input v-model="recordForm.equipment_code" class="kv-input" />
|
||||
<input v-model="clForm.equipment_code" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">设备名称</div>
|
||||
<input v-model="recordForm.equipment_name" class="kv-input" />
|
||||
<input v-model="clForm.equipment_name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">二维码</div>
|
||||
<input v-model="recordForm.scan_code" class="kv-input" ref="scanInput" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">巡检结果 *</div>
|
||||
<select v-model="recordForm.result" class="kv-input">
|
||||
<option value="normal">正常</option>
|
||||
<option value="abnormal">异常</option>
|
||||
<option value="pending">待处理</option>
|
||||
<div class="kv-label">巡检周期</div>
|
||||
<select v-model="clForm.period" class="kv-input">
|
||||
<option value="daily">每日</option>
|
||||
<option value="weekly">每周</option>
|
||||
<option value="monthly">每月</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">巡检人 *</div>
|
||||
<input v-model="recordForm.inspector" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">备注</div>
|
||||
<textarea v-model="recordForm.notes" class="kv-input" rows="3"></textarea>
|
||||
<div class="kv-label">描述</div>
|
||||
<textarea v-model="clForm.description" class="kv-input" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="recordDialogVisible=false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveRecord">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
<button class="btn btn-outline" @click="clDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveChecklist">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="locDialogVisible" class="modal-mask" @click.self="locDialogVisible=false">
|
||||
<div class="modal-box" style="width:380px;">
|
||||
<!-- ─── 编辑模板弹窗 ─── -->
|
||||
<div v-if="editClDialogVisible" class="modal-mask" @click.self="editClDialogVisible = false">
|
||||
<div class="modal-box" style="width:480px;">
|
||||
<div class="modal-header">
|
||||
新增点位
|
||||
<span class="modal-close" @click="locDialogVisible=false">✕</span>
|
||||
编辑模板
|
||||
<span class="modal-close" @click="editClDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">点位编号 *</div>
|
||||
<input v-model="locForm.code" class="kv-input" />
|
||||
<div class="kv-label">模板名称 *</div>
|
||||
<input v-model="editClForm.name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">点位名称 *</div>
|
||||
<input v-model="locForm.name" class="kv-input" />
|
||||
<div class="kv-label">设备编号</div>
|
||||
<input v-model="editClForm.equipment_code" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">设备名称</div>
|
||||
<input v-model="editClForm.equipment_name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">巡检周期</div>
|
||||
<select v-model="editClForm.period" class="kv-input">
|
||||
<option value="daily">每日</option>
|
||||
<option value="weekly">每周</option>
|
||||
<option value="monthly">每月</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">状态</div>
|
||||
<select v-model="editClForm.is_active" class="kv-input">
|
||||
<option :value="true">启用</option>
|
||||
<option :value="false">停用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">描述</div>
|
||||
<input v-model="locForm.description" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">排序</div>
|
||||
<input v-model.number="locForm.sort_order" type="number" class="kv-input" />
|
||||
<textarea v-model="editClForm.description" class="kv-input" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="locDialogVisible=false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveLocation">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
<button class="btn btn-outline" @click="editClDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveEditChecklist">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 添加巡检项弹窗 ─── -->
|
||||
<div v-if="itemDialogVisible" class="modal-mask" @click.self="itemDialogVisible = false">
|
||||
<div class="modal-box" style="width:400px;">
|
||||
<div class="modal-header">
|
||||
添加巡检项目
|
||||
<span class="modal-close" @click="itemDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">项目名称 *</div>
|
||||
<input v-model="itemForm.item_name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">检验标准</div>
|
||||
<input v-model="itemForm.item_standard" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">排序</div>
|
||||
<input v-model.number="itemForm.sort_order" type="number" class="kv-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="itemDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveItem">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 开始巡检弹窗 ─── -->
|
||||
<div v-if="inspectDialogVisible" class="modal-mask" @click.self="inspectDialogVisible = false">
|
||||
<div class="modal-box" style="width:560px;">
|
||||
<div class="modal-header">
|
||||
开始巡检 — {{ selectedCl && selectedCl.name }}
|
||||
<span class="modal-close" @click="inspectDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 基本信息 -->
|
||||
<div class="grid-2" style="gap:12px;margin-bottom:16px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">巡检人 *</div>
|
||||
<input v-model="inspectForm.inspector" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">巡检时间 *</div>
|
||||
<input v-model="inspectForm.inspect_time" type="datetime-local" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">状态</div>
|
||||
<select v-model="inspectForm.status" class="kv-input">
|
||||
<option value="ok">正常</option>
|
||||
<option value="issue">有问题</option>
|
||||
<option value="urgent">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">总体结果</div>
|
||||
<select v-model="inspectForm.overall_result" class="kv-input">
|
||||
<option value="">待定</option>
|
||||
<option value="pass">通过</option>
|
||||
<option value="fail">不通过</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field" style="grid-column:1/-1;">
|
||||
<div class="kv-label">备注</div>
|
||||
<textarea v-model="inspectForm.remark" class="kv-input" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 巡检项填写 -->
|
||||
<div class="sec-title" style="font-size:12px;color:#8b949e;margin-bottom:8px;">巡检项目记录</div>
|
||||
<div v-if="!clItems.length" class="td-muted" style="font-size:12px;padding:8px 0;">该模板暂无巡检项目</div>
|
||||
<div v-for="(item, idx) in inspectDetails" :key="idx" class="inspect-item-row">
|
||||
<div class="inspect-item-name">
|
||||
<span class="td-muted" style="font-size:10px;">{{ item.sort_order }}</span>
|
||||
{{ item.item_name }}
|
||||
<span v-if="item.item_standard" class="td-muted" style="font-size:10px;">({{ item.item_standard }})</span>
|
||||
</div>
|
||||
<div class="inspect-item-inputs">
|
||||
<input v-model="item.actual_value" class="kv-input" style="width:120px;" placeholder="实测值" />
|
||||
<label class="ok-label">
|
||||
<input type="checkbox" v-model="item.is_ok" />
|
||||
<span :style="{ color: item.is_ok ? '#28a745' : '#da3633' }">{{ item.is_ok ? '正常' : '异常' }}</span>
|
||||
</label>
|
||||
<input v-model="item.notes" class="kv-input" style="width:120px;" placeholder="备注" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="inspectDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveInspect">{{ saving ? '提交中...' : '提交巡检' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 巡检明细弹窗 ─── -->
|
||||
<div v-if="detailDialogVisible" class="modal-mask" @click.self="detailDialogVisible = false">
|
||||
<div class="modal-box" style="width:560px;">
|
||||
<div class="modal-header">
|
||||
巡检明细
|
||||
<span class="modal-close" @click="detailDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目名称</th>
|
||||
<th>实测值</th>
|
||||
<th>是否正常</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="d in recordDetails" :key="d.id">
|
||||
<td>{{ d.item_name }}</td>
|
||||
<td class="td-num">{{ d.actual_value || '—' }}</td>
|
||||
<td>
|
||||
<span :class="['badge', d.is_ok ? 'badge-green' : 'badge-red']">{{ d.is_ok ? '正常' : '异常' }}</span>
|
||||
</td>
|
||||
<td class="td-muted">{{ d.notes || '—' }}</td>
|
||||
</tr>
|
||||
<tr v-if="!recordDetails.length">
|
||||
<td colspan="4" class="td-muted" style="text-align:center;padding:16px;">暂无明细</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="detailDialogVisible = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,118 +377,204 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
getInspectionLocations, createInspectionLocation,
|
||||
getInspectionRecords, createInspectionRecord,
|
||||
getEquipments,
|
||||
getChecklists, createChecklist, updateChecklist,
|
||||
getChecklistItems, createChecklistItem,
|
||||
getInspectionRecords, createInspectionRecord, getInspectionRecordDetails,
|
||||
} from '@/api'
|
||||
|
||||
const EQ_STATUS = {
|
||||
normal: { label: '正常', badge: 'badge-green' },
|
||||
fault: { label: '故障', badge: 'badge-red' },
|
||||
maintenance: { label: '检修', badge: 'badge-yellow' },
|
||||
standby: { label: '备用', badge: 'badge-gray' },
|
||||
const PERIOD_MAP = {
|
||||
daily: { label: '每日', badge: 'badge-blue' },
|
||||
weekly: { label: '每周', badge: 'badge-yellow' },
|
||||
monthly: { label: '每月', badge: 'badge' },
|
||||
}
|
||||
|
||||
const RESULT_MAP = {
|
||||
normal: { label: '正常', badge: 'badge-green' },
|
||||
abnormal: { label: '异常', badge: 'badge-red' },
|
||||
pending: { label: '待处理', badge: 'badge-yellow' },
|
||||
const RECORD_STATUS = {
|
||||
ok: { label: '正常', badge: 'badge-green' },
|
||||
issue: { label: '有问题', badge: 'badge-yellow' },
|
||||
urgent: { label: '紧急', badge: 'badge-red' },
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Inspection',
|
||||
data() {
|
||||
return {
|
||||
locations: [],
|
||||
selectedLoc: null,
|
||||
allEquipments: [],
|
||||
records: [],
|
||||
loadingRecords: false,
|
||||
checklists: [],
|
||||
selectedCl: null,
|
||||
clItems: [],
|
||||
records: [], recordTotal: 0,
|
||||
recordQuery: { page: 1, page_size: 20, status: '', start_date: '', end_date: '' },
|
||||
|
||||
saving: false,
|
||||
recordDialogVisible: false,
|
||||
locDialogVisible: false,
|
||||
recordForm: { result: 'normal', inspector: '', equipment_code: '', equipment_name: '', scan_code: '', notes: '' },
|
||||
locForm: { code: '', name: '', description: '', sort_order: 0 },
|
||||
|
||||
// 新增模板
|
||||
clDialogVisible: false,
|
||||
clForm: { name: '', equipment_code: '', equipment_name: '', period: 'daily', description: '' },
|
||||
|
||||
// 编辑模板
|
||||
editClDialogVisible: false,
|
||||
editClForm: {},
|
||||
|
||||
// 添加巡检项
|
||||
itemDialogVisible: false,
|
||||
itemForm: { item_name: '', item_standard: '', sort_order: 0 },
|
||||
|
||||
// 开始巡检
|
||||
inspectDialogVisible: false,
|
||||
inspectForm: { inspector: '', inspect_time: '', status: 'ok', overall_result: '', remark: '' },
|
||||
inspectDetails: [],
|
||||
|
||||
// 明细
|
||||
detailDialogVisible: false,
|
||||
recordDetails: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
locationEquipments() {
|
||||
if (!this.selectedLoc) return []
|
||||
return this.allEquipments.filter(e => e.location === this.selectedLoc.name)
|
||||
cl_is_active_badge() {
|
||||
return this.selectedCl?.is_active ? 'badge-green' : 'badge-gray'
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchLocations()
|
||||
this.fetchAllEquipments()
|
||||
this.fetchChecklists()
|
||||
},
|
||||
methods: {
|
||||
async fetchLocations() {
|
||||
const res = await getInspectionLocations()
|
||||
this.locations = res.data || []
|
||||
async fetchChecklists() {
|
||||
try {
|
||||
const res = await getChecklists()
|
||||
this.checklists = res.data || []
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
async fetchAllEquipments() {
|
||||
const res = await getEquipments({ page: 1, page_size: 200 })
|
||||
this.allEquipments = res.data?.items || []
|
||||
async selectChecklist(cl) {
|
||||
this.selectedCl = cl
|
||||
await Promise.all([this.fetchClItems(), this.fetchRecords()])
|
||||
},
|
||||
async selectLocation(loc) {
|
||||
this.selectedLoc = loc
|
||||
await this.fetchRecords()
|
||||
async fetchClItems() {
|
||||
if (!this.selectedCl) return
|
||||
try {
|
||||
const res = await getChecklistItems(this.selectedCl.id)
|
||||
this.clItems = res.data || []
|
||||
} catch (e) { this.clItems = [] }
|
||||
},
|
||||
async fetchRecords() {
|
||||
if (!this.selectedLoc) return
|
||||
this.loadingRecords = true
|
||||
if (!this.selectedCl) return
|
||||
const params = { checklist_id: this.selectedCl.id, page: this.recordQuery.page, page_size: this.recordQuery.page_size }
|
||||
if (this.recordQuery.status) params.status = this.recordQuery.status
|
||||
if (this.recordQuery.start_date) params.start_date = this.recordQuery.start_date
|
||||
if (this.recordQuery.end_date) params.end_date = this.recordQuery.end_date
|
||||
try {
|
||||
const res = await getInspectionRecords({ location_id: this.selectedLoc.id, page_size: 50 })
|
||||
this.records = res.data?.items || []
|
||||
} finally {
|
||||
this.loadingRecords = false
|
||||
}
|
||||
const res = await getInspectionRecords(params)
|
||||
this.records = res.data.items || []
|
||||
this.recordTotal = res.data.total || 0
|
||||
} catch (e) { this.records = []; this.recordTotal = 0 }
|
||||
},
|
||||
openRecordDialog(eq) {
|
||||
this.recordForm = {
|
||||
result: 'normal',
|
||||
inspector: '',
|
||||
equipment_code: eq ? eq.code : '',
|
||||
equipment_name: eq ? eq.name : '',
|
||||
scan_code: '',
|
||||
|
||||
// ── 模板 ──────────────────────────────────────
|
||||
openChecklistDialog() {
|
||||
this.clForm = { name: '', equipment_code: '', equipment_name: '', period: 'daily', description: '' }
|
||||
this.clDialogVisible = true
|
||||
},
|
||||
async saveChecklist() {
|
||||
if (!this.clForm.name) { this.$message.error('模板名称不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
await createChecklist(this.clForm)
|
||||
this.$message.success('创建成功')
|
||||
this.clDialogVisible = false
|
||||
await this.fetchChecklists()
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
openEditChecklistDialog() {
|
||||
this.editClForm = { ...this.selectedCl }
|
||||
this.editClDialogVisible = true
|
||||
},
|
||||
async saveEditChecklist() {
|
||||
if (!this.editClForm.name) { this.$message.error('模板名称不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
await updateChecklist(this.selectedCl.id, this.editClForm)
|
||||
this.$message.success('更新成功')
|
||||
this.editClDialogVisible = false
|
||||
await this.fetchChecklists()
|
||||
// refresh selectedCl
|
||||
const updated = this.checklists.find(c => c.id === this.selectedCl.id)
|
||||
if (updated) this.selectedCl = updated
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
|
||||
// ── 巡检项 ────────────────────────────────────
|
||||
openItemDialog() {
|
||||
this.itemForm = { item_name: '', item_standard: '', sort_order: this.clItems.length }
|
||||
this.itemDialogVisible = true
|
||||
},
|
||||
async saveItem() {
|
||||
if (!this.itemForm.item_name) { this.$message.error('项目名称不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
await createChecklistItem(this.selectedCl.id, this.itemForm)
|
||||
this.$message.success('添加成功')
|
||||
this.itemDialogVisible = false
|
||||
await this.fetchClItems()
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
|
||||
// ── 开始巡检 ──────────────────────────────────
|
||||
openInspectDialog() {
|
||||
const now = new Date()
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
const localDT = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||
this.inspectForm = { inspector: '', inspect_time: localDT, status: 'ok', overall_result: '', remark: '' }
|
||||
this.inspectDetails = this.clItems.map(item => ({
|
||||
checklist_item_id: item.id,
|
||||
item_name: item.item_name,
|
||||
item_standard: item.item_standard,
|
||||
sort_order: item.sort_order,
|
||||
actual_value: '',
|
||||
is_ok: true,
|
||||
notes: '',
|
||||
}
|
||||
this.recordDialogVisible = true
|
||||
this.$nextTick(() => { if (this.$refs.scanInput) this.$refs.scanInput.focus() })
|
||||
}))
|
||||
this.inspectDialogVisible = true
|
||||
},
|
||||
async saveRecord() {
|
||||
if (!this.recordForm.inspector) { this.$message.error('巡检人不能为空'); return }
|
||||
async saveInspect() {
|
||||
if (!this.inspectForm.inspector) { this.$message.error('巡检人不能为空'); return }
|
||||
if (!this.inspectForm.inspect_time) { this.$message.error('巡检时间不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
await createInspectionRecord({ ...this.recordForm, location_id: this.selectedLoc.id })
|
||||
this.$message.success('保存成功')
|
||||
this.recordDialogVisible = false
|
||||
const details = this.inspectDetails.map(d => ({
|
||||
checklist_item_id: d.checklist_item_id,
|
||||
item_name: d.item_name,
|
||||
actual_value: d.actual_value || null,
|
||||
is_ok: d.is_ok,
|
||||
notes: d.notes || null,
|
||||
}))
|
||||
await createInspectionRecord({
|
||||
checklist_id: this.selectedCl.id,
|
||||
inspector: this.inspectForm.inspector,
|
||||
inspect_time: this.inspectForm.inspect_time,
|
||||
status: this.inspectForm.status,
|
||||
overall_result: this.inspectForm.overall_result || null,
|
||||
remark: this.inspectForm.remark || null,
|
||||
details,
|
||||
})
|
||||
this.$message.success('巡检提交成功')
|
||||
this.inspectDialogVisible = false
|
||||
await this.fetchRecords()
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
openLocDialog() {
|
||||
this.locForm = { code: '', name: '', description: '', sort_order: 0 }
|
||||
this.locDialogVisible = true
|
||||
},
|
||||
async saveLocation() {
|
||||
if (!this.locForm.code || !this.locForm.name) { this.$message.error('编号和名称不能为空'); return }
|
||||
this.saving = true
|
||||
|
||||
// ── 查看明细 ──────────────────────────────────
|
||||
async viewDetails(record) {
|
||||
try {
|
||||
await createInspectionLocation(this.locForm)
|
||||
this.$message.success('保存成功')
|
||||
this.locDialogVisible = false
|
||||
await this.fetchLocations()
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
const res = await getInspectionRecordDetails(record.id)
|
||||
this.recordDetails = res.data || []
|
||||
this.detailDialogVisible = true
|
||||
} catch (e) { this.$message.error('获取明细失败') }
|
||||
},
|
||||
eqStatusLabel(s) { return EQ_STATUS[s]?.label || s },
|
||||
eqStatusBadge(s) { return EQ_STATUS[s]?.badge || 'badge-gray' },
|
||||
resultLabel(r) { return RESULT_MAP[r]?.label || r },
|
||||
resultBadge(r) { return RESULT_MAP[r]?.badge || 'badge-gray' },
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────
|
||||
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
||||
periodLabel(p) { return PERIOD_MAP[p]?.label || p },
|
||||
periodBadge(p) { return PERIOD_MAP[p]?.badge || 'badge-gray' },
|
||||
recordStatusLabel(s) { return RECORD_STATUS[s]?.label || s },
|
||||
recordStatusBadge(s) { return RECORD_STATUS[s]?.badge || 'badge-gray' },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -298,7 +590,7 @@ export default {
|
||||
}
|
||||
|
||||
.insp-sidebar {
|
||||
width: 220px;
|
||||
width: 230px;
|
||||
flex-shrink: 0;
|
||||
background: $bg-card;
|
||||
border: 1px solid $border;
|
||||
@@ -328,14 +620,14 @@ export default {
|
||||
&:hover { opacity: .7; }
|
||||
}
|
||||
|
||||
.loc-list {
|
||||
.cl-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $border transparent;
|
||||
}
|
||||
|
||||
.loc-item {
|
||||
.cl-item {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba($border, .5);
|
||||
@@ -344,9 +636,9 @@ export default {
|
||||
&.active { background: rgba(0,200,255,.08); border-left: 3px solid $sms-highlight; }
|
||||
}
|
||||
|
||||
.loc-code { font-size: 10px; font-family: $font-mono; color: $text-muted; }
|
||||
.loc-name { font-size: 12px; color: $text-primary; margin-top: 2px; }
|
||||
.loc-empty { padding: 20px; text-align: center; font-size: 12px; color: $text-muted; }
|
||||
.cl-name { font-size: 12px; color: $text-primary; margin-bottom: 4px; }
|
||||
.cl-meta { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
|
||||
.cl-empty { padding: 20px; text-align: center; font-size: 12px; color: $text-muted; }
|
||||
|
||||
.insp-main {
|
||||
flex: 1;
|
||||
@@ -366,10 +658,40 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; &:hover { text-decoration: underline; } }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
|
||||
.inspect-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba($border, .4);
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
.inspect-item-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: $text-primary;
|
||||
}
|
||||
.inspect-item-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.ok-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sec-title { font-size: 11px; color: $text-muted; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; }
|
||||
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 8px; &:hover { text-decoration: underline; } }
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
|
||||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
|
||||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
|
||||
.modal-body { padding: 16px; overflow-y: auto; }
|
||||
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
|
||||
|
||||
@@ -1,278 +1,484 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- ─── 过滤栏 ─── -->
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:10px 14px;">
|
||||
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">卷号</span>
|
||||
<input v-model="query.coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">质量等级</span>
|
||||
<select v-model="query.overall_grade" class="kv-input" style="width:100px;">
|
||||
<option value="">全部</option>
|
||||
<option v-for="g in ['A1','A2','B1','B2','C']" :key="g" :value="g">{{ g }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">日期</span>
|
||||
<input v-model="query.start_date" type="date" class="kv-input" style="width:140px;" />
|
||||
<span class="kv-label">~</span>
|
||||
<input v-model="query.end_date" type="date" class="kv-input" style="width:140px;" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-primary" @click="fetchData">查询</button>
|
||||
<button class="btn btn-outline" @click="openDialog()">+ 新增检验</button>
|
||||
<!-- ─── 标签页 ─── -->
|
||||
<div class="tab-bar">
|
||||
<span :class="['tab-item', { active: activeTab === 'tasks' }]" @click="activeTab = 'tasks'">检验任务</span>
|
||||
<span :class="['tab-item', { active: activeTab === 'defects' }]" @click="activeTab = 'defects'; fetchDefects()">缺陷记录</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════ 检验任务 Tab ═══════════════════════════════════════ -->
|
||||
<template v-if="activeTab === 'tasks'">
|
||||
<!-- 过滤栏 -->
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:10px 14px;">
|
||||
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">任务编号</span>
|
||||
<input v-model="taskQuery.task_code" class="kv-input" style="width:130px;" @keyup.enter="fetchTasks" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">卷号</span>
|
||||
<input v-model="taskQuery.coil_no" class="kv-input" style="width:120px;" @keyup.enter="fetchTasks" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">状态</span>
|
||||
<select v-model="taskQuery.status" class="kv-input" style="width:100px;">
|
||||
<option value="">全部</option>
|
||||
<option value="0">待检验</option>
|
||||
<option value="1">检验中</option>
|
||||
<option value="2">待审核</option>
|
||||
<option value="3">完成</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">结果</span>
|
||||
<select v-model="taskQuery.result" class="kv-input" style="width:100px;">
|
||||
<option value="">全部</option>
|
||||
<option value="qualified">合格</option>
|
||||
<option value="unqualified">不合格</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">日期</span>
|
||||
<input v-model="taskQuery.start_date" type="date" class="kv-input" style="width:140px;" />
|
||||
<span class="kv-label">~</span>
|
||||
<input v-model="taskQuery.end_date" type="date" class="kv-input" style="width:140px;" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-primary" @click="fetchTasks">查询</button>
|
||||
<button class="btn btn-outline" @click="openTaskDialog()">+ 新增任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 统计卡片 ─── -->
|
||||
<div class="grid-4">
|
||||
<div class="metric-box">
|
||||
<div class="mb-label">合格率</div>
|
||||
<div class="mb-value" :style="{ color: summary.pass_rate >= 95 ? '#28a745' : '#f0a500' }">
|
||||
{{ summary.pass_rate }}
|
||||
<!-- 主体区域:任务列表 + 检验项面板 -->
|
||||
<div class="section-row">
|
||||
<!-- 任务列表 -->
|
||||
<div class="card" style="flex:3;min-width:0;">
|
||||
<div class="card-header">
|
||||
检验任务列表
|
||||
<span class="ch-badge">共 {{ taskTotal }} 条</span>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务编号</th>
|
||||
<th>卷号</th>
|
||||
<th>方案名称</th>
|
||||
<th>检验人员</th>
|
||||
<th>检验时间</th>
|
||||
<th>状态</th>
|
||||
<th>结果</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in taskData"
|
||||
:key="row.id"
|
||||
:class="{ 'row-selected': selectedTask && selectedTask.id === row.id }"
|
||||
style="cursor:pointer;"
|
||||
@click="selectTask(row)"
|
||||
>
|
||||
<td class="td-num">{{ row.task_code }}</td>
|
||||
<td class="td-num">{{ row.coil_no || '—' }}</td>
|
||||
<td>{{ row.scheme_name || '—' }}</td>
|
||||
<td class="td-muted">{{ row.inspect_user || '—' }}</td>
|
||||
<td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
|
||||
<td><span :class="['badge', taskStatusBadge(row.status)]">{{ taskStatusLabel(row.status) }}</span></td>
|
||||
<td>
|
||||
<span v-if="row.result" :class="['badge', row.result === 'qualified' ? 'badge-green' : 'badge-red']">
|
||||
{{ row.result === 'qualified' ? '合格' : '不合格' }}
|
||||
</span>
|
||||
<span v-else class="td-muted">—</span>
|
||||
</td>
|
||||
<td class="td-muted">{{ fmtTime(row.created_at) }}</td>
|
||||
<td>
|
||||
<span class="action-link" @click.stop="openTaskDialog(row)">编辑</span>
|
||||
<span class="action-link" style="color:#da3633;" @click.stop="deleteTask(row)">删除</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!taskData.length">
|
||||
<td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body" style="padding:8px 14px;" v-if="taskTotal > taskQuery.page_size">
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-outline" :disabled="taskQuery.page <= 1" @click="taskQuery.page--; fetchTasks()">上一页</button>
|
||||
<span class="kv-label">第 {{ taskQuery.page }} / {{ Math.ceil(taskTotal / taskQuery.page_size) }} 页</span>
|
||||
<button class="btn btn-outline" :disabled="taskQuery.page >= Math.ceil(taskTotal / taskQuery.page_size)" @click="taskQuery.page++; fetchTasks()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-unit">%</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="mb-label">平均PI评分</div>
|
||||
<div class="mb-value">{{ summary.avg_pi_score }}</div>
|
||||
<div class="mb-unit">/ 100</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="mb-label">平均表面评分</div>
|
||||
<div class="mb-value">{{ summary.avg_surface_score }}</div>
|
||||
<div class="mb-unit">/ 100</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="mb-label">记录总数</div>
|
||||
<div class="mb-value">{{ summary.total }}</div>
|
||||
<div class="mb-unit">条</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 主体区域 ─── -->
|
||||
<div class="section-row">
|
||||
<!-- 质量记录表 -->
|
||||
<div class="card" style="flex:2;">
|
||||
<!-- 检验项面板 -->
|
||||
<div class="card" style="flex:2;min-width:0;">
|
||||
<div class="card-header">
|
||||
<span v-if="selectedTask">
|
||||
检验项 <span class="td-muted" style="font-weight:400;font-size:11px;">{{ selectedTask.task_code }}</span>
|
||||
</span>
|
||||
<span v-else>检验项(请点击任务行)</span>
|
||||
<button v-if="selectedTask" class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openItemDialog()">+ 添加项目</button>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>检验项名称</th>
|
||||
<th>类型</th>
|
||||
<th>标准值</th>
|
||||
<th>上限</th>
|
||||
<th>下限</th>
|
||||
<th>单位</th>
|
||||
<th>实测值</th>
|
||||
<th>是否合格</th>
|
||||
<th>判定结果</th>
|
||||
<th>检验人</th>
|
||||
<th>检验时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in taskItems" :key="item.id">
|
||||
<td>{{ item.item_name }}</td>
|
||||
<td class="td-muted">{{ item.item_type || '—' }}</td>
|
||||
<td class="td-num">{{ item.standard_value != null ? item.standard_value : '—' }}</td>
|
||||
<td class="td-num">{{ item.upper_limit != null ? item.upper_limit : '—' }}</td>
|
||||
<td class="td-num">{{ item.lower_limit != null ? item.lower_limit : '—' }}</td>
|
||||
<td class="td-muted">{{ item.unit || '—' }}</td>
|
||||
<td class="td-num">{{ item.inspect_value || '—' }}</td>
|
||||
<td>
|
||||
<span v-if="item.is_qualified != null" :class="['badge', item.is_qualified ? 'badge-green' : 'badge-red']">
|
||||
{{ item.is_qualified ? '合格' : '不合格' }}
|
||||
</span>
|
||||
<span v-else class="td-muted">—</span>
|
||||
</td>
|
||||
<td class="td-muted">{{ item.judge_result || '—' }}</td>
|
||||
<td class="td-muted">{{ item.inspect_user || '—' }}</td>
|
||||
<td class="td-muted">{{ fmtTime(item.inspect_time) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!taskItems.length">
|
||||
<td colspan="11" class="td-muted" style="text-align:center;padding:20px;">
|
||||
{{ selectedTask ? '暂无检验项' : '请先选择任务' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══════════════════════════════════════ 缺陷记录 Tab ═══════════════════════════════════════ -->
|
||||
<template v-if="activeTab === 'defects'">
|
||||
<!-- 过滤栏 -->
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:10px 14px;">
|
||||
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">卷号</span>
|
||||
<input v-model="defectQuery.coil_no" class="kv-input" style="width:130px;" @keyup.enter="fetchDefects" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">缺陷类型</span>
|
||||
<input v-model="defectQuery.defect_type" class="kv-input" style="width:120px;" @keyup.enter="fetchDefects" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">严重程度</span>
|
||||
<select v-model="defectQuery.degree" class="kv-input" style="width:100px;">
|
||||
<option value="">全部</option>
|
||||
<option value="light">轻微</option>
|
||||
<option value="normal">一般</option>
|
||||
<option value="serious">严重</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<span class="kv-label">日期</span>
|
||||
<input v-model="defectQuery.start_date" type="date" class="kv-input" style="width:140px;" />
|
||||
<span class="kv-label">~</span>
|
||||
<input v-model="defectQuery.end_date" type="date" class="kv-input" style="width:140px;" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-primary" @click="fetchDefects">查询</button>
|
||||
<button class="btn btn-outline" @click="openDefectDialog()">+ 新增缺陷</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缺陷列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
质量检验记录
|
||||
<span class="ch-badge">共 {{ total }} 条</span>
|
||||
缺陷记录列表
|
||||
<span class="ch-badge">共 {{ defectTotal }} 条</span>
|
||||
</div>
|
||||
<div class="table-scroll" v-loading="loading">
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>卷号</th>
|
||||
<th>实测厚度</th>
|
||||
<th>实测宽度</th>
|
||||
<th>PI评分</th>
|
||||
<th>表面评分</th>
|
||||
<th>质量等级</th>
|
||||
<th>残酸(g/m²)</th>
|
||||
<th>粗糙度Ra</th>
|
||||
<th>是否合格</th>
|
||||
<th>检验员</th>
|
||||
<th>检验时间</th>
|
||||
<th>产线</th>
|
||||
<th>位置</th>
|
||||
<th>板面</th>
|
||||
<th>缺陷代码</th>
|
||||
<th>缺陷类型</th>
|
||||
<th>缺陷率(%)</th>
|
||||
<th>缺陷重量</th>
|
||||
<th>严重程度</th>
|
||||
<th>判定等级</th>
|
||||
<th>判定人</th>
|
||||
<th>判定时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in tableData" :key="row.id">
|
||||
<td class="td-num">{{ row.coil_no }}</td>
|
||||
<td class="td-num">{{ row.thickness_actual || '—' }}</td>
|
||||
<td class="td-num">{{ row.width_actual || '—' }}</td>
|
||||
<td class="td-num">{{ row.pi_score != null ? row.pi_score.toFixed(1) : '—' }}</td>
|
||||
<td class="td-num">{{ row.surface_score != null ? row.surface_score.toFixed(1) : '—' }}</td>
|
||||
<tr v-for="row in defectData" :key="row.id">
|
||||
<td class="td-num">{{ row.coil_no || '—' }}</td>
|
||||
<td class="td-muted">{{ row.production_line || '—' }}</td>
|
||||
<td class="td-muted">{{ row.position || '—' }}</td>
|
||||
<td class="td-muted">{{ row.plate_surface || '—' }}</td>
|
||||
<td class="td-num">{{ row.defect_code || '—' }}</td>
|
||||
<td>{{ row.defect_type || '—' }}</td>
|
||||
<td class="td-num">{{ row.defect_rate != null ? row.defect_rate : '—' }}</td>
|
||||
<td class="td-num">{{ row.defect_weight != null ? row.defect_weight : '—' }}</td>
|
||||
<td>
|
||||
<span v-if="row.overall_grade" :class="['badge', gradeBadge(row.overall_grade)]">
|
||||
{{ row.overall_grade }}
|
||||
</span>
|
||||
<span v-if="row.degree" :class="['badge', degreeBadge(row.degree)]">{{ degreeLabel(row.degree) }}</span>
|
||||
<span v-else class="td-muted">—</span>
|
||||
</td>
|
||||
<td class="td-num">{{ row.acid_residual || '—' }}</td>
|
||||
<td class="td-num">{{ row.roughness_ra || '—' }}</td>
|
||||
<td class="td-muted">{{ row.judge_level || '—' }}</td>
|
||||
<td class="td-muted">{{ row.judge_by || '—' }}</td>
|
||||
<td class="td-muted">{{ fmtTime(row.judge_time) }}</td>
|
||||
<td>
|
||||
<span :class="['badge', row.is_passed ? 'badge-green' : 'badge-red']">
|
||||
{{ row.is_passed ? '合格' : '不合格' }}
|
||||
</span>
|
||||
<span class="action-link" @click="openDefectDialog(row)">编辑</span>
|
||||
<span class="action-link" style="color:#da3633;" @click="deleteDefect(row)">删除</span>
|
||||
</td>
|
||||
<td class="td-muted">{{ row.inspector || '—' }}</td>
|
||||
<td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
|
||||
<td><span class="action-link" @click="openDialog(row)">编辑</span></td>
|
||||
</tr>
|
||||
<tr v-if="!tableData.length && !loading">
|
||||
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
||||
<tr v-if="!defectData.length">
|
||||
<td colspan="13" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body" style="padding:8px 14px;" v-if="total > query.page_size">
|
||||
<div class="card-body" style="padding:8px 14px;" v-if="defectTotal > defectQuery.page_size">
|
||||
<div class="flex-row">
|
||||
<button class="btn btn-outline" :disabled="query.page<=1" @click="query.page--;fetchData()">上一页</button>
|
||||
<span class="kv-label">第 {{ query.page }} / {{ Math.ceil(total/query.page_size) }} 页</span>
|
||||
<button class="btn btn-outline" :disabled="query.page>=Math.ceil(total/query.page_size)" @click="query.page++;fetchData()">下一页</button>
|
||||
<button class="btn btn-outline" :disabled="defectQuery.page <= 1" @click="defectQuery.page--; fetchDefects()">上一页</button>
|
||||
<span class="kv-label">第 {{ defectQuery.page }} / {{ Math.ceil(defectTotal / defectQuery.page_size) }} 页</span>
|
||||
<button class="btn btn-outline" :disabled="defectQuery.page >= Math.ceil(defectTotal / defectQuery.page_size)" @click="defectQuery.page++; fetchDefects()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:14px;min-width:0;">
|
||||
<!-- 快速预测面板 -->
|
||||
<div class="card">
|
||||
<div class="card-header">快速质量预测</div>
|
||||
<div class="card-body">
|
||||
<div class="flex-col">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">厚度 (mm)</div>
|
||||
<input v-model.number="pred.thickness" type="number" class="kv-input" step="0.1" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">平均速度 (m/min)</div>
|
||||
<input v-model.number="pred.avg_speed" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">平均HCl浓度 (g/L)</div>
|
||||
<input v-model.number="pred.acid_conc_avg" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">平均温度 (°C)</div>
|
||||
<input v-model.number="pred.acid_temp_avg" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">铁皮重量 (g/m²)</div>
|
||||
<input v-model.number="pred.scale_weight" type="number" class="kv-input" step="0.5" />
|
||||
</div>
|
||||
<button class="btn btn-primary fw" :disabled="predLoading" @click="doPred">
|
||||
{{ predLoading ? '预测中...' : '质量预测' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 预测结果 -->
|
||||
<div v-if="predResult" class="mt8">
|
||||
<div class="kv-grid" style="margin-bottom:10px;">
|
||||
<span class="kv-label">PI评分</span>
|
||||
<span class="kv-value">{{ predResult.pi_score }}</span>
|
||||
<span class="kv-label">表面评分</span>
|
||||
<span class="kv-value">{{ predResult.surface_score }}</span>
|
||||
<span class="kv-label">综合等级</span>
|
||||
<span>
|
||||
<span :class="['badge', gradeBadge(predResult.overall_grade)]" style="font-size:14px;padding:2px 12px;">
|
||||
{{ predResult.overall_grade }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="sec-title">工艺建议</div>
|
||||
<div v-for="(r, i) in predResult.recommendations" :key="i" class="rec-item">
|
||||
💡 {{ r }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 等级分布图 -->
|
||||
<div class="card">
|
||||
<div class="card-header">等级分布</div>
|
||||
<div class="card-body">
|
||||
<canvas ref="gradeChart" height="140"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 新增/编辑弹窗 ─── -->
|
||||
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
|
||||
<div class="modal-box" style="width:680px;">
|
||||
<!-- ─── 新增/编辑任务弹窗 ─── -->
|
||||
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
|
||||
<div class="modal-box" style="width:560px;">
|
||||
<div class="modal-header">
|
||||
{{ editRow ? '编辑质量记录 #' + editRow.id : '新增质量记录' }}
|
||||
<span class="modal-close" @click="dialogVisible=false">✕</span>
|
||||
{{ editTask ? '编辑任务 #' + editTask.id : '新增检验任务' }}
|
||||
<span class="modal-close" @click="taskDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-3" style="gap:12px;margin-bottom:14px;">
|
||||
<div class="grid-2" style="gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">卷号 *</div>
|
||||
<input v-model="form.coil_no" class="kv-input" />
|
||||
<div class="kv-label">任务编号 *</div>
|
||||
<input v-model="taskForm.task_code" class="kv-input" :disabled="!!editTask" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">实测厚度 (mm)</div>
|
||||
<input v-model.number="form.thickness_actual" type="number" class="kv-input" step="0.01" />
|
||||
<div class="kv-label">卷号</div>
|
||||
<input v-model="taskForm.coil_no" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">实测宽度 (mm)</div>
|
||||
<input v-model.number="form.width_actual" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">平直度 (IU)</div>
|
||||
<input v-model.number="form.flatness" type="number" class="kv-input" step="0.1" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">凸度 (μm)</div>
|
||||
<input v-model.number="form.crown" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">表面缺陷类型</div>
|
||||
<select v-model="form.surface_defect_type" class="kv-input">
|
||||
<option value="">无缺陷</option>
|
||||
<option value="划伤">划伤</option>
|
||||
<option value="压印">压印</option>
|
||||
<option value="氧化色">氧化色</option>
|
||||
<option value="过酸洗">过酸洗</option>
|
||||
<option value="欠酸洗">欠酸洗</option>
|
||||
<option value="锈迹">锈迹</option>
|
||||
<div class="kv-label">任务类型</div>
|
||||
<select v-model="taskForm.task_type" class="kv-input">
|
||||
<option value="">请选择</option>
|
||||
<option value="incoming">来料检验</option>
|
||||
<option value="process">过程检验</option>
|
||||
<option value="final">成品检验</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷长度 (m)</div>
|
||||
<input v-model.number="form.defect_length_m" type="number" class="kv-input" step="0.1" />
|
||||
<div class="kv-label">方案名称</div>
|
||||
<input v-model="taskForm.scheme_name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷位置</div>
|
||||
<input v-model="form.defect_position" class="kv-input" />
|
||||
<div class="kv-label">检验人员</div>
|
||||
<input v-model="taskForm.inspect_user" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">PI评分</div>
|
||||
<input v-model.number="form.pi_score" type="number" class="kv-input" step="0.1" min="0" max="100" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">表面评分</div>
|
||||
<input v-model.number="form.surface_score" type="number" class="kv-input" step="0.1" min="0" max="100" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">综合等级</div>
|
||||
<select v-model="form.overall_grade" class="kv-input">
|
||||
<option v-for="g in ['A1','A2','B1','B2','C']" :key="g" :value="g">{{ g }}</option>
|
||||
<div v-if="editTask" class="form-field">
|
||||
<div class="kv-label">状态</div>
|
||||
<select v-model="taskForm.status" class="kv-input">
|
||||
<option :value="0">待检验</option>
|
||||
<option :value="1">检验中</option>
|
||||
<option :value="2">待审核</option>
|
||||
<option :value="3">完成</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">残酸量 (g/m²)</div>
|
||||
<input v-model.number="form.acid_residual" type="number" class="kv-input" step="0.01" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">粗糙度 Ra (μm)</div>
|
||||
<input v-model.number="form.roughness_ra" type="number" class="kv-input" step="0.01" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">检验员</div>
|
||||
<input v-model="form.inspector" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">是否合格</div>
|
||||
<select v-model="form.is_passed" class="kv-input">
|
||||
<option :value="true">合格</option>
|
||||
<option :value="false">不合格</option>
|
||||
<div v-if="editTask" class="form-field">
|
||||
<div class="kv-label">检验结果</div>
|
||||
<select v-model="taskForm.result" class="kv-input">
|
||||
<option value="">待定</option>
|
||||
<option value="qualified">合格</option>
|
||||
<option value="unqualified">不合格</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field" style="grid-column:1/-1;">
|
||||
<div class="kv-label">备注</div>
|
||||
<textarea v-model="taskForm.remark" class="kv-input" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="save">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
<button class="btn btn-outline" @click="taskDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveTask">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 添加检验项弹窗 ─── -->
|
||||
<div v-if="itemDialogVisible" class="modal-mask" @click.self="itemDialogVisible = false">
|
||||
<div class="modal-box" style="width:500px;">
|
||||
<div class="modal-header">
|
||||
添加检验项
|
||||
<span class="modal-close" @click="itemDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-2" style="gap:12px;">
|
||||
<div class="form-field" style="grid-column:1/-1;">
|
||||
<div class="kv-label">检验项名称 *</div>
|
||||
<input v-model="itemForm.item_name" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">类型</div>
|
||||
<select v-model="itemForm.item_type" class="kv-input">
|
||||
<option value="">请选择</option>
|
||||
<option value="quantitative">定量</option>
|
||||
<option value="qualitative">定性</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">单位</div>
|
||||
<input v-model="itemForm.unit" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">标准值</div>
|
||||
<input v-model.number="itemForm.standard_value" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">上限</div>
|
||||
<input v-model.number="itemForm.upper_limit" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">下限</div>
|
||||
<input v-model.number="itemForm.lower_limit" type="number" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">实测值</div>
|
||||
<input v-model="itemForm.inspect_value" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">是否合格</div>
|
||||
<select v-model="itemForm.is_qualified" class="kv-input">
|
||||
<option :value="null">待判定</option>
|
||||
<option :value="1">合格</option>
|
||||
<option :value="0">不合格</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">检验人</div>
|
||||
<input v-model="itemForm.inspect_user" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field" style="grid-column:1/-1;">
|
||||
<div class="kv-label">判定结果说明</div>
|
||||
<input v-model="itemForm.judge_result" class="kv-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="itemDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveItem">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── 新增/编辑缺陷弹窗 ─── -->
|
||||
<div v-if="defectDialogVisible" class="modal-mask" @click.self="defectDialogVisible = false">
|
||||
<div class="modal-box" style="width:580px;">
|
||||
<div class="modal-header">
|
||||
{{ editDefect ? '编辑缺陷记录 #' + editDefect.id : '新增缺陷记录' }}
|
||||
<span class="modal-close" @click="defectDialogVisible = false">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-2" style="gap:12px;">
|
||||
<div class="form-field">
|
||||
<div class="kv-label">卷号</div>
|
||||
<input v-model="defectForm.coil_no" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">产线</div>
|
||||
<input v-model="defectForm.production_line" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">位置</div>
|
||||
<input v-model="defectForm.position" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">板面</div>
|
||||
<select v-model="defectForm.plate_surface" class="kv-input">
|
||||
<option value="">请选择</option>
|
||||
<option value="上表面">上表面</option>
|
||||
<option value="下表面">下表面</option>
|
||||
<option value="双面">双面</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷代码</div>
|
||||
<input v-model="defectForm.defect_code" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷类型</div>
|
||||
<input v-model="defectForm.defect_type" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷率(%)</div>
|
||||
<input v-model.number="defectForm.defect_rate" type="number" step="0.01" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">缺陷重量(kg)</div>
|
||||
<input v-model.number="defectForm.defect_weight" type="number" step="0.1" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">严重程度</div>
|
||||
<select v-model="defectForm.degree" class="kv-input">
|
||||
<option value="">请选择</option>
|
||||
<option value="light">轻微</option>
|
||||
<option value="normal">一般</option>
|
||||
<option value="serious">严重</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">判定等级</div>
|
||||
<input v-model="defectForm.judge_level" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">判定人</div>
|
||||
<input v-model="defectForm.judge_by" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="kv-label">判定时间</div>
|
||||
<input v-model="defectForm.judge_time" type="datetime-local" class="kv-input" />
|
||||
</div>
|
||||
<div class="form-field" style="grid-column:1/-1;">
|
||||
<div class="kv-label">备注</div>
|
||||
<textarea v-model="defectForm.remark" class="kv-input" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" @click="defectDialogVisible = false">取消</button>
|
||||
<button class="btn btn-primary" :disabled="saving" @click="saveDefect">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,127 +486,202 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getQualityList, createQuality, getQualitySummary, predictQuality } from '@/api'
|
||||
import {
|
||||
getQcTasks, createQcTask, updateQcTask, deleteQcTask,
|
||||
getQcTaskItems, createQcTaskItem,
|
||||
getQcDefects, createQcDefect, updateQcDefect, deleteQcDefect,
|
||||
} from '@/api'
|
||||
|
||||
const TASK_STATUS = {
|
||||
0: { label: '待检验', badge: 'badge-gray' },
|
||||
1: { label: '检验中', badge: 'badge-yellow' },
|
||||
2: { label: '待审核', badge: 'badge' },
|
||||
3: { label: '完成', badge: 'badge-green' },
|
||||
}
|
||||
|
||||
const DEGREE_MAP = {
|
||||
light: { label: '轻微', badge: 'badge-blue' },
|
||||
normal: { label: '一般', badge: 'badge-yellow' },
|
||||
serious: { label: '严重', badge: 'badge-red' },
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Quality',
|
||||
data() {
|
||||
return {
|
||||
loading: false, saving: false, predLoading: false,
|
||||
tableData: [], total: 0,
|
||||
summary: { pass_rate: 0, avg_pi_score: 0, avg_surface_score: 0, total: 0, grade_distribution: {} },
|
||||
query: { page: 1, page_size: 20, coil_no: '', overall_grade: '', start_date: '', end_date: '' },
|
||||
dialogVisible: false, editRow: null, form: {},
|
||||
pred: { thickness: 3.0, avg_speed: 100, acid_conc_avg: 160, acid_temp_avg: 75, scale_weight: 8.5 },
|
||||
predResult: null,
|
||||
activeTab: 'tasks',
|
||||
saving: false,
|
||||
|
||||
// 任务
|
||||
taskData: [], taskTotal: 0,
|
||||
taskQuery: { page: 1, page_size: 20, task_code: '', coil_no: '', status: '', result: '', start_date: '', end_date: '' },
|
||||
selectedTask: null,
|
||||
taskItems: [],
|
||||
taskDialogVisible: false, editTask: null, taskForm: {},
|
||||
itemDialogVisible: false, itemForm: {},
|
||||
|
||||
// 缺陷
|
||||
defectData: [], defectTotal: 0,
|
||||
defectQuery: { page: 1, page_size: 20, coil_no: '', defect_type: '', degree: '', start_date: '', end_date: '' },
|
||||
defectDialogVisible: false, editDefect: null, defectForm: {},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData()
|
||||
this.fetchSummary()
|
||||
this.fetchTasks()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
const params = { ...this.query }
|
||||
if (!params.coil_no) delete params.coil_no
|
||||
if (!params.overall_grade) delete params.overall_grade
|
||||
if (params.start_date) params.start_date = params.start_date + 'T00:00:00'
|
||||
else delete params.start_date
|
||||
if (params.end_date) params.end_date = params.end_date + 'T23:59:59'
|
||||
else delete params.end_date
|
||||
// ── 任务 ──────────────────────────────────────
|
||||
async fetchTasks() {
|
||||
const params = {}
|
||||
if (this.taskQuery.task_code) params.task_code = this.taskQuery.task_code
|
||||
if (this.taskQuery.coil_no) params.coil_no = this.taskQuery.coil_no
|
||||
if (this.taskQuery.status !== '') params.status = this.taskQuery.status
|
||||
if (this.taskQuery.result) params.result = this.taskQuery.result
|
||||
if (this.taskQuery.start_date) params.start_date = this.taskQuery.start_date
|
||||
if (this.taskQuery.end_date) params.end_date = this.taskQuery.end_date
|
||||
params.page = this.taskQuery.page
|
||||
params.page_size = this.taskQuery.page_size
|
||||
try {
|
||||
const res = await getQualityList(params)
|
||||
this.tableData = res.data.items
|
||||
this.total = res.data.total
|
||||
} finally { this.loading = false }
|
||||
},
|
||||
async fetchSummary() {
|
||||
try {
|
||||
const res = await getQualitySummary()
|
||||
this.summary = res.data
|
||||
this.$nextTick(() => this.drawChart())
|
||||
const res = await getQcTasks(params)
|
||||
this.taskData = res.data.items
|
||||
this.taskTotal = res.data.total
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
drawChart() {
|
||||
const canvas = this.$refs.gradeChart
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
const grades = ['A1','A2','B1','B2','C']
|
||||
const dist = this.summary.grade_distribution || {}
|
||||
const counts = grades.map(g => dist[g] || 0)
|
||||
const maxCount = Math.max(...counts, 1)
|
||||
const colors = ['#28a745','#00c8ff','#0078d4','#f0a500','#da3633']
|
||||
const W = canvas.offsetWidth || 300
|
||||
const H = 140
|
||||
canvas.width = W
|
||||
canvas.height = H
|
||||
ctx.clearRect(0, 0, W, H)
|
||||
const barW = Math.floor(W / grades.length) - 8
|
||||
const pad = 4
|
||||
grades.forEach((g, i) => {
|
||||
const barH = Math.floor((counts[i] / maxCount) * (H - 36))
|
||||
const x = i * (barW + 8) + pad
|
||||
const y = H - barH - 20
|
||||
ctx.fillStyle = colors[i]
|
||||
ctx.globalAlpha = 0.85
|
||||
ctx.fillRect(x, y, barW, barH)
|
||||
ctx.globalAlpha = 1
|
||||
ctx.fillStyle = '#e6edf3'
|
||||
ctx.font = '11px Consolas'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(counts[i], x + barW / 2, y - 4)
|
||||
ctx.fillStyle = '#8b949e'
|
||||
ctx.font = '11px "Microsoft YaHei"'
|
||||
ctx.fillText(g, x + barW / 2, H - 4)
|
||||
})
|
||||
async selectTask(row) {
|
||||
this.selectedTask = row
|
||||
try {
|
||||
const res = await getQcTaskItems(row.id)
|
||||
this.taskItems = res.data || []
|
||||
} catch (e) { this.taskItems = [] }
|
||||
},
|
||||
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' },
|
||||
gradeBadge(g) {
|
||||
if (g === 'A1') return 'badge-green'
|
||||
if (g === 'A2') return 'badge-blue'
|
||||
if (g === 'B1') return 'badge-blue'
|
||||
if (g === 'B2') return 'badge-yellow'
|
||||
return 'badge-red'
|
||||
openTaskDialog(row = null) {
|
||||
this.editTask = row
|
||||
this.taskForm = row ? { ...row } : { task_code: '', status: 0 }
|
||||
this.taskDialogVisible = true
|
||||
},
|
||||
openDialog(row = null) {
|
||||
this.editRow = row
|
||||
this.form = row ? { ...row } : { is_passed: true }
|
||||
this.dialogVisible = true
|
||||
},
|
||||
async save() {
|
||||
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return }
|
||||
async saveTask() {
|
||||
if (!this.taskForm.task_code) { this.$message.error('任务编号不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
if (this.editRow) await createQuality({ ...this.form }) // use PUT when available
|
||||
else await createQuality(this.form)
|
||||
if (this.editTask) {
|
||||
await updateQcTask(this.editTask.id, this.taskForm)
|
||||
} else {
|
||||
await createQcTask(this.taskForm)
|
||||
}
|
||||
this.$message.success('保存成功')
|
||||
this.dialogVisible = false
|
||||
this.fetchData()
|
||||
this.fetchSummary()
|
||||
this.taskDialogVisible = false
|
||||
this.fetchTasks()
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
async doPred() {
|
||||
this.predLoading = true
|
||||
async deleteTask(row) {
|
||||
if (!confirm(`确认删除任务 ${row.task_code}?`)) return
|
||||
try {
|
||||
const res = await predictQuality(this.pred)
|
||||
this.predResult = res.data
|
||||
} catch (e) {
|
||||
this.$message.error('预测失败:' + (e.response?.data?.detail || e.message))
|
||||
} finally { this.predLoading = false }
|
||||
await deleteQcTask(row.id)
|
||||
this.$message.success('已删除')
|
||||
if (this.selectedTask && this.selectedTask.id === row.id) {
|
||||
this.selectedTask = null
|
||||
this.taskItems = []
|
||||
}
|
||||
this.fetchTasks()
|
||||
} catch (e) { this.$message.error('删除失败') }
|
||||
},
|
||||
}
|
||||
|
||||
// ── 检验项 ────────────────────────────────────
|
||||
openItemDialog() {
|
||||
this.itemForm = { task_id: this.selectedTask.id, item_name: '', is_qualified: null }
|
||||
this.itemDialogVisible = true
|
||||
},
|
||||
async saveItem() {
|
||||
if (!this.itemForm.item_name) { this.$message.error('检验项名称不能为空'); return }
|
||||
this.saving = true
|
||||
try {
|
||||
await createQcTaskItem({ ...this.itemForm, task_id: this.selectedTask.id })
|
||||
this.$message.success('添加成功')
|
||||
this.itemDialogVisible = false
|
||||
await this.selectTask(this.selectedTask)
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
|
||||
// ── 缺陷 ──────────────────────────────────────
|
||||
async fetchDefects() {
|
||||
const params = {}
|
||||
if (this.defectQuery.coil_no) params.coil_no = this.defectQuery.coil_no
|
||||
if (this.defectQuery.defect_type) params.defect_type = this.defectQuery.defect_type
|
||||
if (this.defectQuery.degree) params.degree = this.defectQuery.degree
|
||||
if (this.defectQuery.start_date) params.start_date = this.defectQuery.start_date
|
||||
if (this.defectQuery.end_date) params.end_date = this.defectQuery.end_date
|
||||
params.page = this.defectQuery.page
|
||||
params.page_size = this.defectQuery.page_size
|
||||
try {
|
||||
const res = await getQcDefects(params)
|
||||
this.defectData = res.data.items
|
||||
this.defectTotal = res.data.total
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
openDefectDialog(row = null) {
|
||||
this.editDefect = row
|
||||
this.defectForm = row ? { ...row } : {}
|
||||
this.defectDialogVisible = true
|
||||
},
|
||||
async saveDefect() {
|
||||
this.saving = true
|
||||
try {
|
||||
if (this.editDefect) {
|
||||
await updateQcDefect(this.editDefect.id, this.defectForm)
|
||||
} else {
|
||||
await createQcDefect(this.defectForm)
|
||||
}
|
||||
this.$message.success('保存成功')
|
||||
this.defectDialogVisible = false
|
||||
this.fetchDefects()
|
||||
} finally { this.saving = false }
|
||||
},
|
||||
async deleteDefect(row) {
|
||||
if (!confirm('确认删除该缺陷记录?')) return
|
||||
try {
|
||||
await deleteQcDefect(row.id)
|
||||
this.$message.success('已删除')
|
||||
this.fetchDefects()
|
||||
} catch (e) { this.$message.error('删除失败') }
|
||||
},
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────
|
||||
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
||||
taskStatusLabel(s) { return TASK_STATUS[s]?.label || s },
|
||||
taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' },
|
||||
degreeLabel(d) { return DEGREE_MAP[d]?.label || d },
|
||||
degreeBadge(d) { return DEGREE_MAP[d]?.badge || 'badge-gray' },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/variables';
|
||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 2px solid $border;
|
||||
}
|
||||
.tab-item {
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: $text-muted;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all .15s;
|
||||
&:hover { color: $text-primary; }
|
||||
&.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
|
||||
}
|
||||
.section-row { display: flex; gap: 14px; align-items: flex-start; }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.row-selected { background: rgba(0,200,255,.08) !important; }
|
||||
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
||||
.rec-item { padding: 5px 8px; margin-top: 4px; background: rgba(0,200,255,.05); border-left: 2px solid $sms-highlight; font-size: 12px; color: $text-secondary; border-radius: 0 3px 3px 0; }
|
||||
.mt8 { margin-top: 8px; }
|
||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
|
||||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; width: 680px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
|
||||
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
|
||||
.modal-body { padding: 16px; overflow-y: auto; }
|
||||
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user