gaoluyang
6 天以前 4d024646448b9c75f51045989cb52c533ceb9c10
修复排产页甘特图渲染异常导致无法跳转其它页面

- 甘特图对 dates 做空值/非法值解析,工单(project)行用子任务最早开始/最晚结束补齐计划时间
- 无有效计划时间的节点不进入甘特图,parse 异常不影响页面其它部分
- 任务列表在没有可展示数据时显示空状态,避免空数据集传给甘特图
- useFilePreview 改用 reactive 包装状态,修复 v-bind 传入 Ref 导致 FilePreviewModal 属性告警

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改5个文件
127 ■■■■■ 文件已修改
src/components/file-preview/use-file-preview.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/chat/index/modules/message/files.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/archive/modules/detail.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/components/gantt-chart.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/file-preview/use-file-preview.ts
@@ -1,34 +1,31 @@
import type { Ref } from 'vue';
import { reactive } from 'vue';
import { computed, ref } from 'vue';
import { getFileNameFromUrl, getFileType } from '@vben/utils';
import { getFileNameFromUrl } from '@vben/utils';
export function useFilePreview() {
  const visible = ref(false);
  const fileUrl = ref('');
  const fileName = ref('');
  const fileType = computed(() => getFileType(fileName.value || fileUrl.value));
  /**
   * 用 reactive 而不是 ref 包装:调用方通过 `v-bind="previewState"` 传参时,
   * 普通对象里的 ref 不会被模板自动解包,会把 Ref 对象当 prop 传给预览组件。
   */
  const previewState = reactive({
    fileUrl: '',
    fileName: '',
    visible: false,
  });
  function openPreview(url: string, name?: string) {
    fileUrl.value = url;
    fileName.value = name || getFileNameFromUrl(url);
    visible.value = true;
    previewState.fileUrl = url;
    previewState.fileName = name || getFileNameFromUrl(url);
    previewState.visible = true;
  }
  function closePreview() {
    visible.value = false;
    previewState.visible = false;
  }
  return {
    closePreview,
    openPreview,
    previewState: {
      fileUrl: fileUrl as Ref<string>,
      fileName: fileName as Ref<string>,
      fileType,
      visible: visible as Ref<boolean>,
    },
    previewState,
  };
}
src/views/ai/chat/index/modules/message/files.vue
@@ -59,8 +59,8 @@
    </div>
  </div>
  <FilePreviewModal
    v-model:visible="previewState.visible.value"
    :file-url="previewState.fileUrl.value"
    :file-name="previewState.fileName.value"
    v-model:visible="previewState.visible"
    :file-url="previewState.fileUrl"
    :file-name="previewState.fileName"
  />
</template>
src/views/mes/pd/archive/modules/detail.vue
@@ -129,7 +129,10 @@
</script>
<template>
  <FilePreviewModal v-bind="previewState" />
  <FilePreviewModal
    v-bind="previewState"
    @update:visible="previewState.visible = $event"
  />
  <Modal v-model:open="open" title="归档详情" width="80%" :footer="null" @cancel="handleClose">
    <div v-if="loading" class="flex justify-center py-8">
      <Spin />
src/views/mes/pro/task/components/gantt-chart.vue
@@ -157,6 +157,15 @@
  ganttInited.value = true;
}
/** 解析时间,非法或空值返回 null(dhtmlx 收到 null 时间会在计算时间轴时崩溃) */
function parseDate(value: any): Date | null {
  if (value === null || value === undefined || value === '') {
    return null;
  }
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}
/** 加载数据到甘特图 */
function loadData(tasks: any[]) {
  if (!ganttInited.value) {
@@ -168,22 +177,69 @@
    [BarcodeBizTypeEnum.WORKORDER]: 'project',
    [BarcodeBizTypeEnum.TASK]: 'task',
  };
  gantt.parse({
    data: tasks.map((item: any) => ({
      ...item,
      type: typeMap[item.type] || item.type,
      start_date: item.startDate ? new Date(item.startDate) : undefined,
      end_date: item.endDate ? new Date(item.endDate) : undefined,
    })),
    links: [],
  });
  const rows = tasks.map((item: any) => ({
    ...item,
    type: typeMap[item.type] || item.type,
    start_date: parseDate(item.startDate),
    end_date: parseDate(item.endDate),
  }));
  // 工单(project)行本身不带计划时间,用其任务的最早开始、最晚结束补齐
  const children = new Map<string, any[]>();
  for (const row of rows) {
    if (row.parent) {
      const list = children.get(String(row.parent)) ?? [];
      list.push(row);
      children.set(String(row.parent), list);
    }
  }
  for (const row of rows) {
    if (row.type !== 'project' || (row.start_date && row.end_date)) {
      continue;
    }
    const childRows: any[] = children.get(String(row.id)) ?? [];
    const starts = childRows
      .map((child) => child.start_date)
      .filter((date): date is Date => date instanceof Date);
    const ends = childRows
      .map((child) => child.end_date)
      .filter((date): date is Date => date instanceof Date);
    if (!row.start_date && starts.length > 0) {
      row.start_date = new Date(Math.min(...starts.map((date) => date.getTime())));
    }
    if (!row.end_date && ends.length > 0) {
      row.end_date = new Date(Math.max(...ends.map((date) => date.getTime())));
    }
  }
  // 没有有效计划时间的节点(如从未排产的工单)不进入甘特图,否则整页渲染会抛异常
  const data = rows.filter(
    (row) =>
      row.start_date &&
      row.end_date &&
      row.start_date.getTime() <= row.end_date.getTime(),
  );
  if (data.length === 0) {
    return;
  }
  try {
    gantt.parse({ data, links: [] });
  } catch (error) {
    // 第三方甘特图异常不能冒泡打断路由/页面渲染
    console.error('甘特图渲染失败', error);
  }
}
watch(
  () => props.tasks,
  (val) => {
    if (val?.length && ganttInited.value) {
    if (!ganttInited.value) {
      return;
    }
    if (val?.length) {
      loadData(val);
    } else {
      gantt.clearAll();
    }
  },
  { deep: true },
src/views/mes/pro/task/index.vue
@@ -184,7 +184,10 @@
      <template #extra>
        <span class="text-xs text-gray-400">展示已确认自制订单的任务计划</span>
      </template>
      <GanttChart :height="350" :readonly="true" :tasks="ganttTasks" />
      <GanttChart v-if="ganttTasks.length" :height="350" :readonly="true" :tasks="ganttTasks" />
      <div v-else class="py-10 text-center text-sm text-gray-400">
        暂无已排产的生产任务
      </div>
    </Card>
    <Grid>