From 0558f330f7e3781b91d00ed4a12e60d9fa469d87 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 17 九月 2026 13:11:12 +0800
Subject: [PATCH] feat(mes): 增加质检指标层级结构与多批次录入功能
---
src/views/mes/qc/indicatorresult/components/form.vue | 393 +++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 318 insertions(+), 75 deletions(-)
diff --git a/src/views/mes/qc/indicatorresult/components/form.vue b/src/views/mes/qc/indicatorresult/components/form.vue
index c6481b2..0dbd5a5 100644
--- a/src/views/mes/qc/indicatorresult/components/form.vue
+++ b/src/views/mes/qc/indicatorresult/components/form.vue
@@ -6,10 +6,11 @@
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
-import { MesQcResultValueType } from '@vben/constants';
+import { MesQcIndicatorItemType, MesQcResultValueType } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
+ Button,
Form as AForm,
Divider,
Input,
@@ -36,11 +37,36 @@
qcType: number;
}
+/** 鍙紪杈戞祴閲忔壒娆★紙鍦ㄦ帴鍙g粨鏋勪笂澧炲姞鏁板�肩粦瀹氬瓧娈碉級 */
+interface EditMeasure {
+ detailId?: number;
+ resultId?: number;
+ measureNo: number;
+ value?: string;
+ valueNumber?: number;
+ remark?: string;
+}
+
+/** 鍙紪杈戞楠岄」锛堟爲褰㈣妭鐐癸級 */
+interface EditItem {
+ indicatorId?: number;
+ indicatorName?: string;
+ valueType?: number;
+ valueSpecification?: string;
+ parentId?: number;
+ itemType?: number;
+ formulaText?: string;
+ unitText?: string;
+ sortOrder?: number;
+ measures: EditMeasure[];
+ children?: EditItem[];
+}
+
defineOptions({ name: 'QcIndicatorResultForm' });
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
-const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]);
+const roots = ref<EditItem[]>([]);
const ctxData = ref<CtxData>();
const getTitle = computed(() =>
@@ -63,6 +89,19 @@
wrapperClass: 'grid-cols-2',
});
+/** 鏄惁鍒嗙粍椤癸紙鍒嗙粍椤逛笉褰曞叆鍊硷紝浠呮壙杞藉眰绾т笌鍏紡鏂囨湰锛� */
+function isGroup(item: EditItem) {
+ return item.itemType === MesQcIndicatorItemType.GROUP;
+}
+
+/** 鏄惁鏁板�煎瀷褰曞叆锛堝喅瀹� InputNumber 涓庢枃鏈帶浠讹級 */
+function isNumeric(item: EditItem) {
+ return (
+ item.valueType === MesQcResultValueType.FLOAT ||
+ item.valueType === MesQcResultValueType.INTEGER
+ );
+}
+
/** 鑾峰彇瀛楀吀閫夐」锛坴alueSpecification 涓虹郴缁熷瓧鍏哥被鍨嬪悕锛� */
function getValueOptions(dictType?: string) {
return dictType
@@ -71,6 +110,75 @@
value: String(value),
}))
: [];
+}
+
+/** 灏嗘帴鍙h繑鍥炵殑鏄庣粏鏍戣浆鎹负鍙紪杈戠粨鏋勶細鍒嗙粍閫掑綊銆佸綍鍏ラ」淇濊瘉鑷冲皯涓�涓祴閲忔壒娆� */
+function toEditItems(
+ items: MesQcIndicatorResultApi.IndicatorResultDetail[] = [],
+): EditItem[] {
+ return items.map((item) => {
+ const node: EditItem = {
+ indicatorId: item.indicatorId,
+ indicatorName: item.indicatorName,
+ valueType: item.valueType,
+ valueSpecification: item.valueSpecification,
+ parentId: item.parentId,
+ itemType: item.itemType,
+ formulaText: item.formulaText,
+ unitText: item.unitText,
+ sortOrder: item.sortOrder,
+ measures: [],
+ children: item.children ? toEditItems(item.children) : undefined,
+ };
+ if (isGroup(node)) {
+ return node;
+ }
+ const measures = (item.measures ?? []).map((m) => ({
+ detailId: m.detailId,
+ resultId: m.resultId,
+ measureNo: m.measureNo ?? 1,
+ value: m.value,
+ valueNumber:
+ isNumeric(node) && m.value !== null && m.value !== undefined
+ ? Number(m.value)
+ : undefined,
+ remark: m.remark,
+ }));
+ node.measures = measures.length > 0 ? measures : [{ measureNo: 1 }];
+ return node;
+ });
+}
+
+/** 鏀堕泦鎵�鏈夊綍鍏ラ」锛堝彾瀛愶級鐢ㄤ簬鎻愪氦涓庡睍绀虹粺璁� */
+function collectLeaves(nodes: EditItem[]): EditItem[] {
+ const leaves: EditItem[] = [];
+ const walk = (list: EditItem[]) => {
+ list.forEach((n) => {
+ if (isGroup(n)) {
+ walk(n.children ?? []);
+ } else {
+ leaves.push(n);
+ }
+ });
+ };
+ walk(nodes);
+ return leaves;
+}
+
+/** 鏂板涓�涓祴閲忔壒娆� */
+function addMeasure(item: EditItem) {
+ const nextNo =
+ item.measures.reduce((max, m) => Math.max(max, m.measureNo ?? 0), 0) + 1;
+ item.measures.push({ measureNo: nextNo });
+}
+
+/** 鍒犻櫎鎸囧畾娴嬮噺鎵规锛堣嚦灏戜繚鐣欎竴涓級 */
+function removeMeasure(item: EditItem, index: number) {
+ if (item.measures.length <= 1) {
+ message.warning('鑷冲皯淇濈暀涓�涓祴閲忔壒娆�');
+ return;
+ }
+ item.measures.splice(index, 1);
}
const [Modal, modalApi] = useVbenModal({
@@ -83,25 +191,26 @@
return;
}
modalApi.lock();
- // 鎻愪氦琛ㄥ崟
const head =
(await formApi.getValues()) as MesQcIndicatorResultApi.IndicatorResult;
- const submitItems = items.value.map((item) => {
- const submit: MesQcIndicatorResultApi.IndicatorResultDetail = {
- id: item.id,
- indicatorId: item.indicatorId,
- remark: item.remark,
- };
- if (
- item.valueType === MesQcResultValueType.FLOAT ||
- item.valueType === MesQcResultValueType.INTEGER
- ) {
- submit.value =
- item.valueNumber === null ? undefined : String(item.valueNumber);
- } else {
- submit.value = item.value;
- }
- return submit;
+ // 灞曞紑锛氭瘡涓綍鍏ラ」鐨勬瘡涓祴閲忔壒娆� 鈫� 涓�鏉℃槑缁�
+ const submitItems: MesQcIndicatorResultApi.IndicatorResultDetail[] = [];
+ collectLeaves(roots.value).forEach((item) => {
+ item.measures.forEach((measure) => {
+ const value = isNumeric(item)
+ ? measure.valueNumber === null || measure.valueNumber === undefined
+ ? undefined
+ : String(measure.valueNumber)
+ : measure.value;
+ submitItems.push({
+ id: measure.detailId,
+ resultId: measure.resultId,
+ indicatorId: item.indicatorId,
+ measureNo: measure.measureNo,
+ value,
+ remark: measure.remark,
+ });
+ });
});
const payload: MesQcIndicatorResultApi.IndicatorResult = {
...head,
@@ -117,7 +226,6 @@
await createIndicatorResult(payload);
message.success($t('common.createSuccess'));
}
- // 鍏抽棴骞舵彁绀�
await modalApi.close();
emit('success');
} finally {
@@ -127,11 +235,10 @@
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
ctxData.value = undefined;
- items.value = [];
+ roots.value = [];
return;
}
formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) });
- // 鍔犺浇鏁版嵁
const data = modalApi.getData<CtxData>();
ctxData.value = data;
formType.value = data.formType;
@@ -142,17 +249,7 @@
data.qcType,
data.id,
);
- // 鍥炲~鏁板�煎瓧娈碉紙鐢ㄤ簬 InputNumber 缁戝畾锛�
- items.value = (detail.items ?? []).map((item) => ({
- ...item,
- valueNumber:
- (item.valueType === MesQcResultValueType.FLOAT ||
- item.valueType === MesQcResultValueType.INTEGER) &&
- item.value !== null
- ? Number(item.value)
- : undefined,
- }));
- // 璁剧疆鍒� values
+ roots.value = toEditItems(detail.items);
await formApi.setValues({
id: detail.id,
code: detail.code,
@@ -164,6 +261,18 @@
}
},
});
+
+/** 鍗曚釜娴嬮噺鍊艰緭鍏ユ帶浠剁粦瀹氾紙鏁板�煎瀷璧� valueNumber锛屽叾浣欒蛋 value锛� */
+function bindMeasure(item: EditItem, measure: EditMeasure) {
+ return isNumeric(item) ? measure.valueNumber : measure.value;
+}
+function onMeasureInput(item: EditItem, measure: EditMeasure, v: any) {
+ if (isNumeric(item)) {
+ measure.valueNumber = v === null ? undefined : Number(v);
+ } else {
+ measure.value = v;
+ }
+}
</script>
<template>
@@ -171,48 +280,182 @@
<div class="px-4">
<Form />
<Divider>妫�娴嬪��</Divider>
- <!-- 妫�娴嬪�兼帶浠剁敱姣忚 valueType 椹卞姩锛岄�愯娓叉煋锛欶LOAT/INTEGER 璧� InputNumber锛孌ICT 璧� Select 骞舵寜 valueSpecification 瑙f瀽閫夐」 -->
- <AForm :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
- <div
- v-for="(item, index) in items"
- :key="item.indicatorId ?? index"
- class="mb-2"
- >
- <AForm.Item
- :label="`妫�娴嬮」${index + 1}锛�${item.indicatorName ?? ''}`"
- >
- <InputNumber
- v-if="
- item.valueType === MesQcResultValueType.FLOAT ||
- item.valueType === MesQcResultValueType.INTEGER
- "
- v-model:value="item.valueNumber"
- :precision="item.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
- class="!w-full"
- placeholder="璇疯緭鍏�"
- />
- <Textarea
- v-else-if="item.valueType === MesQcResultValueType.TEXT"
- v-model:value="item.value"
- :rows="2"
- placeholder="璇疯緭鍏ユ娴嬪��"
- />
- <Select
- v-else-if="item.valueType === MesQcResultValueType.DICT"
- v-model:value="item.value"
- allow-clear
- :options="getValueOptions(item.valueSpecification)"
- placeholder="璇烽�夋嫨"
- />
- <Input
- v-else-if="item.valueType === MesQcResultValueType.FILE"
- v-model:value="item.value"
- placeholder="璇疯緭鍏ユ枃浠跺湴鍧�"
- />
- <Input v-else v-model:value="item.value" placeholder="璇疯緭鍏�" />
- </AForm.Item>
- </div>
- </AForm>
+ <div v-if="roots.length === 0" class="py-4 text-center text-gray-400">
+ 璇ヨ川妫�鍗曟湭閰嶇疆妫�娴嬫寚鏍�
+ </div>
+ <div v-else class="qc-result-tree">
+ <template v-for="(node, ni) in roots" :key="node.indicatorId ?? ni">
+ <!-- 鍒嗙粍椤癸細鏍囬琛� + 鍏紡鏂囨湰锛屼笅鎸傚瓙褰曞叆椤� -->
+ <div v-if="isGroup(node)">
+ <div
+ class="mb-2 flex items-center justify-between rounded bg-gray-100 px-3 py-1.5 font-medium"
+ >
+ <span>{{ node.indicatorName }}</span>
+ <span v-if="node.formulaText" class="text-xs text-gray-500">
+ {{ node.formulaText }}
+ </span>
+ </div>
+ <div
+ v-for="(child, ci) in node.children ?? []"
+ :key="child.indicatorId ?? ci"
+ class="mb-3 ml-4"
+ >
+ <div class="mb-1 flex items-center justify-between">
+ <span class="font-medium">
+ {{ child.indicatorName }}
+ <span v-if="child.unitText" class="text-xs text-gray-500">
+ 锛坽{ child.unitText }}锛�
+ </span>
+ </span>
+ <Button
+ size="small"
+ type="link"
+ @click="addMeasure(child)"
+ >
+ + 娴嬮噺鎵规
+ </Button>
+ </div>
+ <div
+ v-for="(measure, mi) in child.measures"
+ :key="mi"
+ class="mb-1 flex items-center gap-2"
+ >
+ <span class="w-16 shrink-0 text-right text-xs text-gray-500">
+ 绗� {{ measure.measureNo }} 娆�
+ </span>
+ <InputNumber
+ v-if="child.valueType === MesQcResultValueType.FLOAT || child.valueType === MesQcResultValueType.INTEGER"
+ :value="bindMeasure(child, measure)"
+ :precision="child.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
+ class="!w-48"
+ placeholder="璇疯緭鍏�"
+ @update:value="(v) => onMeasureInput(child, measure, v)"
+ />
+ <Textarea
+ v-else-if="child.valueType === MesQcResultValueType.TEXT"
+ v-model:value="measure.value"
+ :rows="1"
+ class="!w-48"
+ placeholder="璇疯緭鍏ユ娴嬪��"
+ />
+ <Select
+ v-else-if="child.valueType === MesQcResultValueType.DICT"
+ v-model:value="measure.value"
+ allow-clear
+ class="!w-48"
+ :options="getValueOptions(child.valueSpecification)"
+ placeholder="璇烽�夋嫨"
+ />
+ <Input
+ v-else-if="child.valueType === MesQcResultValueType.FILE"
+ v-model:value="measure.value"
+ class="!w-48"
+ placeholder="璇疯緭鍏ユ枃浠跺湴鍧�"
+ />
+ <Input
+ v-else
+ v-model:value="measure.value"
+ class="!w-48"
+ placeholder="璇疯緭鍏�"
+ />
+ <Input
+ v-model:value="measure.remark"
+ class="flex-1"
+ placeholder="澶囨敞"
+ />
+ <Button
+ size="small"
+ type="text"
+ danger
+ :disabled="child.measures.length <= 1"
+ @click="removeMeasure(child, mi)"
+ >
+ 鍒犻櫎
+ </Button>
+ </div>
+ </div>
+ </div>
+ <!-- 椤剁骇褰曞叆椤� -->
+ <div v-else class="mb-3">
+ <div class="mb-1 flex items-center justify-between">
+ <span class="font-medium">
+ {{ node.indicatorName }}
+ <span v-if="node.unitText" class="text-xs text-gray-500">
+ 锛坽{ node.unitText }}锛�
+ </span>
+ </span>
+ <Button size="small" type="link" @click="addMeasure(node)">
+ + 娴嬮噺鎵规
+ </Button>
+ </div>
+ <div
+ v-for="(measure, mi) in node.measures"
+ :key="mi"
+ class="mb-1 flex items-center gap-2"
+ >
+ <span class="w-16 shrink-0 text-right text-xs text-gray-500">
+ 绗� {{ measure.measureNo }} 娆�
+ </span>
+ <InputNumber
+ v-if="node.valueType === MesQcResultValueType.FLOAT || node.valueType === MesQcResultValueType.INTEGER"
+ :value="bindMeasure(node, measure)"
+ :precision="node.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
+ class="!w-48"
+ placeholder="璇疯緭鍏�"
+ @update:value="(v) => onMeasureInput(node, measure, v)"
+ />
+ <Textarea
+ v-else-if="node.valueType === MesQcResultValueType.TEXT"
+ v-model:value="measure.value"
+ :rows="1"
+ class="!w-48"
+ placeholder="璇疯緭鍏ユ娴嬪��"
+ />
+ <Select
+ v-else-if="node.valueType === MesQcResultValueType.DICT"
+ v-model:value="measure.value"
+ allow-clear
+ class="!w-48"
+ :options="getValueOptions(node.valueSpecification)"
+ placeholder="璇烽�夋嫨"
+ />
+ <Input
+ v-else-if="node.valueType === MesQcResultValueType.FILE"
+ v-model:value="measure.value"
+ class="!w-48"
+ placeholder="璇疯緭鍏ユ枃浠跺湴鍧�"
+ />
+ <Input
+ v-else
+ v-model:value="measure.value"
+ class="!w-48"
+ placeholder="璇疯緭鍏�"
+ />
+ <Input
+ v-model:value="measure.remark"
+ class="flex-1"
+ placeholder="澶囨敞"
+ />
+ <Button
+ size="small"
+ type="text"
+ danger
+ :disabled="node.measures.length <= 1"
+ @click="removeMeasure(node, mi)"
+ >
+ 鍒犻櫎
+ </Button>
+ </div>
+ </div>
+ </template>
+ </div>
</div>
</Modal>
</template>
+
+<style scoped>
+.qc-result-tree {
+ max-height: 60vh;
+ overflow-y: auto;
+}
+</style>
--
Gitblit v1.9.3