Ver código fonte

[Feat 0000] 添加除尘风机智能管控页面及相关组件

ruienger 3 meses atrás
pai
commit
4da7d02673

+ 35 - 0
src/views/vent/monitorManager/dedustMonitor/components/AlarmHistory.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="alarm-history">
+    <AlarmHistoryTable
+      columns-type="alarm"
+      device-type="sys_surface_juejin"
+      :device-list-api="workFaceDeviceList.bind(null, { id: deviceId })"
+      :list="list"
+      :sys-id="deviceId"
+      designScope="alarm-history"
+    />
+  </div>
+</template>
+<script setup lang="ts">
+  import AlarmHistoryTable from '../../comment/WorkFaceAlarmHistoryTable.vue';
+  import { workFaceDeviceList } from '../../../deviceManager/comment/warningTabel/warning.api';
+  import { defHttp } from '/@/utils/http/axios';
+
+  const props = defineProps({
+    deviceType: {
+      type: String,
+      required: true,
+    },
+    deviceId: {
+      type: String,
+      required: true,
+    },
+  });
+
+  const list = (params) => defHttp.get({ url: '/safety/managesysAutoLog/list', params });
+</script>
+<style lang="less" scoped>
+  .alarm-history {
+    pointer-events: auto;
+  }
+</style>

