云
昨天 8a9338f2f43bfe936124feaaa22420b4ca86eb84
feat(qc): 完善质检流程功能

- 更新出货检验单提交提示信息,明确提交后的审批流程
- 修改检验成功消息,指示用户在销售质检菜单继续后续操作
- 在API层新增workflowStatus字段支持销售质检复检和审批状态
- 新增retestOqc、approveOqc、getOqcActions等销售质检相关接口
- 为种子质量检验增加workflowStatus、retestResult等流程状态字段
- 新增getSeedQualityIndicators、retestSeedQuality、approveSeedQuality等种子质检流程接口
- 在出货检验页面增加销售模式参数,支持显示销售质检流程状态
- 添加复检、审批、流程记录等功能界面组件
- 重构种子质检页面,优化指标录入和结果判定逻辑
- 添加种子质检复检、审批、批量操作等功能
- 创建新的销售质检独立页面salesQuality/index.vue
- 更新统计汇总逻辑,区分不同质检状态的统计数据
- 优化用户界面交互体验和操作提示信息
已添加1个文件
已修改6个文件
452 ■■■■■ 文件已修改
src/api/mes/qc/oqc/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/seedQuality/index.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/index.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/salesQuality/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/seedQuality/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/seedQuality/record.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/oqc/index.ts
@@ -39,6 +39,7 @@
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€
    workflowStatus?: number; // é”€å”®è´¨æ£€å¤æ£€å’Œå®¡æ‰¹çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
}
@@ -69,6 +70,16 @@
export function finishOqc(id: number) {
  return requestClient.put(`/mes/qc/oqc/finish?id=${id}`);
}
export function retestOqc(data: { id: number; checkQuantity: number; qualifiedQuantity: number; unqualifiedQuantity: number; checkResult: number; opinion?: string }) {
  return requestClient.post('/mes/qc/oqc/retest', data);
}
export function approveOqc(stage: 'sales' | 'company', data: { id: number; approved: boolean; opinion: string }) {
  return requestClient.post(`/mes/qc/oqc/${stage}-approve`, data);
}
export interface OqcAction { id: number; action: string; result?: number; opinion?: string; quantitiesJson?: string; operatorId?: number; createTime?: string }
export function getOqcActions(id: number) {
  return requestClient.get<OqcAction[]>('/mes/qc/oqc/actions', { params: { id } });
}
/** åˆ é™¤å‡ºè´§æ£€éªŒå• */
export function deleteOqc(id: number) {
src/api/mes/qc/seedQuality/index.ts
@@ -6,6 +6,7 @@
  code?: string;
  batchCode?: string;
  result?: number;
  workflowStatus?: number;
}
export interface SeedQualityRecord {
@@ -13,11 +14,43 @@
  code: string;
  batchCode?: string;
  result: number;
  workflowStatus: number;
  retestResult?: number;
  reason?: string;
  inspectorId?: number;
  inspectedAt?: string;
  createTime?: string;
}
export interface SeedQualityIndicator {
  indicatorCode: string;
  value: number;
  minValue: number;
  maxValue: number;
}
export function getSeedQualityIndicators(id: number) {
  return requestClient.get<SeedQualityIndicator[]>('/mes/qc/seed-quality/indicators', { params: { id } });
}
export interface SeedQualityAction {
  id: number;
  action: string;
  result?: number;
  opinion?: string;
  indicatorsJson?: string;
  operatorId?: number;
  createTime?: string;
}
export function getSeedQualityActions(id: number) {
  return requestClient.get<SeedQualityAction[]>('/mes/qc/seed-quality/actions', { params: { id } });
}
export function retestSeedQuality(id: number, data: Omit<MesQcSeedQualityApi.SubmitParams, 'qualifiedWarehouseId'>) {
  return requestClient.post('/mes/qc/seed-quality/retest', data, { params: { id } });
}
export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) {
  return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data);
}
export function approveSeedQualityBatch(stage: 'process' | 'company', data: { ids: number[]; approved: boolean; opinion: string }) {
  return requestClient.post(`/mes/qc/seed-quality/${stage}-approve-batch`, data);
}
export namespace MesQcSeedQualityApi {
  export interface Indicator {
src/views/mes/qc/oqc/index.vue
@@ -1,19 +1,95 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcOqcApi } from '#/api/mes/qc/oqc';
import type { OqcAction } from '#/api/mes/qc/oqc';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
import { MesBizTypeEnum, MesQcStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import { approveOqc, deleteOqc, exportOqc, getOqcActions, getOqcPage, retestOqc } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const props = withDefaults(defineProps<{ salesMode?: boolean }>(), { salesMode: false });
const { hasAccessByCodes } = useAccess();
const workflowLabels: Record<number, string> = {
  0: '草稿', 10: '待复检', 20: '待销售部审批', 30: '待公司质检审批',
  40: '已放行', 50: '待销售部不合格评审', 60: '待公司不合格审批', 70: '已拒绝出货',
};
const active = ref<MesQcOqcApi.Oqc>();
const retestOpen = ref(false);
const retestBusy = ref(false);
const retestForm = ref({ checkQuantity: 0, qualifiedQuantity: 0, unqualifiedQuantity: 0, opinion: '' });
function openRetest(row: MesQcOqcApi.Oqc) {
  active.value = row;
  retestForm.value = {
    checkQuantity: Number(row.checkQuantity ?? 0), qualifiedQuantity: Number(row.qualifiedQuantity ?? 0),
    unqualifiedQuantity: Number(row.unqualifiedQuantity ?? 0), opinion: '',
  };
  retestOpen.value = true;
}
async function saveRetest() {
  if (!active.value?.id) return;
  const form = retestForm.value;
  if (form.checkQuantity < 0 || form.qualifiedQuantity < 0 || form.unqualifiedQuantity < 0
    || form.checkQuantity !== form.qualifiedQuantity + form.unqualifiedQuantity) {
    message.warning('检测数量必须等于合格与不合格数量之和');
    return;
  }
  if (form.unqualifiedQuantity > 0 && !form.opinion.trim()) {
    message.warning('复检不合格请填写原因');
    return;
  }
  retestBusy.value = true;
  try {
    await retestOqc({ id: active.value.id, ...form, checkResult: form.unqualifiedQuantity === 0 ? 1 : 2 });
    message.success('复检结果已提交,待销售部评审');
    retestOpen.value = false;
    handleRefresh();
  } catch (error) {
    message.error((error as Error).message || '复检失败');
  } finally { retestBusy.value = false; }
}
const reviewOpen = ref(false);
const reviewBusy = ref(false);
const reviewStage = ref<'sales' | 'company'>('sales');
const reviewApproved = ref(true);
const reviewOpinion = ref('');
function openReview(row: MesQcOqcApi.Oqc, stage: 'sales' | 'company') {
  active.value = row; reviewStage.value = stage; reviewApproved.value = true;
  reviewOpinion.value = ''; reviewOpen.value = true;
}
async function saveReview() {
  if (!active.value?.id) return;
  if (!reviewOpinion.value.trim()) { message.warning('请填写审批意见'); return; }
  reviewBusy.value = true;
  try {
    await approveOqc(reviewStage.value, { id: active.value.id, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() });
    message.success('审批完成'); reviewOpen.value = false; handleRefresh();
  } catch (error) { message.error((error as Error).message || '审批失败'); }
  finally { reviewBusy.value = false; }
}
const historyOpen = ref(false);
const historyActions = ref<OqcAction[]>([]);
const actionLabels: Record<string, string> = {
  INITIAL: '初检', RETEST: '复检', SALES_APPROVE: '销售部通过', SALES_REJECT: '销售部驳回',
  COMPANY_APPROVE: '公司质检通过', COMPANY_REJECT: '公司质检驳回',
};
async function openHistory(row: MesQcOqcApi.Oqc) {
  if (!row.id) return;
  active.value = row;
  try { historyActions.value = await getOqcActions(row.id); historyOpen.value = true; }
  catch (error) { message.error((error as Error).message || '读取流程记录失败'); }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -58,15 +134,23 @@
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportOqc(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '出货检验单.xls', source: data });
  downloadFileFromBlobPart({ fileName: props.salesMode ? '销售质检单.xls' : '出货检验单.xls', source: data });
}
const columns = useGridColumns();
if (props.salesMode && columns) {
  columns.splice(columns.length - 1, 0, {
    field: 'workflowStatus', title: '销售质检流程', width: 170, slots: { default: 'workflow' },
  });
  const actions = columns.at(-1);
  if (actions) actions.width = 320;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    columns,
    height: 'auto',
    keepSource: true,
    proxyConfig: {
@@ -76,6 +160,7 @@
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
            sourceDocType: props.salesMode ? MesBizTypeEnum.WM_PRODUCT_SALES : undefined,
          });
        },
      },
