Răsfoiți Sursa

主路光伏

learshaw 7 ore în urmă
părinte
comite
a66f3ee780

+ 86 - 0
ems-ui-cloud/src/views/largeScreen/dialog/pv-road-alarm.vue

@@ -0,0 +1,86 @@
+<template>
+  <threejs-base-dialog>
+    <template #title>
+      告警概况
+    </template>
+    <template #data>
+      <div>
+        <span>活动告警:</span>
+        <span class="value">{{ alarmData.activeCount }} 条</span>
+      </div>
+      <div>
+        <span>紧急告警:</span>
+        <span class="value" :style="{ color: alarmData.urgentCount > 0 ? '#F56C6C' : '#06E3F9' }">{{ alarmData.urgentCount }} 条</span>
+      </div>
+      <div>
+        <span>最近告警:</span>
+        <span class="value" style="font-size: 14px;">{{ alarmData.latestMsg || '暂无' }}</span>
+      </div>
+    </template>
+  </threejs-base-dialog>
+</template>
+
+<script>
+import threejsBaseDialog from './base.vue'
+import { listActiveAlarm } from "@/api/alarm/alarm"
+
+let timer = null
+
+export default {
+  components: {
+    threejsBaseDialog,
+  },
+  props: {
+    propData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      alarmData: {
+        activeCount: 0,
+        urgentCount: 0,
+        latestMsg: '',
+      },
+    };
+  },
+  beforeDestroy() {
+    timer && clearInterval(timer)
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.fetchData()
+      timer = setInterval(this.fetchData, 1000 * 60 * 3)
+    },
+    async fetchData() {
+      try {
+        const areaCode = this.propData.areaCode || null
+        const res = await listActiveAlarm(areaCode)
+
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          const sorted = res.data.sort((a, b) => {
+            if (b.alarmLevel !== a.alarmLevel) {
+              return b.alarmLevel - a.alarmLevel
+            }
+            return new Date(b.alarmTime) - new Date(a.alarmTime)
+          })
+
+          const activeCount = res.data.length
+          const urgentCount = res.data.filter(a => a.alarmLevel === 3).length
+          const latestMsg = sorted[0]?.alarmMsg || ''
+
+          this.alarmData = { activeCount, urgentCount, latestMsg }
+        } else {
+          this.alarmData = { activeCount: 0, urgentCount: 0, latestMsg: '' }
+        }
+      } catch (error) {
+        console.error('获取告警概况数据失败:', error)
+      }
+    },
+  },
+};
+</script>

+ 83 - 0
ems-ui-cloud/src/views/largeScreen/dialog/pv-road-device.vue

@@ -0,0 +1,83 @@
+<template>
+  <threejs-base-dialog>
+    <template #title>
+      设备运行状态
+    </template>
+    <template #data>
+      <div>
+        <span>设备总数:</span>
+        <span class="value">{{ deviceData.total }} 台</span>
+      </div>
+      <div>
+        <span>在线设备:</span>
+        <span class="value">{{ deviceData.onlineCount }} 台</span>
+      </div>
+      <div>
+        <span>离线设备:</span>
+        <span class="value">{{ deviceData.offlineCount }} 台</span>
+      </div>
+      <div>
+        <span>在线率:</span>
+        <span class="value">{{ deviceData.onlineRate }}%</span>
+      </div>
+    </template>
+  </threejs-base-dialog>
+</template>
+
+<script>
+import threejsBaseDialog from './base.vue'
+import { listDeviceStatus } from "@/api/device/device"
+
+let timer = null
+
+export default {
+  components: {
+    threejsBaseDialog,
+  },
+  props: {
+    propData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      deviceData: {
+        total: 0,
+        onlineCount: 0,
+        offlineCount: 0,
+        onlineRate: '0.0',
+      },
+    };
+  },
+  beforeDestroy() {
+    timer && clearInterval(timer)
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.fetchData()
+      timer = setInterval(this.fetchData, 1000 * 60 * 3)
+    },
+    async fetchData() {
+      try {
+        const areaCode = this.propData.areaCode || '-1'
+        const { data } = await listDeviceStatus({ areaCode })
+
+        if (data) {
+          const total = data.total || 0
+          const onlineCount = data.onlineCount || 0
+          const offlineCount = data.offlineCount || 0
+          const onlineRate = total > 0 ? ((onlineCount / total) * 100).toFixed(1) : '0.0'
+
+          this.deviceData = { total, onlineCount, offlineCount, onlineRate }
+        }
+      } catch (error) {
+        console.error('获取设备状态数据失败:', error)
+      }
+    },
+  },
+};
+</script>

