浏览代码

1. 新增布尔台展会风窗模块

hongrunxia 10 月之前
父节点
当前提交
541bc73480

+ 0 - 21
src/views/vent/monitorManager/compreMonitor/index.vue

@@ -68,27 +68,6 @@ async function getDataSource(t: NodeJS.Timer) {
         const result = await list({ devcode: item.devcode })
         // console.log('综合监测返回数据------>', result, result1, result2)
         let res = result[0]
-
-        // res = {
-        //   "createDt": null,
-        //   "monitDt": null,
-        //   "id": null,
-        //   "stationcode": null,
-        //   "devcode": "14010100313501MN0001053A10",
-        //   "devtypename": null,
-        //   "realvalue": Number(Math.random() * 100.68 + 12).toFixed(2),
-        //   "unit": "%CH4",
-        //   "adddate": "2023-09-08 16:54:19",
-        //   "devaddress": "五盘区一号回风巷瓦斯",
-        //   "uppervalue": null,
-        //   "lowervalue": null,
-        //   "uppervalueAlm": null,
-        //   "lowervalueAlm": null,
-        //   "dataType": "环境瓦斯",
-        //   "dataTypeCode": null,
-        //   "sigType": null,
-        //   "state": "正常"
-        // }
         if (res) {
           switch (item.code) {
             case '1_fs1':

+ 290 - 0
src/views/vent/monitorManager/windowMonitorBet/dandaoFcBet.threejs.ts

@@ -0,0 +1,290 @@
+import * as THREE from 'three';
+
+import { getTextCanvas, renderVideo } from '/@/utils/threejs/util';
+import gsap from 'gsap';
+
+class singleWindowBet {
+  model;
+  modelName = 'ddFcGroup';
+  group: THREE.Object3D = new THREE.Object3D();
+  animationTimer;
+  isLRAnimation = true;
+  direction = 1;
+  windowsActionArr = {
+    frontWindow: [],
+  };
+  player1;
+  player2;
+  playerStartClickTime1 = new Date().getTime();
+  constructor(model) {
+    this.model = model;
+    this.group.name = 'ddFcGroup';
+  }
+  addLight = () => {
+    if (!this.group || !this.group) return;
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
+    directionalLight.position.set(-437, 61, 559);
+    this.group.add(directionalLight);
+  };
+  // 设置模型位置
+  setModalPosition() {
+    this.group?.scale.set(22, 22, 22);
+    this.group?.position.set(-35, 25, 15);
+  }
+
+  addMonitorText(selectData) {
+    if (!this.group) {
+      return;
+    }
+    const textArr = [
+      {
+        text: `远程定量调节自动风窗`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 100,
+        y: 95,
+      },
+      {
+        text: `${selectData.OpenDegree ? '开度值(%)' : selectData.forntArea ? '过风面积(㎡)' : '过风面积(㎡)'}:`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 145,
+      },
+      {
+        text: selectData.OpenDegree
+          ? Number(`${selectData.OpenDegree}`).toFixed(2)
+          : selectData.forntArea
+          ? Number(`${selectData.forntArea}`).toFixed(2)
+          : '-',
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 145,
+      },
+      {
+        text: `${selectData.frontRearDP ? '风窗压差(Pa)' : selectData.windSpeed ? '风速(m/s)' : '通信状态:'}:`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 200,
+      },
+      {
+        text: `${
+          selectData.frontRearDP
+            ? selectData.frontRearDP
+            : selectData.windSpeed
+            ? selectData.windSpeed
+            : selectData.netStatus == '0'
+            ? '断开'
+            : '连接'
+        }`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 200,
+      },
+      {
+        text: `${selectData.fWindowM3 ? '过风量(m³/min)' : '风窗道数'}: `,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 250,
+      },
+      {
+        text: `${selectData.fWindowM3 ? selectData.fWindowM3 : selectData.nwindownum}`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 250,
+      },
+      {
+        text: History_Type['type'] == 'remote' ? `国能神东煤炭集团监制` : '煤炭科学技术研究院有限公司研制',
+        font: 'normal 28px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: History_Type['type'] == 'remote' ? 90 : 30,
+        y: 300,
+      },
+    ];
+    getTextCanvas(750, 546, textArr, '').then((canvas: HTMLCanvasElement) => {
+      const textMap = new THREE.CanvasTexture(canvas); // 关键一步
+      const textMaterial = new THREE.MeshBasicMaterial({
+        // 关于材质并未讲解 实操即可熟悉                 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质.
+        map: textMap, // 设置纹理贴图
+        transparent: true,
+        side: THREE.DoubleSide, // 这里是双面渲染的意思
+      });
+      textMap.dispose();
+      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.0038, 0.004, 0.0038);
+        planeMesh.position.set(4.44, -0.165, -0.46);
+        this.group?.add(planeMesh);
+      }
+    });
+  }
+
+  /* 提取风门序列帧,初始化前后门动画 */
+  initAnimation() {
+    const meshArr01: THREE.Object3D[] = [];
+    this.group?.children.forEach((obj) => {
+      if (obj.type === 'Mesh' && obj.name && obj.name.startsWith('FCshanye')) {
+        obj.rotateOnAxis(new THREE.Vector3(0, 1, 0), 0);
+        meshArr01.push(obj);
+      }
+    });
+    this.windowsActionArr.frontWindow = meshArr01;
+  }
+
+  play(rotationParam, flag) {
+    if (!this.windowsActionArr.frontWindow) {
+      return;
+    }
+    if (flag === 1) {
+      // 前风窗动画
+      this.windowsActionArr.frontWindow.forEach((mesh: THREE.Mesh) => {
+        gsap.to(mesh.rotation, {
+          y: THREE.MathUtils.degToRad(rotationParam.frontDeg1),
+          duration: (1 / 9) * Math.abs(rotationParam.frontDeg1 - mesh.rotation.y),
+          overwrite: true,
+        });
+      });
+    } else if (flag === 0) {
+      ([...this.windowsActionArr.frontWindow] as THREE.Mesh[]).forEach((mesh) => {
+        gsap.to(mesh.rotation, {
+          y: 0,
+          overwrite: true,
+        });
+      });
+    }
+  }
+
+  /* 点击风窗,风窗全屏 */
+  mousedownModel(intersects: THREE.Intersection<THREE.Object3D<THREE.Event>>[]) {
+    this.isLRAnimation = false;
+    if (this.animationTimer) {
+      clearTimeout(this.animationTimer);
+      this.animationTimer = null;
+    }
+    // 判断是否点击到视频
+    intersects.find((intersect) => {
+      const mesh = intersect.object;
+      if (mesh.name === 'player1') {
+        if (new Date().getTime() - this.playerStartClickTime1 < 400) {
+          // 双击,视频放大
+          if (this.player1) {
+            this.player1.requestFullscreen();
+          }
+        }
+        this.playerStartClickTime1 = new Date().getTime();
+        return true;
+      }
+      return false;
+    });
+  }
+
+  mouseUpModel() {
+    // 10s后开始摆动
+    if (!this.animationTimer && !this.isLRAnimation) {
+      this.animationTimer = setTimeout(() => {
+        this.isLRAnimation = true;
+      }, 10000);
+    }
+  }
+
+  /* 风门动画 */
+  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;
+      }
+    }
+  }
+
+  async initCamera(dom1?) {
+    const videoPlayer1 = dom1;
+    let monitorPlane: THREE.Mesh | null = null;
+    const canvas = await getTextCanvas(320, 180, '', 'noSinge.png');
+    const textMap = new THREE.CanvasTexture(canvas); // 关键一步
+    const textMaterial = new THREE.MeshBasicMaterial({
+      map: textMap, // 设置纹理贴图
+      transparent: true,
+      side: THREE.DoubleSide, // 这里是双面渲染的意思
+    });
+    textMaterial.blending = THREE.CustomBlending;
+    monitorPlane = this.group?.getObjectByName('noPlayer');
+    if (monitorPlane) {
+      monitorPlane.material = textMaterial;
+    } else {
+      const planeGeometry = new THREE.PlaneGeometry(100, 100); // 平面3维几何体PlaneGeometry
+      monitorPlane = new THREE.Mesh(planeGeometry, textMaterial);
+      textMaterial.dispose();
+      planeGeometry.dispose();
+    }
+    const videoPlayer = this.group.getObjectByName('player1');
+    if (videoPlayer) {
+      this.model.clearMesh(videoPlayer);
+      this.group.remove(videoPlayer);
+    }
+    const noPlayer1 = this.group.getObjectByName('noPlayer1');
+    if (noPlayer1) {
+      this.model.clearMesh(noPlayer1);
+      this.group.remove(noPlayer1);
+    }
+    if (!videoPlayer1 && videoPlayer1 === null) {
+      monitorPlane.name = 'noPlayer1';
+      monitorPlane.scale.set(0.015, 0.007, 0.011);
+      monitorPlane.position.set(4.04, 0.02, -0.46);
+      this.group?.add(monitorPlane);
+    } else if (videoPlayer1) {
+      const mesh = renderVideo(this.group, videoPlayer1, 'player1');
+      if (mesh) {
+        mesh?.scale.set(-0.038, 0.029, 1);
+        mesh?.position.set(-4.302, 0.15, -0.23);
+        mesh.rotation.y = -Math.PI;
+        this.group.add(mesh);
+      }
+    }
+  }
+
+  mountedThree() {
+    return new Promise((resolve) => {
+      this.model.setGLTFModel(['ddFc-bet'], this.group).then(() => {
+        this.setModalPosition();
+        this.initAnimation();
+        this.addLight();
+        resolve(null);
+      });
+    });
+  }
+
+  destroy() {
+    this.model.clearGroup(this.group);
+    this.windowsActionArr.frontWindow = undefined;
+    this.model = null;
+    this.group = null;
+  }
+}
+export default singleWindowBet;

