云
14 小时以前 1ed040591afdcbe3c5df42ad5e02e2cfa56edd87
src/views/mobile/quality-records.vue
@@ -1,106 +1,272 @@
<script setup lang="ts">
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 { MesWmWarehouseTypeEnum } from '@vben/constants';
import {
  approveSeedQualityBatch,
  getSeedIndicatorRanges,
  getSeedQualityPage,
  resolveSeedQuality,
  resolveSeedRetestScope,
  submitSeedQualityGroup,
  submitSeedRetestScope,
  type SeedQualityRecord,
  type SeedQualityScan,
  type SeedRetestScope,
} from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { errorText } from '#/mobile/api';
import { extractCode } from '#/mobile/access';
import Scanner from './scanner.vue';
const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref('');
type ReviewGroup = { ids: number[]; stage: 'company' | 'process'; status: number };
const { hasAccessByCodes } = useAccess();
const canInitial = computed(() => hasAccessByCodes(['mes:qc-seed-quality:submit']));
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 canProcessReview = computed(() => hasAccessByCodes(['mes:qc-seed-quality:process-approve']));
const canCompanyReview = computed(() => hasAccessByCodes(['mes:qc-seed-quality:company-approve']));
const canBatchAction = computed(() => canInitial.value || canRetest.value || canProcessReview.value || canCompanyReview.value);
const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref(''), notice = ref('');
const scanType = ref<'BATCH' | 'PALLET'>('PALLET'), scannedCode = ref('');
const scope = ref<SeedRetestScope>(), initialScope = ref<SeedQualityScan>();
const workflowStatus = ref<10 | 50>(10), reason = ref('');
const warehouseId = ref<number>();
const warehouseOptions = ref<Array<{ label: string; value: number }>>([]);
const reviewGroup = ref<ReviewGroup>(), reviewApproved = ref(true), reviewOpinion = 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 initialIds = computed(() => initialScope.value?.pendingBagIds || []);
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; }
const complete = computed(() => indicators.value.every((item) =>
  [item.value, item.minValue, item.maxValue].every((value) => String(value).trim() !== '' && Number.isFinite(Number(value)))
  && Number(item.value) >= 0 && Number(item.value) <= 100 && Number(item.minValue) >= 0
  && Number(item.maxValue) <= 100 && Number(item.minValue) <= Number(item.maxValue)));
const passed = computed(() => complete.value && indicators.value.every((item) =>
  Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)));
