- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
690 lines
33 KiB
Vue
690 lines
33 KiB
Vue
<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=inspection,bizId=检验记录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> |