| | |
| | | getSeedQualityPage, |
| | | getSeedQualityIndicators, |
| | | getSeedQualityActions, |
| | | getSeedIndicatorRanges, |
| | | retestSeedQuality, |
| | | resolveSeedRetestScope, |
| | | submitSeedRetestScope, |
| | | approveSeedQuality, |
| | | approveSeedQualityBatch, |
| | | type SeedQualityRecord, |
| | | type SeedQualityIndicator, |
| | | type SeedQualityAction, |
| | | type SeedRetestScope, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | const records = ref<SeedQualityRecord[]>([]); |
| | |
| | | ); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const workflowLabels: Record<number, string> = { |
| | | 10: '待复检', 20: '待加工部审批', 30: '待公司质检审批', |
| | | 40: '已放行', 50: '待加工部不合格评审', 60: '待公司不合格审批', 70: '已报废', |
| | | 10: '待复检', 20: '待初检审核', 25: '待初检不合格处理', 30: '待复检审核', |
| | | 40: '已放行', 50: '初检驳回待重检', 60: '待复检不合格处理', 70: '已报废', |
| | | }; |
| | | const actionLabels: Record<string, string> = { |
| | | INITIAL: '初检', RETEST: '复检', PROCESS_APPROVE: '加工部通过', |
| | | PROCESS_REJECT: '加工部驳回', COMPANY_APPROVE: '公司质检通过', |
| | | COMPANY_REJECT: '公司质检驳回', |
| | | INITIAL: '初检', INITIAL_RECHECK: '重新初检', RETEST: '复检', PROCESS_APPROVE: '初检审核通过', |
| | | PROCESS_REJECT: '初检审核驳回', COMPANY_APPROVE: '复检审核通过', |
| | | COMPANY_REJECT: '复检审核驳回', |
| | | }; |
| | | const indicatorLabels: Record<string, string> = { |
| | | purity: '纯度', cleanliness: '净度', germination_rate: '发芽率', moisture: '水分', |
| | |
| | | const retestBusy = ref(false); |
| | | const retestIndicators = ref<SeedQualityIndicator[]>([]); |
| | | const retestReason = ref(''); |
| | | const scopeOpen = ref(false); |
| | | const scopeType = ref<'PALLET' | 'BATCH'>('PALLET'); |
| | | const scopeCode = ref(''); |
| | | const retestScope = ref<SeedRetestScope>(); |
| | | const scopeStatus = ref<10 | 50>(10); |
| | | const scopeBusy = ref(false); |
| | | const scopeIds = computed(() => scopeStatus.value === 10 ? retestScope.value?.retestIds || [] : retestScope.value?.initialRecheckIds || []); |
| | | const reviewOpen = ref(false); |
| | | const reviewBusy = ref(false); |
| | | const reviewStage = ref<'process' | 'company'>('process'); |
| | |
| | | const selectableStage = computed(() => { |
| | | if (!selectedRows.value.length) return undefined; |
| | | if (!selectedRows.value.every((record) => record.workflowStatus === selectedRows.value[0]?.workflowStatus)) return undefined; |
| | | if (selectedRows.value.every((record) => [20, 50].includes(record.workflowStatus))) return 'process'; |
| | | if (selectedRows.value.every((record) => [20, 25].includes(record.workflowStatus))) return 'process'; |
| | | if (selectedRows.value.every((record) => [30, 60].includes(record.workflowStatus))) return 'company'; |
| | | return undefined; |
| | | }); |
| | |
| | | async function openRetest(record: SeedQualityRecord) { |
| | | activeRecord.value = record; |
| | | try { |
| | | retestIndicators.value = await getSeedQualityIndicators(record.id); |
| | | const previous = await getSeedQualityIndicators(record.id); |
| | | const ranges = await getSeedIndicatorRanges().catch(() => []); |
| | | retestIndicators.value = previous.map((item) => { |
| | | const range = ranges.find((row) => row.indicatorCode === item.indicatorCode); |
| | | return { ...item, minValue: range ? Number(range.minValue) : Number(item.minValue), maxValue: range ? Number(range.maxValue) : Number(item.maxValue) }; |
| | | }); |
| | | if (retestIndicators.value.length !== 4) throw new Error('质检指标不完整'); |
| | | retestIndicators.value = retestIndicators.value.map((item) => ({ ...item, value: undefined })); |
| | | retestReason.value = ''; |
| | | retestOpen.value = true; |
| | | } catch (error) { |
| | | message.error((error as Error).message || '读取质检指标失败'); |
| | | } |
| | | } |
| | | function openScopeRetest() { |
| | | scopeOpen.value = true; retestScope.value = undefined; scopeCode.value = ''; retestReason.value = ''; |
| | | } |
| | | async function identifyScope() { |
| | | if (!scopeCode.value.trim()) { message.warning('请扫描或输入托盘码 / 生产批次码'); return; } |
| | | scopeBusy.value = true; retestScope.value = undefined; |
| | | try { |
| | | const data = await resolveSeedRetestScope(scopeCode.value.trim(), scopeType.value); |
| | | if (!data.retestIds.length && !data.initialRecheckIds.length) { |
| | | const pending = [ |
| | | data.pendingProcessCount ? `${data.pendingProcessCount} 条待初检审核或不合格处理` : '', |
| | | data.pendingCompanyCount ? `${data.pendingCompanyCount} 条待复检审核或不合格处理` : '', |
| | | ].filter(Boolean).join('、'); |
| | | throw new Error(pending |
| | | ? `当前范围有${pending},请先完成对应审核或处理` |
| | | : '当前范围没有待复检或待重新初检的袋码'); |
| | | } |
| | | retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50; |
| | | const ranges = await getSeedIndicatorRanges().catch(() => []); |
| | | retestIndicators.value = Object.entries(indicatorLabels).map(([indicatorCode]) => { |
| | | const range = ranges.find((row) => row.indicatorCode === indicatorCode); |
| | | return { indicatorCode, value: undefined, minValue: range ? Number(range.minValue) : undefined as unknown as number, |
| | | maxValue: range ? Number(range.maxValue) : undefined as unknown as number }; |
| | | }); |
| | | retestReason.value = ''; |
| | | } catch (error) { message.error((error as Error).message || '识别失败'); } |
| | | finally { scopeBusy.value = false; } |
| | | } |
| | | async function saveScopeRetest() { |
| | | if (!retestScope.value || !scopeIds.value.length) return; |
| | | if (retestIndicators.value.some(item => item.value === undefined || item.value === null || item.minValue === undefined || item.maxValue === undefined |
| | | || Number(item.minValue) < 0 || Number(item.maxValue) > 100 || Number(item.minValue) > Number(item.maxValue))) { |
| | | message.warning('请填写四项指标的实测值与本次合格范围'); return; |
| | | } |
| | | const passed = retestIndicators.value.every(item => Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)); |
| | | if (!passed && !retestReason.value.trim()) { message.warning('不合格请填写原因'); return; } |
| | | scopeBusy.value = true; |
| | | try { |
| | | const result = await submitSeedRetestScope({ code: retestScope.value.code, scanType: retestScope.value.scanType, workflowStatus: scopeStatus.value, |
| | | qualityIds: scopeIds.value, reason: retestReason.value.trim(), indicators: retestIndicators.value.map(item => ({ |
| | | code: item.indicatorCode, value: Number(item.value), minValue: Number(item.minValue), maxValue: Number(item.maxValue), |
| | | })) }); |
| | | message.success(`已完成 ${result.processedCount} 袋质检`); scopeOpen.value = false; refreshAll(); |
| | | } catch (error) { message.error((error as Error).message || '批量质检失败'); retestScope.value = undefined; } |
| | | finally { scopeBusy.value = false; } |
| | | } |
| | | async function saveRetest() { |
| | | const record = activeRecord.value; |
| | |
| | | if (retestIndicators.value.some((item) => item.value === null || item.value === undefined |
| | | || item.minValue === null || item.minValue === undefined || item.maxValue === null || item.maxValue === undefined |
| | | || Number(item.minValue) > Number(item.maxValue))) { |
| | | message.warning('请填写完整的实测值及有效上下限'); |
| | | message.warning('请填写完整的实测值'); |
| | | return; |
| | | } |
| | | const passed = retestIndicators.value.every((item) => |
| | | Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)); |
| | | if (!passed && !retestReason.value.trim()) { |
| | | message.warning('复检不合格请填写原因'); |
| | | message.warning('检验不合格请填写原因'); |
| | | return; |
| | | } |
| | | retestBusy.value = true; |
| | | try { |
| | | await retestSeedQuality(record.id, { |
| | | code: record.code, |
| | | const payload = { |
| | | reason: retestReason.value.trim(), |
| | | indicators: retestIndicators.value.map((item) => ({ |
| | | code: item.indicatorCode, value: Number(item.value), |
| | | minValue: Number(item.minValue), maxValue: Number(item.maxValue), |
| | | })), |
| | | }); |
| | | message.success(passed ? '复检合格,待加工部审批' : '复检不合格,待加工部评审'); |
| | | }; |
| | | await retestSeedQuality(record.id, { ...payload, code: record.code }); |
| | | message.success(record.workflowStatus === 50 |
| | | ? passed ? '重新初检合格,待初检审核' : '重新初检不合格,待不合格处理' |
| | | : passed ? '复检合格,待复检审核' : '复检不合格,待不合格处理'); |
| | | retestOpen.value = false; |
| | | selectedIds.value = []; |
| | | refreshAll(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '复检提交失败'); |
| | |
| | | } |
| | | async function saveReview() { |
| | | if (!reviewIds.value.length) return; |
| | | if (!reviewOpinion.value.trim()) { |
| | | message.warning('请填写审批意见'); |
| | | if (!reviewApproved.value && !reviewOpinion.value.trim()) { |
| | | message.warning('驳回时请填写审批意见'); |
| | | return; |
| | | } |
| | | reviewBusy.value = true; |
| | |
| | | <div class="header-copy"> |
| | | <div class="eyebrow">MES / 质量管理</div> |
| | | <h1>质检记录</h1> |
| | | <p>跟踪初检、复检、加工部审批和公司质检审批;完成审批后才流转库存</p> |
| | | <p>初检审核通过后进行复检,复检审核通过后才放行入库或处置不合格品</p> |
| | | </div> |
| | | <div class="header-badge"> |
| | | <span class="badge-pulse" /> |
| | |
| | | <span class="count-chip">{{ total.toLocaleString() }} 条</span> |
| | | </div> |
| | | <div class="filters"> |
| | | <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">批量{{ selectableStage === 'process' ? '加工部审批' : '公司质检审批' }}({{ selectedRows.length }})</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="primary" @click="openScopeRetest">按托盘 / 批次批量复检</a-button> |
| | | <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">批量{{ selectableStage === 'process' ? selectedRows[0]?.workflowStatus === 25 ? '初检不合格处理' : '初检审核' : selectedRows[0]?.workflowStatus === 60 ? '复检不合格处理' : '复检审核' }}({{ selectedRows.length }})</a-button> |
| | | <a-input v-model:value="code" allow-clear placeholder="袋码" @press-enter="searchRecords"> |
| | | <template #prefix><IconifyIcon icon="ep:box" /></template> |
| | | </a-input> |
| | |
| | | :data-source="records" |
| | | :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }" |
| | | :loading="loading" |
| | | :scroll="{ x: 1120 }" |
| | | :scroll="{ x: 1900 }" |
| | | :pagination="{ |
| | | current: page, |
| | | pageSize, |
| | |
| | | </a-table-column> |
| | | <a-table-column key="code" title="袋码" data-index="code" min-width="220"> |
| | | <template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="palletCode" title="托盘码" data-index="palletCode" min-width="190"> |
| | | <template #default="{ record }"><span class="mono">{{ record.palletCode || '—' }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="batchCode" title="生产批次" data-index="batchCode" min-width="190"> |
| | | <template #default="{ record }">{{ record.batchCode || '—' }}</template> |
| | |
| | | <a-table-column key="actions" title="操作" width="260" fixed="right"> |
| | | <template #default="{ record }"> |
| | | <a-space> |
| | | <a-button v-if="record.workflowStatus === 10 && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">复检</a-button> |
| | | <a-button v-if="[20, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">加工部审批</a-button> |
| | | <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">公司质检审批</a-button> |
| | | <a-button v-if="[10, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">{{ record.workflowStatus === 50 ? '重新初检' : '复检' }}</a-button> |
| | | <a-button v-if="[20, 25].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">{{ record.workflowStatus === 25 ? '不合格处理' : '初检审核' }}</a-button> |
| | | <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">{{ record.workflowStatus === 60 ? '不合格处理' : '复检审核' }}</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:query'])" type="link" @click="openHistory(record)">流程记录</a-button> |
| | | </a-space> |
| | | </template> |
| | | </a-table-column> |
| | | </a-table> |
| | | </section> |
| | | <a-modal v-model:open="retestOpen" title="种子质检复检" :confirm-loading="retestBusy" ok-text="提交复检" @ok="saveRetest"> |
| | | <p class="muted">袋码:{{ activeRecord?.code }}。原指标范围已带入,请录入本次实测值并核对上下限。</p> |
| | | <a-modal v-model:open="retestOpen" :title="activeRecord?.workflowStatus === 50 ? '重新初检' : '种子质检复检'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? '提交初检' : '提交复检'" @ok="saveRetest"> |
| | | <p class="muted">袋码:{{ activeRecord?.code }}。四项指标与初检一致;请录入本次实测值并核对合格范围。</p> |
| | | <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] || item.indicatorCode }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="实测值" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="下限" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="上限" /> |
| | | <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="本次实测值" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="下限" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="上限" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="复检不合格时填写原因" /> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="本次检验不合格时填写原因" /> |
| | | </a-modal> |
| | | <a-modal v-model:open="reviewOpen" :title="reviewStage === 'process' ? '加工部审批' : '公司质检审批'" :confirm-loading="reviewBusy" ok-text="提交审批" @ok="saveReview"> |
| | | <a-modal v-model:open="scopeOpen" title="按托盘 / 生产批次批量复检或重新初检" :confirm-loading="scopeBusy" ok-text="提交本阶段全部袋码" :ok-button-props="{ disabled: !scopeIds.length }" @ok="saveScopeRetest"> |
| | | <a-space class="scope-scan"> |
| | | <a-select v-model:value="scopeType" style="width: 130px" :options="[{ label: '托盘码', value: 'PALLET' }, { label: '生产批次码', value: 'BATCH' }]" @change="retestScope = undefined" /> |
| | | <a-input-search v-model:value="scopeCode" placeholder="扫描或输入码值" enter-button="识别" :loading="scopeBusy" @search="identifyScope" /> |
| | | </a-space> |
| | | <template v-if="retestScope"> |
| | | <p>已识别 {{ retestScope.code }} · 有效袋 {{ retestScope.totalCount }} · 生产批次 {{ retestScope.batchCode || '—' }}</p> |
| | | <a-radio-group v-model:value="scopeStatus"> |
| | | <a-radio-button v-if="retestScope.retestIds.length" :value="10">复检 {{ retestScope.retestIds.length }} 袋</a-radio-button> |
| | | <a-radio-button v-if="retestScope.initialRecheckIds.length" :value="50">重新初检 {{ retestScope.initialRecheckIds.length }} 袋</a-radio-button> |
| | | </a-radio-group> |
| | | <p class="muted">同一组指标将应用于本阶段全部 {{ scopeIds.length }} 袋。模板范围可以修改。</p> |
| | | <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :max="100" :precision="3" placeholder="实测值" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="下限" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="上限" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="不合格时填写原因" /> |
| | | </template> |
| | | </a-modal> |
| | | <a-modal v-model:open="reviewOpen" :title="[25, 60].includes(activeRecord?.workflowStatus ?? 0) ? '不合格处理' : reviewStage === 'process' ? '初检审核' : '复检审核'" :confirm-loading="reviewBusy" ok-text="提交审核" @ok="saveReview"> |
| | | <p>{{ reviewIds.length > 1 ? `共 ${reviewIds.length} 条记录` : activeRecord?.code }} · {{ workflowLabels[activeRecord?.workflowStatus ?? 0] }}</p> |
| | | <a-radio-group v-model:value="reviewApproved" class="review-choice"> |
| | | <a-radio :value="true">同意{{ reviewStage === 'company' ? activeRecord?.workflowStatus === 30 ? '放行入库' : '报废处置' : '上报公司质检' }}</a-radio> |
| | | <a-radio :value="false">驳回重检</a-radio> |
| | | <a-radio :value="true">{{ reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '确认不合格,安排复检' : '通过,进入复检' : activeRecord?.workflowStatus === 30 ? '通过,放行入库' : '确认不合格,报废处置' }}</a-radio> |
| | | <a-radio :value="false">{{ reviewStage === 'process' ? '驳回,重新初检' : '驳回,重新复检' }}</a-radio> |
| | | </a-radio-group> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填写审批意见" /> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" :placeholder="reviewApproved ? '审批意见(选填)' : '请填写驳回意见'" /> |
| | | </a-modal> |
| | | <a-modal v-model:open="historyOpen" :title="`流程记录 · ${activeRecord?.code || ''}`" :footer="null"> |
| | | <a-timeline v-if="historyActions.length"> |
| | |
| | | .filters :deep(.ant-btn .iconify) { font-size: 14px; } |
| | | .retest-row { display: grid; grid-template-columns: 60px repeat(3, 1fr); gap: 8px; align-items: center; margin-bottom: 10px; } |
| | | .retest-row :deep(.ant-input-number) { width: 100%; } |
| | | .scope-scan { display: flex; width: 100%; margin-bottom: 16px; } |
| | | .scope-scan :deep(.ant-space-item:last-child) { flex: 1; } |
| | | .review-choice { display: flex; gap: 16px; margin: 10px 0 18px; } |
| | | |
| | | /* ===== 单元格 ===== */ |