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