+ 313 - 0
src/views/vent/monitorManager/windowMonitorBet/dandaoFcBetZh.threejs.ts

@@ -0,0 +1,313 @@
+import * as THREE from 'three';
+
+import { getTextCanvas, renderVideo } from '/@/utils/threejs/util';
+import gsap from 'gsap';
+
+class singleWindowBetZh {
+  model;
+  modelName = 'ddFcGroup';
+  group: THREE.Object3D = new THREE.Object3D();
+  animationTimer;
+  isLRAnimation = true;
+  direction = 1;
+  windowsActionArr = {
+    frontWindow: [],
+  };
+  lineLight;
+  player1;
+  player2;
+  playerStartClickTime1 = new Date().getTime();
+  constructor(model) {
+    this.model = model;
+    this.group.name = 'ddFcGroup';
+  }
+  addLight = () => {
+    if (!this.group || !this.group) return;
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
+    directionalLight.position.set(-437, 61, 559);
+    this.group.add(directionalLight);
+  };
+  // 设置模型位置
+  setModalPosition() {
+    this.group?.scale.set(22, 22, 22);
+    this.group?.position.set(-35, 25, 15);
+  }
+
+  addMonitorText(selectData) {
+    if (!this.group) {
+      return;
+    }
+    const textArr = [
+      {
+        text: `远程定量调节自动风窗`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 100,
+        y: 95,
+      },
+      {
+        text: `${selectData.OpenDegree ? '开度值(%)' : selectData.forntArea ? '过风面积(㎡)' : '过风面积(㎡)'}:`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 145,
+      },
+      {
+        text: selectData.OpenDegree
+          ? Number(`${selectData.OpenDegree}`).toFixed(2)
+          : selectData.forntArea
+          ? Number(`${selectData.forntArea}`).toFixed(2)
+          : '-',
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 145,
+      },
+      {
+        text: `${selectData.frontRearDP ? '风窗压差(Pa)' : selectData.windSpeed ? '风速(m/s)' : '通信状态:'}:`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 200,
+      },
+      {
+        text: `${
+          selectData.frontRearDP
+            ? selectData.frontRearDP
+            : selectData.windSpeed
+            ? selectData.windSpeed
+            : selectData.netStatus == '0'
+            ? '断开'
+            : '连接'
+        }`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 200,
+      },
+      {
+        text: `${selectData.fWindowM3 ? '过风量(m³/min)' : '风窗道数'}: `,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 5,
+        y: 250,
+      },
+      {
+        text: `${selectData.fWindowM3 ? selectData.fWindowM3 : selectData.nwindownum}`,
+        font: 'normal 30px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: 330,
+        y: 250,
+      },
+      {
+        text: History_Type['type'] == 'remote' ? `国能神东煤炭集团监制` : '煤炭科学技术研究院有限公司研制',
+        font: 'normal 28px Arial',
+        color: '#009900',
+        strokeStyle: '#002200',
+        x: History_Type['type'] == 'remote' ? 90 : 30,
+        y: 300,
+      },
+    ];
+    getTextCanvas(750, 546, textArr, '').then((canvas: HTMLCanvasElement) => {
+      const textMap = new THREE.CanvasTexture(canvas); // 关键一步
+      const textMaterial = new THREE.MeshBasicMaterial({
+        // 关于材质并未讲解 实操即可熟悉                 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质.
+        map: textMap, // 设置纹理贴图
+        transparent: true,
+        side: THREE.DoubleSide, // 这里是双面渲染的意思
+      });
+      textMap.dispose();
+      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.0038, 0.004, 0.0038);
+        planeMesh.position.set(0.31, -0.165, -0.46);
+        this.group?.add(planeMesh);
+      }
+    });
+  }
+
+  /* 提取风门序列帧,初始化前后门动画 */
+  initAnimation() {
+    const meshArr01: THREE.Object3D[] = [];
+    const fcObject = this.group.getObjectByName('ddFc-bet-zh');
+    if (fcObject) {
+      fcObject?.children.forEach((obj) => {
+        if (obj.type === 'Mesh' && obj.name && obj.name.startsWith('FCshanye')) {
+          obj.rotateOnAxis(new THREE.Vector3(0, 1, 0), 90);
+          meshArr01.push(obj);
+        }
+      });
+      this.windowsActionArr.frontWindow = meshArr01;
+    }
+
+    const line = fcObject?.getObjectByName('Box562');
+
+    if (line) {
+      line.material.side = THREE.DoubleSide;
+      line.material.opacity = 1;
+      line.material.depthWrite = true;
+
+      this.lineLight = gsap.to(line.material, {
+        opacity: 0,
+        duration: 0.3,
+        ease: 'easeQutQuad',
+        repeat: -1,
+        yoyo: true,
+        paused: true,
+      });
+      this.lineLight.play();
+    }
+  }
+
+  play(rotationParam, flag) {
+    if (!this.windowsActionArr.frontWindow) {
+      return;
+    }
+    console.log(rotationParam);
+    if (flag === 1) {
+      // 前风窗动画
+      this.windowsActionArr.frontWindow.forEach((mesh: THREE.Mesh) => {
+        gsap.to(mesh.rotation, {
+          y: THREE.MathUtils.degToRad(90 - rotationParam.frontDeg1),
+          duration: (1 / 9) * Math.abs(rotationParam.frontDeg1 - mesh.rotation.y),
+          overwrite: true,
+        });
+      });
+    } else if (flag === 0) {
+      ([...this.windowsActionArr.frontWindow] as THREE.Mesh[]).forEach((mesh) => {
+        gsap.to(mesh.rotation, {
+          y: 0,
+          overwrite: true,
+        });
+      });
+    }
+  }
+
+  /* 点击风窗,风窗全屏 */
+  mousedownModel(intersects: THREE.Intersection<THREE.Object3D<THREE.Event>>[]) {
+    this.isLRAnimation = false;
+    if (this.animationTimer) {
+      clearTimeout(this.animationTimer);
+      this.animationTimer = null;
+    }
+    // 判断是否点击到视频
+    intersects.find((intersect) => {
+      const mesh = intersect.object;
+      if (mesh.name === 'player1') {
+        if (new Date().getTime() - this.playerStartClickTime1 < 400) {
+          // 双击,视频放大
+          if (this.player1) {
+            this.player1.requestFullscreen();
+          }
+        }
+        this.playerStartClickTime1 = new Date().getTime();
+        return true;
+      }
+      return false;
+    });
+  }
+
+  mouseUpModel() {
+    // 10s后开始摆动
+    if (!this.animationTimer && !this.isLRAnimation) {
+      this.animationTimer = setTimeout(() => {
+        this.isLRAnimation = true;
+      }, 10000);
+    }
+  }
+
+  /* 风门动画 */
+  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;
+      }
+    }
+  }
+
+  async initCamera(dom1?) {
+    const videoPlayer1 = dom1;
+    let monitorPlane: THREE.Mesh | null = null;
+    const canvas = await getTextCanvas(320, 180, '', 'noSinge.png');
+    const textMap = new THREE.CanvasTexture(canvas); // 关键一步
+    const textMaterial = new THREE.MeshBasicMaterial({
+      map: textMap, // 设置纹理贴图
+      transparent: true,
+      side: THREE.DoubleSide, // 这里是双面渲染的意思
+    });
+    textMaterial.blending = THREE.CustomBlending;
+    monitorPlane = this.group?.getObjectByName('noPlayer');
+    if (monitorPlane) {
+      monitorPlane.material = textMaterial;
+    } else {
+      const planeGeometry = new THREE.PlaneGeometry(100, 100); // 平面3维几何体PlaneGeometry
+      monitorPlane = new THREE.Mesh(planeGeometry, textMaterial);
+      textMaterial.dispose();
+      planeGeometry.dispose();
+    }
+    const videoPlayer = this.group.getObjectByName('player1');
+    if (videoPlayer) {
+      this.model.clearMesh(videoPlayer);
+      this.group.remove(videoPlayer);
+    }
+    const noPlayer1 = this.group.getObjectByName('noPlayer1');
+    if (noPlayer1) {
+      this.model.clearMesh(noPlayer1);
+      this.group.remove(noPlayer1);
+    }
+    if (!videoPlayer1 && videoPlayer1 === null) {
+      monitorPlane.name = 'noPlayer1';
+      monitorPlane.scale.set(0.015, 0.007, 0.011);
+      monitorPlane.position.set(4.04, 0.02, -0.46);
+      this.group?.add(monitorPlane);
+    } else if (videoPlayer1) {
+      const mesh = renderVideo(this.group, videoPlayer1, 'player1');
+      if (mesh) {
+        mesh?.scale.set(-0.038, 0.029, 1);
+        mesh?.position.set(-4.302, 0.15, -0.23);
+        mesh.rotation.y = -Math.PI;
+        this.group.add(mesh);
+      }
+    }
+  }
+
+  mountedThree() {
+    return new Promise((resolve) => {
+      this.model.setGLTFModel(['ddFc-bet-zh'], this.group).then(() => {
+        this.setModalPosition();
+        this.initAnimation();
+        this.addLight();
+        resolve(null);
+      });
+    });
+  }
+
+  destroy() {
+    this.model.clearGroup(this.group);
+    this.windowsActionArr.frontWindow = undefined;
+    this.model = null;
+    this.group = null;
+  }
+}
+export default singleWindowBetZh;