+ 32 - 0
src/views/vent/monitorManager/dedustMonitor/components/DedustHistory.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="history-box">
+    <HistoryTable
+      :columns-type="`${deviceType}`"
+      :device-type="deviceType"
+      :sysId="deviceId"
+      designScope="pressurefan_history"
+      :scroll="{ y: 650 }"
+    />
+  </div>
+</template>
+<script setup lang="ts">
+  import { ref, defineProps } from 'vue';
+  import HistoryTable from '../../comment/HistoryTable.vue';
+  import { getTableList } from '../dedust.api';
+
+  const props = defineProps({
+    deviceType: {
+      type: String,
+      required: true,
+    },
+    deviceId: {
+      type: String,
+      required: true,
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .history-box {
+    pointer-events: auto;
+  }
+</style>

+ 194 - 0
src/views/vent/monitorManager/dedustMonitor/components/DedustHome.vue

@@ -0,0 +1,194 @@
+<template>
+  <a-spin tip="Loading..." :spinning="loading">
+    <div class="monitor-container">
+      <div class="lr left-box vent-margin-t-10">
+        <ventBox1>
+          <template #title>
+            <div>除尘机状态</div>
+          </template>
+          <template #container>
+            <List layout="double-columns" title="故障状态" v-bind="dedustStatusPropA" />
+            <List title="报警状态" v-bind="dedustStatusPropB" />
+            <List title="激活状态" v-bind="dedustStatusPropC" />
+          </template>
+        </ventBox1>
+      </div>
+      <div class="lr right-box">
+        <ventBox1 class="vent-margin-t-10">
+          <template #title>
+            <div>监测参数</div>
+          </template>
+          <template #container>
+            <List v-bind="dedustMonitorProp" />
+          </template>
+        </ventBox1>
+      </div>
+    </div>
+  </a-spin>
+</template>
+
+<script setup lang="ts">
+  import { onBeforeMount, ref, onMounted, onUnmounted, reactive, defineProps, computed } from 'vue';
+  import { list } from '../dedust.api';
+  import ventBox1 from '/@/components/vent/ventBox1.vue';
+  // import { SvgIcon } from '/@/components/Icon';
+  import {
+    dedustMonitorConfig,
+    dedustStatusConfigA,
+    dedustStatusConfigB,
+    dedustStatusConfigC,
+    statusConfigA,
+    statusConfigB,
+    statusConfigC,
+  } from '../dedust.data';
+  import List from '/@/views/vent/gas/components/list/index.vue';
+
+  const props = defineProps({
+    deviceId: {
+      type: String,
+      require: true,
+    },
+  });
+
+  const loading = ref(false);
+
+  // 默认初始是第一行
+  // const openDust = ref(false);
+  const deviceInfo = ref({});
+  const workFaceSource = ref({});
+  const workFaceHistorySource = ref([]);
+  // const gateDataSource = ref([]);
+  // const windowDataSource = ref([]);
+  // const windDataSource = ref([]);
+  // const temperatureDataSource = ref([]);
+  // const fireDataSource = ref([]);
+
+  // 将列表配置项转换为列表可用的prop
+  function transConfigToProp(config, source) {
+    return config.map((c) => {
+      return {
+        ...c,
+        value: _.get(c.prop, source),
+        label: c.label,
+      };
+    });
+  }
+
+  // 各个模块的配置项
+  const dedustMonitorProp = computed(() => {
+    return {
+      items: transConfigToProp(dedustMonitorConfig, deviceInfo.value),
+    };
+  });
+  const dedustStatusPropA = computed(() => {
+    return {
+      status: statusConfigA as any,
+      items: transConfigToProp(dedustStatusConfigA, deviceInfo.value),
+    };
+  });
+  const dedustStatusPropB = computed(() => {
+    return {
+      status: statusConfigB as any,
+      items: transConfigToProp(dedustStatusConfigB, deviceInfo.value),
+    };
+  });
+  const dedustStatusPropC = computed(() => {
+    return {
+      status: statusConfigC as any,
+      items: transConfigToProp(dedustStatusConfigC, deviceInfo.value),
+    };
+  });
+
+  // 监测数据
+  const selectData = reactive({});
+
+  // https获取监测数据
+  let timer: null | NodeJS.Timeout = null;
+  function getMonitor(flag?) {
+    if (Object.prototype.toString.call(timer) === '[object Null]') {
+      timer = setTimeout(
+        async () => {
+          if (props.deviceId) {
+            const data = await getDataSource(props.deviceId);
+            Object.assign(selectData, data);
+          }
+          if (timer) {
+            timer = null;
+          }
+          await getMonitor();
+          loading.value = false;
+        },
+        flag ? 0 : 1000
+      );
+    }
+  }
+
+  async function getDataSource(systemID) {
+    const res = await list({ devicetype: 'sys', systemID, type: 'all' });
+    deviceInfo.value = res.deviceInfo;
+    workFaceHistorySource.value = res['sysInfo']['history'];
+    workFaceSource.value = Object.assign(res['sysInfo'], res['sysInfo']['readData']);
+    loading.value = false;
+  }
+
+  onBeforeMount(() => {});
+
+  onMounted(async () => {
+    loading.value = true;
+    timer = null;
+    await getMonitor(true);
+  });
+  onUnmounted(() => {
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
+    }
+  });
+</script>
+<style lang="less" scoped>
+  @import '/@/design/vent/modal.less';
+  // @import '../less/tunFace.less';
+  @import '../../comment/less/workFace.less';
+  @ventSpace: zxm;
+
+  .dust-fan-monitor {
+    display: flex;
+    flex-wrap: wrap;
+  }
+  .dust-fan-monitor-item {
+    width: 152px;
+    height: 70px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    border: 1px solid rgba(25, 251, 255, 0.4);
+    box-shadow: inset 0 0 20px rgba(0, 197, 255, 0.4);
+    background: rgba(0, 0, 0, 0.06666667);
+    margin-bottom: 5px;
+    padding: 8px 0;
+    &:nth-child(2n) {
+      margin-left: 12px;
+    }
+    .title {
+      color: #5dfaff;
+    }
+    .unit {
+      font-size: 13px;
+      color: #ffffffaa;
+    }
+    .value {
+      color: #ffb212;
+    }
+  }
+
+  .fault {
+    .title {
+      color: #c4fdff;
+    }
+    .value {
+      // color: #FFB212;
+      color: #61ddb1;
+    }
+  }
+</style>

+ 42 - 0
src/views/vent/monitorManager/dedustMonitor/components/HandleHistory.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="handle-history">
+    <HandlerHistoryTable v-if="refresh" columns-type="operator_history" :device-type="type"
+      :sys-id="deviceId"
+      designScope="juejin_history" :scroll="{ y: 650 }"/>
+  </div>
+</template>
+<script setup lang="ts">
+import { watch, ref, nextTick } from 'vue'
+import HandlerHistoryTable from '../../comment/WorkFaceHandlerHistoryTable.vue';
+
+  const props = defineProps({
+    deviceType: {
+      type: String,
+      required: true,
+    },
+    deviceId: {
+      type: String,
+      required: true,
+    }
+  })
+
+  const type = ref(props.deviceType)
+
+  const refresh = ref(true)
+
+  watch(() => props.deviceType, (newVal) => {
+    type.value = newVal
+    refresh.value = false
+    nextTick(() => {
+      refresh.value = true
+    })
+  })
+
+
+</script>
+<style lang="less" scoped>
+.handle-history {
+  width: 100%;
+  pointer-events: auto;
+}
+</style>

+ 23 - 0
src/views/vent/monitorManager/dedustMonitor/dedust.api.ts

@@ -0,0 +1,23 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  systemList = '/monitor/device',
+  list = '/monitor/system',
+  baseList = '/safety/ventanalyManageSystem/list',
+  deviceList = '/safety/ventanalyDeviceInfo/list',
+}
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.post({ url: Api.list, params });
+
+export const systemList = (params) => defHttp.post({ url: Api.systemList, params });
+
+/**
+ * 保存或者更新用户
+ * @param params
+ */
+export const getTableList = (params) => defHttp.get({ url: Api.baseList, params });
+
+export const getDeviceList = (params) => defHttp.get({ url: Api.deviceList, params });

+ 137 - 0
src/views/vent/monitorManager/dedustMonitor/dedust.data.ts

@@ -0,0 +1,137 @@
+/** 故障相关状态配置 */
+export const statusConfigA = [{ value: 1, label: '故障' }, { label: '正常' }];
+/** 报警相关状态配置 */
+export const statusConfigB = [{ value: 1, label: '报警' }, { label: '正常' }];
+/** 激活与否相关状态配置 */
+export const statusConfigC = [{ value: 1, label: '激活' }, { label: '接触' }];
+
+export const dedustMonitorConfig = [
+  // {
+  //   prop: 'FrequencySetPointZD',
+  //   label: '最低频率设置',
+  // },
+  // {
+  //   prop: 'FrequencySetPointYTW',
+  //   label: '以太网频率比率',
+  // },
+  // {
+  //   prop: 'FrequencySetPoint',
+  //   label: '给定频率',
+  // },
+  {
+    prop: 'Frequency',
+    label: '运行频率',
+  },
+  {
+    prop: 'BusVoltage',
+    label: '母线电压',
+  },
+  {
+    prop: 'OutputVoltage',
+    label: '输出电压',
+  },
+  {
+    prop: 'Outputcurrent',
+    label: '输出电流',
+  },
+  {
+    prop: 'OutputPower',
+    label: '输出功率',
+  },
+  {
+    prop: 'Current1',
+    label: '前机电流',
+  },
+  {
+    prop: 'Current2',
+    label: '后机电流',
+  },
+];
+
+/** 故障相关配置 */
+export const dedustStatusConfigA = [
+  {
+    prop: 'NotRunningFault',
+    label: '正常运行',
+  },
+  {
+    prop: 'EquipmentFault',
+    label: '设备故障',
+  },
+  {
+    prop: 'FaultStatus',
+    label: '故障状态',
+  },
+  {
+    prop: 'PowerFault',
+    label: '电源故障',
+  },
+  {
+    prop: 'ContorlPSError',
+    label: '控制电源故障',
+  },
+  {
+    prop: 'VFDFault',
+    label: '变频器故障',
+  },
+  {
+    prop: 'KMFault',
+    label: '电源接触器故障',
+  },
+  {
+    prop: 'CoolFanNotReady1',
+    label: '散热风机跳闸',
+  },
+  {
+    prop: 'CoolFanNotReady2',
+    label: '散热接触器故障',
+  },
+];
+
+/** 报警相关配置 */
+export const dedustStatusConfigB = [
+  {
+    prop: 'Alarmstatus',
+    label: '报警状态',
+  },
+  {
+    prop: 'OverLoadAlarm1',
+    label: '前电机过载',
+  },
+  {
+    prop: 'OverLoadAlarm2',
+    label: '后电机过载',
+  },
+];
+
+/** 激活与否相关配置 */
+export const dedustStatusConfigC = [
+  {
+    prop: 'HMIReset',
+    label: '故障复位',
+  },
+  {
+    prop: 'SwitchOn',
+    label: '启动控制',
+  },
+  {
+    prop: 'SwitchOff',
+    label: '停止控制',
+  },
+];
+
+// export const dedustStatusConfigD = [
+//   {
+//     prop: 'GasLock',
+//     label: '瓦斯闭锁',
+//     status: [{ label: '' }]
+//   },
+//   {
+//     prop: 'SwitchOn',
+//     label: '启动控制',
+//   },
+//   {
+//     prop: 'SwitchOff',
+//     label: '停止控制',
+//   },
+// ];

+ 116 - 0
src/views/vent/monitorManager/dedustMonitor/dedust.threejs.base.ts

@@ -0,0 +1,116 @@
+import * as THREE from 'three';
+// import { setModalCenter } from '/@/utils/threejs/util';
+import Smoke from '/@/views/vent/comment/threejs/Smoke';
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+class WorkFace {
+  model;
+  modelName = 'tunFace';
+  group: THREE.Object3D | null = null;
+  inSmoke: Smoke | null = null;
+  outSmoke: Smoke | null = null;
+
+  constructor(model) {
+    this.model = model;
+  }
+
+  addLight() {
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
+    directionalLight.position.set(6.3, 28, 20);
+    this.group?.add(directionalLight);
+    directionalLight.target = this.group as THREE.Object3D;
+
+    const pointLight = new THREE.PointLight(0xffffff, 1, 1000);
+    pointLight.position.set(45, 51, -4.1);
+    pointLight.shadow.bias = 0.05;
+    this.model.scene.add(pointLight);
+
+    // gui.add(directionalLight.position, 'x', -100, 100);
+    // gui.add(directionalLight.position, 'y', -100, 100);
+    // gui.add(directionalLight.position, 'z', -100, 100);
+  }
+
+  addChamberText() {
+    //
+  }
+
+  initFly = async () => {
+    const inCurve = [
+      {
+        path0: new THREE.Vector3(-15.134, 3.734, -3.485),
+        path1: new THREE.Vector3(-17.13, 3.734, -3.485),
+        isSpread: false,
+        spreadDirection: 0,
+      },
+      {
+        path0: new THREE.Vector3(-17.13, 3.734, -3.485),
+        path1: new THREE.Vector3(-19.518, 3.075, 2.071),
+        isSpread: true,
+        spreadDirection: 1, // 1是由小变大,-1是由大变小
+      },
+    ];
+
+    const outCurve = [
+      {
+        path0: new THREE.Vector3(-31.51, 3.075, 2.071),
+        path1: new THREE.Vector3(-26.51, 3.075, 2.071),
+        isSpread: true,
+        spreadDirection: -1,
+      },
+      {
+        path0: new THREE.Vector3(-26.51, 3.075, 2.071),
+        path1: new THREE.Vector3(-24.514, 3.075, 2.071),
+        isSpread: false,
+        spreadDirection: 0, // 1是由小变大,-1是由大变小
+      },
+    ];
+
+    if (!this.inSmoke) {
+      this.inSmoke = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.5, 0.5, 30);
+      this.inSmoke.setPath(inCurve);
+      await this.inSmoke.setPoints();
+      this.group?.add(this.inSmoke.points);
+    }
+    if (!this.outSmoke) {
+      this.outSmoke = new Smoke('/model/img/texture-smoke.png', '#333333', 0, 1, 0.8, 20);
+      this.outSmoke.setPath(outCurve);
+      await this.outSmoke.setPoints();
+      this.group?.add(this.outSmoke.points);
+    }
+
+    this.inSmoke.startSmoke(1);
+    this.outSmoke.startSmoke(1);
+  };
+
+  clearFly = () => {
+    if (this.inSmoke) this.inSmoke.clearSmoke();
+    if (this.outSmoke) this.outSmoke.clearSmoke();
+  };
+
+  mountedThree() {
+    return new Promise((resolve) => {
+      this.model.setGLTFModel([this.modelName]).then(async (gltf) => {
+        this.group = gltf[0];
+        if (this.group) {
+          resolve(null);
+          this.addLight();
+          await this.initFly();
+        }
+      });
+    });
+  }
+
+  destroy() {
+    if (this.model) {
+      this.clearFly();
+      this.model.clearGroup(this.group);
+      this.model = null;
+      this.group = null;
+      this.inSmoke = null;
+      this.outSmoke = null;
+    }
+  }
+}
+export default WorkFace;

