From 49c9eff61882f1f7d519397ccfdd90cdd8735cb8 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 04 八月 2026 13:12:21 +0800
Subject: [PATCH] feat(aftersales): 售后工单支持附件上传和显示

---
 src/views/aftersales/ticket/data.ts                         |   12 +++
 src/api/mes/pro/route/process/index.ts                      |    2 
 src/views/mes/pro/workorder/modules/process-form.vue        |    3 
 .claude/settings.local.json                                 |    6 +
 src/views/mes/process-design/route/modules/process-list.vue |    4 -
 src/views/mes/pro/workorder/data.ts                         |   11 --
 src/api/mes/pro/workorder/process/index.ts                  |    8 +
 src/views/aftersales/ticket/modules/form.vue                |    8 ++
 src/views/ai/knowledge/document/form/modules/split-step.vue |    3 
 src/api/aftersales/ticket.ts                                |    4 +
 src/views/mes/pro/workorder/modules/process-list.vue        |    5 
 src/views/aftersales/ticket/detail.vue                      |   15 +++
 src/views/mes/md/item/components/select.vue                 |  103 +++++++++++++++++--------
 src/views/mes/process-design/route/data.ts                  |    8 +-
 14 files changed, 133 insertions(+), 59 deletions(-)

diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 3d8e4b9..b82122e 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -22,5 +22,9 @@
       "Bash(node lib/script/postinstall.js)",
       "Bash(mvn compile *)"
     ]
-  }
+  },
+  "enabledMcpjsonServers": [
+    "idea"
+  ],
+  "enableAllProjectMcpServers": true
 }
diff --git a/src/api/aftersales/ticket.ts b/src/api/aftersales/ticket.ts
index be94b90..36120d0 100644
--- a/src/api/aftersales/ticket.ts
+++ b/src/api/aftersales/ticket.ts
@@ -51,6 +51,10 @@
     createTime?: string;
     updater?: string;
     updateTime?: string;
+    /** 闄勪欢 blobId 鍒楄〃锛堜繚瀛樻椂浼犲叆锛� */
+    blobIds?: number[];
+    /** 闄勪欢鍒楄〃锛堣鎯呮椂杩斿洖锛� */
+    attachmentList?: { id: number; name?: string; url?: string }[];
     items?: TicketItem[];
   }
 }
diff --git a/src/api/mes/pro/route/process/index.ts b/src/api/mes/pro/route/process/index.ts
index 3e5c2f8..c207284 100644
--- a/src/api/mes/pro/route/process/index.ts
+++ b/src/api/mes/pro/route/process/index.ts
@@ -30,6 +30,8 @@
     outputItemId?: number;
     outputItemCode?: string;
     outputItemName?: string;
+    outputItemIds?: number[];
+    outputItemNames?: string;
     remark?: string;
     bomItems?: ProcessBomItem[];
     createTime?: Date;
diff --git a/src/api/mes/pro/workorder/process/index.ts b/src/api/mes/pro/workorder/process/index.ts
index 4444ca0..cc3630b 100644
--- a/src/api/mes/pro/workorder/process/index.ts
+++ b/src/api/mes/pro/workorder/process/index.ts
@@ -29,9 +29,11 @@
     keyFlag?: boolean; // 鏄惁鍏抽敭宸ュ簭
     checkFlag?: boolean; // 鏄惁璐ㄦ宸ュ簭
     backflushFlag?: boolean; // 鏄惁鍊掑啿
-    outputItemId?: number; // 浜у嚭浜у搧鐗╂枡缂栧彿
-    outputItemCode?: string; // 浜у嚭浜у搧鐗╂枡缂栫爜
-    outputItemName?: string; // 浜у嚭浜у搧鐗╂枡鍚嶇О
+    outputItemId?: number; // 浜у嚭浜у搧鐗╂枡缂栧彿锛堢涓�涓紝鍚戝悗鍏煎锛�
+    outputItemCode?: string; // 浜у嚭浜у搧鐗╂枡缂栫爜锛堢涓�涓級
+    outputItemName?: string; // 浜у嚭浜у搧鐗╂枡鍚嶇О锛堢涓�涓級
+    outputItemIds?: number[]; // 浜у嚭浜у搧鐗╂枡缂栧彿鍒楄〃
+    outputItemNames?: string; // 浜у嚭浜у搧鐗╂枡鍚嶇О锛堥�楀彿鎷兼帴锛�
     bomItems?: ProcessBomItem[]; // 宸ュ簭鎶曟枡BOM鏄庣粏
     remark?: string; // 澶囨敞
   }
