Files
bj_power/bj_power_wms/frontend/src/pages/Inspection.vue
T
SunYF 8e6e8e1d20 docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
2026-09-20 18:25:44 +08:00

690 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
defineOptions({ name: 'Inspection' })
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { getRealName } from '../utils/auth'
import { fmtTime } from '../utils/format'
import { exportXlsxFetch } from '../utils/export'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import AttachmentPanel from '../components/AttachmentPanel.vue'
import MaterialSelect from '../components/MaterialSelect.vue'
import { helpInspection } from '../help'
// ===== 多 Tab:录入在前默认打开,记录在后点击才见(参照出库管理) =====
const activeTab = ref('entry') // entry=质量检验(录入) / records=检验记录
// 菜单按业务分开(来料/过程/完工检验):从路由 meta 锁定检验类型,同表同接口按类别过滤
const route = useRoute()
const router = useRouter()
const pageType = String(route.meta.inspectionType || '')
// 三检边界(需求23/30):来料检=入库物料验收;过程检=产线在制巡检;完工检=成品 SN 终检。
const isFinal = pageType === 'finished'
const isIncoming = pageType === 'incoming'
/* ===================== Tab1 质量检验(录入,默认打开) ===================== */
// 完工检对象一定是成品 SN,无「批次」选项(临时.md L101)
const targetType = ref(isFinal ? 'SN' : 'BATCH')
const status = ref('合格')
const inspector = ref(getRealName())
const submitting = ref(false)
// 检验量化字段
const inspectQty = ref(0)
const passQty = ref(0)
const checkDesc = ref('')
const inspectionNo = ref('')
const reportNo = ref('')
// 新增字段(客户诉求 L107/L150/L494;需求22:归属默认「其他」)
const ownershipType = ref('other')
const ownershipNo = ref('')
const relatedStandard = ref('')
const manufacturer = ref('')
// 不合格品处置(客户诉求 问题记录 L137-145:退货 / 报废 / 部分入库)
const disposalType = ref('NONE')
const returnTrackingNo = ref('')
// 待检编号列表(合并单/批量,统一一次提交)
const targetList = ref([])
const scanInput = ref('')
const pickerValue = ref('')
// 输入框标题/占位随目标类型变化(而非仅一行小字提示)
const inputLabel = computed(() => (targetType.value === 'BATCH' ? '批次号' : 'SN 序列号'))
const inputPlaceholder = computed(() =>
targetType.value === 'BATCH'
? '扫入或粘贴批次号,可用 逗号/分号/空格/回车 分隔,一次可录多条'
: '扫入或粘贴 SN 序列号,可用 逗号/分号/空格/回车 分隔,一次可录多条'
)
// 库存拾取器:直接选已存在的批次号/SN,避免手输。
// 规范:库存量不确定 → 只加载最近 100 条(created_at desc),输入走远程模糊搜索(keyword 匹配 图号/批次号/SN
const pickerOptions = ref([])
const pickerLoading = ref(false)
async function loadPicker(kw = '') {
pickerLoading.value = true
try {
const mm = targetType.value === 'BATCH' ? 1 : 2
const params = { manageMode: mm, keyword: kw || '', page: 1, pageSize: 100 }
// 完工检只从成品库存(category=3)拾取 SN
if (isFinal) params.category = 3
const data = await request.get('/stock/query', { params })
pickerOptions.value = data?.list || []
} catch (e) {
pickerOptions.value = []
} finally {
pickerLoading.value = false
}
}
function pickerLabel(opt) {
const id = targetType.value === 'BATCH' ? opt.batchNo : opt.snCode
return `${opt.materialCode || ''}${id || ''}|质量:${opt.qualityStatus || '未检'}`
}
function addFromInput() {
const arr = String(scanInput.value)
.split(/[,.。;;、\s]+/)
.map((s) => s.trim())
.filter(Boolean)
for (const v of arr) {
if (!targetList.value.includes(v)) targetList.value.push(v)
}
scanInput.value = ''
}
function addFromPicker() {
if (!pickerValue.value) return
if (!targetList.value.includes(pickerValue.value)) targetList.value.push(pickerValue.value)
pickerValue.value = ''
}
function removeAt(i) {
targetList.value.splice(i, 1)
}
function clearAll() {
targetList.value = []
}
async function submitInspection() {
if (!targetList.value.length) {
ElMessage.warning('请先录入或选择待检编号')
return
}
if (!inspector.value.trim()) {
ElMessage.warning('请填写记录人')
return
}
// 判定不合格必须选择处置方式(不合格不入库、不上产线)
if (status.value === '不合格' && disposalType.value === 'NONE') {
ElMessage.warning('判定不合格时必须选择处置方式(退货 / 换修 / 让步接收 / 报废 / 部分入库)')
return
}
if (status.value === '不合格' && disposalType.value === 'RETURN' && !returnTrackingNo.value.trim()) {
ElMessage.warning('退货处置必须填写货运单号')
return
}
if (status.value === '不合格' && disposalType.value === 'PARTIAL' && targetList.value.length > 1) {
ElMessage.warning('「部分入库」一次只能处理 1 个批次,请分开提交')
return
}
// 报检数守恒校验(临时.md §六:报检数 = 合格数 + 不合格数)
const iq = Number(inspectQty.value) || 0
const pq = Number(passQty.value) || 0
if (iq > 0 && pq > iq) {
ElMessage.warning('合格数不能大于报检数(报检数 = 合格数 + 不合格数)')
return
}
if (status.value === '合格' && iq > 0 && pq !== iq) {
ElMessage.warning('判定合格时,合格数应等于报检数(否则请改判不合格并选处置方式)')
return
}
submitting.value = true
try {
const data = await request.post('/inspection/batch-flip', {
targetType: targetType.value,
targetIds: [...targetList.value],
status: status.value,
inspectQty: Number(inspectQty.value) || 0,
passQty: Number(passQty.value) || 0,
checkDesc: checkDesc.value.trim(),
reportNo: reportNo.value.trim(),
inspector: inspector.value.trim(),
inspectionNo: inspectionNo.value.trim(),
disposalType: status.value === '不合格' ? disposalType.value : 'NONE',
returnTrackingNo: returnTrackingNo.value.trim(),
ownershipType: ownershipType.value,
ownershipNo: ownershipNo.value.trim(),
relatedStandard: relatedStandard.value.trim(),
manufacturer: manufacturer.value.trim(),
})
const n = data?.flipped ?? targetList.value.length
ElMessage.success(`检验已记录:共 ${n} 条 → ${status.value}`)
// 检验合格后引导去入库(方案 C / P1-6):携检测单号跳转,入库页自动回填免手抄
const passedNo = inspectionNo.value.trim()
const goInbound = status.value === '合格'
clearAll()
inspectQty.value = 0
passQty.value = 0
checkDesc.value = ''
reportNo.value = ''
disposalType.value = 'NONE'
returnTrackingNo.value = ''
ownershipType.value = 'other'
ownershipNo.value = ''
relatedStandard.value = ''
manufacturer.value = ''
// 提交成功后主动刷新记录 Tab(用户切过去即可见最新)
await loadRecords()
if (goInbound) {
try {
await ElMessageBox.confirm(
passedNo ? `检验合格,是否携检测单号 ${passedNo} 前往入库?` : '检验合格,是否前往入库?',
'前往入库', { type: 'success', confirmButtonText: '去入库', cancelButtonText: '留在本页' })
router.push({ path: '/inbound', query: passedNo ? { inspectionNo: passedNo } : {} })
} catch { /* 用户选择留在本页 */ }
}
} finally {
submitting.value = false
}
}
/* ===================== 不合格处置(对已判定不合格的记录做退货/报废/部分入库) ===================== */
const disposeDialog = reactive({ visible: false, row: null, type: 'RETURN', remark: '', trackingNo: '' })
function openDispose(row) {
if (row.status !== '不合格') {
ElMessage.warning('仅「不合格」记录可执行处置')
return
}
Object.assign(disposeDialog, { visible: true, row, type: 'RETURN', remark: '', trackingNo: '' })
}
async function submitDispose() {
if (disposeDialog.type === 'RETURN' && !disposeDialog.trackingNo.trim()) {
ElMessage.warning('退货必须填写货运单号')
return
}
await request.post('/inspection/dispose', {
recordId: disposeDialog.row.id,
disposalType: disposeDialog.type,
disposalRemark: disposeDialog.remark.trim(),
returnTrackingNo: disposeDialog.trackingNo.trim(),
})
ElMessage.success('处置完成')
disposeDialog.visible = false
await loadRecords()
}
// 附件抽屉(bizType=inspectionbizId=检验记录ID
const attachDrawer = reactive({ visible: false, id: '', targetId: '' })
function openAttach(row) {
Object.assign(attachDrawer, { visible: true, id: String(row.id), targetId: row.targetId })
}
/* ===================== Tab2 检验记录(点击才加载) ===================== */
// 默认近3个月起始日期
function defaultStart() {
const d = new Date()
d.setMonth(d.getMonth() - 3)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
// 筛选独立条件(客户诉求 L507-510:三框模糊查询 图号/名称/型号,框内模糊、框间取交集);默认近3个月
const filters = reactive({
targetId: '', materialCode: '', materialName: '', spec: '',
inspectionType: pageType, status: '', inspector: '',
ownershipNo: '',
startDate: defaultStart(), endDate: ''
})
const loading = ref(false)
const statsList = ref([])
const statsLoading = ref(false)
// 需求13/58:统计必须先选物料再点「统计」,不做全表实时聚合
const statsMaterialCode = ref('')
const statsQueried = ref(false)
async function loadStats() {
if (!statsMaterialCode.value) {
ElMessage.warning('请先选择物料再统计')
return
}
statsLoading.value = true
try {
const data = await request.get('/inspection/stats', {
params: {
inspectionType: filters.inspectionType,
materialCode: statsMaterialCode.value,
startDate: filters.startDate,
endDate: filters.endDate
}
})
statsList.value = data?.list || []
statsQueried.value = true
} finally {
statsLoading.value = false
}
}
const list = ref([])
const total = ref(0)
const page = reactive({ current: 1, size: 20 })
function typeLabel(t) {
const m = { incoming: '来料检', process: '过程检', finished: '成品检' }
return m[t] || t || '-'
}
function targetTypeLabel(t) {
return t === 'SN' ? 'SN' : t === 'BATCH' ? '批次' : (t || '-')
}
// 不合格处置方式显示(NONE=未处置)
function disposalLabel(t) {
const m = { RETURN: '退货', SCRAP: '报废', PARTIAL: '部分入库', REWORK: '换修', CONCESSION: '让步接收' }
return m[t] || '未处置'
}
// 归属类型显示(需求22:工单编号/科技项目号/其他;旧 none/空 → 其他)
function ownershipTypeLabel(t) {
const m = { workorder: '工单编号', project: '科技项目号' }
return m[t] || '其他'
}
async function loadRecords() {
loading.value = true
try {
const data = await request.get('/inspection/query', {
params: {
targetId: filters.targetId.trim(),
materialCode: filters.materialCode.trim(),
materialName: filters.materialName.trim(),
spec: filters.spec.trim(),
ownershipNo: filters.ownershipNo.trim(),
inspectionType: filters.inspectionType,
status: filters.status,
inspector: filters.inspector.trim(),
startDate: filters.startDate,
endDate: filters.endDate,
page: page.current,
pageSize: page.size
}
})
list.value = data?.list || []
total.value = data?.total || 0
} finally {
loading.value = false
}
}
function search() {
page.current = 1
loadRecords()
}
// 全量导出按当前筛选条件(无分页),后端生成 .xlsx
async function exportCsv() {
try {
await exportXlsxFetch('/api/inspection/export', {
targetId: filters.targetId.trim(),
materialCode: filters.materialCode.trim(),
materialName: filters.materialName.trim(),
spec: filters.spec.trim(),
ownershipNo: filters.ownershipNo.trim(),
inspectionType: filters.inspectionType,
status: filters.status,
inspector: filters.inspector.trim(),
startDate: filters.startDate,
endDate: filters.endDate
}, '检验记录.xlsx', { countUrl: '/inspection/query', name: '检验记录' })
} catch (e) {
/* 错误提示由请求层统一处理 */
}
}
function onTabChange(tab) {
// 进入记录 Tab 才加载,控制首次进入即查询
if (tab === 'records') loadRecords()
}
// 仅录入时需预载库存拾取器;记录 Tab 懒加载
onMounted(() => {
loadPicker()
})
</script>
<template>
<div>
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<!-- ============ 质量检验录入默认打开 ============ -->
<el-tab-pane label="质量检验录入" name="entry">
<el-card shadow="never" style="max-width:860px;margin:0 auto">
<el-alert :closable="false" show-icon class="mb12" :type="isFinal ? 'success' : 'info'">
<template #title>
<span v-if="isIncoming">来料检验对入库物料验收按入库批次/SN),合格才能入库上架不合格隔离退货</span>
<span v-else-if="isFinal">完工检验 MES 回流的成品 SN 做终检对象固定为 SN),合格后成品方可发货不合格可返修/让步接收</span>
<span v-else>过程检验对产线在制物料/半成品巡检判定质量状态</span>
</template>
</el-alert>
<el-form label-width="96px">
<!-- 目标类型直接改变输入框标题与占位完工检对象固定为成品 SN不展示切换 -->
<el-form-item v-if="!isFinal" label="目标类型">
<el-radio-group v-model="targetType" @change="() => loadPicker()">
<el-radio-button value="BATCH">批次 (结构件)</el-radio-button>
<el-radio-button value="SN">序列号 SN (电气件)</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="检验结论">
<el-radio-group v-model="status">
<el-radio value="合格">合格</el-radio>
<el-radio value="不合格">不合格</el-radio>
</el-radio-group>
</el-form-item>
<!-- 不合格品处置判定不合格时才出现):退货/换修/让步接收/报废/部分入库 -->
<template v-if="status === '不合格'">
<el-form-item label="处置方式">
<el-radio-group v-model="disposalType">
<el-radio-button value="RETURN">退货</el-radio-button>
<el-radio-button value="REWORK">换修</el-radio-button>
<el-radio-button value="CONCESSION">让步接收</el-radio-button>
<el-radio-button value="SCRAP">报废</el-radio-button>
<el-radio-button value="PARTIAL">部分入库</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="disposalType === 'RETURN'" label="货运单号">
<el-input v-model="returnTrackingNo" placeholder="退货运单号(必填)" clearable style="width:280px" />
</el-form-item>
<el-form-item label=" ">
<el-alert type="warning" :closable="false" style="max-width:560px"
title="退货/换修/报废:库存隔离(待退/不合格),禁止领料上线,退货必填货运单号;让步接收:审批后放行,质量状态置合格。" />
</el-form-item>
</template>
<el-form-item :label="isFinal ? '审核人' : '记录人'">
<el-input v-model="inspector" :placeholder="isFinal ? '完工检审核人姓名' : '记录人姓名'" clearable style="width:260px" />
</el-form-item>
<el-form-item label="归属">
<div style="display:flex;gap:8px;width:100%;max-width:560px">
<el-select v-model="ownershipType" style="width:150px">
<el-option label="工单编号" value="workorder" />
<el-option label="科技项目号" value="project" />
<el-option label="其他" value="other" />
</el-select>
<el-input v-model="ownershipNo" maxlength="64" show-word-limit
placeholder="归属编号(手动输入,不校验格式)" clearable />
</div>
</el-form-item>
<el-form-item label="相关标准">
<el-input v-model="relatedStandard" placeholder="检验细则/试验方案号(可空)" clearable style="width:420px" />
</el-form-item>
<el-form-item v-if="!isFinal" label="生产厂家">
<el-input v-model="manufacturer" placeholder="生产厂家(可空)" clearable style="width:420px" />
</el-form-item>
<el-form-item v-if="!isFinal" label="检测单号">
<el-input v-model="inspectionNo" placeholder="检验报告单号(可空,与入库单关联)" clearable style="width:420px" />
</el-form-item>
<el-form-item v-if="!isFinal" label="报检单号">
<el-input v-model="reportNo" placeholder="生产报检单号(可空,与报检流程关联)" clearable style="width:420px" />
</el-form-item>
<el-divider content-position="left">检验量化可空</el-divider>
<el-form-item label="检验数量">
<el-input-number v-model="inspectQty" :min="0" :max="999999" placeholder="抽检件数" style="width:160px" />
</el-form-item>
<el-form-item label="合格数量">
<el-input-number v-model="passQty" :min="0" :max="999999" placeholder="合格件数(≤检验数量)" style="width:160px" />
</el-form-item>
<el-form-item label="相关标准">
<el-input v-model="checkDesc" placeholder="检验项目/方法/标准(可空)" clearable style="width:420px" />
</el-form-item>
<el-divider content-position="left">待检编号可扫/选多条统一提交</el-divider>
<!-- 从库存直接拾取避免手输批次号远程模糊搜索最近 100 -->
<el-form-item :label="`从库存选${inputLabel}`">
<el-select
v-model="pickerValue"
filterable
remote
clearable
:remote-method="loadPicker"
:loading="pickerLoading"
:placeholder="`输入批次号/SN/图号搜索(最近100条)`"
style="width:100%"
@change="addFromPicker"
>
<el-option v-for="opt in pickerOptions" :key="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
:value="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
:label="pickerLabel(opt)" />
</el-select>
</el-form-item>
<!-- 批量/扫码录入单条也走这里无需单独的"单个录入" -->
<el-form-item :label="`录入${inputLabel}`">
<div style="width:100%">
<el-input v-model="scanInput" type="textarea" :rows="4"
:placeholder="inputPlaceholder" />
<el-button type="primary" plain style="margin-top:6px" :disabled="!scanInput.trim()"
@click="addFromInput">录入到列表</el-button>
<span style="margin-left:8px;color:#909399;font-size:12px">支持扫码枪/粘贴多编号用逗号分号空格或回车分隔</span>
</div>
</el-form-item>
<!-- 已选清单 -->
<el-form-item label="已选清单">
<div style="width:100%">
<el-tag v-for="(v, i) in targetList" :key="v + i" closable type="info"
style="margin:0 8px 8px 0" @close="removeAt(i)">{{ v }}</el-tag>
<span v-if="!targetList.length" style="color:#909399;font-size:12px">尚未选择任何待检编号</span>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="submitInspection">提交检验</el-button>
<el-button :disabled="!targetList.length" @click="clearAll">清空</el-button>
</el-form-item>
</el-form>
</el-card>
</el-tab-pane>
<!-- ============ 检验记录点击才加载 ============ -->
<el-tab-pane label="检验记录" name="records">
<el-card shadow="never" class="mb12">
<el-form inline @submit.prevent="search">
<el-form-item label="批次/SN">
<el-input v-model="filters.targetId" placeholder="批次号或 SN" clearable style="width:150px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="图号">
<el-input v-model="filters.materialCode" placeholder="图号(模糊)" clearable style="width:140px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="filters.materialName" placeholder="物料名称(模糊)" clearable style="width:140px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="型号">
<el-input v-model="filters.spec" placeholder="规格型号(模糊)" clearable style="width:140px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="归属单号">
<el-input v-model="filters.ownershipNo" placeholder="工单号/科技项目号" clearable style="width:150px"
@keyup.enter="search" />
</el-form-item>
<el-form-item v-if="!pageType" label="检验类型">
<el-select v-model="filters.inspectionType" placeholder="全部" clearable style="width:120px" @change="search">
<el-option label="来料检" value="incoming" />
<el-option label="过程检" value="process" />
<el-option label="成品检" value="finished" />
</el-select>
</el-form-item>
<el-form-item label="结论">
<el-select v-model="filters.status" placeholder="全部" clearable style="width:110px" @change="search">
<el-option label="合格" value="合格" />
<el-option label="不合格" value="不合格" />
<el-option label="未检" value="未检" />
</el-select>
</el-form-item>
<el-form-item label="记录人">
<el-input v-model="filters.inspector" placeholder="记录人" clearable style="width:120px"
@keyup.enter="search" />
</el-form-item>
<el-form-item label="记录日期">
<el-date-picker v-model="filters.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始"
style="width:130px" @change="search" />
<span style="margin:0 6px"></span>
<el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束"
style="width:130px" @change="search" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
<el-button v-if="can('inspection:export')" type="success" plain @click="exportCsv">导出</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" header="质量检验记录">
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无检验记录">
<el-table-column label="ID" prop="id" width="60" align="center" />
<el-table-column label="对象" width="70" align="center">
<template #default="{ row }">{{ targetTypeLabel(row.targetType) }}</template>
</el-table-column>
<el-table-column prop="targetId" label="批次/SN" min-width="150" />
<el-table-column prop="materialCode" label="图号" min-width="130" />
<el-table-column prop="materialName" label="名称" min-width="120">
<template #default="{ row }">{{ row.materialName || '-' }}</template>
</el-table-column>
<el-table-column label="检验类型" width="90" align="center">
<template #default="{ row }">{{ typeLabel(row.inspectionType) }}</template>
</el-table-column>
<el-table-column label="结论" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '合格' ? 'success' : row.status === '不合格' ? 'danger' : 'info'"
size="small">{{ row.status || '未检' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="处置方式" width="100" align="center">
<template #default="{ row }">
<span v-if="row.status === '不合格'">{{ disposalLabel(row.disposalType) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="inspectQty" label="检验数" width="80" align="center">
<template #default="{ row }">{{ row.inspectQty || 0 }}</template>
</el-table-column>
<el-table-column prop="passQty" label="合格数" width="80" align="center">
<template #default="{ row }">{{ row.passQty || 0 }}</template>
</el-table-column>
<el-table-column label="相关标准" min-width="140">
<template #default="{ row }">
<el-tooltip v-if="row.checkDesc || row.relatedStandard" :content="row.relatedStandard || row.checkDesc" placement="top">
<span>{{ row.relatedStandard || row.checkDesc }}</span>
</el-tooltip>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="reportNo" label="报检单号" min-width="130" align="center">
<template #default="{ row }">{{ row.reportNo || '-' }}</template>
</el-table-column>
<el-table-column label="归属" min-width="160" align="center">
<template #default="{ row }">
<span>{{ ownershipTypeLabel(row.ownershipType) }}</span>
<span v-if="row.ownershipNo">{{ row.ownershipNo }}</span>
</template>
</el-table-column>
<el-table-column prop="inspector" label="记录人" width="90" align="center">
<template #default="{ row }">{{ row.inspector || '-' }}</template>
</el-table-column>
<el-table-column prop="inspectionNo" label="检测单号" min-width="130" align="center">
<template #default="{ row }">{{ row.inspectionNo || '-' }}</template>
</el-table-column>
<el-table-column prop="manufacturer" label="生产厂家" min-width="120" align="center">
<template #default="{ row }">{{ row.manufacturer || '-' }}</template>
</el-table-column>
<el-table-column label="记录日期" width="155" align="center">
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === '不合格'" link type="warning" size="small"
@click="openDispose(row)">处置</el-button>
<el-button link type="success" size="small" @click="openAttach(row)">附件</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
@current-change="(p) => { page.current = p; loadRecords() }"
@size-change="(s) => { page.size = s; page.current = 1; loadRecords() }" />
</div>
</el-tab-pane>
<!-- ============ 按物料统计(点击才加载) ============ -->
<el-tab-pane label="按物料统计" name="stats">
<el-card shadow="never" header="检验次数 / 合格率按物料">
<div style="margin-bottom:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<MaterialSelect v-model="statsMaterialCode" placeholder="先选择物料必填" style="width:280px" />
<el-button type="primary" :loading="statsLoading" :disabled="!statsMaterialCode" @click="loadStats">统计</el-button>
<span style="color:#909399;font-size:12px">需先选物料再统计(按当前时间区间与检验类别,每次检验独立计数,不合并记录)</span>
</div>
<el-table :data="statsList" border size="small" v-loading="statsLoading" :empty-text="statsQueried ? '该物料区间内无检验记录' : '请先选择物料再点「统计」'">
<el-table-column prop="materialCode" label="图号" min-width="160" />
<el-table-column prop="total" label="检验次数" width="110" align="center" />
<el-table-column prop="pass" label="合格次数" width="110" align="center" />
<el-table-column prop="fail" label="不合格次数" width="120" align="center" />
<el-table-column label="合格率" width="120" align="center">
<template #default="{ row }">
<span :style="row.passRate >= 100 ? 'color:#67c23a' : 'color:#e6a23c'">{{ row.passRate }}%</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 不合格处置弹窗 -->
<el-dialog v-model="disposeDialog.visible" title="不合格品处置" width="480px">
<el-form label-width="96px">
<el-form-item label="批次/SN">
<el-input :model-value="disposeDialog.row?.targetId || ''" disabled />
</el-form-item>
<el-form-item label="图号">
<el-input :model-value="disposeDialog.row?.materialCode || ''" disabled />
</el-form-item>
<el-form-item label="检验数 / 合格数">
<el-input :model-value="(disposeDialog.row?.inspectQty || 0) + ' / ' + (disposeDialog.row?.passQty || 0)"
disabled />
</el-form-item>
<el-form-item label="处置方式" required>
<el-radio-group v-model="disposeDialog.type">
<el-radio-button value="RETURN">退货</el-radio-button>
<el-radio-button value="REWORK">换修</el-radio-button>
<el-radio-button value="CONCESSION">让步接收</el-radio-button>
<el-radio-button value="SCRAP">报废</el-radio-button>
<el-radio-button value="PARTIAL">部分入库</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="disposeDialog.type === 'RETURN'" label="货运单号" required>
<el-input v-model="disposeDialog.trackingNo" placeholder="退货运单号" clearable />
</el-form-item>
<el-alert v-if="disposeDialog.type === 'PARTIAL'" type="info" :closable="false"
title="部分入库合格数拆分批次合格部分入库差额部分隔离待处置" />
</el-form>
<template #footer>
<el-button @click="disposeDialog.visible = false">取消</el-button>
<el-button type="primary" @click="submitDispose">确认处置</el-button>
</template>
</el-dialog>
<!-- 检验记录附件抽屉 -->
<el-drawer v-model="attachDrawer.visible" :title="`检验附件:${attachDrawer.targetId}`" size="620px">
<AttachmentPanel biz-type="inspection" :biz-id="attachDrawer.id" title="附件检验报告 / 照片" />
</el-drawer>
</div>
<PageHelp :data="helpInspection" page="Inspection" />
</template>
<style scoped>
.mb12 { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.tip { margin-left: 10px; color: #909399; font-size: 12px; }
</style>