+ 102 - 0
src/views/vent/monitorManager/dedustMonitor/dedust.threejs.ts

@@ -0,0 +1,102 @@
+import * as THREE from 'three';
+import UseThree from '../../../../utils/threejs/useThree';
+import WorkFace from './dedust.threejs.base';
+import { animateCamera } from '/@/utils/threejs/util';
+import useEvent from '../../../../utils/threejs/useEvent';
+
+// 模型对象、 文字对象
+let model: UseThree | undefined,
+  workFaceObj: WorkFace | undefined,
+  group: THREE.Object3D | undefined,
+  fiberType = 'tunFace'; // workerFaceFiber
+const { mouseDownFn } = useEvent();
+
+// 鼠标点击、松开事件
+const mouseEvent = (event) => {
+  if (event.button == 0 && model && group) {
+    mouseDownFn(model, group, event, () => {});
+  }
+};
+
+const addMouseEvent = () => {
+  // 定义鼠标点击事件
+  if (!model) return;
+  model.canvasContainer?.addEventListener('mousedown', mouseEvent.bind(null));
+};
+
+const render = () => {
+  if (model && model.isRender && model.renderer) {
+    // model.animationId = requestAnimationFrame(render);
+    model.css3dRender?.render(model.scene as THREE.Scene, model.camera as THREE.PerspectiveCamera);
+    model.renderer.render(model.scene as THREE.Scene, model.camera as THREE.PerspectiveCamera);
+    model.stats?.update();
+  }
+};
+
+export const refreshModal = () => {
+  if (fiberType === 'tunFace') {
+    // workFaceObj.render();
+    render();
+  }
+};
+
+// 切换风窗类型
+export const setModelType = (type) => {
+  if (!model) return;
+  fiberType = type;
+  return new Promise((resolve) => {
+    if (fiberType === 'tunFace' && workFaceObj && workFaceObj.group) {
+      group = workFaceObj.group;
+
+      // const oldCameraPosition = { x: 124.736, y: 63.486, z: 103.337 };
+
+      model?.orbitControls?.addEventListener('change', render);
+
+      setTimeout(async () => {
+        resolve(null);
+        const oldCameraPosition = { x: -693, y: 474, z: 398 };
+        if (!model?.scene?.getObjectByName('tunFace') && group) {
+          model?.scene?.add(group);
+        }
+        const position = { x: 14.826074594663222, y: 16.901762713393836, z: 36.459944037951004 };
+        await animateCamera(
+          oldCameraPosition,
+          { x: 0, y: 0, z: 0 },
+          { x: position.x, y: position.y, z: position.z },
+          { x: 0, y: 0, z: 0 },
+          model,
+          0.8,
+          render
+        );
+      }, 400);
+    }
+  });
+};
+
+export const mountedThree = () => {
+  return new Promise(async (resolve) => {
+    model = new UseThree('#tunFace3D', '#tunFace3DCSS');
+    model.setEnvMap('test1');
+    /** @ts-ignore-next-line */
+    model.renderer.toneMappingExposure = 1.0;
+    // model?.camera?.position.set(100, 0, 1000);
+    workFaceObj = new WorkFace(model);
+    await workFaceObj.mountedThree();
+
+    addMouseEvent();
+    // render();
+    model.animate();
+    resolve(null);
+  });
+};
+
+export const destroy = () => {
+  if (model) {
+    model.isRender = false;
+    workFaceObj?.destroy();
+    workFaceObj = undefined;
+    group = undefined;
+    model.destroy();
+    model = undefined;
+  }
+};

