Procházet zdrojové kódy

统一设备状态数据同步,信号机故障按报警值规范分类

  1. 数据同步:在线状态信号机离线数 = 设备状态信号机故障总数,控制模式总数 =
  信号机总数(718),三个面板共享一致的基准数据
  2. 信号机故障分类:按设备报警值规范分为控制板报警(1~10)、相位板报警(11~40)、检测板报警(41~60)、黄闪报警(255)四类,黄闪
  数量与控制模式中黄闪控制同步
  3. 全页面统一:StatusMonitoring、SpecialSituationMonitoring、TrunkCoordination
  三个页面的在线状态和设备状态组件改为调用API获取数据,与首页保持一致
  4. 设备状态饼图样式优化:信号机故障类型使用不同深浅的红色区分
画安 před 2 týdny
rodič
revize
47e8d1f482

+ 8 - 7
src/components/ui/DeviceStatusPie.vue

@@ -132,6 +132,8 @@ export default {
   align-items: center;
   gap: clamp(10px, 2vw, 30px);
   padding: 10px;
+  padding: 10px 20px;
+  box-sizing: border-box;
   container-type: size; /* 核心修改:让它成为容器查询的根节点 */
 }
 
@@ -140,8 +142,8 @@ export default {
   position: relative;
   /* 核心修改:直接计算出一个绝对的正方形尺寸!
      取“父级宽度的45%”和“父级高度”中更小的那一个作为边长,彻底杜绝拉伸 */
-  width: min(45cqw, 100cqh - 20px);
-  height: min(45cqw, 100cqh - 20px);
+  width: min(40cqw, 100cqh - 20px);
+  height: min(40cqw, 100cqh - 20px);
   
   flex-shrink: 0;
   display: flex;
@@ -229,9 +231,9 @@ export default {
 .status-panel {
   display: flex;
   flex-direction: column;
-  gap: clamp(6px, 1.5vh, 12px);
-  width: 45%;
-  flex-shrink: 0; /* 核心修改:保证右侧内容不被变形挤压 */
+  gap: clamp(2px, 0.8vh, 4px);
+  flex: 1; /* 让面板自动填满右侧的所有剩余空间 */
+  min-width: 0;
 }
 
 .status-item {
@@ -241,9 +243,7 @@ export default {
   font-size: clamp(11px, 1.4vh, 15px);
   font-weight: 500;
   color: #fff;
-  padding: clamp(5px, 1vh, 10px) clamp(8px, 1.2vw, 14px);
   transition: all 0.3s ease;
-  white-space: nowrap;
 }
 
 .status-item:hover {
@@ -260,6 +260,7 @@ export default {
   flex: 1;
   overflow: hidden;
   text-overflow: ellipsis;
+  white-space: nowrap;
 }
 .status-num {
   font-weight: 700;

+ 75 - 25
src/mock/api.js

@@ -438,6 +438,23 @@ export async function apiGetTrunkLineMenuTree() {
  * GET /api/devices/status/summary
  * 在线数每次请求轻微波动
  */
+// ── 信号机故障数缓存:同一秒内多次调用返回同一份数据,保证在线离线与故障同步 ──
+let _smFaultCache = { ts: 0, total: 0, faultTotal: 0 }
+function _getSmFaultSnapshot() {
+  const now = Math.floor(Date.now() / 1000)
+  if (_smFaultCache.ts === now) return _smFaultCache
+  const sm = DB.deviceStatus.signalMachine
+  const total = sm.chartData[0].value + sm.chartData[1].value
+  const yellowFlashMode = DB.homeData.controlModes.find(m => m.name === '黄闪控制')
+  const yellowFlash = yellowFlashMode ? yellowFlashMode.value : 0
+  const ctrlBoard = Math.max(0, _fluctuate(5, 3))
+  const phaseBoard = Math.max(0, _fluctuate(4, 2))
+  const detBoard = Math.max(0, _fluctuate(3, 2))
+  const faultTotal = ctrlBoard + phaseBoard + detBoard + yellowFlash
+  _smFaultCache = { ts: now, total, faultTotal, ctrlBoard, phaseBoard, detBoard, yellowFlash }
+  return _smFaultCache
+}
+
 export async function apiGetDeviceStatus(type) {
   await delay(200)
   function fluctuateStats(base) {
@@ -455,9 +472,24 @@ export async function apiGetDeviceStatus(type) {
       ]
     }
   }
+  // 信号机:离线数 = 故障总数(与设备状态同步)
+  function smStats() {
+    const snap = _getSmFaultSnapshot()
+    const online = snap.total - snap.faultTotal
+    const rate = Math.round(online / snap.total * 100)
+    return {
+      centerTitle: rate + '%',
+      centerSubTitle: `${online}/${snap.total}`,
+      chartData: [
+        { name: '在线', value: online, color: '#32F6F8' },
+        { name: '离线', value: snap.faultTotal, color: '#E4D552' },
+      ]
+    }
+  }
+  if (type === 'signalMachine') return ok(smStats())
   if (type && DB.deviceStatus[type]) return ok(fluctuateStats(DB.deviceStatus[type]))
   return ok({
-    signalMachine: fluctuateStats(DB.deviceStatus.signalMachine),
+    signalMachine: smStats(),
     detector: fluctuateStats(DB.deviceStatus.detector),
     camera: fluctuateStats(DB.deviceStatus.camera),
   })
@@ -488,12 +520,20 @@ export async function apiGetHomeSnapshot() {
   })
 }
 
-/** GET /api/home/control-mode-stats — 控制模式分布(轻微波动) */
+/** GET /api/home/control-mode-stats — 控制模式分布(总数与信号机总数同步,内部重分配) */
 export async function apiGetControlModeStats() {
   await delay(150)
-  return ok(DB.homeData.controlModes.map(m => ({
-    ...m, value: _fluctuate(m.value, Math.ceil(m.value * 0.05)),
-  })))
+  const sm = DB.deviceStatus.signalMachine
+  const total = sm.chartData[0].value + sm.chartData[1].value
+  const modes = DB.homeData.controlModes
+  // 各项按基准值波动
+  const fluctuated = modes.map(m => ({
+    ...m, value: Math.max(0, _fluctuate(m.value, Math.ceil(m.value * 0.05))),
+  }))
+  // 修正总数:将差值补到第一项(定周期控制),保证总数 = 信号机总数
+  const currentSum = fluctuated.reduce((s, m) => s + m.value, 0)
+  fluctuated[0].value = Math.max(0, fluctuated[0].value + (total - currentSum))
+  return ok(fluctuated)
 }
 
 /**
@@ -1029,41 +1069,51 @@ export async function apiGetMapLegendConfig() {
  */
 export async function apiGetDeviceFaultStatus() {
   await delay(200)
-  const sm = DB.deviceStatus.signalMachine
   const dt = DB.deviceStatus.detector
   const cam = DB.deviceStatus.camera
 
-  // 从在线数据推算故障数,每次波动
-  const smTotal = sm.chartData[0].value + sm.chartData[1].value
-  const smFault = 0  // 信号机无故障,用于测试无故障状态
+  // ── 信号机故障:从共享缓存获取,确保与在线状态的离线数一致 ──
+  const snap = _getSmFaultSnapshot()
+  const smTotal = snap.total
+  const smFaultTotal = snap.faultTotal
+  const smFaultList = [
+    { name: '正常', value: Math.max(0, smTotal - smFaultTotal), color: '#A0E551' },
+    { name: '控制板报警', value: snap.ctrlBoard, color: '#FF4545' },
+    { name: '相位板报警', value: snap.phaseBoard, color: '#D42A2A' },
+    { name: '检测板报警', value: snap.detBoard, color: '#9B1B1B' },
+    { name: '黄闪报警', value: snap.yellowFlash, color: '#5C0E0E' },
+  ]
+
+  // ── 检测器故障 ──
   const dtTotal = dt.chartData[0].value + dt.chartData[1].value
-  const dtFault = _fluctuate(dt.chartData[1].value, 5)
+  const dtFault = Math.max(0, _fluctuate(dt.chartData[1].value, 5))
+  const dtCommFault = Math.max(0, Math.floor(dtFault * 0.6))
+
+  // ── 红绿灯故障 ──
   const camTotal = cam.chartData[0].value + cam.chartData[1].value
-  const camFault = _fluctuate(cam.chartData[1].value, 2)
+  const camFault = Math.max(0, _fluctuate(cam.chartData[1].value, 2))
+  const camConflict = Math.max(0, Math.floor(camFault * 0.5))
 
   return ok({
     signalMachineStatus: {
-      centerTitle: Math.max(0, smFault) + '',
-      centerSubTitle: `${Math.max(0, smFault)}/${smTotal}`,
-      chartData: [
-        { name: '正常', value: Math.max(0, smTotal - smFault), color: '#A0E551' },
-        { name: '故障', value: Math.max(0, smFault), color: '#D03030' },
-      ]
+      centerTitle: smFaultTotal + '',
+      centerSubTitle: `${smFaultTotal}/${smTotal}`,
+      chartData: smFaultList,
     },
     detectorStatus: {
-      centerTitle: Math.max(0, dtFault) + '',
-      centerSubTitle: `${Math.max(0, dtFault)}/${dtTotal}`,
+      centerTitle: dtFault + '',
+      centerSubTitle: `${dtFault}/${dtTotal}`,
       chartData: [
-        { name: '通信故障', value: Math.max(0, Math.floor(dtFault * 0.6)), color: '#C6302B' },
-        { name: '数据异常', value: Math.max(0, dtFault - Math.floor(dtFault * 0.6)), color: '#faad14' },
+        { name: '通信故障', value: dtCommFault, color: '#C6302B' },
+        { name: '数据异常', value: Math.max(0, dtFault - dtCommFault), color: '#faad14' },
       ]
     },
     trafficLightStatus: {
-      centerTitle: Math.max(0, camFault) + '',
-      centerSubTitle: `${Math.max(0, camFault)}/${camTotal}`,
+      centerTitle: camFault + '',
+      centerSubTitle: `${camFault}/${camTotal}`,
       chartData: [
-        { name: '红绿冲突', value: Math.max(0, Math.floor(camFault * 0.5)), color: '#C6302B' },
-        { name: '红灯故障', value: Math.max(0, camFault - Math.floor(camFault * 0.5)), color: '#8F1E1E' },
+        { name: '红绿冲突', value: camConflict, color: '#C6302B' },
+        { name: '红灯故障', value: Math.max(0, camFault - camConflict), color: '#8F1E1E' },
       ]
     },
   })

+ 2 - 2
src/mock/mock_data.json

@@ -16562,7 +16562,7 @@
     "controlModes": [
       {
         "name": "定周期控制",
-        "value": 394,
+        "value": 396,
         "color": "#33a3ff"
       },
       {
@@ -16572,7 +16572,7 @@
       },
       {
         "name": "干线协调",
-        "value": 179,
+        "value": 180,
         "color": "#10b981"
       },
       {

+ 13 - 6
src/views/SpecialSituationMonitoring.vue

@@ -80,19 +80,19 @@
                 <!-- 总览Tab -->
                 <template v-if="activeLeftTab === 'overview'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
                 <!-- 路口Tab -->
                 <template v-if="activeLeftTab === 'crossing'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
             </div>
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
 
 
 export default {
@@ -159,6 +159,9 @@ export default {
             // 路口多选分屏
             crossingSelections: [],
             maxCrossingSlots: 4,
+            // 在线状态 & 设备状态数据
+            onlineStatusData: null,
+            deviceFaultData: null,
         };
     },
     watch: {
@@ -175,13 +178,17 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, taskData] = await Promise.all([
+        const [menuData, taskData, onlineData, faultData] = await Promise.all([
             apiGetTongzhouMenuTree(),
             apiGetTasks({ pageSize: 5 }),
+            apiGetDeviceStatus(),
+            apiGetDeviceFaultStatus(),
         ]);
         this.menuData = menuData || [];
         this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
+        this.onlineStatusData = onlineData || null;
+        this.deviceFaultData = faultData || null;
 
         // 组件挂载时检查路由
         this.checkRouteParams();

+ 13 - 6
src/views/StatusMonitoring.vue

@@ -80,19 +80,19 @@
                 <!-- 总览Tab -->
                 <template v-if="activeLeftTab === 'overview'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
                 <!-- 路口Tab -->
                 <template v-if="activeLeftTab === 'crossing'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
             </div>
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
 
 
 export default {
@@ -159,6 +159,9 @@ export default {
             // 路口多选分屏
             crossingSelections: [],
             maxCrossingSlots: 4,
+            // 在线状态 & 设备状态数据
+            onlineStatusData: null,
+            deviceFaultData: null,
         };
     },
     watch: {
@@ -175,13 +178,17 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, taskData] = await Promise.all([
+        const [menuData, taskData, onlineData, faultData] = await Promise.all([
             apiGetTongzhouMenuTree(),
             apiGetTasks({ pageSize: 5 }),
+            apiGetDeviceStatus(),
+            apiGetDeviceFaultStatus(),
         ]);
         this.menuData = menuData || [];
         this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
+        this.onlineStatusData = onlineData || null;
+        this.deviceFaultData = faultData || null;
 
         // 组件挂载时检查路由
         this.checkRouteParams();

+ 13 - 6
src/views/TrunkCoordination.vue

@@ -80,19 +80,19 @@
                 <!-- 总览Tab -->
                 <template v-if="activeLeftTab === 'overview'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
                 <!-- 路口Tab -->
                 <template v-if="activeLeftTab === 'crossing'">
                     <div class="top-chart-box overview-chart-box">
-                        <OnlineStatusTabs />
+                        <OnlineStatusTabs :deviceData="onlineStatusData" />
                     </div>
                     <div class="top-chart-box overview-chart-box">
-                        <DeviceStatusTabs />
+                        <DeviceStatusTabs :statusData="deviceFaultData" />
                     </div>
                 </template>
             </div>
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
 
 
 export default {
@@ -159,6 +159,9 @@ export default {
             // 路口多选分屏
             crossingSelections: [],
             maxCrossingSlots: 4,
+            // 在线状态 & 设备状态数据
+            onlineStatusData: null,
+            deviceFaultData: null,
         };
     },
     watch: {
@@ -175,13 +178,17 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, taskData] = await Promise.all([
+        const [menuData, taskData, onlineData, faultData] = await Promise.all([
             apiGetTongzhouMenuTree(),
             apiGetTasks({ pageSize: 5 }),
+            apiGetDeviceStatus(),
+            apiGetDeviceFaultStatus(),
         ]);
         this.menuData = menuData || [];
         this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
+        this.onlineStatusData = onlineData || null;
+        this.deviceFaultData = faultData || null;
 
         // 组件挂载时检查路由
         this.checkRouteParams();