Преглед изворни кода

[Mod 0000]AI助手修改对dxf格式的处理

wangkeyi пре 21 часа
родитељ
комит
d5e0e1f8bb

+ 4 - 4
src/views/ventAI/manageAssistent/api/cad.ts

@@ -1,7 +1,7 @@
-// api/cad.ts:DWG 图纸预览所需的「服务端转 SVG」接口。
+// api/cad.ts:DWG / DXF 图纸预览所需的「服务端转 SVG」接口。
 //
-// DWG 是专有二进制格式,前端没法本地解析(对比 .dxf 走 lib/dxf.ts 本地渲染),只能把原始文件传给后端转成
-// SVG 再展示。三种取图方式,这里只留两种:
+// 两种格式前端都没法本地解析,统一把原始文件传给后端转成 SVG 再展示(后端对 dwg/dxf 的处理、
+// 参数与返回完全一致)。三种取图方式,这里只留两种:
 //   1) dwgFileToSVGInfo:上传文件 + 转换,回 svg 正文。本地附件(用户刚拖进来的)用这个;
 //   2) dwgServerFileToSVGInfo:按服务端已有的附件 file_id 直接取转换结果,回 svg 正文。
 //      历史消息里的附件后端早就转过一次了,用它就不必重新上传+转换;
@@ -17,7 +17,7 @@ enum Api {
   dwgServerFileToSvgInfo = '/Vmodel/ventanalyModel/dwgServerFileToSVGInfo',
 }
 