+ 493 - 0
src/views/vent/monitorManager/windowMonitorBet/index.vue

@@ -0,0 +1,493 @@
+<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="window3D" v-show="!loading" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
+    <!-- <div id="damper3DCSS" v-show="!loading" style="width: 100%; height: 100%; top:0; left: 0; position: absolute; overflow: hidden;">
+      <div>
+        <div ref="elementContent" class="elementContent">
+          <p><span class="data-title">压力(Pa):</span>{{selectData.frontRearDP}}</p>
+          <p><span class="data-title">动力源压力(MPa):</span>{{selectData.sourcePressure}}</p>
+          <p><span class="data-title">故障诊断:</span>
+            <i
+              :class="{'state-icon': true, 'open': selectData.messageBoxStatus, 'close': !selectData.messageBoxStatus}"
+            ></i>{{selectData.fault}}</p>
+        </div>
+      </div>
+    </div> -->
+  </div>
+  <div class="scene-box">
+    <div class="top-box">
+      <div class="top-center">
+        <!-- <div class="input-box">
+          <span class="input-title">风窗面积:</span>
+          <a-input-number size="small" placeholder="0" :min="0" :max="90" :step="1" v-model:value="windowAngle" />
+        </div> -->
+        <div class="row" v-if="hasPermission('window:control') && selectData.nwindownum > 1">
+          <div class="button-box" @click="setArea(1)">设定前窗面积</div>
+          <div class="button-box" @click="setArea(2)">设定后窗面积</div>
+        </div>
+        <div class="row" v-if="hasPermission('window:control') && selectData.nwindownum == 1">
+          <!-- <div class="button-box" @click="setArea(1)">设定风窗面积</div> -->
+          <div class="button-box" @click="setWind()">设置过风量</div>
+        </div>
+      </div>
+      <div class="top-right row">
+        <!-- <div class="control-type row">
+          <div class="control-title">控制模式:</div>
+          <a-radio-group v-model:value="controlType">
+            <a-radio :value="1">就地</a-radio>
+            <a-radio :value="2">远程</a-radio>
+          </a-radio-group>
+        </div> -->
+        <!-- <div class="run-type row">
+          <div class="control-title">运行状态:</div>
+          <a-radio-group v-model:value="controlType">
+            <a-radio :value="1">检修</a-radio>
+          </a-radio-group>
+        </div>
+        <div class="run-state row">
+          <div class="control-title">网络状态:</div>
+          <a-radio-group v-model:value="controlType">
+            <a-radio :value="1">运行</a-radio>
+          </a-radio-group>
+        </div> -->
+      </div>
+    </div>
+    <div class="title-text">
+      {{ 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"
+              columnsType="wintest_monitor"
+              :dataSource="dataSource"
+              @selectRow="getSelectRow"
+              design-scope="wintest-monitor"
+              :scroll="{ y: scroll.y - 40 }"
+              title="风窗监测"
+              :isShowPagination="true"
+              :isShowActionColumn="true"
+            >
+              <template #filterCell="{ column, record }">
+                <a-tag v-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' ? 'default' : 'green'">{{
+                  record.netStatus == '0' ? '断开' : '连接'
+                }}</a-tag>
+                <div v-if="record.nwindownum == 1 && column.dataIndex === 'rearArea'">/</div>
+              </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="2" tab="实时曲线图" force-render>
+            <div class="tab-item" v-if="activeKey === '2'">
+              <DeviceEcharts
+                chartsColumnsType="window_chart"
+                xAxisPropType="strname"
+                :dataSource="dataSource"
+                height="100%"
+                :chartsColumns="chartsColumns"
+                :device-list-api="baseList"
+                device-type="window"
+              />
+            </div>
+          </a-tab-pane> -->
+          <a-tab-pane key="3" tab="历史数据">
+            <div class="tab-item" v-if="activeKey === '3'">
+              <HistoryTable columns-type="window" device-type="window" designScope="window-history" :scroll="scroll">
+                <template #filterCell="{ column, record }">
+                  <a-tag v-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' ? 'default' : 'green'">{{
+                    record.netStatus == '0' ? '断开' : '连接'
+                  }}</a-tag>
+                  <div v-if="record.nwindownum == 1 && column.dataIndex === 'rearArea'">/</div>
+                </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="window" :device-list-api="baseList" designScope="alarm-history" :scroll="scroll" />
+            </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="window"
+                :device-list-api="baseList"
+                designScope="alarm-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: 10px; width: 300px; height: 280px; margin: auto"></div>
+  <LivePlayer
+    id="fc-player1"
+    style="height: 220px; width: 300px; position: absolute; top: 0px; z-index: -1"
+    ref="player1"
+    :videoUrl="flvURL1()"
+    muted
+    loading
+    autoplay
+    controls
+    loop
+    fluent
+  />
+  <HandleModal :modal-is-show="modalIsShow" :modal-title="modalTitle" :modal-type="modalType" @handle-ok="handleOK" @handle-cancel="handleCancel" />
+  <DeviceBaseInfo @register="regModal" :device-type="selectData['deviceType']" />
+</template>
+
+<script setup lang="ts">
+  import { message } from 'ant-design-vue';
+  import DeviceEcharts from '../comment/DeviceEcharts.vue';
+  import { onBeforeMount, ref, onMounted, onUnmounted, reactive, toRaw, watch, nextTick, inject } 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, destroy, addMonitorText, computePlay, setModelType, initCameraCanvas } from './window.threejs';
+  import { list, getTableList, cameraList, cameraAddrList } from './window.api';
+  import { list as baseList } from '../../deviceManager/windWindowTabel/ventanalyWindow.api';
+  import { chartsColumns } from './window.data';
+  import { deviceControlApi } from '/@/api/vent/index';
+  import lodash from 'lodash';
+  import { setDivHeight } from '/@/utils/event';
+  import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
+  import { useRouter } from 'vue-router';
+  import LivePlayer from '@liveqing/liveplayer-v3';
+  import { useModal } from '/@/components/Modal';
+  import { useCamera } from '/@/hooks/system/useCamera';
+  import { usePermission } from '/@/hooks/web/usePermission';
+  const { hasPermission } = usePermission();
+
+  const globalConfig = inject('globalConfig');
+
+  const { currentRoute } = useRouter();
+
+  const MonitorDataTable = ref();
+
+  const playerRef = ref();
+  const scroll = reactive({
+    y: 230,
+  });
+
+  const modalIsShow = ref<boolean>(false); // 是否显示模态框
+  const modalTitle = ref(''); // 模态框标题显示内容,根据设备操作类型决定
+  const modalType = ref(''); // 模态框内容显示类型,设备操作类型
+
+  const deviceBaseList = ref([]);
+  const activeKey = ref('1');
+  const loading = ref(false);
+  const windowAngle = ref(0);
+  const windowM3 = ref(0);
+
+  const windowM3Unit = ref(0);
+  const windowAngleUnit = ref(0);
+
+  // const rotationParam = {
+  //   frontDeg0: 0, // 前门初始
+  //   frontDeg1: windowAngle.value, // 前门目标
+  //   backDeg0: 0, // 后门初始
+  //   backDeg1: windowAngle.value, // 后门目标
+  // };
+
+  // 默认初始是第一行
+  const selectRowIndex = ref(-1);
+  const dataSource = ref([]);
+
+  // 设备数据
+  const controlType = ref(1);
+
+  const flvURL1 = () => {
+    return `/video/window.mp4`;
+  };
+  const [regModal, { openModal }] = useModal();
+  const { getCamera, removeCamera } = useCamera();
+
+  const tabChange = (activeKeyVal) => {
+    activeKey.value = activeKeyVal;
+    if (activeKeyVal == 1) {
+      nextTick(() => {
+        MonitorDataTable.value.setSelectedRowKeys([selectData.deviceID]);
+      });
+    }
+  };
+
+  const initData = {
+    deviceID: '',
+    deviceType: '',
+    strname: '',
+    dataDh: '-', //压差
+    dataDtestq: '-', //测试风量
+    sourcePressure: '-', //气源压力
+    dataDequivalarea: '-',
+    netStatus: '0', //通信状态
+    fault: '气源压力超限',
+    forntArea: '0',
+    rearArea: '0',
+    frontRearDifference: '-',
+    rearPresentValue: '-',
+    maxarea: 0,
+    nwindownum: 0,
+  };
+
+  // 监测数据
+  const selectData = reactive(lodash.cloneDeep(initData));
+
+  // https获取监测数据
+  let timer: null | NodeJS.Timeout = null;
+  const getMonitor = (flag?) => {
+    if (Object.prototype.toString.call(timer) === '[object Null]') {
+      timer = setTimeout(
+        async () => {
+          const data = await getDataSource();
+          Object.assign(selectData, data);
+          playAnimation(selectData, selectData.maxarea);
+          addMonitorText(selectData);
+          if (timer) {
+            timer = null;
+          }
+          getMonitor();
+        },
+        flag ? 0 : 2000
+      );
+    }
+  };
+
+  const getDataSource = async () => {
+    const res = await list({ devicetype: 'wintest', pagetype: 'normal' });
+    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) {
+      // 初始打开页面
+      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']]);
+      }
+    }
+
+    const data: any = toRaw(dataSource.value[selectRowIndex.value]); //maxarea
+    if (n < 8 && data && data.OpenDegree && n > 0) {
+      data.OpenDegree = Math.round(Number(windowAngleUnit.value) * ++n + Number(data.OpenDegree));
+    } else {
+      if (windowAngle.value && windowAngle.value != data.OpenDegree) {
+        data.OpenDegree = Math.round(windowAngle.value);
+      }
+    }
+
+    if (n < 13 && data && data.fWindowM3 && n > 0) {
+      data.fWindowM3 = (Number(windowM3Unit.value) * ++n + Number(data.fWindowM3)).toFixed(2);
+    } else {
+      if (windowM3.value && windowM3.value != data.fWindowM3) {
+        data.fWindowM3 = windowM3.value;
+      }
+    }
+    return data;
+  };
+
+  // 获取设备基本信息列表
+  const getDeviceBaseList = () => {
+    getTableList({ pageSize: 1000 }).then((res) => {
+      deviceBaseList.value = res.records;
+    });
+  };
+
+  // 切换检测数据
+  const getSelectRow = async (selectRow, index) => {
+    if (!selectRow) return;
+    selectRowIndex.value = index;
+    loading.value = true;
+    const baseData: any = deviceBaseList.value.find((baseData: any) => baseData.id === selectRow.deviceID);
+    Object.assign(selectData, initData, selectRow, baseData);
+
+    // const type = selectData.nwindownum == 1 ? 'singleXkWindow' : 'doubleWindow';
+    const type = selectData.nwindownum == 1 ? 'singleWindow' : 'doubleWindow';
+    setModelType(type).then(() => {
+      addMonitorText(selectData);
+      playAnimation(selectRow, selectData.maxarea, true);
+      loading.value = false;
+    });
+    await getCamera(selectRow.deviceID, playerRef.value);
+  };
+
+  // 判断前后窗的面积是否发生改变,如果改变则开启动画
+  const playAnimation = (data, maxarea, isFirst = false) => {
+    computePlay(data, maxarea, isFirst);
+  };
+
+  // 设置风窗面积
+  const setArea = (flag) => {
+    if (selectData.nwindownum == 2) {
+      modalTitle.value = flag === 1 ? '设定前窗面积' : '设定后窗面积';
+    } else {
+      modalTitle.value = '设定风窗面积';
+    }
+
+    modalType.value = flag + '';
+    modalIsShow.value = true;
+  };
+
+  const setWind = () => {
+    modalTitle.value = '设定风窗过风量';
+    modalType.value = '1';
+    modalIsShow.value = true;
+  };
+  let n = 0;
+  const handleOK = (passWord, handlerState, m3) => {
+    // 先调整打开角度
+    if (m3 < 1428) {
+      windowAngle.value = (72 / 1428) * m3;
+    } else if (m3 >= 1428 && m3 < 1448) {
+      windowAngle.value = 74;
+    } else if (m3 >= 1448 && m3 < 1467) {
+      windowAngle.value = 75;
+    } else if (m3 >= 1467 && m3 <= 1480) {
+      windowAngle.value = 76;
+    } else if (m3 >= 1480 && m3 <= 1504) {
+      windowAngle.value = 80;
+    } else if (m3 >= 1504 && m3 <= 1575) {
+      windowAngle.value = 85;
+    } else if (m3 >= 1575 && m3 <= 1640) {
+      windowAngle.value = 90;
+    } else if (m3 > 1640) {
+      windowAngle.value = (90 / 1640) * m3;
+    }
+
+    const data = {
+      deviceid: selectData.deviceID,
+      devicetype: selectData.deviceType,
+      paramcode: 'OpenDegreeSet',
+      password: passWord || globalConfig?.simulatedPassword,
+      value: Math.round(windowAngle.value),
+    };
+    message.success('指令已下发至生产管控平台成功!');
+    handleCancel();
+    windowAngleUnit.value = selectData.OpenDegree ? (windowAngle.value - selectData.OpenDegree) / 8 : 0;
+    windowM3Unit.value = selectData.fWindowM3 ? (m3 - selectData.fWindowM3) / 13 : 0;
+    n = 1;
+    windowM3.value = m3;
+    setTimeout(() => {
+      deviceControlApi(data)
+        .then((result) => {
+          if (result && result.code == 500) {
+            message.error(result.message);
+          } else {
+            // if (globalConfig.History_Type == 'remote') {
+            //   message.success('指令已下发至生产管控平台成功!');
+            // } else {
+            //   message.success('指令已下发成功!');
+            // }
+            setTimeout(() => {
+              const data1 = {
+                deviceid: selectData.deviceID,
+                devicetype: selectData.deviceType,
+                paramcode: 'ctrlm3',
+                password: passWord || globalConfig?.simulatedPassword,
+                value: m3,
+              };
+              deviceControlApi(data1).then((result) => {
+                if (result && result.code == 500) {
+                  message.error(result.message);
+                } else {
+                }
+
+                n = 0;
+              });
+            }, 5000);
+          }
+        })
+        .catch(() => {})
+        .finally(() => {});
+    }, 8000);
+    setTimeout(() => {
+      windowAngle.value = 0;
+      windowM3.value = 0;
+    }, 30000);
+  };
+
+  const handleCancel = () => {
+    modalIsShow.value = false;
+    modalTitle.value = '';
+    modalType.value = '';
+  };
+
+  function deviceEdit(e: Event, type: string, record) {
+    e.stopPropagation();
+    openModal(true, {
+      type,
+      deviceId: record['deviceID'],
+    });
+  }
+
+  onBeforeMount(() => {
+    // const sendVal = JSON.stringify({ pagetype: 'normal', devicetype: 'window', orgcode: '', ids: '', systemID: '' });
+    // initWebSocket(sendVal);
+    getDeviceBaseList();
+  });
+
+  onMounted(() => {
+    loading.value = true;
+    const playerDom = document.getElementById('fc-player1')?.getElementsByClassName('vjs-tech')[0];
+    mountedThree(playerDom).then(async () => {
+      // await setModelType('singleWindow');
+      getMonitor(true);
+      loading.value = false;
+      addMonitorText(selectData);
+    });
+  });
+  onUnmounted(() => {
+    destroy();
+    removeCamera();
+    if (timer) {
+      clearTimeout(timer);
+      timer = undefined;
+    }
+  });
+</script>
+<style lang="less" scoped>
+  @import '/@/design/vent/modal.less';
+  @ventSpace: zxm;
+
+  // :deep(.@{ventSpace}-tabs-tabpane-active) {
+  //   height: 100%;
+  // }
+  .input-box {
+    display: flex;
+    align-items: center;
+    padding-left: 10px;
+    .input-title {
+      color: #73e8fe;
+      width: auto;
+    }
+    .@{ventSpace}-input-number {
+      border-color: #ffffff88 !important;
+    }
+    margin-right: 10px;
+  }
+  .scene-box {
+    .bottom-tabs-box {
+      height: 350px;
+    }
+  }
+</style>