+ 99 - 0
ems-ui-cloud/src/views/largeScreen/dialog/pv-road-grid.vue

@@ -0,0 +1,99 @@
+<template>
+  <threejs-base-dialog>
+    <template #title>
+      电网交互数据
+    </template>
+    <template #data>
+      <div>
+        <span>上网电量:</span>
+        <span class="value">{{ gridData.upElec }} kW·h</span>
+      </div>
+      <div>
+        <span>自用电量:</span>
+        <span class="value">{{ gridData.useElec }} kW·h</span>
+      </div>
+      <div>
+        <span>上网收益:</span>
+        <span class="value">{{ gridData.earn }} 元</span>
+      </div>
+      <div>
+        <span>自用比例:</span>
+        <span class="value">{{ gridData.useRate }}%</span>
+      </div>
+    </template>
+  </threejs-base-dialog>
+</template>
+
+<script>
+import threejsBaseDialog from './base.vue'
+import { getPvSupplySummary } from '@/api/mgr/pvSupply'
+import { numToStr } from "@/utils"
+
+let timer = null
+
+export default {
+  components: {
+    threejsBaseDialog,
+  },
+  props: {
+    propData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      gridData: {
+        upElec: '0.00',
+        useElec: '0.00',
+        earn: '0.00',
+        useRate: '0.0',
+      },
+    };
+  },
+  beforeDestroy() {
+    timer && clearInterval(timer)
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.fetchData()
+      timer = setInterval(this.fetchData, 1000 * 60 * 3)
+    },
+    async fetchData() {
+      try {
+        const areaCode = this.propData.areaCode || '-1'
+        const now = new Date()
+        const year = now.getFullYear()
+        const month = String(now.getMonth() + 1).padStart(2, '0')
+        const day = String(now.getDate()).padStart(2, '0')
+        const today = `${year}-${month}-${day}`
+
+        const { data } = await getPvSupplySummary({
+          areaCode,
+          startRecTime: `${today} 00:00:00`,
+          endRecTime: `${today} 23:59:59`,
+        })
+
+        if (data) {
+          const gen = Number(data.genElecQuantity || 0)
+          const up = Number(data.upElecQuantity || 0)
+          const use = Number(data.useElecQuantity || 0)
+          const useRate = gen > 0 ? ((use / gen) * 100).toFixed(1) : '0.0'
+
+          this.gridData = {
+            upElec: numToStr(up),
+            useElec: numToStr(use),
+            earn: numToStr(data.upElecEarn || 0),
+            useRate,
+          }
+        }
+      } catch (error) {
+        console.error('获取电网交互数据失败:', error)
+      }
+    },
+  },
+};
+</script>

+ 103 - 0
ems-ui-cloud/src/views/largeScreen/dialog/pv-road-overview.vue