-/** 转换业务目录:后端据此存放上传的 dwg 与生成的 svg(对应返回里的 temp/dwg、/temp/svg/*.svg) */
+/** 转换业务目录:后端据此存放上传的图纸与生成的 svg(dwg / dxf 都用这个值) */
 const DWG_BIZ = 'temp/dwg';
 
 export interface DwgToSvgInfoResult {

+ 0 - 319
src/views/ventAI/manageAssistent/components/resource/CadPreview.vue

@@ -1,319 +0,0 @@
-<template>
-  <div class="cad-preview">
-    <!-- DXF:本地解析 + canvas 渲染,不依赖任何后端服务 -->
-    <div class="cad-dxf">
-      <div v-if="dxfError" class="cad-hint">{{ dxfError }}</div>
-      <div v-else class="cad-canvas-area">
-        <canvas ref="canvasRef" class="cad-canvas" @wheel.prevent="onWheel" @mousedown="onMouseDown" @dblclick="fitView"></canvas>
-        <div class="cad-toolbar">
-          <span class="cad-stat">{{ dxfStat }}</span>
-          <a-button size="small" type="link" @click="fitView">适应窗口</a-button>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
-  import { parseDxf } from '/@/views/ventAI/manageAssistent/lib/dxf';
-  import type { DxfEntity, DxfParseResult } from '/@/views/ventAI/manageAssistent/lib/dxf';
-  import type { AttachedFile } from '/@/views/ventAI/manageAssistent/types/index';
-
-  /**
-   * 右侧面板的 CAD 预览:本地解析 DXF 并在 canvas 上绘制(可滚轮缩放、拖拽平移),不依赖任何后端服务。
-   *
-   * 当前只处理 .dxf——面板仅在 isDxfFile 命中时才渲染本组件。
-   * .dwg(专有二进制,需上传后端转 SVG)见 components/resource/DwgPreview.vue;
-   * .mxweb(依赖 mxcad 及其配套服务)暂不预览,由 fetchPreviewFile 返回 null 走"回退为下载"的老路径。
-   */
-  const props = defineProps<{ file: AttachedFile }>();
-
-  // —— DXF ——
-  const canvasRef = ref<HTMLCanvasElement>();
-  const dxfError = ref('');
-  const parsed = ref<DxfParseResult | null>(null);
-  const view = ref({ scale: 1, tx: 0, ty: 0 });
-  const dxfStat = computed(() => {
-    const r = parsed.value;
-    if (!r) return '';
-    return r.skipped > 0 ? `${r.entities.length} 个对象(跳过 ${r.skipped} 个不支持的对象)` : `${r.entities.length} 个对象`;
-  });
-
-  const loadDxf = () => {
-    parsed.value = null;
-    dxfError.value = '';
-    const buf = props.file.arrayBuffer;
-    const text = props.file.content ?? (buf ? new TextDecoder().decode(buf) : '');
-    if (!text.trim()) {
-      dxfError.value = '未取到 DXF 内容,无法预览';
-      return;
-    }
-    const result = parseDxf(text);
-    if (!result.entities.length) {
-      dxfError.value = result.skipped ? `未解析到可显示的图形(跳过 ${result.skipped} 个不支持的实体)` : 'DXF 中没有可显示的实体';
-      return;
-    }
-    parsed.value = result;
-    fitView();
-  };
-
-  /** 按图形范围缩放到画布(留 10% 边距) */
-  const fitView = () => {
-    const canvas = canvasRef.value;
-    if (!canvas || !parsed.value) return;
-    const w = canvas.clientWidth || 1;
-    const h = canvas.clientHeight || 1;
-    const b = parsed.value.bounds;
-    if (!b) {
-      view.value = { scale: 1, tx: w / 2, ty: h / 2 };
-      draw();
-      return;
-    }
-    const spanX = Math.max(b.maxX - b.minX, 1e-6);
-    const spanY = Math.max(b.maxY - b.minY, 1e-6);
-    const scale = Math.min(w / spanX, h / spanY) * 0.9;
-    const cx = (b.minX + b.maxX) / 2;
-    const cy = (b.minY + b.maxY) / 2;
-    // 纵向取加号:绘制时 y 轴翻转(screenY = ty - y * scale)
-    view.value = { scale, tx: w / 2 - cx * scale, ty: h / 2 + cy * scale };
-    draw();
-  };
-
-  const pathEntity = (ctx: CanvasRenderingContext2D, e: DxfEntity) => {
-    ctx.beginPath();
-    switch (e.kind) {
-      case 'line':
-        ctx.moveTo(e.a.x, e.a.y);
-        ctx.lineTo(e.b.x, e.b.y);
-        break;
-      case 'polyline': {
-        if (!e.points.length) return;
-        ctx.moveTo(e.points[0].x, e.points[0].y);
-        for (let i = 1; i < e.points.length; i++) ctx.lineTo(e.points[i].x, e.points[i].y);
-        if (e.closed) ctx.closePath();
-        break;
-      }
-      case 'circle':
-        ctx.arc(e.center.x, e.center.y, Math.max(e.radius, 0), 0, Math.PI * 2);
-        break;
-      case 'arc':
-        // 绘制变换含 y 翻转,而 arc 的参数 t 恰好等于 DXF 角度(cos/sin 直接对应图纸坐标),
-        // 所以取 false(t 递增)才是 DXF 意义上的逆时针短弧;取 true 会画成 270° 的补弧(实测如此)。
-        ctx.arc(e.center.x, e.center.y, Math.max(e.radius, 0), (e.startDeg * Math.PI) / 180, (e.endDeg * Math.PI) / 180, false);
-        break;
-      case 'ellipse':
-        ctx.ellipse(
-          e.center.x,
-          e.center.y,
-          Math.max(e.rx, 0),
-          Math.max(e.ry, 0),
-          (e.rotationDeg * Math.PI) / 180,
-          (e.startDeg * Math.PI) / 180,
-          (e.endDeg * Math.PI) / 180,
-          false
-        );
-        break;
-      case 'point': {
-        // 单点必须画成小十字,否则 stroke 什么都看不到
-        const r = 1.5 / (view.value.scale || 1);
-        ctx.moveTo(e.at.x - r, e.at.y);
-        ctx.lineTo(e.at.x + r, e.at.y);
-        ctx.moveTo(e.at.x, e.at.y - r);
-        ctx.lineTo(e.at.x, e.at.y + r);
-        break;
-      }
-      default:
-        break;
-    }
-  };
-
-  const drawTextEntity = (ctx: CanvasRenderingContext2D, e: Extract<DxfEntity, { kind: 'text' }>) => {
-    const { scale, tx, ty } = view.value;
-    const px = e.height * scale;
-    // 太小看不清、太大(图纸被缩得极小或字号本就夸张)就不画,避免糊满整屏
-    if (!Number.isFinite(px) || px < 5 || px > 120) return;
-    ctx.save();
-    ctx.translate(tx + e.at.x * scale, ty - e.at.y * scale);
-    if (e.rotationDeg) ctx.rotate((-e.rotationDeg * Math.PI) / 180);
-    ctx.font = `${px}px sans-serif`;
-    ctx.fillStyle = '#9fb3c8';
-    ctx.fillText(e.text, 0, 0);
-    ctx.restore();
-  };
-
-  const draw = () => {
-    const canvas = canvasRef.value;
-    const data = parsed.value;
-    if (!canvas || !data) return;
-    const dpr = window.devicePixelRatio || 1;
-    const w = canvas.clientWidth || 1;
-    const h = canvas.clientHeight || 1;
-    canvas.width = Math.round(w * dpr);
-    canvas.height = Math.round(h * dpr);
-    const ctx = canvas.getContext('2d');
-    if (!ctx) return;
-    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
-    ctx.clearRect(0, 0, w, h);
-
-    const { scale, tx, ty } = view.value;
-    ctx.save();
-    ctx.translate(tx, ty);
-    // DXF 的 y 轴向上,canvas 向下 → 纵向取负;线宽除以 scale 保持屏幕上恒为 1px
-    ctx.scale(scale, -scale);
-    ctx.strokeStyle = '#c9d6e4';
-    ctx.lineWidth = 1 / scale;
-    for (const e of data.entities) {
-      if (e.kind === 'text') continue;
-      pathEntity(ctx, e);
-      ctx.stroke();
-    }
-    ctx.restore();
-
-    // 文字在翻转后的坐标系里会被镜像,单独按屏幕坐标绘制
-    for (const e of data.entities) {
-      if (e.kind === 'text') drawTextEntity(ctx, e);
-    }
-  };
-
-  // 平移/缩放
-  let dragging = false;
-  let lastX = 0;
-  let lastY = 0;
-
-  const onMouseMove = (ev: MouseEvent) => {
-    if (!dragging) return;
-    const v = view.value;
-    view.value = { ...v, tx: v.tx + (ev.clientX - lastX), ty: v.ty + (ev.clientY - lastY) };
-    lastX = ev.clientX;
-    lastY = ev.clientY;
-    draw();
-  };
-
-  const onMouseUp = () => {
-    dragging = false;
-    window.removeEventListener('mousemove', onMouseMove);
-    window.removeEventListener('mouseup', onMouseUp);
-  };
-
-  const onMouseDown = (ev: MouseEvent) => {
-    dragging = true;
-    lastX = ev.clientX;
-    lastY = ev.clientY;
-    window.addEventListener('mousemove', onMouseMove);
-    window.addEventListener('mouseup', onMouseUp);
-  };
-
-  const onWheel = (ev: WheelEvent) => {
-    const canvas = canvasRef.value;
-    if (!canvas) return;
-    const rect = canvas.getBoundingClientRect();
-    const px = ev.clientX - rect.left;
-    const py = ev.clientY - rect.top;
-    const v = view.value;
-    const scale = Math.min(Math.max(v.scale * (ev.deltaY < 0 ? 1.15 : 1 / 1.15), 1e-6), 1e6);
-    // 以光标为不动点:tx' = px - (px - tx) * k
-    const k = scale / v.scale;
-    view.value = { scale, tx: px - (px - v.tx) * k, ty: py - (py - v.ty) * k };
-    draw();
-  };
-
-  let resizeObserver: ResizeObserver | null = null;
-
-  // 每次换文件后都要把观察者绑到「当前这块」画布:DXF 分支的画布是随文件类型重新创建的,
-  // 继续观察被替换掉的旧画布,尺寸变化就不会再触发重绘。
-  const setupResizeObserver = () => {
-    resizeObserver?.disconnect();
-    resizeObserver = null;
-    const el = canvasRef.value;
-    if (!el) return;
-    resizeObserver = new ResizeObserver(() => {
-      if (parsed.value) fitView();
-    });
-    resizeObserver.observe(el);
-  };
-
-  // 面板实例会在多个文件标签之间复用(RightPanelTabs 没加 :key),换文件必须重新初始化。
-  // 必须先等一次 nextTick:面板实例在多个文件标签间复用,此刻模板还停在上一个文件的分支上
-  //(例如文本预览块),画布尚未挂载,canvasRef 为空会让 fitView 直接 return,绘制被静默跳过——
-  // 表现就是面板一片空白,得手动点一次「适应窗口」才出图。
-  const init = async () => {
-    onMouseUp();
-    parsed.value = null;
-    dxfError.value = '';
-    await nextTick();
-    loadDxf();
-    setupResizeObserver();
-  };
-
-  onMounted(() => {
-    void init();
-  });
-
-  watch(
-    () => props.file,
-    () => {
-      void init();
-    }
-  );
-
-  onBeforeUnmount(() => {
-    onMouseUp();
-    resizeObserver?.disconnect();
-    resizeObserver = null;
-  });
-</script>
-
-<style scoped lang="less">
-  .cad-preview {
-    display: flex;
-    flex-direction: column;
-    height: 100%;
-    min-height: 0;
-  }
-
-  .cad-dxf {
-    display: flex;
-    flex-direction: column;
-    flex: 1;
-    min-height: 0;
-  }
-
-  .cad-canvas-area {
-    position: relative;
-    flex: 1;
-    min-height: 320px;
-  }
-
-  .cad-canvas {
-    display: block;
-    width: 100%;
-    height: 100%;
-    cursor: grab;
-    background: rgb(13 21 32 / 55%);
-
-    &:active {
-      cursor: grabbing;
-    }
-  }
-
-  .cad-toolbar {
-    position: absolute;
-    right: 8px;
-    bottom: 8px;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-  }
-
-  .cad-stat {
-    font-size: 12px;
-    color: #8b949e;
-  }
-
-  .cad-hint {
-    padding: 12px 16px;
-    font-size: 13px;
-    color: #8b949e;
-  }
-</style>

+ 3 - 4
src/views/ventAI/manageAssistent/components/resource/DwgPreview.vue

@@ -34,10 +34,9 @@
   import type { AttachedFile } from '/@/views/ventAI/manageAssistent/types/index';
 
   /**
-   * 右侧面板的 DWG 预览。
+   * 右侧面板的 DWG / DXF 预览(后端对两种格式的转换接口、参数与返回完全一致)。
    *
-   * DWG 是专有二进制格式,前端解析不了(对比 .dxf:明文,见 CadPreview 本地解析渲染),
-   * 只能让后端转成 SVG 再当作图片展示。两种取图方式:
+   * 前端解析不了这两种格式,只能让后端转成 SVG 再当作图片展示。两种取图方式:
    *   - 历史消息里的附件(带 fileId):后端早就转过一次,按 fileId 直接取转换结果,不再上传;
    *   - 本地附件(有原始字节、没有 fileId):上传文件再转换。
    *
@@ -175,7 +174,7 @@
     error.value = '图纸加载失败,请稍后重试';
   };
 
-  // 平移/缩放:与 DXF 预览(CadPreview)保持同一套操作——滚轮缩放、按住拖拽、双击复位
+  // 平移/缩放:滚轮缩放、按住拖拽、双击复位
   let dragging = false;
   let lastX = 0;
   let lastY = 0;

+ 2 - 5
src/views/ventAI/manageAssistent/components/resource/FilePreviewPanel.vue

@@ -87,10 +87,8 @@
         <div v-else-if="isMdFile(file.name)" class="markdown-preview">
           <div class="markdown-body" v-html="renderedMarkdown" @click="handleTableAction"></div>
         </div>
-        <div v-else-if="isDxfFile(file.name)" class="cad-preview-wrapper">
-          <CadPreview :file="file" />
-        </div>
-        <div v-else-if="isDwgFile(file.name)" class="cad-preview-wrapper">
+        <!-- DXF 与 DWG 一样:交给后端转 SVG 后展示(接口、参数、返回完全一致) -->
+        <div v-else-if="isDxfFile(file.name) || isDwgFile(file.name)" class="cad-preview-wrapper">
           <DwgPreview :file="file" />
         </div>
         <div v-else-if="isTextFile(file.name)" class="text-preview">
@@ -131,7 +129,6 @@
     flashCopySuccess,
   } from '/@/views/ventAI/manageAssistent/lib/index';
   import type { AttachedFile } from '/@/views/ventAI/manageAssistent/types/index';
-  import CadPreview from '/@/views/ventAI/manageAssistent/components/resource/CadPreview.vue';
   import DwgPreview from '/@/views/ventAI/manageAssistent/components/resource/DwgPreview.vue';
 
   // [perf-base-v1] 卡2c: VueOfficeDocx 改异步加载,仅打开 docx 预览时才拉取 office-preview chunk

+ 3 - 3
src/views/ventAI/manageAssistent/hooks/useChatAttachments.ts

@@ -60,12 +60,12 @@ const ensureAttachmentContent = async (file: AttachedFile): Promise<AttachedFile
   if (hasContent(file)) return file;
   if (!file.fileId) throw new Error('附件缺少下载标识');
 
-  // dwg 历史附件不用把原始文件下载回来:预览面板会按 fileId 直接取后端已转换好的 svg
+  // dwg / dxf 历史附件不用把原始文件下载回来:预览面板会按 fileId 直接取后端已转换好的 svg
   //(见 DwgPreview 的 requestSvgText),下载几百 KB 只为了再上传转换一遍属于纯浪费。
-  if (isDwgFile(file.name)) return file;
+  if (isDwgFile(file.name) || isDxfFile(file.name)) return file;
 
   const blob = await fetchAttachmentBlob(file.fileId, file.downloadUrl);
-  if (isTextFile(file.name) || isCsvFile(file.name) || isDxfFile(file.name)) {
+  if (isTextFile(file.name) || isCsvFile(file.name)) {
     file.content = await blob.text();
   } else if (isWordFile(file.name) || isExcelFile(file.name)) {
     file.arrayBuffer = await blob.arrayBuffer();

+ 4 - 5
src/views/ventAI/manageAssistent/hooks/useChatFiles.ts

@@ -59,13 +59,12 @@ export const useChatFiles = (getChatInput: () => InstanceType<typeof ChatInputAr
       activeFileReader.value = reader;
 
       // 读取方式只判断一次:onload 与 readAs* 两处各写一遍条件容易走偏。
-      // .dxf 必须在此显式分流到文本:否则会落到 dataURL 分支,附件里只有 preview、拿不到 content,
-      // DXF 预览面板只能显示空白。
-      // .dwg 同样要显式分流到二进制:预览面板得拿原始字节上传后端转 SVG,存成 base64 的 dataURL 既白占内存又没法直接用。
+      // .dxf / .dwg 要显式分流到二进制:预览面板得拿原始字节上传后端转 SVG,
+      // 存成 base64 的 dataURL 既白占内存又没法直接用。
       const readMode =
-        isTextFile(name) || isCsvFile(name) || isDxfFile(name)
+        isTextFile(name) || isCsvFile(name)
           ? 'text'
-          : isWordFile(name) || isExcelFile(name) || isDwgFile(name)
+          : isWordFile(name) || isExcelFile(name) || isDwgFile(name) || isDxfFile(name)
             ? 'binary'
             : 'dataurl';
 

+ 0 - 270
src/views/ventAI/manageAssistent/lib/dxf.ts

@@ -1,270 +0,0 @@
-// lib/dxf.ts:极简 DXF 解析(只取"画二维预览"需要的实体)。
-//
-// 为什么不引三方库:three 的 examples 里已无 DXFLoader,而 dxf-parser / three-dxf 需要新增依赖;
-// 这里只覆盖常见几何与文字实体(line/polyline/circle/arc/ellipse/text/point),
-// 不支持的实体按个数统计返回(skipped),由界面如实告知——避免"看着像空图"的误导。
-//
-// DXF 是"组码 + 值"交替两行一组的纯文本格式,且 y 轴向上(与 canvas 相反,渲染时翻转)。
-
-export interface DxfPoint {
-  x: number;
-  y: number;
-}
-
-export type DxfEntity =
-  | { kind: 'line'; a: DxfPoint; b: DxfPoint }
-  | { kind: 'polyline'; points: DxfPoint[]; closed: boolean }
-  | { kind: 'circle'; center: DxfPoint; radius: number }
-  | { kind: 'arc'; center: DxfPoint; radius: number; startDeg: number; endDeg: number }
-  | { kind: 'ellipse'; center: DxfPoint; rx: number; ry: number; rotationDeg: number; startDeg: number; endDeg: number }
-  | { kind: 'text'; at: DxfPoint; height: number; text: string; rotationDeg: number }
-  | { kind: 'point'; at: DxfPoint };
-
-export interface DxfBounds {
-  minX: number;
-  minY: number;
-  maxX: number;
-  maxY: number;
-}
-
-export interface DxfParseResult {
-  entities: DxfEntity[];
-  /** 未支持的实体个数(含 INSERT/SPLINE/HATCH/DIMENSION 等) */
-  skipped: number;
-  bounds: DxfBounds | null;
-}
-
-/** 一个实体在解析过程中的原始组码序列(保序,LWPOLYLINE 的顶点要靠顺序成对还原) */
-interface RawEntity {
-  type: string;
-  codes: Array<[number, string]>;
-}
-
-const num = (raw: RawEntity, code: number, fallback = 0): number => {
-  const hit = raw.codes.find((c) => c[0] === code);
-  const v = hit ? Number.parseFloat(hit[1]) : Number.NaN;
-  return Number.isFinite(v) ? v : fallback;
-};
-
-/** 位标志(code 70):第 1 位表示闭合 */
-const isClosed = (raw: RawEntity): boolean => (Math.trunc(num(raw, 70, 0)) & 1) === 1;
-
-const DEG = 180 / Math.PI;
-
-/** 把 LWPOLYLINE 里交替出现的 10/20 组码还原成顶点序列 */
-const polylinePoints = (raw: RawEntity): Array<{ x: number; y: number }> => {
-  const points: Array<{ x: number; y: number }> = [];
-  let pendingX: number | null = null;
-  for (const [code, value] of raw.codes) {
-    if (code === 10) {
-      pendingX = Number.parseFloat(value);
-    } else if (code === 20 && pendingX !== null) {
-      const y = Number.parseFloat(value);
-      if (Number.isFinite(pendingX) && Number.isFinite(y)) points.push({ x: pendingX, y });
-      pendingX = null;
-    }
-  }
-  return points;
-};
-
-/** 按"组码 值"两行一组切分;容错:空行/非数字组码行只跳过该行,不错位后续内容 */
-const toPairs = (dxfText: string): Array<[number, string]> => {
-  const lines = dxfText.split(/\r\n|\r|\n/);
-  const pairs: Array<[number, string]> = [];
-  let pendingCode: number | null = null;
-  for (const line of lines) {
-    if (pendingCode === null) {
-      const code = Number.parseInt(line.trim(), 10);
-      // 组码行必须是整数;空行等异常行忽略,继续等下一个组码
-      if (Number.isFinite(code)) pendingCode = code;
-      continue;
-    }
-    pairs.push([pendingCode, line]);
-    pendingCode = null;
-  }
-  return pairs;
-};
-
-/** 把原始实体转成可渲染实体;不支持的返回 null(由调用方计入 skipped) */
-const toEntity = (raw: RawEntity, verticesOfPolyline: Array<{ x: number; y: number }> | null): DxfEntity | null => {
-  switch (raw.type) {
-    case 'LINE':
-      return { kind: 'line', a: { x: num(raw, 10), y: num(raw, 20) }, b: { x: num(raw, 11), y: num(raw, 21) } };
-    case 'LWPOLYLINE': {
-      const points = polylinePoints(raw);
-      return points.length >= 2 ? { kind: 'polyline', points, closed: isClosed(raw) } : null;
-    }
-    case 'POLYLINE': {
-      // 顶点在后续的 VERTEX 实体里,由调用方收集后传入
-      const points = verticesOfPolyline ?? [];
-      return points.length >= 2 ? { kind: 'polyline', points, closed: isClosed(raw) } : null;
-    }
-    case 'CIRCLE':
-      return { kind: 'circle', center: { x: num(raw, 10), y: num(raw, 20) }, radius: num(raw, 40) };
-    case 'ARC':
-      return {
-        kind: 'arc',
-        center: { x: num(raw, 10), y: num(raw, 20) },
-        radius: num(raw, 40),
-        startDeg: num(raw, 50),
-        endDeg: num(raw, 51),
-      };
-    case 'ELLIPSE': {
-      // 11/21 是"相对圆心的长轴端点",40 是短长轴比,41/42 是起止参数(弧度,非角度)
-      const mx = num(raw, 11);
-      const my = num(raw, 21);
-      const rx = Math.hypot(mx, my);
-      const ratio = num(raw, 40, 1) || 1;
-      return {
-        kind: 'ellipse',
-        center: { x: num(raw, 10), y: num(raw, 20) },
-        rx,
-        ry: rx * Math.abs(ratio),
-        rotationDeg: Math.atan2(my, mx) * DEG,
-        startDeg: num(raw, 41, 0) * DEG,
-        endDeg: num(raw, 42, Math.PI * 2) * DEG,
-      };
-    }
-    case 'TEXT':
-    case 'MTEXT': {
-      // MTEXT 长文本会拆成 1/3 两个组码,这里拼接
-      const body = raw.codes
-        .filter((c) => c[0] === 1 || c[0] === 3)
-        .map((c) => c[1])
-        .join('');
-      return { kind: 'text', at: { x: num(raw, 10), y: num(raw, 20) }, height: num(raw, 40), text: body, rotationDeg: num(raw, 50) };
-    }
-    case 'POINT':
-      return { kind: 'point', at: { x: num(raw, 10), y: num(raw, 20) } };
-    case 'SOLID':
-    case '3DFACE': {
-      // 四个角点,按闭合折线画(预览足够)
-      const corners = [
-        { x: num(raw, 10), y: num(raw, 20) },
-        { x: num(raw, 11), y: num(raw, 21) },
-        { x: num(raw, 12), y: num(raw, 22) },
-        { x: num(raw, 13), y: num(raw, 23) },
-      ];
-      return { kind: 'polyline', points: corners, closed: true };
-    }
-    default:
-      return null;
-  }
-};
-
-const growBounds = (b: DxfBounds, x: number, y: number): void => {
-  if (!Number.isFinite(x) || !Number.isFinite(y)) return;
-  if (x < b.minX) b.minX = x;
-  if (y < b.minY) b.minY = y;
-  if (x > b.maxX) b.maxX = x;
-  if (y > b.maxY) b.maxY = y;
-};
-
-const entityBounds = (e: DxfEntity, b: DxfBounds): void => {
-  switch (e.kind) {
-    case 'line':
-      growBounds(b, e.a.x, e.a.y);
-      growBounds(b, e.b.x, e.b.y);
-      break;
-    case 'polyline':
-      e.points.forEach((p) => growBounds(b, p.x, p.y));
-      break;
-    case 'circle':
-    case 'arc':
-      growBounds(b, e.center.x - e.radius, e.center.y - e.radius);
-      growBounds(b, e.center.x + e.radius, e.center.y + e.radius);
-      break;
-    case 'ellipse': {
-      const r = Math.max(e.rx, e.ry);
-      growBounds(b, e.center.x - r, e.center.y - r);
-      growBounds(b, e.center.x + r, e.center.y + r);
-      break;
-    }
-    case 'text':
-      growBounds(b, e.at.x, e.at.y);
-      // 文字高度也算进范围,避免只有标注的图被"缩放到看不见"
-      growBounds(b, e.at.x + e.text.length * e.height * 0.6, e.at.y + e.height);
-      break;
-    case 'point':
-      growBounds(b, e.at.x, e.at.y);
-      break;
-  }
-};
-
-/**
- * 解析 DXF 文本。只读 ENTITIES 段;解析失败/无实体时返回空列表,不抛异常。
- */
-export const parseDxf = (dxfText: string): DxfParseResult => {
-  const entities: DxfEntity[] = [];
-  let skipped = 0;
-  const pairs = toPairs(dxfText);
-
-  let inEntities = false;
-  let raw: RawEntity | null = null;
-  // POLYLINE 的顶点来自后续 VERTEX 实体,需要跨实体累积
-  let polylineVertices: Array<{ x: number; y: number }> | null = null;
-
-  const flush = () => {
-    if (!raw) return;
-    const built = toEntity(raw, raw.type === 'POLYLINE' ? polylineVertices : null);
-    if (built) entities.push(built);
-    else skipped++;
-    raw = null;
-    polylineVertices = null;
-  };
-
-  for (let i = 0; i < pairs.length; i++) {
-    const [code, value] = pairs[i];
-    const token = value.trim();
-
-    if (code === 0) {
-      // 实体/段的边界
-      if (raw?.type === 'POLYLINE' && token === 'VERTEX') {
-        // 顶点属于父 POLYLINE:这里只收集它的组码,不 flush 父实体
-        const vertex: RawEntity = { type: 'VERTEX', codes: [] };
-        const vCodes: Array<[number, string]> = [];
-        let j = i + 1;
-        while (j < pairs.length && pairs[j][0] !== 0) {
-          vCodes.push(pairs[j]);
-          j++;
-        }
-        vertex.codes = vCodes;
-        const vx = num(vertex, 10);
-        const vy = num(vertex, 20);
-        if (Number.isFinite(vx) && Number.isFinite(vy)) polylineVertices!.push({ x: vx, y: vy });
-        i = j - 1;
-        continue;
-      }
-
-      flush();
-
-      if (token === 'SECTION') {
-        // 下一组是段名
-        const name = pairs[i + 1]?.[1]?.trim();
-        inEntities = name === 'ENTITIES';
-        if (pairs[i + 1]) i++;
-        continue;
-      }
-      if (token === 'ENDSEC') {
-        inEntities = false;
-        continue;
-      }
-      if (!inEntities) continue; // BLOCKS/HEADER 等段内的实体不参与预览
-      raw = { type: token, codes: [] };
-      if (token === 'POLYLINE') polylineVertices = [];
-      continue;
-    }
-
-    if (raw && inEntities) raw.codes.push([code, value]);
-  }
-  flush();
-
-  let bounds: DxfBounds | null = null;
-  if (entities.length) {
-    bounds = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
-    entities.forEach((e) => entityBounds(e, bounds!));
-    if (!Number.isFinite(bounds.minX) || bounds.minX > bounds.maxX) bounds = null;
-  }
-
-  return { entities, skipped, bounds };
-};