@@ -95,7 +180,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <Grid table-title="出货检验单列表">
    <Grid :table-title="props.salesMode ? '销售质检单列表' : '出货检验单列表'">
      <template #toolbar-tools>
        <TableAction
          :actions="[
@@ -120,6 +205,9 @@
        <Button type="link" @click="handleDetail(row)">
          {{ row.code }}
        </Button>
      </template>
      <template #workflow="{ row }">
        <span>{{ workflowLabels[row.workflowStatus ?? 0] || '未知状态' }}</span>
      </template>
      <template #actions="{ row }">
        <TableAction
@@ -153,7 +241,38 @@
            },
          ]"
        />
        <Button v-if="props.salesMode && row.workflowStatus === 10 && hasAccessByCodes(['mes:qc-oqc:retest'])" type="link" @click="openRetest(row)">复检</Button>
        <Button v-if="props.salesMode && [20, 50].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:sales-approve'])" type="link" @click="openReview(row, 'sales')">销售部审批</Button>
        <Button v-if="props.salesMode && [30, 60].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:company-approve'])" type="link" @click="openReview(row, 'company')">公司质检审批</Button>
        <Button v-if="props.salesMode && hasAccessByCodes(['mes:qc-oqc:query'])" type="link" @click="openHistory(row)">流程记录</Button>
      </template>
    </Grid>
    <a-modal v-if="props.salesMode" v-model:open="retestOpen" title="销售质检复检" :confirm-loading="retestBusy" ok-text="提交复检" @ok="saveRetest">
      <p>检验单:{{ active?.code }}</p>
      <a-form layout="vertical">
        <a-form-item label="本次检测数量"><a-input-number v-model:value="retestForm.checkQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="合格数量"><a-input-number v-model:value="retestForm.qualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="不合格数量"><a-input-number v-model:value="retestForm.unqualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="复检意见"><a-textarea v-model:value="retestForm.opinion" :rows="3" :maxlength="500" /></a-form-item>
      </a-form>
    </a-modal>
    <a-modal v-if="props.salesMode" v-model:open="reviewOpen" :title="reviewStage === 'sales' ? '销售部审批' : '公司质检审批'" :confirm-loading="reviewBusy" ok-text="提交审批" @ok="saveReview">
      <p>{{ active?.code }} Â· {{ workflowLabels[active?.workflowStatus ?? 0] }}</p>
      <a-radio-group v-model:value="reviewApproved" style="display: flex; gap: 16px; margin: 12px 0 18px">
        <a-radio :value="true">同意{{ reviewStage === 'company' ? active?.workflowStatus === 30 ? '出货放行' : '不合格处置' : '上报公司质检' }}</a-radio>
        <a-radio :value="false">驳回重检</a-radio>
      </a-radio-group>
      <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填写审批意见" />
    </a-modal>
    <a-modal v-if="props.salesMode" v-model:open="historyOpen" :title="`流程记录 Â· ${active?.code || ''}`" :footer="null">
      <a-timeline v-if="historyActions.length">
        <a-timeline-item v-for="action in historyActions" :key="action.id">
          <strong>{{ actionLabels[action.action] || action.action }}</strong>
          <span> Â· {{ action.createTime || '—' }} Â· æ“ä½œäºº {{ action.operatorId || '—' }}</span>
          <div v-if="action.opinion">{{ action.opinion }}</div>
        </a-timeline-item>
      </a-timeline>
      <a-empty v-else description="暂无流程记录" />
    </a-modal>
  </Page>