diff --git a/src/views/aftersales/ticket/data.ts b/src/views/aftersales/ticket/data.ts
index 226e4a6..15c83cc 100644
--- a/src/views/aftersales/ticket/data.ts
+++ b/src/views/aftersales/ticket/data.ts
@@ -182,6 +182,18 @@
         rows: 3,
       },
     },
+    {
+      fieldName: 'blobIds',
+      label: '闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        disabled: readonly,
+      },
+      formItemClass: 'col-span-3',
+    },
   ];
 }
 
diff --git a/src/views/aftersales/ticket/detail.vue b/src/views/aftersales/ticket/detail.vue
index 09eb0b4..b9a6d7e 100644
--- a/src/views/aftersales/ticket/detail.vue
+++ b/src/views/aftersales/ticket/detail.vue
@@ -241,6 +241,21 @@
       </div>
     </Card>
 
+    <!-- 闄勪欢 -->
+    <Card v-if="ticket?.attachmentList?.length" title="闄勪欢" class="mb-4">
+      <div class="flex flex-wrap gap-3">
+        <a
+          v-for="file in ticket.attachmentList"
+          :key="file.id"
+          :href="file.url"
+          target="_blank"
+          class="text-blue-500 hover:underline"
+        >
+          {{ file.name || '闄勪欢-' + file.id }}
+        </a>
+      </div>
+    </Card>
+
     <!-- 闂鍒ゅ畾琛ㄥ崟 -->
     <Card v-if="showClassifyInput" title="闂鍒ゅ畾" class="mb-4">
       <div class="flex flex-wrap items-center gap-3">
diff --git a/src/views/aftersales/ticket/modules/form.vue b/src/views/aftersales/ticket/modules/form.vue
index 3a4ba1c..bdb3fab 100644
--- a/src/views/aftersales/ticket/modules/form.vue
+++ b/src/views/aftersales/ticket/modules/form.vue
@@ -145,6 +145,14 @@
         items.value = formData.value?.items || [];
         await refreshItemsGrid();
         await formApi.setValues(formData.value as Record<string, unknown>);
+        // 鍥炴樉闄勪欢
+        if (formData.value?.attachmentList?.length) {
+          const blobIds = formData.value.attachmentList.map((item) => ({
+            uid: String(item.id), name: item.name || '', url: item.url || '',
+            status: 'done', id: item.id,
+          }));
+          await formApi.setFieldValue('blobIds', blobIds);
+        }
         if (formData.value?.saleOrderId) {
           const order = await getSaleOrder(formData.value.saleOrderId);
           orderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
diff --git a/src/views/ai/knowledge/document/form/modules/split-step.vue b/src/views/ai/knowledge/document/form/modules/split-step.vue
index b03cc47..a96285d 100644
--- a/src/views/ai/knowledge/document/form/modules/split-step.vue
+++ b/src/views/ai/knowledge/document/form/modules/split-step.vue
@@ -57,8 +57,9 @@
   splitLoading.value = true;
   try {
     // 璋冪敤鍚庣鍒嗘鎺ュ彛锛岃幏鍙栨枃妗g殑鍒嗘鍐呭銆佸瓧绗︽暟鍜� Token 鏁�
+    const url = typeof file.url === 'string' ? file.url : file.url?.url;
     file.segments = await splitContent(
-      file.url,
+      url,
       modelData.value.segmentMaxTokens,
     );
   } catch (error) {
diff --git a/src/views/mes/md/item/components/select.vue b/src/views/mes/md/item/components/select.vue
index d157aff..6a45bdb 100644
--- a/src/views/mes/md/item/components/select.vue
+++ b/src/views/mes/md/item/components/select.vue
@@ -17,7 +17,8 @@
   defineProps<{
     allowClear?: boolean;
     disabled?: boolean;
-    modelValue?: number;
+    modelValue?: number | number[];
+    multiple?: boolean;
     placeholder?: string;
     productIds?: number[];
   }>(),
@@ -25,53 +26,70 @@
     allowClear: true,
     disabled: false,
     modelValue: undefined,
+    multiple: false,
     placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
     productIds: () => [],
   },
 );
 const emit = defineEmits<{
-  change: [item: MesMdItemApi.Item | undefined];
-  'update:modelValue': [value: number | undefined];
+  change: [items: MesMdItemApi.Item | MesMdItemApi.Item[] | undefined];
+  'update:modelValue': [value: number | number[] | undefined];
 }>();
