Переглянути джерело

1. 新增防火门详情页面
2. 新增没有分站的风门模型监测页面

hongrunxia 7 місяців тому
батько
коміт
dad9b6e3fa

+ 2 - 2
src/layouts/default/header/components/user-dropdown/index.vue

@@ -122,7 +122,7 @@
       // 清除缓存
       async function clearCache() {
         const result = await refreshCache();
-        const refreshRedisResult = await refreshRedis()
+        const refreshRedisResult = await refreshRedis();
         if (result.success && refreshRedisResult) {
           const res = await queryAllDictItems();
           removeAuthCache(DB_DICT_DATA_KEY);
@@ -193,7 +193,7 @@
 <style lang="less">
   @prefix-cls: ~'@{namespace}-header-user-dropdown';
   @ventSpace: zxm;
-  
+
   .@{prefix-cls} {
     //height: @header-height;
     height: 48px;

+ 2 - 0
src/layouts/default/header/index.less

@@ -147,6 +147,8 @@
     .right-position {
       position: absolute;
       right: 0;
+      display: flex;
+      align-items: center;
     }
 
     &__item {

+ 6 - 0
src/layouts/default/header/index.vue

@@ -46,6 +46,7 @@
     <!-- action  -->
     <div :class="`${prefixCls}-action`">
       <div class="right-position">
+        <VoiceBroadcast />
         <UserDropDown v-if="showUserDropdown" :theme="getHeaderTheme" />
       </div>
     </div>
@@ -56,9 +57,11 @@
     style="position: fixed; top: 30px; right: 20px; z-index: 999"
   >
     <div class="right-position">
+      <VoiceBroadcast />
       <UserDropDown v-if="showUserDropdown" :theme="getHeaderTheme" />
     </div>
   </div>
+
   <LoginSelect ref="loginSelectRef" @success="loginSelectOk" />
 </template>
 <script lang="ts">
@@ -88,6 +91,8 @@
   import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
   import { useLocale } from '/@/locales/useLocale';
 
+  import VoiceBroadcast from './components/VoiceBroadcast.vue';
+
   import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
   import { useUserStore } from '/@/store/modules/user';
   import { useRouter } from 'vue-router';
@@ -110,6 +115,7 @@
       ErrorAction,
       LockScreen,
       LoginSelect,
+      VoiceBroadcast,
       SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
         loading: true,
       }),

+ 32 - 0
src/views/vent/monitorManager/fireDoorMonitor/detail.vue

@@ -0,0 +1,32 @@
+<template>
+  <div style="width: 100%; height: calc(100vh - 200px); display: flex; justify-content: center; align-items: center">
+    <a-spin :spinning="loading" />
+    <div id="fengmen3D" v-show="!loading"> </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, onUnmounted } from 'vue';
+  import UseThree from '../../../../utils/threejs/useThree';
+
+  const loading = ref(false);
+  let model;
+
+  onMounted(() => {
+    model = new UseThree('#fengmen3D');
+    // model.setEnvMap('test');
+    //   model.setCustomMaterial = setCustomMaterial
+    loading.value = true;
+    model.setGLTFModel('9f-processed').then(() => {
+      // 模型加载成功
+      loading.value = false;
+    });
+  });
+
+  onUnmounted(() => {
+    if (model) {
+      model.destroy();
+    }
+  });
+</script>
+<style scoped lang="less"></style>

+ 68 - 0
src/views/vent/monitorManager/fireDoorMonitor/fireDoor.api.ts

@@ -0,0 +1,68 @@
+import { defHttp } from '/@/utils/http/axios';
+import { Modal } from 'ant-design-vue';
+
+enum Api {
+  list = '/ventanaly-device/monitor/device',
+  save = '/safety/ventanalyGate/add',
+  edit = '/safety/ventanalyGate/edit',
+  deleteById = '/safety/ventanalyGate/delete',
+  deleteBatch = '/sys/user/deleteBatch',
+  importExcel = '/sys/user/importExcel',
+  exportXls = '/sys/user/exportXls',
+  baseList = '/safety/ventanalyGate/list',
+  cameraList = '/safety/ventanalyCamera/list',
+  cameraAddrList = '/ventanaly-device/camera/info',
+}
+/**
+ * 导出api
+ * @param params
+ */
+export const getExportUrl = Api.exportXls;
+/**
+ * 导入api
+ */
+export const getImportUrl = Api.importExcel;
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.post({ url: Api.list, params });
+
+export const cameraAddrList = (params) => defHttp.post({ url: Api.cameraAddrList, params });
+
+export const cameraList = (params) => defHttp.get({ url: Api.cameraList, params });
+/**
+ * 删除用户
+ */
+export const deleteById = (params, handleSuccess) => {
+  return defHttp.delete({ url: Api.deleteById, params }, { joinParamsToUrl: true }).then(() => {
+    handleSuccess();
+  });
+};
+/**
+ * 批量删除用户
+ * @param params
+ */
+export const batchDeleteById = (params, handleSuccess) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: '是否删除选中数据',
+    okText: '确认',
+    cancelText: '取消',
+    onOk: () => {
+      return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
+        handleSuccess();
+      });
+    },
+  });
+};
+/**
+ * 保存或者更新用户
+ * @param params
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  const url = isUpdate ? Api.edit : Api.save;
+  return defHttp.put({ url: url, params });
+};
+
+export const getTableList = (params) => defHttp.get({ url: Api.baseList, params });

+ 304 - 0
src/views/vent/monitorManager/fireDoorMonitor/fireDoor.data.ts

@@ -0,0 +1,304 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { rules } from '/@/utils/helper/validator';
+export const columns: BasicColumn[] = [
+  {
+    title: '名称',
+    dataIndex: 'strname',
+    width: 120,
+  },
+  {
+    title: '安装位置',
+    dataIndex: 'strinstallpos',
+    width: 100,
+  },
+  {
+    title: '是否为常闭型',
+    dataIndex: 'bnormalclose',
+    width: 100,
+    // customRender: render.renderAvatar,
+  },
+  {
+    title: '净宽',
+    dataIndex: 'fclearwidth',
+    width: 80,
+  },
+  {
+    title: '净高',
+    dataIndex: 'fclearheight',
+    width: 100,
+  },
+  {
+    title: '风门道数',
+    dataIndex: 'ndoorcount',
+    width: 100,
+  },
+  {
+    title: '所属分站',
+    width: 150,
+    dataIndex: 'stationname',
+  },
+  {
+    title: '点表',
+    width: 100,
+    dataIndex: 'strtype',
+  },
+  {
+    title: '监测类型',
+    dataIndex: 'monitorflag',
+    width: 100,
+  },
+  {
+    title: '是否模拟数据',
+    dataIndex: 'testflag',
+    width: 100,
+  },
+];
+
+export const recycleColumns: BasicColumn[] = [
+  {
+    title: '名称',
+    dataIndex: 'strname',
+    width: 100,
+  },
+  {
+    title: '是否为常闭型',
+    dataIndex: 'bnormalclose',
+    width: 100,
+  },
+];
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    label: '名称',
+    field: 'strname',
+    component: 'Input',
+    colProps: { span: 6 },
+  },
+  {
+    label: '安装位置',
+    field: 'strinstallpos',
+    component: 'Input',
+    colProps: { span: 6 },
+  },
+  {
+    label: '是否为常闭型',
+    field: 'bnormalclose',
+    component: 'JDictSelectTag',
+    componentProps: {
+      dictCode: 'user_status',
+      placeholder: '请选择读写类型',
+      stringToNumber: true,
+    },
+    colProps: { span: 6 },
+  },
+];
+
+export const formSchema: FormSchema[] = [
+  {
+    label: '',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '名称',
+    field: 'strname',
+    component: 'Input',
+  },
+  {
+    label: '安装位置',
+    field: 'strinstallpos',
+    component: 'Input',
+  },
+  {
+    label: '是否为常闭型',
+    field: 'bnormalclose',
+    component: 'RadioGroup',
+    defaultValue: 1,
+    componentProps: () => {
+      return {
+        options: [
+          { label: '是', value: 1, key: '1' },
+          { label: '否', value: 0, key: '2' },
+        ],
+      };
+    },
+  },
+  {
+    label: '净宽',
+    field: 'fclearwidth',
+    component: 'Input',
+  },
+  {
+    label: '净高',
+    field: 'fclearheight',
+    component: 'Input',
+  },
+  {
+    label: '风门道数',
+    field: 'ndoorcount',
+    component: 'Input',
+  },
+  {
+    label: '所属分站',
+    field: 'stationname',
+    component: 'JDictSelectTag',
+    componentProps: {
+      dictCode: 'user_status',
+      placeholder: '请选择状态',
+      stringToNumber: true,
+    },
+  },
+  {
+    label: '点表',
+    field: 'strtype',
+    component: 'JDictSelectTag',
+    componentProps: {
+      dictCode: 'user_status',
+      placeholder: '请选择状态',
+      stringToNumber: true,
+    },
+  },
+  {
+    label: '监测类型',
+    field: 'monitorflag',
+    component: 'JDictSelectTag',
+    componentProps: {
+      dictCode: 'user_status',
+      placeholder: '请选择状态',
+      stringToNumber: true,
+    },
+  },
+  {
+    label: '是否模拟数据',
+    field: 'testflag',
+    component: 'RadioGroup',
+    defaultValue: 1,
+    componentProps: () => {
+      return {
+        options: [
+          { label: '是', value: 1, key: '1' },
+          { label: '否', value: 0, key: '2' },
+        ],
+      };
+    },
+  },
+];
+
+export const formPasswordSchema: FormSchema[] = [
+  {
+    label: '用户账号',
+    field: 'username',
+    component: 'Input',
+    componentProps: { readOnly: true },
+  },
+  {
+    label: '登录密码',
+    field: 'password',
+    component: 'StrengthMeter',
+    componentProps: {
+      placeholder: '请输入登录密码',
+    },
+    rules: [
+      {
+        required: true,
+        message: '请输入登录密码',
+      },
+    ],
+  },
+  {
+    label: '确认密码',
+    field: 'confirmPassword',
+    component: 'InputPassword',
+    dynamicRules: ({ values }) => rules.confirmPassword(values, true),
+  },
+];
+
+export const formAgentSchema: FormSchema[] = [
+  {
+    label: '',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'userName',
+    label: '用户名',
+    component: 'Input',
+    componentProps: {
+      readOnly: true,
+      allowClear: false,
+    },
+  },
+  {
+    field: 'agentUserName',
+    label: '代理人用户名',
+    required: true,
+    component: 'JSelectUser',
+    componentProps: {
+      rowKey: 'username',
+      labelKey: 'realname',
+      maxSelectCount: 10,
+    },
+  },
+  {
+    field: 'startTime',
+    label: '代理开始时间',
+    component: 'DatePicker',
+    required: true,
+    componentProps: {
+      showTime: true,
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      placeholder: '请选择代理开始时间',
+    },
+  },
+  {
+    field: 'endTime',
+    label: '代理结束时间',
+    component: 'DatePicker',
+    required: true,
+    componentProps: {
+      showTime: true,
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      placeholder: '请选择代理结束时间',
+    },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'JDictSelectTag',
+    defaultValue: '1',
+    componentProps: {
+      dictCode: 'valid_status',
+      type: 'radioButton',
+    },
+  },
+];
+
+export const chartsColumns = [
+  {
+    legend: '压差',
+    seriesName: '(Pa)',
+    ymax: 100,
+    yname: 'Pa',
+    linetype: 'bar',
+    yaxispos: 'left',
+    color: '#37BCF2',
+    sort: 1,
+    xRotate: 0,
+    dataIndex: 'frontRearDP',
+  },
+  // {
+  //   legend: '气源压力',
+  //   seriesName: '(MPa)',
+  //   ymax: 50,
+  //   yname: 'MPa',
+  //   linetype: 'line',
+  //   yaxispos: 'right',
+  //   color: '#FC4327',
+  //   sort: 2,
+  //   xRotate: 0,
+  //   dataIndex: 'sourcePressure',
+  // },
+];

+ 185 - 0
src/views/vent/monitorManager/fireDoorMonitor/fireDoor.threejs.fire.ts

@@ -0,0 +1,185 @@
+import * as THREE from 'three';
+import { useAppStore } from '/@/store/modules/app';
+
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+class FireDoor {
+  modelName = 'fireDoor';
+  model; //
+  group;
+  isLRAnimation = true; // 是否开启左右摇摆动画
+  direction = 1; // 摇摆方向
+  animationTimer: NodeJS.Timeout | null = null; // 摇摆开启定时器
+  player1;
+  player2;
+  deviceDetailCSS3D;
+  playerStartClickTime1 = new Date().getTime();
+  playerStartClickTime2 = new Date().getTime();
+
+  fmClock = new THREE.Clock();
+  mixers: THREE.AnimationMixer | undefined;
+  appStore = useAppStore();
+  damperOpenMesh;
+  damperClosedMesh;
+
+  clipActionArr = {
+    door: null as unknown as THREE.AnimationAction,
+  };
+
+  constructor(model) {
+    this.model = model;
+  }
+
+  addLight() {
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
+    directionalLight.position.set(344, 690, 344);
+    this.group?.add(directionalLight);
+    directionalLight.target = this.group as THREE.Object3D;
+
+    const pointLight2 = new THREE.PointLight(0xffeeee, 1, 300);
+    pointLight2.position.set(-4, 10, 1.8);
+    pointLight2.shadow.bias = 0.05;
+    this.group?.add(pointLight2);
+
+    const pointLight3 = new THREE.PointLight(0xffeeee, 1, 200);
+    pointLight3.position.set(-0.5, -0.5, 0.75);
+    pointLight3.shadow.bias = 0.05;
+    this.group?.add(pointLight3);
+  }
+  resetCamera() {
+    this.model.camera.far = 274;
+    this.model.orbitControls?.update();
+    this.model.camera.updateProjectionMatrix();
+  }
+  // 设置模型位置
+  setModalPosition() {
+    this.group?.scale.set(22, 22, 22);
+    this.group?.position.set(-20, 20, 9);
+  }
+
+  /* 风门动画 */
+  render() {
+    if (!this.model) {
+      return;
+    }
+    if (this.mixers && this.fmClock.running) {
+      this.mixers.update(2);
+    }
+  }
+
+  /* 点击风门 */
+  mousedownModel(intersects: THREE.Intersection<THREE.Object3D<THREE.Event>>[]) {
+    console.log('摄像头控制信息', this.model?.orbitControls, this.model?.camera);
+  }
+
+  mouseUpModel() {}
+
+  /* 提取风门序列帧,初始化前后门动画 */
+  initAnimation() {
+    const fmGroup = this.group?.getObjectByName('fire-door');
+    if (fmGroup) {
+      const tracks = fmGroup.animations[0].tracks;
+      const doorTracks: any[] = [];
+      for (let i = 0; i < tracks.length; i++) {
+        const track = tracks[i];
+        if (track.name.startsWith('qianmen')) {
+          doorTracks.push(track);
+        }
+      }
+
+      this.mixers = new THREE.AnimationMixer(fmGroup);
+
+      const door = new THREE.AnimationClip('door', 22, doorTracks);
+      const frontClipAction = this.mixers.clipAction(door, fmGroup);
+      frontClipAction.clampWhenFinished = true;
+      frontClipAction.loop = THREE.LoopOnce;
+      this.clipActionArr.door = frontClipAction;
+    }
+  }
+
+  // 播放动画
+  play(handlerState, timeScale = 0.01) {
+    let handler = () => {};
+    if (this.clipActionArr.door) {
+      switch (handlerState) {
+        case 1: // 打开门
+          handler = () => {
+            this.clipActionArr.door.paused = true;
+            this.clipActionArr.door.reset();
+            this.clipActionArr.door.time = 1.2;
+            this.clipActionArr.door.timeScale = timeScale;
+            // this.clipActionArr.door.clampWhenFinished = true;
+            this.clipActionArr.door.play();
+            this.fmClock.start();
+
+            // 显示打开前门文字
+            if (this.damperOpenMesh) this.damperOpenMesh.visible = true;
+          };
+          break;
+        case 2: // 关闭门
+          handler = () => {
+            this.clipActionArr.door.paused = true;
+            this.clipActionArr.door.reset(); //
+            this.clipActionArr.door.time = 4;
+            this.clipActionArr.door.timeScale = -timeScale;
+            // this.clipActionArr.door.clampWhenFinished = true;
+            this.clipActionArr.door.play();
+            this.fmClock.start();
+
+            if (this.damperOpenMesh) this.damperOpenMesh.visible = false;
+          };
+          break;
+        default:
+      }
+      handler();
+    }
+  }
+
+  mountedThree() {
+    this.group = new THREE.Object3D();
+    this.group.name = this.modelName;
+
+    return new Promise((resolve) => {
+      if (!this.model) {
+        resolve(null);
+      }
+      this.model.setGLTFModel('Fire-door').then((gltf) => {
+        const fmModal = gltf[0];
+        fmModal.name = 'Fire-door';
+        this.group?.add(fmModal);
+        this.setModalPosition();
+        // 初始化左右摇摆动画;
+        this.initAnimation();
+        this.addLight();
+        this.model.animate();
+        resolve(this.model);
+
+        this.damperOpenMesh = this.group.getObjectByName('Damper_Open_2');
+        if (this.damperOpenMesh) this.damperOpenMesh.visible = false;
+        this.damperClosedMesh = this.group.getObjectByName('Damper_Closed_2');
+        if (this.damperClosedMesh) this.damperClosedMesh.visible = true;
+      });
+    });
+  }
+
+  destroy() {
+    if (this.model) {
+      if (this.mixers) {
+        this.mixers.uncacheClip(this.clipActionArr.door.getClip());
+        this.mixers.uncacheAction(this.clipActionArr.door.getClip(), this.group);
+        this.mixers.uncacheRoot(this.group);
+
+        if (this.model.animations[0]) this.model.animations[0].tracks = [];
+      }
+      this.model.clearGroup(this.group);
+      this.clipActionArr.door = undefined;
+
+      this.mixers = undefined;
+
+      // document.getElementById('damper3D').parentElement.remove(document.getElementById('damper3D'))
+    }
+  }
+}
+export default FireDoor;

