From 1ed040591afdcbe3c5df42ad5e02e2cfa56edd87 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 20 十月 2026 11:19:20 +0800
Subject: [PATCH] feat(mobile): 优化移动端质检记录界面功能
---
src/views/mes/qc/seedQuality/record.vue | 506 ++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 433 insertions(+), 73 deletions(-)
diff --git a/src/views/mes/qc/seedQuality/record.vue b/src/views/mes/qc/seedQuality/record.vue
index 15396a1..70f832d 100644
--- a/src/views/mes/qc/seedQuality/record.vue
+++ b/src/views/mes/qc/seedQuality/record.vue
@@ -4,7 +4,10 @@
import type { TablePaginationConfig } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { useAccess } from '@vben/access';
-import { message } from 'ant-design-vue';
+import { MesWmWarehouseTypeEnum } from '@vben/constants';
+import { message, Modal } from 'ant-design-vue';
+import type { MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
+import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import {
getSeedQualityPage,
@@ -12,11 +15,15 @@
getSeedQualityActions,
getSeedIndicatorRanges,
retestSeedQuality,
+ retestSeedQualityBatch,
resolveSeedRetestScope,
submitSeedRetestScope,
approveSeedQuality,
approveSeedQualityBatch,
+ resolveSeedQuality,
+ submitSeedQualityGroup,
type SeedQualityRecord,
+ type SeedQualityScan,
type SeedQualityIndicator,
type SeedQualityAction,
type SeedRetestScope,
@@ -30,6 +37,7 @@
const code = ref('');
const batchCode = ref('');
const result = ref<number>();
+const workflowStatus = ref<number>();
const total = ref(0);
const statsTotal = ref(0);
@@ -40,6 +48,10 @@
Math.max(statsTotal.value - passTotal.value - failTotal.value, 0),
);
const { hasAccessByCodes } = useAccess();
+const canScopeAction = computed(() => [
+ 'mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest',
+ 'mes:qc-seed-quality:process-approve', 'mes:qc-seed-quality:company-approve',
+].some((code) => hasAccessByCodes([code])));
const workflowLabels: Record<number, string> = {
10: '寰呭妫�', 20: '寰呭垵妫�瀹℃牳', 25: '寰呭垵妫�涓嶅悎鏍煎鐞�', 30: '寰呭妫�瀹℃牳',
40: '宸叉斁琛�', 50: '鍒濇椹冲洖寰呴噸妫�', 60: '寰呭妫�涓嶅悎鏍煎鐞�', 70: '宸叉姤搴�',
@@ -52,6 +64,11 @@
const indicatorLabels: Record<string, string> = {
purity: '绾害', cleanliness: '鍑�搴�', germination_rate: '鍙戣娊鐜�', moisture: '姘村垎',
};
+function actionColor(action: string) {
+ if (action.includes('REJECT')) return 'red';
+ if (action.includes('APPROVE')) return 'green';
+ return 'blue';
+}
function actionIndicators(action: SeedQualityAction) {
if (!action.indicatorsJson) return [];
try {
@@ -75,32 +92,96 @@
const activeRecord = ref<SeedQualityRecord>();
const retestOpen = ref(false);
const retestBusy = ref(false);
+const retestBatchIds = ref<number[]>([]);
const retestIndicators = ref<SeedQualityIndicator[]>([]);
const retestReason = ref('');
const scopeOpen = ref(false);
const scopeType = ref<'PALLET' | 'BATCH'>('PALLET');
const scopeCode = ref('');
+const scopeError = ref('');
+const initialScope = ref<SeedQualityScan>();
+const initialIndicators = ref<SeedQualityIndicator[]>([]);
+const initialReason = ref('');
+const qualifiedWarehouses = ref<MesWmWarehouseApi.Warehouse[]>([]);
+const qualifiedWarehouseId = ref<number>();
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 scopePendingCount = computed(() => (retestScope.value?.pendingProcessCount || 0) + (retestScope.value?.pendingCompanyCount || 0));
+const initialIds = computed(() => initialScope.value?.pendingBagIds || []);
+const initialSubmitIds = computed(() => initialIds.value.slice(0, 500));
+const scopeSubmitIds = computed(() => scopeIds.value.slice(0, 500));
+const initialPassed = computed(() => initialIndicators.value.length === 4 && initialIndicators.value.every((item) =>
+ item.value !== undefined && item.minValue !== undefined && item.maxValue !== undefined
+ && Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)));
+const qualifiedWarehouseOptions = computed(() => qualifiedWarehouses.value.map((warehouse) => ({
+ label: warehouse.code ? `${warehouse.name}锛�${warehouse.code}锛塦 : warehouse.name || '',
+ value: warehouse.id ?? 0,
+})).filter((item) => item.value));
+const scopeReviewGroups = computed(() => {
+ const scope = retestScope.value;
+ if (!scope) return [];
+ return [
+ { status: 20, stage: 'process' as const, ids: scope.pendingProcessIds || [] },
+ { status: 25, stage: 'process' as const, ids: scope.pendingInitialFailIds || [] },
+ { status: 30, stage: 'company' as const, ids: scope.pendingCompanyIds || [] },
+ { status: 60, stage: 'company' as const, ids: scope.pendingCompanyFailIds || [] },
+ ].filter((group) => group.ids.length && hasAccessByCodes([`mes:qc-seed-quality:${group.stage === 'process' ? 'process-approve' : 'company-approve'}`]));
+});
+const scopeHasReviewPermission = computed(() => {
+ const scope = retestScope.value;
+ if (!scope) return false;
+ return (scope.pendingProcessCount > 0 && hasAccessByCodes(['mes:qc-seed-quality:process-approve']))
+ || (scope.pendingCompanyCount > 0 && hasAccessByCodes(['mes:qc-seed-quality:company-approve']));
+});
const reviewOpen = ref(false);
const reviewBusy = ref(false);
+const reviewFromScope = 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, 25].includes(record.workflowStatus))) return 'process';
- if (selectedRows.value.every((record) => [30, 60].includes(record.workflowStatus))) return 'company';
- return undefined;
+const selectedRecordMap = ref(new Map<number, SeedQualityRecord>());
+const selectedRows = computed(() => selectedIds.value.map((id) => selectedRecordMap.value.get(id)).filter((row): row is SeedQualityRecord => !!row));
+const reviewGroups = computed(() => [20, 25, 30, 60].map((status) => ({
+ status,
+ rows: selectedRows.value.filter((row) => row.workflowStatus === status),
+ stage: status === 20 || status === 25 ? 'process' as const : 'company' as const,
+})).filter((group) => group.rows.length && hasAccessByCodes([`mes:qc-seed-quality:${group.stage === 'process' ? 'process-approve' : 'company-approve'}`])));
+const retestGroups = computed(() => {
+ if (!hasAccessByCodes(['mes:qc-seed-quality:retest'])) return [];
+ const groups = new Map<string, { status: 10 | 50; batchCode: string; rows: SeedQualityRecord[] }>();
+ for (const row of selectedRows.value) {
+ if (row.workflowStatus !== 10 && row.workflowStatus !== 50) continue;
+ if (!row.batchCode) continue;
+ const key = `${row.workflowStatus}:${row.batchCode}`;
+ if (!groups.has(key)) groups.set(key, { status: row.workflowStatus, batchCode: row.batchCode, rows: [] });
+ groups.get(key)!.rows.push(row);
+ }
+ return [...groups.values()];
});
+function changeSelection(keys: (string | number)[]) {
+ const nextIds = keys.map(Number);
+ const nextMap = new Map(selectedRecordMap.value);
+ for (const row of records.value) if (nextIds.includes(row.id)) nextMap.set(row.id, row);
+ for (const id of nextMap.keys()) if (!nextIds.includes(id)) nextMap.delete(id);
+ selectedRecordMap.value = nextMap;
+ selectedIds.value = nextIds;
+}
+function clearSelection() {
+ selectedIds.value = [];
+ selectedRecordMap.value = new Map();
+}
+function removeSelection(ids: number[]) {
+ const processed = new Set(ids);
+ selectedIds.value = selectedIds.value.filter((id) => !processed.has(id));
+ selectedRecordMap.value = new Map([...selectedRecordMap.value].filter(([id]) => !processed.has(id)));
+}
async function openRetest(record: SeedQualityRecord) {
+ retestBatchIds.value = [];
activeRecord.value = record;
try {
const previous = await getSeedQualityIndicators(record.id);
@@ -117,36 +198,122 @@
message.error((error as Error).message || '璇诲彇璐ㄦ鎸囨爣澶辫触');
}
}
-function openScopeRetest() {
- scopeOpen.value = true; retestScope.value = undefined; scopeCode.value = ''; retestReason.value = '';
+async function openBatchRetest(group: { rows: SeedQualityRecord[] }) {
+ if (group.rows.length > 500) { message.warning('姣忔鏈�澶氬鐞� 500 鏉★紝璇风缉灏忛�夋嫨鑼冨洿'); return; }
+ const first = group.rows[0];
+ if (!first) return;
+ await openRetest(first);
+ if (retestOpen.value) retestBatchIds.value = group.rows.map((row) => row.id);
+}
+async function openScopeRetest() {
+ scopeOpen.value = true; retestScope.value = undefined; initialScope.value = undefined;
+ scopeCode.value = ''; scopeError.value = ''; retestReason.value = ''; initialReason.value = ''; qualifiedWarehouseId.value = undefined;
+ try {
+ qualifiedWarehouses.value = (await getWarehouseSimpleList()).filter((warehouse) =>
+ warehouse.type === MesWmWarehouseTypeEnum.QUALIFIED && !warehouse.frozen);
+ if (qualifiedWarehouses.value.length === 1) qualifiedWarehouseId.value = qualifiedWarehouses.value[0]?.id;
+ } catch { qualifiedWarehouses.value = []; }
+}
+function scopeErrorText(error: unknown) {
+ const response = error as { msg?: string; message?: string; data?: { msg?: string }; response?: { data?: { msg?: string } } };
+ return response.response?.data?.msg || response.data?.msg || response.msg || response.message || '璇嗗埆澶辫触锛岃鏍稿鐮佸�煎悗閲嶈瘯';
+}
+async function hydrateScopeReviewIds(scope: SeedRetestScope, scannedCode: string) {
+ const loadedCount = (scope.pendingProcessIds?.length || 0) + (scope.pendingInitialFailIds?.length || 0)
+ + (scope.pendingCompanyIds?.length || 0) + (scope.pendingCompanyFailIds?.length || 0);
+ if (loadedCount >= scope.pendingProcessCount + scope.pendingCompanyCount) return;
+
+ const resolvedBatchCode = scope.batchCode || (scope.scanType === 'BATCH' ? scope.code : undefined);
+ if (!resolvedBatchCode) return;
+ const statuses = [20, 25, 30, 60] as const;
+ const pages = await Promise.all(statuses.map((status) => getSeedQualityPage({
+ pageNo: 1,
+ pageSize: 500,
+ batchCode: resolvedBatchCode,
+ workflowStatus: status,
+ }).catch(() => ({ list: [] as SeedQualityRecord[], total: 0 }))));
+ const scopeCodes = new Set([scope.code, scannedCode]);
+ const idsByStatus = pages.map(({ list }) => list
+ .filter((row) => scope.scanType === 'BATCH' || (!!row.palletCode && scopeCodes.has(row.palletCode)))
+ .map((row) => row.id));
+ scope.pendingProcessIds = idsByStatus[0];
+ scope.pendingInitialFailIds = idsByStatus[1];
+ scope.pendingCompanyIds = idsByStatus[2];
+ scope.pendingCompanyFailIds = idsByStatus[3];
}
async function identifyScope() {
- if (!scopeCode.value.trim()) { message.warning('璇锋壂鎻忔垨杈撳叆鎵樼洏鐮� / 鐢熶骇鎵规鐮�'); return; }
- scopeBusy.value = true; retestScope.value = undefined;
+ scopeError.value = '';
+ if (!scopeCode.value.trim()) { scopeError.value = `璇锋壂鎻忔垨杈撳叆${scopeType.value === 'PALLET' ? '鎵樼洏鐮�' : '鐢熶骇鎵规鐮�'}`; return; }
+ scopeBusy.value = true; retestScope.value = undefined; initialScope.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}锛岃鍏堝畬鎴愬搴斿鏍告垨澶勭悊`
- : '褰撳墠鑼冨洿娌℃湁寰呭妫�鎴栧緟閲嶆柊鍒濇鐨勮鐮�');
+ const scannedCode = scopeCode.value.trim();
+ let data: SeedRetestScope;
+ try {
+ data = await resolveSeedRetestScope(scannedCode, scopeType.value);
+ } catch (error) {
+ if (!scopeErrorText(error).startsWith('鏈壘鍒�')) throw error;
+ const otherType = scopeType.value === 'PALLET' ? 'BATCH' : 'PALLET';
+ try {
+ data = await resolveSeedRetestScope(scannedCode, otherType);
+ scopeType.value = otherType;
+ } catch (otherError) {
+ if (scopeErrorText(otherError).startsWith('鏈壘鍒�')) {
+ throw new Error('鏈壘鍒版墭鐩樼爜鎴栫敓浜ф壒娆$爜锛岃鏍稿瀹屾暣鐮佸��');
+ }
+ throw otherError;
+ }
}
+ await hydrateScopeReviewIds(data, scannedCode);
retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50;
+ initialScope.value = await resolveSeedQuality(scannedCode, data.scanType);
const ranges = await getSeedIndicatorRanges().catch(() => []);
- retestIndicators.value = Object.entries(indicatorLabels).map(([indicatorCode]) => {
+ const emptyIndicators = () => 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 };
});
+ retestIndicators.value = emptyIndicators();
+ initialIndicators.value = emptyIndicators();
retestReason.value = '';
- } catch (error) { message.error((error as Error).message || '璇嗗埆澶辫触'); }
+ initialReason.value = '';
+ } catch (error) { scopeError.value = scopeErrorText(error); }
+ finally { scopeBusy.value = false; }
+}
+async function saveScopeInitial() {
+ const target = initialScope.value;
+ if (!target || !target.inbound || !initialSubmitIds.value.length) return;
+ if (!qualifiedWarehouseId.value) { scopeError.value = '璇烽�夋嫨妫�楠屽悎鏍煎悗鍏ュ簱鐨勫悎鏍煎簱'; return; }
+ if (initialIndicators.value.some((item) => item.value === undefined || item.minValue === undefined || item.maxValue === undefined
+ || Number(item.value) < 0 || Number(item.value) > 100 || Number(item.minValue) < 0 || Number(item.maxValue) > 100
+ || Number(item.minValue) > Number(item.maxValue))) {
+ scopeError.value = '璇峰~鍐欏洓椤规寚鏍囩殑瀹炴祴鍊煎拰 0鈥�100 鑼冨洿鍐呯殑鍚堟牸涓婁笅闄�'; return;
+ }
+ if (!initialPassed.value && !initialReason.value.trim()) { scopeError.value = '鍒濇涓嶅悎鏍兼椂璇峰~鍐欏師鍥�'; return; }
+ scopeError.value = '';
+ const confirmed = await new Promise<boolean>((resolve) => Modal.confirm({
+ title: `纭鎵归噺鍒濇 ${initialSubmitIds.value.length} 琚嬶紵`,
+ content: `鍚屼竴缁勬寚鏍囦笌${initialPassed.value ? '鍚堟牸' : '涓嶅悎鏍�'}缁撹灏嗗簲鐢ㄤ簬鏈琚嬬爜銆傚畬鎴愬悗浼氳嚜鍔ㄥ姞杞藉墿浣欒褰曘�俙,
+ okText: '纭鎻愪氦鍒濇', cancelText: '杩斿洖鏍稿',
+ onOk: () => resolve(true), onCancel: () => resolve(false),
+ }));
+ if (!confirmed) return;
+ scopeBusy.value = true;
+ try {
+ const submitted = await submitSeedQualityGroup({
+ code: target.code, batchCode: target.batchCode, scanType: target.scanType || scopeType.value,
+ bagIds: initialSubmitIds.value, qualifiedWarehouseId: qualifiedWarehouseId.value,
+ reason: initialReason.value.trim() || undefined,
+ indicators: initialIndicators.value.map((item) => ({ code: item.indicatorCode, value: Number(item.value),
+ minValue: Number(item.minValue), maxValue: Number(item.maxValue) })),
+ });
+ message.success(`宸插畬鎴� ${submitted.processedCount} 琚嬪垵妫�`);
+ refreshAll();
+ await identifyScope();
+ } catch (error) { scopeError.value = `${scopeErrorText(error)}銆傝閲嶆柊璇嗗埆骞舵牳瀵瑰緟妫�娓呭崟`; initialScope.value = undefined; }
finally { scopeBusy.value = false; }
}
async function saveScopeRetest() {
- if (!retestScope.value || !scopeIds.value.length) return;
+ if (!retestScope.value || !scopeSubmitIds.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;
@@ -156,11 +323,13 @@
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 => ({
+ qualityIds: scopeSubmitIds.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; }
+ message.success(`宸插畬鎴� ${result.processedCount} 琚嬭川妫�`);
+ refreshAll();
+ await identifyScope();
+ } catch (error) { scopeError.value = `${scopeErrorText(error)}銆傝閲嶆柊璇嗗埆骞舵牳瀵瑰緟妫�娓呭崟`; retestScope.value = undefined; }
finally { scopeBusy.value = false; }
}
async function saveRetest() {
@@ -187,12 +356,16 @@
minValue: Number(item.minValue), maxValue: Number(item.maxValue),
})),
};
- await retestSeedQuality(record.id, { ...payload, code: record.code });
- message.success(record.workflowStatus === 50
+ if (retestBatchIds.value.length) {
+ await retestSeedQualityBatch({ ids: retestBatchIds.value, ...payload });
+ } else {
+ await retestSeedQuality(record.id, { ...payload, code: record.code });
+ }
+ message.success(`${retestBatchIds.value.length || 1} 琚嬪凡鎻愪氦${record.workflowStatus === 50 ? '閲嶆柊鍒濇' : '澶嶆'}锛�${record.workflowStatus === 50
? passed ? '閲嶆柊鍒濇鍚堟牸锛屽緟鍒濇瀹℃牳' : '閲嶆柊鍒濇涓嶅悎鏍硷紝寰呬笉鍚堟牸澶勭悊'
- : passed ? '澶嶆鍚堟牸锛屽緟澶嶆瀹℃牳' : '澶嶆涓嶅悎鏍硷紝寰呬笉鍚堟牸澶勭悊');
+ : passed ? '澶嶆鍚堟牸锛屽緟澶嶆瀹℃牳' : '澶嶆涓嶅悎鏍硷紝寰呬笉鍚堟牸澶勭悊'}`);
retestOpen.value = false;
- selectedIds.value = [];
+ removeSelection(retestBatchIds.value.length ? retestBatchIds.value : [record.id]);
refreshAll();
} catch (error) {
message.error((error as Error).message || '澶嶆鎻愪氦澶辫触');
@@ -201,6 +374,7 @@
}
}
function openReview(record: SeedQualityRecord, stage: 'process' | 'company') {
+ reviewFromScope.value = false;
activeRecord.value = record;
reviewIds.value = [record.id];
reviewStage.value = stage;
@@ -208,12 +382,23 @@
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;
+function openBatchReview(group: { stage: 'process' | 'company'; rows: SeedQualityRecord[] }) {
+ if (group.rows.length > 500) { message.warning('姣忔鏈�澶氬鐞� 500 鏉★紝璇风缉灏忛�夋嫨鑼冨洿'); return; }
+ reviewFromScope.value = false;
+ activeRecord.value = group.rows[0];
+ reviewIds.value = group.rows.map((row) => row.id);
+ reviewStage.value = group.stage;
+ reviewApproved.value = true;
+ reviewOpinion.value = '';
+ reviewOpen.value = true;
+}
+function openScopeReview(group: { status: number; stage: 'process' | 'company'; ids: number[] }) {
+ const scope = retestScope.value;
+ if (!scope || !group.ids.length) return;
+ reviewFromScope.value = true;
+ activeRecord.value = { id: group.ids[0]!, code: scope.code, result: 0, workflowStatus: group.status };
+ reviewIds.value = group.ids.slice(0, 500);
+ reviewStage.value = group.stage;
reviewApproved.value = true;
reviewOpinion.value = '';
reviewOpen.value = true;
@@ -237,10 +422,12 @@
}
message.success(`宸插畬鎴� ${reviewIds.value.length} 鏉″鎵筦);
reviewOpen.value = false;
- selectedIds.value = [];
+ removeSelection(reviewIds.value);
refreshAll();
+ if (reviewFromScope.value) await identifyScope();
+ reviewFromScope.value = false;
} catch (error) {
- message.error((error as Error).message || '瀹℃壒澶辫触');
+ message.error(scopeErrorText(error));
} finally {
reviewBusy.value = false;
}
@@ -264,9 +451,9 @@
code: code.value.trim() || undefined,
batchCode: batchCode.value.trim() || undefined,
result: result.value,
+ workflowStatus: workflowStatus.value,
});
records.value = data.list;
- selectedIds.value = [];
total.value = data.total;
} finally {
loading.value = false;
@@ -290,6 +477,7 @@
function searchRecords() {
page.value = 1;
+ clearSelection();
void loadRecords();
}
@@ -297,11 +485,16 @@
code.value = '';
batchCode.value = '';
result.value = undefined;
+ workflowStatus.value = undefined;
searchRecords();
}
function refreshAll() {
void Promise.all([loadRecords(), loadStats()]);
+}
+function refreshAndClearSelection() {
+ clearSelection();
+ refreshAll();
}
function handleTableChange(pagination: TablePaginationConfig) {
@@ -348,6 +541,20 @@
</div>
</section>
+ <!-- 绉诲姩绔皢鎵归噺鍏ュ彛鍓嶇疆锛岄伩鍏嶉殣钘忓湪杈冮暱鐨勭瓫閫夊尯涓� -->
+ <section v-if="canScopeAction" class="mobile-batch-entry" aria-label="鎵归噺璐ㄦ鎿嶄綔">
+ <div class="mobile-batch-copy">
+ <span class="mobile-batch-icon"><IconifyIcon icon="ep:full-screen" /></span>
+ <div>
+ <strong>鎵归噺妫�楠屼笌瀹℃牳</strong>
+ <p>鎵弿鎴栬緭鍏ユ墭鐩樼爜銆佺敓浜ф壒娆$爜锛屼竴娆″鐞嗗綋鍓嶈寖鍥村唴鐨勮川妫�璁板綍銆�</p>
+ </div>
+ </div>
+ <a-button type="primary" size="large" block @click="openScopeRetest">
+ 鎵撳紑鎵归噺鎿嶄綔
+ </a-button>
+ </section>
+
<!-- 璁板綍鍒楄〃 -->
<section class="record-card">
<div class="card-toolbar">
@@ -357,8 +564,7 @@
<span class="count-chip">{{ total.toLocaleString() }} 鏉�</span>
</div>
<div class="filters">
- <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-button v-if="canScopeAction" class="desktop-scope-action" type="primary" @click="openScopeRetest">鎸夋墭鐩� / 鎵规鎵归噺澶勭悊</a-button>
<a-input v-model:value="code" allow-clear placeholder="琚嬬爜" @press-enter="searchRecords">
<template #prefix><IconifyIcon icon="ep:box" /></template>
</a-input>
@@ -375,17 +581,43 @@
{ label: '澶勭悊涓�', value: 0 },
]"
/>
+ <a-select
+ v-model:value="workflowStatus"
+ allow-clear
+ placeholder="娴佺▼鐘舵��"
+ :options="Object.entries(workflowLabels).map(([value, label]) => ({ label, value: Number(value) }))"
+ />
<a-button type="primary" @click="searchRecords"><IconifyIcon icon="ep:search" />鏌ヨ</a-button>
<a-button @click="resetFilters"><IconifyIcon icon="ep:refresh-left" />閲嶇疆</a-button>
<a-tooltip title="鍒锋柊鏁版嵁">
- <a-button :loading="loading" aria-label="鍒锋柊鏁版嵁" @click="refreshAll"><IconifyIcon icon="ep:refresh" /></a-button>
+ <a-button :loading="loading" aria-label="鍒锋柊鏁版嵁" @click="refreshAndClearSelection"><IconifyIcon icon="ep:refresh" /></a-button>
</a-tooltip>
+ </div>
+ </div>
+
+ <div v-if="selectedIds.length" class="selection-bar" aria-live="polite">
+ <div class="selection-summary">
+ <strong>宸查�� {{ selectedIds.length }} 鏉�</strong>
+ <span>鎸夋祦绋嬬姸鎬佸拰鐢熶骇鎵规鍒楀嚭鍙墽琛屾搷浣滐紱鍏朵粬璁板綍缁х画淇濈暀閫夋嫨銆�</span>
+ <a-button type="link" @click="clearSelection">娓呯┖閫夋嫨</a-button>
+ </div>
+ <div class="selection-actions">
+ <a-button
+ v-for="group in reviewGroups" :key="`review-${group.status}`"
+ @click="openBatchReview(group)"
+ >{{ workflowLabels[group.status] }} 路 澶勭悊 {{ group.rows.length }} 鏉�</a-button>
+ <a-button
+ v-for="group in retestGroups"
+ :key="`retest-${group.status}-${group.batchCode}`"
+ @click="openBatchRetest(group)"
+ >{{ group.status === 50 ? '閲嶆柊鍒濇' : '澶嶆' }} 路 {{ group.batchCode }} 路 {{ group.rows.length }} 琚�</a-button>
+ <span v-if="!reviewGroups.length && !retestGroups.length" class="muted">鎵�閫夎褰曞綋鍓嶆病鏈夊彲鎵归噺澶勭悊鐨勬搷浣滐紝璇锋煡鐪嬫祦绋嬬姸鎬佹垨鍗曟潯鎿嶄綔銆�</span>
</div>
</div>
<a-table
:data-source="records"
- :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }"
+ :row-selection="{ selectedRowKeys: selectedIds, preserveSelectedRowKeys: true, onChange: changeSelection }"
:loading="loading"
:scroll="{ x: 1900 }"
:pagination="{
@@ -452,8 +684,8 @@
</a-table-column>
</a-table>
</section>
- <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>
+ <a-modal v-model:open="retestOpen" wrap-class-name="seed-quality-modal" :title="activeRecord?.workflowStatus === 50 ? '閲嶆柊鍒濇' : '绉嶅瓙璐ㄦ澶嶆'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? '鎻愪氦鍒濇' : '鎻愪氦澶嶆'" @ok="saveRetest">
+ <p class="muted">{{ retestBatchIds.length ? `鏈澶勭悊 ${retestBatchIds.length} 琚嬶紝鍚屼竴缁勬寚鏍囦笌缁撹浼氬簲鐢ㄤ簬鍏ㄩ儴鎵�閫夎鐮併�俙 : `琚嬬爜锛�${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="鏈瀹炴祴鍊�" />
@@ -462,43 +694,95 @@
</div>
<a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="鏈妫�楠屼笉鍚堟牸鏃跺~鍐欏師鍥�" />
</a-modal>
- <a-modal v-model:open="scopeOpen" title="鎸夋墭鐩� / 鐢熶骇鎵规鎵归噺澶嶆鎴栭噸鏂板垵妫�" :confirm-loading="scopeBusy" ok-text="鎻愪氦鏈樁娈靛叏閮ㄨ鐮�" :ok-button-props="{ disabled: !scopeIds.length }" @ok="saveScopeRetest">
+ <a-modal v-model:open="scopeOpen" wrap-class-name="seed-quality-modal seed-quality-scope-modal" title="鎸夋墭鐩� / 鐢熶骇鎵规鎵归噺璐ㄦ涓庡鏍�" width="min(760px, calc(100vw - 24px))" :footer="null" :body-style="{ maxHeight: '70vh', overflowY: 'auto' }">
<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-select v-model:value="scopeType" style="width: 130px" :options="[{ label: '鎵樼洏鐮�', value: 'PALLET' }, { label: '鐢熶骇鎵规鐮�', value: 'BATCH' }]" @change="retestScope = undefined; initialScope = undefined; scopeError = ''" />
+ <a-input-search v-model:value="scopeCode" :placeholder="scopeType === 'PALLET' ? '鎵弿鎴栬緭鍏ュ畬鏁存墭鐩樼爜' : '鎵弿鎴栬緭鍏ュ畬鏁寸敓浜ф壒娆$爜'" enter-button="璇嗗埆" :loading="scopeBusy" @change="retestScope = undefined; initialScope = undefined; scopeError = ''" @search="identifyScope" />
</a-space>
+ <a-alert v-if="scopeError" class="scope-error" type="error" show-icon :message="scopeError" />
<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="涓嶅悎鏍兼椂濉啓鍘熷洜" />
+ <p>宸茶瘑鍒珄{ retestScope.scanType === 'PALLET' ? '鎵樼洏鐮�' : '鐢熶骇鎵规鐮�' }} {{ retestScope.code }} 路 鏈夋晥琚� {{ retestScope.totalCount }} 路 鐢熶骇鎵规 {{ retestScope.batchCode || '鈥�' }}</p>
+ <section class="scope-section">
+ <h3>鎵归噺鍒濇 <a-tag>{{ initialIds.length }} 琚嬪緟妫�</a-tag></h3>
+ <template v-if="initialIds.length">
+ <a-alert v-if="!initialScope?.inbound" type="warning" show-icon message="璇ヨ寖鍥寸殑鏈夋晥琚嬬爜灏氭湭鍏ㄩ儴鍏ュ簱锛屾殏涓嶈兘鎻愪氦鍒濇" />
+ <template v-else-if="hasAccessByCodes(['mes:qc-seed-quality:submit'])">
+ <p class="muted">鍥涢」鎸囨爣鍜岀粨璁轰細搴旂敤浜庢湰娆� {{ initialSubmitIds.length }} 琚嬶紱瓒呰繃 500 琚嬫椂绯荤粺浼氬垎鎵瑰鐞嗗苟鑷姩鍔犺浇鍓╀綑璁板綍銆�</p>
+ <a-form-item label="妫�楠屽悎鏍煎悗鍏ュ簱鐨勫悎鏍煎簱" required>
+ <a-select v-model:value="qualifiedWarehouseId" :options="qualifiedWarehouseOptions" placeholder="璇烽�夋嫨鍚堟牸搴�" style="width: 100%" />
+ </a-form-item>
+ <a-alert v-if="!qualifiedWarehouseOptions.length" type="warning" show-icon message="鏆傛棤鍙敤鍚堟牸搴擄紝璇疯仈绯荤鐞嗗憳閰嶇疆" />
+ <div v-for="item in initialIndicators" :key="`initial-${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="initialReason" :rows="2" :maxlength="500" :placeholder="initialPassed ? '鍚堟牸缁撴灉鏃犻渶濉啓鍘熷洜' : '鍒濇涓嶅悎鏍兼椂濉啓鍘熷洜'" />
+ <div class="scope-actions"><a-button type="primary" :loading="scopeBusy" @click="saveScopeInitial">鎻愪氦鏈 {{ initialSubmitIds.length }} 琚嬪垵妫�</a-button></div>
+ </template>
+ <p v-else class="muted">褰撳墠璐﹀彿娌℃湁鍒濇鎻愪氦鏉冮檺銆�</p>
+ </template>
+ <p v-else class="muted">褰撳墠鑼冨洿娌℃湁寰呭垵妫�琚嬬爜銆�</p>
+ </section>
+ <section v-if="scopePendingCount" class="scope-section">
+ <h3>鎵归噺瀹℃牳 <a-tag color="orange">{{ scopePendingCount }} 鏉″緟澶勭悊</a-tag></h3>
+ <p class="muted">鎸夊鐞嗙粨鏋滃垎缁勫鏍革紝瀹屾垚鍚庝細鑷姩鏇存柊涓嬫柟鍙妫�琚嬫暟銆�</p>
+ <div v-if="scopeReviewGroups.length" class="scope-actions">
+ <a-button v-for="group in scopeReviewGroups" :key="`scope-review-${group.status}`" :disabled="scopeBusy" @click="openScopeReview(group)">
+ {{ workflowLabels[group.status] }} 路 鏈 {{ Math.min(group.ids.length, 500) }} 鏉�<span v-if="group.ids.length > 500"> / 鍏� {{ group.ids.length }} 鏉�</span>
+ </a-button>
+ </div>
+ <p v-else-if="scopeHasReviewPermission" class="muted">寰呭鏍告竻鍗曟殏鏈姞杞芥垚鍔燂紝璇烽噸鏂拌瘑鍒紱涔熷彲浠ュ湪涓嬫柟鍒楄〃鍕鹃�夎褰曞悗鎵归噺瀹℃牳銆�</p>
+ <p v-else class="muted">褰撳墠璐﹀彿娌℃湁璇ラ樁娈电殑瀹℃牳鏉冮檺锛岃鐢卞搴斿鏍镐汉澶勭悊銆�</p>
+ </section>
+ <section v-if="retestScope.retestIds.length || retestScope.initialRecheckIds.length" class="scope-section">
+ <h3>鎵归噺澶嶆 / 閲嶆柊鍒濇</h3>
+ <template v-if="hasAccessByCodes(['mes:qc-seed-quality:retest'])">
+ <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">鍚屼竴缁勬寚鏍囧皢搴旂敤浜庢湰娆� {{ scopeSubmitIds.length }} 琚嬶紱瓒呰繃 500 琚嬫椂瀹屾垚鍚庣户缁鐞嗗墿浣欒褰曘�傛ā鏉胯寖鍥村彲浠ヤ慨鏀广��</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="2" :maxlength="500" placeholder="涓嶅悎鏍兼椂濉啓鍘熷洜" />
+ <div class="scope-actions"><a-button type="primary" :loading="scopeBusy" @click="saveScopeRetest">鎻愪氦鏈 {{ scopeSubmitIds.length }} 琚嬫楠�</a-button></div>
+ </template>
+ <p v-else class="muted">褰撳墠璐﹀彿娌℃湁澶嶆鏉冮檺銆�</p>
+ </section>
+ <a-alert v-if="!initialIds.length && !scopePendingCount && !retestScope.retestIds.length && !retestScope.initialRecheckIds.length" type="success" show-icon message="褰撳墠鑼冨洿娌℃湁寰呭鐞嗙殑璐ㄦ璁板綍" />
</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-modal v-model:open="reviewOpen" wrap-class-name="seed-quality-modal" :title="[25, 60].includes(activeRecord?.workflowStatus ?? 0) ? '涓嶅悎鏍煎鐞�' : reviewStage === 'process' ? '鍒濇瀹℃牳' : '澶嶆瀹℃牳'" :confirm-loading="reviewBusy" :ok-text="reviewApproved ? '纭澶勭悊' : '纭椹冲洖'" @ok="saveReview">
+ <p>{{ reviewFromScope || 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 === 'process' ? activeRecord?.workflowStatus === 25 ? '纭涓嶅悎鏍硷紝瀹夋帓澶嶆' : '閫氳繃锛岃繘鍏ュ妫�' : activeRecord?.workflowStatus === 30 ? '閫氳繃锛屾斁琛屽叆搴�' : '纭涓嶅悎鏍硷紝鎶ュ簾澶勭疆' }}</a-radio>
<a-radio :value="false">{{ reviewStage === 'process' ? '椹冲洖锛岄噸鏂板垵妫�' : '椹冲洖锛岄噸鏂板妫�' }}</a-radio>
</a-radio-group>
+ <a-alert v-if="reviewIds.length > 1" type="info" show-icon :message="reviewApproved ? reviewStage === 'company' ? activeRecord?.workflowStatus === 60 ? `纭鍚� ${reviewIds.length} 琚嬪皢鎶ュ簾` : `纭鍚� ${reviewIds.length} 琚嬪皢鏀捐鍏ュ簱` : `纭鍚� ${reviewIds.length} 琚嬪皢杩涘叆澶嶆` : `椹冲洖鍚� ${reviewIds.length} 琚嬪皢杩斿洖妫�楠宍" />
<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">
+ <a-modal v-model:open="historyOpen" wrap-class-name="seed-quality-modal seed-quality-history-modal"
+ :title="`娴佺▼璁板綍 路 ${activeRecord?.code || ''}`" :footer="null"
+ width="min(560px, calc(100vw - 24px))" :body-style="{ maxHeight: '70vh', overflowY: 'auto' }">
+ <a-timeline v-if="historyActions.length" class="history-timeline">
<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 class="history-head">
+ <a-tag :color="actionColor(action.action)">{{ actionLabels[action.action] || action.action }}</a-tag>
+ <span class="history-meta">{{ formatDateTime(action.createTime) }} 路 鎿嶄綔浜� {{ action.operatorName || action.operatorId || '鈥�' }}</span>
+ </div>
+ <div v-if="action.opinion" class="history-opinion">{{ action.opinion }}</div>
+ <div v-if="actionIndicators(action).length" class="history-indicators">
+ <div v-for="indicator in actionIndicators(action)" :key="indicator.code" class="history-indicator">
+ <span class="indicator-name">{{ indicatorLabels[indicator.code] || indicator.code }}</span>
+ <strong class="indicator-value">{{ indicator.value }}</strong>
+ <span class="indicator-range">{{ indicator.minValue }}鈥搟{ indicator.maxValue }}</span>
+ </div>
</div>
</a-timeline-item>
</a-timeline>
@@ -536,6 +820,9 @@
.stat-card.pending .stat-icon { color: #d97706; background: #fffbeb; }
.stat-card.pending .stat-main strong { color: #d97706; }
+/* ===== 绉诲姩绔壒閲忓叆鍙� ===== */
+.mobile-batch-entry { display: none; }
+
/* ===== 鏄庣粏鍗$墖 ===== */
.record-card { padding: 6px 16px 16px; border: 1px solid #e8edf5; border-radius: 12px; background: #fff; box-shadow: 0 3px 12px #243b5310; }
.card-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 13px 0 15px; flex-wrap: wrap; border-bottom: 1px dashed #eef1f6; }
@@ -547,11 +834,34 @@
.filters .ant-select { width: 128px; }
.filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; }
.filters :deep(.ant-btn .iconify) { font-size: 14px; }
+.selection-bar { padding: 12px 0; border-bottom: 1px solid #eef1f6; }
+.selection-summary, .selection-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
+.selection-summary { margin-bottom: 8px; color: #59677b; }
+.selection-summary strong { color: #172033; }
+.selection-actions .ant-btn { white-space: normal; height: auto; min-height: 32px; text-align: left; }
.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; }
+.scope-error { margin-bottom: 14px; }
+.scope-section { padding: 14px 0; border-top: 1px solid #eef1f6; }
+.scope-section h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; font-size: 15px; font-weight: 700; }
+.scope-section .muted { margin: 8px 0 12px; }
+.scope-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.review-choice { display: flex; gap: 16px; margin: 10px 0 18px; }
+
+/* ===== 娴佺▼璁板綍 ===== */
+.history-timeline { padding-top: 6px; }
+.history-timeline :deep(.ant-timeline-item) { padding-bottom: 22px; }
+.history-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
+.history-head .ant-tag { margin: 0; font-weight: 600; }
+.history-meta { color: #5b6b80; font-size: 12px; line-height: 1.6; }
+.history-opinion { margin-top: 8px; padding: 8px 12px; border-left: 3px solid #1677ff; border-radius: 4px; background: #f5f9ff; color: #3d4a5c; font-size: 13px; line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; }
+.history-indicators { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
+.history-indicator { display: flex; align-items: baseline; gap: 6px; padding: 7px 11px; border: 1px solid #e3e9f2; border-radius: 6px; background: #fff; font-size: 13px; }
+.indicator-name { color: #5b6b80; }
+.indicator-value { color: #172033; font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
+.indicator-range { margin-left: auto; color: #667085; font-variant-numeric: tabular-nums; }
/* ===== 鍗曞厓鏍� ===== */
.strong { font-weight: 600; color: #172033; }
@@ -571,11 +881,61 @@
/* ===== 鍝嶅簲寮� ===== */
@media (max-width: 1100px) { .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px) {
- .record-page { padding: 14px; }
+ .record-page { padding: 12px; }
.page-header { display: block; }
.header-badge { margin-top: 12px; }
.stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .mobile-batch-entry { display: block; margin-bottom: 12px; padding: 14px; border: 1px solid #cfe0fb; border-radius: 12px; background: linear-gradient(135deg, #f5f9ff 0%, #fff 100%); box-shadow: 0 3px 12px #243b5310; }
+ .mobile-batch-copy { display: flex; align-items: flex-start; gap: 11px; margin-bottom: 12px; }
+ .mobile-batch-copy strong { display: block; margin-bottom: 4px; color: #172033; font-size: 16px; }
+ .mobile-batch-copy p { margin: 0; color: #718096; font-size: 13px; line-height: 1.55; }
+ .mobile-batch-icon { display: flex; flex: 0 0 38px; width: 38px; height: 38px; align-items: center; justify-content: center; border-radius: 10px; color: #1677ff; background: #eaf3ff; font-size: 20px; }
+ .desktop-scope-action { display: none !important; }
+ .record-card { padding: 4px 12px 12px; }
.card-toolbar { align-items: flex-start; flex-direction: column; }
- .filters .ant-input { width: 100%; flex: 1; }
+ .filters { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .filters > :first-child,
+ .filters .ant-input,
+ .filters .ant-select { width: 100%; min-width: 0; }
+ .filters > :first-child { grid-column: 1 / -1; min-height: 44px; }
+ .filters .ant-input-affix-wrapper,
+ .filters .ant-select-selector,
+ .filters .ant-btn { min-height: 40px; }
+ .selection-summary { align-items: flex-start; }
+ .selection-summary span { flex-basis: 100%; }
+ .selection-actions { display: grid; grid-template-columns: 1fr; }
+ .selection-actions .ant-btn,
+ .scope-actions .ant-btn { width: 100%; min-height: 44px; }
+ .retest-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; padding: 10px 0; border-bottom: 1px solid #f0f2f5; }
+ .retest-row strong { grid-column: 1 / -1; }
+ .scope-scan { display: flex; flex-direction: column; gap: 8px; }
+ .scope-scan :deep(.ant-space-item) { width: 100%; }
+ .scope-scan :deep(.ant-select) { width: 100% !important; }
+ .scope-scan :deep(.ant-input-search-button) { min-width: 72px; min-height: 40px; }
+ .scope-section { padding: 16px 0; }
+ .scope-section h3 { align-items: flex-start; flex-wrap: wrap; }
+ .scope-actions { display: grid; grid-template-columns: 1fr; }
+ .review-choice { align-items: flex-start; flex-direction: column; gap: 12px; }
+ :deep(.ant-pagination) { justify-content: center; gap: 4px; }
+ :deep(.ant-pagination-options),
+ :deep(.ant-pagination-total-text) { display: none; }
+}
+@media (max-width: 480px) {
+ .header-copy h1 { font-size: 23px; }
+ .header-copy p { font-size: 13px; line-height: 1.6; }
+ .stat-grid { grid-template-columns: 1fr; gap: 8px; }
+ .stat-card { padding: 12px 14px; }
+ .filters { grid-template-columns: 1fr; }
+ .filters > :first-child { grid-column: auto; }
+}
+@media (max-width: 760px) {
+ :global(.seed-quality-modal) { padding-bottom: 0; }
+ :global(.seed-quality-modal .ant-modal) { top: 12px; width: calc(100vw - 24px) !important; max-width: calc(100vw - 24px); margin: 0 auto; padding-bottom: 12px; }
+ :global(.seed-quality-modal .ant-modal-content) { max-height: calc(100dvh - 24px); padding: 18px 16px; overflow: hidden; }
+ :global(.seed-quality-modal .ant-modal-header) { padding-right: 24px; }
+ :global(.seed-quality-modal .ant-modal-title) { white-space: normal; line-height: 1.4; }
+ :global(.seed-quality-modal .ant-modal-body) { max-height: calc(100dvh - 150px) !important; overflow-y: auto; overscroll-behavior: contain; }
+ :global(.seed-quality-modal .ant-modal-footer) { display: grid; grid-template-columns: 1fr 1fr; }
+ :global(.seed-quality-modal .ant-modal-footer .ant-btn) { min-height: 44px; margin-inline-start: 0 !important; }
}
</style>
--
Gitblit v1.9.3