Просмотр исходного кода

Merge branch 'feat/production-safety' of http://192.168.6.110/product-group-fe/sfy-safety-group/sfy-safety into feat/production-safety

sunqijun 1 месяц назад
Родитель
Сommit
9ebaf30529
18 измененных файлов с 427 добавлено и 309 удалено
  1. 1 0
      src/api/receiptRecord/index.ts
  2. 17 0
      src/api/sensor-group/index.ts
  3. 28 5
      src/api/sensor-group/type.ts
  4. 36 21
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleAccountManagement/components/hiddenTroubleAccountManagementDetail.vue
  5. 5 1
      src/views/production-safety/risk-identification-and-control/hazard-manage/list.vue
  6. 3 6
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue
  7. 64 36
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue
  8. 72 22
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue
  9. 97 32
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/CamerasGrid.vue
  10. 4 7
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/ScreenToolbar.vue
  11. 10 10
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/VideosGridBase.vue
  12. 25 107
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/useSensorRealtime.ts
  13. 51 54
      src/views/production-safety/safetyAssessment/evaluationDepartment/components/EvaluationDepartmentFeedback.vue
  14. 2 2
      src/views/production-safety/safetyAssessment/evaluationDepartment/evaluationDepartment.vue
  15. 4 1
      src/views/production-safety/safetyAssessment/evaluationSystem/evaluationSystem.vue
  16. 2 2
      src/views/production-safety/safetyAssessment/receiptRecord/components/ReceiptRecordDetail.vue
  17. 4 2
      src/views/production-safety/safetyAssessment/receiptRecord/receiptRecord.vue
  18. 2 1
      src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagementDept/components/addTrainingInformation.vue

+ 1 - 0
src/api/receiptRecord/index.ts