</template>
src/views/mes/qc/oqc/modules/form.vue
@@ -105,7 +105,7 @@
    return;
  }
  try {
    await confirm('是否完成出货检验单编制?【完成后将不能更改】');
    await confirm('是否提交出货检验结论?提交后进入审批流程,初检不合格需先复检。');
  } catch {
    return;
  }
@@ -126,7 +126,7 @@
      originalSnapshot.value = current;
    }
    await finishOqc(id);
    message.success('完成成功');
    message.success('检验已提交,请在销售质检菜单继续复检或审批');
    await modalApi.close();
    emit('success');
  } finally {
src/views/mes/qc/salesQuality/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
<script setup lang="ts">
import OqcIndex from '../oqc/index.vue';
</script>
<template>
  <div class="sales-quality-page">
    <div class="flow-notice">
      <strong>销售质检</strong>
      <span>销售出库单初检、不合格复检、销售部与公司质检审批;审批完成后回写出库质量状态。</span>
    </div>
    <OqcIndex sales-mode />
  </div>
</template>
<style scoped>
.sales-quality-page { height: 100%; }
.flow-notice { display: flex; align-items: center; gap: 12px; padding: 12px 20px; margin: 0 0 8px; border: 1px solid #d6e6fa; border-radius: 8px; background: #f2f7fe; color: #385071; }
.flow-notice strong { flex: none; color: #1557a0; }
@media (max-width: 720px) { .flow-notice { align-items: flex-start; flex-direction: column; } }
</style>
src/views/mes/qc/seedQuality/index.vue
@@ -54,17 +54,20 @@
const qualifiedWarehouseId = ref<number>();
const indicators = ref<IndicatorForm[]>([
  { code: 'purity', label: '纯度', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'cleanliness', label: '净度', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'germination_rate', label: '发芽率', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'moisture', label: '水分', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'purity', label: '纯度', unit: '%' },
  { code: 'cleanliness', label: '净度', unit: '%' },
  { code: 'germination_rate', label: '发芽率', unit: '%' },
  { code: 'moisture', label: '水分', unit: '%' },
]);
const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0);
const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0);
const enteredCount = computed(() => indicators.value.filter((item) => item.value !== undefined && item.value !== null).length);
const passedCount = computed(() => indicators.value.filter(isPassed).length);
const allEntered = computed(() => enteredCount.value === indicators.value.length);
const allEntered = computed(() => indicators.value.every((item) =>
  item.value !== undefined && item.value !== null
  && item.minValue !== undefined && item.minValue !== null
  && item.maxValue !== undefined && item.maxValue !== null));
const overallPassed = computed(() => allEntered.value && passedCount.value === indicators.value.length);
const isGroupScan = computed(() => scanType.value !== 'BAG');
const qualifiedWarehouseOptions = computed(() =>
@@ -99,8 +102,8 @@
  reason.value = '';
  indicators.value.forEach((item) => {
    item.value = undefined;
    item.minValue = 0;
    item.maxValue = 100;
    item.minValue = undefined;
    item.maxValue = undefined;
  });
}
@@ -163,7 +166,7 @@
    return;
  }
  if (!allEntered.value) {
    message.warning('请先填写四项指标的实测值');
    message.warning('请填写四项指标的实测值及合格上下限');
    return;
  }
  if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