-const attrs = useAttrs(); // 閫忎紶灞炴��
-const dialogRef = ref<InstanceType<typeof MdItemSelectDialog>>(); // 鐗╂枡閫夋嫨寮圭獥
-const hovering = ref(false); // 鏄惁鎮仠
-const selectedItem = ref<MesMdItemApi.Item>(); // 褰撳墠閫変腑鐗╂枡
+const attrs = useAttrs();
+const dialogRef = ref<InstanceType<typeof MdItemSelectDialog>>();
+const hovering = ref(false);
+const selectedItems = ref<MesMdItemApi.Item[]>([]);
 
-const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const displayLabel = computed(() => {
+  if (selectedItems.value.length === 0) return '';
+  return selectedItems.value.map(item => item.name ?? '').join('銆�');
+});
 const showClear = computed(
   () =>
     props.allowClear &&
     !props.disabled &&
     hovering.value &&
-    props.modelValue !== null,
+    (props.multiple
+      ? Array.isArray(props.modelValue) && props.modelValue.length > 0
+      : props.modelValue != null),
 );
 
 /** 鏍规嵁鐗╂枡缂栧彿鍥炴樉閫夋嫨鍣� */
-async function resolveItemById(id: number | undefined) {
-  if (!id) {
-    selectedItem.value = undefined;
+async function resolveItemsById(values: number | number[] | undefined) {
+  if (values == null || (Array.isArray(values) && values.length === 0)) {
+    selectedItems.value = [];
     return;
   }
-  if (selectedItem.value?.id === id) {
+  const ids = Array.isArray(values) ? values : [values];
+  // 宸插叏閮ㄥ洖鏄惧垯璺宠繃
+  if (
+    ids.length === selectedItems.value.length &&
+    ids.every((id, i) => selectedItems.value[i]?.id === id)
+  ) {
     return;
   }
-  selectedItem.value = await getItem(id);
+  const items = await Promise.all(ids.map(id => getItem(id)));
+  selectedItems.value = items.filter(Boolean) as MesMdItemApi.Item[];
 }
 
 watch(
   () => props.modelValue,
   (value) => {
-    resolveItemById(value);
+    resolveItemsById(value);
   },
   { immediate: true },
 );
 
 /** 娓呯┖宸查�夌墿鏂� */
 function clearSelected() {
-  selectedItem.value = undefined;
-  emit('update:modelValue', undefined);
-  emit('change', undefined);
+  selectedItems.value = [];
+  if (props.multiple) {
+    emit('update:modelValue', []);
+    emit('change', []);
+  } else {
+    emit('update:modelValue', undefined);
+    emit('change', undefined);
+  }
 }
 
 /** 鎵撳紑鐗╂枡閫夋嫨寮圭獥 */
@@ -85,24 +103,33 @@
     clearSelected();
     return;
   }
-  const selectedIds = (
-    props.modelValue === null ? [] : [props.modelValue]
-  ) as number[];
+  const selectedIds: number[] = props.multiple
+    ? (props.modelValue as number[] | undefined) ?? []
+    : props.modelValue != null
+      ? [props.modelValue as number]
+      : [];
   dialogRef.value?.open(selectedIds, {
-    multiple: false,
+    multiple: props.multiple,
     productIds: props.productIds,
   });
 }
 
 /** 鍥炲~閫変腑鐨勭墿鏂� */
 function handleSelected(rows: MesMdItemApi.Item[]) {
-  const item = rows[0];
-  if (!item) {
-    return;
+  if (props.multiple) {
+    selectedItems.value = rows;
+    const ids = rows.map(r => r.id).filter(Boolean) as number[];
+    emit('update:modelValue', ids);
+    emit('change', rows);
+  } else {
+    const item = rows[0];
+    if (!item) {
+      return;
+    }
+    selectedItems.value = [item];
+    emit('update:modelValue', item.id);
+    emit('change', item);
   }
-  selectedItem.value = item;
-  emit('update:modelValue', item.id);
-  emit('change', item);
 }
 </script>
 
@@ -115,13 +142,21 @@
     @mouseenter="hovering = true"
     @mouseleave="hovering = false"
   >
-    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+    <Tooltip :mouse-enter-delay="0.5" :open="selectedItems.length > 0 ? undefined : false">
       <template #title>
-        <div v-if="selectedItem" class="leading-6">
-          <div>缂栫爜锛歿{ selectedItem.code || '-' }}</div>
-          <div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
-          <div>瑙勬牸锛歿{ selectedItem.specification || '-' }}</div>
-          <div>鍗曚綅锛歿{ selectedItem.unitMeasureName || '-' }}</div>
+        <div v-if="selectedItems.length === 1" class="leading-6">
+          <div>缂栫爜锛歿{ selectedItems[0].code || '-' }}</div>
+          <div>鍚嶇О锛歿{ selectedItems[0].name || '-' }}</div>
+          <div>瑙勬牸锛歿{ selectedItems[0].specification || '-' }}</div>
+          <div>鍗曚綅锛歿{ selectedItems[0].unitMeasureName || '-' }}</div>
+        </div>
+        <div v-else-if="selectedItems.length > 1" class="leading-6">
+          <div v-for="item in selectedItems" :key="item.id" class="mb-2 border-b border-gray-100 pb-1 last:border-b-0 last:pb-0">
+            <div>缂栫爜锛歿{ item.code || '-' }}</div>
+            <div>鍚嶇О锛歿{ item.name || '-' }}</div>
+            <div>瑙勬牸锛歿{ item.specification || '-' }}</div>
+            <div>鍗曚綅锛歿{ item.unitMeasureName || '-' }}</div>
+          </div>
         </div>
       </template>
       <Input
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index d01a104..af895bd 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -686,14 +686,9 @@
       slots: { default: 'backflushFlag' },
     },
     {
-      field: 'outputItemCode',
-      title: '浜у嚭鐗╂枡缂栫爜',
-      width: 120,
-    },
-    {
-      field: 'outputItemName',
-      title: '浜у嚭鐗╂枡鍚嶇О',
-      width: 120,
+      field: 'outputItemNames',
+      title: '浜у嚭鐗╂枡',
+      width: 160,
     },
     {
       field: 'remark',
diff --git a/src/views/mes/pro/workorder/modules/process-form.vue b/src/views/mes/pro/workorder/modules/process-form.vue
index 7cfb93c..bdbdcaa 100644
--- a/src/views/mes/pro/workorder/modules/process-form.vue
+++ b/src/views/mes/pro/workorder/modules/process-form.vue
@@ -170,10 +170,11 @@
       defaultValue: true,
     },
     {
-      fieldName: 'outputItemId',
+      fieldName: 'outputItemIds',
       label: '浜у嚭鐗╂枡',
       component: MdItemSelect,
       componentProps: {
+        multiple: true,
         placeholder: '璇烽�夋嫨浜у嚭鐗╂枡',
       },
     },
diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
index fd2a7e7..283424d 100644
--- a/src/views/mes/pro/workorder/modules/process-list.vue
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -156,9 +156,8 @@
           keyFlag: item.keyFlag,
           checkFlag: item.checkFlag,
           backflushFlag: item.backflushFlag,
-          outputItemId: item.outputItemId,
-          outputItemCode: item.outputItemCode,
-          outputItemName: item.outputItemName,
+          outputItemIds: item.outputItemIds,
+          outputItemNames: item.outputItemNames,
           remark: item.remark,
           bomItems: item.bomItems,
         }));
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
index 3c76624..941a2be 100644
--- a/src/views/mes/process-design/route/data.ts
+++ b/src/views/mes/process-design/route/data.ts
@@ -248,10 +248,11 @@
       rules: z.boolean().default(true),
     },
     {
-      fieldName: "outputItemId",
+      fieldName: "outputItemIds",
       label: "浜у嚭浜у搧",
       component: markRaw(MdItemSelect),
       componentProps: {
+        multiple: true,
         placeholder: "鐣欑┖鍒欓粯璁や负宸ュ崟鎴愬搧",
       },
       formItemClass: "col-span-2",
@@ -336,10 +337,9 @@
       },
     },
     {
-      field: "outputItemName",
+      field: "outputItemNames",
       title: "浜у嚭浜у搧",
-      width: 140,
-      slots: { default: "outputItemName" },
+      width: 160,
     },
     { field: "prepareTime", title: "鍑嗗鏃堕棿(鍒�)", width: 110 },
     { field: "waitTime", title: "绛夊緟鏃堕棿(鍒�)", width: 110 },
diff --git a/src/views/mes/process-design/route/modules/process-list.vue b/src/views/mes/process-design/route/modules/process-list.vue
index e6a99c1..192cb5c 100644
--- a/src/views/mes/process-design/route/modules/process-list.vue
+++ b/src/views/mes/process-design/route/modules/process-list.vue
@@ -110,10 +110,6 @@
         <span>{{ row.colorCode }}</span>
       </div>
     </template>
-    <template #outputItemName="{ row }">
-      <span v-if="row.outputItemName">{{ row.outputItemName }}</span>
-      <span v-else class="text-gray-400">-</span>
-    </template>
     <template #actions="{ row }">
       <TableAction
         :actions="[

--
Gitblit v1.9.3