+ 129 - 0
src/views/vent/monitorManager/fireDoorMonitor/fireDoor.threejs.ts

@@ -0,0 +1,129 @@
+import * as THREE from 'three';
+import UseThree from '../../../../utils/threejs/useThree';
+import FireDoor from './fireDoor.threejs.fire';
+import { animateCamera } from '/@/utils/threejs/util';
+import useEvent from '../../../../utils/threejs/useEvent';
+import { useGlobSetting } from '/@/hooks/setting';
+
+// 模型对象、 文字对象
+let model,
+  fireDoor, //液压风门
+  group: THREE.Object3D,
+  fmType = '';
+
+const { mouseDownFn } = useEvent();
+
+// 初始化左右摇摆动画
+const startAnimation = () => {
+  // 定义鼠标点击事件
+  model.canvasContainer?.addEventListener('mousedown', mouseEvent.bind(null));
+  model.canvasContainer?.addEventListener('pointerup', (event) => {
+    event.stopPropagation();
+    // 单道、 双道
+    if (fmType === 'fireDoor') {
+      fireDoor?.mouseUpModel.call(fireDoor);
+    }
+  });
+};
+
+// 鼠标点击、松开事件
+const mouseEvent = (event) => {
+  if (event.button == 0) {
+    mouseDownFn(model, group, event, (intersects) => {
+      if (fmType === 'fireDoor' && fireDoor) {
+        fireDoor?.mousedownModel.call(fireDoor, intersects);
+      }
+    });
+    // console.log('摄像头控制信息', model.orbitControls, model.camera);
+  }
+};
+
+export const play = (handlerState, flag?) => {
+  if (fmType === 'fireDoor' && fireDoor) {
+    return fireDoor.play.call(fireDoor, handlerState, flag);
+  }
+};
+
+// 切换风门类型
+export const setModelType = (type) => {
+  fmType = type;
+  return new Promise((resolve) => {
+    // 暂停风门1动画
+    if (fmType === 'fireDoor' && fireDoor && fireDoor.group) {
+      if (fireDoor.clipActionArr.door) {
+        fireDoor.clipActionArr.door.reset();
+        fireDoor.clipActionArr.door.time = 0.5;
+        fireDoor.clipActionArr.door.stop();
+      }
+
+      if (fireDoor.damperOpenMesh) fireDoor.damperOpenMesh.visible = false;
+      if (fireDoor.damperClosedMesh) fireDoor.damperClosedMesh.visible = true;
+      model.scene.remove(group);
+      model.startAnimation = fireDoor.render.bind(fireDoor);
+      group = fireDoor.group;
+      group.rotation.y = 0;
+      const oldCameraPosition = { x: -1000, y: 100, z: 500 };
+      setTimeout(async () => {
+        resolve(null);
+        model.scene.add(fireDoor.group);
+        await animateCamera(
+          oldCameraPosition,
+          { x: 0, y: 0, z: 0 },
+          { x: -621.7019254383079, y: 103.0117761306667, z: -29.92662339085279 },
+          { x: -7.222742181565763, y: 58.55923854292045, z: -28.86454095144525 },
+          model,
+          0.8
+        );
+      }, 300);
+    }
+  });
+};
+
+export const initCameraCanvas = async (playerVal1) => {
+  if (fmType === 'fireDoor' && fireDoor) {
+    return await fireDoor.initCamera.call(fireDoor, playerVal1);
+  }
+};
+const setControls = () => {
+  if (model && model.orbitControls) {
+    model.orbitControls.maxPolarAngle = (Math.PI / 3) * 2;
+    model.orbitControls.minPolarAngle = Math.PI / 3;
+    model.orbitControls.enableRotate = false; //禁止旋转
+    model.orbitControls.minDistance = 600;
+    model.orbitControls.maxDistance = 900;
+  }
+};
+
+export const mountedThree = () => {
+  // const { sysOrgCode } = useGlobSetting();
+  return new Promise(async (resolve) => {
+    model = new UseThree('#damper3D', '', '#deviceDetail');
+    model.setEnvMap('test1');
+    model.renderer.toneMappingExposure = 1.0;
+    model.camera.position.set(100, 0, 1000);
+    fireDoor = new FireDoor(model);
+    fireDoor.mountedThree();
+    resolve(null);
+    setControls();
+    model.animate();
+    startAnimation();
+  });
+};
+
+export const destroy = () => {
+  if (model) {
+    model.orbitControls.maxPolarAngle = Math.PI;
+    model.orbitControls.minPolarAngle = 0;
+    model.orbitControls.enableRotate = true;
+    model.orbitControls.minDistance = 0;
+    model.orbitControls.maxDistance = Infinity;
+    model.orbitControls.update();
+    model.isRender = false;
+    if (fireDoor) fireDoor.destroy();
+    fireDoor = null;
+    group = null;
+    model.mixers = [];
+    model.destroy();
+  }
+  model = null;
+};