+ 78 - 0
src/views/vent/monitorManager/windowMonitorBet/modal.vue

@@ -0,0 +1,78 @@
+<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-number size="small" placeholder="0" :min="0" v-model:value="area" />
+      </div>
+      <div v-if="!globalConfig?.simulatedPassword" 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, inject } from 'vue';
+  import { ExclamationCircleFilled } from '@ant-design/icons-vue';
+
+  const globalConfig = inject('globalConfig');
+
+  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('');
+  const area = ref(0);
+
+  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 = '';
+    area.value = 0;
+  });
+
+  function handleOk() {
+    if (globalConfig?.simulatedPassword) {
+      emit('handleOk', '', type.value, area.value);
+    } else {
+      emit('handleOk', passWord.value, type.value, area.value);
+    }
+  }
+  function handleCancel() {
+    //
+    emit('handleCancel');
+  }
+</script>
+<style scoped lang="less">
+  @ventSpace: zxm;
+
+  .label {
+    width: 110px;
+  }
+  .@{ventSpace}-input,
+  .@{ventSpace}-input-number {
+    width: 150px;
+  }
+</style>

+ 22 - 0
src/views/vent/monitorManager/windowMonitorBet/window.api.ts

@@ -0,0 +1,22 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  list = '/ventanaly-device/monitor/device',
+  baseList = '/safety/ventanalyWindow/list',
+  cameraList = '/safety/ventanalyCamera/list',
+  cameraAddrList = '/ventanaly-device/camera/info',
+}
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.post({ url: Api.list, params });
+
+/**
+ * 保存或者更新用户
+ * @param params
+ */
+export const getTableList = (params) => defHttp.get({ url: Api.baseList, params });
+
+export const cameraList = (params) => defHttp.get({ url: Api.cameraList, params });
+export const cameraAddrList = (params) => defHttp.post({ url: Api.cameraAddrList, params });

