2026-08-04 49c9eff61882f1f7d519397ccfdd90cdd8735cb8
feat(aftersales): 售后工单支持附件上传和显示

- 在售后工单表单中新增附件上传组件,支持多文件上传
- 在工单详情页面添加附件列表显示功能
- 实现工单编辑时附件数据的回显功能
- 添加附件相关的API字段定义和数据处理逻辑
- 优化附件文件的URL处理逻辑

feat(mes): 工序产出物料支持多选功能

- 将产出物料相关字段从单个值改为数组形式
- 修改工艺路线和工单流程中的产出物料选择组件为多选模式
- 更新相关API接口定义以支持多个产出物料
- 调整表格列显示逻辑以适配新的多选数据结构
- 修改物料选择组件以支持单选和多选模式切换

chore(config): 配置文件中启用MCP服务器支持

- 在本地设置中添加MCP JSON服务器配置
- 启用所有项目MCP服务器功能
- 添加IDEA服务器支持配置
已修改14个文件
178 ■■■■ 文件已修改
.claude/settings.local.json 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/aftersales/ticket.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/process/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/process/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/detail.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/modules/form.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/knowledge/document/form/modules/split-step.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/components/select.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/process-form.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/process-list.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-list.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json
@@ -22,5 +22,9 @@
      "Bash(node lib/script/postinstall.js)",
      "Bash(mvn compile *)"
    ]
  }
  },
  "enabledMcpjsonServers": [
    "idea"
  ],
  "enableAllProjectMcpServers": true
}
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[];
  }
}
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;
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; // 备注
  }
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',
    },
  ];
}
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">
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[] || [];
src/views/ai/knowledge/document/form/modules/split-step.vue
@@ -57,8 +57,9 @@
  splitLoading.value = true;
  try {
    // 调用后端分段接口,获取文档的分段内容、字符数和 Token 数
    const url = typeof file.url === 'string' ? file.url : file.url?.url;
    file.segments = await splitContent(
      file.url,
      url,
      modelData.value.segmentMaxTokens,
    );
  } catch (error) {
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;
  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[]) {
  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;
  }
  selectedItem.value = item;
    selectedItems.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
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',
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: '请选择产出物料',
      },
    },
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,
        }));
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 },
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="[