+ 2 - 2
src/views/ventAI/manageAssistent/lib/fileIcon.ts

@@ -18,8 +18,8 @@ export const isExcelFile = (filename: string): boolean => /\.(xls|xlsx)$/i.test(
 export const isCsvFile = (filename: string): boolean => /\.csv$/i.test(filename);
 
 // —— CAD ——
-// .dxf:明文交换格式,前端本地解析渲染(components/resource/CadPreview.vue);
-// .dwg:专有二进制,前端解析不了,预览时上传后端转成 SVG 再展示(components/resource/DwgPreview.vue);
+// .dxf / .dwg:前端都解析不了,预览时统一由后端转成 SVG 再展示
+//(components/resource/DwgPreview.vue,后端对两种格式用的是同一套接口、参数和返回);
 // .mxweb:依赖 mxcad 及其配套服务,暂不预览,由 fetchPreviewFile 返回 null 回退为下载。
 export const isDxfFile = (filename: string): boolean => /\.dxf$/i.test(filename);
 

+ 3 - 5
src/views/ventAI/manageAssistent/lib/filePreview.ts

@@ -1,7 +1,7 @@
 // lib/filePreview.ts:由 lib/utils.ts 按职责拆分(实现未改)
 import { getToken } from '/@/utils/auth/index';
 import type { AttachedFile } from '/@/views/ventAI/manageAssistent/types/resource';
-import { isImageFile, isPdfFile, isWordFile, isExcelFile, isDwgFile, getPreviewMimeType } from './fileIcon';
+import { isImageFile, isPdfFile, isWordFile, isExcelFile, isDwgFile, isDxfFile, getPreviewMimeType } from './fileIcon';
 
 export const fetchPreviewFile = async (url: string, filename: string): Promise<AttachedFile | null> => {
   const resp = await fetch(url, { headers: { 'X-Access-Token': getToken() } });
@@ -15,8 +15,6 @@ export const fetchPreviewFile = async (url: string, filename: string): Promise<A
     yaml: 'text/yaml',
     yml: 'text/yaml',
     csv: 'text/csv',
-    // .dxf 是明文,按文本取回后由预览面板本地解析渲染
-    dxf: 'text/plain',
   };
   const id = `report-${url}`;
   const uploadTime = new Date().toISOString();
@@ -25,9 +23,9 @@ export const fetchPreviewFile = async (url: string, filename: string): Promise<A
     const content = await resp.text();
     return { id, name: filename, size: content.length, type: mimeMap[ext], content, uploadTime };
   }
-  // 二进制文件都按字节取回:word/excel 由预览面板本地渲染,.dwg 是二进制图纸前端解析不了、
+  // 二进制文件都按字节取回:word/excel 由预览面板本地渲染;dwg/dxf 前端解析不了,
   // 要由预览面板上传后端转 SVG(components/resource/DwgPreview.vue),这里只负责把原始字节取回来。
-  if (isWordFile(filename) || isExcelFile(filename) || isDwgFile(filename)) {
+  if (isWordFile(filename) || isExcelFile(filename) || isDwgFile(filename) || isDxfFile(filename)) {
     const arrayBuffer = await resp.arrayBuffer();
     return { id, name: filename, size: arrayBuffer.byteLength, type: 'application/octet-stream', arrayBuffer, uploadTime };
   }

+ 0 - 1
src/views/ventAI/manageAssistent/lib/index.ts

@@ -7,7 +7,6 @@ export * from './theme';
 export * from './filePreview';
 export * from './messageKey';
 export * from './clipboard';
-export * from './dxf';
 export * from './userInput';
 export * from './turnRows';
 export * from './webLink';