+ 257 - 0
src/views/vent/monitorManager/dedustMonitor/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <div
+    v-show="activeKey == 'monitor' && !loading"
+    class="bg"
+    style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden"
+  >
+    <a-spin :spinning="loading" />
+    <div
+      id="tunFace3DCSS"
+      class="threejs-Object-CSS"
+      v-show="!loading"
+      style="width: 100%; height: 100%; position: absolute; pointer-events: none; overflow: hidden; z-index: 1; top: 0"
+    >
+    </div>
+    <div id="tunFace3D" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
+  </div>
+  <div class="scene-box">
+    <customHeader
+      :fieldNames="{ label: 'systemname', value: 'id', options: 'children' }"
+      :options="options"
+      :optionValue="optionValue"
+      @change="getSelectRow"
+    >
+      除尘风机智能管控
+    </customHeader>
+    <div class="center-container">
+      <template v-if="activeKey == 'monitor'">
+        <DedustHome :deviceId="optionValue" />
+      </template>
+      <div v-else class="history-group">
+        <div class="device-button-group" v-if="deviceList.length > 0 && activeKey !== 'faultRecord'">
+          <div
+            class="device-button"
+            :class="{ 'device-active': deviceActive == device.deviceType }"
+            v-for="(device, index) in deviceList"
+            :key="index"
+            @click="deviceChange(index)"
+            >{{ device.deviceName }}</div
+          >
+        </div>
+        <div class="history-container">
+          <DedustHistory
+            v-if="activeKey == 'monitor_history'"
+            ref="historyTable"
+            class="vent-margin-t-20"
+            :deviceId="optionValue"
+            :device-type="deviceType"
+          />
+          <HandleHistory
+            v-if="activeKey == 'handler_history'"
+            ref="alarmHistoryTable"
+            class="vent-margin-t-20"
+            :deviceId="optionValue"
+            :device-type="deviceType"
+          />
+          <AlarmHistory
+            v-if="activeKey == 'faultRecord'"
+            ref="handlerHistoryTable"
+            class="vent-margin-t-20"
+            :deviceId="optionValue"
+            :device-type="deviceType"
+          />
+        </div>
+      </div>
+    </div>
+    <BottomMenu @change="changeActive" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onBeforeMount, ref, onMounted, onUnmounted, reactive } from 'vue';
+  import { mountedThree, destroy, setModelType } from './dedust.threejs';
+  import { systemList, getTableList } from './dedust.api';
+  import customHeader from '/@/components/vent/customHeader.vue';
+  import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
+  import DedustHome from './components/DedustHome.vue';
+  import DedustHistory from './components/DedustHistory.vue';
+  import HandleHistory from './components/HandleHistory.vue';
+  import AlarmHistory from './components/AlarmHistory.vue';
+  import { useRouter } from 'vue-router';
+
+  type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
+
+  const { currentRoute } = useRouter();
+  const activeKey = ref('monitor');
+  const loading = ref(false);
+
+  const historyTable = ref();
+  const alarmHistoryTable = ref();
+  const handlerHistoryTable = ref();
+
+  //关联设备
+  const deviceList = ref<DeviceType[]>([]);
+  const deviceActive = ref('');
+  const deviceType = ref('');
+
+  const options = ref();
+  // 默认初始是第一行
+  // const selectRowIndex = ref(0);
+  const dataSource = ref([]);
+
+  const optionValue = ref('');
+
+  // 监测数据
+  const selectData = reactive({});
+
+  function changeActive(activeValue) {
+    activeKey.value = activeValue;
+    loading.value = true;
+    if (activeKey.value === 'monitor') {
+      setModelType('tunFace');
+      setTimeout(() => {
+        loading.value = false;
+      }, 600);
+    } else {
+      loading.value = false;
+    }
+  }
+
+  function deviceChange(index) {
+    deviceActive.value = deviceType.value = deviceList.value[index].deviceType;
+  }
+
+  // 查询关联设备列表
+  async function getDeviceList() {
+    const res = await systemList({ devicetype: 'sys', systemID: optionValue.value });
+    if (res && res.msgTxt && res.msgTxt.length) {
+      const result = res.msgTxt;
+      const deviceArr: DeviceType[] = [];
+      result.forEach((item) => {
+        const data = item['datalist'].filter((data: any) => {
+          const readData = data.readData;
+          return Object.assign(data, readData);
+        });
+        if (item.type != 'sys') {
+          deviceArr.unshift({
+            deviceType: item.type,
+            deviceName: item['typeName'] ? item['typeName'] : item['datalist'][0]['typeName'],
+            datalist: data,
+          });
+        }
+      });
+      deviceList.value = deviceArr;
+      deviceActive.value = deviceArr[0].deviceType;
+      deviceChange(0);
+    }
+  }
+
+  async function getSysDataSource() {
+    const res = await getTableList({ strtype: 'sys_dedustfan_normal', pagetype: 'normal' });
+    if (!options.value && res) {
+      // 初始时选择第一条数据
+      options.value = res.records || [];
+      if (!optionValue.value) {
+        optionValue.value = options.value[0]['id'];
+        getDeviceList();
+      }
+    }
+  }
+
+  // 切换检测数据
+  async function getSelectRow(deviceID) {
+    const currentData = dataSource.value.find((item: any) => {
+      return item.deviceID == deviceID;
+    });
+    if (currentData) {
+      optionValue.value = currentData['deviceID'];
+      Object.assign(selectData, currentData);
+      await getDeviceList();
+    }
+  }
+
+  onBeforeMount(() => {});
+
+  onMounted(async () => {
+    loading.value = true;
+    mountedThree().then(async () => {
+      setModelType('tunFace');
+      loading.value = false;
+    });
+    if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
+      optionValue.value = currentRoute.value['query']['id'] as string;
+    }
+    await getSysDataSource();
+    await getDeviceList();
+  });
+
+  onUnmounted(() => {
+    destroy();
+  });
+</script>
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+  @import '/@/design/vent/modal.less';
+
+  .history-group {
+    padding: 0 20px;
+    margin-top: 90px;
+    .history-container {
+      position: relative;
+      background: #6195af1a;
+      width: calc(100% + 10px);
+      top: 0px;
+      left: -10px;
+      border: 1px solid #00fffd22;
+      padding: 10px 0;
+      box-shadow: 0 0 20px #44b4ff33 inset;
+    }
+  }
+  .device-button-group {
+    // margin: 0 20px;
+    display: flex;
+    pointer-events: auto;
+    position: relative;
+    &::after {
+      position: absolute;
+      content: '';
+      width: calc(100% + 10px);
+      height: 2px;
+      top: 30px;
+      left: -10px;
+      border-bottom: 1px solid var(--vent-base-border);
+    }
+    .device-button {
+      padding: 4px 15px;
+      position: relative;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      font-size: 14px;
+
+      color: var(--vent-font-color);
+      cursor: pointer;
+      margin: 0 3px;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+        border: 1px solid #6176af;
+        transform: skewX(-38deg);
+        background-color: rgba(0, 77, 103, 85%);
+        z-index: -1;
+      }
+    }
+    .device-active {
+      // color: #0efcff;
+      &::before {
+        border-color: #0efcff;
+        box-shadow: 1px 1px 3px 1px #0efcff inset;
+      }
+    }
+  }
+</style>