| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { getSeedQualityPage, type SeedQualityRecord } from '#/api/mes/qc/seedQuality'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { getSeedIndicatorRanges, getSeedQualityPage, resolveSeedRetestScope, submitSeedRetestScope, type SeedQualityRecord, type SeedRetestScope } from '#/api/mes/qc/seedQuality'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import { extractCode } from '#/mobile/access'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref(''); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const canRetest = computed(() => hasAccessByCodes(['mes:qc-seed-quality:retest'])); |
| | | const scanType = ref<'PALLET' | 'BATCH'>('PALLET'), scope = ref<SeedRetestScope>(); |
| | | const workflowStatus = ref<10 | 50>(10), reason = ref(''), notice = ref(''); |
| | | const indicators = ref([ |
| | | { code: 'purity', name: '纯度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'cleanliness', name: '净度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'germination_rate', name: '发芽率', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'moisture', name: '水分', value: '', minValue: '', maxValue: '' }, |
| | | ]); |
| | | const targetIds = computed(() => workflowStatus.value === 10 ? scope.value?.retestIds || [] : scope.value?.initialRecheckIds || []); |
| | | const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => v.trim() !== '' && Number.isFinite(Number(v))) |
| | | && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.minValue) <= Number(i.maxValue))); |
| | | const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue))); |
| | | async function identifyRetest(raw: string) { |
| | | if (busy.value) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; scope.value = undefined; |
| | | try { |
| | | const data = await resolveSeedRetestScope(extractCode(raw), scanType.value); |
| | | if (!data.retestIds.length && !data.initialRecheckIds.length) throw new Error('当前托盘或批次没有待复检或待重新初检的记录'); |
| | | scope.value = data; |
| | | workflowStatus.value = data.retestIds.length ? 10 : 50; |
| | | indicators.value.forEach(i => { i.value = ''; i.minValue = ''; i.maxValue = ''; }); reason.value = ''; |
| | | try { |
| | | const ranges = await getSeedIndicatorRanges(); |
| | | indicators.value.forEach(i => { const range = ranges.find(r => r.indicatorCode === i.code); if (range) { i.minValue = String(range.minValue); i.maxValue = String(range.maxValue); } }); |
| | | } catch { /* 可手动填写本次范围 */ } |
| | | } catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function submitRetest() { |
| | | if (busy.value || !scope.value || !targetIds.value.length || !complete.value) return; |
| | | if (!passed.value && !reason.value.trim()) { error.value = '检验不合格请填写原因'; return; } |
| | | const count = targetIds.value.length; |
| | | if (!window.confirm(`确认将本次指标用于 ${count} 袋${workflowStatus.value === 10 ? '复检' : '重新初检'}?`)) return; |
| | | busy.value = true; error.value = ''; |
| | | try { |
| | | const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, workflowStatus: workflowStatus.value, |
| | | qualityIds: targetIds.value, reason: reason.value.trim() || undefined, |
| | | indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) }); |
| | | notice.value = `已完成 ${result.processedCount} 袋检验,等待后续审核或不合格处理。`; |
| | | scope.value = undefined; |
| | | } catch (e) { error.value = errorText(e) + '。请重新扫码核对清单后再提交。'; scope.value = undefined; } |
| | | finally { busy.value = false; if (notice.value) void load(); } |
| | | } |
| | | const code = ref(''), batchCode = ref(''), result = ref(''), page = ref(0), total = ref(0); |
| | | const statusNames: Record<number, string> = { 10: '待复检', 20: '待初检审核', 25: '待初检不合格处理', 30: '待复检审核', 40: '已放行', 50: '待重新初检', 60: '待复检不合格处理', 70: '已报废' }; |
| | | let filters: { code?: string; batchCode?: string; result?: number } = {}; |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | |
| | | onMounted(() => load()); |
| | | </script> |
| | | <template> |
| | | <section v-if="canRetest" class="m-panel"> |
| | | <h2 class="m-title">按托盘 / 批次批量质检</h2> |
| | | <p class="m-muted">扫描托盘码或生产批次码,系统会列出待复检和待重新初检的袋数。</p> |
| | | <label class="m-field"><span>扫码类型</span><select v-model="scanType" :disabled="busy" @change="scope = undefined"><option value="PALLET">托盘码</option><option value="BATCH">生产批次码</option></select></label> |
| | | <Scanner :disabled="busy" @code="identifyRetest" /> |
| | | <form v-if="scope" @submit.prevent="submitRetest"> |
| | | <p class="m-muted">{{scope.code}} · {{scope.batchCode || '未标注批次'}} · 有效袋 {{scope.totalCount}}</p> |
| | | <label class="m-field"><span>质检阶段</span><select v-model="workflowStatus" :disabled="busy"><option v-if="scope.retestIds.length" :value="10">复检({{scope.retestIds.length}} 袋)</option><option v-if="scope.initialRecheckIds.length" :value="50">重新初检({{scope.initialRecheckIds.length}} 袋)</option></select></label> |
| | | <p class="m-warning">同一组指标将用于本阶段全部 {{targetIds.length}} 袋。请核对抽样结果是否适用。</p> |
| | | <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}(%)</legend> |
| | | <label class="m-field"><span>实测值</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label> |
| | | <div class="m-grid"><label class="m-field"><span>合格下限</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div> |
| | | </fieldset> |
| | | <p class="m-muted">上下限从模板带入,可按本次检验标准调整。</p> |
| | | <label class="m-field"><span>{{complete && !passed ? '不合格原因(必填)' : '检验备注'}}</span><textarea v-model="reason" :required="complete && !passed" maxlength="500" rows="3" /></label> |
| | | <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{busy ? '正在提交…' : `提交 ${targetIds.length} 袋质检`}}</button> |
| | | </form> |
| | | </section> |
| | | <p v-if="notice" class="m-success" role="status">{{notice}}</p> |
| | | <p class="m-muted">查看质检结果、批次与不合格原因。</p> |
| | | <Scanner :disabled="busy" @code="value => { code = value; load(); }" /> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">暂无匹配的质检记录</p> |
| | | <article v-for="row in rows" :key="row.id" class="m-panel"> |
| | | <strong :class="['m-status', row.result === 1 ? 'success' : row.result === 2 ? 'failed' : '']">{{row.result === 1 ? '合格' : row.result === 2 ? '不合格' : '处理中'}}</strong> |
| | | <dl><div class="m-row"><dt>追溯码</dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{row.batchCode || '—'}}</dd></div><div class="m-row"><dt>检验时间</dt><dd>{{row.inspectedAt || row.createTime || '—'}}</dd></div><div v-if="row.reason" class="m-row"><dt>原因 / 备注</dt><dd>{{row.reason}}</dd></div></dl> |
| | | <dl><div class="m-row"><dt>追溯码</dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{row.batchCode || '—'}}</dd></div><div class="m-row"><dt>流程状态</dt><dd>{{statusNames[row.workflowStatus] || '未知状态'}}</dd></div><div class="m-row"><dt>检验时间</dt><dd>{{row.inspectedAt || row.createTime || '—'}}</dd></div><div v-if="row.reason" class="m-row"><dt>原因 / 备注</dt><dd>{{row.reason}}</dd></div></dl> |
| | | </article> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{busy ? '正在加载…' : '加载更多'}}</button> |
| | | </template> |