Эх сурвалжийг харах

[Feat 0000] 皮带三级防灭火系统模型上添加喷淋喷洒效果

hongrunxia 2 өдөр өмнө
parent
commit
5a7ae5cafb

+ 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="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>
+  .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="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>
+  .bg {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
+  }
   .loading-box {
     position: fixed;
     display: flex;

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

@@ -53,333 +53,287 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue';
-import customHeader from './components/customHeader-belt.vue';
-import { useInitConfigs, useInitPage } from '../hooks/useInit';
-import { testBeltNew, testYjkf, testSpary } from './configurable.data';
-import ModuleCommon from './components/ModuleCommon.vue';
-import ModuleCommonDual from './components/ModuleCommonDual.vue';
-import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
-import BeltNav from './components/BeltNav.vue';
-import { useRouter, useRoute } from 'vue-router';
-import { getSystem, getMonitorAndAlertBelt, getWarnInfo, getDevice, getDataHome, getWarnResult } from './configurable.api';
-import { modalAnimate } from './threejs/belt.threejs';
-import History from './components/detail/history.vue';
-import sys from '/@/locales/lang/en/sys';
-// 初始化配置
-const { configs, fetchConfigs } = useInitConfigs();
-const cfgs = computed(() => configs.value.filter((_, index) => index !== 2 && index !== 3));
-const cfgA = computed<any>(() => configs.value[2]);
-const cfgB = computed<any>(() => configs.value[3]);
-const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
-const isInitModal = ref(false);
-const pageCache = ref({
-  fire_risk_warn: { configs: testBeltNew },
-  emergencyControl: { configs: testYjkf },
-  sprayControl: { configs: testSpary },
-});
-let timer = null;
-const pageType = ref('fire_risk_warn');
-const route = useRoute();
-const modalMonitorData = ref({});
-// 下拉框选项
-/** 场景选项 */
-const options = ref([]);
-const optionValue = ref('');
-async function getSysDataSource() {
-  const res = await getDataHome({ dataList: 'risk_evaluator' }).catch(() => {});
-  options.value = res.risk_evaluator || [];
-  await nextTick();
-  if (options.value.length > 0 && !optionValue.value) {
-    const firstId = options.value[0].sys_id;
-    changeSelectRow(firstId);
+  import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue';
+  import customHeader from './components/customHeader-belt.vue';
+  import { useInitConfigs, useInitPage } from '../hooks/useInit';
+  import { testBeltNew, testYjkf, testSpary } from './configurable.data';
+  import ModuleCommon from './components/ModuleCommon.vue';
+  import ModuleCommonDual from './components/ModuleCommonDual.vue';
+  import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
+  import BeltNav from './components/BeltNav.vue';
+  import { useRouter, useRoute } from 'vue-router';
+  import { getSystem, getMonitorAndAlertBelt, getWarnInfo, getDevice, getDataHome, getWarnResult } from './configurable.api';
+  import { modalAnimate } from './threejs/belt.threejs';
+  import History from './components/detail/history.vue';
+  import sys from '/@/locales/lang/en/sys';
+  // 初始化配置
+  const { configs, fetchConfigs } = useInitConfigs();
+  const cfgs = computed(() => configs.value.filter((_, index) => index !== 2 && index !== 3));
+  const cfgA = computed<any>(() => configs.value[2]);
+  const cfgB = computed<any>(() => configs.value[3]);
+  const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
+  const isInitModal = ref(false);
+  const pageCache = ref({
+    fire_risk_warn: { configs: testBeltNew },
+    emergencyControl: { configs: testYjkf },
+    sprayControl: { configs: testSpary },
+  });
+  let timer = null;
+  const pageType = ref('fire_risk_warn');
+  const route = useRoute();
+  const modalMonitorData = ref({});
+  // 下拉框选项
+  /** 场景选项 */
+  const options = ref([]);
+  const optionValue = ref('');
+  async function getSysDataSource() {
+    const res = await getDataHome({ dataList: 'risk_evaluator' }).catch(() => {});
+    options.value = res.risk_evaluator || [];
+    await nextTick();
+    if (options.value.length > 0 && !optionValue.value) {
+      const firstId = options.value[0].sys_id;
+      changeSelectRow(firstId);
+    }
+  }
+  // 切换检测数据
+  function changeSelectRow(deviceID) {
+    optionValue.value = deviceID;
+    refresh();
+  }
+  function goToHistory() {
+    if (pageType.value === 'history') {
+      // 当前是历史页 → 切回默认页
+      pageType.value = 'fire_risk_warn';
+    } else {
+      // 当前不是 → 打开历史页
+      pageType.value = 'history';
+    }
   }
-}
-// 切换检测数据
-function changeSelectRow(deviceID) {
-  optionValue.value = deviceID;
-  refresh();
-}
-function goToHistory() {
-  if (pageType.value === 'history') {
-    // 当前是历史页 → 切回默认页
-    pageType.value = 'fire_risk_warn';
-  } else {
-    // 当前不是 → 打开历史页
-    pageType.value = 'history';
+  // 预警等级映射
+  const warnTypeMap = {
+    '102': '黄色预警(较大风险)',
+    '103': '橙色预警(重大风险)',
+    '104': '红色预警(特别重大风险)',
+  };
+  // 处理接口返回数据
+  interface WarnResult {
+    warnName: string;
+    coRange?: string;
+    coTrend?: string;
+    tempRange?: string;
+    tempTrend?: string;
+    hclRange?: string;
   }
-}
-// 预警等级映射
-const warnTypeMap = {
-  '102': '黄色预警(较大风险)',
-  '103': '橙色预警(重大风险)',
-  '104': '红色预警(特别重大风险)',
-};
-// 处理接口返回数据
-interface WarnResult {
-  warnName: string;
-  coRange?: string;
-  coTrend?: string;
-  tempRange?: string;
-  tempTrend?: string;
-  hclRange?: string;
-}
-function groupWarnByType(data: any) {
-  const result: WarnResult[] = [];
+  function groupWarnByType(data: any) {
+    const result: WarnResult[] = [];
 
-  // 遍历 102、103、104...
-  Object.keys(data).forEach((warnKey) => {
-    const list = data[warnKey] || [];
-    const warnName = warnTypeMap[warnKey as keyof typeof warnTypeMap];
+    // 遍历 102、103、104...
+    Object.keys(data).forEach((warnKey) => {
+      const list = data[warnKey] || [];
+      const warnName = warnTypeMap[warnKey as keyof typeof warnTypeMap];
 
-    // 构建当前预警对象
-    const warnObj: WarnResult = { warnName };
+      // 构建当前预警对象
+      const warnObj: WarnResult = { warnName };
 
-    list.forEach((item: any) => {
-      const { deviceType, fmin, fmax, trendMin, trendMax, trendCxTimeUnit } = item;
-      // CO
-      if (deviceType === 'modelsensor_co') {
-        if (fmin != null && fmax != null) {
-          warnObj.coRange = `${fmin} - ${fmax}`;
-        } else if (trendMin != null && trendMax != null) {
-          warnObj.coTrend = `${trendMin} - ${trendMax}`;
+      list.forEach((item: any) => {
+        const { deviceType, fmin, fmax, trendMin, trendMax, trendCxTimeUnit } = item;
+        // CO
+        if (deviceType === 'modelsensor_co') {
+          if (fmin != null && fmax != null) {
+            warnObj.coRange = `${fmin} - ${fmax}`;
+          } else if (trendMin != null && trendMax != null) {
+            warnObj.coTrend = `${trendMin} - ${trendMax}`;
+          }
         }
-      }
-      // 温度(带单位)
-      else if (deviceType === 'modelsensor_temperature') {
-        if (fmin != null && fmax != null) {
-          warnObj.tempRange = `${fmin} - ${fmax}℃`;
-        } else if (trendMin != null) {
-          let unit = '';
-          if (trendCxTimeUnit === 0) unit = '分钟';
-          if (trendCxTimeUnit === 1) unit = '小时';
-          warnObj.tempTrend = `>${trendMin}℃ +${unit}`;
+        // 温度(带单位)
+        else if (deviceType === 'modelsensor_temperature') {
+          if (fmin != null && fmax != null) {
+            warnObj.tempRange = `${fmin} - ${fmax}℃`;
+          } else if (trendMin != null) {
+            let unit = '';
+            if (trendCxTimeUnit === 0) unit = '分钟';
+            if (trendCxTimeUnit === 1) unit = '小时';
+            warnObj.tempTrend = `>${trendMin}℃ +${unit}`;
+          }
         }
-      }
-      // HCL
-      else if (deviceType === 'modelsensor_hcl') {
-        if (fmin != null && fmax != null) {
-          warnObj.hclRange = `${fmin} - ${fmax}`;
+        // HCL
+        else if (deviceType === 'modelsensor_hcl') {
+          if (fmin != null && fmax != null) {
+            warnObj.hclRange = `${fmin} - ${fmax}`;
+          }
         }
-      }
+      });
+      result.push(warnObj);
     });
-    result.push(warnObj);
-  });
 
-  return result;
-}
-// 刷新数据
-async function refresh() {
-  // 由于模型中需要用到风门的监测数据,这里进行公共调用(后期精确调用风门)
-  const modalRes = {};
-  const systemParams = {
-    devicetype: 'sys',
-    systemID: optionValue.value,
-  };
-  const resSys = await getSystem(systemParams);
-  const params = {
-    sysId: optionValue.value,
-    monitorType: 2,
-  };
-  const warnInfo = await getWarnInfo(params);
-  Object.assign(modalRes, resSys);
-  if (pageType.value == 'fire_risk_warn') {
-    configs.value = [...testBeltNew];
-    const params = {
-      sysId: optionValue.value,
-      dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
-      alarmLevel: '102,103,104',
-    };
-    const resWarn = await getMonitorAndAlertBelt(params);
-    updateData(resWarn);
-    Object.assign(modalRes, resWarn);
-  } else if (pageType.value == 'emergencyControl') {
-    updateData(resSys);
-    configs.value = [...testYjkf];
-    const alarmParams = {
-      sysId: optionValue.value,
-      alarmLevel: '104',
+    return result;
+  }
+  // 刷新数据
+  async function refresh() {
+    // 由于模型中需要用到风门的监测数据,这里进行公共调用(后期精确调用风门)
+    const modalRes = {};
+    const systemParams = {
+      devicetype: 'sys',
+      systemID: optionValue.value,
     };
-    const alarmRes = await getWarnResult(alarmParams);
-    if (alarmRes.warn_result) {
-      data.value.warn_result = alarmRes.warn_result;
-    }
-    data.value.warnInfo = groupWarnByType(warnInfo);
-    console.log(data.value, '11111111');
-    updateData(data.value);
-  } else if (pageType.value == 'sprayControl') {
-    updateData(resSys);
-    const params1 = {
+    const resSys = await getSystem(systemParams);
+    const params = {
       sysId: optionValue.value,
-      alarmLevel: '103,104',
+      monitorType: 2,
     };
-    const sprayData = [];
-    if (data.value?.deviceInfo) {
-      // 遍历对象的所有 value
-      Object.values(data.value.deviceInfo).forEach((item) => {
-        const hasSprayAuto = item.type && item.type.toLowerCase().includes('spray');
-        if (hasSprayAuto) {
-          sprayData.push({ ...item, ...item.readData });
-        }
-      });
-    }
-    data.value.sprayData = sprayData;
-    const alarmRes = await getWarnResult(params1);
-    if (alarmRes.warn_result) {
-      data.value.warn_result = alarmRes.warn_result;
+    const warnInfo = await getWarnInfo(params);
+    Object.assign(modalRes, resSys);
+    if (pageType.value == 'fire_risk_warn') {
+      configs.value = [...testBeltNew];
+      const params = {
+        sysId: optionValue.value,
+        dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
+        alarmLevel: '102,103,104',
+      };
+      const resWarn = await getMonitorAndAlertBelt(params);
+      updateData(resWarn);
+      Object.assign(modalRes, resWarn);
+    } else if (pageType.value == 'emergencyControl') {
+      updateData(resSys);
+      configs.value = [...testYjkf];
+      const alarmParams = {
+        sysId: optionValue.value,
+        alarmLevel: '104',
+      };
+      const alarmRes = await getWarnResult(alarmParams);
+      if (alarmRes.warn_result) {
+        data.value.warn_result = alarmRes.warn_result;
+      }
+      data.value.warnInfo = groupWarnByType(warnInfo);
+      console.log(data.value, '11111111');
+      updateData(data.value);
+    } else if (pageType.value == 'sprayControl') {
+      updateData(resSys);
+      const params1 = {
+        sysId: optionValue.value,
+        alarmLevel: '103,104',
+      };
+      const sprayData = [];
+      if (data.value?.deviceInfo) {
+        // 遍历对象的所有 value
+        Object.values(data.value.deviceInfo).forEach((item) => {
+          const hasSprayAuto = item.type && item.type.toLowerCase().includes('spray');
+          if (hasSprayAuto) {
+            sprayData.push({ ...item, ...item.readData });
+          }
+        });
+      }
+      data.value.sprayData = sprayData;
+      const alarmRes = await getWarnResult(params1);
+      if (alarmRes.warn_result) {
+        data.value.warn_result = alarmRes.warn_result;
+      }
+      configs.value = [...testSpary];
+    } else {
+      configs.value = testBeltNew;
     }
-    configs.value = [...testSpary];
-  } else {
-    configs.value = testBeltNew;
+    modalMonitorData.value = modalRes;
   }
-  modalMonitorData.value = modalRes;
-}
 
-// // 定时刷新
-function initInterval() {
-  if (timer) clearInterval(timer);
-  timer = setInterval(() => {
-    refresh();
-  }, 60000);
-}
+  // // 定时刷新
+  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();
-}
+  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(); // 切换路由自动刷新
-//     }
-//   }
-// );
+  // 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;
+  function initModalAnimate(modal) {
+    console.log('初始化模型', modal);
+    modal.isRender = true;
+    modalAnimate(modal, modalMonitorData);
   }
-}
-watch(
-  () => route.query.pageType,
-  (newQueryType) => {
-    if (newQueryType) {
-      changePage(newQueryType as string);
+  function clearTimer() {
+    if (timer) {
+      clearInterval(timer);
+      timer = null;
     }
-  },
-  { immediate: true } // 初始化立刻执行
-);
+  }
+  watch(
+    () => route.query.pageType,
+    (newQueryType) => {
+      if (newQueryType) {
+        changePage(newQueryType as string);
+      }
+    },
+    { immediate: true } // 初始化立刻执行
+  );
 
-watch(
-  () => modalMonitorData.value,
-  (newData, oldData) => {
-    if (newData && !Object.keys(oldData).length) {
-      isInitModal.value = true;
+  watch(
+    () => modalMonitorData.value,
+    (newData, oldData) => {
+      if (newData && !Object.keys(oldData).length) {
+        isInitModal.value = true;
+      }
     }
-  }
-);
+  );
 
-onMounted(async () => {
-  await getSysDataSource();
-  await refresh();
-  initInterval();
-});
-onUnmounted(() => {
-  clearTimer();
-});
+  onMounted(async () => {
+    await getSysDataSource();
+    await refresh();
+    initInterval();
+  });
+  onUnmounted(() => {
+    clearTimer();
+  });
 </script>
 <style lang="less" scoped>
-.company-home {
-  background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
-  width: 100%;
-  height: 100%;
-  color: @white;
-  position: relative;
-  font-family: 'Microsoft YaHei', sans-serif;
-  .top-bg {
-    width: 100%;
-    height: 56px;
-    position: absolute;
-    margin-top: 10px;
-    z-index: 1;
-  }
-  .header-container {
-    position: absolute;
-    top: 20px;
-    left: 20px;
-    z-index: 10;
-  }
-
-  .border {
-    width: 100%;
-    height: 94%;
-    background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
-    background-size: 100% 100%;
-    position: relative;
-    width: 100%;
-  }
-}
-
-.center-warning-container {
-  position: absolute;
-  left: 50%;
-  transform: translateX(-50%);
-  top: 50%;
-  width: 600px;
-  height: 200px;
-  background-color: rgba(0, 0, 0, 0.7);
-  border-radius: 10px;
-  padding: 15px;
-  box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
-  z-index: 5;
-  color: #fff;
-
-  .warning-header {
-    font-size: 18px;
-    font-weight: bold;
-    margin-bottom: 10px;
-    color: #ff6b6b;
-  }
-
-  .warning-list {
+  .company-home {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
     width: 100%;
     height: 100%;
-    overflow-y: auto;
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-  }
-
-  .warning-item {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    padding: 8px;
-    background-color: rgba(0, 0, 0, 0.5);
-    border-radius: 5px;
-    border-left: 4px solid #ff6b6b;
+    color: @white;
+    position: relative;
+    font-family: 'Microsoft YaHei', sans-serif;
+    .top-bg {
+      width: 100%;
+      height: 56px;
+      position: absolute;
+      margin-top: 10px;
+      z-index: 1;
+    }
+    .header-container {
+      position: absolute;
+      top: 20px;
+      left: 20px;
+      z-index: 10;
+    }
 
-    .warning-time {
-      font-size: 14px;
-      color: #ccc;
+    .border {
+      width: 100%;
+      height: 94%;
+      background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
+      background-size: 100% 100%;
+      position: relative;
+      width: 100%;
     }
   }
 
-  // 中间预警结果区
   .center-warning-container {
     position: absolute;
     left: 50%;
@@ -423,67 +377,113 @@ onUnmounted(() => {
         font-size: 14px;
         color: #ccc;
       }
+    }
 
-      .warning-level {
-        font-size: 14px;
+    // 中间预警结果区
+    .center-warning-container {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      top: 50%;
+      width: 600px;
+      height: 200px;
+      background-color: rgba(0, 0, 0, 0.7);
+      border-radius: 10px;
+      padding: 15px;
+      box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
+      z-index: 5;
+      color: #fff;
+
+      .warning-header {
+        font-size: 18px;
         font-weight: bold;
-        padding: 4px 8px;
-        border-radius: 4px;
-        &.level-critical {
-          background-color: #ff6b6b;
-          color: white;
-        }
-        &.level-high {
-          background-color: #ffcc00;
-          color: black;
-        }
-        &.level-normal {
-          background-color: #66cc66;
-          color: white;
-        }
+        margin-bottom: 10px;
+        color: #ff6b6b;
+      }
+
+      .warning-list {
+        width: 100%;
+        height: 100%;
+        overflow-y: auto;
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
       }
 
-      .warning-action {
-        .btn-start-spray {
-          background-color: #00e1ff;
-          color: #000;
-          border: none;
-          padding: 4px 10px;
+      .warning-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px;
+        background-color: rgba(0, 0, 0, 0.5);
+        border-radius: 5px;
+        border-left: 4px solid #ff6b6b;
+
+        .warning-time {
+          font-size: 14px;
+          color: #ccc;
+        }
+
+        .warning-level {
+          font-size: 14px;
+          font-weight: bold;
+          padding: 4px 8px;
           border-radius: 4px;
-          cursor: pointer;
-          font-size: 12px;
-          transition: all 0.3s;
-          &:hover {
-            background-color: #00c3e6;
+          &.level-critical {
+            background-color: #ff6b6b;
+            color: white;
+          }
+          &.level-high {
+            background-color: #ffcc00;
+            color: black;
+          }
+          &.level-normal {
+            background-color: #66cc66;
+            color: white;
+          }
+        }
+
+        .warning-action {
+          .btn-start-spray {
+            background-color: #00e1ff;
+            color: #000;
+            border: none;
+            padding: 4px 10px;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 12px;
+            transition: all 0.3s;
+            &:hover {
+              background-color: #00c3e6;
+            }
           }
         }
       }
     }
-  }
 
-  // 巷道示意图
-  .belt-diagram {
-    position: absolute;
-    left: 50%;
-    transform: translateX(-50%);
-    bottom: 50px;
-    width: 800px;
-    height: 100px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+    // 巷道示意图
+    .belt-diagram {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 50px;
+      width: 800px;
+      height: 100px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
 
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
     }
   }
-}
-.modal-box {
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  z-index: 1;
-}
+  .modal-box {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    z-index: 1;
+  }
 </style>

+ 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>

+ 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, // 传入场景,方便组件内部销毁
     });

+ 152 - 31
src/views/vent/home/configurable/belt/threejs/belt.threejs.ts

@@ -5,10 +5,13 @@ import { ref, watch, Ref } from 'vue';
 import { modelMouseHandler } from '/@/utils/threejs/useEvent';
 import { panelManager } from './PanelManager';
 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, doubleEvent;
 const normalColor = new THREE.Color(0xff0000);
@@ -41,6 +44,8 @@ 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();
 
@@ -48,11 +53,12 @@ export async function modalAnimate(modal, modalMonitorData: Ref<any, any>) {
     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();
+      updateSprayPanel3D(modal, sprayPartitionList);
       const alarminfo = res?.alarminfo;
       if (Object.keys(alarminfo).length > 0) {
         for (const key in alarminfo) {
@@ -348,7 +354,7 @@ function createMonitorPanel3D(modal, partition) {
     //   data['alarmLevel'] = 102;
     // }
 
-    panelManager.createPanel(modal.scene, {
+    panelManager.createPanel(modal.scene, MonitorPanel, {
       instanceId: partition.name,
       sensorData: {
         positionName: `分区#${index}`,
@@ -412,6 +418,40 @@ function updateMonitorPanel3D() {
   });
 }
 
+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');
 }
@@ -753,35 +793,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;
@@ -931,6 +942,116 @@ 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);

+ 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>

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

@@ -55,8 +55,8 @@
         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 },
         },
       };
       await initModal();

+ 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>