Jelajahi Sumber

[Feat 0000] 新增皮带监测数据弹框

hongrunxia 1 bulan lalu
induk
melakukan
e3c544bf50

TEMPAT SAMPAH
public/model/hdr/studio_small_09_1k.hdr


TEMPAT SAMPAH
public/model/img/4-1.png


TEMPAT SAMPAH
public/model/img/4-2.png


TEMPAT SAMPAH
public/model/img/5-1.png


TEMPAT SAMPAH
public/model/img/5-2.png


+ 23 - 1
src/utils/threejs/useEvent.ts

@@ -9,7 +9,6 @@ export default function useEvent() {
 
   let startTime: number = 0;
   const mouseDownFn = (modal: UseThree, group: THREE.Object3D | THREE.Object3D[], event: MouseEvent, callBack?: Function) => {
-    debugger;
     event.stopPropagation();
     event.preventDefault();
 
@@ -168,3 +167,26 @@ export default function useEvent() {
 
   return { mouseDownFn, mouseUpFn, mousemoveFn };
 }
+
+export function modelMouseHandler(modal: UseThree, group: THREE.Object3D | THREE.Object3D[], event: MouseEvent, callBack?: Function) {
+  if (!modal || !modal.canvasContainer || !modal.orbitControls) return;
+  const appStore = useAppStore();
+
+  const widthScale = appStore.getWidthScale;
+  const heightScale = appStore.getHeightScale;
+  // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
+  modal.mouse.x =
+    ((-modal.canvasContainer.getBoundingClientRect().left * widthScale + event.clientX) / (modal.canvasContainer.clientWidth * widthScale)) * 2 - 1;
+  modal.mouse.y =
+    -((-modal.canvasContainer.getBoundingClientRect().top + event.clientY) / (modal.canvasContainer.clientHeight * heightScale)) * 2 + 1;
+  (modal.rayCaster as THREE.Raycaster).setFromCamera(modal.mouse, modal.camera as THREE.Camera);
+  if (group) {
+    let intersects = <THREE.Intersection[]>[];
+    if (Object.prototype.toString.call(group) === '[object Array]') {
+      intersects = modal.rayCaster?.intersectObjects([...(group as THREE.Object3D[])], true) as THREE.Intersection[];
+    } else {
+      intersects = modal.rayCaster?.intersectObjects((group as THREE.Object3D).children, true) as THREE.Intersection[];
+    }
+    if (callBack) callBack(intersects);
+  }
+}

+ 378 - 0
src/views/vent/home/configurable/belt/threejs/MonitorPanel.vue

@@ -0,0 +1,378 @@
+<!-- MonitorPanel.vue 完整版(支持销毁) -->
+<template>
+  <!-- 外层动画包裹层 -->
+  <div class="panel-wrapper">
+    <div class="monitor-panel" ref="panelRef" :class="{ alarm: hasAlarm }">
+      <div class="panel-title">{{ sensorData.positionName || 'XXXXX位置' }}</div>
+      <div class="panel-grid">
+        <div class="panel-item">
+          <span class="item-label">▸ 微震测声</span>
+          <span class="item-value" :class="getStatusClass('microseism')">
+            {{ sensorData.microseism ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ 光纤测温</span>
+          <span class="item-value" :class="getStatusClass('fiberTemp')">
+            {{ sensorData.fiberTemp ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ 温度</span>
+          <span class="item-value" :class="getStatusClass('temperature')">
+            {{ sensorData.temperature ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ 火焰</span>
+          <span class="item-value" :class="getStatusClass('flame')">
+            {{ sensorData.flame ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item item-full">
+          <span class="item-label ai-label">▸ AI视频火焰识别</span>
+          <span class="item-value" :class="getStatusClass('aiFlame')">
+            {{ sensorData.aiFlame ?? 'XXXX' }}
+          </span>
+        </div>
+
+        <div class="panel-item">
+          <span class="item-label">▸ HCl</span>
+          <span class="item-value" :class="getStatusClass('hcl')">
+            {{ sensorData.hcl ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ CO</span>
+          <span class="item-value" :class="getStatusClass('co')">
+            {{ sensorData.co ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ 烟雾</span>
+          <span class="item-value" :class="getStatusClass('smoke')">
+            {{ sensorData.smoke ?? 'XXXX' }}
+          </span>
+        </div>
+        <div class="panel-item">
+          <span class="item-label">▸ 多参数</span>
+          <span class="item-value" :class="getStatusClass('multiParam')">
+            {{ sensorData.multiParam ?? 'XXXX' }}
+          </span>
+        </div>
+      </div>
+      <div class="panel-arrow"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, computed, onUnmounted } from 'vue';
+  import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
+  import type { Scene, Camera } from 'three';
+
+  export interface AlarmThreshold {
+    microseism?: number;
+    hcl?: number;
+    fiberTemp?: number;
+    co?: number;
+    temperature?: number;
+    smoke?: number;
+    flame?: number;
+    multiParam?: number;
+    aiFlame?: number;
+  }
+
+  export interface SensorData {
+    positionName?: string;
+    microseism?: number;
+    hcl?: number;
+    fiberTemp?: number;
+    co?: number;
+    temperature?: number;
+    smoke?: number;
+    flame?: number;
+    multiParam?: number;
+    aiFlame?: number;
+  }
+
+  const props = defineProps({
+    sensorData: {
+      type: Object as () => SensorData,
+      default: () => ({}),
+    },
+    threshold: {
+      type: Object as () => AlarmThreshold,
+      default: () => ({}),
+    },
+    position: {
+      type: Array as () => [number, number, number],
+      default: () => [0, 5, -10],
+    },
+    scale: {
+      type: Number,
+      default: 0.5,
+    },
+    threeScene: {
+      type: Object as () => Scene,
+      default: null,
+    },
+    instanceId: {
+      type: String,
+      default: '',
+    },
+  });
+
+  const panelRef = ref<HTMLElement | null>(null);
+  const cssObject = ref<CSS3DObject | null>(null);
+
+  // 默认阈值
+  const defaultThreshold: Required<AlarmThreshold> = {
+    microseism: 100,
+    hcl: 100,
+    fiberTemp: 100,
+    co: 10,
+    temperature: 50,
+    smoke: 30,
+    flame: 1,
+    multiParam: 100,
+    aiFlame: 1,
+  };
+
+  // 合并阈值
+  const currentThreshold = computed(() => ({
+    ...defaultThreshold,
+    ...props.threshold,
+  }));
+
+  // 判断状态:normal/warn/alarm
+  const getStatusClass = (key: keyof AlarmThreshold) => {
+    const value = props.sensorData[key];
+    if (value == null) return 'normal';
+    const threshold = currentThreshold.value[key]!;
+    if (value > threshold) return 'alarm';
+    return 'normal';
+  };
+
+  // 全局报警状态(任意项异常则面板变红)
+  const hasAlarm = computed(() => {
+    return Object.keys(currentThreshold.value).some((key) => {
+      const value = props.sensorData[key as keyof AlarmThreshold];
+      return value != null && value > currentThreshold.value[key as keyof AlarmThreshold]!;
+    });
+  });
+
+  // 面朝相机更新
+  const update = (camera: Camera) => {
+    if (cssObject.value) {
+      cssObject.value.lookAt(camera.position);
+      // 修复翻转
+      cssObject.value.rotation.z = Math.PI;
+      cssObject.value.rotation.x = Math.PI;
+    }
+  };
+
+  onMounted(() => {
+    if (!panelRef.value) return;
+    cssObject.value = new CSS3DObject(panelRef.value);
+    cssObject.value.position.set(props.position[0], props.position[1] + 2, props.position[2] + 3.4);
+    cssObject.value.rotation.z += Math.PI;
+    cssObject.value.rotation.x += Math.PI;
+    cssObject.value.scale.setScalar(props.scale);
+    cssObject.value.lookAt(props.position[0], props.position[1] + 30, props.position[2] - 10);
+    if (props.threeScene) {
+      props.threeScene.add(cssObject.value);
+    }
+  });
+
+  // 销毁方法
+  const destroy = () => {
+    if (cssObject.value && props.threeScene) {
+      props.threeScene.remove(cssObject.value);
+      (cssObject.value as any).element = null;
+      cssObject.value = null;
+    }
+  };
+
+  onUnmounted(() => {
+    destroy();
+  });
+
+  // 暴露方法
+  defineExpose({
+    id: props.instanceId,
+    cssObject: cssObject,
+    update,
+    destroy,
+  });
+</script>
+
+<style scoped>
+  /* ====================================== */
+  /* 🔥 核心:从中间向两边扩散展开动画 */
+  /* ====================================== */
+  .panel-wrapper {
+    animation: panelExpand 1.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+  }
+
+  /* 扩散动画:中间 → 左右展开 */
+  @keyframes panelExpand {
+    0% {
+      opacity: 0;
+      transform: scaleX(0); /* 中间闭合 */
+    }
+    100% {
+      opacity: 1;
+      transform: scaleX(1); /* 展开到完整宽度 */
+    }
+  }
+
+  /* ====================================== */
+  /* 主面板(科技风 + 状态颜色) */
+  /* ====================================== */
+  .monitor-panel {
+    width: 760px;
+    background: linear-gradient(145deg, rgba(10, 30, 60, 0.95), rgba(15, 40, 80, 0.9));
+    border: 5px solid #40a9ff;
+    border-radius: 16px;
+    padding: 28px 24px 40px;
+    box-sizing: border-box;
+    position: absolute;
+    font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
+    color: #fff;
+    box-shadow:
+      0 12px 40px rgba(64, 169, 255, 0.25),
+      inset 0 0 0 1px rgba(64, 169, 255, 0.15);
+    backdrop-filter: blur(12px);
+    transition: all 0.4s ease;
+    overflow: visible;
+    transform-origin: center center; /* 扩散中心点:中间 */
+  }
+
+  /* 报警状态整体变红 */
+  .monitor-panel.alarm {
+    border-color: #ff4d4f;
+    box-shadow:
+      0 12px 40px rgba(255, 77, 79, 0.35),
+      inset 0 0 0 1px rgba(255, 77, 79, 0.2);
+    animation: panelPulse 1.5s infinite alternate;
+  }
+
+  @keyframes panelPulse {
+    0% {
+      box-shadow:
+        0 12px 40px rgba(255, 77, 79, 0.35),
+        inset 0 0 0 1px rgba(255, 77, 79, 0.2);
+    }
+    100% {
+      box-shadow:
+        0 12px 50px rgba(255, 77, 79, 0.5),
+        inset 0 0 0 1px rgba(255, 77, 79, 0.3);
+    }
+  }
+
+  /* 标题 */
+  .panel-title {
+    text-align: center;
+    font-size: 40px;
+    font-weight: 700;
+    margin-bottom: 24px;
+    letter-spacing: 4px;
+    background: linear-gradient(90deg, #40a9ff, #69c0ff);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    text-shadow: 0 0 16px rgba(64, 169, 255, 0.4);
+  }
+
+  /* 网格布局 */
+  .panel-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 16px;
+  }
+
+  .item-full {
+    grid-column: 1 / -1;
+  }
+
+  /* 监控项 */
+  .panel-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: rgba(30, 70, 120, 0.4);
+    border: 3px solid rgba(64, 169, 255, 0.3);
+    padding: 16px 20px;
+    border-radius: 12px;
+    font-size: 25px;
+    transition: all 0.3s ease;
+    cursor: default;
+  }
+
+  .panel-item:hover {
+    background: rgba(40, 90, 150, 0.5);
+    border-color: rgba(64, 169, 255, 0.6);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(64, 169, 255, 0.2);
+  }
+
+  .item-label {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-weight: 500;
+    color: #e6f3ff;
+  }
+
+  .ai-label {
+    color: #36cffb;
+    text-shadow: 0 0 8px rgba(54, 207, 251, 0.3);
+  }
+
+  /* 数值正常颜色 */
+  .item-value {
+    font-weight: 700;
+    font-size: 27px;
+    color: #40a9ff;
+    letter-spacing: 2px;
+    transition: all 0.3s ease;
+  }
+
+  /* 报警闪烁 */
+  .item-value.alarm {
+    color: #ff4d4f !important;
+    text-shadow: 0 0 12px rgba(255, 77, 79, 0.6);
+    animation: valueBlink 0.8s infinite alternate;
+  }
+
+  @keyframes valueBlink {
+    0% {
+      opacity: 0.6;
+      transform: scale(1);
+    }
+    100% {
+      opacity: 1;
+      transform: scale(1.05);
+    }
+  }
+
+  /* 底部定位箭头 */
+  .panel-arrow {
+    position: absolute;
+    bottom: -18px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 0;
+    height: 0;
+    border-left: 18px solid transparent;
+    border-right: 18px solid transparent;
+    border-top: 18px solid #40a9ff;
+    filter: drop-shadow(0 4px 8px rgba(64, 169, 255, 0.3));
+    transition: all 0.4s ease;
+  }
+
+  .monitor-panel.alarm .panel-arrow {
+    border-top-color: #ff4d4f;
+    filter: drop-shadow(0 4px 8px rgba(255, 77, 79, 0.4));
+  }
+</style>

+ 98 - 0
src/views/vent/home/configurable/belt/threejs/PanelManager.ts

@@ -0,0 +1,98 @@
+// PanelManager.ts
+import { createApp, App } from 'vue';
+import { Scene } from 'three';
+import MonitorPanel, { SensorData, AlarmThreshold } from './MonitorPanel.vue';
+
+// 面板实例结构
+export interface PanelInstance {
+  id: string;
+  app: App;
+  vm: any; // Vue 实例
+  threeScene: Scene;
+}
+
+class PanelManager {
+  private panels: Map<string, PanelInstance> = new Map();
+
+  /**
+   * 创建并添加一个监控面板到 Three.js 场景
+   */
+  createPanel(
+    threeScene: Scene,
+    options: {
+      instanceId: string;
+      sensorData: SensorData;
+      threshold?: AlarmThreshold;
+      position?: [number, number, number];
+      scale?: number;
+    }
+  ): PanelInstance {
+    const root = document.createElement('div');
+    // 关键:给容器加 ID(方便调试)
+    const panelId = options.instanceId;
+    root.id = panelId;
+    document.body.appendChild(root);
+
+    // 创建 Vue 应用
+    const app = createApp(MonitorPanel, {
+      ...options,
+      threeScene, // 传入场景,方便组件内部销毁
+    });
+
+    const vm = app.mount(root);
+
+    const instance: PanelInstance = {
+      id: vm.id, // 来自组件内 expose 的 id
+      app,
+      vm,
+      threeScene,
+    };
+
+    this.panels.set(vm.id, instance);
+    return instance;
+  }
+
+  /**
+   * 根据 ID 获取面板实例
+   */
+  getPanel(id: string): PanelInstance | undefined {
+    return this.panels.get(id);
+  }
+
+  /**
+   * 销毁指定 ID 的面板
+   */
+  destroyPanel(id: string): boolean {
+    const instance = this.panels.get(id);
+    if (!instance) return false;
+
+    // 1. 调用组件内部销毁(清理 Three.js)
+    instance.vm.destroy?.();
+    // 2. 卸载 Vue 实例
+    instance.app.unmount();
+    // 3. 移除 DOM
+    const el = document.getElementById(id);
+    if (el) el.remove();
+
+    // 4. 从管理器删除
+    this.panels.delete(id);
+    return true;
+  }
+
+  /**
+   * 销毁所有面板
+   */
+  destroyAll(): void {
+    this.panels.forEach((_, id) => this.destroyPanel(id));
+  }
+
+  /**
+   * 获取所有面板 ID 列表
+   */
+  getAllPanelIds(): string[] {
+    return Array.from(this.panels.keys());
+  }
+}
+
+// 单例导出
+export const panelManager = new PanelManager();

File diff ditekan karena terlalu besar
+ 713 - 0
src/views/vent/home/configurable/belt/threejs/belt.threejs.ts


+ 3 - 4
src/views/vent/home/configurable/components/three3D.vue

@@ -8,7 +8,6 @@
   import UseThree from '/@/utils/threejs/useThree';
   import { animateCamera } from '/@/utils/threejs/util';
   import * as THREE from 'three';
-  import { e } from 'unocss';
 
   const props = defineProps<{
     modalName: string;
@@ -56,8 +55,8 @@
         pidai: {
           render: null,
           group: modalGroup ? modalGroup : null,
-          newP: { x: -0.2434227102934745, y: 33.049017194608794, z: -3.7074883293849323 },
-          newT: { x: -0.24342271027624807, y: -8.62992463364641, z: -3.7074466439636744 },
+          newP: { x: -0.8292668304847632, y: 27.328559112319077, z: -13.883314493708111 },
+          newT: { x: -0.7961264623845425, y: -8.037007172591878, z: -1.646425804633182 },
         },
       };
       await initModal();
@@ -90,7 +89,7 @@
         addLight();
         customModal();
         modal.animate();
-        addMouseEvent();
+        // addMouseEvent();
         debugger;
         // this.group.name = this.modelName;
         // setModalCenter(this.group);

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini