Selaa lähdekoodia

fix: 传感器接口联调/逻辑调整

hewei 1 kuukausi sitten
vanhempi
commit
58e76c71dd

+ 17 - 0
src/api/sensor-group/index.ts

@@ -3,6 +3,8 @@ import type {
   AddSensorGroupReq,
   BatchSaveSensor2GroupReq,
   DeleteSensorFromGroupReq,
+  QueryDevicePropertiesHistoryReq,
+  QueryDevicePropertiesHistoryRes,
   ProductionSensorGroup,
   QueryDeviceListReq,
   QueryDeviceListRes,
@@ -130,3 +132,18 @@ export function queryDeviceListApi(data: QueryDeviceListReq) {
     data,
   });
 }
+
+/**
+ * 查询传感器历史属性(温度/湿度/浓度)
+ */
+export function queryDevicePropertiesHistoryApi(data: QueryDevicePropertiesHistoryReq) {
+  const historyApiUrl = `${window.location.protocol}//${window.location.hostname}`;
+  return http.request<QueryDevicePropertiesHistoryRes>({
+    url: `${historyApiUrl}/openapi/devices/properties/history`,
+    method: 'post',
+    data,
+    headers: {
+      Authorization: 'Basic ODM2NDIxOGUxNDE0NDQ3NTllNjY5Yjc5YmU2ZDQyNTI6YWJjZWNhZjg1N2I4NDljNDg3N2ZkNzRmZThkZDZkZTM=',
+    },
+  });
+}

+ 28 - 5
src/api/sensor-group/type.ts

@@ -7,6 +7,7 @@ export interface ProductionSensorGroupDetail {
   updatedAt: string;
   isDeleted: number;
   deviceNo: string;
+  deviceName: string;
   status: string;
 }
 