@@ -187,8 +190,8 @@
    Modal.confirm({
      title: overallPassed.value ? '确认提交合格结果?' : '确认提交不合格结果?',
      content: overallPassed.value
        ? `本次 ${count} è¢‹å°†ç»Ÿä¸€åˆ¤å®šåˆæ ¼ï¼Œå¹¶ä»Žæš‚存库存转入所选合格库。${scopeNote}`
        : `本次 ${count} è¢‹å°†ä»Žæš‚存库存中清除并标记不合格,同时保留不合格原因用于质量分析。${scopeNote}`,
        ? `本次 ${count} è¢‹å°†ç»Ÿä¸€è®°å½•为初检合格,待加工部和公司质检审批后才转入合格库。${scopeNote}`
        : `本次 ${count} è¢‹å°†è®°å½•为初检不合格,保留暂存库存并等待复检。${scopeNote}`,
      okText: '确认提交',
      cancelText: '返回修改',
      okButtonProps: { danger: !overallPassed.value },
@@ -216,7 +219,7 @@
      })),
    });
    submittedCount.value = result.processedCount;
    message.success(`本次 ${result.processedCount} è¢‹å·²æäº¤ï¼š${passed ? '质检合格,已转入所选合格库' : '质检不合格,已记录原因并移除暂存库存'}`);
    message.success(`本次 ${result.processedCount} è¢‹å·²æäº¤ï¼š${passed ? '初检合格,待两级审批' : '初检不合格,待复检'}`);
    if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      gsap.fromTo('.result-card', { scale: 0.97, autoAlpha: 0.5 }, { scale: 1, autoAlpha: 1, duration: 0.35, ease: 'power2.out' });
    }