+ 500 - 0
src/views/vent/monitorManager/fireDoorMonitor/index.vue

@@ -0,0 +1,500 @@
+<template>
+  <div class="bg" style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden">
+    <a-spin :spinning="loading" />
+    <div id="damper3D" style="width: 100%; height: 100%; position: absolute; overflow: hidden"></div>
+  </div>
+  <div class="scene-box">
+    <div class="top-box">
+      <div class="top-center row">
+        <div v-if="hasPermission('btn:control')" class="button-box" @click="playAnimation(1)">打开</div>
+        <div v-if="hasPermission('btn:control')" class="button-box" @click="playAnimation(2)">关闭</div>
+      </div>
+      <!-- 控制模式 -->
+      <div class="top-right row">
+        <div class="vent-flex-m row" v-if="selectData.contrlMod == 'loopCtrl'">
+          <div class="control-title">控制模式:</div>
+          <a-radio-group v-model:value="selectData.autoRoManual">
+            <template v-for="(item, index) in modelList" :key="index">
+              <a-radio :value="item.value" :disabled="true">{{ item.text }}</a-radio>
+            </template>
+          </a-radio-group>
+          <div class="button-box" @click="playAnimation(7)">切换模式</div>
+        </div>
+        <div class="vent-flex-m row" v-else>
+          <div class="control-title">控制模式:</div>
+          <a-radio-group v-model:value="selectData.autoRoManual">
+            <template v-for="(item, index) in modelList" :key="index">
+              <a-radio :value="item.value" :disabled="true">{{ item.text }}</a-radio>
+            </template>
+          </a-radio-group>
+          <div class="button-box" v-for="(item, index) in modelList" @click="playAnimation(7, item.value)" :key="index">{{ item.text }}</div>
+        </div>
+
+        <!-- <div class="run-type row">
+          <div class="control-title">运行状态:</div>
+          <a-radio-group v-model:value="selectData.runRoRecondition">
+            <a-radio :value="`0`">检修</a-radio>
+            <a-radio :value="`1`">运行</a-radio>
+          </a-radio-group>
+        </div> -->
+      </div>
+    </div>
+    <div class="title-text">
+      {{ selectData.supplyAirAddr || selectData.strinstallpos || selectData.strname }}
+    </div>
+    <div class="bottom-tabs-box" @mousedown="setDivHeight($event, 350, scroll)">
+      <dv-border-box8 :dur="5" :style="`padding: 5px; height: ${scroll.y + 120}px`">
+        <a-tabs class="tabs-box" v-model:activeKey="activeKey" @change="tabChange">
+          <a-tab-pane key="1" tab="实时监测">
+            <MonitorTable
+              v-if="activeKey === '1'"
+              ref="MonitorDataTable"
+              class="monitor-table"
+              :columnsType="deviceType"
+              :isShowActionColumn="true"
+              :dataSource="dataSource"
+              design-scope="gate-monitor"
+              @selectRow="getSelectRow"
+              :scroll="{ y: scroll.y - 40 }"
+              title="风门监测"
+              :isShowPagination="true"
+            >
+              <template #filterCell="{ column, record }">
+                <a-tag v-if="column.dataIndex === 'doorOpen' && record.doorOpen == '0' && record.doorClose == '0'" color="red">正在运行</a-tag>
+                <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '0' && record.doorClose == 1" color="default">关闭</a-tag>
+                <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '1' && record.doorClose == '0'" color="#46C66F">打开</a-tag>
+                <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '1' && record.doorClose == '1'" color="#FF0000"
+                  >点位异常</a-tag
+                >
+                <template v-else-if="column.dataIndex === 'warnLevel'">
+                  <a-tag v-if="record.warnLevel == '101'" color="green">低风险</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '102'" color="#FF5812">一般风险</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '103'" color="#FF5812">较大风险</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '104'" color="#FF5812">重大风险</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '201'" color="#FF0000">报警</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '10000'" color="#FF5812">数据超限</a-tag>
+                  <a-tag v-else-if="record.warnLevel == '1001'" color="default">网络中断</a-tag>
+                  <a-tag v-else color="green">正常</a-tag>
+                </template>
+                <a-tag v-else-if="column.dataIndex === 'warnFlag'" :color="record.warnFlag == '0' ? 'green' : 'red'">{{
+                  record.warnFlag == '0' ? '正常' : '报警'
+                }}</a-tag>
+                <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '0' ? '#f00' : 'green'">{{
+                  record.netStatus == '0' ? '断开' : '连接'
+                }}</a-tag>
+              </template>
+              <template #action="{ record }">
+                <a v-if="globalConfig?.showReport" class="table-action-link" @click="deviceEdit($event, 'reportInfo', record)">报表录入</a>
+                <a class="table-action-link" @click="deviceEdit($event, 'deviceInfo', record)">设备编辑</a>
+              </template>
+            </MonitorTable>
+          </a-tab-pane>
+          <a-tab-pane key="3" tab="历史数据">
+            <div class="tab-item" v-if="activeKey === '3'">
+              <HistoryTable :columnsType="deviceType" :device-type="deviceType" designScope="gate-history" :scroll="scroll">
+                <template #filterCell="{ column, record }">
+                  <a-tag v-if="column.dataIndex === 'doorOpen' && record.doorOpen == '0' && record.doorClose == '0'" color="red">正在运行</a-tag>
+                  <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '0' && record.doorClose == 1" color="default">关闭</a-tag>
+                  <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '1' && record.doorClose == '0'" color="#46C66F">打开</a-tag>
+                  <a-tag v-else-if="column.dataIndex === 'doorOpen' && record.doorOpen == '1' && record.doorClose == '1'" color="#FF0000"
+                    >点位异常</a-tag
+                  >
+                  <template v-else-if="column.dataIndex === 'warnLevel'">
+                    <a-tag v-if="record.warnLevel == '101'" color="green">低风险</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '102'" color="#FF5812">一般风险</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '103'" color="#FF5812">较大风险</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '104'" color="#FF5812">重大风险</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '201'" color="#FF0000">报警</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '10000'" color="#FF5812">数据超限</a-tag>
+                    <a-tag v-else-if="record.warnLevel == '1001'" color="default">网络中断</a-tag>
+                    <a-tag v-else color="green">正常</a-tag>
+                  </template>
+                </template>
+              </HistoryTable>
+            </div>
+          </a-tab-pane>
+          <a-tab-pane key="4" tab="报警历史">
+            <div class="tab-item" v-if="activeKey === '4'">
+              <AlarmHistoryTable
+                columns-type="alarm"
+                :device-type="deviceType"
+                :device-list-api="getTableList"
+                designScope="alarm-history"
+                :scroll="scroll"
+              >
+                <template #filterCell="{ column, record }">
+                  <a-tag v-if="column.dataIndex === 'warnFlag'" :color="record.warnFlag == '0' ? 'green' : record.warnFlag == 1 ? '#FF5812' : 'gray'">
+                    {{ record.warnFlag == '0' ? '正常' : record.warnFlag == 1 ? '报警' : record.warnFlag == 2 ? '断开' : '未监测' }}</a-tag
+                  >
+                  <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '0' ? '#f00' : 'green'">{{
+                    record.netStatus == '0' ? '断开' : '连接'
+                  }}</a-tag>
+                </template>
+              </AlarmHistoryTable>
+            </div>
+          </a-tab-pane>
+          <a-tab-pane key="5" tab="操作历史">
+            <div class="tab-item" v-if="activeKey === '5'">
+              <HandlerHistoryTable
+                columns-type="operator_history"
+                :device-type="deviceType"
+                :device-list-api="getTableList"
+                designScope="operator_history"
+                :scroll="scroll"
+              />
+            </div>
+          </a-tab-pane>
+        </a-tabs>
+      </dv-border-box8>
+    </div>
+  </div>
+  <div ref="playerRef" style="z-index: 999; position: absolute; top: 100px; right: 15px; width: 300px; height: 280px; margin: auto"> </div>
+  <HandleModal
+    v-if="!globalConfig?.simulatedPassword"
+    :modal-is-show="modalIsShow"
+    :modal-title="modalTitle"
+    :modal-type="modalType"
+    @handle-ok="handleOK"
+    @handle-cancel="handleCancel"
+  />
+  <DeviceBaseInfo @register="registerModal" :device-type="selectData['deviceType']" />
+</template>
+
+<script setup lang="ts">
+  import { onBeforeUnmount, onUnmounted, onMounted, ref, reactive, nextTick, inject, unref } from 'vue';
+  import MonitorTable from '../comment/MonitorTable.vue';
+  import HistoryTable from '../comment/HistoryTable.vue';
+  import AlarmHistoryTable from '../comment/AlarmHistoryTable.vue';
+  import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
+  import HandleModal from './modal.vue';
+  import DeviceBaseInfo from '../comment/components/DeviceBaseInfo.vue';
+  import { mountedThree, play, destroy, setModelType } from './fireDoor.threejs';
+  import { deviceControlApi } from '/@/api/vent/index';
+  import { message } from 'ant-design-vue';
+  import { list, getTableList } from './fireDoor.api';
+  import lodash from 'lodash';
+  import { setDivHeight } from '/@/utils/event';
+  import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
+  import { useRouter } from 'vue-router';
+  import { useModal } from '/@/components/Modal';
+  import { useCamera } from '/@/hooks/system/useCamera';
+  import { usePermission } from '/@/hooks/web/usePermission';
+  import { getDictItems } from '/@/api/common/api';
+
+  const { hasPermission } = usePermission();
+
+  const globalConfig = inject('globalConfig');
+
+  const { currentRoute } = useRouter();
+  const MonitorDataTable = ref();
+  let contrlValue = '';
+  const playerRef = ref();
+  const deviceType = ref('firedoor');
+  const activeKey = ref('1'); // tab
+  const loading = ref(false);
+
+  const scroll = reactive({
+    y: 230,
+  });
+  const modelList = ref<{ text: string; value: string }[]>([]);
+  const doorIsOpen = ref(false); //前门是否开启
+  const modalIsShow = ref<boolean>(false); // 是否显示模态框
+  const modalTitle = ref(''); // 模态框标题显示内容,根据设备操作类型决定
+  const modalType = ref(''); // 模态框内容显示类型,设备操作类型
+
+  const selectRowIndex = ref(-1); // 选中行
+  const dataSource = ref([]);
+
+  const deviceBaseList = ref([]); // 设备基本信息
+  const [registerModal, { openModal, closeModal }] = useModal();
+
+  const { getCamera, removeCamera } = useCamera();
+
+  const tabChange = (activeKeyVal) => {
+    activeKey.value = activeKeyVal;
+    if (activeKeyVal == 1) {
+      nextTick(() => {
+        if (MonitorDataTable.value) MonitorDataTable.value.setSelectedRowKeys([selectData.deviceID]);
+      });
+    }
+  };
+
+  const initData = {
+    deviceID: '',
+    deviceType: '',
+    strname: '',
+    frontRearDP: '-', //压差
+    // sourcePressure: '-', //气源压力
+    runRoRecondition: null,
+    autoRoManual: null,
+    netStatus: '0', //通信状态
+    frontGateOpen: '0',
+    frontGateClose: '1',
+    rearGateOpen: '0',
+    rearGateClose: '1',
+    midGateOpen: '0',
+    midGateClose: '1',
+    fault: '气源压力超限',
+    masterComputer: 0,
+    frontGateOpenCtrl: false,
+    rearGateOpenCtrl: false,
+    cameras: [],
+  };
+
+  // 监测数据
+  const selectData = reactive(lodash.cloneDeep(initData));
+  function deviceEdit(e: Event, type: string, record) {
+    e.stopPropagation();
+    openModal(true, {
+      type,
+      deviceId: record['deviceID'],
+    });
+  }
+  // 获取设备基本信息列表
+  function getDeviceBaseList() {
+    getTableList({ pageSize: 1000 }).then((res) => {
+      deviceBaseList.value = res.records;
+    });
+  }
+
+  // https获取监测数据
+  let timer: null | NodeJS.Timeout = null;
+  async function getMonitor(flag?) {
+    if (Object.prototype.toString.call(timer) === '[object Null]') {
+      timer = await setTimeout(
+        async () => {
+          const res = await list({ devicetype: deviceType.value, pagetype: 'normal' });
+          if (res.msgTxt && res.msgTxt[0]) {
+            dataSource.value = res.msgTxt[0].datalist || [];
+            dataSource.value.forEach((data: any) => {
+              const readData = data.readData;
+              data = Object.assign(data, readData);
+            });
+            if (dataSource.value.length > 0 && selectRowIndex.value == -1 && MonitorDataTable.value) {
+              // 初始打开页面
+              if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
+                MonitorDataTable.value.setSelectedRowKeys([currentRoute.value['query']['id']]);
+              } else {
+                MonitorDataTable.value.setSelectedRowKeys([dataSource.value[0]['deviceID']]);
+              }
+            }
+            Object.assign(selectData, dataSource.value[selectRowIndex.value]);
+            monitorAnimation(selectData);
+            if (timer) {
+              timer = null;
+            }
+            getMonitor();
+          }
+        },
+        flag ? 0 : 1000
+      );
+    }
+  }
+
+  // 切换检测数据
+  async function getSelectRow(selectRow, index) {
+    if (!selectRow) return;
+    loading.value = true;
+    selectRowIndex.value = index;
+
+    const baseData: any = deviceBaseList.value.find((baseData: any) => baseData.id === selectRow.deviceID);
+    Object.assign(selectData, initData, selectRow, baseData);
+    isdoorOpenRunning = false; //开关门动作是否在进行
+    doorDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+    // const type = selectData.nwindownum == 1 ? 'singleWindow' : 'doubleWindow';
+    let type = 'fireDoor';
+
+    setModelType(type).then(async () => {
+      loading.value = false;
+    });
+    await getCamera(selectRow.deviceID, playerRef.value);
+  }
+
+  function playAnimation(handlerState, data: any = null) {
+    const value = data;
+    switch (handlerState) {
+      case 1: // 打开前门
+        modalTitle.value = '打开';
+        modalType.value = '1';
+        modalIsShow.value = true;
+        break;
+      case 2: // 关闭前门
+        modalTitle.value = '关闭';
+        modalType.value = '2';
+        modalIsShow.value = true;
+        break;
+      case 7: // 控制模式切换
+        modalTitle.value = '控制模式切换';
+        modalType.value = '7';
+        modalIsShow.value = true;
+        break;
+    }
+
+    if (globalConfig?.simulatedPassword) {
+      handleOK('', handlerState + '');
+    }
+    contrlValue = value;
+  }
+
+  function handleOK(passWord, handlerState) {
+    if (passWord == '') {
+      message.warning('请输入密码');
+      return;
+    }
+    if (isOpenRunning) {
+      return;
+    }
+    const data = {
+      deviceid: selectData.deviceID,
+      devicetype: selectData.deviceType,
+      paramcode: '',
+      value: contrlValue,
+      password: passWord || globalConfig?.simulatedPassword,
+      masterComputer: selectData.masterComputer,
+    };
+    switch (handlerState) {
+      case '1': // 打开前门
+        if (selectData.doorOpen == '0' && selectData.doorClose == '1') {
+          data.paramcode = 'doorOpenCtr';
+        }
+        break;
+      case '2': // 关闭前门
+        if (selectData.doorOpen == '1' && selectData.doorClose == '0') {
+          data.paramcode = 'doorCloseCtr';
+        }
+        break;
+      case '7': // 远程与就地
+        data.paramcode = 'autoRoManualControl';
+        data.value = selectData.contrlMod != 'loopCtrl' ? contrlValue : '';
+        selectData.autoRoManual = null;
+    }
+
+    if (data.paramcode) {
+      deviceControlApi(data).then((res) => {
+        // 模拟时开启
+        if (res.success) {
+          modalIsShow.value = false;
+          if (globalConfig.History_Type == 'remote') {
+            message.success('指令已下发至生产管控平台成功!');
+          } else {
+            message.success('指令已下发成功!');
+          }
+        } else {
+          message.error(res.message);
+        }
+      });
+    }
+  }
+  let isOpenRunning = false; //开关门动作是否在进行
+  /** 开关门动画调用 */
+  let isdoorOpenRunning = false; //开关门动作是否在进行
+  // let isMidCloseRunning = false; //中间门动作是否在进行
+  // 0 关闭 1 正在打开 2 打开 3正在关闭
+  let doorDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+  function monitorAnimation(selectData) {
+    const timeScale = 0.005;
+
+    if (selectData.doorOpen == '1' && selectData.doorClose == '0' && !isdoorOpenRunning) {
+      isdoorOpenRunning = true;
+      if (doorDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
+        play(1, timeScale);
+        doorDeviceState = 1;
+        doorIsOpen.value = true;
+      }
+    }
+
+    if (selectData.doorOpen == '0' && selectData.doorClose == '1' && !isdoorOpenRunning) {
+      isdoorOpenRunning = true;
+      if (doorDeviceState != 0) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
+        play(2, timeScale);
+        doorDeviceState = 0;
+        doorIsOpen.value = false;
+      }
+    }
+
+    // if (selectData.frontGateClose == '1' && selectData.frontGateOpen == '0' && isFrontOpenRunning) {
+    //   isFrontOpenRunning = false;
+    //   if (frontDeviceState != 0) {
+    //     // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(2, timeScale) : play(2);
+    //     play(2, timeScale);
+    //     frontDeviceState = 0;
+    //     frontDoorIsOpen.value = false;
+    //     // backDoorIsOpen.value = false
+    //   }
+    // }
+  }
+
+  function handleCancel() {
+    modalIsShow.value = false;
+    modalTitle.value = '';
+    modalType.value = '';
+    selectData.autoRoManual = null;
+  }
+
+  onMounted(async () => {
+    const { query } = unref(currentRoute);
+    if (query['deviceType']) deviceType.value = query['deviceType'] as string;
+    modelList.value = await getDictItems('fireDoorModel');
+    loading.value = true;
+    mountedThree().then(async () => {
+      await getMonitor(true);
+      loading.value = false;
+    });
+  });
+
+  onBeforeUnmount(() => {
+    getDeviceBaseList();
+  });
+
+  onUnmounted(() => {
+    removeCamera();
+    if (timer) {
+      clearTimeout(timer);
+      timer = undefined;
+    }
+    destroy();
+  });
+</script>
+,
+<style lang="less" scoped>
+  @import '/@/design/vent/modal.less';
+  .scene-box {
+    .bottom-tabs-box {
+      height: 350px;
+    }
+  }
+  .button-box {
+    border: none !important;
+    height: 34px !important;
+
+    &:hover {
+      background: linear-gradient(#2cd1ff55, #1eb0ff55) !important;
+    }
+
+    &::before {
+      height: 27px !important;
+      background: linear-gradient(#1fa6cb, #127cb5) !important;
+    }
+
+    &::after {
+      top: 35px !important;
+    }
+  }
+
+  :deep(.@{ventSpace}-tabs-tabpane-active) {
+    height: 100%;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
+    background: #4288a444;
+  }
+  :deep(.zxm-radio-disabled + span) {
+    color: #fff !important;
+  }
+  :deep(.zxm-radio-disabled .zxm-radio-inner::after) {
+    background-color: #127cb5 !important;
+  }
+</style>

+ 66 - 0
src/views/vent/monitorManager/fireDoorMonitor/modal.vue

@@ -0,0 +1,66 @@
+<template>
+  <a-modal v-model:visible="visible" :title="title" @ok="handleOk" @cancel="handleCancel">
+    <div class="modal-container">
+      <div class="vent-flex-row">
+        <ExclamationCircleFilled style="color: #ffb700; font-size: 30px" />
+        <div class="warning-text">您是否要进行{{ title }}操作?</div>
+      </div>
+      <div class="vent-flex-row input-box">
+        <div class="label">操作密码:</div>
+        <a-input size="small" type="password" v-model:value="passWord" />
+      </div>
+    </div>
+  </a-modal>
+</template>
+<script setup lang="ts">
+  import { watch, ref } from 'vue';
+  import { ExclamationCircleFilled } from '@ant-design/icons-vue';
+
+  const props = defineProps({
+    modalIsShow: {
+      type: Boolean,
+      default: false,
+    },
+    modalTitle: {
+      type: String,
+      default: '',
+    },
+    modalType: {
+      type: String,
+      default: '',
+    },
+  });
+
+  const emit = defineEmits(['handleOk', 'handleCancel']);
+
+  const visible = ref<Boolean>(false);
+  const title = ref<String>('');
+  const type = ref<String>('');
+  const passWord = ref('');
+
+  watch([() => props.modalIsShow, () => props.modalTitle, () => props.modalType], ([newVal, newModalTitle, newModalType]) => {
+    visible.value = newVal;
+    if (newModalTitle) title.value = newModalTitle;
+    if (newModalType) type.value = newModalType;
+    passWord.value = '';
+  });
+
+  function handleOk() {
+    //
+    emit('handleOk', passWord.value, type.value);
+  }
+  function handleCancel() {
+    //
+    emit('handleCancel');
+  }
+</script>
+<style scoped lang="less">
+  @ventSpace: zxm;
+
+  .label {
+    width: 80px;
+  }
+  .@{ventSpace}-input {
+    width: 150px;
+  }
+</style>

+ 408 - 0
src/views/vent/monitorManager/gateMonitor/gate.threejs.noStation.ts

@@ -0,0 +1,408 @@
+import * as THREE from 'three';
+import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
+import { getTextCanvas, renderVideo } from '/@/utils/threejs/util';
+import { drawHot } from '/@/utils/threejs/util';
+import { useAppStore } from '/@/store/modules/app';
+
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+class FmNoStation {
+  modelName = 'FmNoStation';
+  model; //
+  group;
+  isLRAnimation = true; // 是否开启左右摇摆动画
+  direction = 1; // 摇摆方向
+  animationTimer: NodeJS.Timeout | null = null; // 摇摆开启定时器
+  player1;
+  player2;
+  deviceDetailCSS3D;
+  playerStartClickTime1 = new Date().getTime();
+  playerStartClickTime2 = new Date().getTime();
+
+  fmClock = new THREE.Clock();
+  mixers: THREE.AnimationMixer | undefined;
+  appStore = useAppStore();
+
+  backDamperOpenMesh;
+  backDamperClosedMesh;
+  frontDamperOpenMesh;
+  frontDamperClosedMesh;
+
+  clipActionArr = {
+    frontDoor: null as unknown as THREE.AnimationAction,
+    backDoor: null as unknown as THREE.AnimationAction,
+  };
+
+  constructor(model) {
+    this.model = model;
+  }
+
+  addLight() {
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
+    directionalLight.position.set(344, 690, 344);
+    this.group?.add(directionalLight);
+    directionalLight.target = this.group as THREE.Object3D;
+
+    const pointLight2 = new THREE.PointLight(0xffeeee, 1, 300);
+    pointLight2.position.set(-4, 10, 1.8);
+    pointLight2.shadow.bias = 0.05;
+    this.group?.add(pointLight2);
+
+    const pointLight3 = new THREE.PointLight(0xffeeee, 1, 200);
+    pointLight3.position.set(-0.5, -0.5, 0.75);
+    pointLight3.shadow.bias = 0.05;
+    this.group?.add(pointLight3);
+  }
+  resetCamera() {
+    this.model.camera.far = 274;
+    this.model.orbitControls?.update();
+    this.model.camera.updateProjectionMatrix();
+  }
+  // 设置模型位置
+  setModalPosition() {
+    this.group?.scale.set(22, 22, 22);
+    this.group?.position.set(-20, 20, 9);
+  }
+
+  /* 添加监控数据 */
+  addMonitorText(selectData) {
+    if (!this.group) {
+      return;
+    }
+    const textArr = [
+      {
+        text: `远程控制自动风门`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 120,
+        y: 100,
+      },
+      {
+        text: `净通行高度(m):`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 0,
+        y: 155,
+      },
+      {
+        text: `${selectData.fclearheight ? selectData.fclearheight : '-'}`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 330,
+        y: 155,
+      },
+      {
+        text: `净通行宽度(m): `,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 0,
+        y: 215,
+      },
+      {
+        text: ` ${selectData.fclearwidth ? selectData.fclearwidth : '-'}`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 320,
+        y: 215,
+      },
+      {
+        text: `故障诊断:`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 0,
+        y: 275,
+      },
+      {
+        text: `${selectData.warnLevel_str ? selectData.warnLevel_str : '-'}`,
+        font: 'normal 30px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: 320,
+        y: 275,
+      },
+      {
+        text: History_Type['type'] == 'remote' ? `国能神东煤炭集团监制` : '煤炭科学技术研究院有限公司研制',
+        font: 'normal 28px Arial',
+        color: '#00FF00',
+        strokeStyle: '#007400',
+        x: History_Type['type'] == 'remote' ? 90 : 30,
+        y: 325,
+      },
+    ];
+
+    //
+    getTextCanvas(526, 346, textArr, '').then((canvas: HTMLCanvasElement) => {
+      const textMap = new THREE.CanvasTexture(canvas); // 关键一步
+      textMap.colorSpace = THREE.SRGBColorSpace;
+      const textMaterial = new THREE.MeshBasicMaterial({
+        // 关于材质并未讲解 实操即可熟悉                 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质.
+        map: textMap, // 设置纹理贴图
+        transparent: true,
+        side: THREE.FrontSide, // 这里是双面渲染的意思
+      });
+      textMaterial.blending = THREE.CustomBlending;
+      const monitorPlane = this.group.getObjectByName('monitorText');
+      if (monitorPlane) {
+        monitorPlane.material = textMaterial;
+      } else {
+        const planeGeometry = new THREE.PlaneGeometry(526, 346); // 平面3维几何体PlaneGeometry
+        const planeMesh = new THREE.Mesh(planeGeometry, textMaterial);
+        planeMesh.name = 'monitorText';
+        planeMesh.scale.set(0.002, 0.002, 0.002);
+        planeMesh.position.set(3.665, 0.09, -0.4);
+        this.group.add(planeMesh);
+      }
+      textMap.dispose();
+    });
+  }
+
+  /* 风门动画 */
+  render() {
+    if (!this.model) {
+      return;
+    }
+    if (this.isLRAnimation && this.group) {
+      // 左右摇摆动画
+      if (Math.abs(this.group.rotation.y) >= 0.2) {
+        this.direction = -this.direction;
+        this.group.rotation.y += 0.00002 * 30 * this.direction;
+      } else {
+        this.group.rotation.y += 0.00002 * 30 * this.direction;
+      }
+    }
+
+    if (this.mixers && this.fmClock.running) {
+      this.mixers.update(2);
+    }
+  }
+
+  /* 点击风门 */
+  mousedownModel(intersects: THREE.Intersection<THREE.Object3D<THREE.Event>>[]) {
+    this.isLRAnimation = false;
+    if (this.animationTimer) {
+      clearTimeout(this.animationTimer);
+      this.animationTimer = null;
+    }
+  }
+
+  mouseUpModel() {
+    // 10s后开始摆动
+    if (!this.animationTimer && !this.isLRAnimation) {
+      this.animationTimer = setTimeout(() => {
+        this.isLRAnimation = true;
+      }, 10000);
+    }
+  }
+
+  /* 提取风门序列帧,初始化前后门动画 */
+  initAnimation() {
+    const fmGroup = this.group?.getObjectByName('Fm-noStation');
+    if (fmGroup) {
+      const tracks = fmGroup.animations[0].tracks;
+      const fontTracks: any[] = [],
+        backTracks: any[] = [];
+      for (let i = 0; i < tracks.length; i++) {
+        const track = tracks[i];
+        if (track.name.startsWith('qianmen')) {
+          fontTracks.push(track);
+        } else if (track.name.startsWith('houmen')) {
+          backTracks.push(track);
+        }
+      }
+
+      this.mixers = new THREE.AnimationMixer(fmGroup);
+
+      const frontDoor = new THREE.AnimationClip('frontDoor', 22, fontTracks);
+      const frontClipAction = this.mixers.clipAction(frontDoor, fmGroup);
+      frontClipAction.clampWhenFinished = true;
+      frontClipAction.loop = THREE.LoopOnce;
+      this.clipActionArr.frontDoor = frontClipAction;
+
+      const backDoor = new THREE.AnimationClip('backDoor', 22, backTracks);
+      const backClipAction = this.mixers.clipAction(backDoor, fmGroup);
+      backClipAction.clampWhenFinished = true;
+      backClipAction.loop = THREE.LoopOnce;
+      this.clipActionArr.backDoor = backClipAction;
+    }
+  }
+
+  // 播放动画
+  play(handlerState, timeScale = 0.01) {
+    let handler = () => {};
+    if (this.clipActionArr.frontDoor && this.clipActionArr.backDoor) {
+      switch (handlerState) {
+        case 1: // 打开前门
+          handler = () => {
+            this.clipActionArr.frontDoor.paused = true;
+            this.clipActionArr.frontDoor.reset();
+            this.clipActionArr.frontDoor.time = 1.2;
+            this.clipActionArr.frontDoor.timeScale = timeScale;
+            // this.clipActionArr.frontDoor.clampWhenFinished = true;
+            this.clipActionArr.frontDoor.play();
+            this.fmClock.start();
+
+            // 显示打开前门文字
+            if (this.frontDamperOpenMesh) this.frontDamperOpenMesh.visible = true;
+            if (this.frontDamperClosedMesh) this.frontDamperClosedMesh.visible = false;
+          };
+          break;
+        case 2: // 关闭前门
+          handler = () => {
+            this.clipActionArr.frontDoor.paused = true;
+            this.clipActionArr.frontDoor.reset(); //
+            this.clipActionArr.frontDoor.time = 4;
+            this.clipActionArr.frontDoor.timeScale = -timeScale;
+            // this.clipActionArr.frontDoor.clampWhenFinished = true;
+            this.clipActionArr.frontDoor.play();
+            this.fmClock.start();
+
+            if (this.frontDamperOpenMesh) this.frontDamperOpenMesh.visible = false;
+            if (this.frontDamperClosedMesh) this.frontDamperClosedMesh.visible = true;
+          };
+          break;
+        case 3: // 打开后门
+          handler = () => {
+            this.clipActionArr.backDoor.paused = true;
+            this.clipActionArr.backDoor.reset();
+            this.clipActionArr.backDoor.time = 1.2;
+            this.clipActionArr.backDoor.timeScale = timeScale;
+            // this.clipActionArr.backDoor.clampWhenFinished = true;
+            this.clipActionArr.backDoor.play();
+            this.fmClock.start();
+
+            if (this.backDamperOpenMesh) this.backDamperOpenMesh.visible = true;
+            if (this.backDamperClosedMesh) this.backDamperClosedMesh.visible = false;
+          };
+          break;
+        case 4: // 关闭后门
+          handler = () => {
+            this.clipActionArr.backDoor.paused = true;
+            this.clipActionArr.backDoor.reset();
+            this.clipActionArr.backDoor.time = 4;
+            this.clipActionArr.backDoor.timeScale = -timeScale;
+            // this.clipActionArr.backDoor.clampWhenFinished = true;
+            this.clipActionArr.backDoor.play();
+            this.fmClock.start();
+
+            if (this.backDamperOpenMesh) this.backDamperOpenMesh.visible = false;
+            if (this.backDamperClosedMesh) this.backDamperClosedMesh.visible = true;
+          };
+          break;
+        // case 5: // 打开前后门
+        //   handler = () => {
+        //     this.clipActionArr.backDoor.paused = true;
+        //     this.clipActionArr.frontDoor.paused = true;
+
+        //     this.clipActionArr.frontDoor.reset();
+        //     this.clipActionArr.frontDoor.time = 0;
+        //     this.clipActionArr.frontDoor.timeScale = 0.01;
+        //     this.clipActionArr.frontDoor.clampWhenFinished = true;
+        //     this.clipActionArr.frontDoor.play();
+
+        //     this.clipActionArr.backDoor.reset();
+        //     this.clipActionArr.backDoor.time = 0;
+        //     this.clipActionArr.backDoor.timeScale = 0.01;
+        //     this.clipActionArr.backDoor.clampWhenFinished = true;
+        //     this.clipActionArr.backDoor.play();
+        //     this.frontClock.start();
+        //     this.backClock.start();
+        //   };
+        //   break;
+        // case 6: // 关闭前后门
+        //   handler = () => {
+        //     debugger;
+        //     this.clipActionArr.backDoor.paused = true;
+        //     this.clipActionArr.frontDoor.paused = true;
+
+        //     this.clipActionArr.frontDoor.reset();
+        //     this.clipActionArr.frontDoor.time = 4;
+        //     this.clipActionArr.frontDoor.timeScale = -0.01;
+        //     this.clipActionArr.frontDoor.clampWhenFinished = true;
+        //     this.clipActionArr.frontDoor.play();
+        //     this.clipActionArr.backDoor.reset();
+        //     this.clipActionArr.backDoor.time = 4;
+        //     this.clipActionArr.backDoor.timeScale = -0.01;
+        //     this.clipActionArr.backDoor.clampWhenFinished = true;
+        //     this.clipActionArr.backDoor.play();
+        //     this.frontClock.start();
+        //     this.backClock.start();
+        //   };
+        //   break;
+        default:
+      }
+      handler();
+    }
+    // model.clock.start();
+    // const honglvdeng = group.getObjectByName('honglvdeng');
+    // const material = honglvdeng.material;
+    // setTimeout(() => {
+    //   if (handlerState === 2 || handlerState === 4 || handlerState === 6) {
+    //     material.color = new THREE.Color(0x00ff00);
+    //   } else {
+    //     material.color = new THREE.Color(0xff0000);
+    //   }
+    // }, 1000);
+  }
+
+  mountedThree(playerDom) {
+    this.group = new THREE.Object3D();
+    this.group.name = this.modelName;
+
+    return new Promise((resolve) => {
+      if (!this.model) {
+        resolve(null);
+      }
+      this.model.setGLTFModel('Fm-noStation').then((gltf) => {
+        debugger;
+        const fmModal = gltf[0];
+        fmModal.name = 'Fm-noStation';
+        this.group?.add(fmModal);
+        this.setModalPosition();
+        // 初始化左右摇摆动画;
+        this.initAnimation();
+        this.addLight();
+        this.model.animate();
+        resolve(this.model);
+        this.backDamperOpenMesh = this.group.getObjectByName('Dampler_open_1');
+        if (this.backDamperOpenMesh) this.backDamperOpenMesh.visible = false;
+        this.backDamperClosedMesh = this.group.getObjectByName('Damper_Closed_1');
+        if (this.backDamperClosedMesh) this.backDamperClosedMesh.visible = true;
+
+        this.frontDamperOpenMesh = this.group.getObjectByName('Damper_Open_2');
+        if (this.frontDamperOpenMesh) this.frontDamperOpenMesh.visible = false;
+        this.frontDamperClosedMesh = this.group.getObjectByName('Damper_Closed_2');
+        if (this.frontDamperClosedMesh) this.frontDamperClosedMesh.visible = true;
+      });
+    });
+  }
+
+  destroy() {
+    if (this.model) {
+      if (this.mixers) {
+        this.mixers.uncacheClip(this.clipActionArr.frontDoor.getClip());
+        this.mixers.uncacheClip(this.clipActionArr.backDoor.getClip());
+        this.mixers.uncacheAction(this.clipActionArr.frontDoor.getClip(), this.group);
+        this.mixers.uncacheAction(this.clipActionArr.backDoor.getClip(), this.group);
+        this.mixers.uncacheRoot(this.group);
+
+        if (this.model.animations[0]) this.model.animations[0].tracks = [];
+      }
+      this.model.clearGroup(this.group);
+      this.clipActionArr.backDoor = undefined;
+      this.clipActionArr.frontDoor = undefined;
+
+      this.mixers = undefined;
+
+      // document.getElementById('damper3D').parentElement.remove(document.getElementById('damper3D'))
+    }
+  }
+}
+export default FmNoStation;