@@ -32,11 +33,11 @@ export interface ProductionSensorGroup {
 
 export interface SensorDeviceViewItem {
   id: number;
-  cameraGroupDetailId: number;
+  cameraGroupDetailId?: number;
   code: string;
   name: string;
-  url: string;
-  imageUrl: string;
+  url?: string;
+  imageUrl?: string;
   status: string;
 }
 
@@ -103,18 +104,20 @@ export interface QueryDeviceListRes {
 
 export interface UpdateSensorInGroupReq {
   groupId: number;
-  deviceNoList: string[];
+  deviceList: { deviceNo: string; deviceName: string; status: string }[];
 }
 
 export interface BatchSaveSensor2GroupReq {
   groupId: number;
-  deviceNoList: string[];
+  deviceList: { deviceNo: string; deviceName: string; status: string }[];
 }
 
 export interface SaveSensor2GroupReq {
   groupId: number;
   deviceNo: string;
   orderNum: number;
+  deviceName: string;
+  status: string;
 }
 
 export interface DeleteSensorFromGroupReq {
@@ -132,3 +135,23 @@ export interface UpdateSensorOrderItem {
   deviceNo: string;
   orderNum: number;
 }
+
+export interface QueryDevicePropertiesHistoryReq {
+  deviceId: number;
+  startTime: string;
+  endTime: string;
+  page: number;
+  size: number;
+}
+
+export interface DevicePropertiesHistoryItem {
+  deviceId: number;
+  key: string;
+  value: string | number;
+  timestamp: number;
+}
+
+export interface QueryDevicePropertiesHistoryRes {
+  total: number;
+  list: DevicePropertiesHistoryItem[];
+}

+ 3 - 6
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue

@@ -85,12 +85,9 @@
   function normalizeSensorGroups(list: ProductionSensorGroup[]): SensorGroupView[] {
     return (list || []).map((group) => {
       const details: SensorDeviceViewItem[] = (group.details || []).map((detail) => ({
-        id: detail.sensorId || detail.id,
-        cameraGroupDetailId: detail.id,
-        code: detail.deviceNo || String(detail.sensorId || detail.id),
-        name: detail.deviceNo || `传感器${String(detail.sensorId || detail.id).slice(-3)}`,
-        url: '',
-        imageUrl: '',
+        id: detail.sensorId,
+        code: detail.deviceNo,
+        name: detail.deviceName,
         status: detail.status,
       }));
 

+ 64 - 36
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue

@@ -1,6 +1,6 @@
 <template>
   <div ref="dragRef">
-    <div class="camera" v-for="camera in groupSensors" :key="camera.cameraGroupDetailId || camera.id">
+    <div class="camera" v-for="camera in groupSensors" :key="camera.id">
       <div class="IconAndCameraName">
         <div class="cameraIcon">
           <WarningFilled
@@ -35,9 +35,7 @@
         />
       </div>
 
-      <Thumbnail :imageUrl="camera.imageUrl" :code="camera.code" position="right">
-        <div class="mask"></div>
-      </Thumbnail>
+      <div class="mask"></div>
     </div>
   </div>
 </template>
@@ -45,13 +43,19 @@
 <script setup lang="ts">
   import { computed, ref, watch } from 'vue';
   import { storeToRefs } from 'pinia';
-  import type { SensorDeviceViewItem, SensorGroupView } from '@/api/sensor-group/type';
+  import type { ProductionSensorGroup, SensorDeviceViewItem, SensorGroupView } from '@/api/sensor-group/type';
   import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
   import { WarningFilled } from '@element-plus/icons-vue';
   import { ElMessageBox } from 'element-plus';
   import { useDraggable } from 'vue-draggable-plus';
-  import Thumbnail from '@/components/thumbnail/Thumbnail.vue';
-  import { deleteSensorFromGroupApi, updateSensorOrderApi } from '@/api/sensor-group';
+  import {
+    deleteSensorFromGroupApi,
+    querySensorGroupListApi,
+    updateSensorGroupApi,
+    updateSensorOrderApi,
+  } from '@/api/sensor-group';
+  import { userGridType } from '@/store/modules/userGridType';
+  import { GridType } from '../../type';
 
   const props = defineProps<{
     cameraGroup: SensorGroupView;
@@ -62,11 +66,31 @@
     return props.cameraGroup.details;
   });
 
-  const { carouselList, playingGroup, cameraInPlay } = storeToRefs(useCameraGroupList());
-  const { restartPlay } = useCameraGroupList();
+  const { carouselList, playingGroup, cameraInPlay, cameraGroupList, isPlaying, isPaused, playIntervalTime } =
+    storeToRefs(useCameraGroupList());
+  const { restartPlay, setPlayGroup, groupStartPlay } = useCameraGroupList();
+  const { changeGridType } = userGridType();
+
+  function normalizeSensorGroups(list: ProductionSensorGroup[]): SensorGroupView[] {
+    return (list || []).map((group) => {
+      const details: SensorDeviceViewItem[] = (group.details || []).map((detail) => ({
+        id: detail.sensorId,
+        code: detail.deviceNo,
+        name: detail.deviceName,
+        status: detail.status,
+      }));
+
+      return {
+        ...group,
+        isPaused: group.isPaused ?? 1,
+        details,
+        children: details,
+      } as any;
+    });
+  }
 
   function handleDelete(cameraGroup: SensorGroupView, camera: SensorDeviceViewItem) {
-    const text = '删除后,相机数据不可恢复,是否确认删除?';
+    const text = '删除后,传感器数据不可恢复,是否确认删除?';
     ElMessageBox.confirm(text, '提示', {
       cancelButtonText: '取消',
       confirmButtonText: '确定',
@@ -75,22 +99,36 @@
     })
       .then(async () => {
         await deleteSensorFromGroupApi({ groupId: cameraGroup.id, deviceNo: camera.code });
-        const newSensors = groupSensors.value.filter((x) => x.id !== camera.id);
-        cameraGroup.details = newSensors;
-        cameraInPlay.value = cameraInPlay.value.map((x) =>
-          x.id === camera.id
-            ? {
-                ...x,
-                id: -Date.now(),
-                cameraGroupDetailId: -Date.now(),
-                name: '',
-                code: '',
-                url: '',
-                imageUrl: '',
-              }
-            : x,
-        );
-        restartPlay();
+
+        const latestGroupList = await querySensorGroupListApi();
+        cameraGroupList.value = normalizeSensorGroups(latestGroupList) as any;
+
+        const defaultGroup = cameraGroupList.value.find((x) => x.isDefault === 1) as unknown as SensorGroupView;
+        if (defaultGroup) {
+          await updateSensorGroupApi({
+            groupId: defaultGroup.id,
+            isDefault: 1,
+          });
+
+          isPlaying.value = true;
+          setPlayGroup({ ...(defaultGroup as any), children: defaultGroup.details } as any);
+          playIntervalTime.value = defaultGroup.refreshIntervalSec || 60;
+          isPaused.value = !defaultGroup.isPaused;
+          changeGridType(GridType.nineGrids);
+          groupStartPlay(playIntervalTime.value);
+        } else {
+          cameraInPlay.value = cameraInPlay.value.map((x) =>
+            x.id === camera.id
+              ? {
+                  ...x,
+                  id: -Date.now(),
+                  name: '',
+                  code: '',
+                }
+              : x,
+          );
+          restartPlay();
+        }
       })
       .catch(() => {
         return;
@@ -198,14 +236,4 @@
       width: 100%;
     }
   }
-
-  :deep(.thumb-nail) {
-    display: block;
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: 9;
-  }
 </style>

+ 72 - 22
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue

@@ -28,9 +28,7 @@
               <WarningFilled v-if="isInvalid(data)" class="invalidCamera" style="color: red" />
             </div>
             <div class="cameraName">{{ node.label }}</div>
-            <Thumbnail :imageUrl="data.imageUrl" :code="data.code" position="right">
-              <div class="mask"></div>
-            </Thumbnail>
+            <div class="mask"></div>
           </div>
         </template>
       </el-tree>
@@ -51,10 +49,18 @@
     batchSaveSensor2GroupApi,
     deleteSensorFromGroupApi,
     queryDeviceListApi,
+    querySensorGroupListApi,
     saveSensor2GroupApi,
+    updateSensorGroupApi,
   } from '@/api/sensor-group';
-  import type { DeviceListItem, SensorGroupView, SensorDeviceViewItem } from '@/api/sensor-group/type';
-  import Thumbnail from '@/components/thumbnail/Thumbnail.vue';
+  import type {
+    DeviceListItem,
+    ProductionSensorGroup,
+    SensorGroupView,
+    SensorDeviceViewItem,
+  } from '@/api/sensor-group/type';
+  import { userGridType } from '@/store/modules/userGridType';
+  import { GridType } from '../../type';
 
   enum CameraTreeNodeType {
     group = 'group',
@@ -85,8 +91,9 @@
   // 已在分组中的设备 code(deviceNo),用于初始勾选
   const initialCheckedKeys = computed(() => groupSensors.value.map((s) => s.code));
 
-  const { cameraInPlay, cameraGroupList } = storeToRefs(useCameraGroupList());
-  const { restartPlay } = useCameraGroupList();
+  const { cameraInPlay, cameraGroupList, isPlaying, isPaused, playIntervalTime } = storeToRefs(useCameraGroupList());
+  const { restartPlay, setPlayGroup, groupStartPlay } = useCameraGroupList();
+  const { changeGridType } = userGridType();
 
   function syncGroupSensors(newSensors: SensorDeviceViewItem[]) {
     const targetGroup = cameraGroupList.value.find((x: any) => x.id === props.cameraGroup.id) as
@@ -96,6 +103,24 @@
     targetGroup.details = newSensors;
   }
 
+  function normalizeSensorGroups(list: ProductionSensorGroup[]): SensorGroupView[] {
+    return (list || []).map((group) => {
+      const details: SensorDeviceViewItem[] = (group.details || []).map((detail) => ({
+        id: detail.sensorId,
+        code: detail.deviceNo,
+        name: detail.deviceName,
+        status: detail.status,
+      }));
+
+      return {
+        ...group,
+        isPaused: group.isPaused ?? 1,
+        details,
+        children: details,
+      } as any;
+    });
+  }
+
   async function loadDeviceList(deviceName = '') {
     treeLoading.value = true;
     try {
@@ -127,37 +152,64 @@
       }
 
       if (addedDeviceNoList.length === 1) {
+        const addedDevice = deviceList.value.find((item) => item.deviceNo === addedDeviceNoList[0]);
         await saveSensor2GroupApi({
           groupId: props.cameraGroup.id,
           deviceNo: addedDeviceNoList[0],
           orderNum: props.cameraGroup.details.length + 1,
+          deviceName: addedDevice?.deviceName || '',
+          status: addedDevice?.deviceStatus || '',
         });
       } else if (addedDeviceNoList.length > 1) {
         await batchSaveSensor2GroupApi({
           groupId: props.cameraGroup.id,
-          deviceNoList: addedDeviceNoList,
+          deviceList: addedDeviceNoList.map((deviceNo) => {
+            const device = deviceList.value.find((item) => item.deviceNo === deviceNo);
+            return {
+              deviceNo,
+              deviceName: device?.deviceName || '',
+              status: device?.deviceStatus || '',
+            };
+          }),
         });
       }
 
       // 更新本地状态
-      const newSensors: SensorDeviceViewItem[] = checkedNodes.map((n) => ({
-        id: n.id,
-        cameraGroupDetailId: n.id,
-        code: n.code,
-        name: n.name,
-        url: '',
-        imageUrl: n.imageUrl || '',
-        status: n.disable ? 'offline' : 'online',
-      }));
+      const newSensors: SensorDeviceViewItem[] = checkedNodes.map((n) => {
+        const device = deviceList.value.find((item) => item.deviceNo === n.code);
+        return {
+          id: n.id,
+          code: n.code,
+          name: device?.deviceName || n.name,
+          status: device?.deviceStatus || '',
+        };
+      });
 
       syncGroupSensors(newSensors);
 
+      // 与初始化逻辑一致:保存后刷新分组列表并重置右侧播放数据
+      const latestGroupList = await querySensorGroupListApi();
+      cameraGroupList.value = normalizeSensorGroups(latestGroupList) as any;
+
+      const defaultGroup = cameraGroupList.value.find((x) => x.isDefault === 1) as unknown as SensorGroupView;
+      if (defaultGroup) {
+        await updateSensorGroupApi({
+          groupId: defaultGroup.id,
+          isDefault: 1,
+        });
+
+        isPlaying.value = true;
+        setPlayGroup({ ...(defaultGroup as any), children: defaultGroup.details } as any);
+        playIntervalTime.value = defaultGroup.refreshIntervalSec || 60;
+        isPaused.value = !defaultGroup.isPaused;
+        changeGridType(GridType.nineGrids);
+        groupStartPlay(playIntervalTime.value);
+      }
+
       // 移除已不在分组中的正在播放的传感器
       cameraInPlay.value = cameraInPlay.value.map((x) => {
         const stillExists = newSensors.find((s) => s.id === x.id);
-        return stillExists
-          ? x
-          : { ...x, id: -Date.now(), cameraGroupDetailId: -Date.now(), name: '', code: '', url: '', imageUrl: '' };
+        return stillExists ? x : { ...x, id: -Date.now(), name: '', code: '' };
       });
 
       restartPlay();
@@ -175,8 +227,6 @@
       id: device.id,
       code: device.deviceNo || String(device.id),
       name: device.deviceName || device.deviceNo || `设备${String(device.id).slice(-3)}`,
-      url: '',
-      imageUrl: device.imgUrl || '',
       nodeType: CameraTreeNodeType.camera,
       networkingState: device.deviceStatus === 'offline' ? 1 : 0,
       disable: device.deviceStatus === 'offline',

+ 97 - 32
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/CamerasGrid.vue

@@ -2,14 +2,22 @@
   <div class="main-grid" id="main-grid">
     <div class="sensor-grid-wrap" id="video-grid">
       <div class="sensor-grid" :class="gridClassName">
-        <div v-for="sensor in displaySensors" :key="sensor.id" class="sensor-card">
+        <div v-for="(sensor, index) in displaySensors" :key="`${sensor.id}-${index}`" class="sensor-card">
           <template v-if="hasSensor(sensor)">
-            <div class="sensor-title">{{ sensor.name || `传感器${String(sensor.id).slice(-3)}` }}</div>
-            <template v-if="getMetrics(sensor.id).length > 0">
+            <div class="sensor-title">{{ sensor.name }}</div>
+            <template v-if="getMetrics(getSensorId(sensor)).length > 0">
               <div class="metric-list">
-                <div v-for="item in getMetrics(sensor.id)" :key="item.label" class="metric-item">
+                <div
+                  v-for="(item, idx) in getMetrics(getSensorId(sensor))"
+                  :key="item.label"
+                  class="metric-item"
+                  :class="`metric-item-${idx}`"
+                >
                   <div class="metric-label">{{ item.label }}</div>
-                  <div class="metric-value">{{ item.value }}</div>
+                  <div class="metric-value">
+                    <span class="value-main">{{ item.value }}</span>
+                    <span class="value-unit">{{ getMetricUnit(item.label) }}</span>
+                  </div>
                 </div>
               </div>
             </template>
@@ -24,12 +32,14 @@
 
 <script setup lang="ts">
   import { computed } from 'vue';
-  import { CameraInPlay } from '../type';
+  import type { SensorDeviceViewItem } from '@/api/sensor-group/type';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import type { SensorRealtimeData } from './useSensorRealtime';
 
+  const METRIC_NAMES = ['温度', '湿度', '浓度'] as const;
+
   const props = defineProps<{
-    cameraInPlay: CameraInPlay[];
+    cameraInPlay: SensorDeviceViewItem[];
     currentGrid: LocalGridType;
     sensorData: SensorRealtimeData;
   }>();
@@ -37,13 +47,11 @@
   const displaySensors = computed(() => {
     const list = props.cameraInPlay;
     const appendNum = Math.max(0, props.currentGrid - list.length);
-    const emptyList: CameraInPlay[] = Array.from({ length: appendNum }, (_, index) => ({
+    const emptyList: SensorDeviceViewItem[] = Array.from({ length: appendNum }, (_, index) => ({
       id: -1000 - index,
-      cameraGroupDetailId: -1000 - index,
-      url: '',
       name: '',
       code: '',
-      imageUrl: '',
+      status: '',
     }));
 
     return list.concat(emptyList);
@@ -52,15 +60,27 @@
   const gridClassName = computed(() => `grid-${props.currentGrid}`);
 
   function getMetrics(sensorId: number) {
-    if (sensorId <= 0) return [];
-    const data = props.sensorData[sensorId] ?? {};
-    return Object.entries(data).map(([, { name, value, unit }]) => ({
-      label: name,
-      value: `${value}${unit || ''}`,
+    if (sensorId < 0) return [];
+    const data = props.sensorData[sensorId] ?? [];
+    return data.slice(0, 3).map((item, index) => ({
+      label: METRIC_NAMES[index] || `指标${index + 1}`,
+      value: String(item?.value ?? '-'),
     }));
   }
 
-  const hasSensor = (sensor: CameraInPlay) => sensor.id > 0 && (sensor.name !== '' || sensor.code !== '');
+  function getMetricUnit(label: string) {
+    if (label === '温度') return '°C';
+    if (label === '湿度') return '%RH';
+    if (label === '浓度') return 'mg/m3';
+    return '';
+  }
+
+  function getSensorId(sensor: SensorDeviceViewItem & { sensorId?: number }) {
+    return sensor.sensorId ?? sensor.id;
+  }
+
+  const hasSensor = (sensor: SensorDeviceViewItem & { sensorId?: number }) =>
+    getSensorId(sensor) >= 0 && (sensor.name !== '' || sensor.code !== '');
 </script>
 
 <style lang="scss" scoped>
@@ -103,51 +123,96 @@
       box-shadow: 0 1px 8px rgba(23, 119, 255, 0.05);
       padding: 10px;
       overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      min-height: 0;
 
       .sensor-title {
-        font-size: 16px;
+        font-size: 15px;
         color: #253b61;
         font-weight: 600;
-        margin-bottom: 10px;
+        margin-bottom: 8px;
       }
 
       .metric-list {
+        flex: 1;
         display: grid;
-        grid-template-columns: repeat(2, minmax(120px, 1fr));
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+        grid-template-rows: repeat(2, minmax(0, 1fr));
         gap: 8px;
-        align-content: start;
+        min-height: 0;
+        align-content: stretch;
       }
 
       .metric-item {
-        border: 1px solid #d8e3f6;
+        border: 1px solid #dbe6f8;
         border-radius: 4px;
-        background: #ffffff;
-        padding: 8px;
+        background: linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
+        padding: 6px 8px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        min-height: 0;
+        height: 100%;
 
         .metric-label {
-          font-size: 14px;
-          line-height: 20px;
-          color: #1f2f46;
+          font-size: 13px;
+          line-height: 18px;
+          color: #4f6488;
           font-weight: 600;
           margin-bottom: 4px;
         }
 
         .metric-value {
-          font-size: 14px;
-          line-height: 20px;
-          color: #8d9cb3;
+          display: flex;
+          align-items: baseline;
+          gap: 4px;
+
+          .value-main {
+            font-size: 20px;
+            line-height: 1;
+            font-weight: 700;
+            color: #1d3557;
+          }
+
+          .value-unit {
+            font-size: 12px;
+            color: #7d91b0;
+            font-weight: 500;
+          }
         }
       }
 
+      .metric-item-0 {
+        border-color: #ffd4c2;
+        background: linear-gradient(180deg, #fff7f2 0%, #fff3eb 100%);
+      }
+
+      .metric-item-1 {
+        border-color: #c6e7ff;
+        background: linear-gradient(180deg, #f5fbff 0%, #edf7ff 100%);
+      }
+
+      .metric-item-2 {
+        grid-column: 1 / -1;
+        width: 100%;
+        justify-self: stretch;
+        border-color: #d8e8c0;
+        background: linear-gradient(180deg, #f8fdf1 0%, #f1fae6 100%);
+      }
+
       .metric-empty {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
         font-size: 13px;
         color: #b0bec5;
-        margin-top: 8px;
       }
     }
 
     .empty-card {
-      height: 100%;
+      flex: 1;
       display: flex;
       align-items: center;
       justify-content: center;

+ 4 - 7
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/ScreenToolbar.vue

@@ -138,7 +138,6 @@
   import nineGrids_selected from '@/assets/icons/nine-square-grid/nineGrids-selected.png';
   import sixteenGrids from '@/assets/icons/nine-square-grid/sixteenGrids.png';
   import sixteenGrids_selected from '@/assets/icons/nine-square-grid/sixteenGrids-selected.png';
-  import type { SensorGroupStats } from './useSensorRealtime';
 
   export type LocalGridType = 1 | 4 | 9 | 16;
 
@@ -156,7 +155,6 @@
     playIntervalTime: number;
     currentRound: number;
     totalRound: number;
-    realtimeGroupStats?: SensorGroupStats;
   }>();
 
   const { currentGrid, isPlaying, isPaused, currentRound, totalRound } = toRefs(props);
@@ -187,12 +185,11 @@
 
   const groupStats = computed(() => {
     const g = playingGroup.value as any;
-    const realtime = props.realtimeGroupStats;
     return {
-      onlineRate: realtime?.onlineRate ?? g?.onlineRate ?? '--',
-      exceptionCount: realtime?.exceptionCount ?? g?.exceptionCount ?? '--',
-      outlineCount: realtime?.outlineCount ?? g?.outlineCount ?? '--',
-      failureCount: realtime?.failureCount ?? g?.failureCount ?? '--',
+      onlineRate: g?.onlineRate ?? '--',
+      exceptionCount: g?.exceptionCount ?? '--',
+      outlineCount: g?.outlineCount ?? '--',
+      failureCount: g?.failureCount ?? '--',
     };
   });
 

+ 10 - 10
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/VideosGridBase.vue

@@ -6,7 +6,6 @@
     :isPaused="isPaused"
     :currentRound="currentRound"
     :totalRound="totalRound"
-    :realtimeGroupStats="groupStats"
     @prev-round="playPreviousRound"
     @next-round="playNextRound"
     @toggle-pause="togglePause"
@@ -19,21 +18,18 @@
   import { storeToRefs } from 'pinia';
   import ScreenToolbar from './ScreenToolbar.vue';
   import CamerasGrid from './CamerasGrid.vue';
-  import { type CameraInPlay } from '../type';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import { useSensorRealtime } from './useSensorRealtime';
   import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
   import type { SensorGroupView } from '@/api/sensor-group/type';
 
-  const props = defineProps<{ cameraInPlay: CameraInPlay[] }>();
-
   const currentGrid = ref<LocalGridType>(9);
   const playIntervalTime = ref<number>(60);
   const isPaused = ref<boolean>(false);
   const currentRound = ref<number>(1);
   let autoplayTimer: ReturnType<typeof setInterval> | null = null;
 
-  const { sensorData, groupStats, connect, close } = useSensorRealtime();
+  const { sensorData, connect, close } = useSensorRealtime();
   const { playingGroup } = storeToRefs(useCameraGroupList());
 
   watch(
@@ -57,7 +53,7 @@
   watch(
     () => (playingGroup.value as unknown as SensorGroupView)?.details,
     (details) => {
-      const ids = (details || []).map((d) => d.id).filter((id) => id > 0);
+      const ids = (details || []).map((d) => d.id);
       if (ids.length > 0) {
         connect(ids);
       } else {
@@ -67,9 +63,13 @@
     { immediate: true, deep: true },
   );
 
+  const currentGroupSensors = computed(() => {
+    return ((playingGroup.value as unknown as SensorGroupView)?.details || []) as SensorGroupView['details'];
+  });
+
   const totalRound = computed(() => {
-    if (props.cameraInPlay.length === 0) return 1;
-    return Math.max(1, Math.ceil(props.cameraInPlay.length / currentGrid.value));
+    if (currentGroupSensors.value.length === 0) return 1;
+    return Math.max(1, Math.ceil(currentGroupSensors.value.length / currentGrid.value));
   });
 
   const isPlaying = computed(() => totalRound.value > 1);
@@ -77,7 +77,7 @@
   const cameraInPlayOfCurrentRound = computed(() => {
     const startIndex = (currentRound.value - 1) * currentGrid.value;
     const endIndex = startIndex + currentGrid.value;
-    return props.cameraInPlay.slice(startIndex, endIndex);
+    return currentGroupSensors.value.slice(startIndex, endIndex);
   });
 
   function normalizePlayInterval(value: number) {
@@ -141,7 +141,7 @@
   );
 
   watch(
-    () => props.cameraInPlay.length,
+    () => currentGroupSensors.value.length,
     () => {
       if (currentRound.value > totalRound.value) {
         currentRound.value = totalRound.value;

+ 25 - 107
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/useSensorRealtime.ts

@@ -1,130 +1,48 @@
 import { ref, onUnmounted } from 'vue';
+import { queryDevicePropertiesHistoryApi } from '@/api/sensor-group';
+import type { DevicePropertiesHistoryItem } from '@/api/sensor-group/type';
 
-export interface RealtimePropertyValue {
-  /** 中文名,如"温度" */
-  name: string;
-  /** 当前值 */
-  value: string | number;
-  /** 单位,如"°C" */
-  unit: string;
-}
-
-export type SensorRealtimeData = Record<number, Record<string, RealtimePropertyValue>>;
-
-export interface SensorGroupStats {
-  onlineRate: string | number;
-  exceptionCount: string | number;
-  outlineCount: string | number;
-  failureCount: string | number;
-}
-
-interface WsMessage {
-  deviceId: number;
-  properties: Record<string, RealtimePropertyValue>;
-}
-
-type WsGroupStatsMessage = Partial<SensorGroupStats>;
-
-function isWsSensorMessage(payload: unknown): payload is WsMessage {
-  if (!payload || typeof payload !== 'object') return false;
-  const msg = payload as Partial<WsMessage>;
-  return typeof msg.deviceId === 'number' && !!msg.properties && typeof msg.properties === 'object';
-}
-
-function isWsGroupStatsMessage(payload: unknown): payload is WsGroupStatsMessage {
-  if (!payload || typeof payload !== 'object') return false;
-  const msg = payload as Record<string, unknown>;
-  return 'onlineRate' in msg || 'exceptionCount' in msg || 'outlineCount' in msg || 'failureCount' in msg;
-}
+export type SensorRealtimeData = Record<number, DevicePropertiesHistoryItem[]>;
 
-function unwrapWsPayload(payload: unknown): unknown {
-  if (!payload || typeof payload !== 'object') return payload;
-  const maybeWrapped = payload as Record<string, unknown>;
-  if (maybeWrapped.data && typeof maybeWrapped.data === 'object') {
-    return maybeWrapped.data;
-  }
-  return payload;
-}
+const FIXED_HISTORY_PARAMS = {
+  startTime: '1742109231',
+  endTime: '1836803631',
+  page: 1,
+  size: 3,
+} as const;
 
 export function useSensorRealtime() {
   const sensorData = ref<SensorRealtimeData>({});
-  const groupStats = ref<SensorGroupStats>({
-    onlineRate: '--',
-    exceptionCount: '--',
-    outlineCount: '--',
-    failureCount: '--',
-  });
-  let ws: WebSocket | null = null;
 
-  function connect(deviceIds: number[]) {
-    close();
+  async function connect(deviceIds: number[]) {
     if (!deviceIds.length) return;
 
-    const protocol = window.location.protocol.includes('https') ? 'wss' : 'ws';
-    const url = `${protocol}://${window.location.host}/ws_api_bak/ws/properties/realtime`;
-    ws = new WebSocket(url);
-
-    ws.onopen = () => {
-      // 发送认证信息
-      ws?.send(
-        JSON.stringify({
-          action: 'auth',
-          authorization:
-            'Basic ODM2NDIxOGUxNDE0NDQ3NTllNjY5Yjc5YmU2ZDQyNTI6YWJjZWNhZjg1N2I4NDljNDg3N2ZkNzRmZThkZDZkZTM=',
-        }),
-      );
-
-      deviceIds.forEach((deviceId) => {
-        ws?.send(JSON.stringify({ action: 'subscribe', deviceId, keys: [] }));
-      });
-    };
-
-    ws.onmessage = (e: MessageEvent) => {
-      try {
-        const raw = JSON.parse(e.data as string);
-        const payload = unwrapWsPayload(raw);
+    await Promise.all(
+      deviceIds.map(async (deviceId) => {
+        try {
+          const res = await queryDevicePropertiesHistoryApi({
+            deviceId,
+            ...FIXED_HISTORY_PARAMS,
+          });
 
-        if (isWsSensorMessage(payload)) {
           sensorData.value = {
             ...sensorData.value,
-            [payload.deviceId]: {
-              ...(sensorData.value[payload.deviceId] ?? {}),
-              ...payload.properties,
-            },
+            [deviceId]: res?.list || [],
           };
-          return;
+        } catch {
+          // 忽略单个设备失败,避免影响其他设备展示
         }
-
-        if (isWsGroupStatsMessage(payload)) {
-          groupStats.value = {
-            onlineRate: payload.onlineRate ?? groupStats.value.onlineRate,
-            exceptionCount: payload.exceptionCount ?? groupStats.value.exceptionCount,
-            outlineCount: payload.outlineCount ?? groupStats.value.outlineCount,
-            failureCount: payload.failureCount ?? groupStats.value.failureCount,
-          };
-        }
-      } catch {}
-    };
-
-    ws.onerror = () => {
-      console.error('[SensorWS] connection error');
-      close();
-    };
-
-    ws.onclose = (event) => {
-      console.log('[SensorWS] closed:', event.code, event.reason);
-      close();
-    };
+      }),
+    );
   }
 
   function close() {
-    ws?.close();
-    ws = null;
+    return;
   }
 
   onUnmounted(() => {
-    close();
+    sensorData.value = {};
   });
 
-  return { sensorData, groupStats, connect, close };
+  return { sensorData, connect, close };
 }