@@ -0,0 +1,103 @@
+<template>
+  <threejs-base-dialog>
+    <template #title>
+      发电概览
+    </template>
+    <template #data>
+      <div>
+        <span>今日发电量:</span>
+        <span class="value">{{ overview.todayGen }} kW·h</span>
+      </div>
+      <div>
+        <span>本月发电量:</span>
+        <span class="value">{{ overview.monthGen }} kW·h</span>
+      </div>
+      <div>
+        <span>累计发电量:</span>
+        <span class="value">{{ overview.totalGen }} kW·h</span>
+      </div>
+      <div>
+        <span>上网收益:</span>
+        <span class="value">{{ overview.totalEarn }} 元</span>
+      </div>
+    </template>
+  </threejs-base-dialog>
+</template>
+
+<script>
+import threejsBaseDialog from './base.vue'
+import { getPvSupplySummary } from '@/api/mgr/pvSupply'
+import { numToStr } from "@/utils"
+import { DateTool } from "@/utils/DateTool"
+
+let timer = null
+
+export default {
+  components: {
+    threejsBaseDialog,
+  },
+  props: {
+    propData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      overview: {
+        todayGen: '0.00',
+        monthGen: '0.00',
+        totalGen: '0.00',
+        totalEarn: '0.00',
+      },
+    };
+  },
+  beforeDestroy() {
+    timer && clearInterval(timer)
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.fetchData()
+      timer = setInterval(this.fetchData, 1000 * 60 * 3)
+    },
+    async fetchData() {
+      try {
+        const areaCode = this.propData.areaCode || '-1'
+        const now = new Date()
+        const year = now.getFullYear()
+        const month = String(now.getMonth() + 1).padStart(2, '0')
+        const day = String(now.getDate()).padStart(2, '0')
+        const today = `${year}-${month}-${day}`
+        const monthStart = `${year}-${month}-01`
+        const yearStart = `${year}-01-01`
+
+        const [todayRes, monthRes, totalRes] = await Promise.all([
+          getPvSupplySummary({
+            areaCode,
+            startRecTime: `${today} 00:00:00`,
+            endRecTime: `${today} 23:59:59`,
+          }),
+          getPvSupplySummary({
+            areaCode,
+            startRecTime: `${monthStart} 00:00:00`,
+            endRecTime: `${today} 23:59:59`,
+          }),
+          getPvSupplySummary({ areaCode }),
+        ])
+
+        this.overview = {
+          todayGen: numToStr(todayRes?.data?.genElecQuantity || 0),
+          monthGen: numToStr(monthRes?.data?.genElecQuantity || 0),
+          totalGen: numToStr(totalRes?.data?.genElecQuantity || 0),
+          totalEarn: numToStr(totalRes?.data?.upElecEarn || 0),
+        }
+      } catch (error) {
+        console.error('获取发电概览数据失败:', error)
+      }
+    },
+  },
+};
+</script>

+ 121 - 0
ems-ui-cloud/src/views/largeScreen/dialog/pv-road-power.vue