@@ -263,7 +266,7 @@
      <div>
        <div class="eyebrow">MES / è´¨é‡ç®¡ç†</div>
        <h1>种子质检</h1>
        <p>选择袋码、托盘码或批次码识别待检范围,录入纯度、净度、发芽率和水分,系统自动判定检验结论</p>
        <p>识别待检范围并录入四项指标;合格经加工部与公司质检审批后入库,不合格先复检</p>
      </div>
      <div class="header-badge"><IconifyIcon icon="ep:checked" /> è¢‹ç  / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç </div>
    </header>
@@ -318,14 +321,14 @@
          <a-divider orientation="left">指标结果</a-divider>
          <div class="indicator-list">
            <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: !isPassed(item) }">
            <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: allEntered && !isPassed(item) }">
              <div class="indicator-name"><span class="indicator-dot" :class="isPassed(item) ? 'pass' : 'fail'" /><strong>{{ item.label }}</strong><small>{{ item.unit }}</small></div>
              <a-input-number v-model:value="item.value" class="indicator-input" :disabled="loading" :min="0" :precision="3" placeholder="实测值" />
              <span class="range-separator">范围</span>
              <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="下限" />
              <span>—</span>
              <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="上限" />
              <a-tag :color="isPassed(item) ? 'success' : 'error'">{{ isPassed(item) ? '合格' : '超出范围' }}</a-tag>
              <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? '待录入' : isPassed(item) ? '合格' : '超出范围' }}</a-tag>
            </div>
          </div>