+ 304 - 0
src/views/vent/monitorManager/windowMonitorBet/window.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: '(m/min)',
+    ymax: 20,
+    yname: 'm/min',
+    linetype: 'bar',
+    yaxispos: 'left',
+    color: '#37BCF2',
+    sort: 1,
+    xRotate: 0,
+    dataIndex: 'frontPresentValue',
+  },
+  {
+    legend: '后窗风速',
+    seriesName: '',
+    ymax: 50,
+    yname: 'm/min',
+    linetype: 'bar',
+    yaxispos: 'right',
+    color: '#FC4327',
+    sort: 1,
+    xRotate: 0,
+    dataIndex: 'rearPresentValue',
+  },
+];

+ 176 - 0
src/views/vent/monitorManager/windowMonitorBet/window.threejs.ts

@@ -0,0 +1,176 @@
+import * as THREE from 'three';
+import UseThree from '../../../../utils/threejs/useThree';
+import singleWindow from './dandaoFcBetZh.threejs';
+import { animateCamera } from '/@/utils/threejs/util';
+import useEvent from '../../../../utils/threejs/useEvent';
+
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+// 模型对象、 文字对象
+let model: UseThree,
+  singleWindowObj,
+  group: THREE.Object3D,
+  windowType = 'singleWindow';
+
+const rotationParam = {
+  frontDeg0: 0, // 前门初始
+  frontDeg1: 0, // 前门目标
+  backDeg0: 0, // 后门初始
+  backDeg1: 0, // 后门目标
+};
+
+const { mouseDownFn } = useEvent();
+// 打灯光
+const addLight = () => {
+  if (!model || !model.scene) return;
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+  directionalLight.position.set(-110, 150, 647);
+  model.scene?.add(directionalLight);
+  // directionalLight.target = group;
+
+  const pointLight2 = new THREE.PointLight(0xffffff, 1, 150);
+  pointLight2.position.set(-101, 34, 16);
+  pointLight2.shadow.bias = 0.05;
+  model.scene.add(pointLight2);
+
+  const pointLight3 = new THREE.PointLight(0xffffff, 1, 150);
+  pointLight3.position.set(19, 25, -7);
+  pointLight3.shadow.bias = 0.05;
+  model.scene.add(pointLight3);
+
+  const pointLight6 = new THREE.PointLight(0xffffff, 1, 300);
+  pointLight6.position.set(51, 51, 9);
+  pointLight6.shadow.bias = 0.05;
+  model.scene.add(pointLight6);
+};
+
+// 初始化左右摇摆动画
+const startAnimation = () => {
+  // 定义鼠标点击事件
+  model.canvasContainer?.addEventListener('mousedown', mouseEvent.bind(null));
+  model.canvasContainer?.addEventListener('pointerup', (event) => {
+    event.stopPropagation();
+    // 单道、 双道
+    if (windowType === 'singleWindow' && singleWindowObj) {
+      singleWindowObj.mouseUpModel.call(singleWindowObj);
+    }
+  });
+};
+
+// 鼠标点击、松开事件
+const mouseEvent = (event) => {
+  if (event.button == 0) {
+    mouseDownFn(model, group, event, (intersects) => {
+      if (windowType === 'singleWindow' && singleWindowObj) {
+        singleWindowObj.mousedownModel.call(singleWindowObj, intersects);
+      }
+    });
+  }
+};
+
+export const addMonitorText = (selectData) => {
+  if (windowType === 'singleWindow' && singleWindowObj) {
+    return singleWindowObj.addMonitorText.call(singleWindowObj, selectData);
+  }
+};
+
+export function computePlay(data, maxarea, isFirst = false) {
+  if (windowType === 'doubleWindow' || windowType === 'singleWindow') {
+    if (!maxarea) maxarea = 90;
+    rotationParam.frontDeg0 = (90 / maxarea) * Number(isFirst ? 0 : data.forntArea);
+    rotationParam.backDeg0 = (90 / maxarea) * Number(isFirst ? 0 : data.rearArea);
+    rotationParam.frontDeg1 = (90 / maxarea) * Number(data.forntArea) || 0;
+    rotationParam.backDeg1 = (90 / maxarea) * Number(data.rearArea) || 0;
+    if (!rotationParam.frontDeg1 && !rotationParam.backDeg1) {
+      // 当返回值有误时默认关闭
+      play(rotationParam, 0);
+    } else {
+      if (data.nwindownum == 1 || data.nwindownum == 2) {
+        setTimeout(() => {
+          play(rotationParam, 1);
+        }, 0);
+      }
+      if (data.nwindownum == 2) {
+        setTimeout(() => {
+          play(rotationParam, 2);
+        }, 0);
+      }
+    }
+  } else if (windowType === 'singleXkWindow') {
+    const acosToAngle = (cosValue) => {
+      cosValue = Math.max(Math.min(cosValue, 1), -1);
+      // 计算角度
+      return Math.asin(cosValue) * (180 / Math.PI);
+    };
+    const sina = Math.sqrt((Math.sin((78 * Math.PI) / 180) ** 2 * parseFloat(data.forntArea)) / parseFloat(maxarea));
+    const angleInRadians = acosToAngle(sina);
+    rotationParam.frontDeg1 = angleInRadians;
+    if (!rotationParam.frontDeg1 && !rotationParam.backDeg1) {
+      // 当返回值有误时默认关闭
+      play(rotationParam, 0);
+    } else {
+      setTimeout(() => {
+        play(rotationParam, 1);
+      }, 0);
+    }
+  }
+}
+
+export const play = (rotationParam, flag) => {
+  if (windowType === 'singleWindow' && singleWindowObj) {
+    return singleWindowObj.play.call(singleWindowObj, rotationParam, flag);
+  }
+};
+
+// 切换风窗类型
+export const setModelType = (type) => {
+  // if (!model || !model.scene) return;
+
+  windowType = type;
+  return new Promise((resolve) => {
+    // 显示双道风窗
+    if (windowType === 'singleWindow') {
+      // 显示单道风窗
+      model.startAnimation = singleWindowObj.render.bind(singleWindowObj);
+      model.scene?.remove(group);
+      group = singleWindowObj.group;
+      const oldCameraPosition = { x: 100, y: 0, z: 10 };
+      model.scene?.add(singleWindowObj.group);
+      setTimeout(async () => {
+        resolve(null);
+        await animateCamera(oldCameraPosition, { x: 0, y: 0, z: 0 }, { x: 66.257, y: 57.539, z: 94.313 }, { x: 0, y: 0, z: 0 }, model);
+      }, 1000);
+    }
+  });
+};
+
+export const mountedThree = (playerDom) => {
+  return new Promise(async (resolve) => {
+    model = new UseThree('#window3D');
+    if (!model || !model.renderer || !model.camera) return;
+    model.setEnvMap('test1');
+    model.camera.position.set(100, 0, 1000);
+    singleWindowObj = new singleWindow(model);
+    singleWindowObj.mountedThree(playerDom);
+    model.animate();
+    addLight();
+    startAnimation();
+    resolve(null);
+  });
+};
+
+export const destroy = () => {
+  if (model) {
+    model.isRender = false;
+    console.log('场景销毁前信息----------->', model.renderer?.info);
+    model.isRender = false;
+    singleWindowObj.destroy();
+    model.destroy();
+    model = null;
+    group = null;
+    singleWindowObj = null;
+  }
+};