@@ -0,0 +1,121 @@
+<template>
+  <threejs-base-dialog>
+    <template #title>
+      实时发电功率
+    </template>
+    <template #data>
+      <div>
+        <span>最近时段发电量:</span>
+        <span class="value">{{ powerData.lastHourGen }} kW·h</span>
+      </div>
+      <div>
+        <span>今日已发电时段:</span>
+        <span class="value">{{ powerData.activeHours }} 个</span>
+      </div>
+      <div>
+        <span>平均时段发电量:</span>
+        <span class="value">{{ powerData.avgHourGen }} kW·h</span>
+      </div>
+      <div>
+        <span>当前功率估算:</span>
+        <span class="value">{{ powerData.estimatedPower }} kW</span>
+      </div>
+    </template>
+  </threejs-base-dialog>
+</template>
+
+<script>
+import threejsBaseDialog from './base.vue'
+import { listPvSupplyHourly } from '@/api/mgr/pvSupply'
+import { numToStr } from "@/utils"
+import { DateTool } from "@/utils/DateTool"
+
+let timer = null
+
+export default {
+  components: {
+    threejsBaseDialog,
+  },
+  props: {
+    propData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      powerData: {
+        lastHourGen: '0.00',
+        activeHours: 0,
+        avgHourGen: '0.00',
+        estimatedPower: '0.00',
+      },
+    };
+  },
+  beforeDestroy() {
+    timer && clearInterval(timer)
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.fetchData()
+      timer = setInterval(this.fetchData, 1000 * 60 * 3)
+    },
+    async fetchData() {
+      try {
+        const areaCode = this.propData.areaCode || '-1'
+        const todayStr = DateTool.now(DateTool.DateFormat.YYYY_MM_DD)
+
+        const res = await listPvSupplyHourly({
+          areaCode,
+          startRecTime: `${todayStr} 00:00:00`,
+          endRecTime: `${todayStr} 23:59:59`,
+          pageNum: 1,
+          pageSize: 24,
+        })
+
+        const hourlyData = res.rows || []
+
+        if (hourlyData.length === 0) {
+          this.powerData = {
+            lastHourGen: '0.00',
+            activeHours: 0,
+            avgHourGen: '0.00',
+            estimatedPower: '0.00',
+          }
+          return
+        }
+
+        // 找到最后一条有数据的记录
+        const lastRecord = hourlyData[hourlyData.length - 1]
+        const lastHourGen = Number(lastRecord?.genElecQuantity || 0)
+
+        // 计算有发电量的时段数
+        const activeHours = hourlyData.filter(
+          item => Number(item.genElecQuantity || 0) > 0
+        ).length
+
+        // 计算总发电量和平均值
+        const totalGen = hourlyData.reduce(
+          (sum, item) => sum + Number(item.genElecQuantity || 0), 0
+        )
+        const avgHourGen = activeHours > 0 ? totalGen / activeHours : 0
+
+        // 估算当前功率(最近时段发电量 × 1000,因为1小时段的kW·h ≈ kW)
+        const estimatedPower = lastHourGen
+
+        this.powerData = {
+          lastHourGen: numToStr(lastHourGen),
+          activeHours,
+          avgHourGen: numToStr(avgHourGen),
+          estimatedPower: numToStr(estimatedPower),
+        }
+      } catch (error) {
+        console.error('获取实时功率数据失败:', error)
+      }
+    },
+  },
+};
+</script>

+ 35 - 12
ems-ui-cloud/src/views/largeScreen/pv-road/center.vue

@@ -10,7 +10,16 @@
 import {mapMutations} from 'vuex';
 import renderModel from '../three/renderModel'
 import ComponentHandle from "@/utils/ComponentHandle";