@@ -338,7 +341,7 @@
        <template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />判定说明</div></template>
        <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '当前结果合格' : allEntered ? '当前存在不合格项' : '等待录入指标' }}</strong><span>{{ allEntered ? '实测值需处于最小值与最大值之间' : `已录入 ${enteredCount} / 4 é¡¹å®žæµ‹å€¼` }}</span></div>
        <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>先识别码值再录入指标,四项指标必须全部提交</span></div><div><IconifyIcon icon="ep:check" /><span>边界值按合格处理</span></div><div><IconifyIcon icon="ep:check" /><span>托盘码、批次码的结论应用于本次全部待检袋</span></div><div><IconifyIcon icon="ep:check" /><span>不合格结果需保留原因</span></div><div><IconifyIcon icon="ep:check" /><span>质检员与检验时间由服务端记录</span></div></div>
        <a-alert message="库存流转由后端处理" description="合格袋将从暂存库存转入所选合格库;不合格袋将直接清除暂存库存并保留不合格原因。提交时服务端会重新核对本次待检袋清单,清单变化需重新识别。" type="info" show-icon />
        <a-alert message="审批完成后流转库存" description="初检不改变库存;不合格待复检。复检结论经加工部和公司质检审批后,合格袋才转入合格库,不合格袋才报废。" type="info" show-icon />
        <div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>最近提交质检:<strong>{{ submittedCount }} è¢‹</strong></span></div>
      </a-card>
    </section>
src/views/mes/qc/seedQuality/record.vue
@@ -3,10 +3,19 @@
import type { TablePaginationConfig } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { useAccess } from '@vben/access';
import { message } from 'ant-design-vue';
import {
  getSeedQualityPage,
  getSeedQualityIndicators,
  getSeedQualityActions,
  retestSeedQuality,
  approveSeedQuality,
  approveSeedQualityBatch,
  type SeedQualityRecord,
  type SeedQualityIndicator,
  type SeedQualityAction,
} from '#/api/mes/qc/seedQuality';
const records = ref<SeedQualityRecord[]>([]);
@@ -19,12 +28,157 @@
const result = ref<number>();
const total = ref(0);
const statsTotal = ref(0);
const passTotal = ref(0);
const failTotal = ref(0);
const processingTotal = computed(() =>
  Math.max(total.value - passTotal.value - failTotal.value, 0),
  Math.max(statsTotal.value - passTotal.value - failTotal.value, 0),
);
const { hasAccessByCodes } = useAccess();
const workflowLabels: Record<number, string> = {
  10: '待复检', 20: '待加工部审批', 30: '待公司质检审批',
  40: '已放行', 50: '待加工部不合格评审', 60: '待公司不合格审批', 70: '已报废',
};
const actionLabels: Record<string, string> = {
  INITIAL: '初检', RETEST: '复检', PROCESS_APPROVE: '加工部通过',
  PROCESS_REJECT: '加工部驳回', COMPANY_APPROVE: '公司质检通过',
  COMPANY_REJECT: '公司质检驳回',
};
const indicatorLabels: Record<string, string> = {
  purity: '纯度', cleanliness: '净度', germination_rate: '发芽率', moisture: '水分',
};
function actionIndicators(action: SeedQualityAction) {
  if (!action.indicatorsJson) return [];
  try {
    const parsed: unknown = JSON.parse(action.indicatorsJson);
    return Array.isArray(parsed) ? parsed as Array<{ code: string; value: number; minValue: number; maxValue: number }> : [];
  } catch {
    return [];
  }
}
const historyOpen = ref(false);
const historyActions = ref<SeedQualityAction[]>([]);
async function openHistory(record: SeedQualityRecord) {
  activeRecord.value = record;
  try {
    historyActions.value = await getSeedQualityActions(record.id);
    historyOpen.value = true;
  } catch (error) {
    message.error((error as Error).message || '读取流程记录失败');
  }
}
const activeRecord = ref<SeedQualityRecord>();
const retestOpen = ref(false);
const retestBusy = ref(false);
const retestIndicators = ref<SeedQualityIndicator[]>([]);
const retestReason = ref('');
const reviewOpen = ref(false);
const reviewBusy = ref(false);
const reviewStage = ref<'process' | 'company'>('process');
const reviewApproved = ref(true);
const reviewOpinion = ref('');
const reviewIds = ref<number[]>([]);
const selectedIds = ref<number[]>([]);
const selectedRows = computed(() => records.value.filter((record) => selectedIds.value.includes(record.id)));
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) => [30, 60].includes(record.workflowStatus))) return 'company';
  return undefined;
});
async function openRetest(record: SeedQualityRecord) {
  activeRecord.value = record;
  try {
    retestIndicators.value = await getSeedQualityIndicators(record.id);
    if (retestIndicators.value.length !== 4) throw new Error('质检指标不完整');
    retestReason.value = '';
    retestOpen.value = true;
  } catch (error) {
    message.error((error as Error).message || '读取质检指标失败');
  }
}
async function saveRetest() {
  const record = activeRecord.value;
  if (!record) return;
  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('请填写完整的实测值及有效上下限');
    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;
  }
  retestBusy.value = true;
  try {
    await retestSeedQuality(record.id, {
      code: record.code,
      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 ? '复检合格,待加工部审批' : '复检不合格,待加工部评审');
    retestOpen.value = false;
    refreshAll();
  } catch (error) {
    message.error((error as Error).message || '复检提交失败');
  } finally {
    retestBusy.value = false;
  }
}
function openReview(record: SeedQualityRecord, stage: 'process' | 'company') {
  activeRecord.value = record;
  reviewIds.value = [record.id];
  reviewStage.value = stage;
  reviewApproved.value = true;
  reviewOpinion.value = '';
  reviewOpen.value = true;
}
function openBatchReview() {
  const stage = selectableStage.value;
  if (!stage) return;
  activeRecord.value = selectedRows.value[0];
  reviewIds.value = selectedRows.value.map((row) => row.id);
  reviewStage.value = stage;
  reviewApproved.value = true;
  reviewOpinion.value = '';
  reviewOpen.value = true;
}
async function saveReview() {
  if (!reviewIds.value.length) return;
  if (!reviewOpinion.value.trim()) {
    message.warning('请填写审批意见');
    return;
  }
  reviewBusy.value = true;
  try {
    if (reviewIds.value.length === 1) {
      await approveSeedQuality(reviewStage.value, {
        id: reviewIds.value[0]!, approved: reviewApproved.value, opinion: reviewOpinion.value.trim(),
      });
    } else {
      await approveSeedQualityBatch(reviewStage.value, {
        ids: reviewIds.value, approved: reviewApproved.value, opinion: reviewOpinion.value.trim(),
      });
    }
    message.success(`已完成 ${reviewIds.value.length} æ¡å®¡æ‰¹`);
    reviewOpen.value = false;
    selectedIds.value = [];
    refreshAll();
  } catch (error) {
    message.error((error as Error).message || '审批失败');
  } finally {
    reviewBusy.value = false;
  }
}
const resultLabel = (value: number) =>
  value === 1 ? '合格' : value === 2 ? '不合格' : '处理中';
