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