瀏覽代碼

Merge branch 'master' of http://182.92.126.35:3000/hrx/mky-vent-base

lxh 5 月之前
父節點
當前提交
0dde5b1360

二進制
src/assets/images/home-container/configurable/board_bg_10.png


二進制
src/assets/images/home-container/configurable/board_bg_7.png


二進制
src/assets/images/home-container/configurable/board_bg_8.png


二進制
src/assets/images/home-container/configurable/board_bg_9.png


二進制
src/assets/images/home-container/configurable/minehome/list-bg-n5.png


二進制
src/assets/images/home-container/configurable/minehome/list-bg-n6.png


二進制
src/assets/images/home-container/configurable/minehome/list-icon-file.png


+ 37 - 0
src/views/vent/home/configurable/components/detail/MiniBoard.vue

@@ -212,6 +212,10 @@
     --image-board-bg-nl2: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-n3.png');
     --image-board-bg-nr2: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-n4.png');
     --image-board-bg-o: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-o.png');
+    --image-board_bg_7: url('/@/assets/images/home-container/configurable/board_bg_7.png');
+    --image-board_bg_8: url('/@/assets/images/home-container/configurable/board_bg_8.png');
+    --image-board_bg_9: url('/@/assets/images/home-container/configurable/board_bg_9.png');
+    --image-board_bg_10: url('/@/assets/images/home-container/configurable/board_bg_10.png');
 
     --image-hycd: url(/@/assets/images/home-container/configurable/dusthome/hycd.png);
     --image-dyfl: url(/@/assets/images/home-container/configurable/dusthome/dyfl.png);
@@ -474,6 +478,33 @@
     background-size: 100% 100%;
   }
 
+  .mini-board_P {
+    width: 97px;
+    height: 170px;
+    padding-top: 105px;
+    background-image: var(--image-board_bg_7), var(--image-board_bg_10);
+    background-size:
+      97px 105px,
+      100% 40px;
+    background-repeat: no-repeat;
+    background-position:
+      center top,
+      center bottom;
+  }
+  .mini-board_P:nth-of-type(2) {
+    width: 97px;
+    height: 170px;
+    padding-top: 105px;
+    background-image: var(--image-board_bg_8), var(--image-board_bg_9);
+    background-size:
+      97px 105px,
+      100% 40px;
+    background-repeat: no-repeat;
+    background-position:
+      center top,
+      center bottom;
+  }
+
   .mini-board__value_New {
     color: @vent-gas-primary-text;
     font-size: 15px;
@@ -655,6 +686,12 @@
     color: @vent-gas-primary-text;
   }
 
+  .mini-board__value_P {
+    font-family: 'douyuFont';
+    font-size: 20px;
+    margin-top: 10px;
+  }
+
   .mini-board_E:nth-child(1) {
     .mini-board__label_E {
       background-image: var(--image-hycd);

+ 15 - 11
src/views/vent/monitorManager/balancePressMonitor/components/balancePressHandleHistory.vue

@@ -1,12 +1,16 @@
 <template>
   <div class="handle-history">
-    <HandlerHistoryTable columns-type="operator_history" device-type="sys_surface_caimei"
-      :device-list-api="getTableList.bind(null, { strtype: 'pressurefan' })" designScope="pressurefan_history" />
+    <HandlerHistoryTable
+      columns-type="operator_history"
+      device-type="sys_surface_caimei"
+      :device-list-api="getTableList.bind(null, { strtype: 'pressurefan' })"
+      designScope="pressurefan_history"
+    />
   </div>
 </template>
 <script setup lang="ts">
-import HandlerHistoryTable from '../../comment/HandlerHistoryTable.vue';
-import { getTableList } from '../balancePress.api'
+  import HandlerHistoryTable from '../../comment/HandlerHistoryTable.vue';
+  import { getTableList } from '../balancePress.api';
   const props = defineProps({
     deviceType: {
       type: String,
@@ -15,12 +19,12 @@ import { getTableList } from '../balancePress.api'
     deviceId: {
       type: String,
       required: true,
-    }
-  })
+    },
+  });
 </script>
 <style lang="less" scoped>
-.handle-history {
-  width: 100%;
-  pointer-events: auto;
-}
-</style>
+  .handle-history {
+    width: 100%;
+    pointer-events: auto;
+  }
+</style>

+ 1 - 1
src/views/vent/monitorManager/comment/HandlerHistoryTable.vue

@@ -27,7 +27,7 @@
     },
     deviceListApi: {
       type: Function,
-      required: true,
+      required: false,
     },
     designScope: {
       type: String,

+ 136 - 78
src/views/vent/monitorManager/sprayMonitor/component.vue

@@ -7,55 +7,64 @@
   >
     {{ mainTitle }}
   </customHeader>
-  <div class="bg" style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden">
-    <a-spin :spinning="loading" />
-    <div id="model3D" v-show="!loading" style="width: 100%; height: 100%; position: absolute; overflow: hidden"></div>
-    <slot name="monitorSlot3D"></slot>
-  </div>
+  <slot :monitor-data="monitorData"></slot>
   <div class="scene-box">
     <div class="center-container">
       <template v-if="activeKey == 'monitor'">
-        <!-- <balancePressHome v-if="activeKey == 'monitor'" :deviceId="optionValue" /> -->
+        <slot name="monitor">
+          <ModuleCommon
+            v-for="cfg in mainConfig.configs"
+            :key="cfg.deviceType"
+            :show-style="cfg.showStyle"
+            :module-data="cfg.moduleData"
+            :module-name="cfg.moduleName"
+            :device-type="cfg.deviceType"
+            :data="monitorData"
+            :visible="true"
+          />
+        </slot>
       </template>
       <div v-else class="history-group">
-        <div class="device-button-group" v-if="deviceList.length > 0">
+        <div v-if="showDeviceList && deviceList.length > 0" class="device-button-group">
           <div
-            class="device-button"
-            :class="{ 'device-active': deviceActive == device.deviceType }"
             v-for="(device, index) in deviceList"
+            :class="{ 'device-button': true, 'device-active': deviceActive == device.deviceType }"
             :key="index"
             @click="deviceChange(index)"
-            >{{ device.deviceName }}</div
           >
+            {{ device.deviceName }}
+          </div>
         </div>
         <div class="history-container">
-          <HistoryTable
-            v-if="activeKey == 'monitor_history'"
-            class="vent-margin-t-20"
-            :columns-type="`${deviceType}`"
-            :device-type="deviceType"
-            :sysId="optionValue"
-            :scroll="{ y: 650 }"
-            :only-bouned-devices="monitorHistoryConfig.onlyBounedDevices"
-            :show-history-curve="monitorHistoryConfig.showHistoryCurve"
-          />
-          <HandlerHistoryTable
-            v-if="activeKey == 'handler_history'"
-            class="vent-margin-t-20"
-            columns-type="operator_history"
-            :deviceType="deviceType"
-            :device-list-api="getHandlerList"
-          />
-
-          <AlarmHistoryTable
-            v-if="activeKey == 'faultRecord'"
-            columns-type="alarm"
-            :device-type="deviceType"
-            :list="getAlarmList"
-            :sys-id="optionValue"
-            :device-list-api="workFaceDeviceList.bind(null, { id: optionValue })"
-            designScope="alarm-history"
-          />
+          <slot name="history" :device-type="deviceType" :device-id="optionValue">
+            <HistoryTable
+              v-if="activeKey == 'monitor_history'"
+              class="vent-margin-t-20"
+              :columns-type="deviceType"
+              :device-type="deviceType"
+              :sysId="optionValue"
+              :scroll="{ y: 650 }"
+              v-bind="monitorHistoryConfig"
+            />
+          </slot>
+          <slot name="handler" :device-type="deviceType" :device-id="optionValue">
+            <HandlerHistoryTable
+              v-if="activeKey == 'handler_history'"
+              class="vent-margin-t-20"
+              columns-type="operator_history"
+              :deviceType="deviceType"
+              v-bind="handlerHistoryConfig"
+            />
+          </slot>
+          <slot name="alarm" :device-type="deviceType" :device-id="optionValue">
+            <AlarmHistoryTable
+              v-if="activeKey == 'faultRecord'"
+              columns-type="alarm"
+              :device-type="deviceType"
+              :sys-id="optionValue"
+              v-bind="alarmHistoryConfig"
+            />
+          </slot>
         </div>
       </div>
     </div>
@@ -68,58 +77,81 @@
   import { ref, onMounted, onUnmounted } from 'vue';
   import { getDevice, sysList } from '../comment/comment.api';
   import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
-  // import balancePressHome from './components/balancePressHome.vue';
+  import ModuleCommon from '../../home/configurable/components/ModuleCommon.vue';
   import HistoryTable from '../comment/HistoryTable.vue';
   import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
+  import AlarmHistoryTable from '../comment/AlarmHistoryTable.vue';
   import { useRouter } from 'vue-router';
   import { Config } from '../../deviceManager/configurationTable/types';
-  import { defHttp } from '/@/utils/http/axios';
-  import { workFaceDeviceList } from '../../deviceManager/comment/warningTabel/warning.api';
+  import { message } from 'ant-design-vue';
+  import _ from 'lodash';
 
   type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
 
   const props = withDefaults(
     defineProps<{
       mainTitle: string;
+      /** 是否显示历史数据上方的设备列表 */
+      showDeviceList?: boolean;
+      /** 请求场景数据传入的类型字符 */
+      strtype: string;
+      /** 请求场景数据传入的页面类型字符 */
+      pagetype?: string;
       /** 获取各表格配置时依赖的设备类型 */
       // deviceType: string;
       /** 主要模块配置 */
       mainConfig: {
         configs: Config[];
+        /** 获取该场景所含设备及其监测信息的API */
+        monitorApi?: (params: { deviceType: string; deviceId: number | string }) => Promise<any>;
+        /** 定时获取监测信息的配置,单位为毫秒,不传入即默认,传0即停用 */
+        timer?: number;
       };
       /** 历史数据配置 */
       monitorHistoryConfig: {
         /** 请求历史数据时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
         /** 仅展示已绑定设备,选择是则从系统中获取sysId下已绑定设备。仅能查询到已绑定设备的历史数据 */
-        onlyBounedDevices: boolean;
+        onlyBounedDevices?: boolean;
         /** 显示历史数据曲线图 */
-        showHistoryCurve: boolean;
+        showHistoryCurve?: boolean;
       };
       /** 操作历史配置 */
       handlerHistoryConfig: {
         /** 请求操作历史时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
+        /** 获取操作历史的API,可以不提供以使用默认的请求 */
+        deviceListApi?: (params: any) => Promise<any[]>;
       };
       /** 报警历史配置 */
       alarmHistoryConfig: {
         /** 请求报警历史时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
+        /** 获取报警历史的API,可以不提供以使用默认的请求 */
+        list?: (params: any) => Promise<any[]>;
+        /** 获取设备以供报警历史过滤的API,可以不提供以使用默认的请求 */
+        deviceListApi?: (params: any) => Promise<any[]>;
       };
-      /** 请求场景数据传入的类型字符 */
-      strtype: string;
-      /** 请求场景数据传入的页面类型字符 */
-      pagetype: string;
     }>(),
     {
+      mainConfig: () => ({
+        configs: [],
+      }),
+      monitorHistoryConfig: () => ({}),
+      handlerHistoryConfig: () => ({}),
+      alarmHistoryConfig: () => ({}),
       pagetype: 'normal',
-      onlyBounedDevices: false,
-      showHistoryCurve: false,
+      showDeviceList: true,
     }
   );
 
   const { currentRoute } = useRouter();
-  const loading = ref(false);
 
   const activeKey = ref('monitor');
 
@@ -130,8 +162,11 @@
   const options = ref([]);
   const optionValue = ref('');
 
+  /** 获取左上角场景选择框数据的方法,如果此时初始场景未赋值则选择首项做初始化 */
   async function getSysDataSource() {
-    const res = await sysList({ strtype: props.strtype, pagetype: props.pagetype });
+    const res = await sysList({ strtype: props.strtype, pagetype: props.pagetype }).catch(() => {
+      message.error('获取场景数据时发生了错误');
+    });
     // 初始时选择第一条数据
     options.value = res.records || [];
     if (!optionValue.value) {
@@ -151,16 +186,18 @@
   const deviceType = ref('');
 
   function deviceChange(index) {
-    deviceType.value = deviceList.value[index].deviceType;
-    deviceActive.value = deviceList.value[index].deviceType;
+    deviceType.value = deviceList.value[index]?.deviceType || '';
+    deviceActive.value = deviceList.value[index]?.deviceType || '';
   }
 
   // 查询关联设备列表
   async function getDeviceList() {
-    const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value });
+    const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
+      message.error('获取已绑定设备时发生了错误');
+    });
 
     deviceList.value = msgTxt.reduce((arr, item) => {
-      const data = item.datalist.forEach((data: any) => {
+      const data = item.datalist.map((data: any) => {
         return Object.assign(data, data.readData);
       });
       // sys代表场景本身,应该过滤掉去处理该场景下的关联设备
@@ -173,45 +210,66 @@
       }
 
       return arr;
-    });
+    }, []);
     if (!deviceActive.value) {
       deviceChange(0);
     }
   }
 
