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