const reviewGroups = computed<ReviewGroup[]>(() => {
  if (!scope.value) return [];
  return [
    { status: 20, stage: 'process' as const, ids: scope.value.pendingProcessIds || [] },
    { status: 25, stage: 'process' as const, ids: scope.value.pendingInitialFailIds || [] },
    { status: 30, stage: 'company' as const, ids: scope.value.pendingCompanyIds || [] },
    { status: 60, stage: 'company' as const, ids: scope.value.pendingCompanyFailIds || [] },
  ].filter((group) => group.ids.length && (group.stage === 'process' ? canProcessReview.value : canCompanyReview.value));
});
const statusNames: Record<number, string> = {
  10: '待复检', 20: '待初检审核', 25: '待初检不合格处理', 30: '待复检审核',
  40: '已放行', 50: '待重新初检', 60: '待复检不合格处理', 70: '已报废',
};
function resetIndicators() {
  indicators.value.forEach((item) => { item.value = ''; });
  reason.value = '';
}
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;
async function hydrateReviewIds(current: SeedRetestScope, rawCode: string) {
  const loaded = (current.pendingProcessIds?.length || 0) + (current.pendingInitialFailIds?.length || 0)
    + (current.pendingCompanyIds?.length || 0) + (current.pendingCompanyFailIds?.length || 0);
  if (loaded >= current.pendingProcessCount + current.pendingCompanyCount) return;
  const batch = current.batchCode || (current.scanType === 'BATCH' ? current.code : undefined);
  if (!batch) return;
  const statuses = [20, 25, 30, 60] as const;
  const pages = await Promise.all(statuses.map((workflow) => getSeedQualityPage({
    batchCode: batch, pageNo: 1, pageSize: 500, workflowStatus: workflow,
  }).catch(() => ({ list: [] as SeedQualityRecord[], total: 0 }))));
  const scopeCodes = new Set([current.code, rawCode]);
  const ids = pages.map(({ list }) => list
    .filter((row) => current.scanType === 'BATCH' || (!!row.palletCode && scopeCodes.has(row.palletCode)))
    .map((row) => row.id));
  current.pendingProcessIds = ids[0];
  current.pendingInitialFailIds = ids[1];
  current.pendingCompanyIds = ids[2];
  current.pendingCompanyFailIds = ids[3];
}
async function identifyScope(raw: string) {
  if (busy.value || !canBatchAction.value) return;
  busy.value = true; error.value = ''; notice.value = '';
  scope.value = undefined; initialScope.value = undefined; reviewGroup.value = undefined;
  try {
    const codeValue = extractCode(raw);
    scannedCode.value = codeValue;
    const current = await resolveSeedRetestScope(codeValue, scanType.value);
    await hydrateReviewIds(current, codeValue);
    scope.value = current;
    scanType.value = current.scanType;
    workflowStatus.value = current.retestIds.length ? 10 : 50;
    initialScope.value = await resolveSeedQuality(codeValue, current.scanType);
    resetIndicators();
    if (!initialIds.value.length && !targetIds.value.length && !reviewGroups.value.length) {
      notice.value = '当前托盘或生产批次没有可处理的质检记录。';
    }
  } catch (exception) { error.value = errorText(exception); }
  finally { busy.value = false; }
}
function indicatorPayload() {
  return indicators.value.map((item) => ({ code: item.code, value: Number(item.value), minValue: Number(item.minValue), maxValue: Number(item.maxValue) }));
}
async function refreshScopeAndList(success: string) {
  await identifyScope(scannedCode.value);
  await load();
  notice.value = success;
}
async function submitInitial() {
  const ids = initialIds.value.slice(0, 500);
  if (busy.value || !canInitial.value || !initialScope.value || !ids.length || !complete.value) return;
  if (!warehouseId.value) { error.value = '请选择检验合格后的目标合格库'; return; }
  if (!passed.value && !reason.value.trim()) { error.value = '初检不合格请填写原因'; return; }
  if (!window.confirm(`确认批量提交 ${ids.length} 袋初检?`)) 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 result = await submitSeedQualityGroup({ code: initialScope.value.code, batchCode: initialScope.value.batchCode,
      scanType: initialScope.value.scanType || scanType.value, bagIds: ids, qualifiedWarehouseId: warehouseId.value,
      reason: reason.value.trim() || undefined, indicators: indicatorPayload() });
    busy.value = false;
    await refreshScopeAndList(`已完成 ${result.processedCount} 袋初检。`);
  } catch (exception) { error.value = errorText(exception) + '。请重新识别后核对待检清单。'; }
  finally { busy.value = false; }
}
async function submitRetest() {
  const ids = targetIds.value.slice(0, 500);
  if (busy.value || !canRetest.value || !scope.value || !ids.length || !complete.value) return;
  if (!passed.value && !reason.value.trim()) { error.value = '检验不合格请填写原因'; return; }
  if (!window.confirm(`确认将本次指标用于 ${ids.length} 袋${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: ids, reason: reason.value.trim() || undefined, indicators: indicatorPayload() });
    busy.value = false;
    await refreshScopeAndList(`已完成 ${result.processedCount} 袋检验。`);
  } catch (exception) { error.value = errorText(exception) + '。请重新识别后核对待检清单。'; }
  finally { busy.value = false; }
}
function selectReview(group: ReviewGroup) {
  reviewGroup.value = group; reviewApproved.value = true; reviewOpinion.value = ''; error.value = '';
}
async function submitReview() {
  if (busy.value || !reviewGroup.value?.ids.length) return;
  if (!reviewApproved.value && !reviewOpinion.value.trim()) { error.value = '驳回时请填写审核意见'; return; }
  const ids = reviewGroup.value.ids.slice(0, 500);
  if (!window.confirm(`确认${reviewApproved.value ? '通过' : '驳回'}本次 ${ids.length} 条记录?`)) return;
  busy.value = true; error.value = '';
  try {
    await approveSeedQualityBatch(reviewGroup.value.stage, { ids, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() });
    busy.value = false;
    await refreshScopeAndList(`已完成 ${ids.length} 条审核。`);
  } catch (exception) { error.value = errorText(exception) + '。请重新识别后核对待审核清单。'; }
  finally { busy.value = false; }
}
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 } = {};
let filters: { batchCode?: string; code?: string; result?: number } = {};
async function load(reset = true) {
  if (busy.value) return;
  busy.value = true; error.value = '';
  const next = reset ? 1 : page.value + 1;
  const query = reset ? { code: code.value.trim() || undefined, batchCode: batchCode.value.trim() || undefined, result: result.value ? Number(result.value) : undefined } : filters;
  const query = reset ? { code: code.value.trim() || undefined, batchCode: batchCode.value.trim() || undefined,
    result: result.value ? Number(result.value) : undefined } : filters;
  try {
    const data = await getSeedQualityPage({ ...query, pageNo: next, pageSize: 20 });
    rows.value = reset ? data.list : [...rows.value, ...data.list];
    page.value = next; total.value = data.total; filters = query;
  } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
  } catch (exception) { error.value = errorText(exception); }
  finally { busy.value = false; }
}
onMounted(() => load());
onMounted(async () => {
  if (canInitial.value) {
    try {
      const warehouses = await getWarehouseSimpleList();
      warehouseOptions.value = warehouses.filter((item) => item.type === MesWmWarehouseTypeEnum.QUALIFIED && !item.frozen && item.id)
        .map((item) => ({ label: item.code ? `${item.name}(${item.code})` : item.name || '', value: item.id! }));
      if (warehouseOptions.value.length === 1) warehouseId.value = warehouseOptions.value[0]?.value;
    } catch { warehouseOptions.value = []; }
  }
  if (canInitial.value || canRetest.value) {
    try {
      const ranges = await getSeedIndicatorRanges();
      indicators.value.forEach((item) => { const range = ranges.find((row) => row.indicatorCode === item.code);
        if (range) { item.minValue = String(range.minValue); item.maxValue = String(range.maxValue); } });
    } catch { /* 合格范围仍可手工填写 */ }
  }
  await 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 v-if="canBatchAction" 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; initialScope = undefined"><option value="PALLET">托盘码</option><option value="BATCH">生产批次码</option></select></label>
    <Scanner :disabled="busy" @code="identifyScope" />
    <template v-if="scope">
      <div class="m-panel"><h3 class="m-title">已识别范围</h3><dl><div class="m-row"><dt>码值</dt><dd>{{ scope.code }}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{ scope.batchCode || '—' }}</dd></div><div class="m-row"><dt>有效袋数</dt><dd>{{ scope.totalCount }}</dd></div></dl></div>
      <form v-if="canInitial && initialIds.length" class="m-panel" @submit.prevent="submitInitial">
        <h3 class="m-title">批量初检({{ Math.min(initialIds.length, 500) }} 袋)</h3>
        <p v-if="!initialScope?.inbound" class="m-warning">该范围的有效袋码尚未全部入库,暂不能提交初检。</p>
        <template v-else>
          <label class="m-field"><span>目标合格库</span><select v-model="warehouseId" required :disabled="busy"><option :value="undefined" disabled>请选择合格库</option><option v-for="item in warehouseOptions" :key="item.value" :value="item.value">{{ item.label }}</option></select></label>
          <fieldset v-for="item in indicators" :key="`initial-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}(%)</legend><label class="m-field"><span>实测值</span><input v-model="item.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="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset>
          <label class="m-field"><span>{{ complete && !passed ? '不合格原因(必填)' : '检验备注' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label>
          <button class="m-primary" :disabled="busy || !complete || !warehouseId">{{ busy ? '正在提交…' : `提交 ${Math.min(initialIds.length, 500)} 袋初检` }}</button>
        </template>
      </form>
      <section v-if="reviewGroups.length" class="m-panel">
        <h3 class="m-title">批量审核</h3>
        <button v-for="group in reviewGroups" :key="group.status" type="button" class="m-secondary" :disabled="busy" @click="selectReview(group)">{{ statusNames[group.status] }} · {{ Math.min(group.ids.length, 500) }} 条</button>
        <form v-if="reviewGroup" @submit.prevent="submitReview">
          <label class="m-field"><span>审核结论</span><select v-model="reviewApproved" :disabled="busy"><option :value="true">通过 / 确认处理</option><option :value="false">驳回重新检验</option></select></label>
          <label class="m-field"><span>{{ reviewApproved ? '审核意见(选填)' : '驳回意见(必填)' }}</span><textarea v-model="reviewOpinion" rows="3" maxlength="500" /></label>
          <button class="m-primary" :disabled="busy">{{ busy ? '正在审核…' : `确认审核 ${Math.min(reviewGroup.ids.length, 500)} 条` }}</button>
        </form>
      </section>
      <form v-if="canRetest && (scope.retestIds.length || scope.initialRecheckIds.length)" class="m-panel" @submit.prevent="submitRetest">
        <h3 class="m-title">批量复检 / 重新初检</h3>
        <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">同一组指标将用于本次 {{ Math.min(targetIds.length, 500) }} 袋,请核对抽样结果。</p>
        <fieldset v-for="item in indicators" :key="`retest-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}(%)</legend><label class="m-field"><span>实测值</span><input v-model="item.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="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset>
        <label class="m-field"><span>{{ complete && !passed ? '不合格原因(必填)' : '检验备注' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label>
        <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{ busy ? '正在提交…' : `提交 ${Math.min(targetIds.length, 500)} 袋检验` }}</button>
      </form>
    </template>
  </section>
  <p v-if="notice" class="m-success" role="status">{{notice}}</p>
  <section v-else class="m-panel"><h2 class="m-title">批量检验与审核</h2><p class="m-muted">当前账号只有质检记录查看权限。如需批量操作,请联系管理员配置初检、复检或审核权限。</p></section>
  <p v-if="notice" class="m-success" role="status">{{ notice }}</p>
  <p v-if="error" class="m-error" role="alert">{{ error }}</p>
  <p class="m-muted">查看质检结果、批次与不合格原因。</p>
  <Scanner :disabled="busy" @code="value => { code = value; load(); }" />
  <form class="m-panel" @submit.prevent="load()">
    <label class="m-field"><span>业务码 / 追溯码</span><input v-model="code" :disabled="busy" placeholder="输入袋码" /></label>
    <label class="m-field"><span>生产批次</span><input v-model="batchCode" :disabled="busy" placeholder="输入批次号" /></label>
    <label class="m-field"><span>质检结果</span><select v-model="result" :disabled="busy"><option value="">全部结果</option><option value="1">合格</option><option value="2">不合格</option></select></label>
    <button class="m-secondary" :disabled="busy">{{busy ? '正在查询…' : '查询记录'}}</button>
    <button class="m-secondary" :disabled="busy">{{ busy ? '正在查询…' : '查询记录' }}</button>
  </form>
  <p v-if="error" class="m-error" role="alert">{{error}}</p>
  <p class="m-muted" role="status">共 {{total}} 条记录</p>
  <p class="m-muted" role="status">共 {{ total }} 条记录</p>
  <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>{{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>
    <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>{{ 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>
  <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{ busy ? '正在加载…' : '加载更多' }}</button>
</template>