-  /** 获取操作历史 */
-  function getHandlerList() {
-    return sysList({ strtype: deviceType.value });
-  }
-
-  function getAlarmList() {
-    return (params) => defHttp.get({ url: '/safety/managesysAutoLog/list', params });
+  let timer: NodeJS.Timeout;
+  const monitorData = ref<any>({});
+  /** 获取本场景下所绑定的设备,将监测数据赋值 */
+  async function getMonitor() {
+    if (props.mainConfig.monitorApi) {
+      monitorData.value = await props.mainConfig
+        .monitorApi({
+          deviceType: deviceType.value,
+          deviceId: optionValue.value,
+        })
+        .catch(() => {
+          message.error('获取已绑定设备时发生了错误');
+        });
+    } else if (optionValue.value) {
+      const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
+        message.error('获取已绑定设备时发生了错误');
+      });
+      msgTxt.forEach((item) => {
+        _.set(monitorData.value, item.type, item.datalist);
+      });
+    }
   }
 
-  onMounted(() => {
+  onMounted(async () => {
     if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
       optionValue.value = currentRoute.value['query']['id'] as string;
     }
-    getSysDataSource();
+    await getSysDataSource();
+    if (props.mainConfig.timer !== 0) {
+      timer = setInterval(() => {
+        getMonitor();
+      }, props.mainConfig.timer || 5000);
+    } else {
+      getMonitor();
+    }
   });
 
-  onUnmounted(() => {});
+  onUnmounted(() => {
+    clearInterval(timer);
+  });
 </script>
 <style lang="less" scoped>
   @import '/@/design/vent/modal.less';
   @ventSpace: zxm;
   .scene-box {
-    margin-top: 20px;
+    margin-top: 40px;
     pointer-events: none;
     .history-group {
-      padding: 0 20px;
+      margin-top: 80px;
+      padding: 0 10px;
       .history-container {
         pointer-events: auto;
-        position: relative;
         background: #6195af1a;
-        width: calc(100% + 10px);
-        top: 0px;
-        left: -10px;
+        // width: 100%;
         border: 1px solid #00fffd22;
         padding: 10px 0;
         box-shadow: 0 0 20px #44b4ff33 inset;
@@ -219,17 +277,17 @@
     }
     .device-button-group {
       // margin: 0 20px;
+      padding: 0 10px;
       display: flex;
       pointer-events: auto;
       position: relative;
-      margin-top: 90px;
       &::after {
         position: absolute;
         content: '';
-        width: calc(100% + 10px);
+        width: 100%;
         height: 2px;
         top: 30px;
-        left: -10px;
+        left: -1px;
         border-bottom: 1px solid #0efcff;
       }
       .device-button {
@@ -268,7 +326,7 @@
   }
   .center-container {
     width: 100%;
-    height: calc(100% - 200px);
+    height: calc(100% - 150px);
   }
 
   .input-box {

+ 308 - 3
src/views/vent/monitorManager/sprayMonitor/index.vue

@@ -1,6 +1,311 @@
+<!-- eslint-disable vue/multi-word-component-names -->
 <template>
-  <div></div>
+  <div class="spray-wrapper">
+    <MonitorComponent
+      main-title="凝胶防灭火材料自动喷洒系统"
+      :main-config="{
+        configs: defaultConfigs,
+      }"
+      :monitor-history-config="{}"
+      :handler-history-config="{}"
+      :alarm-history-config="{}"
+      strtype="sys_surface_juejin"
+    >
+      <template #default="{ monitorData }">
+        <div id="spray3D" class="w-full h-full">
+          <a-spin :spinning="loading" />
+          <div id="sprayCSS3D" v-show="!loading" style="width: 100%; height: 100%; position: absolute; overflow: hidden">
+            <FourBorderBg id="sprayCSS3DEnvA">
+              <div>送料电机</div>
+              <div>电压US:{{ monitorData.slus }}</div>
+              <div>电流LA:{{ monitorData.slla }}</div>
+              <div>电流LB:{{ monitorData.sllb }}</div>
+              <div>电流LC:{{ monitorData.sllc }}</div>
+            </FourBorderBg>
+            <FourBorderBg id="sprayCSS3DEnvB">
+              <div>输送电机</div>
+              <div>电压US:{{ monitorData.ssus }}</div>
+              <div>电流LA:{{ monitorData.ssla }}</div>
+              <div>电流LB:{{ monitorData.sslb }}</div>
+              <div>电流LC:{{ monitorData.sslc }}</div>
+            </FourBorderBg>
+          </div>
+        </div>
+      </template>
+    </MonitorComponent>
+  </div>
 </template>
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import FourBorderBg from '/@/views/vent/comment/components/fourBorderBg.vue';
+  import MonitorComponent from './component.vue';
+  import { Config } from '../../deviceManager/configurationTable/types';
+  import { useInitConfigs } from '../../home/configurable/hooks/useInit';
+  import { mountedThree, setModelType } from './spray.three';
 
-<style lang="scss" scoped></style>
+  const loading = ref(false);
+  const defaultConfigs: Config[] = [
+    {
+      deviceType: '',
+      moduleName: '压力传感器',
+      pageType: 'spray',
+      moduleData: {
+        header: {
+          show: false,
+          readFrom: '',
+          selector: {
+            show: false,
+            value: '',
+          },
+          slot: {
+            show: false,
+            value: '',
+          },
+        },
+        background: {
+          show: false,
+          type: 'video',
+          link: '',
+        },
+        layout: {
+          direction: 'row',
+          items: [
+            {
+              name: 'board',
+              basis: '100%',
+            },
+          ],
+        },
+        board: [
+          {
+            type: 'P',
+            layout: 'label-top',
+            items: [
+              {
+                label: '在线数量',
+                value: '99',
+              },
+              {
+                label: '异常数量',
+                value: '0',
+              },
+            ],
+            readFrom: '',
+          },
+        ],
+      },
+      showStyle: {
+        size: 'width:440px;height:250px;',
+        version: '保德',
+        position: 'top:80px;left:10px;',
+      },
+    },
+    {
+      deviceType: '',
+      moduleName: '喷洒实况',
+      pageType: 'spray',
+      moduleData: {
+        header: {
+          show: false,
+          readFrom: '',
+          selector: {
+            show: false,
+            value: '',
+          },
+          slot: {
+            show: false,
+            value: '',
+          },
+        },
+        background: {
+          show: false,
+          type: 'video',
+          link: '',
+        },
+        layout: {
+          direction: 'row',
+          items: [
+            {
+              name: 'table',
+              basis: '100%',
+            },
+          ],
+        },
+        table: [
+          {
+            type: 'A',
+            columns: [
+              {
+                name: '支架',
+                prop: 'strinstallpos',
+              },
+              {
+                name: '工作状态',
+                prop: 'airStatus_str',
+              },
+            ],
+            readFrom: '',
+          },
+        ],
+        preset: [],
+      },
+      showStyle: {
+        size: 'width:440px;height:430px;',
+        version: '保德',
+        position: 'top:350px;left:10px;',
+      },
+    },
+    {
+      deviceType: '',
+      moduleName: '场景信息总览',
+      pageType: 'spray',
+      moduleData: {
+        header: { show: false, readFrom: '', selector: { show: false, value: '' }, slot: { show: false, value: '' } },
+        background: { show: false, type: 'video', link: '' },
+        layout: {
+          direction: 'column',
+          items: [
+            {
+              name: 'list',
+              basis: '50%',
+              overflow: false,
+            },
+            {
+              name: 'partition',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'list',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'partition',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'list',
+              basis: 'auto',
+              overflow: false,
+            },
+          ],
+        },
+        list: [
+          {
+            type: 'L',
+            readFrom: '',
+            items: [
+              {
+                label: '绑定控制箱名称',
+                value: '${cumulativeFlow}',
+              },
+              {
+                label: '控制箱地址',
+                value: '${heaterTemperature}',
+              },
+              {
+                label: '控制箱在线状态',
+                value: '${nitrogen}',
+              },
+              {
+                label: '故障从机',
+                value: '${nitrogenContent}',
+              },
+              {
+                label: '浮球液位报警',
+                value: '${nitrogenContent}',
+              },
+              {
+                label: '输送管道压力',
+                value: '${nitrogenContent}',
+              },
+            ],
+          },
+          {
+            type: 'N',
+            readFrom: '',
+            items: [
+              {
+                label: '启动状态',
+                value: '0.97',
+                color: 'blue',
+              },
+              {
+                label: '报警状态',
+                value: '84.4',
+              },
+            ],
+          },
+          {
+            type: 'N',
+            readFrom: '',
+            items: [
+              {
+                label: '启动状态',
+                value: '0.97',
+                color: 'blue',
+              },
+              {
+                label: '故障状态',
+                value: '84.4',
+              },
+            ],
+          },
+        ],
+        partition: [
+          {
+            type: 'A',
+            readFrom: '',
+            layout: 'icon-pre',
+            label: '送料电机',
+            icon: '/src/assets/images/home-container/configurable/tashanhome/partition-icon-1.png',
+          },
+          {
+            type: 'A',
+            readFrom: '',
+            layout: 'icon-pre',
+            label: '输送电机',
+            icon: '/src/assets/images/home-container/configurable/tashanhome/partition-icon-2.png',
+          },
+        ],
+        mock: {},
+      },
+      showStyle: {
+        size: 'width:440px;height:700px;',
+        version: '原版',
+        position: 'top:80px;right:10px;',
+      },
+    },
+  ];
+
+  const { configs, fetchConfigs } = useInitConfigs();
+
+  onMounted(() => {
+    fetchConfigs('spray');
+    // loading.value = true;
+    // mountedThree('#spray3D', ['#sprayCSS3D', '#sprayCSS3DEnvA', '#sprayCSS3DEnvB']).then(() => {
+    //   setModelType('spray').finally(() => {
+    //     loading.value = false;
+    //   });
+    // });
+  });
+</script>
+
+<style lang="less" scoped>
+  .spray-wrapper {
+    width: 100%;
+    height: 100%;
+  }
+
+  .spray-wrapper :deep(.list-item_L .list-item__icon_L) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-icon-file.png');
+  }
+  .spray-wrapper :deep(.list-item_N:nth-child(1)) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-bg-n5.png');
+  }
+  .spray-wrapper :deep(.list-item_N:nth-child(2)) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-bg-n6.png');
+  }
+</style>

+ 113 - 0
src/views/vent/monitorManager/sprayMonitor/spray.three.ts

@@ -0,0 +1,113 @@
+import * as THREE from 'three';
+import UseThree from '../../../../utils/threejs/useThree';
+import ModelContext from './spray.threejs.base';
+import { animateCamera } from '/@/utils/threejs/util';
+import useEvent from '../../../../utils/threejs/useEvent';
+
+/** 模型总控制器 */
+let model: UseThree;
+/** 当前展示的具体模型的 Object3D 对象 */
+let group: THREE.Object3D;
+/** 具体模型内容列表,包含此模型总控制器下的所有可用的具体模型内容 */
+const modelContextList: {
+  /** 当前模型类型,在控制器下有多个具体模型时分辨它们 */
+  type: string;
+  /** 模型的具体内容,即负责模型导入、绘制的上下文对象,一个控制器可以新建多个 */
+  context?: ModelContext;
+}[] = [];
+const { mouseDownFn } = useEvent();
+
+/** 分发鼠标事件到具体实现方法 */
+function dispatchMouseEvent(event) {
+  if (event.button == 0 && model && group) {
+    mouseDownFn(model, group, event, () => {});
+  }
+}
+
+/** 初始化模型CSS展示框的鼠标事件,应该在模型总控制器初始化后调用 */
+function initEventListender() {
+  if (!model) return;
+  model.canvasContainer?.addEventListener('mousedown', (e) => dispatchMouseEvent(e));
+  // model.orbitControls?.addEventListener('change', () => render());
+}
+
+/** 渲染并更新总模型 */
+// function 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 function refreshModal() {
+//   render();
+//   // modelContextList.forEach((item) => {
+//   //   if (item.context) {
+//   //     item.context.render();
+//   //   }
+//   // });
+// }
+
+/** 设置模型类型并切换,不同的类型通常对应不同的具体模型,在模型总控制器下的具体模型会根据传入的参数彼此交互、切换 */
+export function setModelType(modelType: 'spray') {
+  return new Promise((resolve, reject) => {
+    if (!model) return reject('模型未初始化');
+    modelContextList.forEach(({ type, context }) => {
+      if (!context) return reject('模型未初始化');
+      if (modelType === type) {
+        group = context?.group as THREE.Object3D;
+        setTimeout(async () => {
+          if (!model.scene?.getObjectByName(group.name) && group) {
+            model.scene?.add(group);
+          }
+          const oldCameraPosition = { x: -693, y: 474, z: 398 };
+          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
+          );
+          resolve(null);
+        }, 400);
+      }
+    });
+  });
+}
+
+/** 挂载模型控制器,sceneSelctor表示放置模型的元素选择器,cssSelectors表示放置类似详情框的元素选择器,其中第一项需要是根元素选择器 */
+export function mountedThree(sceneSelctor: string, cssSelectors: string[]) {
+  return new Promise(async (resolve) => {
+    const [rootSelector, ...selectors] = cssSelectors;
+    model = new UseThree(sceneSelctor, rootSelector);
+    model.setEnvMap('test1.hdr');
+    /** @ts-ignore-next-line */
+    model.renderer.toneMappingExposure = 1.0;
+
+    const model1 = new ModelContext(model);
+    await model1.mountedThree();
+    model1.initCssElement(selectors);
+    modelContextList.push({
+      type: 'spray',
+      context: model1,
+    });
+
+    initEventListender();
+    model.animate();
+    resolve(null);
+  });
+}
+
+export const destroy = () => {
+  if (!model) return;
+  model.isRender = false;
+  modelContextList.forEach((item) => {
+    if (item.context) item.context.destroy();
+  });
+  model.destroy();
+};