-import pvReal from "@/views/largeScreen/dialog/pv-real.vue";
+import pvRoadOverview from "@/views/largeScreen/dialog/pv-road-overview.vue";
+import pvRoadDevice from "@/views/largeScreen/dialog/pv-road-device.vue";
+import pvRoadGrid from "@/views/largeScreen/dialog/pv-road-grid.vue";
+import pvRoadPower from "@/views/largeScreen/dialog/pv-road-power.vue";
+
+const AREA_CODE = "321283124S3003"
+const BUBBLE_SCALE = 0.08
+// 整体偏移:往左(-x) 往下(-y),修正连接线对齐
+const GX = -30
+const GY = -40
 
 export default {
   name: 'Center',
@@ -22,7 +31,7 @@ export default {
   },
   components: {},
   mounted() {
-    this.setAreaType("321283124S3003")
+    this.setAreaType(AREA_CODE)
     this.initThree();
   },
   methods: {
@@ -31,16 +40,30 @@ export default {
     initThree() {
       setTimeout(() => {
         this.modelApi = new renderModel('#model')
-        this.modelApi.initGltfModel()
-        this.modelApi.addDialog(ComponentHandle.createComponent({
-          component: pvReal,
-          props: {
-            areaCode: "321283124S3003"
-          }
-        }), "主路光伏", {
-          x: 0,
-          y: 0,
-          z: 20
+        this.modelApi.initGltfModel(() => {
+          // 西北 10:30 — 发电概览
+          this.modelApi.addDialog(ComponentHandle.createComponent({
+            component: pvRoadOverview,
+            props: { areaCode: AREA_CODE }
+          }), "发电概览", { x: -50 + GX, y: 50 + GY, z: 20 }, BUBBLE_SCALE)
+
+          // 西南 7:30 — 设备状态
+          this.modelApi.addDialog(ComponentHandle.createComponent({
+            component: pvRoadDevice,
+            props: { areaCode: AREA_CODE }
+          }), "设备状态", { x: -50 + GX, y: -50 + GY, z: 20 }, BUBBLE_SCALE)
+
+          // 东北 1:30 — 电网交互
+          this.modelApi.addDialog(ComponentHandle.createComponent({
+            component: pvRoadGrid,
+            props: { areaCode: AREA_CODE }
+          }), "电网交互", { x: 50 + GX, y: 50 + GY, z: 20 }, BUBBLE_SCALE)
+
+          // 东南 4:30 — 实时功率
+          this.modelApi.addDialog(ComponentHandle.createComponent({
+            component: pvRoadPower,
+            props: { areaCode: AREA_CODE }
+          }), "实时功率", { x: 50 + GX, y: -50 + GY, z: 20 }, BUBBLE_SCALE)
         })
       }, 100)
 

+ 9 - 10
ems-ui-cloud/src/views/largeScreen/three/renderModel.js

@@ -156,7 +156,7 @@ class renderModel {
     window.addEventListener('click', this.onMouseClick.bind(this));
   }
 
-  initGltfModel() {
+  initGltfModel(loaded) {
     //初始化场景
     this.initScene();
     //初始化相机
@@ -167,7 +167,7 @@ class renderModel {
     this.createLight();
     //初始化控制器,控制摄像头,控制器一定要在渲染器后
     this.initControls();
-    this.setGlbModel();
+    this.setGlbModel(loaded);
     //监听场景大小改变,跳转渲染尺寸
     window.addEventListener('resize', this.onWindowResizes.bind(this));
     //场景渲染
@@ -353,17 +353,15 @@ class renderModel {
     return label; //返回CSS3模型标签
   }
 
-  createDialog(html) {
+  createDialog(html, scale = 0.15) {
     //div元素包装为CSS3模型对象CSS3DSprite
     const element = document.createElement('div');
     element.className = 'customDialog';
     element.appendChild(html);
     const dialog = new CSS3DSprite(element);
     element.style.pointerEvents = 'none'; //避免HTML标签遮挡三维场景的鼠标事件
-    // 设置HTML元素标签在three.js世界坐标中位置
-    // label.position.set(x, y, z);
-    //缩放CSS3DSprite模型对象
-    dialog.scale.set(0.15, 0.15, 0.15); //根据相机渲染范围控制HTML 3D标签尺寸
+    //设置缩放比例
+    dialog.scale.set(scale, scale, scale);
     dialog.rotateY(Math.PI / 2); //控制HTML标签CSS3对象姿态角度
     return dialog; //返回CSS3模型标签
   }
@@ -383,7 +381,7 @@ class renderModel {
     object.position.z = 0;
   }
 
-  setGlbModel() {
+  setGlbModel(loaded) {
     new GltfModelManager().loadModel('model-road-pv', '/models/model-road-pv.glb').then(object => {
       this.calcMeshCenter(object.scene);
       this.model = object.scene;
@@ -411,6 +409,7 @@ class renderModel {
       // if (selectedObjects.length > 0) {
       //   this.outlineObj(selectedObjects);
       // }
+      loaded && loaded();
     });
   }
 
@@ -619,10 +618,10 @@ class renderModel {
     this[modelType].add(group);
     this.roomLabelGroup = group; // 保存引用
   }
-  addDialog(html, labelName, position) {
+  addDialog(html, labelName, position, scale) {
     const { x, y, z } = position;
     const label3D = this.tag3D(labelName);
-    const dialog3D = this.createDialog(html); //设置标签名称
+    const dialog3D = this.createDialog(html, scale); //设置标签名称
     label3D.position.y += y;
     label3D.position.x += x;
     label3D.position.z += z;