From 346a988f537228ae44731cd31007f1412b482e83 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 16:32:10 +0800
Subject: [PATCH] refactor(mdm): 移除物料主数据中废弃的字段配置
---
src/views/mes/qc/seedRange/index.vue | 91 +++
src/views/basicData/mdm/modules/form.vue | 9
src/views/mes/qc/seedQuality/record.vue | 155 ++++-
src/views/mobile/quality-records.vue | 72 ++
src/views/mobile/quality.vue | 24
src/api/mes/qc/seedQuality/index.ts | 45 +
src/views/mes/pro/batch/modules/form.vue | 45 +
src/views/mes/qc/approverConfig/data.ts | 199 +++++++
src/views/mes/qc/oqc/index.vue | 129 ----
src/views/mes/qc/oqc/modules/form.vue | 4
/dev/null | 20
src/api/mes/pro/batch/index.ts | 4
src/api/mes/qc/approverConfig/index.ts | 53 ++
src/views/mes/qc/approverConfig/index.vue | 141 +++++
src/api/mes/qc/oqc/index.ts | 11
src/views/basicData/mdm/data.ts | 276 -----------
src/api/mes/pro/bagCode/index.ts | 5
src/views/mes/qc/seedQuality/index.vue | 37 +
src/views/mes/pro/batch/data.ts | 43 +
src/views/mes/qc/approverConfig/modules/form.vue | 92 +++
20 files changed, 956 insertions(+), 499 deletions(-)
diff --git a/src/api/mes/pro/bagCode/index.ts b/src/api/mes/pro/bagCode/index.ts
index 7ea1122..9d6e141 100644
--- a/src/api/mes/pro/bagCode/index.ts
+++ b/src/api/mes/pro/bagCode/index.ts
@@ -222,6 +222,11 @@
);
}
+/** 鍙粦瀹氭壒娆$殑鍚ㄨ锛堟棤鎵樼洏涓旀湭缁戝畾鎵规锛� */
+export function getUnboundTonBags() {
+ return requestClient.get<MesProBagCodeApi.BagCode[]>('/mes/pro/bag-code/unbound-ton-bags');
+}
+
/** 鎵归噺瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級 */
export function exportBagCodeExcel(params: MesProBagCodeApi.PageParams) {
return requestClient.download('/mes/pro/bag-code/export-excel', { params });
diff --git a/src/api/mes/pro/batch/index.ts b/src/api/mes/pro/batch/index.ts
index d681c62..327d14c 100644
--- a/src/api/mes/pro/batch/index.ts
+++ b/src/api/mes/pro/batch/index.ts
@@ -18,9 +18,10 @@
/** 鐢熶骇鎵规 */
export interface Batch {
id?: number; // 缂栧彿
- itemId?: number; // 鎵樼洏甯﹀叆鐨勫搧绉嶇紪鍙�
+ itemId?: number; // 鎵樼洏鎴栧惃琚嬪甫鍏ョ殑鍝佺缂栧彿
itemCode?: string;
itemName?: string;
+ bindingMode?: 'pallet' | 'tonBag'; // 鎵规宸叉湁缁戝畾鏂瑰紡
businessCode?: string; // 鍐呴儴鍑哄叆搴撲笟鍔$爜
code?: string; // 鎵规鍙凤紙琛ュ綍姝e紡淇℃伅鍚庤嚜鍔ㄧ敓鎴愶級
tempCode?: string; // 涓存椂鎵规鐮侊紙棰勭敓鎴愪簩缁寸爜闃舵浣跨敤锛屾壂鐮佽拷婧吋瀹癸級
@@ -39,6 +40,7 @@
createTime?: string; // 鍒涘缓鏃堕棿
workers?: BatchWorker[]; // 鐢熶骇浜哄憳閰嶇疆鍒楄〃
palletIds?: number[]; // 缁戝畾鎵樼洏缂栧彿鍒楄〃锛堜粎鏂板/琛ュ綍鎻愪氦鏃朵紶鍏ワ紝缁戝畾鏃舵壒娆$洿鎺ヨ繘鍏ョ敓浜т腑锛�
+ tonBagIds?: number[]; // 缁戝畾鍚ㄨ缂栧彿鍒楄〃锛涗笌鎵樼洏浜掓枼
}
/** 鐢熶骇鎵规鎵爜杩芥函 */
diff --git a/src/api/mes/qc/approverConfig/index.ts b/src/api/mes/qc/approverConfig/index.ts
new file mode 100644
index 0000000..28922e6
--- /dev/null
+++ b/src/api/mes/qc/approverConfig/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesQcApproverConfigApi {
+ /** MES 璐ㄦ瀹℃牳浜洪厤缃� */
+ export interface ApproverConfig {
+ id?: number; // 缂栧彿
+ qcType?: number; // 璐ㄦ绫诲瀷锛�1绉嶅瓙璐ㄦ
+ approveNode?: number; // 瀹℃牳鑺傜偣锛�1鍔犲伐閮ㄥ鎵� 3鍏徃璐ㄦ瀹℃壒
+ userId?: number; // 瀹℃牳浜虹敤鎴� ID
+ userNickname?: string; // 瀹℃牳浜烘樀绉帮紙鍏宠仈鏌ヨ锛�
+ status?: number; // 鐘舵�侊細0鍚敤 1鍋滅敤
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 鏌ヨ璐ㄦ瀹℃牳浜洪厤缃垎椤� */
+export function getApproverConfigPage(params: PageParam) {
+ return requestClient.get<PageResult<MesQcApproverConfigApi.ApproverConfig>>(
+ '/mes/qc/approver-config/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ璐ㄦ瀹℃牳浜洪厤缃鎯� */
+export function getApproverConfig(id: number) {
+ return requestClient.get<MesQcApproverConfigApi.ApproverConfig>(
+ `/mes/qc/approver-config/get?id=${id}`,
+ );
+}
+
+/** 鏂板璐ㄦ瀹℃牳浜洪厤缃� */
+export function createApproverConfig(
+ data: MesQcApproverConfigApi.ApproverConfig,
+) {
+ return requestClient.post('/mes/qc/approver-config/create', data);
+}
+
+/** 淇敼璐ㄦ瀹℃牳浜洪厤缃� */
+export function updateApproverConfig(
+ data: MesQcApproverConfigApi.ApproverConfig,
+) {
+ return requestClient.put('/mes/qc/approver-config/update', data);
+}
+
+/** 鍒犻櫎璐ㄦ瀹℃牳浜洪厤缃� */
+export function deleteApproverConfig(id: number) {
+ return requestClient.delete(
+ `/mes/qc/approver-config/delete?id=${id}`,
+ );
+}
diff --git a/src/api/mes/qc/oqc/index.ts b/src/api/mes/qc/oqc/index.ts
index c6ee5c0..793f6db 100644
--- a/src/api/mes/qc/oqc/index.ts
+++ b/src/api/mes/qc/oqc/index.ts
@@ -39,7 +39,6 @@
inspectorUserId?: number; // 妫�娴嬩汉鍛樼敤鎴� ID
inspectorNickname?: string; // 妫�娴嬩汉鍛樻樀绉帮紙鍏宠仈鏌ヨ锛�
status?: number; // 鐘舵��
- workflowStatus?: number; // 閿�鍞川妫�澶嶆鍜屽鎵圭姸鎬�
remark?: string; // 澶囨敞
}
}
@@ -69,16 +68,6 @@
/** 瀹屾垚鍑鸿揣妫�楠屽崟 */
export function finishOqc(id: number) {
return requestClient.put(`/mes/qc/oqc/finish?id=${id}`);
-}
-export function retestOqc(data: { id: number; checkQuantity: number; qualifiedQuantity: number; unqualifiedQuantity: number; checkResult: number; opinion?: string }) {
- return requestClient.post('/mes/qc/oqc/retest', data);
-}
-export function approveOqc(stage: 'sales' | 'company', data: { id: number; approved: boolean; opinion: string }) {
- return requestClient.post(`/mes/qc/oqc/${stage}-approve`, data);
-}
-export interface OqcAction { id: number; action: string; result?: number; opinion?: string; quantitiesJson?: string; operatorId?: number; createTime?: string }
-export function getOqcActions(id: number) {
- return requestClient.get<OqcAction[]>('/mes/qc/oqc/actions', { params: { id } });
}
/** 鍒犻櫎鍑鸿揣妫�楠屽崟 */
diff --git a/src/api/mes/qc/seedQuality/index.ts b/src/api/mes/qc/seedQuality/index.ts
index 415c043..3012db0 100644
--- a/src/api/mes/qc/seedQuality/index.ts
+++ b/src/api/mes/qc/seedQuality/index.ts
@@ -1,5 +1,20 @@
import { requestClient } from '#/api/request';
+export interface SeedIndicatorRange {
+ indicatorCode: string;
+ minValue: number;
+ maxValue: number;
+ updateTime?: string;
+}
+
+export function getSeedIndicatorRanges() {
+ return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges');
+}
+
+export function saveSeedIndicatorRanges(ranges: SeedIndicatorRange[]) {
+ return requestClient.put('/mes/qc/seed-quality/ranges', { ranges });
+}
+
export interface SeedQualityPageParams {
pageNo?: number;
pageSize?: number;
@@ -12,6 +27,7 @@
export interface SeedQualityRecord {
id: number;
code: string;
+ palletCode?: string;
batchCode?: string;
result: number;
workflowStatus: number;
@@ -23,7 +39,7 @@
}
export interface SeedQualityIndicator {
indicatorCode: string;
- value: number;
+ value?: number;
minValue: number;
maxValue: number;
}
@@ -45,6 +61,33 @@
export function retestSeedQuality(id: number, data: Omit<MesQcSeedQualityApi.SubmitParams, 'qualifiedWarehouseId'>) {
return requestClient.post('/mes/qc/seed-quality/retest', data, { params: { id } });
}
+
+export function retestSeedQualityBatch(data: {
+ ids: number[];
+ reason?: string;
+ indicators: MesQcSeedQualityApi.Indicator[];
+}) {
+ return requestClient.post('/mes/qc/seed-quality/retest-batch', data);
+}
+export interface SeedRetestScope {
+ code: string;
+ scanType: 'PALLET' | 'BATCH';
+ batchCode?: string;
+ totalCount: number;
+ retestIds: number[];
+ initialRecheckIds: number[];
+ pendingProcessCount: number;
+ pendingCompanyCount: number;
+}
+export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH') {
+ return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType } });
+}
+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);
+}
export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) {
return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data);
}
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index 7604cc1..490572c 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -129,56 +129,6 @@
},
},
{
- component: "ApiSelect",
- fieldName: "unitMeasureId2",
- label: "杈呭崟浣�1",
- formItemClass: "col-span-1",
- componentProps: {
- placeholder: "璇烽�夋嫨杈呭崟浣�1",
- allowClear: true,
- api: getUnitListCached,
- labelField: "name",
- valueField: "id",
- },
- },
- {
- component: "InputNumber",
- fieldName: "unitMeasureRate1",
- label: "杈呭崟浣�1鎹㈢畻姣旂巼",
- formItemClass: "col-span-1",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 6,
- placeholder: "璇疯緭鍏ユ崲绠楁瘮鐜�",
- },
- },
- {
- component: "ApiSelect",
- fieldName: "unitMeasureId3",
- label: "杈呭崟浣�2",
- formItemClass: "col-span-1",
- componentProps: {
- placeholder: "璇烽�夋嫨杈呭崟浣�2",
- allowClear: true,
- api: getUnitListCached,
- labelField: "name",
- valueField: "id",
- },
- },
- {
- component: "InputNumber",
- fieldName: "unitMeasureRate2",
- label: "杈呭崟浣�2鎹㈢畻姣旂巼",
- formItemClass: "col-span-1",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 6,
- placeholder: "璇疯緭鍏ユ崲绠楁瘮鐜�",
- },
- },
- {
component: 'ApiSelect',
fieldName: 'warehouseId',
label: '榛樿浠撳簱',
@@ -205,102 +155,6 @@
},
},
{
- component: "Input",
- fieldName: "barCode",
- label: "鏉$爜",
- componentProps: {
- placeholder: "璇疯緭鍏ユ潯鐮�",
- },
- },
- {
- component: "InputNumber",
- fieldName: "salesPrice",
- label: "閿�鍞环",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 2,
- placeholder: "璇疯緭鍏ラ攢鍞环",
- },
- },
- {
- component: "InputNumber",
- fieldName: "costPrice",
- label: "鎴愭湰浠�",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 2,
- placeholder: "璇疯緭鍏ユ垚鏈环",
- },
- },
- {
- component: "InputNumber",
- fieldName: "safetyStock",
- label: "瀹夊叏搴撳瓨",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 2,
- placeholder: "璇疯緭鍏ュ畨鍏ㄥ簱瀛�",
- },
- },
- {
- component: "InputNumber",
- fieldName: "minStock",
- label: "鏈�浣庡簱瀛�",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 2,
- placeholder: "璇疯緭鍏ユ渶浣庡簱瀛�",
- },
- },
- {
- component: "InputNumber",
- fieldName: "maxStock",
- label: "鏈�楂樺簱瀛�",
- componentProps: {
- class: "!w-full",
- min: 0,
- precision: 2,
- placeholder: "璇疯緭鍏ユ渶楂樺簱瀛�",
- },
- },
- {
- component: 'Switch',
- fieldName: 'isBatchManaged',
- label: '鎵规绠$悊',
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- rules: z.boolean().default(false),
- },
- {
- component: "InputNumber",
- fieldName: "expiryDay",
- label: "鏈夋晥鏈�(澶�)",
- componentProps: {
- class: "!w-full",
- min: 0,
- placeholder: "璇疯緭鍏ユ湁鏁堟湡",
- },
- },
- {
- component: 'Switch',
- fieldName: 'syncMes',
- label: '鍚屾鍒癕ES',
- defaultValue: true,
- componentProps: {
- checkedChildren: '鏄�',
- unCheckedChildren: '鍚�',
- },
- rules: z.boolean().default(true),
- },
- {
component: "RadioGroup",
fieldName: "status",
label: "鐘舵��",
@@ -310,136 +164,6 @@
optionType: "button",
},
rules: z.number().default(CommonStatusEnum.ENABLE),
- },
- {
- component: "Textarea",
- fieldName: "remark",
- label: "澶囨敞",
- formItemClass: "col-span-2",
- componentProps: {
- placeholder: "璇疯緭鍏ュ娉�",
- rows: 3,
- },
- },
- // ===== 鎵规灞炴�ч厤缃紙isBatchManaged = true 鏃舵樉绀猴級 =====
- {
- component: "Divider",
- fieldName: "batchConfigDivider",
- label: "鎵规灞炴�ч厤缃�",
- formItemClass: "col-span-2",
- componentProps: {
- orientation: "left",
- plain: true,
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "produceDateFlag",
- label: "鐢熶骇鏃ユ湡蹇呭~",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "expireDateFlag",
- label: "鏈夋晥鏈熷繀濉�",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "receiptDateFlag",
- label: "鍏ュ簱鏃ユ湡蹇呭~",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "vendorFlag",
- label: "渚涘簲鍟嗗繀濉�",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "purchaseOrderCodeFlag",
- label: "閲囪喘璁㈠崟鍙峰繀濉�",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "lotNumberFlag",
- label: "鐢熶骇鎵瑰彿蹇呭~",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
- },
- {
- component: "Switch",
- fieldName: "qualityStatusFlag",
- label: "璐ㄩ噺鐘舵�佸繀濉�",
- defaultValue: false,
- componentProps: {
- class: "w-auto",
- checkedChildren: "鏄�",
- unCheckedChildren: "鍚�",
- },
- dependencies: {
- triggerFields: ["isBatchManaged"],
- show: (values) => values.isBatchManaged === true,
- },
},
];
}
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
index 00191f6..12fff0f 100644
--- a/src/views/basicData/mdm/modules/form.vue
+++ b/src/views/basicData/mdm/modules/form.vue
@@ -38,7 +38,12 @@
return;
}
modalApi.lock();
- const data = (await formApi.getValues()) as MdmItemApi.Item;
+ // 缂栬緫鏃朵繚鐣欏垪琛ㄦ湭灞曠ず鐨勫巻鍙插瓧娈碉紝閬垮厤淇濆瓨绮剧畝琛ㄥ崟鏃舵竻绌哄畠浠��
+ const data = {
+ ...formData.value,
+ ...(await formApi.getValues()),
+ syncMes: formData.value?.syncedMes ?? true,
+ } as MdmItemApi.Item;
try {
await (formData.value?.id ? updateItem(data) : createItem(data));
await modalApi.close();
@@ -63,8 +68,6 @@
modalApi.lock();
try {
formData.value = await getItem(data.id);
- // 鍚屾鐘舵�佹槧灏勶細璇︽儏鎺ュ彛杩斿洖 syncedMes锛岃〃鍗曞瓧娈典负 syncMes
- formData.value.syncMes = formData.value.syncedMes ?? true;
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
diff --git a/src/views/mes/pro/batch/data.ts b/src/views/mes/pro/batch/data.ts
index 408d018..579f7ba 100644
--- a/src/views/mes/pro/batch/data.ts
+++ b/src/views/mes/pro/batch/data.ts
@@ -23,11 +23,16 @@
itemCode?: string;
}
+export type TonBagOption = PalletOption;
+
/** 鏂板/淇敼鐨勮〃鍗� */
export function useFormSchema(
formType: FormType,
palletOptions: PalletOption[] = [],
onPalletChange?: (ids: number[]) => void,
+ tonBagOptions: TonBagOption[] = [],
+ onTonBagChange?: (ids: number[]) => void,
+ existingBinding?: 'pallet' | 'tonBag',
): VbenFormSchema[] {
const disabled = formType === 'detail';
return [
@@ -141,11 +146,11 @@
},
{
fieldName: 'itemName', label: '鍝佺鍚嶇О', component: 'Input',
- componentProps: { disabled: true, placeholder: '閫夋嫨鎵樼洏鍚庤嚜鍔ㄥ甫鍏�' },
+ componentProps: { disabled: true, placeholder: '閫夋嫨鎵樼洏鎴栧惃琚嬪悗鑷姩甯﹀叆' },
},
{
fieldName: 'itemCode', label: '鍝佺缂栫爜', component: 'Input',
- componentProps: { disabled: true, placeholder: '閫夋嫨鎵樼洏鍚庤嚜鍔ㄥ甫鍏�' },
+ componentProps: { disabled: true, placeholder: '閫夋嫨鎵樼洏鎴栧惃琚嬪悗鑷姩甯﹀叆' },
},
{
fieldName: 'palletIds',
@@ -165,17 +170,47 @@
showSearch: true,
},
dependencies: {
- triggerFields: ['itemId'],
+ triggerFields: ['itemId', 'tonBagIds'],
show: () => formType !== 'detail',
componentProps: (values) => ({
options: palletOptions.map(option => ({ ...option,
- disabled: !option.itemId || (!!values.itemId && option.itemId !== values.itemId),
+ disabled: existingBinding === 'tonBag' || !!values.tonBagIds?.length || !option.itemId || (!!values.itemId && option.itemId !== values.itemId),
})),
+ disabled: disabled || existingBinding === 'tonBag' || !!values.tonBagIds?.length,
}),
},
help: '鍙兘缁戝畾鐩稿悓鍝佺鐨勬墭鐩橈紝鍝佺淇℃伅鑷姩甯﹀叆锛涚己灏戝搧绉嶇殑鎵樼洏璇峰厛瑁呰銆傜粦瀹氬悗鎵规鐩存帴杩涘叆鐢熶骇涓細鎵樼洏鐢熸垚姝e紡鎵樼洏鐮侊紝鎵樼洏涓嬫湭褰曟壒娆$殑琚嬬爜鑷姩绾ц仈褰曞叆鏈壒娆★紙琚嬬爜鍝佺椤讳竴鑷达級锛涚紪杈戞椂浠呴渶鍕鹃�夋柊澧炴墭鐩橈紝宸茬粦瀹氱殑涓嶅彲瑙g粦',
},
{
+ fieldName: 'tonBagIds',
+ label: '缁戝畾鍚ㄨ',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ disabled,
+ maxTagCount: 3,
+ dropdownMatchSelectWidth: false,
+ dropdownStyle: { minWidth: '380px', maxWidth: 'calc(100vw - 32px)' },
+ mode: 'multiple',
+ optionFilterProp: 'label',
+ options: tonBagOptions,
+ onChange: onTonBagChange,
+ placeholder: '璇烽�夋嫨鐩稿悓鍝佺鐨勫惃琚�',
+ showSearch: true,
+ },
+ dependencies: {
+ triggerFields: ['itemId', 'palletIds'],
+ show: () => formType !== 'detail',
+ componentProps: (values) => ({
+ options: tonBagOptions.map(option => ({ ...option,
+ disabled: existingBinding === 'pallet' || !!values.palletIds?.length || (!!values.itemId && option.itemId !== values.itemId),
+ })),
+ disabled: disabled || existingBinding === 'pallet' || !!values.palletIds?.length,
+ }),
+ },
+ help: '浠呭彲缁戝畾璁¢噺鍗曚綅涓哄惃銆佹棤鎵樼洏涓旀湭褰曞叆鎵规鐨勮鐮侊紱涓庣粦瀹氭墭鐩樹簰鏂ャ�傜紪杈戞椂浠呴�夋嫨鏂板鍚ㄨ锛屽凡缁戝畾鐨勪笉鍙В缁�',
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
diff --git a/src/views/mes/pro/batch/modules/form.vue b/src/views/mes/pro/batch/modules/form.vue
index b909f02..f27d9bb 100644
--- a/src/views/mes/pro/batch/modules/form.vue
+++ b/src/views/mes/pro/batch/modules/form.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
- import type { FormType, PalletOption } from "../data";
+ import type { FormType, PalletOption, TonBagOption } from "../data";
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
import type { MesProBatchApi } from "#/api/mes/pro/batch";
@@ -22,6 +22,7 @@
updateBatch,
} from "#/api/mes/pro/batch";
import { getUnboundPalletList } from "#/api/mes/pro/pallet";
+ import { getUnboundTonBags } from "#/api/mes/pro/bagCode";
import { $t } from "#/locales";
import { UserSelect } from "#/views/system/user/components";
@@ -38,12 +39,37 @@
const formType = ref<FormType>("create");
const formData = ref<MesProBatchApi.Batch>();
let palletOptions: PalletOption[] = [];
+ let tonBagOptions: TonBagOption[] = [];
async function handlePalletChange(ids: number[] = []) {
+ const values = await formApi.getValues();
+ if (ids.length && (values.tonBagIds?.length || formData.value?.bindingMode === 'tonBag')) {
+ message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+ await formApi.setFieldValue('palletIds', []);
+ return;
+ }
const selected = palletOptions.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('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勬墭鐩�');
await formApi.setFieldValue('palletIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value));
+ }
+ await formApi.setFieldValue('itemId', source?.itemId);
+ await formApi.setFieldValue('itemName', source?.itemName);
+ await formApi.setFieldValue('itemCode', source?.itemCode);
+ }
+
+ async function handleTonBagChange(ids: number[] = []) {
+ const values = await formApi.getValues();
+ if (ids.length && (values.palletIds?.length || formData.value?.bindingMode === 'pallet')) {
+ message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+ await formApi.setFieldValue('tonBagIds', []);
+ return;
+ }
+ const selected = tonBagOptions.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('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勫惃琚�');
+ await formApi.setFieldValue('tonBagIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value));
}
await formApi.setFieldValue('itemId', source?.itemId);
await formApi.setFieldValue('itemName', source?.itemName);
@@ -198,6 +224,10 @@
modalApi.lock();
try {
const data = (await formApi.getValues()) as MesProBatchApi.Batch;
+ if (data.palletIds?.length && data.tonBagIds?.length) {
+ message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+ return;
+ }
if (formType.value === "fill") {
await fillBatchInfo({ ...data, id: formData.value!.id, workers });
} else if (formData.value?.id) {
@@ -221,18 +251,25 @@
formType.value = data.formType;
// 鏂板/琛ュ綍/缂栬緫鎬佸姞杞芥湭缁戝畾鎵规鐨勬墭鐩橈紝渚涖�岀粦瀹氭墭鐩樸�嶅閫変笅鎷変娇鐢�
palletOptions = [];
+ tonBagOptions = [];
if (formType.value !== "detail") {
- const pallets = await getUnboundPalletList();
+ const [pallets, tonBags] = await Promise.all([getUnboundPalletList(), getUnboundTonBags()]);
palletOptions = 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) => ({
+ label: `${bag.code ?? `鍚ㄨ${bag.id}`} 路 ${bag.itemName || '寰呭畬鍠勫搧绉�'}`,
+ value: bag.id as number,
+ itemId: bag.itemId, itemName: bag.itemName, itemCode: bag.itemCode,
+ }));
}
- formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange) });
+ formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange) });
if (formType.value !== "detail") {
// 娓呯┖涓婃鎵撳紑鏃跺嬀閫夌殑鎵樼洏锛岄伩鍏嶆畫鐣欏�艰瑙﹀彂銆屾壒娆¤繘鍏ョ敓浜т腑銆�
await formApi.setFieldValue("palletIds", undefined);
+ await formApi.setFieldValue("tonBagIds", undefined);
}
// 鏍规嵁琛ㄥ崟鎬佸垏鎹汉鍛樺瓙琛ㄥ垪
workerGridApi.setGridOptions({
@@ -244,6 +281,8 @@
modalApi.lock();
try {
formData.value = await getBatch(data.id);
+ const existingBinding = formData.value.bindingMode;
+ formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange, existingBinding) });
await formApi.setValues(formData.value);
const workers = (formData.value.workers ?? []).map(
(worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
diff --git a/src/views/mes/qc/approverConfig/data.ts b/src/views/mes/qc/approverConfig/data.ts
new file mode 100644
index 0000000..be42021
--- /dev/null
+++ b/src/views/mes/qc/approverConfig/data.ts
@@ -0,0 +1,199 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 璐ㄦ绫诲瀷 */
+export const QC_TYPE_OPTIONS = [{ label: '绉嶅瓙璐ㄦ', value: 1 }];
+
+/** 瀹℃牳鑺傜偣 */
+export const APPROVE_NODE_OPTIONS = [
+ { label: '鍒濇瀹℃牳', value: 1 },
+ { label: '澶嶆瀹℃牳', value: 3 },
+];
+
+/** 鎸夎川妫�绫诲瀷杩斿洖鍙�夌殑瀹℃牳鑺傜偣 */
+export function getNodeOptions(_qcType?: number) {
+ return APPROVE_NODE_OPTIONS;
+}
+
+export function getQcTypeLabel(qcType?: number): string {
+ return QC_TYPE_OPTIONS.find((item) => item.value === qcType)?.label || '鈥�';
+}
+
+export function getApproveNodeLabel(approveNode?: number): string {
+ return (
+ APPROVE_NODE_OPTIONS.find((item) => item.value === approveNode)?.label ||
+ '鈥�'
+ );
+}
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'qcType',
+ label: '璐ㄦ绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: QC_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨璐ㄦ绫诲瀷',
+ onChange: async () => {
+ await formApi?.setFieldValue('approveNode', undefined);
+ },
+ },
+ defaultValue: 1,
+ rules: 'required',
+ },
+ {
+ fieldName: 'approveNode',
+ label: '瀹℃牳鑺傜偣',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: APPROVE_NODE_OPTIONS,
+ placeholder: '璇烽�夋嫨瀹℃牳鑺傜偣',
+ },
+ dependencies: {
+ triggerFields: ['qcType'],
+ componentProps: (values) => ({
+ allowClear: true,
+ options: getNodeOptions(values.qcType),
+ placeholder: '璇烽�夋嫨瀹℃牳鑺傜偣',
+ }),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userId',
+ label: '瀹℃牳浜�',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨瀹℃牳浜�',
+ valueField: 'id',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ defaultValue: 0,
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'qcType',
+ label: '璐ㄦ绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: QC_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨璐ㄦ绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'approveNode',
+ label: '瀹℃牳鑺傜偣',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: APPROVE_NODE_OPTIONS,
+ placeholder: '璇烽�夋嫨瀹℃牳鑺傜偣',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>['columns'] {
+ return [
+ {
+ field: 'qcType',
+ title: '璐ㄦ绫诲瀷',
+ width: 140,
+ slots: { default: 'qcType' },
+ },
+ {
+ field: 'approveNode',
+ title: '瀹℃牳鑺傜偣',
+ width: 160,
+ slots: { default: 'approveNode' },
+ },
+ {
+ field: 'userNickname',
+ title: '瀹℃牳浜�',
+ width: 140,
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/qc/approverConfig/index.vue b/src/views/mes/qc/approverConfig/index.vue
new file mode 100644
index 0000000..feabdcb
--- /dev/null
+++ b/src/views/mes/qc/approverConfig/index.vue
@@ -0,0 +1,141 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteApproverConfig,
+ getApproverConfigPage,
+} from '#/api/mes/qc/approverConfig';
+import { $t } from '#/locales';
+
+import {
+ getApproveNodeLabel,
+ getQcTypeLabel,
+ useGridColumns,
+ useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鍒涘缓瀹℃牳浜洪厤缃� */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫瀹℃牳浜洪厤缃� */
+function handleEdit(row: MesQcApproverConfigApi.ApproverConfig) {
+ formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎瀹℃牳浜洪厤缃� */
+async function handleDelete(row: MesQcApproverConfigApi.ApproverConfig) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.userNickname || '']),
+ duration: 0,
+ });
+ try {
+ await deleteApproverConfig(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.userNickname || '']));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getApproverConfigPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>,
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+
+ <Grid table-title="璐ㄦ瀹℃牳浜洪厤缃�">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['瀹℃牳浜洪厤缃�']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:qc-approver:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #qcType="{ row }">
+ <span>{{ getQcTypeLabel(row.qcType) }}</span>
+ </template>
+ <template #approveNode="{ row }">
+ <span>{{ getApproveNodeLabel(row.approveNode) }}</span>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:qc-approver:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:qc-approver:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.userNickname || '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/qc/approverConfig/modules/form.vue b/src/views/mes/qc/approverConfig/modules/form.vue
new file mode 100644
index 0000000..36a6b7f
--- /dev/null
+++ b/src/views/mes/qc/approverConfig/modules/form.vue
@@ -0,0 +1,92 @@
+<script lang="ts" setup>
+import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createApproverConfig,
+ getApproverConfig,
+ updateApproverConfig,
+} from '#/api/mes/qc/approverConfig';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesQcApproverConfigApi.ApproverConfig>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['瀹℃牳浜洪厤缃�'])
+ : $t('ui.actionTitle.create', ['瀹℃牳浜洪厤缃�']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data =
+ (await formApi.getValues()) as MesQcApproverConfigApi.ApproverConfig;
+ try {
+ await (formData.value?.id
+ ? updateApproverConfig(data)
+ : createApproverConfig(data));
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ // 鍔犺浇鏁版嵁
+ const data = modalApi.getData<MesQcApproverConfigApi.ApproverConfig>();
+ if (!data || !data.id) {
+ await formApi.resetForm();
+ await formApi.setValues({ qcType: 1, status: 0 });
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getApproverConfig(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/qc/oqc/index.vue b/src/views/mes/qc/oqc/index.vue
index 92d4cea..965cfda 100644
--- a/src/views/mes/qc/oqc/index.vue
+++ b/src/views/mes/qc/oqc/index.vue
@@ -1,95 +1,19 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcOqcApi } from '#/api/mes/qc/oqc';
-import type { OqcAction } from '#/api/mes/qc/oqc';
-
-import { ref } from 'vue';
-import { useAccess } from '@vben/access';
import { Page, useVbenModal } from '@vben/common-ui';
-import { MesBizTypeEnum, MesQcStatusEnum } from '@vben/constants';
+import { MesQcStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { approveOqc, deleteOqc, exportOqc, getOqcActions, getOqcPage, retestOqc } from '#/api/mes/qc/oqc';
+import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
-
-const props = withDefaults(defineProps<{ salesMode?: boolean }>(), { salesMode: false });
-const { hasAccessByCodes } = useAccess();
-const workflowLabels: Record<number, string> = {
- 0: '鑽夌', 10: '寰呭妫�', 20: '寰呴攢鍞儴瀹℃壒', 30: '寰呭叕鍙歌川妫�瀹℃壒',
- 40: '宸叉斁琛�', 50: '寰呴攢鍞儴涓嶅悎鏍艰瘎瀹�', 60: '寰呭叕鍙镐笉鍚堟牸瀹℃壒', 70: '宸叉嫆缁濆嚭璐�',
-};
-const active = ref<MesQcOqcApi.Oqc>();
-const retestOpen = ref(false);
-const retestBusy = ref(false);
-const retestForm = ref({ checkQuantity: 0, qualifiedQuantity: 0, unqualifiedQuantity: 0, opinion: '' });
-function openRetest(row: MesQcOqcApi.Oqc) {
- active.value = row;
- retestForm.value = {
- checkQuantity: Number(row.checkQuantity ?? 0), qualifiedQuantity: Number(row.qualifiedQuantity ?? 0),
- unqualifiedQuantity: Number(row.unqualifiedQuantity ?? 0), opinion: '',
- };
- retestOpen.value = true;
-}
-async function saveRetest() {
- if (!active.value?.id) return;
- const form = retestForm.value;
- if (form.checkQuantity < 0 || form.qualifiedQuantity < 0 || form.unqualifiedQuantity < 0
- || form.checkQuantity !== form.qualifiedQuantity + form.unqualifiedQuantity) {
- message.warning('妫�娴嬫暟閲忓繀椤荤瓑浜庡悎鏍间笌涓嶅悎鏍兼暟閲忎箣鍜�');
- return;
- }
- if (form.unqualifiedQuantity > 0 && !form.opinion.trim()) {
- message.warning('澶嶆涓嶅悎鏍艰濉啓鍘熷洜');
- return;
- }
- retestBusy.value = true;
- try {
- await retestOqc({ id: active.value.id, ...form, checkResult: form.unqualifiedQuantity === 0 ? 1 : 2 });
- message.success('澶嶆缁撴灉宸叉彁浜わ紝寰呴攢鍞儴璇勫');
- retestOpen.value = false;
- handleRefresh();
- } catch (error) {
- message.error((error as Error).message || '澶嶆澶辫触');
- } finally { retestBusy.value = false; }
-}
-const reviewOpen = ref(false);
-const reviewBusy = ref(false);
-const reviewStage = ref<'sales' | 'company'>('sales');
-const reviewApproved = ref(true);
-const reviewOpinion = ref('');
-function openReview(row: MesQcOqcApi.Oqc, stage: 'sales' | 'company') {
- active.value = row; reviewStage.value = stage; reviewApproved.value = true;
- reviewOpinion.value = ''; reviewOpen.value = true;
-}
-async function saveReview() {
- if (!active.value?.id) return;
- if (!reviewOpinion.value.trim()) { message.warning('璇峰~鍐欏鎵规剰瑙�'); return; }
- reviewBusy.value = true;
- try {
- await approveOqc(reviewStage.value, { id: active.value.id, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() });
- message.success('瀹℃壒瀹屾垚'); reviewOpen.value = false; handleRefresh();
- } catch (error) { message.error((error as Error).message || '瀹℃壒澶辫触'); }
- finally { reviewBusy.value = false; }
-}
-const historyOpen = ref(false);
-const historyActions = ref<OqcAction[]>([]);
-const actionLabels: Record<string, string> = {
- INITIAL: '鍒濇', RETEST: '澶嶆', SALES_APPROVE: '閿�鍞儴閫氳繃', SALES_REJECT: '閿�鍞儴椹冲洖',
- COMPANY_APPROVE: '鍏徃璐ㄦ閫氳繃', COMPANY_REJECT: '鍏徃璐ㄦ椹冲洖',
-};
-async function openHistory(row: MesQcOqcApi.Oqc) {
- if (!row.id) return;
- active.value = row;
- try { historyActions.value = await getOqcActions(row.id); historyOpen.value = true; }
- catch (error) { message.error((error as Error).message || '璇诲彇娴佺▼璁板綍澶辫触'); }
-}
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@@ -134,23 +58,15 @@
/** 瀵煎嚭琛ㄦ牸 */
async function handleExport() {
const data = await exportOqc(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: props.salesMode ? '閿�鍞川妫�鍗�.xls' : '鍑鸿揣妫�楠屽崟.xls', source: data });
+ downloadFileFromBlobPart({ fileName: '鍑鸿揣妫�楠屽崟.xls', source: data });
}
-const columns = useGridColumns();
-if (props.salesMode && columns) {
- columns.splice(columns.length - 1, 0, {
- field: 'workflowStatus', title: '閿�鍞川妫�娴佺▼', width: 170, slots: { default: 'workflow' },
- });
- const actions = columns.at(-1);
- if (actions) actions.width = 320;
-}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
- columns,
+ columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -160,7 +76,6 @@
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
- sourceDocType: props.salesMode ? MesBizTypeEnum.WM_PRODUCT_SALES : undefined,
});
},
},
@@ -180,7 +95,7 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <Grid :table-title="props.salesMode ? '閿�鍞川妫�鍗曞垪琛�' : '鍑鸿揣妫�楠屽崟鍒楄〃'">
+ <Grid table-title="鍑鸿揣妫�楠屽崟鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
@@ -205,9 +120,6 @@
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
- </template>
- <template #workflow="{ row }">
- <span>{{ workflowLabels[row.workflowStatus ?? 0] || '鏈煡鐘舵��' }}</span>
</template>
<template #actions="{ row }">
<TableAction
@@ -241,38 +153,7 @@
},
]"
/>
- <Button v-if="props.salesMode && row.workflowStatus === 10 && hasAccessByCodes(['mes:qc-oqc:retest'])" type="link" @click="openRetest(row)">澶嶆</Button>
- <Button v-if="props.salesMode && [20, 50].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:sales-approve'])" type="link" @click="openReview(row, 'sales')">閿�鍞儴瀹℃壒</Button>
- <Button v-if="props.salesMode && [30, 60].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:company-approve'])" type="link" @click="openReview(row, 'company')">鍏徃璐ㄦ瀹℃壒</Button>
- <Button v-if="props.salesMode && hasAccessByCodes(['mes:qc-oqc:query'])" type="link" @click="openHistory(row)">娴佺▼璁板綍</Button>
</template>
</Grid>
- <a-modal v-if="props.salesMode" v-model:open="retestOpen" title="閿�鍞川妫�澶嶆" :confirm-loading="retestBusy" ok-text="鎻愪氦澶嶆" @ok="saveRetest">
- <p>妫�楠屽崟锛歿{ active?.code }}</p>
- <a-form layout="vertical">
- <a-form-item label="鏈妫�娴嬫暟閲�"><a-input-number v-model:value="retestForm.checkQuantity" :min="0" style="width: 100%" /></a-form-item>
- <a-form-item label="鍚堟牸鏁伴噺"><a-input-number v-model:value="retestForm.qualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
- <a-form-item label="涓嶅悎鏍兼暟閲�"><a-input-number v-model:value="retestForm.unqualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
- <a-form-item label="澶嶆鎰忚"><a-textarea v-model:value="retestForm.opinion" :rows="3" :maxlength="500" /></a-form-item>
- </a-form>
- </a-modal>
- <a-modal v-if="props.salesMode" v-model:open="reviewOpen" :title="reviewStage === 'sales' ? '閿�鍞儴瀹℃壒' : '鍏徃璐ㄦ瀹℃壒'" :confirm-loading="reviewBusy" ok-text="鎻愪氦瀹℃壒" @ok="saveReview">
- <p>{{ active?.code }} 路 {{ workflowLabels[active?.workflowStatus ?? 0] }}</p>
- <a-radio-group v-model:value="reviewApproved" style="display: flex; gap: 16px; margin: 12px 0 18px">
- <a-radio :value="true">鍚屾剰{{ reviewStage === 'company' ? active?.workflowStatus === 30 ? '鍑鸿揣鏀捐' : '涓嶅悎鏍煎缃�' : '涓婃姤鍏徃璐ㄦ' }}</a-radio>
- <a-radio :value="false">椹冲洖閲嶆</a-radio>
- </a-radio-group>
- <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="璇峰~鍐欏鎵规剰瑙�" />
- </a-modal>
- <a-modal v-if="props.salesMode" v-model:open="historyOpen" :title="`娴佺▼璁板綍 路 ${active?.code || ''}`" :footer="null">
- <a-timeline v-if="historyActions.length">
- <a-timeline-item v-for="action in historyActions" :key="action.id">
- <strong>{{ actionLabels[action.action] || action.action }}</strong>
- <span> 路 {{ action.createTime || '鈥�' }} 路 鎿嶄綔浜� {{ action.operatorId || '鈥�' }}</span>
- <div v-if="action.opinion">{{ action.opinion }}</div>
- </a-timeline-item>
- </a-timeline>
- <a-empty v-else description="鏆傛棤娴佺▼璁板綍" />
- </a-modal>
</Page>
</template>
diff --git a/src/views/mes/qc/oqc/modules/form.vue b/src/views/mes/qc/oqc/modules/form.vue
index 6dcd7cc..2207b24 100644
--- a/src/views/mes/qc/oqc/modules/form.vue
+++ b/src/views/mes/qc/oqc/modules/form.vue
@@ -105,7 +105,7 @@
return;
}
try {
- await confirm('鏄惁鎻愪氦鍑鸿揣妫�楠岀粨璁猴紵鎻愪氦鍚庤繘鍏ュ鎵规祦绋嬶紝鍒濇涓嶅悎鏍奸渶鍏堝妫�銆�');
+ await confirm('鏄惁鎻愪氦鍑鸿揣妫�楠岀粨璁猴紵');
} catch {
return;
}
@@ -126,7 +126,7 @@
originalSnapshot.value = current;
}
await finishOqc(id);
- message.success('妫�楠屽凡鎻愪氦锛岃鍦ㄩ攢鍞川妫�鑿滃崟缁х画澶嶆鎴栧鎵�');
+ message.success('妫�楠屽凡鎻愪氦');
await modalApi.close();
emit('success');
} finally {
diff --git a/src/views/mes/qc/salesQuality/index.vue b/src/views/mes/qc/salesQuality/index.vue
deleted file mode 100644
index 4b8733a..0000000
--- a/src/views/mes/qc/salesQuality/index.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script setup lang="ts">
-import OqcIndex from '../oqc/index.vue';
-</script>
-
-<template>
- <div class="sales-quality-page">
- <div class="flow-notice">
- <strong>閿�鍞川妫�</strong>
- <span>閿�鍞嚭搴撳崟鍒濇銆佷笉鍚堟牸澶嶆銆侀攢鍞儴涓庡叕鍙歌川妫�瀹℃壒锛涘鎵瑰畬鎴愬悗鍥炲啓鍑哄簱璐ㄩ噺鐘舵�併��</span>
- </div>
- <OqcIndex sales-mode />
- </div>
-</template>
-
-<style scoped>
-.sales-quality-page { height: 100%; }
-.flow-notice { display: flex; align-items: center; gap: 12px; padding: 12px 20px; margin: 0 0 8px; border: 1px solid #d6e6fa; border-radius: 8px; background: #f2f7fe; color: #385071; }
-.flow-notice strong { flex: none; color: #1557a0; }
-@media (max-width: 720px) { .flow-notice { align-items: flex-start; flex-direction: column; } }
-</style>
diff --git a/src/views/mes/qc/seedQuality/index.vue b/src/views/mes/qc/seedQuality/index.vue
index a034bc7..e69dfe3 100644
--- a/src/views/mes/qc/seedQuality/index.vue
+++ b/src/views/mes/qc/seedQuality/index.vue
@@ -11,6 +11,7 @@
import {
resolveSeedQuality,
submitSeedQualityGroup,
+ getSeedIndicatorRanges,
} from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
@@ -59,6 +60,17 @@
{ code: 'germination_rate', label: '鍙戣娊鐜�', unit: '%' },
{ code: 'moisture', label: '姘村垎', unit: '%' },
]);
+const rangesReady = computed(() => indicators.value.every((item) =>
+ item.minValue !== undefined && item.maxValue !== undefined));
+
+async function loadRanges() {
+ const ranges = await getSeedIndicatorRanges();
+ for (const item of indicators.value) {
+ const range = ranges.find((row) => row.indicatorCode === item.code);
+ item.minValue = range?.minValue === undefined ? undefined : Number(range.minValue);
+ item.maxValue = range?.maxValue === undefined ? undefined : Number(range.maxValue);
+ }
+}
const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0);
const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0);
@@ -102,8 +114,6 @@
reason.value = '';
indicators.value.forEach((item) => {
item.value = undefined;
- item.minValue = undefined;
- item.maxValue = undefined;
});
}
@@ -132,6 +142,7 @@
resolving.value = true;
submittedCount.value = undefined;
try {
+ await loadRanges().catch(() => undefined);
const data = await resolveSeedQuality(value, scanType.value);
if (data.canceled) {
target.value = undefined;
@@ -166,7 +177,7 @@
return;
}
if (!allEntered.value) {
- message.warning('璇峰~鍐欏洓椤规寚鏍囩殑瀹炴祴鍊煎強鍚堟牸涓婁笅闄�');
+ message.warning('璇峰~鍐欏洓椤规寚鏍囩殑瀹炴祴鍊煎拰鏈鍚堟牸涓婁笅闄�');
return;
}
if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
@@ -190,8 +201,8 @@
Modal.confirm({
title: overallPassed.value ? '纭鎻愪氦鍚堟牸缁撴灉锛�' : '纭鎻愪氦涓嶅悎鏍肩粨鏋滐紵',
content: overallPassed.value
- ? `鏈 ${count} 琚嬪皢缁熶竴璁板綍涓哄垵妫�鍚堟牸锛屽緟鍔犲伐閮ㄥ拰鍏徃璐ㄦ瀹℃壒鍚庢墠杞叆鍚堟牸搴撱��${scopeNote}`
- : `鏈 ${count} 琚嬪皢璁板綍涓哄垵妫�涓嶅悎鏍硷紝淇濈暀鏆傚瓨搴撳瓨骞剁瓑寰呭妫�銆�${scopeNote}`,
+ ? `鏈 ${count} 琚嬪皢缁熶竴璁板綍涓哄垵妫�鍚堟牸锛屽緟鍒濇瀹℃牳閫氳繃鍚庡妫�锛涘妫�瀹℃牳閫氳繃鍚庢墠杞叆鍚堟牸搴撱��${scopeNote}`
+ : `鏈 ${count} 琚嬪皢璁板綍涓哄垵妫�涓嶅悎鏍硷紝淇濈暀鏆傚瓨搴撳瓨锛涘垵妫�瀹℃牳鍚庡喅瀹氭槸鍚﹁繘鍏ュ妫�銆�${scopeNote}`,
okText: '纭鎻愪氦',
cancelText: '杩斿洖淇敼',
okButtonProps: { danger: !overallPassed.value },
@@ -219,7 +230,7 @@
})),
});
submittedCount.value = result.processedCount;
- message.success(`鏈 ${result.processedCount} 琚嬪凡鎻愪氦锛�${passed ? '鍒濇鍚堟牸锛屽緟涓ょ骇瀹℃壒' : '鍒濇涓嶅悎鏍硷紝寰呭妫�'}`);
+ message.success(`鏈 ${result.processedCount} 琚嬪凡鎻愪氦鍒濇锛屽緟鍒濇瀹℃牳`);
if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
gsap.fromTo('.result-card', { scale: 0.97, autoAlpha: 0.5 }, { scale: 1, autoAlpha: 1, duration: 0.35, ease: 'power2.out' });
}
@@ -235,6 +246,11 @@
}
onMounted(async () => {
+ try {
+ await loadRanges();
+ } catch (error) {
+ message.error(errorText(error));
+ }
try {
const warehouses = await getWarehouseSimpleList();
qualifiedWarehouses.value = (warehouses ?? []).filter(
@@ -266,7 +282,7 @@
<div>
<div class="eyebrow">MES / 璐ㄩ噺绠$悊</div>
<h1>绉嶅瓙璐ㄦ</h1>
- <p>璇嗗埆寰呮鑼冨洿骞跺綍鍏ュ洓椤规寚鏍囷紱鍚堟牸缁忓姞宸ラ儴涓庡叕鍙歌川妫�瀹℃壒鍚庡叆搴擄紝涓嶅悎鏍煎厛澶嶆</p>
+ <p>璇嗗埆寰呮鑼冨洿骞跺綍鍏ュ洓椤规寚鏍囷紱鍒濇瀹℃牳鍚庡妫�锛屽妫�瀹℃牳鍚庡叆搴撴垨澶勭疆</p>
</div>
<div class="header-badge"><IconifyIcon icon="ep:checked" /> 琚嬬爜 / 鎵樼洏鐮� / 鎵规鐮�</div>
</header>
@@ -318,6 +334,7 @@
</a-form-item>
</div>
<p v-if="!qualifiedWarehouses.length" class="target-warning">鏆傛棤鍙敤鍚堟牸搴擄紝璇疯仈绯荤鐞嗗憳閰嶇疆鍚庡啀鎻愪氦璐ㄦ銆�</p>
+ <a-alert v-if="!rangesReady" class="target-empty" type="info" show-icon message="鏈璇锋墜鍔ㄥ~鍐欏悎鏍艰寖鍥�" description="绠$悊鍛樺皻鏈厤缃畬鏁撮粯璁よ寖鍥达紱浠嶅彲鎵嬪姩濉啓鍥涢」鎸囨爣鐨勫悎鏍间笂涓嬮檺銆�" />
<a-divider orientation="left">鎸囨爣缁撴灉</a-divider>
<div class="indicator-list">
@@ -325,9 +342,9 @@
<div class="indicator-name"><span class="indicator-dot" :class="isPassed(item) ? 'pass' : 'fail'" /><strong>{{ item.label }}</strong><small>{{ item.unit }}</small></div>
<a-input-number v-model:value="item.value" class="indicator-input" :disabled="loading" :min="0" :precision="3" placeholder="瀹炴祴鍊�" />
<span class="range-separator">鑼冨洿</span>
- <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="涓嬮檺" />
+ <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="涓嬮檺" />
<span>鈥�</span>
- <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="涓婇檺" />
+ <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="涓婇檺" />
<a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? '寰呭綍鍏�' : isPassed(item) ? '鍚堟牸' : '瓒呭嚭鑼冨洿' }}</a-tag>
</div>
</div>
@@ -341,7 +358,7 @@
<template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />鍒ゅ畾璇存槑</div></template>
<div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '褰撳墠缁撴灉鍚堟牸' : allEntered ? '褰撳墠瀛樺湪涓嶅悎鏍奸」' : '绛夊緟褰曞叆鎸囨爣' }}</strong><span>{{ allEntered ? '瀹炴祴鍊奸渶澶勪簬鏈�灏忓�间笌鏈�澶у�间箣闂�' : `宸插綍鍏� ${enteredCount} / 4 椤瑰疄娴嬪�糮 }}</span></div>
<div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>鍏堣瘑鍒爜鍊煎啀褰曞叆鎸囨爣锛屽洓椤规寚鏍囧繀椤诲叏閮ㄦ彁浜�</span></div><div><IconifyIcon icon="ep:check" /><span>杈圭晫鍊兼寜鍚堟牸澶勭悊</span></div><div><IconifyIcon icon="ep:check" /><span>鎵樼洏鐮併�佹壒娆$爜鐨勭粨璁哄簲鐢ㄤ簬鏈鍏ㄩ儴寰呮琚�</span></div><div><IconifyIcon icon="ep:check" /><span>涓嶅悎鏍肩粨鏋滈渶淇濈暀鍘熷洜</span></div><div><IconifyIcon icon="ep:check" /><span>璐ㄦ鍛樹笌妫�楠屾椂闂寸敱鏈嶅姟绔褰�</span></div></div>
- <a-alert message="瀹℃壒瀹屾垚鍚庢祦杞簱瀛�" description="鍒濇涓嶆敼鍙樺簱瀛橈紱涓嶅悎鏍煎緟澶嶆銆傚妫�缁撹缁忓姞宸ラ儴鍜屽叕鍙歌川妫�瀹℃壒鍚庯紝鍚堟牸琚嬫墠杞叆鍚堟牸搴擄紝涓嶅悎鏍艰鎵嶆姤搴熴��" type="info" show-icon />
+ <a-alert message="澶嶆瀹℃牳瀹屾垚鍚庢祦杞簱瀛�" description="鍒濇鍙婂妫�鏈熼棿琚嬬爜鐣欏湪鏆傚瓨搴撱�傚垵妫�瀹℃牳閫氳繃鍚庤繘鍏ュ妫�锛涘妫�鍚堟牸骞跺鏍搁�氳繃鎵嶈浆鍏ュ悎鏍煎簱锛屽妫�涓嶅悎鏍肩粡纭鍚庢墠鎶ュ簾銆�" type="info" show-icon />
<div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>鏈�杩戞彁浜よ川妫�锛�<strong>{{ submittedCount }} 琚�</strong></span></div>
</a-card>
</section>
diff --git a/src/views/mes/qc/seedQuality/record.vue b/src/views/mes/qc/seedQuality/record.vue
index a03ccd8..15396a1 100644
--- a/src/views/mes/qc/seedQuality/record.vue
+++ b/src/views/mes/qc/seedQuality/record.vue
@@ -10,12 +10,16 @@
getSeedQualityPage,
getSeedQualityIndicators,
getSeedQualityActions,
+ getSeedIndicatorRanges,
retestSeedQuality,
+ resolveSeedRetestScope,
+ submitSeedRetestScope,
approveSeedQuality,
approveSeedQualityBatch,
type SeedQualityRecord,
type SeedQualityIndicator,
type SeedQualityAction,
+ type SeedRetestScope,
} from '#/api/mes/qc/seedQuality';
const records = ref<SeedQualityRecord[]>([]);
@@ -37,13 +41,13 @@
);
const { hasAccessByCodes } = useAccess();
const workflowLabels: Record<number, string> = {
- 10: '寰呭妫�', 20: '寰呭姞宸ラ儴瀹℃壒', 30: '寰呭叕鍙歌川妫�瀹℃壒',
- 40: '宸叉斁琛�', 50: '寰呭姞宸ラ儴涓嶅悎鏍艰瘎瀹�', 60: '寰呭叕鍙镐笉鍚堟牸瀹℃壒', 70: '宸叉姤搴�',
+ 10: '寰呭妫�', 20: '寰呭垵妫�瀹℃牳', 25: '寰呭垵妫�涓嶅悎鏍煎鐞�', 30: '寰呭妫�瀹℃牳',
+ 40: '宸叉斁琛�', 50: '鍒濇椹冲洖寰呴噸妫�', 60: '寰呭妫�涓嶅悎鏍煎鐞�', 70: '宸叉姤搴�',
};
const actionLabels: Record<string, string> = {
- INITIAL: '鍒濇', RETEST: '澶嶆', PROCESS_APPROVE: '鍔犲伐閮ㄩ�氳繃',
- PROCESS_REJECT: '鍔犲伐閮ㄩ┏鍥�', COMPANY_APPROVE: '鍏徃璐ㄦ閫氳繃',
- COMPANY_REJECT: '鍏徃璐ㄦ椹冲洖',
+ INITIAL: '鍒濇', INITIAL_RECHECK: '閲嶆柊鍒濇', RETEST: '澶嶆', PROCESS_APPROVE: '鍒濇瀹℃牳閫氳繃',
+ PROCESS_REJECT: '鍒濇瀹℃牳椹冲洖', COMPANY_APPROVE: '澶嶆瀹℃牳閫氳繃',
+ COMPANY_REJECT: '澶嶆瀹℃牳椹冲洖',
};
const indicatorLabels: Record<string, string> = {
purity: '绾害', cleanliness: '鍑�搴�', germination_rate: '鍙戣娊鐜�', moisture: '姘村垎',
@@ -73,6 +77,13 @@
const retestBusy = ref(false);
const retestIndicators = ref<SeedQualityIndicator[]>([]);
const retestReason = ref('');
+const scopeOpen = ref(false);
+const scopeType = ref<'PALLET' | 'BATCH'>('PALLET');
+const scopeCode = ref('');
+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 reviewOpen = ref(false);
const reviewBusy = ref(false);
const reviewStage = ref<'process' | 'company'>('process');
@@ -84,7 +95,7 @@
const selectableStage = computed(() => {
if (!selectedRows.value.length) return undefined;
if (!selectedRows.value.every((record) => record.workflowStatus === selectedRows.value[0]?.workflowStatus)) return undefined;
- if (selectedRows.value.every((record) => [20, 50].includes(record.workflowStatus))) return 'process';
+ if (selectedRows.value.every((record) => [20, 25].includes(record.workflowStatus))) return 'process';
if (selectedRows.value.every((record) => [30, 60].includes(record.workflowStatus))) return 'company';
return undefined;
});
@@ -92,13 +103,65 @@
async function openRetest(record: SeedQualityRecord) {
activeRecord.value = record;
try {
- retestIndicators.value = await getSeedQualityIndicators(record.id);
+ const previous = await getSeedQualityIndicators(record.id);
+ const ranges = await getSeedIndicatorRanges().catch(() => []);
+ retestIndicators.value = previous.map((item) => {
+ const range = ranges.find((row) => row.indicatorCode === item.indicatorCode);
+ return { ...item, minValue: range ? Number(range.minValue) : Number(item.minValue), maxValue: range ? Number(range.maxValue) : Number(item.maxValue) };
+ });
if (retestIndicators.value.length !== 4) throw new Error('璐ㄦ鎸囨爣涓嶅畬鏁�');
+ retestIndicators.value = retestIndicators.value.map((item) => ({ ...item, value: undefined }));
retestReason.value = '';
retestOpen.value = true;
} catch (error) {
message.error((error as Error).message || '璇诲彇璐ㄦ鎸囨爣澶辫触');
}
+}
+function openScopeRetest() {
+ scopeOpen.value = true; retestScope.value = undefined; scopeCode.value = ''; retestReason.value = '';
+}
+async function identifyScope() {
+ if (!scopeCode.value.trim()) { message.warning('璇锋壂鎻忔垨杈撳叆鎵樼洏鐮� / 鐢熶骇鎵规鐮�'); return; }
+ scopeBusy.value = true; retestScope.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}锛岃鍏堝畬鎴愬搴斿鏍告垨澶勭悊`
+ : '褰撳墠鑼冨洿娌℃湁寰呭妫�鎴栧緟閲嶆柊鍒濇鐨勮鐮�');
+ }
+ retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50;
+ const ranges = await getSeedIndicatorRanges().catch(() => []);
+ retestIndicators.value = 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 };
+ });
+ retestReason.value = '';
+ } catch (error) { message.error((error as Error).message || '璇嗗埆澶辫触'); }
+ finally { scopeBusy.value = false; }
+}
+async function saveScopeRetest() {
+ if (!retestScope.value || !scopeIds.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;
+ }
+ const passed = retestIndicators.value.every(item => Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue));
+ if (!passed && !retestReason.value.trim()) { message.warning('涓嶅悎鏍艰濉啓鍘熷洜'); return; }
+ 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 => ({
+ 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; }
+ finally { scopeBusy.value = false; }
}
async function saveRetest() {
const record = activeRecord.value;
@@ -106,27 +169,30 @@
if (retestIndicators.value.some((item) => item.value === null || item.value === undefined
|| item.minValue === null || item.minValue === undefined || item.maxValue === null || item.maxValue === undefined
|| Number(item.minValue) > Number(item.maxValue))) {
- message.warning('璇峰~鍐欏畬鏁寸殑瀹炴祴鍊煎強鏈夋晥涓婁笅闄�');
+ message.warning('璇峰~鍐欏畬鏁寸殑瀹炴祴鍊�');
return;
}
const passed = retestIndicators.value.every((item) =>
Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue));
if (!passed && !retestReason.value.trim()) {
- message.warning('澶嶆涓嶅悎鏍艰濉啓鍘熷洜');
+ message.warning('妫�楠屼笉鍚堟牸璇峰~鍐欏師鍥�');
return;
}
retestBusy.value = true;
try {
- await retestSeedQuality(record.id, {
- code: record.code,
+ const payload = {
reason: retestReason.value.trim(),
indicators: retestIndicators.value.map((item) => ({
code: item.indicatorCode, value: Number(item.value),
minValue: Number(item.minValue), maxValue: Number(item.maxValue),
})),
- });
- message.success(passed ? '澶嶆鍚堟牸锛屽緟鍔犲伐閮ㄥ鎵�' : '澶嶆涓嶅悎鏍硷紝寰呭姞宸ラ儴璇勫');
+ };
+ await retestSeedQuality(record.id, { ...payload, code: record.code });
+ message.success(record.workflowStatus === 50
+ ? passed ? '閲嶆柊鍒濇鍚堟牸锛屽緟鍒濇瀹℃牳' : '閲嶆柊鍒濇涓嶅悎鏍硷紝寰呬笉鍚堟牸澶勭悊'
+ : passed ? '澶嶆鍚堟牸锛屽緟澶嶆瀹℃牳' : '澶嶆涓嶅悎鏍硷紝寰呬笉鍚堟牸澶勭悊');
retestOpen.value = false;
+ selectedIds.value = [];
refreshAll();
} catch (error) {
message.error((error as Error).message || '澶嶆鎻愪氦澶辫触');
@@ -154,8 +220,8 @@
}
async function saveReview() {
if (!reviewIds.value.length) return;
- if (!reviewOpinion.value.trim()) {
- message.warning('璇峰~鍐欏鎵规剰瑙�');
+ if (!reviewApproved.value && !reviewOpinion.value.trim()) {
+ message.warning('椹冲洖鏃惰濉啓瀹℃壒鎰忚');
return;
}
reviewBusy.value = true;
@@ -254,7 +320,7 @@
<div class="header-copy">
<div class="eyebrow">MES / 璐ㄩ噺绠$悊</div>
<h1>璐ㄦ璁板綍</h1>
- <p>璺熻釜鍒濇銆佸妫�銆佸姞宸ラ儴瀹℃壒鍜屽叕鍙歌川妫�瀹℃壒锛涘畬鎴愬鎵瑰悗鎵嶆祦杞簱瀛�</p>
+ <p>鍒濇瀹℃牳閫氳繃鍚庤繘琛屽妫�锛屽妫�瀹℃牳閫氳繃鍚庢墠鏀捐鍏ュ簱鎴栧缃笉鍚堟牸鍝�</p>
</div>
<div class="header-badge">
<span class="badge-pulse" />
@@ -291,7 +357,8 @@
<span class="count-chip">{{ total.toLocaleString() }} 鏉�</span>
</div>
<div class="filters">
- <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">鎵归噺{{ selectableStage === 'process' ? '鍔犲伐閮ㄥ鎵�' : '鍏徃璐ㄦ瀹℃壒' }}锛坽{ selectedRows.length }}锛�</a-button>
+ <a-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-input v-model:value="code" allow-clear placeholder="琚嬬爜" @press-enter="searchRecords">
<template #prefix><IconifyIcon icon="ep:box" /></template>
</a-input>
@@ -320,7 +387,7 @@
:data-source="records"
:row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }"
:loading="loading"
- :scroll="{ x: 1120 }"
+ :scroll="{ x: 1900 }"
:pagination="{
current: page,
pageSize,
@@ -343,6 +410,9 @@
</a-table-column>
<a-table-column key="code" title="琚嬬爜" data-index="code" min-width="220">
<template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template>
+ </a-table-column>
+ <a-table-column key="palletCode" title="鎵樼洏鐮�" data-index="palletCode" min-width="190">
+ <template #default="{ record }"><span class="mono">{{ record.palletCode || '鈥�' }}</span></template>
</a-table-column>
<a-table-column key="batchCode" title="鐢熶骇鎵规" data-index="batchCode" min-width="190">
<template #default="{ record }">{{ record.batchCode || '鈥�' }}</template>
@@ -373,32 +443,53 @@
<a-table-column key="actions" title="鎿嶄綔" width="260" fixed="right">
<template #default="{ record }">
<a-space>
- <a-button v-if="record.workflowStatus === 10 && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">澶嶆</a-button>
- <a-button v-if="[20, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">鍔犲伐閮ㄥ鎵�</a-button>
- <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">鍏徃璐ㄦ瀹℃壒</a-button>
+ <a-button v-if="[10, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">{{ record.workflowStatus === 50 ? '閲嶆柊鍒濇' : '澶嶆' }}</a-button>
+ <a-button v-if="[20, 25].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">{{ record.workflowStatus === 25 ? '涓嶅悎鏍煎鐞�' : '鍒濇瀹℃牳' }}</a-button>
+ <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">{{ record.workflowStatus === 60 ? '涓嶅悎鏍煎鐞�' : '澶嶆瀹℃牳' }}</a-button>
<a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:query'])" type="link" @click="openHistory(record)">娴佺▼璁板綍</a-button>
</a-space>
</template>
</a-table-column>
</a-table>
</section>
- <a-modal v-model:open="retestOpen" title="绉嶅瓙璐ㄦ澶嶆" :confirm-loading="retestBusy" ok-text="鎻愪氦澶嶆" @ok="saveRetest">
- <p class="muted">琚嬬爜锛歿{ activeRecord?.code }}銆傚師鎸囨爣鑼冨洿宸插甫鍏ワ紝璇峰綍鍏ユ湰娆″疄娴嬪�煎苟鏍稿涓婁笅闄愩��</p>
+ <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>
<div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row">
<strong>{{ indicatorLabels[item.indicatorCode] || item.indicatorCode }}</strong>
- <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="瀹炴祴鍊�" />
- <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="涓嬮檺" />
- <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="涓婇檺" />
+ <a-input-number v-model:value="item.value" :min="0" :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="澶嶆涓嶅悎鏍兼椂濉啓鍘熷洜" />
+ <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="鏈妫�楠屼笉鍚堟牸鏃跺~鍐欏師鍥�" />
</a-modal>
- <a-modal v-model:open="reviewOpen" :title="reviewStage === 'process' ? '鍔犲伐閮ㄥ鎵�' : '鍏徃璐ㄦ瀹℃壒'" :confirm-loading="reviewBusy" ok-text="鎻愪氦瀹℃壒" @ok="saveReview">
+ <a-modal v-model:open="scopeOpen" title="鎸夋墭鐩� / 鐢熶骇鎵规鎵归噺澶嶆鎴栭噸鏂板垵妫�" :confirm-loading="scopeBusy" ok-text="鎻愪氦鏈樁娈靛叏閮ㄨ鐮�" :ok-button-props="{ disabled: !scopeIds.length }" @ok="saveScopeRetest">
+ <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-space>
+ <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="涓嶅悎鏍兼椂濉啓鍘熷洜" />
+ </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-radio-group v-model:value="reviewApproved" class="review-choice">
- <a-radio :value="true">鍚屾剰{{ reviewStage === 'company' ? activeRecord?.workflowStatus === 30 ? '鏀捐鍏ュ簱' : '鎶ュ簾澶勭疆' : '涓婃姤鍏徃璐ㄦ' }}</a-radio>
- <a-radio :value="false">椹冲洖閲嶆</a-radio>
+ <a-radio :value="true">{{ reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '纭涓嶅悎鏍硷紝瀹夋帓澶嶆' : '閫氳繃锛岃繘鍏ュ妫�' : activeRecord?.workflowStatus === 30 ? '閫氳繃锛屾斁琛屽叆搴�' : '纭涓嶅悎鏍硷紝鎶ュ簾澶勭疆' }}</a-radio>
+ <a-radio :value="false">{{ reviewStage === 'process' ? '椹冲洖锛岄噸鏂板垵妫�' : '椹冲洖锛岄噸鏂板妫�' }}</a-radio>
</a-radio-group>
- <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="璇峰~鍐欏鎵规剰瑙�" />
+ <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">
@@ -458,6 +549,8 @@
.filters :deep(.ant-btn .iconify) { font-size: 14px; }
.retest-row { display: grid; grid-template-columns: 60px repeat(3, 1fr); gap: 8px; align-items: center; margin-bottom: 10px; }
.retest-row :deep(.ant-input-number) { width: 100%; }
+.scope-scan { display: flex; width: 100%; margin-bottom: 16px; }
+.scope-scan :deep(.ant-space-item:last-child) { flex: 1; }
.review-choice { display: flex; gap: 16px; margin: 10px 0 18px; }
/* ===== 鍗曞厓鏍� ===== */
diff --git a/src/views/mes/qc/seedRange/index.vue b/src/views/mes/qc/seedRange/index.vue
new file mode 100644
index 0000000..72b6e4b
--- /dev/null
+++ b/src/views/mes/qc/seedRange/index.vue
@@ -0,0 +1,91 @@
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import { useAccess } from '@vben/access';
+import { getSeedIndicatorRanges, saveSeedIndicatorRanges } from '#/api/mes/qc/seedQuality';
+
+const definitions = [
+ { indicatorCode: 'purity', name: '绾害' },
+ { indicatorCode: 'cleanliness', name: '鍑�搴�' },
+ { indicatorCode: 'germination_rate', name: '鍙戣娊鐜�' },
+ { indicatorCode: 'moisture', name: '姘村垎' },
+];
+const ranges = ref(definitions.map((item) => ({ ...item, minValue: undefined as number | undefined, maxValue: undefined as number | undefined })));
+const loading = ref(false);
+const saving = ref(false);
+const { hasAccessByCodes } = useAccess();
+const complete = computed(() => ranges.value.every((item) =>
+ item.minValue !== undefined && item.maxValue !== undefined && item.minValue <= item.maxValue));
+
+async function load() {
+ loading.value = true;
+ try {
+ const data = await getSeedIndicatorRanges();
+ ranges.value = definitions.map((item) => {
+ const saved = data.find((row) => row.indicatorCode === item.indicatorCode);
+ return { ...item, minValue: saved ? Number(saved.minValue) : undefined,
+ maxValue: saved ? Number(saved.maxValue) : undefined };
+ });
+ } catch (error) {
+ message.error((error as Error).message || '璇诲彇鍚堟牸鑼冨洿澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
+
+async function save() {
+ if (!complete.value || ranges.value.some((item) => item.minValue! < 0 || item.maxValue! > 100)) {
+ message.warning('璇蜂负鍥涢」鎸囨爣閰嶇疆 0鈥�100% 鐨勬湁鏁堜笂涓嬮檺');
+ return;
+ }
+ saving.value = true;
+ try {
+ await saveSeedIndicatorRanges(ranges.value.map((item) => ({
+ indicatorCode: item.indicatorCode,
+ minValue: item.minValue!, maxValue: item.maxValue!,
+ })));
+ message.success('榛樿鍚堟牸鑼冨洿宸蹭繚瀛橈紝妫�楠屾椂浠嶅彲鎸夊疄闄呮儏鍐佃皟鏁�');
+ await load();
+ } catch (error) {
+ message.error((error as Error).message || '淇濆瓨鍚堟牸鑼冨洿澶辫触');
+ } finally {
+ saving.value = false;
+ }
+}
+
+onMounted(load);
+</script>
+
+<template>
+ <div class="range-page">
+ <header class="page-header">
+ <div><div class="eyebrow">MES / 璐ㄩ噺绠$悊</div><h1>璐ㄦ椤瑰悎鏍艰寖鍥撮厤缃�</h1>
+ <p>涓哄垵妫�鍜屽妫�鎻愪緵鍥涢」鎸囨爣鐨勯粯璁よ寖鍥淬��</p></div>
+ </header>
+ <a-alert type="info" show-icon message="杩欓噷閰嶇疆鐨勬槸濉啓妯℃澘" description="妫�楠屽憳鍙互淇敼鏈鍚堟牸涓婁笅闄愶紱瀹為檯鎻愪氦鐨勪笂涓嬮檺鍜屽疄娴嬪�间竴鍚屼繚瀛橈紝鍘嗗彶璁板綍涓嶅彈妯℃澘淇敼褰卞搷銆�" />
+ <a-card class="range-card" :loading="loading" :bordered="false">
+ <div class="grid-head"><span>璐ㄦ椤�</span><span>鍚堟牸涓嬮檺</span><span>鍚堟牸涓婇檺</span><span>鍗曚綅</span></div>
+ <div v-for="item in ranges" :key="item.indicatorCode" class="range-row">
+ <strong>{{ item.name }}</strong>
+ <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="涓嬮檺" />
+ <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="涓婇檺" />
+ <span>%</span>
+ </div>
+ <div class="actions">
+ <a-button @click="load">鍒锋柊</a-button>
+ <a-button v-if="hasAccessByCodes(['mes:qc-seed-range:update'])" type="primary" :loading="saving" :disabled="!complete" @click="save">淇濆瓨鍚堟牸鑼冨洿</a-button>
+ </div>
+ </a-card>
+ </div>
+</template>
+
+<style scoped>
+.range-page { padding: 24px; min-height: 100%; background: #f4f7fb; }
+.page-header { margin-bottom: 20px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
+h1 { margin: 6px 0; color: #172033; font-size: 28px; }.page-header p { margin: 0; color: #718096; }
+.range-card { margin-top: 16px; border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }
+.grid-head, .range-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 1fr) 70px; align-items: center; gap: 16px; padding: 12px 16px; }
+.grid-head { color: #718096; background: #f7f9fc; font-weight: 600; }.range-row { border-bottom: 1px solid #e8edf5; }.range-row :deep(.ant-input-number) { width: 100%; }
+.actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
+@media (max-width: 640px) { .range-page { padding: 14px; }.grid-head { display: none; }.range-row { grid-template-columns: 1fr 1fr; }.range-row strong { grid-column: 1 / -1; } }
+</style>
diff --git a/src/views/mobile/quality-records.vue b/src/views/mobile/quality-records.vue
index a25cae6..306e369 100644
--- a/src/views/mobile/quality-records.vue
+++ b/src/views/mobile/quality-records.vue
@@ -1,11 +1,58 @@
<script setup lang="ts">
-import { onMounted, ref } from 'vue';
-import { getSeedQualityPage, type SeedQualityRecord } from '#/api/mes/qc/seedQuality';
+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 { errorText } from '#/mobile/api';
+import { extractCode } from '#/mobile/access';
import Scanner from './scanner.vue';
const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref('');
+const { hasAccessByCodes } = useAccess();
+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 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 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; }
+}
+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;
+ 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 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 } = {};
async function load(reset = true) {
if (busy.value) return;
@@ -21,6 +68,25 @@
onMounted(() => 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>
+ <p v-if="notice" class="m-success" role="status">{{notice}}</p>
<p class="m-muted">鏌ョ湅璐ㄦ缁撴灉銆佹壒娆′笌涓嶅悎鏍煎師鍥犮��</p>
<Scanner :disabled="busy" @code="value => { code = value; load(); }" />
<form class="m-panel" @submit.prevent="load()">
@@ -34,7 +100,7 @@
<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>{{row.inspectedAt || row.createTime || '鈥�'}}</dd></div><div v-if="row.reason" class="m-row"><dt>鍘熷洜 / 澶囨敞</dt><dd>{{row.reason}}</dd></div></dl>
+ <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>
</template>
diff --git a/src/views/mobile/quality.vue b/src/views/mobile/quality.vue
index 4e870f2..81bf336 100644
--- a/src/views/mobile/quality.vue
+++ b/src/views/mobile/quality.vue
@@ -2,7 +2,7 @@
import { computed, onMounted, ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useUserStore } from '@vben/stores';
-import { resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality';
+import { getSeedIndicatorRanges, resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList, type MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
import { allowedMobilePath, extractCode } from '#/mobile/access';
import { errorText } from '#/mobile/api';
@@ -18,10 +18,10 @@
const pendingType = ref<ScanType>('BAG'), pendingIds = ref<number[]>([]);
const storageKey = 'mom-mobile-quality-' + useUserStore().userInfo?.id;
const indicators = ref([
- { code: 'purity', name: '绾害', value: '', minValue: '0', maxValue: '100' },
- { code: 'cleanliness', name: '鍑�搴�', value: '', minValue: '0', maxValue: '100' },
- { code: 'germination_rate', name: '鍙戣娊鐜�', value: '', minValue: '0', maxValue: '100' },
- { code: 'moisture', name: '姘村垎', value: '', minValue: '0', maxValue: '100' },
+ { 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 complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => String(v).trim() !== '' && Number.isFinite(Number(v))) && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.value) >= 0 && Number(i.value) <= 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)));
@@ -33,20 +33,24 @@
const data = await resolveSeedQuality(extractCode(raw), scanType.value);
if (data.canceled || !data.inbound) throw new Error('璇ュ璞″凡浣滃簾鎴栨湁鏁堣鐮佸皻鏈叏閮ㄥ叆搴擄紝涓嶈兘璐ㄦ');
if (!data.pendingBagIds?.length) throw new Error('璇ュ璞℃病鏈夊緟璐ㄦ琚嬬爜锛岃鍦ㄨ川妫�璁板綍涓煡鐪嬪凡鏈夌粨鏋�');
- bag.value = data; indicators.value.forEach(i => i.value = ''); reason.value = '';
+ bag.value = data; 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; }
}
async function submit() {
if (busy.value || pendingCode.value || !bag.value || !complete.value || !warehouseId.value) return;
if (!passed.value && !reason.value.trim()) { error.value = '璇峰~鍐欎笉鍚堟牸鍘熷洜'; return; }
const count = bag.value.pendingBagIds!.length;
- if (!window.confirm(passed.value ? `纭瀵规湰娆� ${count} 琚嬬粺涓�鍒ゅ畾鍚堟牸锛熷叏閮ㄥ皢杞叆鎵�閫夊悎鏍煎簱銆俙 : `纭瀵规湰娆� ${count} 琚嬬粺涓�鍒ゅ畾涓嶅悎鏍硷紵鍏ㄩ儴灏嗕粠鏆傚瓨搴撳瓨绉婚櫎骞舵爣璁版姤搴熴�俙)) return;
+ if (!window.confirm(passed.value ? `纭瀵规湰娆� ${count} 琚嬬粺涓�鎻愪氦鍒濇鍚堟牸锛熷鏍搁�氳繃鍚庤繘鍏ュ妫�銆俙 : `纭瀵规湰娆� ${count} 琚嬬粺涓�鎻愪氦鍒濇涓嶅悎鏍硷紵灏嗚繘鍏ヤ笉鍚堟牸澶勭悊銆俙)) return;
error.value = ''; notice.value = '';
pendingCode.value = bag.value.code; pendingType.value = scanType.value; pendingIds.value = [...bag.value.pendingBagIds!];
sessionStorage.setItem(storageKey, JSON.stringify({ code: pendingCode.value, scanType: pendingType.value, bagIds: pendingIds.value })); busy.value = true;
try {
const result = await submitSeedQualityGroup({ code: bag.value.code, scanType: scanType.value, bagIds: pendingIds.value, qualifiedWarehouseId: warehouseId.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) })) });
- clearPending(); notice.value = `鏈 ${result.processedCount} 琚媊 + (passed.value ? '璐ㄦ鍚堟牸锛屽凡杞叆鍚堟牸搴撱��' : '璐ㄦ涓嶅悎鏍硷紝宸茶褰曞師鍥犲苟绉婚櫎鏆傚瓨搴撳瓨銆�');
+ clearPending(); notice.value = `鏈 ${result.processedCount} 琚媊 + (passed.value ? '鍒濇鍚堟牸锛岀瓑寰呭鏍搞��' : '鍒濇涓嶅悎鏍硷紝绛夊緟涓嶅悎鏍煎鐞嗐��');
bag.value = undefined; reason.value = ''; indicators.value.forEach(i => i.value = '');
} catch (e) { error.value = '鎻愪氦缁撴灉寰呯‘璁わ細' + errorText(e) + '銆傝鏍稿疄鍚庡啀鎿嶄綔涓嬩竴琚嬨��'; } finally { busy.value = false; }
}
@@ -87,8 +91,8 @@
<label class="m-field"><span>瀹炴祴鍊�</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required placeholder="杈撳叆瀹炴祴鐧惧垎姣�" /></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>
- <div class="m-panel"><p class="m-muted">涓婁笅闄愭部鐢ㄧ幇鏈夎川妫�瑙勫垯褰曞叆鏂瑰紡锛岃鎸夎鍝佺妫�楠屾爣鍑嗘牳瀵广��</p><Picker v-model="warehouseId" label="鐩爣鍚堟牸搴�" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">鏆傛棤鍙敤鍚堟牸搴擄紝璇疯仈绯荤鐞嗗憳閰嶇疆銆�</p><label class="m-field"><span>{{complete && !passed ? '涓嶅悎鏍煎師鍥狅紙蹇呭~锛�' : '妫�楠屽娉�'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div>
- <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">棰勫垽锛歿{passed ? '鍚堟牸锛屾彁浜ゅ悗杞叆鍚堟牸搴�' : '涓嶅悎鏍硷紝鎻愪氦鍚庣Щ闄ゆ殏瀛樺簱瀛�'}}銆傛渶缁堜互鏈嶅姟绔粨鏋滀负鍑嗐��</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? '姝e湪鎻愪氦鈥�' : '纭鎻愪氦璐ㄦ'}}</button></div>
+ <div class="m-panel"><p class="m-muted">鍚堟牸鑼冨洿浠庢ā鏉垮甫鍏ワ紝鍙湪鏈璐ㄦ涓慨鏀癸紱璇锋寜璇ュ搧绉嶆爣鍑嗘牳瀵广��</p><Picker v-model="warehouseId" label="鐩爣鍚堟牸搴�" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">鏆傛棤鍙敤鍚堟牸搴擄紝璇疯仈绯荤鐞嗗憳閰嶇疆銆�</p><label class="m-field"><span>{{complete && !passed ? '涓嶅悎鏍煎師鍥狅紙蹇呭~锛�' : '妫�楠屽娉�'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div>
+ <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">棰勫垽锛歿{passed ? '鍒濇鍚堟牸锛岀瓑寰呭垵妫�瀹℃牳' : '鍒濇涓嶅悎鏍硷紝杩涘叆涓嶅悎鏍煎鐞�'}}銆傛渶缁堜互鏈嶅姟绔粨鏋滀负鍑嗐��</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? '姝e湪鎻愪氦鈥�' : '纭鎻愪氦璐ㄦ'}}</button></div>
</form>
</template>
<RouterLink v-if="allowedMobilePath('/mobile/quality-records')" to="/mobile/quality-records" class="m-link">鏌ョ湅璐ㄦ璁板綍</RouterLink>
--
Gitblit v1.9.3