@@ -46,6 +200,7 @@
      result: result.value,
    });
    records.value = data.list;
    selectedIds.value = [];
    total.value = data.total;
  } finally {
    loading.value = false;
@@ -56,10 +211,10 @@
  try {
    const [all, pass, fail] = await Promise.all([
      getSeedQualityPage({ pageNo: 1, pageSize: 1 }),
      getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 1 }),
      getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 2 }),
      getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 40 }),
      getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 70 }),
    ]);
    total.value = all.total;
    statsTotal.value = all.total;
    passTotal.value = pass.total;
    failTotal.value = fail.total;
  } catch {
@@ -99,7 +254,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" />
@@ -111,19 +266,19 @@
    <section class="stat-grid" aria-label="质检结果汇总">
      <div class="stat-card total">
        <div class="stat-icon"><IconifyIcon icon="ep:document" /></div>
        <div class="stat-main"><span>记录总数</span><strong>{{ total.toLocaleString() }}</strong><small>按袋码计</small></div>
        <div class="stat-main"><span>记录总数</span><strong>{{ statsTotal.toLocaleString() }}</strong><small>按袋码计</small></div>
      </div>
      <div class="stat-card pass">
        <div class="stat-icon"><IconifyIcon icon="ep:success-filled" /></div>
        <div class="stat-main"><span>质检合格</span><strong>{{ passTotal.toLocaleString() }}</strong><small>可正常入合格库</small></div>
        <div class="stat-main"><span>已放行</span><strong>{{ passTotal.toLocaleString() }}</strong><small>已转入合格库</small></div>
      </div>
      <div class="stat-card fail">
        <div class="stat-icon"><IconifyIcon icon="ep:warning-filled" /></div>
        <div class="stat-main"><span>质检不合格</span><strong>{{ failTotal.toLocaleString() }}</strong><small>已标记报废</small></div>
        <div class="stat-main"><span>已报废</span><strong>{{ failTotal.toLocaleString() }}</strong><small>审批完成后处置</small></div>
      </div>
      <div class="stat-card pending">
        <div class="stat-icon"><IconifyIcon icon="ep:clock" /></div>
        <div class="stat-main"><span>处理中</span><strong>{{ processingTotal.toLocaleString() }}</strong><small>待定结论</small></div>
        <div class="stat-main"><span>流程中</span><strong>{{ processingTotal.toLocaleString() }}</strong><small>复检或审批中</small></div>
      </div>
    </section>
@@ -136,6 +291,7 @@
          <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-input v-model:value="code" allow-clear placeholder="袋码" @press-enter="searchRecords">
            <template #prefix><IconifyIcon icon="ep:box" /></template>
          </a-input>
@@ -162,6 +318,7 @@
      <a-table
        :data-source="records"
        :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }"
        :loading="loading"
        :scroll="{ x: 1120 }"
        :pagination="{
