云
昨天 346a988f537228ae44731cd31007f1412b482e83
src/views/mes/qc/seedQuality/record.vue
@@ -10,12 +10,16 @@
  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[]>([]);
@@ -37,13 +41,13 @@
);
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: '水分',
@@ -73,6 +77,13 @@
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');
@@ -84,7 +95,7 @@
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;
});
@@ -92,13 +103,65 @@
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;
@@ -106,27 +169,30 @@
  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 || '复检提交失败');
@@ -154,8 +220,8 @@
}
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;
@@ -254,7 +320,7 @@
      <div class="header-copy">
        <div class="eyebrow">MES / 质量管理</div>
        <h1>质检记录</h1>
        <p>跟踪初检、复检、加工部审批和公司质检审批;完成审批后才流转库存</p>
        <p>初检审核通过后进行复检,复检审核通过后才放行入库或处置不合格品</p>
      </div>
      <div class="header-badge">
        <span class="badge-pulse" />
@@ -291,7 +357,8 @@
          <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>
@@ -320,7 +387,7 @@
        :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,
@@ -343,6 +410,9 @@
        </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>
@@ -373,32 +443,53 @@
        <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">
@@ -458,6 +549,8 @@
.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; }
/* ===== 单元格 ===== */