| | |
| | | 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, |
| | |
| | | getSeedQualityActions, |
| | | getSeedIndicatorRanges, |
| | | retestSeedQuality, |
| | | retestSeedQualityBatch, |
| | | resolveSeedRetestScope, |
| | | submitSeedRetestScope, |
| | | approveSeedQuality, |
| | | approveSeedQualityBatch, |
| | | resolveSeedQuality, |
| | | submitSeedQualityGroup, |
| | | type SeedQualityRecord, |
| | | type SeedQualityScan, |
| | | type SeedQualityIndicator, |
| | | type SeedQualityAction, |
| | | type SeedRetestScope, |
| | |
| | | const code = ref(''); |
| | | const batchCode = ref(''); |
| | | const result = ref<number>(); |
| | | const workflowStatus = ref<number>(); |
| | | |
| | | const total = ref(0); |
| | | const statsTotal = ref(0); |
| | |
| | | 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: '已报废', |
| | |
| | | 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 { |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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() { |
| | |
| | | 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 || '复检提交失败'); |
| | |
| | | } |
| | | } |
| | | function openReview(record: SeedQualityRecord, stage: 'process' | 'company') { |
| | | reviewFromScope.value = false; |
| | | activeRecord.value = record; |
| | | reviewIds.value = [record.id]; |
| | | reviewStage.value = stage; |
| | |
| | | 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; |
| | |
| | | } |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | function searchRecords() { |
| | | page.value = 1; |
| | | clearSelection(); |
| | | void loadRecords(); |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | </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"> |
| | |
| | | <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> |
| | |
| | | { 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="{ |
| | |
| | | </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="本次实测值" /> |
| | |
| | | </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> |
| | |
| | | .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; } |
| | |
| | | .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; } |
| | |
| | | /* ===== 响应式 ===== */ |
| | | @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> |