@@ -198,6 +355,9 @@
            </span>
          </template>
        </a-table-column>
        <a-table-column key="workflowStatus" title="流程状态" width="190">
          <template #default="{ record }"><a-tag color="blue">{{ workflowLabels[record.workflowStatus] || '未知状态' }}</a-tag></template>
        </a-table-column>
        <a-table-column key="reason" title="不合格原因" data-index="reason" min-width="220" ellipsis>
          <template #default="{ record }">
            <span v-if="record.reason" class="reason-text">{{ record.reason }}</span>
@@ -210,8 +370,49 @@
        <a-table-column key="createTime" title="提交时间" width="190">
          <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template>
        </a-table-column>
        <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="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>
      <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="上限" />
      </div>
      <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">
      <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-group>
      <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填写审批意见" />
    </a-modal>
    <a-modal v-model:open="historyOpen" :title="`流程记录 Â· ${activeRecord?.code || ''}`" :footer="null">
      <a-timeline v-if="historyActions.length">
        <a-timeline-item v-for="action in historyActions" :key="action.id">
          <strong>{{ actionLabels[action.action] || action.action }}</strong>
          <span class="muted"> Â· {{ formatDateTime(action.createTime) }} Â· æ“ä½œäºº {{ action.operatorId || '—' }}</span>
          <div v-if="action.opinion">{{ action.opinion }}</div>
          <div v-for="indicator in actionIndicators(action)" :key="indicator.code" class="muted">
            {{ indicatorLabels[indicator.code] || indicator.code }}:{{ indicator.value }}({{ indicator.minValue }}—{{ indicator.maxValue }})
          </div>
        </a-timeline-item>
      </a-timeline>
      <a-empty v-else description="暂无流程记录" />
    </a-modal>
  </div>
</template>
@@ -255,6 +456,9 @@
.filters .ant-select { width: 128px; }
.filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; }
.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%; }
.review-choice { display: flex; gap: 16px; margin: 10px 0 18px; }
/* ===== å•元格 ===== */
.strong { font-weight: 600; color: #172033; }