From 8a9338f2f43bfe936124feaaa22420b4ca86eb84 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 11:31:24 +0800
Subject: [PATCH] feat(qc): 完善质检流程功能

---
 src/views/mes/qc/seedQuality/record.vue |  222 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 213 insertions(+), 9 deletions(-)

diff --git a/src/views/mes/qc/seedQuality/record.vue b/src/views/mes/qc/seedQuality/record.vue
index 00cc702..a03ccd8 100644
--- a/src/views/mes/qc/seedQuality/record.vue
+++ b/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; }

--
Gitblit v1.9.3