@@ -24,6 +24,7 @@ export interface ReceiptRecordItem {
   templateId?: number; // 审批流程模板ID(可选,后端字段为 templateId)
   templateId?: number; // 审批流程模板ID(可选,后端字段为 templateId)
   approvalOrder?: number; // 审批顺序(管理员审核页使用)
   approvalOrder?: number; // 审批顺序(管理员审核页使用)
   rejectReson?: string; // 审核不通过原因(后端字段为 rejectReson)
   rejectReson?: string; // 审核不通过原因(后端字段为 rejectReson)
+  conformClaimButton?: number; // 确认发放按钮
 }
 }
 
 
 /**
 /**

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

@@ -3,6 +3,8 @@ import type {
   AddSensorGroupReq,
   AddSensorGroupReq,
   BatchSaveSensor2GroupReq,
   BatchSaveSensor2GroupReq,
   DeleteSensorFromGroupReq,
   DeleteSensorFromGroupReq,
+  QueryDevicePropertiesHistoryReq,
+  QueryDevicePropertiesHistoryRes,
   ProductionSensorGroup,
   ProductionSensorGroup,
   QueryDeviceListReq,
   QueryDeviceListReq,
   QueryDeviceListRes,
   QueryDeviceListRes,
@@ -130,3 +132,18 @@ export function queryDeviceListApi(data: QueryDeviceListReq) {
     data,
     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;
   updatedAt: string;
   isDeleted: number;
   isDeleted: number;
   deviceNo: string;
   deviceNo: string;
+  deviceName: string;
   status: string;
   status: string;
 }
 }
 
 
@@ -32,11 +33,11 @@ export interface ProductionSensorGroup {
 
 
 export interface SensorDeviceViewItem {
 export interface SensorDeviceViewItem {
   id: number;
   id: number;
-  cameraGroupDetailId: number;
+  cameraGroupDetailId?: number;
   code: string;
   code: string;
   name: string;
   name: string;
-  url: string;
-  imageUrl: string;
+  url?: string;
+  imageUrl?: string;
   status: string;
   status: string;
 }
 }
 
 
@@ -103,18 +104,20 @@ export interface QueryDeviceListRes {
 
 
 export interface UpdateSensorInGroupReq {
 export interface UpdateSensorInGroupReq {
   groupId: number;
   groupId: number;
-  deviceNoList: string[];
+  deviceList: { deviceNo: string; deviceName: string; status: string }[];
 }
 }
 
 
 export interface BatchSaveSensor2GroupReq {
 export interface BatchSaveSensor2GroupReq {
   groupId: number;
   groupId: number;
-  deviceNoList: string[];
+  deviceList: { deviceNo: string; deviceName: string; status: string }[];
 }
 }
 
 
 export interface SaveSensor2GroupReq {
 export interface SaveSensor2GroupReq {
   groupId: number;
   groupId: number;
   deviceNo: string;
   deviceNo: string;
   orderNum: number;
   orderNum: number;
+  deviceName: string;
+  status: string;
 }
 }
 
 
 export interface DeleteSensorFromGroupReq {
 export interface DeleteSensorFromGroupReq {
@@ -132,3 +135,23 @@ export interface UpdateSensorOrderItem {
   deviceNo: string;
   deviceNo: string;
   orderNum: number;
   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[];
+}

+ 36 - 21
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleAccountManagement/components/hiddenTroubleAccountManagementDetail.vue

@@ -113,6 +113,7 @@
           clearable
           clearable
           :disabled="isViewMode"
           :disabled="isViewMode"
           style="width: 450px"
           style="width: 450px"
+          @change="onReviewDeptChange"
         />
         />
       </el-form-item>
       </el-form-item>
 
 
@@ -379,7 +380,7 @@ import { ru } from 'element-plus/es/locale';
 
 
   const router = useRouter();
   const router = useRouter();
   const route = useRoute();
   const route = useRoute();
-
+  const searchDeptName = ref('');
   const operate = computed(() => (route.query.operate as string) || 'hidden-trouble-account-create');
   const operate = computed(() => (route.query.operate as string) || 'hidden-trouble-account-create');
   const currentId = computed(() => Number(route.query.id));
   const currentId = computed(() => Number(route.query.id));
 
 
@@ -512,7 +513,9 @@ const attachmentsFileList = ref([]) as any
     try {
     try {
       const [deptRes, userRes] = await Promise.all([
       const [deptRes, userRes] = await Promise.all([
         getAllDepartments(),
         getAllDepartments(),
-        queryAvailableUserList({ pageNumber: 1, pageSize: 9999, queryParam: {} }),
+        queryAvailableUserList({ pageNumber: 1, pageSize: 9999, queryParam: {
+          deptName: searchDeptName.value || undefined,
+        } }),
       ]);
       ]);
       const fullTree = (deptRes as DeptTree[]) ?? [];
       const fullTree = (deptRes as DeptTree[]) ?? [];
       deptTree.value = Array.isArray(fullTree) && fullTree[0]?.children ? fullTree[0].children : [];
       deptTree.value = Array.isArray(fullTree) && fullTree[0]?.children ? fullTree[0].children : [];
@@ -829,29 +832,41 @@ const attachmentsFileList = ref([]) as any
     }
     }
   }
   }
 
 
-  const issueDeptTree = ref<DeptTree[]>([]);
-  const issueUserList = ref<Array<{ id: number; realname?: string; username?: string }>>([]);
-
-  const onIssueDialogOpen = async () => {
-    try {
-      const [deptRes, userRes] = await Promise.all([
-        getAllDepartments(),
-        queryAvailableUserList({ pageNumber: 1, pageSize: 9999, queryParam: {} }),
-      ]);
-      const fullTree = (deptRes as DeptTree[]) ?? [];
-      issueDeptTree.value = Array.isArray(fullTree) && fullTree[0]?.children ? fullTree[0].children : [];
-      issueUserList.value = (userRes as any)?.records ?? [];
-    } catch (e) {
-      console.error('获取部门/用户列表失败:', e);
-      ElMessage.error(e?.message || e?.data || '加载部门或负责人列表失败');
-      issueDeptTree.value = [];
-      issueUserList.value = [];
+  // const issueDeptTree = ref<DeptTree[]>([]);
+  // const issueUserList = ref<Array<{ id: number; realname?: string; username?: string }>>([]);
+
+  // const onIssueDialogOpen = async () => {
+  //   try {
+  //     const [deptRes, userRes] = await Promise.all([
+  //       getAllDepartments(),
+  //       queryAvailableUserList({ pageNumber: 1, pageSize: 9999, queryParam: {
+  //         deptName: searchDeptName.value || undefined,
+  //       } }),
+  //     ]);
+  //     const fullTree = (deptRes as DeptTree[]) ?? [];
+  //     issueDeptTree.value = Array.isArray(fullTree) && fullTree[0]?.children ? fullTree[0].children : [];
+  //     issueUserList.value = (userRes as any)?.records ?? [];
+  //   } catch (e) {
+  //     console.error('获取部门/用户列表失败:', e);
+  //     ElMessage.error(e?.message || e?.data || '加载部门或负责人列表失败');
+  //     issueDeptTree.value = [];
+  //     issueUserList.value = [];
+  //   }
+  // };
+
+  const onReviewDeptChange = (val: number[]) => {
+    const selectedNodes = reviewDeptCascaderRef.value?.getCheckedNodes();
+    if (selectedNodes && selectedNodes.length > 0) {
+      const selectedNode = selectedNodes[0];
+      const selectedLabel = selectedNode.label;
+      searchDeptName.value = selectedLabel;
+      ruleFormData.reviewPersonId = undefined;
+      loadDeptAndUserOptions();
     }
     }
-  };
+  }
 
 
   onMounted(async () => {
   onMounted(async () => {
     cloneRuleFormData();
     cloneRuleFormData();
-    onIssueDialogOpen();
     await loadDeptAndUserOptions();
     await loadDeptAndUserOptions();
     if (isEditMode.value || isViewMode.value) {
     if (isEditMode.value || isViewMode.value) {
       await getDetail();
       await getDetail();

+ 5 - 1
src/views/production-safety/risk-identification-and-control/hazard-manage/list.vue

@@ -89,7 +89,11 @@
           <el-table-column label="危险源编号" prop="hazardCode" width="180" />
           <el-table-column label="危险源编号" prop="hazardCode" width="180" />
           <el-table-column label="关键过程/作业单元" prop="keyProcessUnit" width="180" />
           <el-table-column label="关键过程/作业单元" prop="keyProcessUnit" width="180" />
           <el-table-column label="危险源名称" prop="hazardName" width="180" />
           <el-table-column label="危险源名称" prop="hazardName" width="180" />
-          <el-table-column label="是否重大危险" prop="isMajorHazard" width="180" />
+          <el-table-column label="是否重大危险" prop="isMajorHazard" width="180">
+            <template #default="scope">
+              {{ scope.row.isMajorHazard === 1 ? '是' : '否' }}
+            </template>
+          </el-table-column>
           <el-table-column label="危险源来源" prop="hazardSource" width="180" />
           <el-table-column label="危险源来源" prop="hazardSource" width="180" />
           <el-table-column label="涉及型号" prop="involvedDepartment" width="180" />
           <el-table-column label="涉及型号" prop="involvedDepartment" width="180" />
           <el-table-column label="危险源管理的主责单位/部门" prop="riskManagementDept" width="240" />
           <el-table-column label="危险源管理的主责单位/部门" prop="riskManagementDept" width="240" />

+ 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[] {
   function normalizeSensorGroups(list: ProductionSensorGroup[]): SensorGroupView[] {
     return (list || []).map((group) => {
     return (list || []).map((group) => {
       const details: SensorDeviceViewItem[] = (group.details || []).map((detail) => ({
       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,
         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>
 <template>
   <div ref="dragRef">
   <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="IconAndCameraName">
         <div class="cameraIcon">
         <div class="cameraIcon">
           <WarningFilled
           <WarningFilled
@@ -35,9 +35,7 @@
         />
         />
       </div>
       </div>
 
 
-      <Thumbnail :imageUrl="camera.imageUrl" :code="camera.code" position="right">
-        <div class="mask"></div>
-      </Thumbnail>
+      <div class="mask"></div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -45,13 +43,19 @@
 <script setup lang="ts">
 <script setup lang="ts">
   import { computed, ref, watch } from 'vue';
   import { computed, ref, watch } from 'vue';
   import { storeToRefs } from 'pinia';
   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 { useCameraGroupList } from '@/store/modules/useCameraGroupList';
   import { WarningFilled } from '@element-plus/icons-vue';
   import { WarningFilled } from '@element-plus/icons-vue';
   import { ElMessageBox } from 'element-plus';
   import { ElMessageBox } from 'element-plus';
   import { useDraggable } from 'vue-draggable-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<{
   const props = defineProps<{
     cameraGroup: SensorGroupView;
     cameraGroup: SensorGroupView;
@@ -62,11 +66,31 @@
     return props.cameraGroup.details;
     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) {
   function handleDelete(cameraGroup: SensorGroupView, camera: SensorDeviceViewItem) {
-    const text = '删除后,相机数据不可恢复,是否确认删除?';
+    const text = '删除后,传感器数据不可恢复,是否确认删除?';
     ElMessageBox.confirm(text, '提示', {
     ElMessageBox.confirm(text, '提示', {
       cancelButtonText: '取消',
       cancelButtonText: '取消',
       confirmButtonText: '确定',
       confirmButtonText: '确定',
@@ -75,22 +99,36 @@
     })
     })
       .then(async () => {
       .then(async () => {
         await deleteSensorFromGroupApi({ groupId: cameraGroup.id, deviceNo: camera.code });
         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(() => {
       .catch(() => {
         return;
         return;
@@ -198,14 +236,4 @@
       width: 100%;
       width: 100%;
     }
     }
   }
   }
-
-  :deep(.thumb-nail) {
-    display: block;
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: 9;
-  }
 </style>
 </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" />
               <WarningFilled v-if="isInvalid(data)" class="invalidCamera" style="color: red" />
             </div>
             </div>
             <div class="cameraName">{{ node.label }}</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>
           </div>
         </template>
         </template>
       </el-tree>
       </el-tree>
@@ -51,10 +49,18 @@
     batchSaveSensor2GroupApi,
     batchSaveSensor2GroupApi,
     deleteSensorFromGroupApi,
     deleteSensorFromGroupApi,
     queryDeviceListApi,
     queryDeviceListApi,
+    querySensorGroupListApi,
     saveSensor2GroupApi,
     saveSensor2GroupApi,
+    updateSensorGroupApi,
   } from '@/api/sensor-group';
   } 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 {
   enum CameraTreeNodeType {
     group = 'group',
     group = 'group',
@@ -85,8 +91,9 @@
   // 已在分组中的设备 code(deviceNo),用于初始勾选
   // 已在分组中的设备 code(deviceNo),用于初始勾选
   const initialCheckedKeys = computed(() => groupSensors.value.map((s) => s.code));
   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[]) {
   function syncGroupSensors(newSensors: SensorDeviceViewItem[]) {
     const targetGroup = cameraGroupList.value.find((x: any) => x.id === props.cameraGroup.id) as
     const targetGroup = cameraGroupList.value.find((x: any) => x.id === props.cameraGroup.id) as
@@ -96,6 +103,24 @@
     targetGroup.details = newSensors;
     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 = '') {
   async function loadDeviceList(deviceName = '') {
     treeLoading.value = true;
     treeLoading.value = true;
     try {
     try {
@@ -127,37 +152,64 @@
       }
       }
 
 
       if (addedDeviceNoList.length === 1) {
       if (addedDeviceNoList.length === 1) {
+        const addedDevice = deviceList.value.find((item) => item.deviceNo === addedDeviceNoList[0]);
         await saveSensor2GroupApi({
         await saveSensor2GroupApi({
           groupId: props.cameraGroup.id,
           groupId: props.cameraGroup.id,
           deviceNo: addedDeviceNoList[0],
           deviceNo: addedDeviceNoList[0],
           orderNum: props.cameraGroup.details.length + 1,
           orderNum: props.cameraGroup.details.length + 1,
+          deviceName: addedDevice?.deviceName || '',
+          status: addedDevice?.deviceStatus || '',
         });
         });
       } else if (addedDeviceNoList.length > 1) {
       } else if (addedDeviceNoList.length > 1) {
         await batchSaveSensor2GroupApi({
         await batchSaveSensor2GroupApi({
           groupId: props.cameraGroup.id,
           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);
       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) => {
       cameraInPlay.value = cameraInPlay.value.map((x) => {
         const stillExists = newSensors.find((s) => s.id === x.id);
         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();
       restartPlay();
@@ -175,8 +227,6 @@
       id: device.id,
       id: device.id,
       code: device.deviceNo || String(device.id),
       code: device.deviceNo || String(device.id),
       name: device.deviceName || device.deviceNo || `设备${String(device.id).slice(-3)}`,
       name: device.deviceName || device.deviceNo || `设备${String(device.id).slice(-3)}`,
-      url: '',
-      imageUrl: device.imgUrl || '',
       nodeType: CameraTreeNodeType.camera,
       nodeType: CameraTreeNodeType.camera,
       networkingState: device.deviceStatus === 'offline' ? 1 : 0,
       networkingState: device.deviceStatus === 'offline' ? 1 : 0,
       disable: device.deviceStatus === 'offline',
       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="main-grid" id="main-grid">
     <div class="sensor-grid-wrap" id="video-grid">
     <div class="sensor-grid-wrap" id="video-grid">
       <div class="sensor-grid" :class="gridClassName">
       <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)">
           <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 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-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>
               </div>
               </div>
             </template>
             </template>
@@ -24,12 +32,14 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
   import { computed } from 'vue';
   import { computed } from 'vue';
-  import { CameraInPlay } from '../type';
+  import type { SensorDeviceViewItem } from '@/api/sensor-group/type';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import type { SensorRealtimeData } from './useSensorRealtime';
   import type { SensorRealtimeData } from './useSensorRealtime';
 
 
+  const METRIC_NAMES = ['温度', '湿度', '浓度'] as const;
+
   const props = defineProps<{
   const props = defineProps<{
-    cameraInPlay: CameraInPlay[];
+    cameraInPlay: SensorDeviceViewItem[];
     currentGrid: LocalGridType;
     currentGrid: LocalGridType;
     sensorData: SensorRealtimeData;
     sensorData: SensorRealtimeData;
   }>();
   }>();
@@ -37,13 +47,11 @@
   const displaySensors = computed(() => {
   const displaySensors = computed(() => {
     const list = props.cameraInPlay;
     const list = props.cameraInPlay;
     const appendNum = Math.max(0, props.currentGrid - list.length);
     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,
       id: -1000 - index,
-      cameraGroupDetailId: -1000 - index,
-      url: '',
       name: '',
       name: '',
       code: '',
       code: '',
-      imageUrl: '',
+      status: '',
     }));
     }));
 
 
     return list.concat(emptyList);
     return list.concat(emptyList);
@@ -52,15 +60,27 @@
   const gridClassName = computed(() => `grid-${props.currentGrid}`);
   const gridClassName = computed(() => `grid-${props.currentGrid}`);
 
 
   function getMetrics(sensorId: number) {
   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>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -103,51 +123,96 @@
       box-shadow: 0 1px 8px rgba(23, 119, 255, 0.05);
       box-shadow: 0 1px 8px rgba(23, 119, 255, 0.05);
       padding: 10px;
       padding: 10px;
       overflow: hidden;
       overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      min-height: 0;
 
 
       .sensor-title {
       .sensor-title {
-        font-size: 16px;
+        font-size: 15px;
         color: #253b61;
         color: #253b61;
         font-weight: 600;
         font-weight: 600;
-        margin-bottom: 10px;
+        margin-bottom: 8px;
       }
       }
 
 
       .metric-list {
       .metric-list {
+        flex: 1;
         display: grid;
         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;
         gap: 8px;
-        align-content: start;
+        min-height: 0;
+        align-content: stretch;
       }
       }
 
 
       .metric-item {
       .metric-item {
-        border: 1px solid #d8e3f6;
+        border: 1px solid #dbe6f8;
         border-radius: 4px;
         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 {
         .metric-label {
-          font-size: 14px;
-          line-height: 20px;
-          color: #1f2f46;
+          font-size: 13px;
+          line-height: 18px;
+          color: #4f6488;
           font-weight: 600;
           font-weight: 600;
           margin-bottom: 4px;
           margin-bottom: 4px;
         }
         }
 
 
         .metric-value {
         .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 {
       .metric-empty {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
         font-size: 13px;
         font-size: 13px;
         color: #b0bec5;
         color: #b0bec5;
-        margin-top: 8px;
       }
       }
     }
     }
 
 
     .empty-card {
     .empty-card {
-      height: 100%;
+      flex: 1;
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       justify-content: 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 nineGrids_selected from '@/assets/icons/nine-square-grid/nineGrids-selected.png';
   import sixteenGrids from '@/assets/icons/nine-square-grid/sixteenGrids.png';
   import sixteenGrids from '@/assets/icons/nine-square-grid/sixteenGrids.png';
   import sixteenGrids_selected from '@/assets/icons/nine-square-grid/sixteenGrids-selected.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;
   export type LocalGridType = 1 | 4 | 9 | 16;
 
 
@@ -156,7 +155,6 @@
     playIntervalTime: number;
     playIntervalTime: number;
     currentRound: number;
     currentRound: number;
     totalRound: number;
     totalRound: number;
-    realtimeGroupStats?: SensorGroupStats;
   }>();
   }>();
 
 
   const { currentGrid, isPlaying, isPaused, currentRound, totalRound } = toRefs(props);
   const { currentGrid, isPlaying, isPaused, currentRound, totalRound } = toRefs(props);
@@ -187,12 +185,11 @@
 
 
   const groupStats = computed(() => {
   const groupStats = computed(() => {
     const g = playingGroup.value as any;
     const g = playingGroup.value as any;
-    const realtime = props.realtimeGroupStats;
     return {
     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"
     :isPaused="isPaused"
     :currentRound="currentRound"
     :currentRound="currentRound"
     :totalRound="totalRound"
     :totalRound="totalRound"
-    :realtimeGroupStats="groupStats"
     @prev-round="playPreviousRound"
     @prev-round="playPreviousRound"
     @next-round="playNextRound"
     @next-round="playNextRound"
     @toggle-pause="togglePause"
     @toggle-pause="togglePause"
@@ -19,21 +18,18 @@
   import { storeToRefs } from 'pinia';
   import { storeToRefs } from 'pinia';
   import ScreenToolbar from './ScreenToolbar.vue';
   import ScreenToolbar from './ScreenToolbar.vue';
   import CamerasGrid from './CamerasGrid.vue';
   import CamerasGrid from './CamerasGrid.vue';
-  import { type CameraInPlay } from '../type';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import type { LocalGridType } from './ScreenToolbar.vue';
   import { useSensorRealtime } from './useSensorRealtime';
   import { useSensorRealtime } from './useSensorRealtime';
   import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
   import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
   import type { SensorGroupView } from '@/api/sensor-group/type';
   import type { SensorGroupView } from '@/api/sensor-group/type';
 
 
-  const props = defineProps<{ cameraInPlay: CameraInPlay[] }>();
-
   const currentGrid = ref<LocalGridType>(9);
   const currentGrid = ref<LocalGridType>(9);
   const playIntervalTime = ref<number>(60);
   const playIntervalTime = ref<number>(60);
   const isPaused = ref<boolean>(false);
   const isPaused = ref<boolean>(false);
   const currentRound = ref<number>(1);
   const currentRound = ref<number>(1);
   let autoplayTimer: ReturnType<typeof setInterval> | null = null;
   let autoplayTimer: ReturnType<typeof setInterval> | null = null;
 
 
-  const { sensorData, groupStats, connect, close } = useSensorRealtime();
+  const { sensorData, connect, close } = useSensorRealtime();
   const { playingGroup } = storeToRefs(useCameraGroupList());
   const { playingGroup } = storeToRefs(useCameraGroupList());
 
 
   watch(
   watch(
@@ -57,7 +53,7 @@
   watch(
   watch(
     () => (playingGroup.value as unknown as SensorGroupView)?.details,
     () => (playingGroup.value as unknown as SensorGroupView)?.details,
     (details) => {
     (details) => {
-      const ids = (details || []).map((d) => d.id).filter((id) => id > 0);
+      const ids = (details || []).map((d) => d.id);
       if (ids.length > 0) {
       if (ids.length > 0) {
         connect(ids);
         connect(ids);
       } else {
       } else {
@@ -67,9 +63,13 @@
     { immediate: true, deep: true },
     { immediate: true, deep: true },
   );
   );
 
 
+  const currentGroupSensors = computed(() => {
+    return ((playingGroup.value as unknown as SensorGroupView)?.details || []) as SensorGroupView['details'];
+  });
+
   const totalRound = computed(() => {
   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);
   const isPlaying = computed(() => totalRound.value > 1);
@@ -77,7 +77,7 @@
   const cameraInPlayOfCurrentRound = computed(() => {
   const cameraInPlayOfCurrentRound = computed(() => {
     const startIndex = (currentRound.value - 1) * currentGrid.value;
     const startIndex = (currentRound.value - 1) * currentGrid.value;
     const endIndex = startIndex + currentGrid.value;
     const endIndex = startIndex + currentGrid.value;
-    return props.cameraInPlay.slice(startIndex, endIndex);
+    return currentGroupSensors.value.slice(startIndex, endIndex);
   });
   });
 
 
   function normalizePlayInterval(value: number) {
   function normalizePlayInterval(value: number) {
@@ -141,7 +141,7 @@
   );
   );
 
 
   watch(
   watch(
-    () => props.cameraInPlay.length,
+    () => currentGroupSensors.value.length,
     () => {
     () => {
       if (currentRound.value > totalRound.value) {
       if (currentRound.value > totalRound.value) {
         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 { 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() {
 export function useSensorRealtime() {
   const sensorData = ref<SensorRealtimeData>({});
   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;
     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 = {
             ...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() {
   function close() {
-    ws?.close();
-    ws = null;
+    return;
   }
   }
 
 
   onUnmounted(() => {
   onUnmounted(() => {
-    close();
+    sensorData.value = {};
   });
   });
 
 
-  return { sensorData, groupStats, connect, close };
+  return { sensorData, connect, close };
 }
 }

+ 51 - 54
src/views/production-safety/safetyAssessment/evaluationDepartment/components/EvaluationDepartmentFeedback.vue

@@ -74,7 +74,7 @@
               <UploadFiles
               <UploadFiles
                 label="上传附件"
                 label="上传附件"
                 :file-list="scope.row.attachmentFileList"
                 :file-list="scope.row.attachmentFileList"
-                @uploadSuccess="(files: FileItem[]) => handleRowUploadSuccess(scope.$index, files)"
+                @uploadSuccess="(list: FileItem[]) => handleRowUploadSuccess(scope.$index, list)"
               />
               />
             </template>
             </template>
           </el-table-column>
           </el-table-column>
@@ -200,38 +200,36 @@
       });
       });
     });
     });
   };
   };
-let spanArr = ref<number[]>([]); // 存储每行的合并数,0表示被合并的行
-const generateSpanArr = (data) => {
-  spanArr.value = [];
-  let position = 0; // 记录当前考核项目的起始位置
-  
-  data.forEach((item, index) => {
-    if (index === 0) {
-      spanArr.value.push(1);
-      position = 0;
-    } else {
-      // 当前考核项目与前一项相同时合并
-      if (item.evaluationItem === data[index-1].evaluationItem) {
-        spanArr.value[position] += 1;   // 起始行合并数+1
-        spanArr.value.push(0);           // 当前行标记为合并
+
+  let spanArr = ref<number[]>([]); // 存储每行的合并数,0表示被合并的行
+  const generateSpanArr = (data) => {
+    spanArr.value = [];
+    let position = 0; // 记录当前考核项目的起始位置
+    
+    data.forEach((item, index) => {
+      if (index === 0) {
+        spanArr.value.push(1);
+        position = 0;
       } else {
       } else {
-        spanArr.value.push(1);           // 新考核项目组
-        position = index;                // 更新起始位置
+        // 当前考核项目与前一项相同时合并
+        if (item.evaluationItem === data[index-1].evaluationItem) {
+          spanArr.value[position] += 1;   // 起始行合并数+1
+          spanArr.value.push(0);           // 当前行标记为合并
+        } else {
+          spanArr.value.push(1);           // 新考核项目组
+          position = index;                // 更新起始位置
+        }
       }
       }
+    });
+  };
+  const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
+    // 仅对考核项目列(columnIndex=1)进行合并
+    if (columnIndex === 1) {
+      const _row = spanArr.value[rowIndex];
+      const _col = _row > 0 ? 1 : 0;
+      return [_row, _col];
     }
     }
-  });
-};
-const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
-  // 仅对考核项目列(columnIndex=1)进行合并
-  if (columnIndex === 1) {
-    const _row = spanArr.value[rowIndex];
-    const _col = _row > 0 ? 1 : 0;
-    return [_row, _col];
-  }
-  return [1, 1]; // 其他列不合并
-};
-  const handleUploadSuccess = (files: any[]) => {
-    ruleFormData.value.attachmentDocument = files;
+    return [1, 1]; // 其他列不合并
   };
   };
 
 
   const handleDownloadTemplate = () => {
   const handleDownloadTemplate = () => {
@@ -291,9 +289,9 @@ const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
   };
   };
 
 
   // 行内上传附件成功
   // 行内上传附件成功
-  const handleRowUploadSuccess = (rowIndex: number, files: FileItem[]) => {
+  const handleRowUploadSuccess = async (rowIndex: number, list: FileItem[]) => {
     if (!evaluationItems.value[rowIndex]) return;
     if (!evaluationItems.value[rowIndex]) return;
-    evaluationItems.value[rowIndex].attachmentFileList = files;
+    evaluationItems.value[rowIndex].attachmentFileList = await formatAttachmentList(list);
   };
   };
 
 
   // 将逗号分隔的 URL 字符串转换为 FileItem[] 格式
   // 将逗号分隔的 URL 字符串转换为 FileItem[] 格式
@@ -357,7 +355,7 @@ const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
           selfScore: score.selfScore, // 自评得分
           selfScore: score.selfScore, // 自评得分
           materialDescription: score.attachments || '', // 资料说明(使用附件字段,字符串)
           materialDescription: score.attachments || '', // 资料说明(使用附件字段,字符串)
           reviewRejectReson: score.reviewRejectReson || '', // 复核不通过原因
           reviewRejectReson: score.reviewRejectReson || '', // 复核不通过原因
-          attachmentFileList: parseAttachmentsToFileList(score.attachments || ''), // 对应的附件文件列表
+          attachmentFileList: JSON.parse(score.attachments || '[]'), // 对应的附件文件列表
         }));
         }));
         generateSpanArr(evaluationItems.value);
         generateSpanArr(evaluationItems.value);
       } else {
       } else {
@@ -400,30 +398,29 @@ const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
         (await Promise.all(
         (await Promise.all(
           (detailData.value.scores || []).map(async (score: any) => {
           (detailData.value.scores || []).map(async (score: any) => {
             const item = evaluationItems.value.find((row) => row.id === score.id);
             const item = evaluationItems.value.find((row) => row.id === score.id);
-
             // 处理资料说明附件:将 UploadFiles 返回的文件列表转换为逗号分隔的 URL 字符串
             // 处理资料说明附件:将 UploadFiles 返回的文件列表转换为逗号分隔的 URL 字符串
             let attachments = score.attachments || '';
             let attachments = score.attachments || '';
             if (item && Array.isArray(item.attachmentFileList)) {
             if (item && Array.isArray(item.attachmentFileList)) {
-              const existingFiles: string[] = [];
-              const newFiles: any[] = [];
-
-              item.attachmentFileList.forEach((file: any) => {
-                if (file.fileUrl && !file.file) {
-                  existingFiles.push(file.fileUrl);
-                } else {
-                  newFiles.push(file);
-                }
-              });
-
-              let uploadedUrls: string[] = [];
-              if (newFiles.length > 0) {
-                const uploadedFiles = await formatAttachmentList(newFiles);
-                uploadedUrls = uploadedFiles
-                  .map((f: any) => f.fileUrl || f.url || '')
-                  .filter((url: string) => url);
-              }
-
-              attachments = [...existingFiles, ...uploadedUrls].filter((url) => url).join(',');
+              // const existingFiles: string[] = [];
+              // const newFiles: any[] = [];
+
+              // item.attachmentFileList.forEach((file: any) => {
+              //   if (file.fileUrl && !file.file) {
+              //     existingFiles.push(file.fileUrl);
+              //   } else {
+              //     newFiles.push(file);
+              //   }
+              // });
+
+              // let uploadedUrls: string[] = [];
+              // if (newFiles.length > 0) {
+              //   const uploadedFiles = await formatAttachmentList(newFiles);
+              //   uploadedUrls = uploadedFiles
+              //     .map((f: any) => f.fileUrl || f.url || '')
+              //     .filter((url: string) => url);
+              // }
+              attachments = JSON.stringify(item.attachmentFileList || '[]');
+              // attachments = [...existingFiles, ...uploadedUrls].filter((url) => url).join(',');
             }
             }
 
 
             return {
             return {

+ 2 - 2
src/views/production-safety/safetyAssessment/evaluationDepartment/evaluationDepartment.vue

@@ -103,8 +103,8 @@
             </template>
             </template>
             <template #action="scope">
             <template #action="scope">
               <div class="action-container--div" style="justify-content: left">
               <div class="action-container--div" style="justify-content: left">
-                <!-- 待反馈(2):显示反馈 -->
-                <template v-if="Number(scope.row.status) === 2">
+                <!-- 待反馈(2):显示反馈 待反馈(3):待评分 -->
+                <template v-if="Number(scope.row.status) === 2 || Number(scope.row.status) === 3">
                   <ActionButton text="反馈" @click="handleFeedback(scope.row.id)" />
                   <ActionButton text="反馈" @click="handleFeedback(scope.row.id)" />
                 </template>
                 </template>
                 <!-- 已完成(1):显示反馈和先进个人申报 -->
                 <!-- 已完成(1):显示反馈和先进个人申报 -->

+ 4 - 1
src/views/production-safety/safetyAssessment/evaluationSystem/evaluationSystem.vue

@@ -365,7 +365,10 @@
   const getDeptTreeData = async () => {
   const getDeptTreeData = async () => {
     try {
     try {
       const res = await getAllDepartments();
       const res = await getAllDepartments();
-      deptTree.value = res?.[0]?.children ?? [];
+      deptTree.value = (res?.[0]?.children ?? []).map(dept => ({
+        ...dept,
+        children: []
+      }));
     } catch (e) {
     } catch (e) {
       console.error('获取部门树失败:', e);
       console.error('获取部门树失败:', e);
     }
     }

+ 2 - 2
src/views/production-safety/safetyAssessment/receiptRecord/components/ReceiptRecordDetail.vue

@@ -70,9 +70,9 @@
     <template #form>
     <template #form>
       <div class="form">
       <div class="form">
         <el-form ref="approvalFormRef" :model="approvalForm">
         <el-form ref="approvalFormRef" :model="approvalForm">
-          <el-form-item label="审批描述:" label-position="top">
+          <!-- <el-form-item label="审批描述:" label-position="top">
             <el-input v-model="approvalForm.description" placeholder="请输入审批描述" type="textarea" />
             <el-input v-model="approvalForm.description" placeholder="请输入审批描述" type="textarea" />
-          </el-form-item>
+          </el-form-item> -->
           <div class="form-item">
           <div class="form-item">
             <span>审批流程:</span>
             <span>审批流程:</span>
             <template v-for="item in approvalNodeList" :key="item.id">
             <template v-for="item in approvalNodeList" :key="item.id">

+ 4 - 2
src/views/production-safety/safetyAssessment/receiptRecord/receiptRecord.vue

@@ -81,8 +81,9 @@
                     @confirm="handleDelete(scope.row.id)"
                     @confirm="handleDelete(scope.row.id)"
                   />
                   />
                   <ActionButton
                   <ActionButton
-                    text="确认领取"
-                    :popconfirm="{ title: '确定要确认领取?' }"
+                    text="确认发放"
+                    v-if="scope.row.conformClaimButton"
+                    :popconfirm="{ title: '确定要发放吗?' }"
                     @confirm="handleConfirm(scope.row.id)"
                     @confirm="handleConfirm(scope.row.id)"
                   />
                   />
                   <ActionButton text="查看" @click="handleView(scope.row.id)" />
                   <ActionButton text="查看" @click="handleView(scope.row.id)" />
@@ -203,6 +204,7 @@
           department: item.deptName, // 部门
           department: item.deptName, // 部门
           recipient: item.userName, // 领取人
           recipient: item.userName, // 领取人
           status: item.status, // 0 待审核 1 审核通过 -1 审核不通过 2 已领取
           status: item.status, // 0 待审核 1 审核通过 -1 审核不通过 2 已领取
+          conformClaimButton: item.conformClaimButton, // 确认发放按钮
         }));
         }));
         pagination.total = res.totalRow;
         pagination.total = res.totalRow;
       }
       }

+ 2 - 1
src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagementDept/components/addTrainingInformation.vue

@@ -422,7 +422,7 @@ const openState = ref(false)
         </el-form-item>
         </el-form-item>
 
 
         <el-form-item label="培训课程简述:" prop="courseIntroduction">
         <el-form-item label="培训课程简述:" prop="courseIntroduction">
-          <div class="editor-container">
+          <div class="editor-container" v-if="!isViewMode">
             <Toolbar style="border-bottom: 1px solid #dcdfe6" :editor="editorRef" />
             <Toolbar style="border-bottom: 1px solid #dcdfe6" :editor="editorRef" />
             <Editor
             <Editor
               :disabled="isViewMode"
               :disabled="isViewMode"
@@ -434,6 +434,7 @@ const openState = ref(false)
               @on-change="handleEditorChange"
               @on-change="handleEditorChange"
             />
             />
           </div>
           </div>
+          <div v-else v-html="form.courseIntroduction"></div>
         </el-form-item>
         </el-form-item>
 
 
         <el-form-item label="课程内容:" prop="courseContent">
         <el-form-item label="课程内容:" prop="courseContent">