Explorar o código

'合并冲突代码'

bobo04052021@163.com hai 1 día
pai
achega
c23239835b

+ 4 - 1
src/components/vent/micro/ventModal.vue

@@ -1,5 +1,5 @@
 <template>
-  <div style="position: absolute; width: 100%; height: 100%">
+  <div style="position: absolute; width: 100%; height: 100%" class="modal-bg">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
   <div id="micro-vent-3dModal"></div>
@@ -30,6 +30,9 @@
   });
 </script>
 <style lang="less" scoped>
+  .modal-bg {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
+  }
   .loading-box {
     position: fixed;
     display: flex;

+ 4 - 1
src/components/vent/micro/ventModal2D.vue

@@ -1,5 +1,5 @@
 <template>
-  <div style="position: absolute; width: 100%; height: 100%">
+  <div style="position: absolute; width: 100%; height: 100%" class="modal-bg">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
   <div id="micro-vent-2dModal"></div>
@@ -28,6 +28,9 @@
   });
 </script>
 <style lang="less" scoped>
+  .modal-bg {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
+  }
   .loading-box {
     position: fixed;
     display: flex;

+ 2 - 1
src/main.ts

@@ -66,9 +66,10 @@ async function bootstrap() {
 
   // 注册第三方组件
   await registerThirdComp(app);
-
+  // console.time('debug'); // 49842 ms - 13552 ms
   // 当路由准备好时再执行挂载( https://next.router.vuejs.org/api/#isready)
   await router.isReady();
+  // console.timeEnd('debug');
 
   initModalWorker();
 

+ 65 - 17
src/views/vent/home/configurable/belt/belt-new.vue

@@ -316,27 +316,75 @@ watch(
     if (newQueryType) {
       changePage(newQueryType as string);
     }
-  },
-  { immediate: true } // 初始化立刻执行
-);
+    modalMonitorData.value = modalRes;
+  }
 
-watch(
-  () => modalMonitorData.value,
-  (newData, oldData) => {
-    if (newData && !Object.keys(oldData).length) {
-      isInitModal.value = true;
+  // // 定时刷新
+  function initInterval() {
+    if (timer) clearInterval(timer);
+    timer = setInterval(() => {
+      refresh();
+    }, 60000);
+  }
+
+  async function changePage(pageTypeStr) {
+    const target = pageTypeStr || route.query.pageType || 'fire_risk_warn';
+    if (pageType.value === target) return;
+    pageType.value = target;
+    configs.value = pageCache.value[target]?.configs || testBeltNew;
+    await nextTick();
+    await refresh();
+  }
+
+  // watch(
+  //   // 监听动态路由参数 :type
+  //   () => route.params.type,
+  //   (newVal) => {
+  //     if (newVal) {
+  //       console.log('切换页面类型:', newVal);
+  //       refresh(); // 切换路由自动刷新
+  //     }
+  //   }
+  // );
+
+  function initModalAnimate(modal) {
+    console.log('初始化模型', modal);
+    modal.isRender = true;
+    modalAnimate(modal, modalMonitorData);
+  }
+  function clearTimer() {
+    if (timer) {
+      clearInterval(timer);
+      timer = null;
     }
   }
-);
+  watch(
+    () => route.query.pageType,
+    (newQueryType) => {
+      if (newQueryType) {
+        changePage(newQueryType as string);
+      }
+    },
+    { immediate: true } // 初始化立刻执行
+  );
 
-onMounted(async () => {
-  await getSysDataSource();
-  await refresh();
-  initInterval();
-});
-onUnmounted(() => {
-  clearTimer();
-});
+  watch(
+    () => modalMonitorData.value,
+    (newData, oldData) => {
+      if (newData && !Object.keys(oldData).length) {
+        isInitModal.value = true;
+      }
+    }
+  );
+
+  onMounted(async () => {
+    await getSysDataSource();
+    await refresh();
+    initInterval();
+  });
+  onUnmounted(() => {
+    clearTimer();
+  });
 </script>
 <style lang="less" scoped>
 .company-home {

+ 118 - 118
src/views/vent/home/configurable/belt/belt.vue

@@ -19,7 +19,7 @@
             :page-type="cfg.pageType"
             :data="data"
             :visible="true"
-            @clickItem="handleItemClick"
+            @click-item="handleItemClick"
             :active-id="currentSelectedId"
           />
         </template>
@@ -28,142 +28,142 @@
   </div>
 </template>
 <script setup lang="ts">
-import { onMounted, onUnmounted, ref } from 'vue';
-import customHeader from './components/customHeader-belt.vue';
-import { useInitConfigs, useInitPage } from '../hooks/useInit';
-import { testBeltLaneFire } from './configurable.data';
-import ModuleCommon from './components/ModuleCommon.vue';
-import SubApp from '/@/components/vent/micro/createSubApp.vue';
-import History from './components/detail/history.vue';
-import { getDataHome } from './configurable.api';
+  import { onMounted, onUnmounted, ref } from 'vue';
+  import customHeader from './components/customHeader-belt.vue';
+  import { useInitConfigs, useInitPage } from '../hooks/useInit';
+  import { testBeltLaneFire } from './configurable.data';
+  import ModuleCommon from './components/ModuleCommon.vue';
+  import SubApp from '/@/components/vent/micro/createSubApp.vue';
+  import History from './components/detail/history.vue';
+  import { getDataHome } from './configurable.api';
 
-const { configs, devicesTypes, fetchConfigs } = useInitConfigs();
-const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
+  const { configs, devicesTypes, fetchConfigs } = useInitConfigs();
+  const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
 
-const currentSelectedId = ref<string | number>('');
+  const currentSelectedId = ref<string | number>('');
 
-let timer = null;
-const showHistory = ref(false);
-// 接收子组件上报的点击事件,更新全局选中状态
-const handleItemClick = (item) => {
-  const clickId = item.id;
-  if (!clickId) return;
-  currentSelectedId.value = clickId;
-};
-function goToHistory() {
-  showHistory.value = !showHistory.value;
-}
+  let timer = null;
+  const showHistory = ref(false);
+  // 接收子组件上报的点击事件,更新全局选中状态
+  const handleItemClick = (item) => {
+    const clickId = item.id;
+    if (!clickId) return;
+    currentSelectedId.value = clickId;
+  };
+  function goToHistory() {
+    showHistory.value = !showHistory.value;
+  }
 
-// 数据筛选
-const filterDataById = (sourceData, clickId: string | number) => {
-  if (!sourceData || !clickId) return sourceData;
-  const id = String(clickId);
-  const monitor = sourceData.monitor_alert?.find((item) => String(item.sysId) === id);
-  const spray = sourceData.spray_control?.find((item) => String(item.sysId) === id);
-  const gate = sourceData.gate_control?.find((item) => String(item.sysId) === id);
-  return {
-    ...sourceData,
-    monitor_alert: monitor ? [monitor] : [],
-    spray_control: spray ? [spray] : [],
-    gate_control: gate ? [gate] : [],
+  // 数据筛选
+  const filterDataById = (sourceData, clickId: string | number) => {
+    if (!sourceData || !clickId) return sourceData;
+    const id = String(clickId);
+    const monitor = sourceData.monitor_alert?.find((item) => String(item.sysId) === id);
+    const spray = sourceData.spray_control?.find((item) => String(item.sysId) === id);
+    const gate = sourceData.gate_control?.find((item) => String(item.sysId) === id);
+    return {
+      ...sourceData,
+      monitor_alert: monitor ? [monitor] : [],
+      spray_control: spray ? [spray] : [],
+      gate_control: gate ? [gate] : [],
+    };
   };
-};
 
-function refresh() {
-  fetchConfigs('belt').then(() => {
-    if (!configs.value || configs.value.length === 0) {
-      configs.value = testBeltLaneFire;
-    }
+  function refresh() {
+    fetchConfigs('belt').then(() => {
+      if (!configs.value || configs.value.length === 0) {
+        configs.value = testBeltLaneFire;
+      }
 
-    const dataListStr = configs.value
-      .filter((e) => e.deviceType)
-      .map((e) => e.deviceType)
-      .join(',');
+      const dataListStr = configs.value
+        .filter((e) => e.deviceType)
+        .map((e) => e.deviceType)
+        .join(',');
 
-    getDataHome({ dataList: dataListStr }).then((res: any) => {
-      res.spray_control = [
-        {
-          systemName: '东翼胶带运输大巷',
-          sysId: '2028657172566073346',
-          sysList: [{ netstatus: '1', deviceStatus: '1', plsy: '1#区域 1.4MPa', kzms: '手动' }],
-        },
-        {
-          sysId: '2046500718274756609',
-          systemName: '1101胶带运输顺槽',
-          sysList: [{ netstatus: '1', deviceStatus: '1', plqy: '2#区域', plsy: '1.6MPa', kzms: '自动' }],
-        },
-      ];
-      let showData = res;
+      getDataHome({ dataList: dataListStr }).then((res: any) => {
+        res.spray_control = [
+          {
+            systemName: '东翼胶带运输大巷',
+            sysId: '2028657172566073346',
+            sysList: [{ netstatus: '1', deviceStatus: '1', plsy: '1#区域 1.4MPa', kzms: '手动' }],
+          },
+          {
+            sysId: '2046500718274756609',
+            systemName: '1101胶带运输顺槽',
+            sysList: [{ netstatus: '1', deviceStatus: '1', plqy: '2#区域', plsy: '1.6MPa', kzms: '自动' }],
+          },
+        ];
+        let showData = res;
 
-      if (currentSelectedId.value) {
-        showData = filterDataById(res, currentSelectedId.value);
-      } else {
-        const firstId = res.monitor_alert?.[0]?.sysId;
-        const warnVal = res.monitor_alert?.[0]?.value;
-        if (firstId) {
-          currentSelectedId.value = firstId;
-          showData = filterDataById(res, firstId);
+        if (currentSelectedId.value) {
+          showData = filterDataById(res, currentSelectedId.value);
+        } else {
+          const firstId = res.monitor_alert?.[0]?.sysId;
+          const warnVal = res.monitor_alert?.[0]?.value;
+          if (firstId) {
+            currentSelectedId.value = firstId;
+            showData = filterDataById(res, firstId);
+          }
         }
-      }
 
-      updateData(showData);
+        updateData(showData);
+      });
     });
-  });
-}
-// 轮询
-function initInterval() {
-  if (timer) clearInterval(timer);
-  timer = setInterval(() => {
+  }
+  // 轮询
+  function initInterval() {
+    if (timer) clearInterval(timer);
+    timer = setInterval(() => {
+      refresh();
+    }, 5000);
+  }
+  onMounted(() => {
     refresh();
-  }, 5000);
-}
-onMounted(() => {
-  refresh();
-  initInterval();
-});
+    initInterval();
+  });
 
-onUnmounted(() => {
-  clearInterval(timer);
-  timer = null;
-});
+  onUnmounted(() => {
+    clearInterval(timer);
+    timer = null;
+  });
 </script>
 <style lang="less" scoped>
-.spray-wrapper {
-  width: 100%;
-  height: 100%;
-  background-image: url('/@/assets/images/beltFire/baseMap.png');
-  background-size: cover;
-}
+  .spray-wrapper {
+    width: 100%;
+    height: 100%;
+    background-image: url('/@/assets/images/beltFire/baseMap.png');
+    background-size: cover;
+  }
 
-#spray3D {
-  pointer-events: all;
-}
+  #spray3D {
+    pointer-events: all;
+  }
 
-.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');
-}
-.company-home {
-  background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
-  width: 100%;
-  height: 100%;
-  color: @white;
-  position: relative;
-  .border {
+  .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');
+  }
+  .company-home {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
     width: 100%;
-    height: 94%;
-    background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
-    background-size: 100% 100%;
-    margin-top: 50px;
-    .test {
-      background: url('./test.png') no-repeat;
+    height: 100%;
+    color: @white;
+    position: relative;
+    .border {
+      width: 100%;
+      height: 94%;
+      background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
       background-size: 100% 100%;
+      margin-top: 50px;
+      .test {
+        background: url('./test.png') no-repeat;
+        background-size: 100% 100%;
+      }
     }
   }
-}
-</style>
+</style>

+ 95 - 20
src/views/vent/home/configurable/belt/components/detail/fireGateBoard.vue

@@ -16,10 +16,10 @@
             <div class="door-position">
               <div class="position"></div>
               <div class="door-name"
-                ><span>{{ item.strname }}</span></div
+                ><span>{{ item.devicePos }}</span></div
               >
-              <a-button class="door-btn" @click="oneKeyClose(index)">一键关闭</a-button>
-              <a-button class="door-btn" @click="oneKeyOpen(index)">一键打开</a-button>
+              <a-button class="door-btn" @click="oneKeyClose(item, index, 'frontGateClose_S', '关闭')">关闭</a-button>
+              <a-button class="door-btn" @click="oneKeyOpen(item, index, 'frontGateOpen_S', '打开')">打开</a-button>
             </div>
             <div class="door-header">
               <div class="info-column" v-for="(i, idx) in config.config.items" :key="idx">
@@ -48,16 +48,28 @@
         </div>
       </div>
     </div>
+    <HandleModal
+      v-if="!globalConfig?.simulatedPassword"
+      :modal-is-show="modalIsShow"
+      :modal-title="modalTitle"
+      :modal-type="modalType"
+      @handle-ok="handleOK"
+      @handle-cancel="handleCancel"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, watch } from 'vue';
+  import { ref, onMounted, watch, inject } from 'vue';
   import { getFormattedText } from '../../../hooks/helper';
   // import gateSVG from '../gateSVG.vue';
   import gateSVG from '../../../../../monitorManager/gateMonitor/components/gateDualSVG.vue';
   import gatePng from '/@/assets/images/fireDoorMonitor.png'; //暂时用图片
   import { nextTick } from 'process';
+  import HandleModal from '/@/views/vent/monitorManager/gateMonitor/modal.vue';
+  import { deviceControlApi } from '/@/api/vent/index';
+  import { message } from 'ant-design-vue';
+  import { doorControlApi } from '/@/views/vent/home/configurable/configurable.api.fireDoorMonitor';
   const props = withDefaults(
     defineProps<{
       config: {
@@ -74,18 +86,19 @@
     }>(),
     {
       data: () => ({
-        data: {
-          strname: '设备1',
-          readData: {
-            frontGateOpen: 1,
-            rearGateOpen: 0,
-          },
-          netStatus: 1,
-        },
+        data: {},
       }),
     }
   );
+
+  const emit = defineEmits(['refresh']);
+  const globalConfig = inject<any>('globalConfig');
+  const modalIsShow = ref<boolean>(false); // 是否显示模态框
+  const modalTitle = ref(''); // 模态框标题显示内容,根据设备操作类型决定
+  const modalType = ref(''); // 模态框内容显示类型,设备操作类型
+  const paramcode = ref(''); // 模态框操作代码
   const childRefs = ref<(InstanceType<typeof gateSVG> | null)[]>([]);
+  const selectData = ref<any>({});
   const setChildRef = (el, index) => {
     if (el) {
       childRefs.value[index] = el;
@@ -104,21 +117,83 @@
     }
   }
 
-  function oneKeyOpen(index) {
-    if (childRefs.value[index]) {
-      childRefs.value[index].animate(true, true, true);
-    }
+  function oneKeyOpen(item, index, code, title) {
+    selectData.value = item;
+    modalTitle.value = title;
+    paramcode.value = code;
+    openGateControlModal();
+    // if (childRefs.value[index]) {
+    //   childRefs.value[index].animate(true, true, true);
+    // }
+  }
+  function oneKeyClose(item, index, code, title) {
+    selectData.value = item;
+    modalTitle.value = title;
+    paramcode.value = code;
+    openGateControlModal();
+    // if (childRefs.value[index]) {
+    //   childRefs.value[index].animate(false, false, false);
+    // }
   }
-  function oneKeyClose(index) {
-    if (childRefs.value[index]) {
-      childRefs.value[index].animate(false, false, false);
+
+  function handleOK(passWord, handlerState, value?) {
+    console.log('handleOK', passWord, handlerState, value);
+    if (!passWord && !globalConfig?.simulatedPassword) {
+      message.warning('请输入密码');
+      return;
     }
+    const data = {
+      systemID: selectData.value.systemDeviceId,
+      deviceid: selectData.value.deviceID,
+      devicetype: selectData.value.deviceType,
+      paramcode: paramcode.value,
+      value: '',
+      password: passWord || globalConfig?.simulatedPassword,
+    };
+    console.log('data', data);
+
+    doorControlApi(data)
+      .then((res) => {
+        // 模拟时开启
+        if (res.success) {
+          modalIsShow.value = false;
+          if (globalConfig.History_Type == 'remote') {
+            message.success('指令已下发至生产管控平台成功!');
+          } else {
+            message.success('指令已下发成功!');
+          }
+          // 触发刷新事件
+          emit('refresh');
+        } else {
+          message.error(res.message);
+        }
+      })
+      .catch(() => {
+        message.error('控制异常,请排查问题。。。');
+      });
+  }
+  function handleCancel() {
+    modalIsShow.value = false;
+    modalTitle.value = '';
+    modalType.value = '';
   }
 
+  /** 封装通用的模态框打开方法 */
+  const openGateControlModal = (
+    customTitle?: string // 自定义标题(可选)
+  ) => {
+    // 统一赋值逻辑
+    modalIsShow.value = true;
+
+    // 如果有模拟密码,则直接执行操作
+    // if (globalConfig?.simulatedPassword) {
+    //   handleOK('', String(controlType));
+    // }
+  };
+
   watch(
     () => props.data,
     (newData) => {
-      console.log('数据更新11111', newData);
       if (!newData || !newData.length) return;
       newData.forEach((item, index) => {
         monitorAnimation(item, index);

+ 72 - 17
src/views/vent/home/configurable/belt/threejs/MonitorPanel.vue

@@ -1,14 +1,14 @@
 <!-- MonitorPanel.vue 完整版(支持销毁) -->
 <template>
   <!-- 外层动画包裹层 -->
-  <div class="panel-wrapper">
-    <div class="monitor-panel" ref="panelRef" :class="{ alarm: monitorData.alarmLevel > 102 }">
+  <div class="panel-wrapper" v-if="monitorData">
+    <div class="monitor-panel" ref="panelRef" :class="getStatusClass()">
       <div class="panel-title">{{ monitorData.positionName || 'XXXXX位置' }}</div>
       <div class="panel-grid">
         <div class="panel-item">
           <span class="item-label">▸ 微震测声</span>
           <span class="item-value" :class="getStatusClass('microseism')">
-            {{ monitorData.microseism ?? '-' }}
+            {{ monitorData.microseism != null ? (monitorData.microseism == 0 ? '无震动' : '有震动') : '-' }}
           </span>
         </div>
         <div class="panel-item">
@@ -26,7 +26,7 @@
         <div class="panel-item">
           <span class="item-label">▸ 火焰</span>
           <span class="item-value" :class="getStatusClass('flame')">
-            {{ monitorData.flame ?? '-' }}
+            {{ monitorData.flame != null ? (monitorData.flame == 0 ? '无火' : '有火') : '-' }}
           </span>
         </div>
         <!-- <div class="panel-item item-full">
@@ -50,7 +50,7 @@
         <div class="panel-item">
           <span class="item-label">▸ 烟雾</span>
           <span class="item-value" :class="getStatusClass('smoke')">
-            {{ monitorData.smoke ?? '-' }}
+            {{ monitorData.smoke != null ? (monitorData.smoke == 0 ? '无烟' : '有烟') : '-' }}
           </span>
         </div>
         <div class="panel-item">
@@ -126,6 +126,7 @@
   const panelRef = ref<HTMLElement | null>(null);
   const cssObject = ref<CSS3DObject | null>(null);
   const monitorData = ref<SensorData>(props.sensorData);
+  const threshold = ref<AlarmThreshold>(props.threshold);
 
   // 默认阈值
   const defaultThreshold: Required<AlarmThreshold> = {
@@ -147,11 +148,19 @@
   }));
 
   // 判断状态:normal/warn/alarm
-  const getStatusClass = (key: keyof AlarmThreshold) => {
-    const value = props.sensorData[key];
-    if (value == null) return 'normal';
-    const threshold = currentThreshold.value[key]!;
-    if (threshold > 102) return 'alarm';
+  const getStatusClass = (key?: keyof AlarmThreshold) => {
+    debugger;
+    if (key) {
+      const value = props.sensorData[key];
+      if (value == null) return 'normal';
+      const threshold = currentThreshold.value[key]!;
+      if (threshold >= 102) return `alarm alarm_${threshold}`;
+    } else {
+      // const threshold = monitorData.value.alarmLevel;
+      const threshold = monitorData.value.alarmLevel;
+      if (threshold >= 102) return `alarm alarm_${threshold}`;
+    }
+
     return 'normal';
   };
 
@@ -173,8 +182,9 @@
     }
   };
 
-  const updateMonitorData = (data: SensorData) => {
-    monitorData.value = data;
+  const updateMonitorData = (data: { sensorData: SensorData; threshold: AlarmThreshold }) => {
+    monitorData.value = data.sensorData;
+    threshold.value = data.threshold;
   };
 
   onMounted(() => {
@@ -258,12 +268,47 @@
 
   /* 报警状态整体变红 */
   .monitor-panel.alarm {
-    /* border-color: #ff4d4f; */
-    box-shadow: inset 0 0 60px rgb(206, 0, 0.7); /* 内发光效果 */
-    animation: panelPulse 0.8s infinite alternate;
+    border-color: #ff4d4f;
+    /* box-shadow: inset 0 0 60px rgb(206, 0, 0.7); 内发光效果
+    animation: panelPulse 0.8s infinite alternate; */
+  }
+  .monitor-panel.alarm_102 {
+    /* color: #ffdb3d; */
+    box-shadow: inset 0 0 60px rgb(206, 202, 0); /* 内发光效果 */
+    animation: panelPulse_102 0.8s infinite alternate;
+  }
+  @keyframes panelPulse_102 {
+    0% {
+      border-color: #ffed4d55;
+      box-shadow: inset 0 0 60px rgba(206, 175, 0, 0.5); /* 内发光效果 */
+    }
+    100% {
+      border-color: #bb9f0088;
+      box-shadow: inset 0 0 60px rgb(206, 175, 0); /* 内发光效果 */
+    }
+  }
+
+  .monitor-panel.alarm_103 {
+    /* color: #ff5e00; */
+    box-shadow: inset 0 0 60px rgb(206, 82, 0); /* 内发光效果 */
+    animation: panelPulse_103 0.8s infinite alternate;
+  }
+  @keyframes panelPulse_103 {
+    0% {
+      border-color: #ffb24d55;
+      box-shadow: inset 0 0 60px rgba(206, 100, 0, 0.5); /* 内发光效果 */
+    }
+    100% {
+      border-color: #bb450088;
+      box-shadow: inset 0 0 60px rgb(206, 82, 0); /* 内发光效果 */
+    }
   }
 
-  @keyframes panelPulse {
+  .monitor-panel.alarm_104 {
+    box-shadow: inset 0 0 60px rgb(206, 0, 0.7); /* 内发光效果 */
+    animation: panelPulse_104 0.8s infinite alternate;
+  }
+  @keyframes panelPulse_104 {
     0% {
       border-color: #ff4d4f55;
       box-shadow: inset 0 0 60px rgb(206, 0, 0, 0.5); /* 内发光效果 */
@@ -345,11 +390,21 @@
 
   /* 报警闪烁 */
   .item-value.alarm {
-    color: #ff4d4f !important;
+    /* color: #ff4d4f !important; */
     text-shadow: 0 0 12px rgba(255, 77, 79, 0.6);
     animation: valueBlink 0.8s infinite alternate;
   }
 
+  .alarm_102 {
+    color: #ffdb3d;
+  }
+  .alarm_103 {
+    color: #ff5e00;
+  }
+  .alarm_104 {
+    color: #ff2424;
+  }
+
   @keyframes valueBlink {
     0% {
       opacity: 0.6;

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

@@ -1,7 +1,6 @@
 // PanelManager.ts
-import { createApp, App } from 'vue';
+import { createApp, App, Component } from 'vue';
 import { Scene } from 'three';
-import MonitorPanel, { SensorData, AlarmThreshold } from './MonitorPanel.vue';
 
 // 面板实例结构
 export interface PanelInstance {
@@ -19,10 +18,11 @@ class PanelManager {
    */
   createPanel(
     threeScene: Scene,
+    component: Component,
     options: {
       instanceId: string;
-      sensorData: SensorData;
-      threshold?: AlarmThreshold;
+      sensorData: any;
+      threshold?: any;
       position?: [number, number, number];
       scale?: number;
     }
@@ -34,7 +34,7 @@ class PanelManager {
     document.body.appendChild(root);
 
     // 创建 Vue 应用
-    const app = createApp(MonitorPanel, {
+    const app = createApp(component, {
       ...options,
       threeScene, // 传入场景,方便组件内部销毁
     });

+ 323 - 71
src/views/vent/home/configurable/belt/threejs/belt.threejs.ts

@@ -1,22 +1,32 @@
 import * as THREE from 'three';
 import { PathPointList, PathGeometry } from 'three.path';
 import gsap from 'gsap';
-import { ref, watch } from 'vue';
+import { ref, watch, Ref } from 'vue';
 import { modelMouseHandler } from '/@/utils/threejs/useEvent';
 import { panelManager } from './PanelManager';
-import { Ref } from 'vue';
 import { defHttp } from '/@/utils/http/axios';
+import MonitorPanel from './MonitorPanel.vue';
+import SprinklerPanel from './sprinklerPanel.vue';
 import { animateCamera } from '/@/utils/threejs/util';
+import doubleWindow from '/@/views/vent/monitorManager/windowMonitorBet/shuangdaoFc.threejs';
 
 const gateList = ref([]);
+const sprayList = ref([]);
 const panelApp = [];
-let mouseoverEvent, mouseUpEvent;
+let mouseoverEvent, mouseUpEvent, doubleEvent;
 const normalColor = new THREE.Color(0xff0000);
 const warningColor = new THREE.Color(0xfc5f2e);
+const warningColorMap = new Map([
+  [102, new THREE.Color('#ffdb3d')],
+  [103, new THREE.Color('#ff5e00')],
+  [104, new THREE.Color('#ff2424')],
+]);
 const color = new THREE.Color(0x00ff00);
 let clickSelecteObject;
 const modalData = ref(null);
 const warningPartitionIndex = ref('-1');
+const warningPartitionIndexList: number[] = [];
+const partitionRefMap = new Map();
 
 export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
   // const data = modalMonitorData.value;
@@ -40,29 +50,43 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
   // const len = Object.keys(res?.device).length;
   const { partitionList, blinkAnimationList } = drawPartition(beltModal, 100, 2000);
 
+  const { sprayPartitionList } = drawSprayPartition(beltModal, 100, 2000);
+
+  modal.orbitControls?.update();
+  modal.camera?.updateMatrixWorld();
+
   watch(
     modalMonitorData,
     async (data) => {
       gateList.value = data.deviceInfo['gate']?.datalist as [];
-
+      sprayList.value = data.deviceInfo['spray']?.datalist as [];
       const api = '/ventanaly-device/monitor/disaster/findDeviceInfoBySystem';
       const res = await defHttp.post({ url: api, params: { sysId: '2028657172566073346' } });
       modalData.value = res?.device;
-      updateMonitorPanel3D();
+
       const alarminfo = res?.alarminfo;
+      const alarmLevels: number[] = [...Object.values(alarminfo)];
+      const maxLeval = Math.max(...alarmLevels);
+
       if (Object.keys(alarminfo).length > 0) {
         for (const key in alarminfo) {
           if (!Object.hasOwn(alarminfo, key)) continue;
-          if (alarminfo[key] > 102) {
-            if (modalData.value && modalData.value[key]) modalData.value[key]['alarmLevel'] = alarminfo[key];
+          if (modalData.value && modalData.value[key]) modalData.value[key]['alarmLevel'] = alarminfo[key];
+          if (Number(alarminfo[key]) >= 102) {
+            if (!warningPartitionIndexList.includes(key)) warningPartitionIndexList.push(key);
+          }
+          if (Number(alarminfo[key]) >= 102 && alarminfo[key] == maxLeval) {
             warningPartitionIndex.value = key;
             setPartitionAnimate();
-            break;
           }
         }
       }
+      if (maxLeval == 0 || maxLeval < 102) warningPartitionIndex.value = '-1';
+
       // 根据监测数据进行风门模型动画控制(前提条件:模型上的风门与实际风门设备建立了映射关系)
       handleGateAnimate(gateList.value);
+      updateMonitorPanel3D();
+      updateSprayPanel3D(modal, sprayPartitionList);
     },
     { immediate: true }
   );
@@ -218,16 +242,18 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
   }
 
   function setPartitionAnimate() {
-    if (Number(warningPartitionIndex.value) > -1) panelManager.destroyPanel(`partition${Number(warningPartitionIndex.value)}`);
     warningPartition.length = 0;
     partitionList.forEach((partition, index) => {
       const solidBox = partition.getObjectByName(partition.name + '_solid') as THREE.Mesh;
       const line = partition.getObjectByName(partition.name + '_line') as THREE.LineSegments;
+      const partitionIndex = partition.name.split('partition')[1];
       if (solidBox && line) {
-        if (Number(warningPartitionIndex.value) == index + 1) {
-          if (!solidBox.material.color.equals(warningColor)) solidBox.material.color.setHex(0xfc5f2e);
+        if (warningPartitionIndexList.includes(partitionIndex)) {
+          const data = modalData.value[partitionIndex + ''];
+          const warningColor = warningColorMap.get(data['alarmLevel']) || normalColor;
+          if (!solidBox.material.color.equals(warningColor)) solidBox.material.color.setHex(warningColor.getHex());
           if (solidBox.material.opacity !== 0.3) solidBox.material.opacity = 0.3;
-          if (!line.material.color.equals(warningColor)) line.material.color.setHex(0xfc5f2e);
+          if (!line.material.color.equals(warningColor)) line.material.color.setHex(warningColor.getHex());
           if (line.material.opacity !== 0.8) line.material.opacity = 0.8;
           warningPartition.push(partition);
         } else {
@@ -242,11 +268,13 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
     // 进行动画检测
     const warningOpacity = 0.9;
     const normalOpacity = 0.05;
+
     for (let i = 0; i < blinkAnimationList.length; i++) {
-      if (Number(warningPartitionIndex.value) == i + 1) {
+      const partition = partitionList[i];
+      const partitionIndex = partition.name.split('partition')[1];
+      if (warningPartitionIndexList.includes(partitionIndex)) {
         // 开始动画
         if (!blinkAnimationList[i]) {
-          const partition = partitionList[i];
           const solidBox = partition.getObjectByName(partition.name + '_solid') as THREE.Mesh;
 
           blinkAnimationList[i] = gsap.to(solidBox.material, {
@@ -264,26 +292,10 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
               solidBox.material.needsUpdate = true;
             },
           });
-
-          const pos = createMonitorPanel3D(modal, partition);
-
-          // 这里进行重新定位
-          console.log(pos);
-          gsap.fromTo(
-            modal.camera.position,
-            { x: modal.camera.position.x, y: modal.camera.position.y, z: modal.camera.position.z },
-            {
-              x: pos.x,
-              y: modal.camera.position.y,
-              z: pos.y,
-              duration: 1,
-              onUpdate: () => {
-                modal.orbitControls.target.set(modal.camera.position.x, pos.z, -modal.camera.position.z);
-                modal.orbitControls.update();
-                modal.renderer?.render(modal.scene, modal.camera);
-              },
-            }
-          );
+          if (partitionIndex == warningPartitionIndex.value) {
+            createMonitorPanel3D(modal, partition);
+            partitionAnimate(partition, modal);
+          }
         }
       } else {
         // 停止动画
@@ -303,8 +315,8 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
               solidBox.material.needsUpdate = true;
             },
           });
-          panelManager.destroyPanel(partition.name);
         }
+        panelManager.destroyPanel(partition.name);
       }
     }
   }
@@ -335,14 +347,19 @@ function createMonitorPanel3D(modal, partition) {
   const box = new THREE.Box3();
   box.setFromObject(partition);
   const center = box.getCenter(new THREE.Vector3());
-  const partitionIndex = partition.name.split('_')[0];
-  const index = Number(partitionIndex.split('partition')[1]) + 1;
+  const partitionIndex = partition.name.split('partition')[1];
+  const index = partitionIndex.split('_')[0];
   const data = modalData.value[index + ''];
-  if (data) {
-    panelManager.createPanel(modal.scene, {
+  // if (partitionRefMap.has(partition.name)) {
+  //   partitionRefMap.set(partition.name, ref(data));
+  // } else {
+  //   partitionRefMap.get(partition.name).value = data;
+  // }
+  if (data && !panelManager.getPanel(`partition${index}`)) {
+    panelManager.createPanel(modal.scene, MonitorPanel, {
       instanceId: partition.name,
       sensorData: {
-        positionName: `分区#${index}`,
+        positionName: `${index}#分区`,
         temperature: data['温度传感器'] ? data['温度传感器'][0]?.value : null,
         smoke: data['烟雾传感器'] ? data['烟雾传感器'][0]?.value : null,
         co: data['CO传感器'] ? data['CO传感器'][0]?.value : null,
@@ -373,13 +390,70 @@ function updateMonitorPanel3D() {
     const instance = panelManager.getPanel(id);
     if (instance?.vm?.update && modalData.value) {
       const partitionIndex = id.split('_')[0];
-      const index = Number(partitionIndex.split('partition')[1]) + 1;
+      const index = partitionIndex.split('partition')[1];
       const data = modalData.value[index + ''];
-      instance.vm.updateMonitorData(data);
+      const res = {
+        sensorData: {
+          positionName: `${index}#分区`,
+          temperature: data['温度传感器'] ? data['温度传感器'][0]?.value : null,
+          smoke: data['烟雾传感器'] ? data['烟雾传感器'][0]?.value : null,
+          co: data['CO传感器'] ? data['CO传感器'][0]?.value : null,
+          microseism: data['微震测声传感器'] ? data['微震测声传感器'][0]?.value : null,
+          fiberTemp: data['光纤测温'] ? data['光纤测温'][0]?.value : null,
+          flame: data['火焰传感器'] ? data['火焰传感器'][0]?.value : null,
+          hcl: data['HCl传感器'] ? data['HCl传感器'][0]?.value : null,
+          alarmLevel: data['alarmLevel'] ? data['alarmLevel'] : null,
+        },
+        threshold: {
+          temperature: data['温度传感器'] ? data['温度传感器'][0]?.alarmLevel : null,
+          smoke: data['烟雾传感器'] ? data['烟雾传感器'][0]?.alarmLevel : null,
+          co: data['CO传感器'] ? data['CO传感器'][0]?.alarmLevel : null,
+          microseism: data['微震测声传感器'] ? data['微震测声传感器'][0]?.alarmLevel : null,
+          fiberTemp: data['光纤测温'] ? data['光纤测温'][0]?.alarmLevel : null,
+          flame: data['火焰传感器'] ? data['火焰传感器'][0]?.alarmLevel : null,
+          hcl: data['HCl传感器'] ? data['HCl传感器'][0]?.alarmLevel : null,
+        },
+      };
+
+      instance.vm.updateMonitorData(res);
     }
   });
 }
 
+function updateSprayPanel3D(modal, sprayPartitionList) {
+  for (let i = 0; i < sprayList.value.length; i++) {
+    const spray = sprayList.value[i];
+    if (spray['deviceStatus'] == 1) {
+      // 喷淋开启
+      if (!panelManager.getPanel(spray['deviceID']) && spray['strinstallpos']) {
+        const arr = ((spray['strinstallpos'] as string) || '').split('#');
+        const index = Number(arr[0]);
+        const sprayPartition = sprayPartitionList[index];
+        const box = new THREE.Box3();
+        box.setFromObject(sprayPartition);
+        const center = box.getCenter(new THREE.Vector3());
+        panelManager.createPanel(modal.scene, SprinklerPanel, {
+          instanceId: spray['deviceID'],
+          sensorData: {
+            location: spray['strinstallpos'],
+            deviceStatus: spray['readData']['deviceStatus'],
+            volume: spray['readData']['volume'],
+            MPa: spray['readData']['MPa'],
+            sprayval: spray['readData']['sprayval'],
+          },
+          position: [center.x, center.y, center.z],
+          scale: 0.0125,
+        });
+      }
+    } else {
+      // 喷淋关闭
+      if (panelManager.getPanel(spray['deviceID'])) {
+        panelManager.destroyPanel(spray['deviceID']);
+      }
+    }
+  }
+}
+
 function initCss3DContainer(modal) {
   modal.initCSS3Renderer('#css3dContainer');
 }
@@ -396,13 +470,28 @@ function initMouseEvent(modal, beltModal, partitionList) {
       partitionEvent(intersects, partitionList, modal, event);
     });
   };
+
+  doubleEvent = (event) => {
+    modelMouseHandler(modal, penlin, event, (intersects) => {
+      const intersect0 = intersects[0];
+      if (intersect0 && intersect0.object.name.includes('partition')) {
+        partitionAnimate(intersect0.object, modal);
+      }
+    });
+  };
+
+  modal.canvasContainer?.addEventListener('dblclick', doubleEvent);
   modal.canvasContainer?.addEventListener('mousemove', mouseoverEvent);
   modal.canvasContainer?.addEventListener('mouseup', mouseoverEvent);
 }
 
 function partitionEvent(intersects, partitionList, modal, event) {
+  // 重置
   partitionList.forEach((partition) => {
+    const index = partition.name.split('partition')[1];
     if (!clickSelecteObject || (clickSelecteObject && clickSelecteObject.name !== partition.name)) {
+      const data = modalData.value[index + ''];
+      const warningColor = warningColorMap.get(data['alarmLevel']) || normalColor;
       const solidBox = partition.getObjectByName(partition.name + '_solid');
       const line = partition.getObjectByName(partition.name + '_line');
       if (solidBox && line) {
@@ -411,14 +500,18 @@ function partitionEvent(intersects, partitionList, modal, event) {
       }
     }
   });
+  // 设置
   if (intersects && intersects.length > 0) {
     const partitionObj = intersects.find((item) => {
+      const index = item.object.name.split('_')[0]?.split('partition')[1];
       if (item.object.type === 'Mesh' && item.object.name.indexOf('partition') > -1) {
         const partition = partitionList.find((partition) => partition.name === item.object.parent.name);
         if (partition) {
           const solidBox = partition.getObjectByName(partition.name + '_solid');
           const line = partition.getObjectByName(partition.name + '_line');
           if (solidBox && line) {
+            const data = modalData.value[index + ''];
+            const warningColor = warningColorMap.get(data['alarmLevel']) || normalColor;
             if (!solidBox.material.color.equals(warningColor)) solidBox.material.opacity = 0.3;
             if (!line.material.color.equals(warningColor)) line.material.opacity = 0.8;
           }
@@ -710,35 +803,6 @@ function drawPartition(beltModal: THREE.Group, partitionLen: number, len: number
     box.setFromObject(group1);
     const min = box.min;
     const max = box.max;
-    // const vertices = [
-    //   // 前面 4 个
-    //   new THREE.Vector3(min.x, min.y, min.z),
-    //   new THREE.Vector3(max.x, min.y, min.z),
-    //   new THREE.Vector3(max.x, max.y, min.z),
-    //   new THREE.Vector3(min.x, max.y, min.z),
-
-    //   // 后面 4 个
-    //   new THREE.Vector3(min.x, min.y, max.z),
-    //   new THREE.Vector3(max.x, min.y, max.z),
-    //   new THREE.Vector3(max.x, max.y, max.z),
-    //   new THREE.Vector3(min.x, max.y, max.z),
-    // ];
-
-    // // 绘制 vertices 四个点
-    // for (let i = 0; i < vertices.length; i++) {
-    //   let color = '#f00';
-    //   if (i == 4) {
-    //     color = '#f00';
-    //   } else if (i == 6) {
-    //     color = '#0f0';
-    //   } else {
-    //     color = '#ff0';
-    //   }
-    //   const vertex = vertices[i];
-    //   const ball = new THREE.Mesh(new THREE.SphereGeometry(20, 32, 32), new THREE.MeshStandardMaterial({ color: color }));
-    //   ball.position.copy(vertex);
-    //   penlin.add(ball);
-    // }
 
     // 为皮带巷进行分区
     const modalLen = max.x - min.x;
@@ -888,7 +952,195 @@ function drawPartitionByBox(group: THREE.Object3D, vertices: THREE.Vector3[], in
   return partitionGroup;
 }
 
+// 根据皮带绘制分区
+function drawSprayPartition(beltModal: THREE.Group, partitionLen: number, len: number) {
+  const penlin = beltModal.getObjectByName('penlin') as THREE.Object3D;
+  const group1 = penlin?.getObjectByName('qiang8');
+  const sprayPartitionList: THREE.Object3D[] = [];
+  if (group1) {
+    const box = new THREE.Box3();
+    box.setFromObject(group1);
+    const min = box.min;
+    const max = box.max;
+
+    // 为皮带巷进行分区
+    const modalLen = max.x - min.x;
+    const ratio = modalLen / len;
+    const partitionNum = Math.ceil(len / partitionLen);
+    for (let i = 0; i < partitionNum; i++) {
+      let minLen, maxLen;
+      if (i < partitionNum - 1) {
+        minLen = min.x + i * partitionLen * ratio;
+        maxLen = min.x + (i + 1) * partitionLen * ratio;
+      } else {
+        minLen = min.x + i * partitionLen * ratio;
+        maxLen = max.x;
+      }
+      const vertices = [
+        // 前面 4 个
+        new THREE.Vector3(minLen, min.y, min.z),
+        new THREE.Vector3(maxLen, min.y, min.z),
+        new THREE.Vector3(maxLen, max.y, min.z),
+        new THREE.Vector3(minLen, max.y, min.z),
+
+        // 后面 4 个
+        new THREE.Vector3(minLen, min.y, max.z),
+        new THREE.Vector3(maxLen, min.y, max.z),
+        new THREE.Vector3(maxLen, max.y, max.z),
+        new THREE.Vector3(minLen, max.y, max.z),
+      ];
+      // 绘制正方体
+      const partitionObject3D = drawSprayPartitionByBox(penlin, vertices, partitionNum - i);
+      sprayPartitionList.push(partitionObject3D);
+    }
+  }
+
+  return { sprayPartitionList };
+}
+
+function drawSprayPartitionByBox(group: THREE.Object3D, vertices: THREE.Vector3[], index) {
+  const partitionGroup = new THREE.Object3D();
+
+  const solidGeometry = new THREE.BufferGeometry();
+  solidGeometry.setFromPoints(vertices);
+
+  // 6个面 × 2个三角形 = 12个三角面索引
+  const solidIndices = [
+    0,
+    1,
+    2,
+    0,
+    2,
+    3, // 前面
+    4,
+    5,
+    6,
+    4,
+    6,
+    7, // 后面
+    0,
+    4,
+    7,
+    0,
+    7,
+    3, // 左面
+    1,
+    5,
+    6,
+    1,
+    6,
+    2, // 右面
+    3,
+    2,
+    6,
+    3,
+    6,
+    7, // 上面
+    0,
+    1,
+    5,
+    0,
+    5,
+    4, // 下面
+  ];
+  solidGeometry.setIndex(solidIndices);
+  solidGeometry.computeVertexNormals(); // 计算法线(必须加,否则不亮)
+
+  // 实心材质
+  const solidMaterial = new THREE.MeshLambertMaterial({
+    visible: false,
+    side: THREE.DoubleSide,
+  });
+
+  const solidBox = new THREE.Mesh(solidGeometry, solidMaterial.clone());
+  solidBox.name = 'spray' + index + '_solid';
+  partitionGroup.add(solidBox);
+
+  partitionGroup.name = 'spray' + index;
+  group.add(partitionGroup);
+
+  return partitionGroup;
+}
+
 export function destroy(modal, gateList) {
   gateList.value = null;
   modal.canvasContainer?.removeEventListener('mousemove', mouseoverEvent);
 }
+
+function partitionAnimate(intersect0, modal) {
+  const box = new THREE.Box3();
+  box.setFromObject(intersect0);
+  const point = box.getCenter(new THREE.Vector3());
+
+  const target0 = <THREE.Vector3>modal.orbitControls.target.clone();
+  const oldCameraPosition = <THREE.Vector3>modal.camera.position.clone();
+
+  const nor = point.clone().sub(oldCameraPosition.clone()).normalize();
+  const d = 28;
+
+  const animateObj = {
+    x1: target0.x, // 相机x
+    y1: target0.y, // 相机y
+    z1: target0.z, // 相机z
+    x2: oldCameraPosition.x, // 相机x
+    y2: oldCameraPosition.y, // 相机y
+    z2: oldCameraPosition.z, // 相机z
+  };
+  let newAnimateObj;
+  console.log('point---->', point);
+  if (oldCameraPosition.z > target0.z) {
+    newAnimateObj = {
+      x1: point.x,
+      y1: point.y - 10,
+      z1: point.z,
+      x2: point.x,
+      // y2: Math.abs(nor.y) * d + point.y,
+      // z2: Math.abs(nor.z) * d + point.z,
+      y2: 25.5 + point.y,
+      z2: 10.5 + point.z,
+    };
+    console.log('Math.abs(nor.z) * d', { x: point.x, y: Math.abs(nor.y) * d, z: Math.abs(nor.z) * d });
+  } else {
+    newAnimateObj = {
+      x1: point.x,
+      y1: point.y - 10,
+      z1: point.z,
+      x2: point.x,
+      // y2: Math.abs(nor.y) * d + point.y,
+      y2: 25.5 + point.y,
+      // z2: -Math.abs(nor.z) * d + point.z,
+      z2: -10.5 + point.z,
+    };
+    console.log('-Math.abs(nor.z) * d', { x: point.x, y: Math.abs(nor.y) * d, z: -Math.abs(nor.z) * d });
+  }
+
+  modal.orbitControls.renderEnabled = false;
+  modal.orbitControls.enabled = false;
+  gsap.fromTo(
+    animateObj,
+    {
+      x1: target0.x, // 相机x
+      y1: target0.y, // 相机y
+      z1: target0.z, // 相机z
+      x2: oldCameraPosition.x, // 相机x
+      y2: oldCameraPosition.y, // 相机y
+      z2: oldCameraPosition.z, // 相机z
+    },
+    {
+      ...newAnimateObj,
+      duration: 1,
+      ease: 'easeInCirc',
+      onUpdate: function (object) {
+        modal.orbitControls.target.set(object.x1, object.y1, object.z1);
+        modal.camera.position.set(object.x2, object.y2, object.z2);
+      },
+      onUpdateParams: [animateObj],
+      onComplete: function () {
+        modal.orbitControls.renderEnabled = true;
+        modal.orbitControls.enabled = true;
+        modal.orbitControls?.update();
+        modal.camera?.updateMatrixWorld();
+      },
+    }
+  );
+}

+ 289 - 0
src/views/vent/home/configurable/belt/threejs/sprinklerPanel.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="sprinkler-box" ref="panelRef">
+    <!-- 喷淋组件容器 -->
+    <div class="sprinkler-container" :class="{ on: isOn }">
+      <!-- SVG 喷淋头与水珠 -->
+      <div class="info-text"> {{ monitorData.location || 'XXXXX位置' }} </div>
+      <svg class="sprinkler-svg" viewBox="0 0 100 110" xmlns="http://www.w3.org/2000/svg">
+        <!-- 喷淋头主体 (梯形+矩形连接处) -->
+        <path class="head" d="M40,10 L60,10 L65,30 L35,30 Z" />
+        <rect class="head" x="40" y="0" width="20" height="10" />
+
+        <!-- 水珠阵列 (4行,共18个) -->
+        <!-- 第一行 (3个) -->
+        <circle class="drop" cx="40" cy="45" r="3" />
+        <circle class="drop" cx="50" cy="45" r="3" />
+        <circle class="drop" cx="60" cy="45" r="3" />
+
+        <!-- 第二行 (4个) -->
+        <circle class="drop" cx="35" cy="60" r="3" />
+        <circle class="drop" cx="45" cy="60" r="3" />
+        <circle class="drop" cx="55" cy="60" r="3" />
+        <circle class="drop" cx="65" cy="60" r="3" />
+
+        <!-- 第三行 (5个) -->
+        <circle class="drop" cx="30" cy="75" r="3" />
+        <circle class="drop" cx="40" cy="75" r="3" />
+        <circle class="drop" cx="50" cy="75" r="3" />
+        <circle class="drop" cx="60" cy="75" r="3" />
+        <circle class="drop" cx="70" cy="75" r="3" />
+
+        <!-- 第四行 (6个) -->
+        <circle class="drop" cx="25" cy="90" r="3" />
+        <circle class="drop" cx="35" cy="90" r="3" />
+        <circle class="drop" cx="45" cy="90" r="3" />
+        <circle class="drop" cx="55" cy="90" r="3" />
+        <circle class="drop" cx="65" cy="90" r="3" />
+        <circle class="drop" cx="75" cy="90" r="3" />
+      </svg>
+    </div>
+    <!-- 数据显示面板 -->
+    <!-- <div class="data-panel">
+      <div class="data-item">
+        <span class="label">当前状态</span>
+        <span class="value" :class="{ 'status-running': monitorData.deviceStatus == 1, 'status-stopped': monitorData.deviceStatus == 0 }">
+          {{ monitorData.deviceStatus != null ? (monitorData.deviceStatus == 0 ? '停止' : '运行') : '-' }}
+        </span>
+      </div>
+      <div class="data-item">
+        <span class="label">实时水压</span>
+        <span class="value">
+          {{ monitorData.MPa }}
+        </span>
+      </div>
+    </div> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, defineProps, onMounted, onUnmounted } from 'vue';
+  import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
+  import type { Scene, Camera } from 'three';
+
+  export interface SensorData {
+    location?: string;
+    deviceStatus?: number;
+    volume?: number;
+    MPa?: number;
+    sprayval?: number;
+  }
+
+  // 1. 定义 Props 接收外部数据
+  const props = defineProps({
+    // 时间
+    sensorData: {
+      type: Object as () => SensorData,
+      default: () => ({}),
+    },
+    position: {
+      type: Array as () => [number, number, number],
+      default: () => [0, 5, -10],
+    },
+    scale: {
+      type: Number,
+      default: 0.5,
+    },
+    threeScene: {
+      type: Object as () => Scene,
+      default: null,
+    },
+    instanceId: {
+      type: String,
+      default: '',
+    },
+  });
+
+  // 2. 内部状态:控制喷淋头开关
+  const isOn = ref(true); // 默认开启
+  const monitorData = ref<SensorData>(props.sensorData);
+
+  const panelRef = ref<HTMLElement | null>(null);
+  const cssObject = ref<CSS3DObject | null>(null);
+
+  // 面朝相机更新
+  const update = (camera: Camera) => {
+    if (cssObject.value) {
+      cssObject.value.lookAt(camera.position);
+      // 修复翻转
+      cssObject.value.rotation.z = Math.PI;
+      cssObject.value.rotation.x = Math.PI;
+    }
+  };
+
+  const updateMonitorData = (data: { sensorData: SensorData }) => {
+    monitorData.value = data.sensorData;
+  };
+
+  onMounted(() => {
+    if (!panelRef.value) return;
+    cssObject.value = new CSS3DObject(panelRef.value);
+    cssObject.value.position.set(props.position[0], props.position[1] - 2, props.position[2] + 3.4);
+    cssObject.value.rotation.z += Math.PI;
+    cssObject.value.rotation.x += Math.PI;
+    cssObject.value.scale.setScalar(props.scale);
+    cssObject.value.lookAt(props.position[0], props.position[1] + 30, props.position[2] - 10);
+    if (props.threeScene) {
+      props.threeScene.add(cssObject.value);
+    }
+  });
+
+  // 销毁方法
+  const destroy = () => {
+    if (cssObject.value && props.threeScene) {
+      props.threeScene.remove(cssObject.value);
+      (cssObject.value as any).element = null;
+      cssObject.value = null;
+    }
+  };
+
+  onUnmounted(() => {
+    destroy();
+  });
+
+  // 暴露方法
+  defineExpose({
+    id: props.instanceId,
+    cssObject: cssObject,
+    update,
+    destroy,
+    updateMonitorData,
+  });
+</script>
+
+<style scoped>
+  /* --- 基础样式 --- */
+  .sprinkler-box {
+    height: 340px;
+    width: 280px;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    /* border: 1px solid rgba(64, 158, 255, 0.3);
+    border-radius: 16px;
+    box-shadow: 0 0 20px rgba(0, 225, 255, 0.5);
+    background-color: #1a1e2622;
+    backdrop-filter: blur(20px); */
+  }
+
+  /* --- 喷淋容器 --- */
+  .sprinkler-container {
+    width: 220px;
+    height: 300px;
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+  }
+  /* 文字信息 */
+  .info-text {
+    width: 100%;
+    font-size: 48px;
+    margin-bottom: 20px;
+    text-align: center;
+    line-height: 1.5;
+    background-color: #000000;
+    font-weight: 600;
+    color: #a6f5ff;
+  }
+
+  /* --- SVG 样式 --- */
+  .sprinkler-svg {
+    width: 100%;
+    height: 100%;
+    overflow: visible;
+  }
+
+  /* 喷头主体 */
+  .head {
+    fill: #555;
+    transition: fill 0.3s ease;
+  }
+
+  /* 水珠圆点 */
+  .drop {
+    fill: #555;
+    transition: fill 0.3s ease;
+  }
+
+  /* 开启状态 (.on) 的样式 */
+  .sprinkler-container.on .head {
+    fill: #00f0ff;
+    filter: drop-shadow(0 0 5px #00f0ff);
+  }
+
+  .sprinkler-container.on .drop {
+    fill: #00f0ff;
+    animation: waterFlow 1.5s infinite ease-in-out;
+  }
+
+  /* --- 动画延迟设置 --- */
+  /* 为每一行水珠设置不同的延迟,制造流动感 */
+  .sprinkler-container.on .drop:nth-child(-n + 3) {
+    animation-delay: 0s;
+  }
+  .sprinkler-container.on .drop:nth-child(n + 4):nth-child(-n + 7) {
+    animation-delay: 0.1s;
+  }
+  .sprinkler-container.on .drop:nth-child(n + 8):nth-child(-n + 12) {
+    animation-delay: 0.2s;
+  }
+  .sprinkler-container.on .drop:nth-child(n + 13) {
+    animation-delay: 0.3s;
+  }
+
+  /* --- 关键帧动画 --- */
+  @keyframes waterFlow {
+    0%,
+    100% {
+      opacity: 0.6;
+      transform: translateY(0);
+    }
+    50% {
+      opacity: 1;
+      transform: translateY(2px);
+    }
+  }
+
+  /* --- 数据面板 --- */
+  .data-panel {
+    width: 100%;
+    display: flex;
+    justify-content: space-around;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 8px;
+    margin-bottom: 20px;
+    border: 1px solid rgba(255, 255, 255, 0.05);
+  }
+
+  .data-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .label {
+    font-size: 26px;
+    color: #8ba4c7;
+    margin-bottom: 4px;
+  }
+
+  .value {
+    font-size: 28px;
+    font-weight: bold;
+    font-family: 'Courier New', monospace;
+    color: #ffffff;
+    text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
+  }
+
+  .value.status-running {
+    color: #00eaff;
+    text-shadow: 0 0 8px #00eaff;
+  }
+
+  .value.status-stopped {
+    color: #666;
+  }
+</style>

+ 40 - 42
src/views/vent/home/configurable/components/content.vue

@@ -135,7 +135,7 @@
               :type="config.type"
               :columns="config.columns"
               :auto-scroll="config.autoScroll"
-              :data="config.data.data"
+              :data="config.data"
             />
           </template>
         </template>
@@ -256,48 +256,49 @@
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref } from 'vue';
 import { CommonItem, Config } from '../../../deviceManager/configurationTable/types';
-import MiniBoard from './detail/MiniBoard.vue';
-import TimelineList from './detail/TimelineList.vue';
-import TimelineListNew from './detail/TimelineListNew.vue';
-import CustomList from './detail/CustomList.vue';
-import CustomGallery from './detail/CustomGallery.vue';
-import ComplexList from './detail/ComplexList.vue';
-import GalleryList from './detail/GalleryList.vue';
-import CustomTable from './detail/CustomTable.vue';
-import CustomChart from './detail/CustomChart.vue';
 import { clone } from 'lodash-es';
 import { getData, getFormattedText } from '../hooks/helper';
-import BlastDelta from '../../../monitorManager/deviceMonitor/components/device/modal/blastDelta.vue';
-import QHCurve from './preset/QHCurve.vue';
-import MeasureDetail from './preset/MeasureDetail.vue';
-import CustomTabs from './preset/CustomTabs.vue';
-import AIChat from '/@/components/AIChat/MiniChat.vue';
-import DeviceAlarm from './preset/DeviceAlarm.vue';
-import SelectCs from './preset/SelectCs.vue';
-import MiniBoardNew from './detail/MiniBoard-New.vue';
-import Partition from './preset/partition.vue';
-import SelectorDualChart from './preset/selectorDualChart.vue';
-import RadioLabel from './preset/radioLabel.vue';
-import ButtonList from './preset/buttonList.vue';
-import NitrogenBtnList from './preset/nitrogenBtnList.vue';
-import cardList from './preset/cardList.vue';
-import generalList from './preset/generalList.vue';
-import GateBoard from '../belt/components/detail/gateBoard.vue';
-import fireGateBoard from '../belt/components/detail/fireGateBoard.vue';
-import PersonPositioning from './preset/PersonPositioning.vue';
+
+const MiniBoard = defineAsyncComponent(() => import('./detail/MiniBoard.vue'));
+const TimelineList = defineAsyncComponent(() => import('./detail/TimelineList.vue'));
+const TimelineListNew = defineAsyncComponent(() => import('./detail/TimelineListNew.vue'));
+const CustomList = defineAsyncComponent(() => import('./detail/CustomList.vue'));
+const CustomGallery = defineAsyncComponent(() => import('./detail/CustomGallery.vue'));
+const ComplexList = defineAsyncComponent(() => import('./detail/ComplexList.vue'));
+const GalleryList = defineAsyncComponent(() => import('./detail/GalleryList.vue'));
+const CustomTable = defineAsyncComponent(() => import('./detail/CustomTable.vue'));
+const CustomChart = defineAsyncComponent(() => import('./detail/CustomChart.vue'));
+const BlastDelta = defineAsyncComponent(() => import('../../../monitorManager/deviceMonitor/components/device/modal/blastDelta.vue'));
+const QHCurve = defineAsyncComponent(() => import('./preset/QHCurve.vue'));
+const MeasureDetail = defineAsyncComponent(() => import('./preset/MeasureDetail.vue'));
+const CustomTabs = defineAsyncComponent(() => import('./preset/CustomTabs.vue'));
+const AIChat = defineAsyncComponent(() => import('/@/components/AIChat/MiniChat.vue'));
+const DeviceAlarm = defineAsyncComponent(() => import('./preset/DeviceAlarm.vue'));
+const SelectCs = defineAsyncComponent(() => import('./preset/SelectCs.vue'));
+const MiniBoardNew = defineAsyncComponent(() => import('./detail/MiniBoard-New.vue'));
+const Partition = defineAsyncComponent(() => import('./preset/partition.vue'));
+const SelectorDualChart = defineAsyncComponent(() => import('./preset/selectorDualChart.vue'));
+const RadioLabel = defineAsyncComponent(() => import('./preset/radioLabel.vue'));
+const ButtonList = defineAsyncComponent(() => import('./preset/buttonList.vue'));
+const NitrogenBtnList = defineAsyncComponent(() => import('./preset/nitrogenBtnList.vue'));
+const cardList = defineAsyncComponent(() => import('./preset/cardList.vue'));
+const generalList = defineAsyncComponent(() => import('./preset/generalList.vue'));
+const GateBoard = defineAsyncComponent(() => import('../belt/components/detail/gateBoard.vue'));
+const fireGateBoard = defineAsyncComponent(() => import('../belt/components/detail/fireGateBoard.vue'));
+const PersonPositioning = defineAsyncComponent(() => import('./preset/PersonPositioning.vue'));
 // ==================== 新增皮带巷火灾监测组件 ====================
-import SensorStatusPanel from './belt/SensorStatusPanel.vue';
-import FireSensorAnalysis from './belt/FireSensorAnalysis.vue';
-import WarningResultList from './belt/WarningResultList.vue';
-import VehicleCOAnalysis from './belt/VehicleCOAnalysis.vue';
+const SensorStatusPanel = defineAsyncComponent(() => import('./belt/SensorStatusPanel.vue'));
+const FireSensorAnalysis = defineAsyncComponent(() => import('./belt/FireSensorAnalysis.vue'));
+const WarningResultList = defineAsyncComponent(() => import('./belt/WarningResultList.vue'));
+const VehicleCOAnalysis = defineAsyncComponent(() => import('./belt/VehicleCOAnalysis.vue'));
 // 首页组件
-import CustomListBelt from './belt/CustomListBelt.vue';
-import ComplexListBelt from './belt/ComplexListBelt.vue';
-import ComplexList1Belt from './belt/ComplexList1Belt.vue';
-import CustomTableBelt from './belt/CustomTableBelt.vue';
-import SprayControl from './belt/SprayControl.vue';
-import CameraList from './belt/CameraList.vue';
-import CameraListTest from './belt/CameraListTest.vue';
+const CustomListBelt = defineAsyncComponent(() => import('./belt/CustomListBelt.vue'));
+const ComplexListBelt = defineAsyncComponent(() => import('./belt/ComplexListBelt.vue'));
+const ComplexList1Belt = defineAsyncComponent(() => import('./belt/ComplexList1Belt.vue'));
+const CustomTableBelt = defineAsyncComponent(() => import('./belt/CustomTableBelt.vue'));
+const SprayControl = defineAsyncComponent(() => import('./belt/SprayControl.vue'));
+const CameraList = defineAsyncComponent(() => import('./belt/CameraList.vue'));
+const CameraListTest = defineAsyncComponent(() => import('./belt/CameraListTest.vue'));
 
 const SysWindCard = defineAsyncComponent(() => import('./preset/SysWindCard.vue'));
 
@@ -378,9 +379,6 @@ const layoutConfig = computed(() => {
         const cfg = list.shift();
         if (!cfg) break;
         const data = getData(refData, cfg.readFrom, cfg.parser);
-        console.log(cfg.readFrom, '111111111');
-        console.log(refData, '111111111');
-        console.log(data, '111111111');
         arr.push({
           // overflow: true,
           ...item,

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

@@ -555,6 +555,7 @@
     background-image: var(--image-board-bg-m1);
     background-size: 100% 100%;
     margin: 5px 0 15px 0;
+    padding-top: 10px;
   }
 
   .mini-board_M:nth-child(2),

+ 2 - 2
src/views/vent/home/configurable/components/preset/SelectCs.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="select-cs">
-    <div class="select-item">
+    <div v-if="config && config.switch1 && config.switch2 && config.switch3" class="select-item">
       <div class="select-item-box">
         <a-select v-model:value="selectVal" :bordered="false" style="width: 100%">
           <a-select-option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</a-select-option>
@@ -16,7 +16,7 @@
         <span>{{ config.switch1.label[1] }}</span>
       </div>
     </div>
-    <div class="select-item">
+    <div v-if="config && config.switch2 && config.switch3" class="select-item">
       <div class="select-item-box1">
         <span>{{ config.switch2.label[0] }}</span>
         <a-switch

+ 11 - 8
src/views/vent/home/configurable/components/preset/SysWindCard.vue

@@ -43,10 +43,10 @@
   </a-space>
 </template>
 <script lang="ts" setup>
-  import { inRange } from 'lodash-es';
+  import { defaultTo, inRange } from 'lodash-es';
   import { getFormattedText } from '../../hooks/helper';
-  import { useEventListener, useScroll } from '@vueuse/core';
-  import { ref } from 'vue';
+  // import { useEventListener, useScroll } from '@vueuse/core';
+  // import { ref } from 'vue';
 
   // import { get } from 'lodash-es';
   // import { computed } from 'vue';
@@ -63,13 +63,16 @@
   //   defineEmits(['click']);
 
   // 判断val是否在low、high指定的范围内
-  function isOverLimit(val: string, low?: string, high?: string) {
-    const l = low ? parseFloat(low) : -Infinity;
-    const h = high ? parseFloat(high) : Infinity;
-    return !inRange(parseFloat(val), l, h);
+  function isOverLimit(val: string, low: string = '', high: string = '') {
+    const l = parseFloat(low);
+    const h = parseFloat(high);
+    const v = parseFloat(val);
+
+    console.log('debug rrr', defaultTo(v, 0), defaultTo(l, -Infinity), defaultTo(h, Infinity));
+    return !inRange(defaultTo(v, 0), defaultTo(l, -Infinity), defaultTo(h, Infinity));
   }
 
-  const container = ref<HTMLElement>();
+  // const container = ref<HTMLElement>();
   // const { y, directions } = useScroll(container); // 当前滚动位置(响应式)
 
   // const STEP = 234;

+ 8 - 2
src/views/vent/home/configurable/components/three3D.vue

@@ -55,8 +55,14 @@
         pidai: {
           render: null,
           group: modalGroup ? modalGroup : null,
-          newP: { x: -0.5914335489879506, y: 27.923772285542388, z: -12.163745046232812 },
-          newT: { x: -0.5582925489879514, y: -7.4417937144576145, z: 0.07314295376718441 },
+          newP: { x: -56.54402372998481, y: 80.01047089769048, z: -30.089368652402282 },
+          newT: { x: -56.54402372998481, y: -10.747834071844238, z: -3.2453629571876617 },
+        },
+        'pidai-fire-door': {
+          render: null,
+          group: modalGroup ? modalGroup : null,
+          newP: { x: 64.36035750268913, y: 55.854573154864724, z: -0.07480379918286666 },
+          newT: { x: -0.55829999999998, y: -27.441793999999998, z: 0.07314299999999997 },
         },
       };
       await initModal();

+ 13 - 0
src/views/vent/home/configurable/configurable.api.fireDoorMonitor.ts

@@ -0,0 +1,13 @@
+import { defHttp } from '/@/utils/http/axios';
+import _ from 'lodash';
+
+enum Api {
+  manageFireGateHome = '/ventanaly-device/monitor/disaster/manageFireGateHome',
+  manageFireList = '/safety/ventanalyManageSystem/list',
+  doorControl = '/ventanaly-device/safety/ventanalyMonitorData/doorControl',
+}
+export const manageFireGateHome = (params) => defHttp.post({ url: Api.manageFireGateHome, params });
+
+export const manageFireList = (params) => defHttp.get({ url: Api.manageFireList, params });
+
+export const doorControlApi = (params) => defHttp.put({ url: Api.doorControl, params });

+ 124 - 214
src/views/vent/home/configurable/configurable.data.fireDoorMonitor.ts

@@ -5,7 +5,7 @@ export const testFireDoorMonitor: Config[] = [
   {
     deviceType: 'fireMonitor',
     moduleName: '设备监测与分析',
-    pageType: 'fireMonitorLeft',
+    pageType: 'fireMonitor',
     moduleData: {
       header: {
         show: false,
@@ -42,31 +42,31 @@ export const testFireDoorMonitor: Config[] = [
       list: [],
       preset: [
         {
-          readFrom: 'fmhjcInfo[0]',
+          readFrom: 'devTypeCountInfo',
           list: [
             {
               title: '温度传感器',
               contentTop: [
                 {
                   label: '平均值',
-                  code: 'avg',
+                  code: '${modelsensor_temperature.aveVal}',
                   color: 'white',
                 },
                 {
                   label: '最大值',
-                  code: 'max',
+                  code: '${modelsensor_temperature.maxVal}',
                   color: 'white',
                 },
                 {
                   label: '最小值',
-                  code: 'min',
+                  code: '${modelsensor_temperature.minVal}',
                   color: 'white',
                 },
               ],
               contents: [
                 {
                   label: '是否报警',
-                  code: 'alarm',
+                  code: '${modelsensor_temperature.isAlarm}',
                   trans: {
                     true: '报警',
                     false: '正常',
@@ -75,10 +75,10 @@ export const testFireDoorMonitor: Config[] = [
                 },
                 {
                   label: '最大值产生于',
-                  code: 'maxTime',
+                  code: '${modelsensor_temperature.maxTime}',
                   color: 'white',
                   info: {
-                    code: 'pos',
+                    code: '${modelsensor_temperature.maxStrInstallPos}',
                   },
                 },
               ],
@@ -88,24 +88,24 @@ export const testFireDoorMonitor: Config[] = [
               contentTop: [
                 {
                   label: '平均值',
-                  code: 'avg',
+                  code: '${modelsensor_co.aveVal}',
                   color: 'white',
                 },
                 {
                   label: '最大值',
-                  code: 'max',
+                  code: '${modelsensor_co.maxVal}',
                   color: 'white',
                 },
                 {
                   label: '最小值',
-                  code: 'min',
+                  code: '${modelsensor_co.minVal}',
                   color: 'white',
                 },
               ],
               contents: [
                 {
                   label: '是否报警',
-                  code: 'alarm',
+                  code: '${modelsensor_co.isAlarm}',
                   trans: {
                     true: '报警',
                     false: '正常',
@@ -114,37 +114,37 @@ export const testFireDoorMonitor: Config[] = [
                 },
                 {
                   label: '最大值产生于',
-                  code: 'maxTime',
+                  code: '${modelsensor_co.maxTime}',
                   color: 'white',
                   info: {
-                    code: 'pos',
+                    code: '${modelsensor_co.maxStrInstallPos}',
                   },
                 },
               ],
             },
             {
-              title: 'HCl传感器',
+              title: '光纤测温传感器',
               contentTop: [
                 {
                   label: '平均值',
-                  code: '${avg}',
+                  code: '${fiber.aveVal}',
                   color: 'white',
                 },
                 {
                   label: '最大值',
-                  code: 'max',
+                  code: '${fiber.maxVal}',
                   color: 'white',
                 },
                 {
                   label: '最小值',
-                  code: 'min',
+                  code: '${fiber.minVal}',
                   color: 'white',
                 },
               ],
               contents: [
                 {
                   label: '是否报警',
-                  code: 'alarm',
+                  code: '${fiber.isAlarm}',
                   trans: {
                     true: '报警',
                     false: '正常',
@@ -153,50 +153,93 @@ export const testFireDoorMonitor: Config[] = [
                 },
                 {
                   label: '最大值产生于',
-                  code: 'maxTime',
+                  code: '${fiber.maxTime}',
                   color: 'white',
                   info: {
-                    code: 'pos',
+                    code: '${fiber.maxStrInstallPos}',
                   },
                 },
               ],
             },
+          ],
+        },
+      ],
+    },
+    showStyle: {
+      size: 'width:400px;height:480px;',
+      version: '原版',
+      position: 'top:45px;left:25px;',
+    },
+  },
+  {
+    deviceType: 'fireMonitor',
+    moduleName: '监测设备状态',
+    pageType: 'fireMonitor',
+    moduleData: {
+      header: {
+        show: false,
+        readFrom: '',
+        selector: {
+          show: false,
+          value: '',
+        },
+        slot: {
+          show: false,
+          value: '',
+        },
+      },
+      background: {
+        show: false,
+        type: 'video',
+        link: '',
+      },
+      layout: {
+        direction: 'column',
+        items: [
+          {
+            name: 'fire_sensor_analysis',
+            basis: '100%',
+          },
+        ],
+      },
+      board: [],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      complex_list: [],
+      list: [],
+      preset: [
+        {
+          readFrom: 'devTypeCountInfo',
+          config: [
             {
-              title: '光纤测温传感器',
-              contentTop: [
-                {
-                  label: '平均值',
-                  code: 'avg',
-                  color: 'white',
-                },
+              title: '火焰传感器',
+              items: [
                 {
-                  label: '最大值',
-                  code: 'max',
-                  color: 'white',
+                  label: '是否报警',
+                  code: '${modelsensor_fire.isAlarm}',
+                  trans: { true: '报警', false: '正常' },
                 },
                 {
-                  label: '最小值',
-                  code: 'min',
-                  color: 'white',
+                  label: '最大值产生于',
+                  code: '${modelsensor_fire.maxTime}',
+                  info: { code: '${modelsensor_fire.maxStrInstallPos}' },
                 },
               ],
-              contents: [
+            },
+            {
+              title: '烟雾传感器',
+              items: [
                 {
                   label: '是否报警',
-                  code: 'alarm',
-                  trans: {
-                    true: '报警',
-                    false: '正常',
-                  },
-                  color: 'green',
+                  code: '${modelsensor_smoke.isAlarm}',
+                  trans: { true: '报警', false: '正常' },
                 },
                 {
                   label: '最大值产生于',
-                  code: 'maxTime',
-                  color: 'white',
-                  info: {
-                    code: 'pos',
-                  },
+                  code: '${modelsensor_smoke.maxTime}',
+                  info: { code: '${modelsensor_smoke.maxStrInstallPos}' },
                 },
               ],
             },
@@ -205,111 +248,12 @@ export const testFireDoorMonitor: Config[] = [
       ],
     },
     showStyle: {
-      size: 'width:400px;height:800px;',
+      size: 'width:400px;height:295px;',
       version: '原版',
-      position: 'top:60px;left:25px;',
+      position: 'top:540px;left:25px;',
     },
   },
-
-  // ==================== 右侧 ====================
-
-  // {
-  //   deviceType: 'fireMonitor',
-  //   moduleName: '设备监测与分析',
-  //   pageType: 'fireMonitor',
-  //   moduleData: {
-  //     header: {
-  //       show: false,
-  //       readFrom: '',
-  //       selector: {
-  //         show: false,
-  //         value: '${beltName}',
-  //       },
-  //       slot: {
-  //         show: false,
-  //         value: '',
-  //       },
-  //     },
-  //     background: {
-  //       show: false,
-  //       type: 'video',
-  //       link: '',
-  //     },
-  //     layout: {
-  //       direction: 'column',
-  //       items: [
-  //         {
-  //           name: 'fire_sensor_analysis',
-  //           basis: '100%',
-  //         },
-  //       ],
-  //     },
-  //     board: [],
-  //     chart: [],
-  //     gallery: [],
-  //     gallery_list: [],
-  //     table: [],
-  //     list: [],
-  //     complex_list: [],
-  //     preset: [
-  //       {
-  //         readFrom: 'sensorAnalysis',
-  //         config: [
-  //           {
-  //             title: '火焰传感器', // 对应 UI 图中的组标题
-  //             items: [
-  //               {
-  //                 label: '是否报警',
-  //                 code: '${hy.alarm}', // 占位符
-  //                 status: '1', // 状态映射逻辑 (0:正常, 1:报警)
-  //               },
-  //               {
-  //                 label: '最大值产生于',
-  //                 code: 'maxTime',
-  //                 info: '1521胶运顺槽600m',
-  //               },
-  //             ],
-  //           },
-  //           {
-  //             title: '温度传感器',
-  //             items: [
-  //               {
-  //                 label: '是否报警',
-  //                 code: 'isAlarm', // 占位符
-  //                 status: 'isAlarm', // 状态映射逻辑 (0:正常, 1:报警)
-  //               },
-  //               {
-  //                 label: '最大值产生于',
-  //                 code: 'maxTime',
-  //                 info: '1521胶运顺槽600m',
-  //               },
-  //             ],
-  //           },
-  //           {
-  //             title: '烟雾传感器',
-  //             items: [
-  //               {
-  //                 label: '是否报警',
-  //                 code: 'isAlarm', // 占位符
-  //                 status: 'isAlarm', // 状态映射逻辑 (0:正常, 1:报警)
-  //               },
-  //               {
-  //                 label: '最大值产生于',
-  //                 code: 'maxTime',
-  //                 info: '1521胶运顺槽600m',
-  //               },
-  //             ],
-  //           },
-  //         ],
-  //       },
-  //     ],
-  //   },
-  //   showStyle: {
-  //     size: 'width:400px;height:225px;',
-  //     version: '原版',
-  //     position: 'top:10px;right:25px;',
-  //   },
-  // },
+  // 防火门监测
   {
     deviceType: 'fireMonitor', //
     moduleName: '防火门监测',
@@ -351,65 +295,41 @@ export const testFireDoorMonitor: Config[] = [
       complex_list: [],
       preset: [
         {
-          readFrom: 'deviceInfo.gate.datalist',
+          readFrom: 'devMonitorMap.door',
           type: 'C',
           config: {
-            tilte: 'strname',
+            tilte: '${readData.frontGateOpen.value}',
             items: [
               {
-                label: '前门状态',
-                value: '${readData.frontGateOpen}',
-                trans: {
-                  1: '打开',
-                  0: '关闭',
-                },
-              },
-              {
-                label: '后门状态',
-                value: '${readData.rearGateOpen}',
+                label: '防火门状态',
+                value: '${readData.frontGateOpen.value}',
                 trans: {
-                  1: '打开',
-                  0: '关闭',
+                  '1': '打开',
+                  '0': '关闭',
                 },
               },
               {
                 label: '网络状态',
                 value: '${netStatus}',
                 trans: {
-                  1: '连接',
-                  0: '断开',
+                  '1': '连接',
+                  '0': '断开',
                 },
               },
             ],
           },
         },
       ],
-      mock: {
-        deviceInfo: {
-          gate: {
-            datalist: [
-              {
-                strname: '设备1',
-                readData: {
-                  frontGateOpen: 1,
-                  rearGateOpen: 0,
-                },
-                netStatus: 1,
-              },
-            ],
-          },
-        },
-      },
     },
     showStyle: {
-      size: 'width:400px;height:420px;',
+      size: 'width:400px;height:410px;',
       version: '原版',
       position: 'top:15px;right:25px;',
     },
   },
-  // ==================== 人员定位与CO浓度关联分析 ====================
+  // 人员定位
   {
-    deviceType: 'fireMonitor',
+    deviceType: 'peopleInfo',
     moduleName: '人员定位',
     pageType: 'fireMonitor',
     moduleData: {
@@ -418,7 +338,7 @@ export const testFireDoorMonitor: Config[] = [
         readFrom: '',
         selector: {
           show: false,
-          value: '${beltName}',
+          value: '',
         },
         slot: {
           show: false,
@@ -434,7 +354,7 @@ export const testFireDoorMonitor: Config[] = [
         direction: 'column',
         items: [
           {
-            name: 'person_position',
+            name: 'table',
             basis: '100%',
           },
         ],
@@ -443,43 +363,33 @@ export const testFireDoorMonitor: Config[] = [
       chart: [],
       gallery: [],
       gallery_list: [],
-      table: [],
-      list: [],
-      complex_list: [],
-      preset: [
+      table: [
         {
-          readFrom: 'vehicleCOAnalysis',
-          list: [
+          type: 'A',
+          readFrom: 'locationInfo',
+          columns: [
             {
-              type: 'control',
-              title: '人员位置',
-              layout: 'horizontal',
-              readFrom: '',
-              items: [
-                {
-                  label: '状态',
-                  code: 'isRisk',
-                },
-              ],
+              name: '工作面名称',
+              prop: 'devName',
             },
             {
-              type: 'analysis',
-              title: '设备情况',
-              readFrom: 'analysisList',
-              items: [
-                {
-                  label: '',
-                  code: 'pos',
-                },
-                {
-                  label: '',
-                  code: 'analysisText',
-                },
-              ],
+              name: '人员名称',
+              prop: 'personNameList',
+            },
+            {
+              name: '门内人数',
+              prop: 'personNum',
+            },
+            {
+              name: '设备位置',
+              prop: 'strInstallPos',
             },
           ],
         },
       ],
+      list: [],
+      complex_list: [],
+      preset: [],
     },
     showStyle: {
       size: 'width:400px;height:400px;',

+ 100 - 88
src/views/vent/home/configurable/fireDoorMonitor.vue

@@ -1,10 +1,11 @@
-<!-- belt-new.vue -->
 <template>
   <div class="company-home">
     <!-- 顶部标题栏 + 下拉选择 -->
-    <customHeader> XXX工作面防火门监控系统 </customHeader>
+    <div class="top-bg">
+      <div class="main-title">工作面防火门监控系统</div>
+    </div>
     <div class="modal-box" id="modalBox">
-      <!-- <Three3D :modal-name="modalName" /> -->
+      <Three3D :modal-name="modalName" />
     </div>
     <!-- 主体区域 -->
     <div class="border">
@@ -14,116 +15,107 @@
           ref="select"
           v-model:value="deviceId"
           @change="handleDeviceChange"
-          popupClassName="drop"
-          :field-names="fieldNames"
           :options="selectorOptions"
-          :dropdownStyle="{
-            width: '380px',
-            background: 'transparent',
-            borderBottom: '1px solid #ececec66',
-            backdropFilter: 'blur(50px)',
-            color: '#fff',
-          }"
+          placeholder="请选择场景"
         />
       </div>
       <!-- 配置模块区 -->
-      <template v-if="pageType == 'fireMonitor'">
-        <ModuleCommon
-          v-for="cfg in configs"
-          :key="cfg.deviceType"
-          :show-style="cfg.showStyle"
-          :module-data="cfg.moduleData"
-          :module-name="cfg.moduleName"
-          :device-type="cfg.deviceType"
-          :page-type="cfg.pageType"
-          :data="data"
-          :visible="true"
-        />
-      </template>
+      <ModuleCommon
+        v-for="cfg in configs"
+        :key="cfg.deviceType"
+        :show-style="cfg.showStyle"
+        :module-data="cfg.moduleData"
+        :module-name="cfg.moduleName"
+        :device-type="cfg.deviceType"
+        :page-type="cfg.pageType"
+        :data="data"
+        :visible="true"
+        @refresh-data="loadFireGateData"
+      />
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref, watch, computed } from 'vue';
-  import customHeader from './belt/components/customHeader-belt.vue';
+  import { onMounted, onUnmounted, ref } from 'vue';
   import { useInitConfigs, useInitPage } from './hooks/useInit';
   import { testFireDoorMonitor } from './configurable.data.fireDoorMonitor';
+  import { manageFireGateHome, manageFireList } from './configurable.api.fireDoorMonitor';
   import ModuleCommon from './belt/components/ModuleCommon.vue';
   import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
-  import { useRouter, useRoute } from 'vue-router';
-  import { modalAnimate } from './belt/threejs/belt.threejs';
 
   // 初始化配置
   const { configs, fetchConfigs } = useInitConfigs();
   const { updateEnhancedConfigs, updateData, data } = useInitPage('工作面防火门监控系统');
-  // const modalName = ref('Fire-doorf');
-  // const pageType = computed(() => {
-  //   const currentType = route.params.type as string;
-  //   return currentType;
-  // });
-  const pageType = ref('fireMonitor');
-  const fieldNames = { label: 'name', value: 'id' }; // 下拉框字段映射
+  const modalName = ref('pidai-fire-door');
 
-  // 下拉框选项
-  const selectorOptions = [
-    { id: '1', label: '主运巷皮带 1' },
-    { id: '2', label: '主运巷皮带 2' },
-  ];
+  // 设备场景下拉框选项
+  const selectorOptions = ref<{ value: any; label: any }[]>([]);
+  const deviceId = ref<string>('');
 
-  const deviceId = ref('1');
-
-  // 切换设备事件
-  function handleDeviceChange(param) {
-    // 查询数据接口
+  // 切换设备事件:当下拉框改变时触发
+  function handleDeviceChange(value: string) {
+    if (value) {
+      loadFireGateData();
+    }
   }
 
-  // 刷新数据
-  function refresh() {
-    fetchConfigs('belt').then(() => {
-      configs.value = testFireDoorMonitor;
-      updateEnhancedConfigs(configs.value);
-    });
-  }
+  onMounted(() => {
+    // 1. 获取场景列表
+    manageFireList({
+      strtype: 'sys_door',
+      pagetype: 'normal',
+    }).then((res) => {
+      if (res.records && res.records.length > 0) {
+        res.records.forEach((item) => {
+          selectorOptions.value.push({
+            value: item.id,
+            label: item.systemname,
+          });
+        });
 
-  // // 定时刷新
-  // function initInterval() {
-  //   setInterval(() => {
-  //     refresh();
-  //   }, 60000);
-  // }
+        // 2. 默认选中第一个场景
+        deviceId.value = selectorOptions.value[0].value;
 
-  function changePage(pageTypeStr: string) {
-    pageType.value = pageTypeStr;
-    refresh();
-  }
+        // 3. 初始化页面配置并加载第一个场景的数据
+        fetchConfigs('fireMonitor').then(() => {
+          configs.value = testFireDoorMonitor;
+          updateEnhancedConfigs(configs.value);
+          loadFireGateData();
+        });
+      }
+    });
+  });
 
-  // watch(
-  //   // 监听动态路由参数 :type
-  //   () => route.params.type,
-  //   (newVal) => {
-  //     if (newVal) {
-  //       console.log('切换页面类型:', newVal);
-  //       refresh(); // 切换路由自动刷新
-  //     }
-  //   }
-  // );
+  // 加载防火门数据的方法:根据当前选中的 deviceId 请求数据
+  const loadFireGateData = () => {
+    if (!deviceId.value) return;
 
-  async function initModalAnimate(modal) {
-    console.log('初始化模型', modal);
-    modal.isRender = true;
+    manageFireGateHome({
+      sysId: deviceId.value,
+    }).then((res) => {
+      // 处理数据:为 door 数组中的每一项添加 systemDeviceId
+      if (res.devMonitorMap && Array.isArray(res.devMonitorMap.door)) {
+        res.devMonitorMap.door.forEach((item) => {
+          item.systemDeviceId = deviceId.value;
+        });
+      }
+      updateData(res);
+    });
+  };
 
-    await modalAnimate(modal);
-  }
+  // 设置定时刷新,每10秒执行一次
+  const interval = setInterval(() => {
+    loadFireGateData();
+  }, 10000);
 
-  onMounted(() => {
-    refresh();
-    // initInterval();
+  onUnmounted(() => {
+    clearInterval(interval);
   });
 </script>
-
 <style lang="less" scoped>
   .company-home {
+    --image-vent-header: url('/@/assets/images/vent/vent-header1.png');
     background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
     width: 100%;
     height: 100%;
@@ -132,10 +124,20 @@
     font-family: 'Microsoft YaHei', sans-serif;
     .top-bg {
       width: 100%;
-      height: 56px;
+      height: 100px;
+      background: var(--image-vent-header) no-repeat top;
       position: absolute;
-      margin-top: 10px;
       z-index: 1;
+      .main-title {
+        height: 80px;
+        font-family: 'douyuFont';
+        font-size: 22px;
+        letter-spacing: 2px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        padding: 0 0 10px 0;
+      }
     }
     .header-container {
       position: absolute;
@@ -146,10 +148,11 @@
 
     .border {
       width: 100%;
-      height: 94%;
+      height: calc(100% - 70px);
       background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
       background-size: 100% 100%;
       position: relative;
+      top: 60px;
       overflow: hidden;
 
       .box-container {
@@ -188,9 +191,10 @@
         color: #fff !important;
         font-size: 20px;
       }
-      .zxm-select-arrow {
-        color: #fff;
-      }
+    }
+    :deep(.zxm-select-arrow) {
+      color: #fff;
+      right: 50px;
     }
 
     // 中间预警结果区
@@ -300,4 +304,12 @@
     position: absolute;
     z-index: 1;
   }
+  :deep(.table__content) {
+    .table__content_label {
+      width: 100% !important;
+    }
+    .table__content_list {
+      width: 100% !important;
+    }
+  }
 </style>

+ 585 - 0
src/views/vent/monitorManager/fanLocalMonitor/components/conditionAssistanceCY.vue

@@ -0,0 +1,585 @@
+<template>
+  <BasicModal
+    @register="register"
+    title="局部通风机运行工况智能决策"
+    :maskStyle="{ backgroundColor: '#000000aa', backdropFilter: 'blur(3px)' }"
+    :width="isComputeGas ? '1350px' : '950px'"
+    v-bind="$attrs"
+    @ok="onSubmit"
+    :closeFunc="onCancel"
+    :canFullscreen="false"
+    :destroyOnClose="true"
+    :footer="null"
+    :maskClosable="false"
+  >
+    <div class="modal-box">
+      <div v-if="isComputeGas" class="left-box" style="width: 550px; height: 400px">
+        <BarAndLine
+          class="echarts-line"
+          xAxisPropType="time"
+          :dataSource="monitorData"
+          height="400px"
+          :chartsColumns="chartsColumnList"
+          :option="echatsOption"
+        />
+      </div>
+      <div class="center-box">
+        <a-spin :spinning="loadding" tip="正在计算,请稍等。。。">
+          <div ref="ChartRef" class="info-echarts" :style="{ width: isComputeGas ? '450px' : '520px', height: '350px' }"></div>
+        </a-spin>
+      </div>
+      <div class="right-box">
+        <!-- <div class="box-title">曲线方程</div> -->
+        <dv-decoration7 style="height: 20px">
+          <div class="box-title">曲线方程</div>
+        </dv-decoration7>
+        <div class="info-lines">
+          <div v-for="(item, index) in lineEquation" class="info-item" :key="index">
+            <div class="title">{{ item }}</div>
+          </div>
+        </div>
+      </div>
+      <div class="tip-box">
+        <div class="title">最佳工况点 <SendOutlined class="ml-5px" /></div>
+        <div class="tip-container" :style="{ width: isComputeGas ? '898px' : '1000px' }">
+          <template v-if="resultObj && isHaCross">
+            <div class="ml-10px">
+              <span>风量:</span>
+              <span style="color: #d066ff; padding: 0 10px; font-weight: 600">{{ formatNum(resultObj.x) }} m³/min</span>
+            </div>
+            <div class="ml-10px">
+              <span>频率</span>
+              <span style="color: #ffbe34; padding: 0 10px; font-weight: 600">{{ formatNum(resultObj.y) }} Hz</span>
+            </div>
+          </template>
+          <div v-else-if="isHaCross" class="ml-10px">暂无</div>
+          <div v-else style="color: #ffbe34; padding: 0 10px; font-weight: 600" class="ml-10px">无有效工况点</div>
+        </div>
+      </div>
+    </div>
+    <div class="setting-box">
+      <div class="right-inputs">
+        <div class="vent-flex-row">
+          <div class="input-title">风量(m³/min):</div>
+          <InputNumber :disabled="isComputeGas" class="input-box" size="large" v-model:value="uQ1" />
+          <!-- <div class="input-title">风压(Pa):</div>
+          <InputNumber :disabled="isComputeGas" class="input-box" size="large" v-model:value="uH" /> -->
+          <div v-if="!isComputeGas" class="btn btn1" @click="makeLine">决策工况</div>
+          <template v-else>
+            <div class="btn btn1" @click="startCompute">一键调控</div>
+            <div class="btn btn1" @click="resetCompute">一键复位</div>
+          </template>
+        </div>
+      </div>
+    </div>
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  //ts语法
+  import { ref, onMounted, reactive, nextTick, computed } from 'vue';
+  import echarts from '/@/utils/lib/echarts';
+  import { option, initDataCY, fanInfoData, chartsColumnList, echatsOption } from '../fanLocal.data';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { useForm } from '/@/components/Form/index';
+  import { Input, InputNumber } from 'ant-design-vue';
+  import { Decoration7 as DvDecoration7 } from '@kjgl77/datav-vue3';
+  import { message } from 'ant-design-vue';
+  import { formatNum } from '/@/utils/ventutil';
+  import BarAndLine from '/@/components/chart/BarAndLine.vue';
+  import { cloneDeep } from 'lodash-es';
+  import dayjs from 'dayjs';
+  import { SendOutlined } from '@ant-design/icons-vue';
+
+  const emit = defineEmits(['close', 'register', 'openModal']);
+  const props = defineProps({
+    dataSource: {
+      type: Array,
+      default: () => [],
+    },
+    frequency: {
+      type: Number,
+      default: 30,
+    },
+    m3: {
+      type: Number,
+      default: 670.8,
+    },
+    // gasWarningMax: { type: Number, default: 0.5 },
+    // gasWarningVal: { type: Number, default: 0.6 },
+    // windQuantity: { type: Number, default: 635.84 },
+  });
+  type AssistanceItemType = {
+    angle: number;
+    Hz: number;
+    a: number;
+    b: number;
+    c: number;
+    min: number;
+    max: number;
+  };
+
+  // 注册 modal
+  const [register, { closeModal }] = useModalInner((data) => {
+    nextTick(() => {
+      computeAssistance();
+      if (option['xAxis']) option['xAxis']['data'] = xData;
+      option['series'] = yDataList;
+      initEcharts();
+      isComputeGas.value = false;
+    });
+  });
+  const loadding = ref<boolean>(false);
+  const formShow = ref(false);
+  const formType = ref('');
+  const ChartRef = ref();
+  const myChart = ref();
+  const refresh = ref(true);
+  const xDataMax = 1000;
+  let xDataMin = 0;
+  const xData: any[] = [];
+  const yDataList: [] = [];
+  let lineNum = 0;
+  const lineEquation = ref<string[]>([]);
+  const assistanceData = ref([]);
+  const monitorData = ref([]);
+  const gasWarningVal = ref(0);
+  const gasWarningMax = ref(0.5);
+  const isComputeGas = ref(false);
+  const isStartCompute = ref(0);
+  const uHz = ref(0);
+  const uQ1 = ref(0);
+  const uQ = computed(() => {
+    if (uQ1.value) {
+      if (gasWarningVal.value) {
+        return ((uQ1.value * gasWarningVal.value) / gasWarningMax.value) * 1.08;
+      }
+      return uQ1.value;
+    } else {
+      return 0;
+    }
+  });
+  const uH = ref<number | undefined>(undefined); //  - 1000
+  const isHaCross = ref(true);
+  const resultObj = ref<{ x: number; y: number; Hz: number } | null>(null);
+
+  const [registerForm, {}] = useForm({
+    labelWidth: 120,
+    actionColOptions: {
+      span: 24,
+    },
+    compact: true,
+    showSubmitButton: true,
+    submitButtonOptions: {
+      text: '提交',
+      preIcon: '',
+    },
+    showResetButton: true,
+    resetButtonOptions: {
+      text: '关闭',
+      preIcon: '',
+    },
+    resetFunc: async () => {
+      formShow.value = false;
+    },
+  });
+
+  function computeAssistance() {
+    assistanceData.value = initDataCY();
+    lineNum = 0;
+    const assistanceDataList = [];
+    const lineEquationList: string[] = [];
+    for (const key in assistanceData.value) {
+      const item = assistanceData.value[key];
+      assistanceDataList.push(item);
+      lineEquationList.push(
+        `H = ${Number(item['b']) > 0 ? '' : '-'} ${Math.abs(Number(item['b'])).toFixed(4)}Q ${
+          Number(item['c']) > 0 ? '+' : '-'
+        } ${Math.abs(Number(item['c'])).toFixed(4)}`
+      );
+    }
+    lineEquation.value = lineEquationList;
+    lineNum = assistanceDataList.length;
+    xDataMin = 100;
+    const xDataMax = 300;
+    // const xDataMax = Math.max.apply(Math, assistanceDataList.map(item => { return item.max }))
+    fanInfoData.flfw = `${xDataMin}~${xDataMax}`;
+    const computeItem = (item: AssistanceItemType) => {
+      const min = item.min;
+      const max = item.max;
+      const HList: number[] = [];
+      for (let i = xDataMin; i <= xDataMax; i++) {
+        if (i < min) {
+          HList.push(null);
+        } else if (i > max) {
+          HList.push(null);
+        } else {
+          HList.push(item.a * i * i + item.b * i + item.c);
+        }
+      }
+      return HList;
+    };
+
+    for (const key in assistanceData.value) {
+      const element: AssistanceItemType = assistanceData.value[key];
+      const yData: number[] = computeItem(element);
+      const series = {
+        type: 'line',
+        smooth: true,
+        showSymbol: false,
+        symbol: 'none',
+        emphasis: {
+          focus: 'series',
+        },
+        itemStyle: { normal: { label: { show: false } } },
+        lineStyle: {
+          width: 1,
+          color: '#BF954D',
+        },
+        zlevel: 0,
+        z: 1,
+        data: yData,
+      };
+
+      yDataList.push(series);
+    }
+
+    for (let i = xDataMin; i <= xDataMax; i++) {
+      xData.push(i);
+    }
+  }
+
+  function computeR() {
+    const item: AssistanceItemType = assistanceData.value[0];
+    const y = item.b * uQ1.value + item.c;
+    if (y < 20 || y > 50) {
+      isHaCross.value = false;
+      resultObj.value = null;
+    } else {
+      isHaCross.value = true;
+      resultObj.value = { x: uQ1.value, y: y, Hz: 0 };
+    }
+    const series = {
+      type: 'effectScatter',
+      symbolSize: 5,
+      // symbolOffset:[1, 1],
+      showEffectOn: 'render',
+      // 涟漪特效相关配置。
+      rippleEffect: {
+        // 波纹的绘制方式,可选 'stroke' 和 'fill'。
+        brushType: 'stroke',
+      },
+      zlevel: 1,
+      z: 999,
+      itemStyle: {
+        color: '#C60000',
+      },
+      data: isHaCross.value ? [[uQ1.value - 100, y]] : [[uQ1.value - 100, -100]],
+    };
+    yDataList[lineNum] = series;
+  }
+
+  function startCompute() {
+    setTimeout(() => {
+      message.success('指令下发成功!');
+      isStartCompute.value = 1;
+    }, 800);
+  }
+
+  function resetCompute() {
+    setTimeout(() => {
+      message.success('指令下发成功!');
+      isStartCompute.value = -1;
+      tempList.length = 0;
+      n = 0;
+    }, 800);
+  }
+
+  function reSetLine() {
+    for (let i = 0; i < yDataList.length; i++) {
+      if (i !== lineNum && i != lineNum + 1) {
+        if (yDataList[i]) {
+          if (yDataList[i]['lineStyle']) yDataList[i]['lineStyle']['color'] = '#BF954D';
+          if (yDataList[i]['lineStyle']) yDataList[i]['lineStyle']['width'] = 1;
+          if (yDataList[i]['endLabel'] && yDataList[i]['endLabel']['color']) {
+            yDataList[i]['endLabel']['color'] = '#39E9FE99';
+          }
+          if (yDataList[i]['endLabel'] && yDataList[i]['endLabel']['backgroundColor']) yDataList[i]['endLabel']['backgroundColor'] = 'transparent';
+          if (yDataList[i]['z']) yDataList[i]['z'] = 1;
+        }
+      }
+    }
+  }
+
+  function onSubmit() {
+    emit('close');
+    closeModal();
+    ChartRef.value = null;
+    uQ.value = undefined;
+    uH.value = undefined;
+    formType.value = '';
+    refresh.value = true;
+    xData.length = 0;
+    yDataList.length = 0;
+    lineNum = 0;
+    lineEquation.value = [];
+    resultObj.value = null;
+    monitorData.value = [];
+    clearTimeout(timer);
+    timer = undefined;
+  }
+
+  async function onCancel() {
+    return new Promise((resolve) => {
+      if (isStartCompute.value == 0) {
+        onSubmit();
+        resolve(true);
+      } else {
+        message.warning('为保障矿井安全生产,请确保已复位!!!');
+        resolve(false);
+      }
+    });
+  }
+
+  function initEcharts() {
+    if (ChartRef.value) {
+      reSetLine();
+      myChart.value = echarts.init(ChartRef.value);
+      option && myChart.value.setOption(option);
+      refresh.value = false;
+      nextTick(() => {
+        setTimeout(() => {
+          refresh.value = true;
+        }, 0);
+      });
+    }
+  }
+
+  function makeLine() {
+    loadding.value = true;
+    setTimeout(() => {
+      computeR();
+      reSetLine();
+      option && myChart.value.setOption(option);
+      loadding.value = false;
+    }, 1200);
+  }
+
+  onMounted(() => {});
+</script>
+
+<style scoped lang="less">
+  .modal-box {
+    display: flex;
+    flex-direction: row;
+    background-color: #ffffff05;
+    padding: 20px 8px 0 8px;
+    border: 1px solid #00d8ff22;
+    position: relative;
+    // min-height: 600px;
+    .box-title {
+      width: calc(100% - 40px);
+      text-align: center;
+      background-color: #1dc1f522;
+    }
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 2px 0px;
+      margin: 10px 0;
+      line-height: 30px;
+      background-image: linear-gradient(to right, #39deff15, #3977e500);
+      &:first-child {
+        margin-top: 0;
+      }
+      .title {
+        width: 200px;
+        text-align: left;
+        padding-left: 20px;
+        color: #f1f1f1cc;
+      }
+      .value {
+        width: 150px;
+        color: #00d8ff;
+        padding-right: 20px;
+        text-align: right;
+      }
+    }
+    .right-box {
+      width: 320px;
+      .info-lines {
+        width: calc(100% - 2px);
+        height: 450px;
+        box-shadow: 0px 0px 50px #86baff08 inset;
+        overflow-y: auto;
+        margin-top: 5px;
+        .title {
+          width: 100%;
+          color: #f1f1f1cc;
+        }
+      }
+    }
+    .center-box {
+      flex: 1;
+      margin: 0 10px;
+      display: flex;
+      .info-echarts {
+        // background-color: #ffffff11;
+      }
+      .result-tip {
+        text-align: center;
+        background-color: #00000011;
+        line-height: 28px;
+        margin: 10px 50px 0 50px;
+        border: 1px solid #00d8ff22;
+        border-radius: 2px;
+      }
+    }
+    .tip-box {
+      width: 1040px;
+      height: 44px;
+      position: absolute;
+      top: 417px;
+      display: flex;
+      padding: 0 20px;
+      .title {
+        width: 142px;
+        height: 43px;
+        display: flex;
+        align-items: center;
+        padding-left: 30px;
+        background-image: url('@/assets/images/fanlocal-tip/tip-title.png');
+        position: relative;
+        &::before {
+          content: '';
+          display: inline-block;
+          position: absolute;
+          width: 31px;
+          height: 31px;
+          top: 5px;
+          left: -8px;
+          background-image: url('@/assets/images/fanlocal-tip/tip-icon.png');
+        }
+      }
+      .tip-container {
+        width: 898px;
+        height: 44px;
+        line-height: 44px;
+        display: flex;
+        background-image: url('@/assets/images/fanlocal-tip/tip-bg.png');
+        background-size: cover;
+      }
+    }
+  }
+  .setting-box {
+    width: 1170px;
+    height: 70px;
+    margin: 10px 0;
+    background-color: #ffffff05;
+    border: 1px solid #00d8ff22;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .right-inputs {
+      width: 100%;
+      display: flex;
+      height: 40px;
+      margin: 0 10px;
+      justify-content: space-between;
+    }
+    .left-buttons {
+      display: flex;
+      height: 40px;
+
+      .btn {
+        margin: 0 10px;
+      }
+    }
+    .border-clip {
+      width: 1px;
+      height: 25px;
+      border-right: 1px solid #8b8b8b77;
+    }
+    .input-title {
+      max-width: 150px;
+    }
+    .input-box {
+      width: 220px !important;
+      background: transparent !important;
+      border-color: #00d8ff44 !important;
+      margin-right: 20px;
+      color: #fff !important;
+    }
+    .btn {
+      padding: 8px 20px;
+      position: relative;
+      border-radius: 2px;
+      color: #fff;
+      width: fit-content;
+      cursor: pointer;
+
+      &::before {
+        position: absolute;
+        display: block;
+        content: '';
+        width: calc(100% - 4px);
+        height: calc(100% - 4px);
+        top: 2px;
+        left: 2px;
+        border-radius: 2px;
+        z-index: -1;
+      }
+    }
+
+    .btn1 {
+      border: 1px solid #5cfaff;
+
+      &::before {
+        background-image: linear-gradient(#2effee92, #0cb1d592);
+      }
+
+      &:hover {
+        border: 1px solid #5cfaffaa;
+
+        &::before {
+          background-image: linear-gradient(#2effee72, #0cb1d572);
+        }
+      }
+    }
+  }
+  .is-open {
+    animation: open 0.5s;
+    animation-iteration-count: 1;
+    animation-fill-mode: forwards;
+    animation-timing-function: ease-in;
+  }
+  .is-close {
+    height: 0px;
+  }
+
+  @keyframes open {
+    0% {
+      height: 0px;
+    }
+    100% {
+      height: fit-content;
+    }
+  }
+
+  @keyframes close {
+    0% {
+      height: fit-content;
+    }
+    100% {
+      height: 0px;
+    }
+  }
+  :deep(.zxm-divider-inner-text) {
+    color: #cacaca88 !important;
+  }
+  :deep(.zxm-form-item) {
+    margin-bottom: 10px;
+  }
+</style>

+ 725 - 0
src/views/vent/monitorManager/mainFanMonitor/components/conditionAssistanceCY.vue

@@ -0,0 +1,725 @@
+<template>
+  <BasicModal
+    @register="register"
+    title="风机运行工况辅助决策"
+    :maskStyle="{ backgroundColor: '#000000aa' }"
+    width="1200px"
+    v-bind="$attrs"
+    @ok="onSubmit"
+    @cancel="onSubmit"
+    :canFullscreen="false"
+    :destroyOnClose="true"
+    :footer="null"
+  >
+    <div class="modal-box">
+      <div class="right-box">
+        <!-- <div class="box-title">风机信息</div> -->
+        <dv-decoration7 style="height: 20px">
+          <div class="box-title">风机信息</div>
+        </dv-decoration7>
+        <div class="info-container">
+          <template v-if="isMock">
+            <div v-for="(item, index) in fanInfo" class="info-item" :key="index">
+              <div class="title">{{ item.title }}:</div>
+              <div class="value">{{ fanInfoData && fanInfoData[item.code] ? fanInfoData[item.code] : '-' }}</div>
+            </div>
+          </template>
+          <template v-else>
+            <div v-for="(item, index) in columns" class="info-item" :key="index">
+              <div class="title">{{ item['title'] }}:</div>
+              <div v-if="item['dict']" class="value">{{ render.renderDictText(selectData[item['dataIndex']], 'adjustmentMethod') }}</div>
+              <div v-else class="value">{{ selectData && selectData[item['dataIndex']] ? selectData[item['dataIndex']] : '-' }}</div>
+            </div>
+          </template>
+        </div>
+      </div>
+      <div class="center-box">
+        <a-spin :spinning="loadding" tip="正在计算,请稍等。。。">
+          <div ref="chartRef" class="info-echarts" style="width: 450px; height: 375px"></div>
+          <div v-if="resultObj" class="result-tip">
+            最佳工况点为
+            <span style="color: #9a60b4; padding: 0 10px; font-weight: 600">{{ parseInt(resultObj.Hz) + (showFre ? 'Hz' : '°') }}</span>
+            <span style="color: #c60000; padding: 0 10px; font-weight: 600">{{ formatNum(resultObj.x) }} m³/s</span>
+            <span style="color: #c60000; padding: 0 10px; font-weight: 600">{{ formatNum(resultObj.y) }} Pa</span></div
+          >
+        </a-spin>
+      </div>
+      <div class="left-box">
+        <!-- <div class="box-title">曲线方程</div> -->
+        <dv-decoration7 style="height: 20px">
+          <div class="box-title">曲线方程</div>
+        </dv-decoration7>
+        <div class="info-lines">
+          <div v-for="(item, index) in lineEquation" class="info-item" :key="index">
+            <div class="title">{{ item }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="setting-box">
+      <!-- <div class="left-buttons"> -->
+      <!-- <div class="btn btn1" @click="edit('info')">编辑风机信息</div> -->
+      <!-- <div class="btn btn1" @click="edit('line')">编辑特性曲线</div> -->
+      <!-- 风机当前角度:<span>{{ selectData.bladeAngle }} &nbsp;°</span> -->
+      <!-- </div> -->
+      <!-- <div class="border-clip"></div> -->
+      <div class="right-inputs">
+        <div class="vent-flex-row">
+          <div class="input-title">风量(m³/s):</div>
+          <Input class="input-box" size="large" v-model:value="uQ" />
+          <div class="input-title">风压(Pa):</div>
+          <Input class="input-box" size="large" v-model:value="uH" />
+        </div>
+        <div class="btn btn1" @click="makeLine">决策工况</div>
+      </div>
+    </div>
+    <!-- <div v-if="formShow" class="is-close" :class="{ 'is-open': formShow }">
+      <a-divider orientation="left" style="border-color: #00d8ff22">{{ formType }}</a-divider>
+      <BasicForm @register="registerForm" @submit="handleSubmit" :schemas="columns" />
+    </div> -->
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  //ts语法
+  import { ref, onMounted, reactive, nextTick, defineProps, defineEmits, watch } from 'vue';
+  import echarts from '/@/utils/lib/echarts';
+  import { setOption, initData2, fanInfoData, fanInfo, getSchamas, getSchamas1, lineFormData } from '../main.data.ts';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { Input } from 'ant-design-vue';
+  import { Decoration7 as DvDecoration7 } from '@kjgl77/datav-vue3';
+  import { message } from 'ant-design-vue';
+  import { formatNum } from '/@/utils/ventutil';
+  import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { render } from '/@/utils/common/renderUtils';
+  const { sysOrgCode } = useGlobSetting();
+  const props = defineProps({
+    deviceType: {
+      type: String,
+    },
+    selectData: {
+      type: Object,
+      default: () => {},
+    },
+    m3: {
+      type: Number,
+      default: 670.8,
+    },
+  });
+  const emit = defineEmits(['close', 'register', 'openModal']);
+  type AssistanceItemType = {
+    angle: number;
+    Hz: number;
+    a: number;
+    b: number;
+    c: number;
+    min: number;
+    max: number;
+  };
+  const columns = ref([]);
+  const showFre = ref(true); // 是否显示频率曲线, 是显示,不是显示角度
+  const isMock = false;
+  let option = reactive({});
+  // 注册 modal
+  const [register, { closeModal }] = useModalInner(() => {
+    nextTick(() => {
+      computeAssistance();
+      option = setOption('cy');
+      if (option['xAxis']) option['xAxis']['data'] = xData;
+      option['series'] = yDataList;
+      initEcharts();
+    });
+  });
+  const loadding = ref<boolean>(false);
+  const formShow = ref(false);
+  const formType = ref('');
+  const chartRef = ref();
+  const myChart = ref();
+  const refresh = ref(true);
+  const xData: any[] = [];
+  const yDataList: [] = [];
+  let lineNum = 0;
+  const lineEquation = ref<string[]>([]);
+  const deviceData = ref({});
+  const uQ = ref<string | undefined>(undefined); // 100 - 400
+  const uH = ref<number | undefined>(undefined); //  - 1000
+  const resultObj = ref<{ x: number; y: number; Hz: number } | null>(null);
+
+  const [registerForm, {}] = useForm({
+    labelWidth: 120,
+    actionColOptions: {
+      span: 24,
+    },
+    compact: true,
+    showSubmitButton: true,
+    submitButtonOptions: {
+      text: '提交',
+      preIcon: '',
+    },
+    showResetButton: true,
+    resetButtonOptions: {
+      text: '关闭',
+      preIcon: '',
+    },
+    resetFunc: async () => {
+      formShow.value = false;
+    },
+  });
+
+  function computeAssistance() {
+    const assistanceData = initData2();
+    lineNum = 0;
+    const assistanceDataList = [];
+    const lineEquationList: string[] = [];
+    for (const key in assistanceData) {
+      const item = assistanceData[key];
+      assistanceDataList.push(item);
+      lineEquationList.push(
+        `H${parseInt(item['Hz'])} = ${item['a']}Q² ${Number(item['b']) > 0 ? '+' : '-'} ${Math.abs(Number(item['b'])).toFixed(5)}Q ${
+          Number(item['c']) > 0 ? '+' : '-'
+        } ${Math.abs(Number(item['c'])).toFixed(5)}`
+      );
+    }
+    lineEquation.value = lineEquationList;
+    lineNum = assistanceDataList.length;
+    // const xDataMin =
+    //   Math.min.apply(
+    //     Math,
+    //     assistanceDataList.map((item) => {
+    //       return item.min;
+    //     })
+    //   ) - 10;
+    let xDataMin = Math.min.apply(
+      Math,
+      assistanceDataList.map((item) => {
+        return item.min;
+      })
+    );
+
+    let xDataMax = Math.max.apply(
+      Math,
+      assistanceDataList.map((item) => {
+        return item.max;
+      })
+    );
+    xDataMin = Number((xDataMin - (xDataMax - xDataMin) / 10).toFixed(0));
+    xDataMax = Number((xDataMax + (xDataMax - xDataMin) / 10).toFixed(0));
+
+    fanInfoData.flfw = `${xDataMin}~${xDataMax}`;
+    const computeItem = (item: AssistanceItemType) => {
+      const min = item.min;
+      const max = item.max;
+      const HList: number[] = [];
+      for (let i = xDataMin; i <= xDataMax; i++) {
+        if (i < min) {
+          HList.push(null);
+        } else if (i > max) {
+          HList.push(null);
+        } else {
+          HList.push(item.a * i * i + item.b * i + item.c);
+        }
+      }
+      return HList;
+    };
+
+    for (const key in assistanceData) {
+      const element: AssistanceItemType = assistanceData[key];
+      const yData: number[] = computeItem(element);
+      const series = {
+        name: `${element['Hz']}${showFre.value ? 'Hz' : '°'}`,
+        type: 'line',
+        smooth: true,
+        showSymbol: false,
+        symbol: 'none',
+        emphasis: {
+          focus: 'series',
+        },
+        itemStyle: { normal: { label: { show: true } } },
+        lineStyle: {
+          width: 1,
+          color: '#ffffff88',
+        },
+        zlevel: 0,
+        z: 1,
+        endLabel: {
+          show: true,
+          formatter: '{a}',
+          distance: 0,
+          color: '#39E9FE99',
+          backgroundColor: 'transparent',
+          padding: [3, 3, 2, 3],
+        },
+        data: yData,
+      };
+      yDataList.push(series);
+    }
+
+    for (let i = xDataMin; i <= xDataMax; i++) {
+      xData.push(i);
+    }
+  }
+
+  function computeRLine() {
+    if (uH.value && uQ.value) {
+      const R = uH.value / Number(uQ.value) / Number(uQ.value);
+      const yAxis: number[] = [];
+      for (let i = 0; i < xData.length; i++) {
+        const x = xData[i];
+        const y = R * x * x;
+        if (x == uQ.value) {
+          uH.value = y;
+        }
+        yAxis.push(y);
+      }
+      const series = {
+        name: 'R',
+        type: 'line',
+        smooth: true,
+        showSymbol: false,
+        zlevel: 0,
+        emphasis: {
+          focus: 'series',
+        },
+        itemStyle: { normal: { label: { show: true } } },
+        lineStyle: {
+          width: 2,
+          color: '#D0A343',
+        },
+        endLabel: {
+          show: true,
+          formatter: '{a}',
+          distance: 0,
+          color: '#D0A343',
+        },
+        data: yAxis,
+      };
+      yDataList[lineNum] = series;
+    }
+  }
+
+  function reSetLine() {
+    let minIndex = -1;
+    for (let i = 0; i < yDataList.length; i++) {
+      if (i !== lineNum && i != lineNum + 1) {
+        if (yDataList[i]['lineStyle']) yDataList[i]['lineStyle']['color'] = '#ffffff88';
+        if (yDataList[i]['lineStyle']) yDataList[i]['lineStyle']['width'] = 1;
+        if (yDataList[i]['endLabel'] && yDataList[i]['endLabel']['color']) {
+          yDataList[i]['endLabel']['color'] = '#39E9FE99';
+        }
+        if (yDataList[i]['endLabel'] && yDataList[i]['endLabel']['backgroundColor']) yDataList[i]['endLabel']['backgroundColor'] = 'transparent';
+        if (yDataList[i]['z']) yDataList[i]['z'] = 1;
+      }
+
+      if (resultObj.value && `${resultObj.value.Hz}${showFre.value ? 'Hz' : '°'}` == yDataList[i]['name']) {
+        minIndex = i;
+      }
+    }
+    if (minIndex != -1) {
+      yDataList[minIndex]['lineStyle']['color'] = '#9A60B4';
+      yDataList[minIndex]['lineStyle']['width'] = 2;
+      yDataList[minIndex]['endLabel']['color'] = '#9A60B4';
+      yDataList[minIndex]['endLabel']['backgroundColor'] = '#111';
+      yDataList[minIndex]['z'] = 999;
+    }
+  }
+
+  // 根据风量计算压差
+  function computePa() {
+    const R = uH.value / Number(uQ.value) / Number(uQ.value);
+    const pointX = Number(uQ.value);
+    const pointY = Number(uH.value);
+    type ItemType = {
+      x: number;
+      y: number;
+      Hz: number;
+    };
+    const assistanceData = initData2('cy');
+    const paList = new Map<number, ItemType>(); // key 是最近距离
+    const getIntersectionPoint = (a, b, c, R, min, max) => {
+      const obj: { x: undefined | number; y: undefined | number } = { x: undefined, y: undefined };
+      // 计算二次方程的判别式
+      const discriminant = b * b - 4 * (a - R) * c;
+
+      if (discriminant > 0) {
+        // 有两个实根
+        const x1 = (-b + Math.sqrt(discriminant)) / (2 * (a - R));
+        const x2 = (-b - Math.sqrt(discriminant)) / (2 * (a - R));
+
+        const y1 = R * x1 * x1;
+        const y2 = R * x2 * x2;
+        if (x1 >= min && x1 <= max) {
+          obj.x = x1;
+          obj.y = y1;
+        } else {
+          obj.x = x2;
+          obj.y = y2;
+        }
+      } else if (discriminant === 0) {
+        // 有一个实根
+        const x = -b / (2 * (a - R));
+        const y = R * x * x;
+        if (x >= min && x <= max) {
+          obj.x = x;
+          obj.y = y;
+        }
+        // console.log(`唯一交点: (${x}, ${y})`);
+      } else {
+        // 没有实根,交点在虚数域
+        console.log('没有实数交点');
+      }
+      return obj;
+    };
+
+    for (let key in assistanceData) {
+      const item: AssistanceItemType = assistanceData[key];
+      paList.set(item.Hz, getIntersectionPoint(item.a, item.b, item.c, R, item.min, item.max));
+    }
+
+    const min = (points: Map<number, ItemType>) => {
+      const targetX = uQ.value;
+      const targetY = uH.value;
+      let minDistance = Number.POSITIVE_INFINITY;
+      let closestPoint = null;
+      let keyVal = '';
+      // 遍历已知点数组,计算距离并更新最小距离和对应的点
+      for (const [key, point] of points) {
+        const distance = Math.sqrt((targetX - point.x) ** 2 + (targetY - point.y) ** 2);
+        if (distance < minDistance) {
+          minDistance = distance;
+          closestPoint = point;
+          keyVal = key;
+        }
+      }
+
+      if (closestPoint !== null) {
+        console.log(`距离最小的点是 (${closestPoint.x}, ${closestPoint.y}), 距离为 ${minDistance}`);
+        resultObj.value = { x: closestPoint.x, y: closestPoint.y, Hz: keyVal };
+      } else {
+        console.log('没有找到最小距离的点');
+      }
+    };
+    min(paList);
+    reSetLine();
+  }
+
+  function computeR() {
+    if (uQ.value && uH.value) {
+      computeRLine();
+      computePa();
+      if (resultObj.value && resultObj.value.x && resultObj.value.y) {
+        const series = {
+          type: 'effectScatter',
+          symbolSize: 5,
+          // symbolOffset:[1, 1],
+          showEffectOn: 'render',
+          // 涟漪特效相关配置。
+          rippleEffect: {
+            // 波纹的绘制方式,可选 'stroke' 和 'fill'。
+            brushType: 'stroke',
+          },
+          zlevel: 1,
+          z: 999,
+          itemStyle: {
+            color: '#C60000',
+          },
+          data: [[resultObj.value.x.toFixed(0), Number(resultObj.value.y.toFixed(0))]],
+        };
+        yDataList[lineNum + 1] = series;
+      }
+    }
+  }
+
+  function edit(flag) {
+    if (flag == 'info') {
+      formType.value = '编辑风机信息';
+    }
+    if (flag == 'line') {
+      formType.value = '编辑特性曲线';
+    }
+    if (formShow.value == true) {
+      formShow.value = false;
+      nextTick(() => {
+        formShow.value = true;
+      });
+    } else {
+      formShow.value = true;
+    }
+  }
+
+  async function onSubmit() {
+    emit('close');
+    closeModal();
+    chartRef.value = null;
+    uQ.value = undefined;
+    uH.value = undefined;
+    formType.value = '';
+    myChart.value = undefined;
+    refresh.value = true;
+    xData.length = 0;
+    yDataList.length = 0;
+    lineNum = 0;
+    lineEquation.value = [];
+    resultObj.value = null;
+  }
+
+  function initEcharts() {
+    if (chartRef.value) {
+      computeR();
+      myChart.value = echarts.init(chartRef.value);
+      option && myChart.value.setOption(option);
+      refresh.value = false;
+      nextTick(() => {
+        setTimeout(() => {
+          refresh.value = true;
+        }, 0);
+      });
+    }
+  }
+
+  function makeLine() {
+    if (uQ.value && uH.value) {
+      loadding.value = true;
+      setTimeout(() => {
+        initEcharts();
+        loadding.value = false;
+      }, 1200);
+    }
+  }
+  function handleSubmit() {
+    message.success('提交成功');
+    setTimeout(() => {
+      formShow.value = false;
+    }, 800);
+  }
+  function getColumn() {
+    let lineColumns = [];
+    if (props.deviceType) {
+      lineColumns = getTableHeaderColumns(props.deviceType + '_input') as [];
+      if (lineColumns && lineColumns.length < 1) {
+        lineColumns = getTableHeaderColumns(props.deviceType.split('_')[0] + '_input') as [];
+      }
+      if (lineColumns.length > 0) {
+        lineColumns = lineColumns.filter((item) => item['dataIndex'] && (item['dataIndex'] as string).endsWith('_mainFanInfo'));
+        columns.value = lineColumns;
+      }
+    }
+    if (!columns.value || columns.value.length == 0) {
+      columns.value = fanInfo;
+    }
+  }
+  watch(
+    () => props.deviceType,
+    async () => {
+      getColumn();
+      // deviceData.value = await list({ devicetype: 'fanmain', pagetype: 'normal' });
+    }
+  );
+  watch(
+    () => props.selectData,
+    async (selectData) => {
+      deviceData.value = selectData;
+      if (selectData && selectData['adjustmentMethod_mainFanInfo'] != 'angleadjust') {
+        // 说明是角度了调节
+        showFre.value = true;
+      } else {
+        showFre.value = false;
+      }
+    },
+    { immediate: true }
+  );
+  onMounted(() => {});
+</script>
+
+<style scoped lang="less">
+  .modal-box {
+    display: flex;
+    flex-direction: row;
+    background-color: #ffffff05;
+    padding: 20px 8px;
+    border: 1px solid #00d8ff22;
+    // min-height: 600px;
+    .box-title {
+      width: calc(100% - 40px);
+      text-align: center;
+      background-color: #1dc1f522;
+    }
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 2px 0px;
+      margin: 4px 0;
+      background-image: linear-gradient(to right, #39deff15, #3977e500);
+      &:first-child {
+        margin-top: 0;
+      }
+      .title {
+        width: 200px;
+        text-align: left;
+        padding-left: 20px;
+        color: #f1f1f1cc;
+      }
+      .value {
+        width: 150px;
+        color: #00d8ff;
+        padding-right: 20px;
+        text-align: right;
+      }
+    }
+    .right-box {
+      width: 350px;
+
+      .info-container {
+        width: calc(100% - 2px);
+        margin-top: 5px;
+        box-shadow: 0px 0px 50px #86baff08 inset;
+      }
+    }
+    .left-box {
+      width: 350px;
+      .info-lines {
+        width: calc(100% - 2px);
+        height: 390px;
+        box-shadow: 0px 0px 50px #86baff08 inset;
+        overflow-y: auto;
+        margin-top: 5px;
+        .title {
+          width: 100%;
+          color: #f1f1f1cc;
+        }
+      }
+      .info-item {
+        padding: 8px 0px;
+        margin: 4px 0;
+      }
+    }
+    .center-box {
+      margin: 0 10px;
+
+      .info-echarts {
+        // background-color: #ffffff11;
+      }
+      .result-tip {
+        text-align: center;
+        background-color: #00000011;
+        line-height: 28px;
+        margin: 10px 50px 0 50px;
+        border: 1px solid #00d8ff22;
+        border-radius: 2px;
+      }
+    }
+  }
+  .setting-box {
+    width: 1170px;
+    height: 70px;
+    margin: 10px 0;
+    background-color: #ffffff05;
+    border: 1px solid #00d8ff22;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .right-inputs {
+      display: flex;
+      height: 40px;
+      margin-right: 10px;
+    }
+    .left-buttons {
+      display: flex;
+      height: 40px;
+      margin-left: 15px;
+      .btn {
+        margin: 0 10px;
+      }
+      span {
+        color: #00d8ff;
+      }
+    }
+    .border-clip {
+      width: 1px;
+      height: 25px;
+      border-right: 1px solid #8b8b8b77;
+    }
+    .input-title {
+      width: 120px;
+    }
+    .input-box {
+      width: 300px !important;
+      background: transparent !important;
+      border-color: #00d8ff44 !important;
+      margin-right: 20px;
+      color: #fff !important;
+    }
+    .btn {
+      padding: 8px 20px;
+      position: relative;
+      border-radius: 2px;
+      color: #fff;
+      width: fit-content;
+      cursor: pointer;
+
+      &::before {
+        position: absolute;
+        display: block;
+        content: '';
+        width: calc(100% - 4px);
+        height: calc(100% - 4px);
+        top: 2px;
+        left: 2px;
+        border-radius: 2px;
+        z-index: -1;
+      }
+    }
+
+    .btn1 {
+      border: 1px solid #5cfaff;
+
+      &::before {
+        background-image: linear-gradient(#2effee92, #0cb1d592);
+      }
+
+      &:hover {
+        border: 1px solid #5cfaffaa;
+
+        &::before {
+          background-image: linear-gradient(#2effee72, #0cb1d572);
+        }
+      }
+    }
+  }
+  .is-open {
+    animation: open 0.5s;
+    animation-iteration-count: 1;
+    animation-fill-mode: forwards;
+    animation-timing-function: ease-in;
+  }
+  .is-close {
+    height: 0px;
+  }
+
+  @keyframes open {
+    0% {
+      height: 0px;
+    }
+    100% {
+      height: fit-content;
+    }
+  }
+
+  @keyframes close {
+    0% {
+      height: fit-content;
+    }
+    100% {
+      height: 0px;
+    }
+  }
+  :deep(.zxm-divider-inner-text) {
+    color: #cacaca88 !important;
+  }
+  :deep(.zxm-form-item) {
+    margin-bottom: 10px;
+  }
+</style>