3 Angajamente c922957358 ... 8a13f5f9cc

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  houzekong 8a13f5f9cc [Style 0000] 修复APP logo样式、注氮泵气泡样式及默认主题回退问题 1 săptămână în urmă
  houzekong c913dbf96f [Fix 0000] 自动登录功能修复 1 săptămână în urmă
  houzekong a5a01b94ac [Feat 0000] 布尔台开发按设备灵活加减注氮机的模型 1 săptămână în urmă

+ 1 - 1
public/js/config.js

@@ -3,7 +3,7 @@ const VUE_APP_URL = {
   webRtcUrl: '/webRtc', // rtsp服务器IP地址
   webRtcUrl: '/webRtc', // rtsp服务器IP地址
   wsUrl: window.location.origin + '/modelreq', // [SEC-20260913] WebSocket 地址:同源经 /modelreq 反代至网关(覆盖旧云环境硬编码 IP)
   wsUrl: window.location.origin + '/modelreq', // [SEC-20260913] WebSocket 地址:同源经 /modelreq 反代至网关(覆盖旧云环境硬编码 IP)
   onlyOfficeUrl: '/documents', // [perf-base-v1] 卡1: OnlyOffice Document Server 反向代理前缀(api.js 按需加载时拼接)
   onlyOfficeUrl: '/documents', // [perf-base-v1] 卡1: OnlyOffice Document Server 反向代理前缀(api.js 按需加载时拼接)
-  autoLoginToken: '', // [SEC-20260913-R2] 跨系统自动登录票据,由部署环境注入(与服务端 SDJT_AUTO_LOGIN_TOKEN 配套);空 = 不携带该头
+  autoLoginToken: 'MTIzNDU2', // [SEC-20260913-R2] 跨系统自动登录票据,由部署环境注入(与服务端 SDJT_AUTO_LOGIN_TOKEN 配套);空 = 不携带该头
 }
 }
 const History_Type = {
 const History_Type = {
   // type: 'remote', // remote、vent  (remote 代表的是历史查询走的装备院的接口,vent是走的咱们的,目前神东的项目都用remote, 其他矿用vent)
   // type: 'remote', // remote、vent  (remote 代表的是历史查询走的装备院的接口,vent是走的咱们的,目前神东的项目都用remote, 其他矿用vent)

+ 5 - 0
src/components/Application/src/AppLogo.vue

@@ -83,11 +83,16 @@
       color: @white;
       color: @white;
     }
     }
 
 
+    &.vent1 &__title {
+      color: @white;
+    }
+
     &__title {
     &__title {
       font-size: 18px;
       font-size: 18px;
       font-weight: 700;
       font-weight: 700;
       transition: all 0.5s;
       transition: all 0.5s;
       line-height: normal;
       line-height: normal;
+      color: @white;
     }
     }
   }
   }
 </style>
 </style>

+ 7 - 1
src/layouts/default/header/components/user-dropdown/HomeStyleSelect.vue

@@ -40,6 +40,7 @@
   import { getPermissionList } from '/@/api/sys/menu';
   import { getPermissionList } from '/@/api/sys/menu';
   import { getSavedHomeStyle, saveHomeStyle } from '/@/utils/homeStyle';
   import { getSavedHomeStyle, saveHomeStyle } from '/@/utils/homeStyle';
   import type { HomeStyleOption } from '/@/utils/homeStyle';
   import type { HomeStyleOption } from '/@/utils/homeStyle';
+  import projectSetting from '/@/settings/projectSetting';
 
 
   const visible = ref(false);
   const visible = ref(false);
   const selectedId = ref<string>('');
   const selectedId = ref<string>('');
@@ -51,6 +52,10 @@
   const glob = useGlobSetting();
   const glob = useGlobSetting();
   const { setDarkMode } = useRootSetting();
   const { setDarkMode } = useRootSetting();
 
 
