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/api/request.ts | 3
src/packages/effects/request/src/request-client/types.ts | 2
src/views/mes/pro/batch/data.ts | 17
src/views/mes/qc/seedQuality/record.vue | 506 +++++++++++++++++++++++++++----
src/views/mobile/quality-records.vue | 290 ++++++++++++++---
src/api/mes/qc/seedQuality/index.ts | 17
src/views/mes/pro/batch/modules/form.vue | 122 ++++++
7 files changed, 799 insertions(+), 158 deletions(-)
diff --git a/src/api/mes/qc/seedQuality/index.ts b/src/api/mes/qc/seedQuality/index.ts
index 3012db0..295736b 100644
--- a/src/api/mes/qc/seedQuality/index.ts
+++ b/src/api/mes/qc/seedQuality/index.ts
@@ -53,6 +53,7 @@
opinion?: string;
indicatorsJson?: string;
operatorId?: number;
+ operatorName?: string;
createTime?: string;
}
export function getSeedQualityActions(id: number) {
@@ -78,21 +79,25 @@
initialRecheckIds: number[];
pendingProcessCount: number;
pendingCompanyCount: number;
+ pendingProcessIds?: number[];
+ pendingInitialFailIds?: number[];
+ pendingCompanyIds?: number[];
+ pendingCompanyFailIds?: number[];
}
export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH') {
- return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType } });
+ return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType }, suppressErrorMessage: true });
}
export function submitSeedRetestScope(data: {
code: string; scanType: 'PALLET' | 'BATCH'; workflowStatus: 10 | 50;
qualityIds: number[]; indicators: MesQcSeedQualityApi.Indicator[]; reason?: string;
}) {
- return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/retest-scope', data);
+ return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/retest-scope', data, { suppressErrorMessage: true });
}
export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) {
- return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data);
+ return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data, { suppressErrorMessage: true });
}
export function approveSeedQualityBatch(stage: 'process' | 'company', data: { ids: number[]; approved: boolean; opinion: string }) {
- return requestClient.post(`/mes/qc/seed-quality/${stage}-approve-batch`, data);
+ return requestClient.post(`/mes/qc/seed-quality/${stage}-approve-batch`, data, { suppressErrorMessage: true });
}
export namespace MesQcSeedQualityApi {
@@ -127,8 +132,8 @@
pendingBagIds?: number[]; completedBagIds?: number[];
}
export function resolveSeedQuality(code: string, scanType: 'BAG' | 'PALLET' | 'BATCH' = 'BAG') {
- return requestClient.get<SeedQualityScan>('/mes/qc/seed-quality/resolve', { params: { code, scanType } });
+ return requestClient.get<SeedQualityScan>('/mes/qc/seed-quality/resolve', { params: { code, scanType }, suppressErrorMessage: true });
}
export function submitSeedQualityGroup(data: MesQcSeedQualityApi.SubmitParams & { scanType: 'BAG' | 'PALLET' | 'BATCH'; bagIds: number[] }) {
- return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/submit-group', data);
+ return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/submit-group', data, { suppressErrorMessage: true });
}
diff --git a/src/api/request.ts b/src/api/request.ts
index 1687786..a5507a8 100644
--- a/src/api/request.ts
+++ b/src/api/request.ts
@@ -194,6 +194,9 @@
// 閫氱敤鐨勯敊璇鐞�,濡傛灉娌℃湁杩涘叆涓婇潰鐨勯敊璇鐞嗛�昏緫锛屽氨浼氳繘鍏ヨ繖閲�
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
+ if (error?.config?.suppressErrorMessage || error?.response?.config?.suppressErrorMessage) {
+ return;
+ }
// 杩欓噷鍙互鏍规嵁涓氬姟杩涜瀹氬埗,浣犲彲浠ユ嬁鍒� error 鍐呯殑淇℃伅杩涜瀹氬埗鍖栧鐞嗭紝鏍规嵁涓嶅悓鐨� code 鍋氫笉鍚岀殑鎻愮ず锛岃�屼笉鏄洿鎺ヤ娇鐢� message.error 鎻愮ず msg
// 褰撳墠mock鎺ュ彛杩斿洖鐨勯敊璇瓧娈垫槸 error 鎴栬�� message
const responseData = error?.response?.data ?? {};
diff --git a/src/packages/effects/request/src/request-client/types.ts b/src/packages/effects/request/src/request-client/types.ts
index c1722b8..3eda3b9 100644
--- a/src/packages/effects/request/src/request-client/types.ts
+++ b/src/packages/effects/request/src/request-client/types.ts
@@ -6,6 +6,8 @@
} from 'axios';
type ExtendOptions<T = any> = {
+ /** 鐢遍〉闈㈠湪琛ㄥ崟鍐呭睍绀洪敊璇椂锛屼笉鍐嶅脊鍑哄叏灞�閿欒鎻愮ず銆� */
+ suppressErrorMessage?: boolean;
/**
* 鍙傛暟搴忓垪鍖栨柟寮忋�傞缃殑鏈�
* - brackets: ids[]=1&ids[]=2&ids[]=3
diff --git a/src/views/mes/pro/batch/data.ts b/src/views/mes/pro/batch/data.ts
index 579f7ba..f32b8f0 100644
--- a/src/views/mes/pro/batch/data.ts
+++ b/src/views/mes/pro/batch/data.ts
@@ -25,13 +25,20 @@
export type TonBagOption = PalletOption;
+/** 鎵樼洏銆佸惃琚嬩笅鎷夌殑浜や簰浜嬩欢锛堟敮鎸佹悳绱笌婊氬姩鍒嗘鍔犺浇锛� */
+export interface BindingSelectHandlers {
+ onChange?: (ids: number[]) => void;
+ onPopupScroll?: (event: Event) => void;
+ onSearch?: (keyword: string) => void;
+}
+
/** 鏂板/淇敼鐨勮〃鍗� */
export function useFormSchema(
formType: FormType,
palletOptions: PalletOption[] = [],
- onPalletChange?: (ids: number[]) => void,
+ palletHandlers: BindingSelectHandlers = {},
tonBagOptions: TonBagOption[] = [],
- onTonBagChange?: (ids: number[]) => void,
+ tonBagHandlers: BindingSelectHandlers = {},
existingBinding?: 'pallet' | 'tonBag',
): VbenFormSchema[] {
const disabled = formType === 'detail';
@@ -165,7 +172,8 @@
mode: 'multiple',
optionFilterProp: 'label',
options: palletOptions,
- onChange: onPalletChange,
+ filterOption: false,
+ ...palletHandlers,
placeholder: '璇烽�夋嫨鐩稿悓鍝佺鐨勬墭鐩�',
showSearch: true,
},
@@ -194,7 +202,8 @@
mode: 'multiple',
optionFilterProp: 'label',
options: tonBagOptions,
- onChange: onTonBagChange,
+ filterOption: false,
+ ...tonBagHandlers,
placeholder: '璇烽�夋嫨鐩稿悓鍝佺鐨勫惃琚�',
showSearch: true,
},
diff --git a/src/views/mes/pro/batch/modules/form.vue b/src/views/mes/pro/batch/modules/form.vue
index f27d9bb..f405c55 100644
--- a/src/views/mes/pro/batch/modules/form.vue
+++ b/src/views/mes/pro/batch/modules/form.vue
@@ -5,7 +5,7 @@
import type { MesProBatchApi } from "#/api/mes/pro/batch";
import type { SystemUserApi } from "#/api/system/user";
- import { computed, nextTick, ref } from "vue";
+ import { computed, nextTick, reactive, ref } from "vue";
import { useVbenModal } from "@vben/common-ui";
import { DICT_TYPE } from "@vben/constants";
@@ -38,8 +38,77 @@
const formType = ref<FormType>("create");
const formData = ref<MesProBatchApi.Batch>();
- let palletOptions: PalletOption[] = [];
- let tonBagOptions: TonBagOption[] = [];
+ const BINDING_OPTION_PAGE_SIZE = 50;
+ const BINDING_SCROLL_THRESHOLD = 32;
+ let palletOptionSource: PalletOption[] = [];
+ let tonBagOptionSource: TonBagOption[] = [];
+ const palletOptions = reactive<PalletOption[]>([]);
+ const tonBagOptions = reactive<TonBagOption[]>([]);
+ let palletKeyword = "";
+ let tonBagKeyword = "";
+
+ function filterBindingOptions<T extends PalletOption>(source: T[], keyword: string): T[] {
+ const normalized = keyword.trim().toLowerCase();
+ if (!normalized) return source;
+ return source.filter((option) => option.label.toLowerCase().includes(normalized));
+ }
+
+ function resetBindingOptions<T extends PalletOption>(
+ target: T[],
+ source: T[],
+ keyword = ""
+ ) {
+ const filtered = filterBindingOptions(source, keyword);
+ target.splice(0, target.length, ...filtered.slice(0, BINDING_OPTION_PAGE_SIZE));
+ }
+
+ function appendBindingOptions<T extends PalletOption>(
+ target: T[],
+ source: T[],
+ keyword: string
+ ) {
+ const filtered = filterBindingOptions(source, keyword);
+ if (target.length >= filtered.length) return;
+ target.push(...filtered.slice(target.length, target.length + BINDING_OPTION_PAGE_SIZE));
+ }
+
+ function isPopupNearBottom(event: Event) {
+ const element = event.target as HTMLElement;
+ return element.scrollHeight - element.scrollTop - element.clientHeight <= BINDING_SCROLL_THRESHOLD;
+ }
+
+ function handlePalletSearch(keyword: string) {
+ palletKeyword = keyword;
+ resetBindingOptions(palletOptions, palletOptionSource, keyword);
+ }
+
+ function handleTonBagSearch(keyword: string) {
+ tonBagKeyword = keyword;
+ resetBindingOptions(tonBagOptions, tonBagOptionSource, keyword);
+ }
+
+ function handlePalletPopupScroll(event: Event) {
+ if (isPopupNearBottom(event)) {
+ appendBindingOptions(palletOptions, palletOptionSource, palletKeyword);
+ }
+ }
+
+ function handleTonBagPopupScroll(event: Event) {
+ if (isPopupNearBottom(event)) {
+ appendBindingOptions(tonBagOptions, tonBagOptionSource, tonBagKeyword);
+ }
+ }
+
+ const palletHandlers = {
+ onChange: handlePalletChange,
+ onPopupScroll: handlePalletPopupScroll,
+ onSearch: handlePalletSearch,
+ };
+ const tonBagHandlers = {
+ onChange: handleTonBagChange,
+ onPopupScroll: handleTonBagPopupScroll,
+ onSearch: handleTonBagSearch,
+ };
async function handlePalletChange(ids: number[] = []) {
const values = await formApi.getValues();
if (ids.length && (values.tonBagIds?.length || formData.value?.bindingMode === 'tonBag')) {
@@ -47,7 +116,7 @@
await formApi.setFieldValue('palletIds', []);
return;
}
- const selected = palletOptions.filter(option => ids.includes(option.value));
+ const selected = palletOptionSource.filter(option => ids.includes(option.value));
const source = formData.value?.itemId ? formData.value : selected[0];
if (selected.some(option => !option.itemId || (source?.itemId && option.itemId !== source.itemId))) {
message.warning('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勬墭鐩�');
@@ -65,7 +134,7 @@
await formApi.setFieldValue('tonBagIds', []);
return;
}
- const selected = tonBagOptions.filter(option => ids.includes(option.value));
+ const selected = tonBagOptionSource.filter(option => ids.includes(option.value));
const source = formData.value?.itemId ? formData.value : selected[0];
if (selected.some(option => option.itemId !== source?.itemId)) {
message.warning('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勫惃琚�');
@@ -129,8 +198,9 @@
let workerRowSeq = 0;
/** 娣诲姞浜哄憳琛� */
- function handleAddWorker() {
- workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
+ async function handleAddWorker() {
+ await waitWorkerGridReady();
+ await workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
}
/** 鍒犻櫎浜哄憳琛� */
@@ -250,22 +320,47 @@
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
// 鏂板/琛ュ綍/缂栬緫鎬佸姞杞芥湭缁戝畾鎵规鐨勬墭鐩橈紝渚涖�岀粦瀹氭墭鐩樸�嶅閫変笅鎷変娇鐢�
- palletOptions = [];
- tonBagOptions = [];
+ palletOptionSource = [];
+ tonBagOptionSource = [];
+ palletOptions.splice(0);
+ tonBagOptions.splice(0);
+ palletKeyword = "";
+ tonBagKeyword = "";
+ // 鍏堟寕杞藉熀纭�琛ㄥ崟锛屽�欓�夋墭鐩�/鍚ㄨ鍦ㄥ悗鍙板姞杞斤紝閬垮厤鍙樉绀轰汉鍛橀厤缃殑绌虹獥鏈�
+ formApi.setState({
+ schema: useFormSchema(
+ formType.value,
+ palletOptions,
+ palletHandlers,
+ tonBagOptions,
+ tonBagHandlers
+ ),
+ });
if (formType.value !== "detail") {
const [pallets, tonBags] = await Promise.all([getUnboundPalletList(), getUnboundTonBags()]);
- palletOptions = pallets.map((pallet) => ({
+ palletOptionSource = pallets.map((pallet) => ({
label: `${pallet.tempCode ?? `鎵樼洏${pallet.id}`} 路 ${pallet.itemName || "寰呭畬鍠勫搧绉�"}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚嬶級`,
value: pallet.id as number,
itemId: pallet.itemId, itemName: pallet.itemName, itemCode: pallet.itemCode,
}));
- tonBagOptions = tonBags.map((bag) => ({
+ tonBagOptionSource = tonBags.map((bag) => ({
label: `${bag.code ?? `鍚ㄨ${bag.id}`} 路 ${bag.itemName || '寰呭畬鍠勫搧绉�'}`,
value: bag.id as number,
itemId: bag.itemId, itemName: bag.itemName, itemCode: bag.itemCode,
}));
+ resetBindingOptions(palletOptions, palletOptionSource);
+ resetBindingOptions(tonBagOptions, tonBagOptionSource);
}
- formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange) });
+ // 閲嶆柊鍚屾涓�娆� Schema锛屼娇渚濊禆瀛楁涓殑鍔ㄦ�� options 绔嬪嵆鎷垮埌棣栨壒鍊欓�夐」
+ formApi.setState({
+ schema: useFormSchema(
+ formType.value,
+ palletOptions,
+ palletHandlers,
+ tonBagOptions,
+ tonBagHandlers
+ ),
+ });
if (formType.value !== "detail") {
// 娓呯┖涓婃鎵撳紑鏃跺嬀閫夌殑鎵樼洏锛岄伩鍏嶆畫鐣欏�艰瑙﹀彂銆屾壒娆¤繘鍏ョ敓浜т腑銆�
await formApi.setFieldValue("palletIds", undefined);
@@ -282,7 +377,7 @@
try {
formData.value = await getBatch(data.id);
const existingBinding = formData.value.bindingMode;
- formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange, existingBinding) });
+ formApi.setState({ schema: useFormSchema(formType.value, palletOptions, palletHandlers, tonBagOptions, tonBagHandlers, existingBinding) });
await formApi.setValues(formData.value);
const workers = (formData.value.workers ?? []).map(
(worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
@@ -295,6 +390,7 @@
} else {
await waitWorkerGridReady();
await workerGridApi.grid.loadData([]);
+ await handleAddWorker();
}
},
});
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>
diff --git a/src/views/mobile/quality-records.vue b/src/views/mobile/quality-records.vue
index 306e369..dceff3b 100644
--- a/src/views/mobile/quality-records.vue
+++ b/src/views/mobile/quality-records.vue
@@ -1,106 +1,272 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
+
import { useAccess } from '@vben/access';
-import { getSeedIndicatorRanges, getSeedQualityPage, resolveSeedRetestScope, submitSeedRetestScope, type SeedQualityRecord, type SeedRetestScope } from '#/api/mes/qc/seedQuality';
+import { MesWmWarehouseTypeEnum } from '@vben/constants';
+
+import {
+ approveSeedQualityBatch,
+ getSeedIndicatorRanges,
+ getSeedQualityPage,
+ resolveSeedQuality,
+ resolveSeedRetestScope,
+ submitSeedQualityGroup,
+ submitSeedRetestScope,
+ type SeedQualityRecord,
+ type SeedQualityScan,
+ type SeedRetestScope,
+} from '#/api/mes/qc/seedQuality';
+import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { errorText } from '#/mobile/api';
import { extractCode } from '#/mobile/access';
+
import Scanner from './scanner.vue';
-const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref('');
+type ReviewGroup = { ids: number[]; stage: 'company' | 'process'; status: number };
+
const { hasAccessByCodes } = useAccess();
+const canInitial = computed(() => hasAccessByCodes(['mes:qc-seed-quality:submit']));
const canRetest = computed(() => hasAccessByCodes(['mes:qc-seed-quality:retest']));
-const scanType = ref<'PALLET' | 'BATCH'>('PALLET'), scope = ref<SeedRetestScope>();
-const workflowStatus = ref<10 | 50>(10), reason = ref(''), notice = ref('');
+const canProcessReview = computed(() => hasAccessByCodes(['mes:qc-seed-quality:process-approve']));
+const canCompanyReview = computed(() => hasAccessByCodes(['mes:qc-seed-quality:company-approve']));
+const canBatchAction = computed(() => canInitial.value || canRetest.value || canProcessReview.value || canCompanyReview.value);
+
+const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref(''), notice = ref('');
+const scanType = ref<'BATCH' | 'PALLET'>('PALLET'), scannedCode = ref('');
+const scope = ref<SeedRetestScope>(), initialScope = ref<SeedQualityScan>();
+const workflowStatus = ref<10 | 50>(10), reason = ref('');
+const warehouseId = ref<number>();
+const warehouseOptions = ref<Array<{ label: string; value: number }>>([]);
+const reviewGroup = ref<ReviewGroup>(), reviewApproved = ref(true), reviewOpinion = ref('');
const indicators = ref([
{ code: 'purity', name: '绾害', value: '', minValue: '', maxValue: '' },
{ code: 'cleanliness', name: '鍑�搴�', value: '', minValue: '', maxValue: '' },
{ code: 'germination_rate', name: '鍙戣娊鐜�', value: '', minValue: '', maxValue: '' },
{ code: 'moisture', name: '姘村垎', value: '', minValue: '', maxValue: '' },
]);
+
+const initialIds = computed(() => initialScope.value?.pendingBagIds || []);
const targetIds = computed(() => workflowStatus.value === 10 ? scope.value?.retestIds || [] : scope.value?.initialRecheckIds || []);
-const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => v.trim() !== '' && Number.isFinite(Number(v)))
- && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.minValue) <= Number(i.maxValue)));
-const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue)));
-async function identifyRetest(raw: string) {
- if (busy.value) return;
- busy.value = true; error.value = ''; notice.value = ''; scope.value = undefined;
- try {
- const data = await resolveSeedRetestScope(extractCode(raw), scanType.value);
- if (!data.retestIds.length && !data.initialRecheckIds.length) throw new Error('褰撳墠鎵樼洏鎴栨壒娆℃病鏈夊緟澶嶆鎴栧緟閲嶆柊鍒濇鐨勮褰�');
- scope.value = data;
- workflowStatus.value = data.retestIds.length ? 10 : 50;
- indicators.value.forEach(i => { i.value = ''; i.minValue = ''; i.maxValue = ''; }); reason.value = '';
- try {
- const ranges = await getSeedIndicatorRanges();
- indicators.value.forEach(i => { const range = ranges.find(r => r.indicatorCode === i.code); if (range) { i.minValue = String(range.minValue); i.maxValue = String(range.maxValue); } });
- } catch { /* 鍙墜鍔ㄥ~鍐欐湰娆¤寖鍥� */ }
- } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
+const complete = computed(() => indicators.value.every((item) =>
+ [item.value, item.minValue, item.maxValue].every((value) => String(value).trim() !== '' && Number.isFinite(Number(value)))
+ && Number(item.value) >= 0 && Number(item.value) <= 100 && Number(item.minValue) >= 0
+ && Number(item.maxValue) <= 100 && Number(item.minValue) <= Number(item.maxValue)));
+const passed = computed(() => complete.value && indicators.value.every((item) =>
+ Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)));
+const reviewGroups = computed<ReviewGroup[]>(() => {
+ if (!scope.value) return [];
+ return [
+ { status: 20, stage: 'process' as const, ids: scope.value.pendingProcessIds || [] },
+ { status: 25, stage: 'process' as const, ids: scope.value.pendingInitialFailIds || [] },
+ { status: 30, stage: 'company' as const, ids: scope.value.pendingCompanyIds || [] },
+ { status: 60, stage: 'company' as const, ids: scope.value.pendingCompanyFailIds || [] },
+ ].filter((group) => group.ids.length && (group.stage === 'process' ? canProcessReview.value : canCompanyReview.value));
+});
+const statusNames: Record<number, string> = {
+ 10: '寰呭妫�', 20: '寰呭垵妫�瀹℃牳', 25: '寰呭垵妫�涓嶅悎鏍煎鐞�', 30: '寰呭妫�瀹℃牳',
+ 40: '宸叉斁琛�', 50: '寰呴噸鏂板垵妫�', 60: '寰呭妫�涓嶅悎鏍煎鐞�', 70: '宸叉姤搴�',
+};
+
+function resetIndicators() {
+ indicators.value.forEach((item) => { item.value = ''; });
+ reason.value = '';
}
-async function submitRetest() {
- if (busy.value || !scope.value || !targetIds.value.length || !complete.value) return;
- if (!passed.value && !reason.value.trim()) { error.value = '妫�楠屼笉鍚堟牸璇峰~鍐欏師鍥�'; return; }
- const count = targetIds.value.length;
- if (!window.confirm(`纭灏嗘湰娆℃寚鏍囩敤浜� ${count} 琚�${workflowStatus.value === 10 ? '澶嶆' : '閲嶆柊鍒濇'}锛焋)) return;
+
+async function hydrateReviewIds(current: SeedRetestScope, rawCode: string) {
+ const loaded = (current.pendingProcessIds?.length || 0) + (current.pendingInitialFailIds?.length || 0)
+ + (current.pendingCompanyIds?.length || 0) + (current.pendingCompanyFailIds?.length || 0);
+ if (loaded >= current.pendingProcessCount + current.pendingCompanyCount) return;
+ const batch = current.batchCode || (current.scanType === 'BATCH' ? current.code : undefined);
+ if (!batch) return;
+ const statuses = [20, 25, 30, 60] as const;
+ const pages = await Promise.all(statuses.map((workflow) => getSeedQualityPage({
+ batchCode: batch, pageNo: 1, pageSize: 500, workflowStatus: workflow,
+ }).catch(() => ({ list: [] as SeedQualityRecord[], total: 0 }))));
+ const scopeCodes = new Set([current.code, rawCode]);
+ const ids = pages.map(({ list }) => list
+ .filter((row) => current.scanType === 'BATCH' || (!!row.palletCode && scopeCodes.has(row.palletCode)))
+ .map((row) => row.id));
+ current.pendingProcessIds = ids[0];
+ current.pendingInitialFailIds = ids[1];
+ current.pendingCompanyIds = ids[2];
+ current.pendingCompanyFailIds = ids[3];
+}
+
+async function identifyScope(raw: string) {
+ if (busy.value || !canBatchAction.value) return;
+ busy.value = true; error.value = ''; notice.value = '';
+ scope.value = undefined; initialScope.value = undefined; reviewGroup.value = undefined;
+ try {
+ const codeValue = extractCode(raw);
+ scannedCode.value = codeValue;
+ const current = await resolveSeedRetestScope(codeValue, scanType.value);
+ await hydrateReviewIds(current, codeValue);
+ scope.value = current;
+ scanType.value = current.scanType;
+ workflowStatus.value = current.retestIds.length ? 10 : 50;
+ initialScope.value = await resolveSeedQuality(codeValue, current.scanType);
+ resetIndicators();
+ if (!initialIds.value.length && !targetIds.value.length && !reviewGroups.value.length) {
+ notice.value = '褰撳墠鎵樼洏鎴栫敓浜ф壒娆℃病鏈夊彲澶勭悊鐨勮川妫�璁板綍銆�';
+ }
+ } catch (exception) { error.value = errorText(exception); }
+ finally { busy.value = false; }
+}
+
+function indicatorPayload() {
+ return indicators.value.map((item) => ({ code: item.code, value: Number(item.value), minValue: Number(item.minValue), maxValue: Number(item.maxValue) }));
+}
+
+async function refreshScopeAndList(success: string) {
+ await identifyScope(scannedCode.value);
+ await load();
+ notice.value = success;
+}
+
+async function submitInitial() {
+ const ids = initialIds.value.slice(0, 500);
+ if (busy.value || !canInitial.value || !initialScope.value || !ids.length || !complete.value) return;
+ if (!warehouseId.value) { error.value = '璇烽�夋嫨妫�楠屽悎鏍煎悗鐨勭洰鏍囧悎鏍煎簱'; return; }
+ if (!passed.value && !reason.value.trim()) { error.value = '鍒濇涓嶅悎鏍艰濉啓鍘熷洜'; return; }
+ if (!window.confirm(`纭鎵归噺鎻愪氦 ${ids.length} 琚嬪垵妫�锛焋)) return;
busy.value = true; error.value = '';
try {
- const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, workflowStatus: workflowStatus.value,
- qualityIds: targetIds.value, reason: reason.value.trim() || undefined,
- indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) });
- notice.value = `宸插畬鎴� ${result.processedCount} 琚嬫楠岋紝绛夊緟鍚庣画瀹℃牳鎴栦笉鍚堟牸澶勭悊銆俙;
- scope.value = undefined;
- } catch (e) { error.value = errorText(e) + '銆傝閲嶆柊鎵爜鏍稿娓呭崟鍚庡啀鎻愪氦銆�'; scope.value = undefined; }
- finally { busy.value = false; if (notice.value) void load(); }
+ const result = await submitSeedQualityGroup({ code: initialScope.value.code, batchCode: initialScope.value.batchCode,
+ scanType: initialScope.value.scanType || scanType.value, bagIds: ids, qualifiedWarehouseId: warehouseId.value,
+ reason: reason.value.trim() || undefined, indicators: indicatorPayload() });
+ busy.value = false;
+ await refreshScopeAndList(`宸插畬鎴� ${result.processedCount} 琚嬪垵妫�銆俙);
+ } catch (exception) { error.value = errorText(exception) + '銆傝閲嶆柊璇嗗埆鍚庢牳瀵瑰緟妫�娓呭崟銆�'; }
+ finally { busy.value = false; }
}
+
+async function submitRetest() {
+ const ids = targetIds.value.slice(0, 500);
+ if (busy.value || !canRetest.value || !scope.value || !ids.length || !complete.value) return;
+ if (!passed.value && !reason.value.trim()) { error.value = '妫�楠屼笉鍚堟牸璇峰~鍐欏師鍥�'; return; }
+ if (!window.confirm(`纭灏嗘湰娆℃寚鏍囩敤浜� ${ids.length} 琚�${workflowStatus.value === 10 ? '澶嶆' : '閲嶆柊鍒濇'}锛焋)) return;
+ busy.value = true; error.value = '';
+ try {
+ const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType,
+ workflowStatus: workflowStatus.value, qualityIds: ids, reason: reason.value.trim() || undefined, indicators: indicatorPayload() });
+ busy.value = false;
+ await refreshScopeAndList(`宸插畬鎴� ${result.processedCount} 琚嬫楠屻�俙);
+ } catch (exception) { error.value = errorText(exception) + '銆傝閲嶆柊璇嗗埆鍚庢牳瀵瑰緟妫�娓呭崟銆�'; }
+ finally { busy.value = false; }
+}
+
+function selectReview(group: ReviewGroup) {
+ reviewGroup.value = group; reviewApproved.value = true; reviewOpinion.value = ''; error.value = '';
+}
+
+async function submitReview() {
+ if (busy.value || !reviewGroup.value?.ids.length) return;
+ if (!reviewApproved.value && !reviewOpinion.value.trim()) { error.value = '椹冲洖鏃惰濉啓瀹℃牳鎰忚'; return; }
+ const ids = reviewGroup.value.ids.slice(0, 500);
+ if (!window.confirm(`纭${reviewApproved.value ? '閫氳繃' : '椹冲洖'}鏈 ${ids.length} 鏉¤褰曪紵`)) return;
+ busy.value = true; error.value = '';
+ try {
+ await approveSeedQualityBatch(reviewGroup.value.stage, { ids, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() });
+ busy.value = false;
+ await refreshScopeAndList(`宸插畬鎴� ${ids.length} 鏉″鏍搞�俙);
+ } catch (exception) { error.value = errorText(exception) + '銆傝閲嶆柊璇嗗埆鍚庢牳瀵瑰緟瀹℃牳娓呭崟銆�'; }
+ finally { busy.value = false; }
+}
+
const code = ref(''), batchCode = ref(''), result = ref(''), page = ref(0), total = ref(0);
-const statusNames: Record<number, string> = { 10: '寰呭妫�', 20: '寰呭垵妫�瀹℃牳', 25: '寰呭垵妫�涓嶅悎鏍煎鐞�', 30: '寰呭妫�瀹℃牳', 40: '宸叉斁琛�', 50: '寰呴噸鏂板垵妫�', 60: '寰呭妫�涓嶅悎鏍煎鐞�', 70: '宸叉姤搴�' };
-let filters: { code?: string; batchCode?: string; result?: number } = {};
+let filters: { batchCode?: string; code?: string; result?: number } = {};
async function load(reset = true) {
if (busy.value) return;
busy.value = true; error.value = '';
const next = reset ? 1 : page.value + 1;
- const query = reset ? { code: code.value.trim() || undefined, batchCode: batchCode.value.trim() || undefined, result: result.value ? Number(result.value) : undefined } : filters;
+ const query = reset ? { code: code.value.trim() || undefined, batchCode: batchCode.value.trim() || undefined,
+ result: result.value ? Number(result.value) : undefined } : filters;
try {
const data = await getSeedQualityPage({ ...query, pageNo: next, pageSize: 20 });
rows.value = reset ? data.list : [...rows.value, ...data.list];
page.value = next; total.value = data.total; filters = query;
- } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
+ } catch (exception) { error.value = errorText(exception); }
+ finally { busy.value = false; }
}
-onMounted(() => load());
+
+onMounted(async () => {
+ if (canInitial.value) {
+ try {
+ const warehouses = await getWarehouseSimpleList();
+ warehouseOptions.value = warehouses.filter((item) => item.type === MesWmWarehouseTypeEnum.QUALIFIED && !item.frozen && item.id)
+ .map((item) => ({ label: item.code ? `${item.name}锛�${item.code}锛塦 : item.name || '', value: item.id! }));
+ if (warehouseOptions.value.length === 1) warehouseId.value = warehouseOptions.value[0]?.value;
+ } catch { warehouseOptions.value = []; }
+ }
+ if (canInitial.value || canRetest.value) {
+ try {
+ const ranges = await getSeedIndicatorRanges();
+ indicators.value.forEach((item) => { const range = ranges.find((row) => row.indicatorCode === item.code);
+ if (range) { item.minValue = String(range.minValue); item.maxValue = String(range.maxValue); } });
+ } catch { /* 鍚堟牸鑼冨洿浠嶅彲鎵嬪伐濉啓 */ }
+ }
+ await load();
+});
</script>
+
<template>
- <section v-if="canRetest" class="m-panel">
- <h2 class="m-title">鎸夋墭鐩� / 鎵规鎵归噺璐ㄦ</h2>
- <p class="m-muted">鎵弿鎵樼洏鐮佹垨鐢熶骇鎵规鐮侊紝绯荤粺浼氬垪鍑哄緟澶嶆鍜屽緟閲嶆柊鍒濇鐨勮鏁般��</p>
- <label class="m-field"><span>鎵爜绫诲瀷</span><select v-model="scanType" :disabled="busy" @change="scope = undefined"><option value="PALLET">鎵樼洏鐮�</option><option value="BATCH">鐢熶骇鎵规鐮�</option></select></label>
- <Scanner :disabled="busy" @code="identifyRetest" />
- <form v-if="scope" @submit.prevent="submitRetest">
- <p class="m-muted">{{scope.code}} 路 {{scope.batchCode || '鏈爣娉ㄦ壒娆�'}} 路 鏈夋晥琚� {{scope.totalCount}}</p>
- <label class="m-field"><span>璐ㄦ闃舵</span><select v-model="workflowStatus" :disabled="busy"><option v-if="scope.retestIds.length" :value="10">澶嶆锛坽{scope.retestIds.length}} 琚嬶級</option><option v-if="scope.initialRecheckIds.length" :value="50">閲嶆柊鍒濇锛坽{scope.initialRecheckIds.length}} 琚嬶級</option></select></label>
- <p class="m-warning">鍚屼竴缁勬寚鏍囧皢鐢ㄤ簬鏈樁娈靛叏閮� {{targetIds.length}} 琚嬨�傝鏍稿鎶芥牱缁撴灉鏄惁閫傜敤銆�</p>
- <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}锛�%锛�</legend>
- <label class="m-field"><span>瀹炴祴鍊�</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label>
- <div class="m-grid"><label class="m-field"><span>鍚堟牸涓嬮檺</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>鍚堟牸涓婇檺</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div>
- </fieldset>
- <p class="m-muted">涓婁笅闄愪粠妯℃澘甯﹀叆锛屽彲鎸夋湰娆℃楠屾爣鍑嗚皟鏁淬��</p>
- <label class="m-field"><span>{{complete && !passed ? '涓嶅悎鏍煎師鍥狅紙蹇呭~锛�' : '妫�楠屽娉�'}}</span><textarea v-model="reason" :required="complete && !passed" maxlength="500" rows="3" /></label>
- <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{busy ? '姝e湪鎻愪氦鈥�' : `鎻愪氦 ${targetIds.length} 琚嬭川妫�`}}</button>
- </form>
+ <section v-if="canBatchAction" class="m-panel">
+ <h2 class="m-title">鎵归噺妫�楠屼笌瀹℃牳</h2>
+ <p class="m-muted">鎵弿鎵樼洏鐮佹垨鐢熶骇鎵规鐮侊紝绯荤粺浼氭寜褰撳墠璐﹀彿鏉冮檺鍒楀嚭鎵归噺鍒濇銆佸鏍搞�佸妫�鎿嶄綔銆�</p>
+ <label class="m-field"><span>鎵爜绫诲瀷</span><select v-model="scanType" :disabled="busy" @change="scope = undefined; initialScope = undefined"><option value="PALLET">鎵樼洏鐮�</option><option value="BATCH">鐢熶骇鎵规鐮�</option></select></label>
+ <Scanner :disabled="busy" @code="identifyScope" />
+
+ <template v-if="scope">
+ <div class="m-panel"><h3 class="m-title">宸茶瘑鍒寖鍥�</h3><dl><div class="m-row"><dt>鐮佸��</dt><dd>{{ scope.code }}</dd></div><div class="m-row"><dt>鐢熶骇鎵规</dt><dd>{{ scope.batchCode || '鈥�' }}</dd></div><div class="m-row"><dt>鏈夋晥琚嬫暟</dt><dd>{{ scope.totalCount }}</dd></div></dl></div>
+
+ <form v-if="canInitial && initialIds.length" class="m-panel" @submit.prevent="submitInitial">
+ <h3 class="m-title">鎵归噺鍒濇锛坽{ Math.min(initialIds.length, 500) }} 琚嬶級</h3>
+ <p v-if="!initialScope?.inbound" class="m-warning">璇ヨ寖鍥寸殑鏈夋晥琚嬬爜灏氭湭鍏ㄩ儴鍏ュ簱锛屾殏涓嶈兘鎻愪氦鍒濇銆�</p>
+ <template v-else>
+ <label class="m-field"><span>鐩爣鍚堟牸搴�</span><select v-model="warehouseId" required :disabled="busy"><option :value="undefined" disabled>璇烽�夋嫨鍚堟牸搴�</option><option v-for="item in warehouseOptions" :key="item.value" :value="item.value">{{ item.label }}</option></select></label>
+ <fieldset v-for="item in indicators" :key="`initial-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}锛�%锛�</legend><label class="m-field"><span>瀹炴祴鍊�</span><input v-model="item.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><div class="m-grid"><label class="m-field"><span>鍚堟牸涓嬮檺</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>鍚堟牸涓婇檺</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset>
+ <label class="m-field"><span>{{ complete && !passed ? '涓嶅悎鏍煎師鍥狅紙蹇呭~锛�' : '妫�楠屽娉�' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label>
+ <button class="m-primary" :disabled="busy || !complete || !warehouseId">{{ busy ? '姝e湪鎻愪氦鈥�' : `鎻愪氦 ${Math.min(initialIds.length, 500)} 琚嬪垵妫�` }}</button>
+ </template>
+ </form>
+
+ <section v-if="reviewGroups.length" class="m-panel">
+ <h3 class="m-title">鎵归噺瀹℃牳</h3>
+ <button v-for="group in reviewGroups" :key="group.status" type="button" class="m-secondary" :disabled="busy" @click="selectReview(group)">{{ statusNames[group.status] }} 路 {{ Math.min(group.ids.length, 500) }} 鏉�</button>
+ <form v-if="reviewGroup" @submit.prevent="submitReview">
+ <label class="m-field"><span>瀹℃牳缁撹</span><select v-model="reviewApproved" :disabled="busy"><option :value="true">閫氳繃 / 纭澶勭悊</option><option :value="false">椹冲洖閲嶆柊妫�楠�</option></select></label>
+ <label class="m-field"><span>{{ reviewApproved ? '瀹℃牳鎰忚锛堥�夊~锛�' : '椹冲洖鎰忚锛堝繀濉級' }}</span><textarea v-model="reviewOpinion" rows="3" maxlength="500" /></label>
+ <button class="m-primary" :disabled="busy">{{ busy ? '姝e湪瀹℃牳鈥�' : `纭瀹℃牳 ${Math.min(reviewGroup.ids.length, 500)} 鏉 }}</button>
+ </form>
+ </section>
+
+ <form v-if="canRetest && (scope.retestIds.length || scope.initialRecheckIds.length)" class="m-panel" @submit.prevent="submitRetest">
+ <h3 class="m-title">鎵归噺澶嶆 / 閲嶆柊鍒濇</h3>
+ <label class="m-field"><span>璐ㄦ闃舵</span><select v-model="workflowStatus" :disabled="busy"><option v-if="scope.retestIds.length" :value="10">澶嶆锛坽{ scope.retestIds.length }} 琚嬶級</option><option v-if="scope.initialRecheckIds.length" :value="50">閲嶆柊鍒濇锛坽{ scope.initialRecheckIds.length }} 琚嬶級</option></select></label>
+ <p class="m-warning">鍚屼竴缁勬寚鏍囧皢鐢ㄤ簬鏈 {{ Math.min(targetIds.length, 500) }} 琚嬶紝璇锋牳瀵规娊鏍风粨鏋溿��</p>
+ <fieldset v-for="item in indicators" :key="`retest-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}锛�%锛�</legend><label class="m-field"><span>瀹炴祴鍊�</span><input v-model="item.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><div class="m-grid"><label class="m-field"><span>鍚堟牸涓嬮檺</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>鍚堟牸涓婇檺</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset>
+ <label class="m-field"><span>{{ complete && !passed ? '涓嶅悎鏍煎師鍥狅紙蹇呭~锛�' : '妫�楠屽娉�' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label>
+ <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{ busy ? '姝e湪鎻愪氦鈥�' : `鎻愪氦 ${Math.min(targetIds.length, 500)} 琚嬫楠宍 }}</button>
+ </form>
+ </template>
</section>
- <p v-if="notice" class="m-success" role="status">{{notice}}</p>
+ <section v-else class="m-panel"><h2 class="m-title">鎵归噺妫�楠屼笌瀹℃牳</h2><p class="m-muted">褰撳墠璐﹀彿鍙湁璐ㄦ璁板綍鏌ョ湅鏉冮檺銆傚闇�鎵归噺鎿嶄綔锛岃鑱旂郴绠$悊鍛橀厤缃垵妫�銆佸妫�鎴栧鏍告潈闄愩��</p></section>
+
+ <p v-if="notice" class="m-success" role="status">{{ notice }}</p>
+ <p v-if="error" class="m-error" role="alert">{{ error }}</p>
<p class="m-muted">鏌ョ湅璐ㄦ缁撴灉銆佹壒娆′笌涓嶅悎鏍煎師鍥犮��</p>
<Scanner :disabled="busy" @code="value => { code = value; load(); }" />
<form class="m-panel" @submit.prevent="load()">
<label class="m-field"><span>涓氬姟鐮� / 杩芥函鐮�</span><input v-model="code" :disabled="busy" placeholder="杈撳叆琚嬬爜" /></label>
<label class="m-field"><span>鐢熶骇鎵规</span><input v-model="batchCode" :disabled="busy" placeholder="杈撳叆鎵规鍙�" /></label>
<label class="m-field"><span>璐ㄦ缁撴灉</span><select v-model="result" :disabled="busy"><option value="">鍏ㄩ儴缁撴灉</option><option value="1">鍚堟牸</option><option value="2">涓嶅悎鏍�</option></select></label>
- <button class="m-secondary" :disabled="busy">{{busy ? '姝e湪鏌ヨ鈥�' : '鏌ヨ璁板綍'}}</button>
+ <button class="m-secondary" :disabled="busy">{{ busy ? '姝e湪鏌ヨ鈥�' : '鏌ヨ璁板綍' }}</button>
</form>
- <p v-if="error" class="m-error" role="alert">{{error}}</p>
- <p class="m-muted" role="status">鍏� {{total}} 鏉¤褰�</p>
+ <p class="m-muted" role="status">鍏� {{ total }} 鏉¤褰�</p>
<p v-if="!rows.length && !busy && !error" class="m-empty">鏆傛棤鍖归厤鐨勮川妫�璁板綍</p>
<article v-for="row in rows" :key="row.id" class="m-panel">
- <strong :class="['m-status', row.result === 1 ? 'success' : row.result === 2 ? 'failed' : '']">{{row.result === 1 ? '鍚堟牸' : row.result === 2 ? '涓嶅悎鏍�' : '澶勭悊涓�'}}</strong>
- <dl><div class="m-row"><dt>杩芥函鐮�</dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>鐢熶骇鎵规</dt><dd>{{row.batchCode || '鈥�'}}</dd></div><div class="m-row"><dt>娴佺▼鐘舵��</dt><dd>{{statusNames[row.workflowStatus] || '鏈煡鐘舵��'}}</dd></div><div class="m-row"><dt>妫�楠屾椂闂�</dt><dd>{{row.inspectedAt || row.createTime || '鈥�'}}</dd></div><div v-if="row.reason" class="m-row"><dt>鍘熷洜 / 澶囨敞</dt><dd>{{row.reason}}</dd></div></dl>
+ <strong :class="['m-status', row.result === 1 ? 'success' : row.result === 2 ? 'failed' : '']">{{ row.result === 1 ? '鍚堟牸' : row.result === 2 ? '涓嶅悎鏍�' : '澶勭悊涓�' }}</strong>
+ <dl><div class="m-row"><dt>杩芥函鐮�</dt><dd>{{ row.code }}</dd></div><div class="m-row"><dt>鐢熶骇鎵规</dt><dd>{{ row.batchCode || '鈥�' }}</dd></div><div class="m-row"><dt>娴佺▼鐘舵��</dt><dd>{{ statusNames[row.workflowStatus] || '鏈煡鐘舵��' }}</dd></div><div class="m-row"><dt>妫�楠屾椂闂�</dt><dd>{{ row.inspectedAt || row.createTime || '鈥�' }}</dd></div><div v-if="row.reason" class="m-row"><dt>鍘熷洜 / 澶囨敞</dt><dd>{{ row.reason }}</dd></div></dl>
</article>
- <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{busy ? '姝e湪鍔犺浇鈥�' : '鍔犺浇鏇村'}}</button>
+ <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{ busy ? '姝e湪鍔犺浇鈥�' : '鍔犺浇鏇村' }}</button>
</template>
--
Gitblit v1.9.3