Przeglądaj źródła

[Mod 0000]修改二维面板的子应用接入逻辑

wangkeyi 1 tydzień temu
rodzic
commit
8558f5c265

+ 22 - 7
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -204,7 +204,7 @@
   import type { AttachedFile, Message, ScheduleTask, SubAppRoute, Task } from '/@/views/ventAI/manageAssistent/types/index';
   import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
   import { parseModalPreviewLink, isFileDownloadLink, downloadLinkFile } from '/@/views/ventAI/manageAssistent/lib/index';
-  import { locateDevice, locateDeviceReport, locateTunnel } from '/@/views/ventAI/manageAssistent/lib/modalPreviewBridge';
+  import { locateDevice, locateDeviceReport, locateTunnel, onLocateResult } from '/@/views/ventAI/manageAssistent/lib/modalPreviewBridge';
   import { useChatStream } from '../hooks/useChatStream';
   import { useChatTasks } from '../hooks/useChatTasks';
   import { useChatPanels } from '../hooks/useChatPanels';
@@ -380,12 +380,12 @@
    * 同一模型下只做定位,不重载 iframe:
    * tunid → 巷道定位,locationId → 设备定位,locationName → 报表定位。
    */
-  const applyModelLocate = (route: SubAppRoute | null) => {
+  const applyModelLocate = (route: SubAppRoute | null): boolean => {
     const query = route?.query || {};
     const modelId = query.model_id || modelPreview.modelId;
-    if (!modelId) return;
+    if (!modelId) return false;
     const signature = [modelId, query.tunid || '', query.locationId || '', query.locationName || '', query.deviceType || ''].join('|');
-    if (signature === lastModelLocateSignature) return;
+    if (signature === lastModelLocateSignature) return false;
     lastModelLocateSignature = signature;
 
     const deviceType = query.deviceType || (query.tunid ? 'tun' : 'fanlocal');
@@ -396,6 +396,7 @@
     } else if (query.locationName) {
       locateDeviceReport({ model_id: modelId, deviceType, locationName: query.locationName });
     }
+    return true;
   };
 
   /**
@@ -410,7 +411,8 @@
 
     if (modelPreview.loaded && modelPreview.modelId === modelId) {
       modelPreview.ownerSessionId = currentSessionKey();
-      applyModelLocate(route);
+      // 同一个模型只做定位;定位参数也没变时界面不会有任何变化,明确提示一下
+      if (!applyModelLocate(route)) message.info('当前显示的已是该模型');
       return;
     }
 
@@ -425,7 +427,7 @@
 
         if (modelPreview.loaded && modelPreview.modelId === targetModelId) {
           modelPreview.ownerSessionId = currentSessionKey();
-          applyModelLocate(target);
+          if (!applyModelLocate(target)) message.info('当前显示的已是该模型');
           continue;
         }
 
@@ -468,10 +470,22 @@
   };
 
   const openModelPreview = (route: SubAppRoute, title?: string) => {
-    openModalPreviewTab({ title: title || '二维模型', subAppRoute: route });
+    const tab = openModalPreviewTab({ title: title || '二维模型', subAppRoute: route });
+    if (!tab) {
+      // 没有当前会话时开不了标签页,不提示的话点击就是“没反应”
+      message.warning('请先新建或选择一个会话,再打开模型预览');
+      return;
+    }
     void ensureModelPreview(route);
   };
 
+  // 子应用没能定位(例如链接里的设备在模型里找不到)时界面不会有任何变化,必须提示用户,
+  // 否则表现就是“点了没反应”
+  const offLocateResult = onLocateResult((result) => {
+    if (result.ok) return;
+    message.warning(result.message || '未能在模型中找到该位置,请检查链接参数');
+  });
+
   // 隐藏后开始 5 分钟空闲倒计时;重新显示立即取消
   watch(modelPreviewVisible, (visible) => {
     if (visible) clearModelPreviewIdleTimer();
@@ -1165,6 +1179,7 @@
   );
 
   onBeforeUnmount(() => {
+    offLocateResult();
     // 移除挂载空壳,避免残留
     portalWrapper.value?.remove();
     portalWrapper.value = null;

+ 12 - 2
src/views/ventAI/manageAssistent/components/resource/ModalPreview.vue

@@ -30,6 +30,7 @@
 
 <script setup lang="ts">
   import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
+  import { message as antMessage } from 'ant-design-vue';
   import { apps } from '/@/qiankun/apps';
   import MicroLoadError from '/@/components/vent/micro/MicroLoadError.vue';
   import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
@@ -37,6 +38,7 @@
   import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
   import { isDevMode } from '/@/utils/env';
   import type { SubAppRoute } from '/@/views/ventAI/manageAssistent/types/index';
+  import { getSubAppPort, resolveSubAppOrigin } from '/@/views/ventAI/manageAssistent/lib/subAppUrl';
   import {
     getLastLocateResult,
     isPreviewReady,
@@ -67,8 +69,13 @@
   // 子应用 ready 后由桥统一补发,切模型请求才不会 401→500。
   const userStore = useUserStore();
 
-  // 与 qiankun/apps.ts 共用同一份入口解析:dev 为 //localhost:8088,prod 为 //host/tun2D/
-  const entry = apps.find((item) => item.name === APP_NAME)?.entry as string | undefined;
+  // 二维子应用的访问基址:入口是绝对地址(开发环境配的 //host:port)就直接用;
+  // 生产上入口是主应用路径(/tun2D/),这时改为「当前访问的主机 + 配置的子应用端口」直连——
+  // 主应用路径下的深层地址没做服务器回退时,iframe 拿到的是主应用页面而不是子应用(见 lib/subAppUrl.ts)
+  const entry = resolveSubAppOrigin(
+    apps.find((item) => item.name === APP_NAME)?.entry as string,
+    getSubAppPort(import.meta.env.VITE_APP_SUB_APP, APP_NAME)
+  );
 
   const joinUrl = (base: string, path: string) => `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
 
@@ -113,6 +120,7 @@
       loading.value = false;
       loadFailed.value = true;
       logLoadError('MicroApp Load Timeout', `子应用 ${APP_NAME} 加载超时`, `iframe 在 ${LOAD_TIMEOUT_MS}ms 内未触发 load:${iframeSrc.value}`);
+      antMessage.error('二维模型加载超时,请稍后重试');
     }, LOAD_TIMEOUT_MS);
   };
 
@@ -137,6 +145,7 @@
     loading.value = false;
     loadFailed.value = true;
     logLoadError('MicroApp Load Error', `子应用 ${APP_NAME} 加载失败`, iframeSrc.value);
+    antMessage.error('二维模型加载失败,请检查子应用服务是否可访问');
   };
 
   // 子应用的实际来源:entry 形如 //localhost:8088 或 //host/tun2D/,按当前页面协议补全
@@ -247,6 +256,7 @@
       loading.value = false;
       loadFailed.value = true;
       logLoadError('MicroApp Not Configured', `子应用 ${APP_NAME} 未在 VITE_APP_SUB_APP 中配置`, 'apps.ts 未解析到该子应用的 entry');
+      antMessage.error('二维模型地址未配置,无法打开预览');
       return;
     }
     startLoadTimeout();

+ 23 - 3
src/views/ventAI/manageAssistent/lib/modalPreviewBridge.ts

@@ -64,6 +64,18 @@ let ready = false;
 let pending: BaseToPreviewMessage[] = [];
 let listening = false;
 let lastResult: LocateResult | null = null;
+// 定位回执的订阅方:子应用“没找到目标”时界面不会有任何变化,需要据此提示用户
+const resultListeners = new Set<(result: LocateResult) => void>();
+
+/**
+ * 订阅子应用的定位回执。
+ * @param listener 收到回执时调用(ok=false 表示子应用没能在模型里找到目标)
+ * @returns 取消订阅函数
+ */
+export const onLocateResult = (listener: (result: LocateResult) => void): (() => void) => {
+  resultListeners.add(listener);
+  return () => resultListeners.delete(listener);
+};
 
 const getTargetWindow = (): Window | null => {
   try {
@@ -118,11 +130,19 @@ const handleMessage = (event: MessageEvent) => {
     flush();
     return;
   }
-  // 定位执行回执:只在控制台与测试面板展示,不参与业务逻辑
+  // 定位执行回执:控制台与测试面板展示之外,也通知订阅方(定位失败时要提示用户)
   if (data.type === 'locate-result' && data.payload) {
-    lastResult = { ...data.payload, at: Date.now() };
+    const result: LocateResult = { ...data.payload, at: Date.now() };
+    lastResult = result;
     // eslint-disable-next-line no-console
-    console.info('[modalPreviewBridge] 子应用定位结果', lastResult);
+    console.info('[modalPreviewBridge] 子应用定位结果', result);
+    resultListeners.forEach((listener) => {
+      try {
+        listener(result);
+      } catch (err) {
+        console.error('[modalPreviewBridge] 定位回执订阅回调异常', err);
+      }
+    });
   }
 };
 

+ 40 - 0
src/views/ventAI/manageAssistent/lib/subAppUrl.ts

@@ -0,0 +1,40 @@
+// lib/subAppUrl.ts:以 iframe 打开子应用内部页面时的地址解析(只在 AI 助手模块内部使用)。
+//
+// 为什么需要单独处理:生产上二维子应用是「挂在主应用路径下」的(VITE_APP_SUB_APP 里配成 /tun2D/),
+// 深层地址(/tun2D/modal-preview)需要服务器做 SPA 回退;没配好回退的部署会把请求兜到主应用页面,
+// iframe 里看到的就成了主应用而不是子应用。子应用本身有独立端口(配置第三项,如 ":8099/"),
+// 直连端口可以绕开这个问题,所以这里按「当前访问的主机 + 配置的端口」拼地址。
+//
+// 公共的 qiankun 配置(src/qiankun/apps.ts)只保留入口信息、不在这里改动。
+
+/**
+ * 从 VITE_APP_SUB_APP 配置里取某个子应用的独立端口(配置第三项)。
+ * 配置形如 `[["micro-vent-2dModal", "/tun2D/", ":8099/"]]`,返回 `8099`。
+ * 第三项也常被写成容器名(如 `"micro-vent-2dModal"`),所以只认 `:8099/`、`:8099`、`8099` 这种端口写法,
+ * 避免把名字里的数字("2dModal" 的 2)当成端口;不符合时返回空串。
+ */
+export const getSubAppPort = (configRaw: string | undefined, name: string): string => {
+  try {
+    const list = JSON.parse((configRaw || '').replace(/'/g, '"'));
+    const item = Array.isArray(list) ? list.find((it: unknown[]) => Array.isArray(it) && it[0] === name) : null;
+    const raw = String((item && item[2]) || '').trim();
+    const matched = /^:?(\d{2,5})\/?$/.exec(raw);
+    return matched ? matched[1] : '';
+  } catch {
+    return '';
+  }
+};
+
+/**
+ * 解析子应用「可直连的访问基址」(不含末尾斜杠):
+ * - 入口是绝对地址(开发环境配的 `//host:8099`)→ 原样使用;
+ * - 入口是路径(生产环境配的 `/tun2D/`)且配了端口 → `//当前主机:端口`;
+ * - 都没有 → 退回原入口(同源挂载,依赖服务器回退)。
+ */
+export const resolveSubAppOrigin = (entry: string, port?: string, hostname: string = window.location.hostname): string => {
+  const raw = (entry || '').trim();
+  if (!raw) return '';
+  if (/^(https?:)?\/\//i.test(raw)) return raw.replace(/\/+$/, '');
+  if (port) return `//${hostname}:${port}`;
+  return raw.replace(/\/+$/, '');
+};