+  function getSystemDefaultTheme(): ThemeEnum {
+    return (projectSetting.headerSetting?.theme as ThemeEnum) || ThemeEnum.VENT1;
+  }
+
   /** 递归展平菜单树(兼容 children 嵌套) */
   /** 递归展平菜单树(兼容 children 嵌套) */
   function flattenMenu(nodes: any[], list: any[] = []): any[] {
   function flattenMenu(nodes: any[], list: any[] = []): any[] {
     nodes?.forEach((node) => {
     nodes?.forEach((node) => {
@@ -164,7 +169,8 @@
     const defaultOption = options.value.find((o) => o.route === glob.homePath);
     const defaultOption = options.value.find((o) => o.route === glob.homePath);
     saveHomeStyle(null);
     saveHomeStyle(null);
     selectedId.value = defaultOption?.id || '';
     selectedId.value = defaultOption?.id || '';
-    applyTheme(defaultOption ?? { theme: ThemeEnum.VENT1 });
+    // 系统默认首页未在可选列表中注册时,主题退回生效的项目配置,而不是写死 vent1
+    applyTheme(defaultOption || { theme: getSystemDefaultTheme() });
     createMessage.success('已恢复默认首页风格,正在应用系统默认首页…');
     createMessage.success('已恢复默认首页风格,正在应用系统默认首页…');
     visible.value = false;
     visible.value = false;
     go(glob.homePath || PageEnum.BASE_HOME);
     go(glob.homePath || PageEnum.BASE_HOME);

+ 1 - 1
src/views/vent/monitorManager/compressor/components/nitrogenHome_bet.vue

@@ -488,7 +488,7 @@
   watch([monitorDataGroupNum, loading], ([newMonitorDataGroupNum, newLoading]) => {
   watch([monitorDataGroupNum, loading], ([newMonitorDataGroupNum, newLoading]) => {
     nextTick(() => {
     nextTick(() => {
       if (newMonitorDataGroupNum && !newLoading) {
       if (newMonitorDataGroupNum && !newLoading) {
-        setModelType(props.modalType, newMonitorDataGroupNum);
+        setModelType('nitrogenFlex', newMonitorDataGroupNum);
       }
       }
     });
     });
   });
   });

+ 329 - 0
src/views/vent/monitorManager/compressor/nitrogen.flex.threejs.ts

@@ -0,0 +1,329 @@
+import * as THREE from 'three';
+import { CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
+import { setModalCenter } from '/@/utils/threejs/util';
+
+/**
+ * 注氮机地下管路模型(nitrogenFlex)
+ *
+ * 模型构成:
+ *   - 基底:nitrogen_base(管路 / 底座)
+ *   - 注氮机:nitrogen_ele_1 / nitrogen_ele_2 / nitrogen_ele_3
+ *
+ * ── 重要:三个注氮机模型是「预制好机位」的,必须原位(identity)摆放 ──
+ */
+class NitrogenFlex {
+  model;
+  /** 基底模型名 */
+  baseName = 'nitrogen_base';
+  /** 注氮机模型名,按机数取前 N 个(对应 右 / 中 / 左 机位) */
+  unitNames = ['nitrogen_ele_1', 'nitrogen_ele_2', 'nitrogen_ele_3'];
+  /** 总根节点 */
+  group: THREE.Object3D = new THREE.Object3D();
+  /** 已加载的注氮机模板 */
+  private unitTemplates: (THREE.Object3D | null)[] = [];
+  /** 当前场景中实际显示的注氮机节点 */
+  private unitGroups: THREE.Object3D[] = [];
+  /** 本模型显示的 css 详情元素数组 */
+  cssSprites: CSS3DSprite[] = [];
+  /** 当前机数 */
+  nitrogenNum = 0;
+  /** css 精灵世界缩放 */
+  private spriteScale = 0.0062;
+  /** 模型是否加载完成 */
+  private loaded = false;
+
+  /** 所有机体整体的垂直微调(默认 0 = 模型设计高度) */
+  private unitLiftY = 0;
+  /** 所有机体整体的深度微调(默认 0) */
+  private unitOffsetZ = 0;
+  /** 下风侧监测挂载位置:相对基底包围盒归一化(0=最小边,1=最大边) */
+  private downWindAnchor = { nx: 0.04, ny: 0.75, nz: 0.5 };
+
+  /**
+   * 相机取景:模型宽度占画面的比例(0.92 = 横向占满 92%)。
+   * 调大 = 相机更近 = 模型更大。
+   */
+  private cameraWidthFill = 0.92;
+  /** 相机距离下限(包围球拟合 × 该系数),防止超宽屏时贴太近被裁 */
+  private cameraMinFactor = 0.62;
+  /** 相机观察方向(从前上方俯视),会归一化后乘以上面的距离 */
+  private cameraDir = { x: 0.18, y: 0.85, z: 1.35 };
+  /** 机顶气泡再往上的高度 */
+  private spriteTopOffset = 0.34;
+
+  constructor(model) {
+    this.model = model;
+    this.group.name = 'nitrogenFlex';
+  }
+
+  addLight() {
+    // 灯光按场景尺度推导,避免小场景过曝、大场景欠曝
+    const box = new THREE.Box3().setFromObject(this.group);
+    const size = box.getSize(new THREE.Vector3());
+    const center = box.getCenter(new THREE.Vector3());
+    const maxDim = Math.max(size.x, size.y, size.z) || 1;
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
+    directionalLight.position.set(center.x + maxDim * 0.6, center.y + maxDim * 1.2, center.z + maxDim * 0.8);
+    directionalLight.target = this.group;
+    this.group.add(directionalLight);
+
+    const pointLight = new THREE.PointLight(0xffffff, 1, maxDim * 3);
+    pointLight.position.set(center.x, center.y + maxDim * 0.9, center.z + maxDim * 0.5);
+    this.group.add(pointLight);
+  }
+
+  /** 加载基底与三台注氮机模型 */
+  mountedThree() {
+    return new Promise((resolve) => {
+      const names = [this.baseName, ...this.unitNames];
+      this.model
+        .setGLTFModel(names)
+        .then((gltf) => {
+          const base = gltf[0];
+          if (base) {
+            base.name = this.baseName;
+            this.group.add(base);
+          } else {
+            console.warn(`[nitrogen-flex] 基底模型 ${this.baseName} 加载失败`);
+          }
+
+          this.unitTemplates = [];
+          for (let i = 0; i < this.unitNames.length; i++) {
+            const unit = gltf[i + 1];
+            if (!unit) {
+              console.warn(`[nitrogen-flex] 注氮机模型 ${this.unitNames[i]} 加载失败`);
+              this.unitTemplates.push(null);
+              continue;
+            }
+            unit.name = 'nitrogenUnitTpl' + i;
+            this.unitTemplates.push(unit);
+          }
+
+          this.loaded = true;
+          resolve(null);
+        })
+        .catch((e) => {
+          console.error('[nitrogen-flex] 模型加载异常', e);
+          this.loaded = true;
+          resolve(null);
+        });
+    });
+  }
+
+  /**
+   * 探测「机体」在模型内的位置。
+   *
+   * 每个 ele 模型除了机体,还含一条从机体延伸到管汇的长管路,
+   * 整体包围盒的中心会被管路拉偏,因此不能直接用整体包围盒做气泡锚点。
+   * 这里按尺寸特征找机体主箱体:X 宽 1.0~2.0 且 Y 高 > 0.4 的网格。
+   */
+  private computeMachineAnchor(unit: THREE.Object3D) {
+    unit.updateMatrixWorld(true);
+
+    const meshes: THREE.Mesh[] = [];
+    unit.traverse((o) => {
+      if ((o as THREE.Mesh).isMesh) meshes.push(o as THREE.Mesh);
+    });
+
+    let bestBox: THREE.Box3 | undefined;
+    let bestWidth = -1;
+    for (const mesh of meshes) {
+      const box = new THREE.Box3().setFromObject(mesh);
+      if (box.isEmpty()) continue;
+      const s = box.getSize(new THREE.Vector3());
+      if (s.x > 1.0 && s.x < 2.0 && s.y > 0.4 && s.x > bestWidth) {
+        bestBox = box;
+        bestWidth = s.x;
+      }
+    }
+
+    if (bestBox) {
+      const center = bestBox.getCenter(new THREE.Vector3());
+      return { x: center.x, y: bestBox.max.y, z: center.z };
+    }
+
+    // 回退:整体包围盒中心(仅在识别失败时使用)
+    const box = new THREE.Box3().setFromObject(unit);
+    const center = box.getCenter(new THREE.Vector3());
+    return { x: center.x, y: box.max.y, z: center.z };
+  }
+
+  /**
+   * 相机参数:按「包围球 + 相机 FOV + 画面宽高比」推导取景距离。
+   *
+   * 本场景宽而扁(约 8.58 × 1.12 × 1.78),横向才是真正的约束,
+   * 因此以「宽度刚好占满画面 WIDTH_FILL 比例」定距离;
+   * 再设一个包围球拟合的下限,避免超宽屏时相机贴得过近导致上下被裁。
+   *
+   * 相比写死坐标,这样在不同窗口尺寸下取景一致,不会屏幕一宽模型就变小。
+   */
+  getCameraConfig() {
+    const box = new THREE.Box3().setFromObject(this.group);
+    const sphere = box.getBoundingSphere(new THREE.Sphere());
+    const center = sphere.center;
+    const radius = sphere.radius || 1;
+    const size = box.getSize(new THREE.Vector3());
+
+    const cam = this.model?.camera;
+    const vFov = (((cam?.fov as number) ?? 50) * Math.PI) / 180;
+    const aspect = (cam?.aspect as number) || 1.8;
+    const hFov = 2 * Math.atan(Math.tan(vFov / 2) * aspect);
+
+    // 按宽度装下
+    const dByWidth = size.x / 2 / Math.tan(hFov / 2) / this.cameraWidthFill;
+    // 下限:包围球拟合再乘系数,保证纵向也不会被裁
+    const dFloor = (radius / Math.sin(Math.min(vFov, hFov) / 2)) * this.cameraMinFactor;
+    const distance = Math.max(dByWidth, dFloor);
+
+    const dir = new THREE.Vector3(this.cameraDir.x, this.cameraDir.y, this.cameraDir.z).normalize();
+    const pos = center.clone().add(dir.multiplyScalar(distance));
+
+    return {
+      camera: { x: pos.x, y: pos.y, z: pos.z },
+      target: { x: center.x, y: center.y, z: center.z },
+    };
+  }
+
+  /**
+   * 设置机数并重建机体。
+   * 模型为原位摆放:克隆后不做任何缩放/位移,机位与管路对接由模型自身保证。
+   */
+  setUnitCount(num: number) {
+    if (!this.loaded) return;
+    const count = Math.max(0, Math.min(this.unitNames.length, Number(num) || 0));
+    this.nitrogenNum = count;
+
+    this.clearCssText();
+    this.unitGroups.forEach((u) => this.group.remove(u));
+    this.unitGroups = [];
+
+    for (let i = 0; i < count; i++) {
+      const tpl = this.unitTemplates[i];
+      if (!tpl) {
+        console.warn(`[nitrogen-flex] 第 ${i + 1} 台注氮机模型缺失,跳过`);
+        continue;
+      }
+      const unit = tpl.clone(true);
+      unit.name = 'nitrogenUnit' + i;
+      // 原位摆放:保持模型内置机位,杜绝错位与管路脱开
+      unit.position.set(0, 0, 0);
+      unit.scale.set(1, 1, 1);
+      this.group.add(unit);
+      this.unitGroups.push(unit);
+    }
+
+    // 整体垂直/深度微调(默认 0,即模型设计位置)
+    if (this.unitLiftY || this.unitOffsetZ) {
+      this.unitGroups.forEach((u) => {
+        u.position.set(0, this.unitLiftY, this.unitOffsetZ);
+      });
+    }
+
+    setModalCenter(this.group);
+    this.addCssText();
+  }
+
+  /** 挂载数据气泡:三台机的监测框 + 下风侧监测框 */
+  addCssText = () => {
+    this.unitGroups.forEach((unit, i) => {
+      if (!unit) return;
+      if (unit.getObjectByName('monitorNitrogenText')) return;
+      const element = document.getElementById('nitrogenMonitor' + (i + 1)) as HTMLElement;
+      if (!element) return;
+
+      const anchor = this.computeMachineAnchor(unit);
+      const sprite = new CSS3DSprite(element);
+      sprite.name = 'monitorNitrogenText';
+      // CSS3DObject 构造会把元素设为 pointerEvents:'auto',会遮挡轨道控制器拖拽
+      element.style.pointerEvents = 'none';
+      sprite.scale.set(this.spriteScale, this.spriteScale, this.spriteScale);
+      // 气泡挂在机顶上方(机体位置由模型内部布局决定,故按探测结果定位)
+      sprite.position.set(anchor.x, anchor.y + this.spriteTopOffset, anchor.z);
+      unit.add(sprite);
+      this.cssSprites.push(sprite);
+    });
+
+    this.attachDownWindMonitor();
+  };
+
+  /**
+   * 挂载「下风侧监测」框。
+   *
+   * 若不显式挂载,该元素会以未定位状态留在 DOM 中,表现为跑到页面左上角。
+   * 这里按基底包围盒归一化锚点定位,默认放在管路的下风侧一端。
+   */
+  private attachDownWindMonitor() {
+    if (this.group.getObjectByName('downWindMonitor')) return;
+    const element = document.getElementById('downWindMonitor') as HTMLElement;
+    if (!element) return;
+
+    const base = this.group.getObjectByName(this.baseName);
+    if (!base) return;
+    const box = new THREE.Box3().setFromObject(base);
+    const { nx, ny, nz } = this.downWindAnchor;
+    const x = box.min.x + (box.max.x - box.min.x) * nx;
+    const y = box.min.y + (box.max.y - box.min.y) * ny;
+    const z = box.min.z + (box.max.z - box.min.z) * nz;
+
+    const sprite = new CSS3DSprite(element);
+    sprite.name = 'downWindMonitor';
+    element.style.pointerEvents = 'none';
+    sprite.scale.set(this.spriteScale, this.spriteScale, this.spriteScale);
+    sprite.position.set(x, y, z);
+    // 挂在 group 上而非某台机体,避免机体变换影响该框
+    this.group.add(sprite);
+    this.cssSprites.push(sprite);
+  }
+
+  clearCssText = () => {
+    this.cssSprites.forEach((s) => {
+      s.parent?.remove(s);
+    });
+    this.cssSprites = [];
+    // 兜底清理可能残留的精灵节点
+    this.unitGroups.forEach((u) => {
+      const old = u.getObjectByName('monitorNitrogenText');
+      if (old) u.remove(old);
+    });
+    const dw = this.group.getObjectByName('downWindMonitor');
+    if (dw) this.group.remove(dw);
+  };
+
+  /** 与既有类保持一致的接口(本模型无需额外处理) */
+  setModalPosition() {}
+
+  resetModel() {}
+
+  /**
+   * 布局/取景微调入口(默认值即当前效果,通常无需调整)。
+   * 仅在实物效果需要微调时使用,避免写死进结构里。
+   */
+  setLayoutTuning(opts: {
+    unitLiftY?: number;
+    unitOffsetZ?: number;
+    downWindAnchor?: { nx: number; ny: number; nz: number };
+    cameraWidthFill?: number;
+    spriteTopOffset?: number;
+  }) {
+    if (opts.unitLiftY !== undefined) this.unitLiftY = opts.unitLiftY;
+    if (opts.unitOffsetZ !== undefined) this.unitOffsetZ = opts.unitOffsetZ;
+    if (opts.downWindAnchor !== undefined) this.downWindAnchor = opts.downWindAnchor;
+    // 取景与气泡高度是纯渲染参数,改完直接重排即可生效
+    if (opts.cameraWidthFill !== undefined) this.cameraWidthFill = opts.cameraWidthFill;
+    if (opts.spriteTopOffset !== undefined) this.spriteTopOffset = opts.spriteTopOffset;
+    this.setUnitCount(this.nitrogenNum);
+  }
+
+  destroy() {
+    if (this.model) {
+      this.clearCssText();
+      this.model.clearGroup(this.group);
+      this.unitTemplates = [];
+      this.unitGroups = [];
+      this.model = null;
+    }
+  }
+}
+
+export default NitrogenFlex;

+ 33 - 0
src/views/vent/monitorManager/compressor/nitrogen.threejs.ts

@@ -3,6 +3,7 @@ import { animateCamera, setModalCenter, updateAxisCenter } from '/@/utils/threej
 import UseThree from '../../../../utils/threejs/useThree';
 import UseThree from '../../../../utils/threejs/useThree';
 import NitrogenOverground from './nitrogen.dishang.threejs';
 import NitrogenOverground from './nitrogen.dishang.threejs';
 import NitrogenUnderground from './nitrogen.dixia.threejs';
 import NitrogenUnderground from './nitrogen.dixia.threejs';
+import NitrogenFlex from './nitrogen.flex.threejs';
 import useEvent from '../../../../utils/threejs/useEvent';
 import useEvent from '../../../../utils/threejs/useEvent';
 // import * as dat from 'dat.gui';
 // import * as dat from 'dat.gui';
 // const gui = new dat.GUI();
 // const gui = new dat.GUI();
@@ -13,6 +14,7 @@ let model: UseThree | undefined, //
   group: THREE.Object3D | undefined,
   group: THREE.Object3D | undefined,
   nitrogenObj: NitrogenOverground | undefined,
   nitrogenObj: NitrogenOverground | undefined,
   nitrogenUndergroundObj: NitrogenUnderground | undefined,
   nitrogenUndergroundObj: NitrogenUnderground | undefined,
+  nitrogenFlexObj: NitrogenFlex | undefined,
   modalType = 'nitrogen';
   modalType = 'nitrogen';
 
 
 const { mouseDownFn } = useEvent();
 const { mouseDownFn } = useEvent();
@@ -127,6 +129,30 @@ export const setModelType = (type, nitrogenNum?) => {
         resolve(null);
         resolve(null);
         await animateCamera(oldCameraPosition, oldControlsPosition, newCameraPosition, newControlsPosition, model, 0.8);
         await animateCamera(oldCameraPosition, oldControlsPosition, newCameraPosition, newControlsPosition, model, 0.8);
       }, 300);
       }, 300);
+    } else if (modalType === 'nitrogenFlex' && nitrogenFlexObj && nitrogenFlexObj.group) {
+      // 地下管路模型(基底 + 注氮机),与地上模型、地下管网模型三者共存,互不影响
+      if (model?.scene?.getObjectByName('nitrogen') && nitrogenObj && nitrogenObj.group) {
+        model.scene.remove(nitrogenObj.group);
+        nitrogenObj.clearCssText();
+      }
+      if (model?.scene?.getObjectByName('nitrogenUnderground') && nitrogenUndergroundObj && nitrogenUndergroundObj.group) {
+        model.scene.remove(nitrogenUndergroundObj.group);
+        nitrogenUndergroundObj.clearCssText();
+      }
+
+      // 机数由后端决定(1~3 台),动态增减
+      nitrogenFlexObj.setUnitCount(nitrogenNum ?? 3);
+
+      group = nitrogenFlexObj.group;
+      const oldCameraPosition = { x: -1000, y: 100, z: 500 };
+      const oldControlsPosition = { x: -10, y: 10, z: 10 };
+      const { camera, target } = nitrogenFlexObj.getCameraConfig();
+
+      setTimeout(async () => {
+        model?.scene?.add(group as THREE.Object3D);
+        resolve(null);
+        await animateCamera(oldCameraPosition, oldControlsPosition, camera, target, model, 0.8);
+      }, 300);
     }
     }
   });
   });
 };
 };
@@ -143,6 +169,11 @@ export const mountedThree = () => {
 
 
     nitrogenUndergroundObj = new NitrogenUnderground(model);
     nitrogenUndergroundObj = new NitrogenUnderground(model);
     await nitrogenUndergroundObj.mountedThree();
     await nitrogenUndergroundObj.mountedThree();
+
+    // 地下管路模型(基底 + 注氮机),作为新增视图与上面两个共存
+    nitrogenFlexObj = new NitrogenFlex(model);
+    await nitrogenFlexObj.mountedThree();
+
     setModelType('nitrogenUnderground');
     setModelType('nitrogenUnderground');
     resolve(null);
     resolve(null);
     model.animate();
     model.animate();
@@ -157,6 +188,8 @@ export const destroy = () => {
     nitrogenObj = undefined;
     nitrogenObj = undefined;
     nitrogenUndergroundObj?.destroy();
     nitrogenUndergroundObj?.destroy();
     nitrogenUndergroundObj = undefined;
     nitrogenUndergroundObj = undefined;
+    nitrogenFlexObj?.destroy();
+    nitrogenFlexObj = undefined;
     group = undefined;
     group = undefined;
     model.destroy();
     model.destroy();
     model = undefined;
     model = undefined;