+ 95 - 0
src/views/vent/monitorManager/sprayMonitor/spray.threejs.base.ts

@@ -0,0 +1,95 @@
+import * as THREE from 'three';
+import { CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer';
+import { setModalCenter } from '/@/utils/threejs/util';
+// import { setModalCenter } from '/@/utils/threejs/util';
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+class ModelContext {
+  model;
+  modelName = 'tunFace';
+  // modelName = 'dedust';
+  group: THREE.Object3D | null = null;
+  /** 本模型内支持的锚点位置数组 */
+  anchors: [number, number, number][] = [
+    [-46.1, -12.8, 8],
+    // [0, 0, 0],
+    // 右侧传感器
+    [11.7, -14, 8],
+    // [-25.847, 8.783, 3.267],
+    // [31.142, 8.783, 3.267],
+  ];
+  /** 本模型显示的css详情元素数组 */
+  cssSprites: CSS3DSprite[] = [];
+
+  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);
+  }
+
+  /** 初始化css元素,将css元素选择器传入,该方法会将这些元素按顺序放入本模型支持的锚点中 */
+  initCssElement(selectors: string[]) {
+    selectors.forEach((selector, index) => {
+      const element = document.querySelector(selector) as HTMLElement;
+      if (element) {
+        const css3D = new CSS3DSprite(element);
+        this.cssSprites.push(css3D);
+        css3D.name = selector;
+        css3D.scale.set(0.05, 0.05, 0.05);
+        // const ff = gui.addFolder(`css元素${index}`);
+        // ff.add(css3D.position, 'x', -100, 100);
+        // ff.add(css3D.position, 'y', -100, 100);
+        // ff.add(css3D.position, 'z', -100, 100);
+        if (index < this.anchors.length) {
+          const [x, y, z] = this.anchors[index];
+          css3D.position.set(x, y, z);
+          this.group?.add(css3D);
+        } else {
+          console.warn(`指定的元素${selector}没有合适的位置放置`);
+        }
+      }
+    });
+  }
+
+  /** 清除css元素 */
+  clearCssElement() {
+    this.cssSprites.forEach((sprite) => {
+      this.group?.remove(sprite);
+    });
+    this.cssSprites = [];
+  }
+
+  mountedThree() {
+    return new Promise((resolve) => {
+      this.model.setGLTFModel([this.modelName]).then(async (gltf) => {
+        this.group = gltf[0];
+        if (this.group) {
+          setModalCenter(this.group);
+          resolve(null);
+          this.addLight();
+        }
+      });
+    });
+  }
+
+  destroy() {
+    if (this.model) {
+      this.model.clearGroup(this.group);
+      this.model = null;
+      this.group = null;
+    }
+  }
+}
+export default ModelContext;