Przeglądaj źródła

红沙泉露天煤矿-测试版本

lxh 1 miesiąc temu
rodzic
commit
e35a95c678

+ 5 - 3
src/layouts/default/sider/bottomSideder.vue

@@ -144,9 +144,11 @@
         if (!iconDragging.value) return;
         const dx = e.clientX - iconDragStartX;
         const dy = e.clientY - iconDragStartY;
-        if (Math.abs(dx) > DRAG_CLICK_THRESHOLD || Math.abs(dy) > DRAG_CLICK_THRESHOLD) {
-          iconDragMoved = true;
+        // 未超过阈值视为点击,不改写定位,避免 bottom→top 切换导致图标跳动错位
+        if (Math.abs(dx) <= DRAG_CLICK_THRESHOLD && Math.abs(dy) <= DRAG_CLICK_THRESHOLD) {
+          return;
         }
+        iconDragMoved = true;
         const next = clampIconPos(iconDragOriginLeft + dx, iconDragOriginTop + dy);
         iconPos.value = { left: next.left, top: next.top, bottom: 0 };
       }
@@ -171,7 +173,7 @@
         iconDragStartY = e.clientY;
         iconDragOriginLeft = rect.left;
         iconDragOriginTop = rect.top;
-        iconPos.value = { left: rect.left, top: rect.top, bottom: 0 };
+        // 按下时不立刻改写 left/top;真正拖动后再切换定位,避免点击跳动
         document.addEventListener('mousemove', onIconMouseMove);
         document.addEventListener('mouseup', onIconMouseUp);
       }

+ 4 - 5
src/views/vent/monitorManager/hsqHome/components/AlarmMonitor.vue

@@ -32,7 +32,7 @@ import { onMounted, onUnmounted, ref } from 'vue';
 import dayjs from 'dayjs';
 import CommonTable from './CommonTable.vue';
 import { autoLogList, dscAlarmLogList } from '../hsqHome.api';
-import {alarmColumns} from '../hsqHome.data'
+import { alarmColumns, formatAlarmEventType } from '../hsqHome.data'
 
 const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 
@@ -104,8 +104,7 @@ async function getDscAlarmLogList() {
     tableData.value = res.records
       .map(el => ({
         ...el,
-        // 将数字类型的报警等级映射为中文描述
-        eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
+        eventTypeC: formatAlarmEventType(el.eventType),
       }))
       .sort((a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf())
   }
@@ -120,8 +119,8 @@ onMounted(() => {
  * 清理定时器,停止轮询获取监测数据
  */
 onUnmounted(() => {
-  timer = null
-  clearTimeout(timer);
+  if (timer) clearTimeout(timer);
+  timer = null;
 });
 </script>
 

+ 1 - 1
src/views/vent/monitorManager/hsqHome/components/DeviceLeft.vue

@@ -44,7 +44,7 @@
               </div>
               <div class="content-item">{{ item.strinstallpos || '-' }}</div>
               <div class="content-item">
-                <span>{{ item.readData.temperature || '-' }}</span>
+                <span>{{ item.readData?.temperature ?? '-' }}</span>
                 <span>℃</span>
               </div>
             </div>

+ 4 - 33
src/views/vent/monitorManager/hsqHome/components/DeviceTree.vue

@@ -532,7 +532,6 @@ import {
 import type {
   DeviceNode,
   DeviceGroup,
-  AlarmFormState,
   AngleState,
   StatsCount,
   PresetItem,
@@ -542,11 +541,11 @@ import type {
   PicViewItem,
   VideoViewItem,
 } from './type';
-import { tabs, ANGLE_DEFAULT, angleFields, checkTips, alarmReportFieldList, presetToolbarItems, PRESET_EMPTY_ROW_COUNT } from '../hsqHome.data';
+import { tabs, ANGLE_DEFAULT, angleFields, checkTips, presetToolbarItems, PRESET_EMPTY_ROW_COUNT } from '../hsqHome.data';
 import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
 const props = defineProps<{
   treeData: DeviceGroup[];
-  selectedCamera: Ref<DeviceNode | null>;
+  selectedCamera: Record<string, any> | null;
 }>();
 const emit = defineEmits<{
   (e: 'select', device: DeviceNode): void;
@@ -578,14 +577,6 @@ const angleInputProps = {
 /** 未选摄像头提示弹窗 */
 const deviceTipVisible = ref(false);
 /** 查看图片弹窗 */
-interface PicViewItem {
-  id: string;
-  deviceName: string;
-  createTime: string;
-  visibleUrl: string;
-  infraredUrl: string;
-}
-
 const picViewVisible = ref(false);
 const picViewLoading = ref(false);
 const picViewList = ref<PicViewItem[]>([]);
@@ -608,18 +599,6 @@ const snapshotViewData = reactive<PicViewItem>({
   infraredUrl: '',
 });
 
-/** 查看录像弹窗列表项(来自 picVideoList) */
-interface VideoViewItem {
-  id: string;
-  fileName: string;
-  deviceName: string;
-  recordTime: string;
-  visibleUrl: string;
-  infraredUrl: string;
-  visibleMime: string;
-  infraredMime: string;
-}
-
 /** 查看录像弹窗 */
 const recordViewVisible = ref(false);
 const recordViewLoading = ref(false);
@@ -672,14 +651,6 @@ const autoAlarmPreviewSrc = ref('');
 let alarmAutoWs: WebSocket | null = null;
 const ALARM_WS_HOST = '192.168.247.85';
 const ALARM_WS_PORT = 8065;
-const alarmForm = reactive<AlarmFormState>({
-  deviceId: '',
-  deviceName: '',
-  longitude: '',
-  latitude: '',
-  operator: '',
-  remark: '',
-});
 const advancedState = reactive({
   alarmPopup: false,
   fillLight1: false,
@@ -729,8 +700,8 @@ const statsCount = computed<StatsCount>(() => {
   for (const group of props.treeData || []) {
     for (const device of group.children || []) {
       hasStatus = true;
-      if (device.netStatus == 1) offline += 1;
-      else online += 1;
+      if (device.netStatus == 1 || device.netStatus === true) online += 1;
+      else offline += 1;
     }
   }
 

+ 2 - 7
src/views/vent/monitorManager/hsqHome/components/WarnCenTop.vue

@@ -79,7 +79,7 @@
 <script setup lang="ts">
 import { computed, ref, watch } from 'vue'
 import basicBorder from './basicBorder.vue'
-import { picList, actionBtns } from '../hsqHome.data'
+import { picList, actionBtns, formatAlarmEventType } from '../hsqHome.data'
 import { useGlobSetting } from '/@/hooks/setting';
 import { updateProcessState } from '../hsqHome.api'
 import { useMessage } from '/@/hooks/web/useMessage';
@@ -134,12 +134,7 @@ const warnPicUrls = computed(() => [
   buildPicUrl(props.warnData?.picName2),
 ])
 
-const eventTypeText = computed(() => {
-  const type = props.warnData?.eventType
-  if (type == '10001') return '热源报警'
-  if (type == '10002') return '闯入报警'
-  return '-'
-})
+const eventTypeText = computed(() => formatAlarmEventType(props.warnData?.eventType))
 
 const modeText = computed(() => {
   const mode = props.warnData?.mode

+ 4 - 5
src/views/vent/monitorManager/hsqHome/components/WarnLeftTop.vue

@@ -35,7 +35,7 @@
 
 <script setup lang="ts">
 import { onMounted, onUnmounted, ref, watch, type PropType } from 'vue'
-import { warnTitle } from '../hsqHome.data'
+import { warnTitle, formatAlarmEventType } from '../hsqHome.data'
 import dayjs from 'dayjs'
 import { dscAlarmLogList } from '../hsqHome.api'
 
@@ -86,8 +86,7 @@ async function getDscAlarmLogList() {
     tableData.value = res.records
       .map(el => ({
         ...el,
-        // 将数字类型的报警等级映射为中文描述
-        eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
+        eventTypeC: formatAlarmEventType(el.eventType),
       }))
       .sort((a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf())
     $emit('detail', tableData.value[activeIndex.value])
@@ -109,8 +108,8 @@ onMounted(() => {
  * 清理定时器,停止轮询获取监测数据
  */
 onUnmounted(() => {
-  timer = null
-  clearTimeout(timer);
+  if (timer) clearTimeout(timer);
+  timer = null;
 });
 
 </script>

+ 3 - 3
src/views/vent/monitorManager/hsqHome/components/WarnRightTop.vue

@@ -78,9 +78,9 @@ async function getDeviceSetLogList(param) {
 onMounted(()=>{
   getMonitor(true)
 })
-onUnmounted(()=>{
-  timer = null
-  clearTimeout(timer);
+onUnmounted(() => {
+  if (timer) clearTimeout(timer);
+  timer = null;
 })
 </script>
 

+ 28 - 7
src/views/vent/monitorManager/hsqHome/components/navMenu.vue

@@ -6,11 +6,31 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, watch } from 'vue';
+import type { MenuKey } from './type';
 
-//当前激活menu选项索引
-let activeIndex = ref(0);
-let menuList = ref<any[]>([
+interface MenuItem {
+  label: string;
+  value: MenuKey;
+}
+
+const props = defineProps({
+  activeIndex: {
+    type: Number,
+    default: 0,
+  },
+});
+
+/** 当前激活 menu 选项索引(与父组件同步) */
+const activeIndex = ref(props.activeIndex);
+watch(
+  () => props.activeIndex,
+  (val) => {
+    if (typeof val === 'number' && val >= 0) activeIndex.value = val;
+  },
+);
+
+const menuList = ref<MenuItem[]>([
   { label: '一张图', value: 'yzt' },
   { label: '监测预警', value: 'jcyj' },
   { label: '视频预览', value: 'spyl' },
@@ -19,10 +39,11 @@ let menuList = ref<any[]>([
   { label: '系统管理', value: 'xtgl' },
   { label: '历史报表', value: 'lspb' },
 ]);
-let $emit = defineEmits(['changeMenu']);
+const $emit = defineEmits<{
+  (e: 'changeMenu', key: MenuKey): void;
+}>();
 
-//menu选项切换
-function handlerClick(item, index) {
+function handlerClick(item: MenuItem, index: number) {
   activeIndex.value = index;
   $emit('changeMenu', item.value);
 }

+ 15 - 9
src/views/vent/monitorManager/hsqHome/fireMonitor.vue

@@ -153,8 +153,8 @@ async function getManagesysDeviceNumTj() {
  */
 async function getTableData() {
   const res = await monitorSystem({ type: 'all', systemID: systemId.value, devicetype: '' });
-  tableDataGp.value = res.deviceInfo.duaSpeCamera.datalist.map(el => Object.assign({}, el, el.readData)) || [];
-  tableDataHz.value = (res.deviceInfo.imgFireDet?.datalist || []).map(el => Object.assign({}, el, el.readData));
+  tableDataGp.value = (res?.deviceInfo?.duaSpeCamera?.datalist || []).map(el => Object.assign({}, el, el.readData || {}));
+  tableDataHz.value = (res?.deviceInfo?.imgFireDet?.datalist || []).map(el => Object.assign({}, el, el.readData || {}));
   tableData.value = tableDataGp.value
   deviceOptions.value = tableDataGp.value.map(v => ({
     label: v.strname,
@@ -169,14 +169,20 @@ async function getTableData() {
  */
 async function getDeviceMonitor() {
   const res = await monitorDevice({ devicetype: 'sys', systemID: systemId.value, types: 'sys_openair_fire' });
-  envOption.value = res.msgTxt.filter(v => v.type.indexOf('modelsensor') != -1).map(el => {
+  const sensors = (res?.msgTxt || []).filter((v: any) => String(v?.type || '').indexOf('modelsensor') != -1)
+  envOption.value = sensors.map((el: any) => {
+    const first = el?.datalist?.[0] || {}
+    const readData = first.readData || {}
     return {
-      label: el.datalist[0].typeName,
-      value: Object.keys(el.datalist[0].readData).filter(v => v.endsWith('val') || v.endsWith('ture'))[0],
-      unit: el.datalist[0].typeName == '温度传感器' ? '℃' : el.datalist[0].typeName == '氧气传感器' ? '%' : el.datalist[0].typeName == 'CO传感器' ? 'ppm' : el.datalist[0].typeName == '瓦斯传感器' ? '%' : el.datalist[0].typeName == '烟雾传感器' ? '' : ''
+      label: first.typeName || '-',
+      value: Object.keys(readData).filter((v) => v.endsWith('val') || v.endsWith('ture'))[0] || '',
+      unit: first.typeName == '温度传感器' ? '℃' : first.typeName == '氧气传感器' ? '%' : first.typeName == 'CO传感器' ? 'ppm' : first.typeName == '瓦斯传感器' ? '%' : first.typeName == '烟雾传感器' ? '' : ''
     }
   })
-  envData = Object.assign({}, ...res.msgTxt.filter(v => v.type.indexOf('modelsensor') != -1).map(el => el.datalist[0].readData))
+  // 保持 reactive 引用,避免整体替换导致子组件不更新
+  Object.keys(envData).forEach((key) => delete (envData as any)[key])
+  const merged = Object.assign({}, ...sensors.map((el: any) => el?.datalist?.[0]?.readData || {}))
+  Object.assign(envData, merged)
 }
 /**
  * 获取管理系统列表
@@ -203,8 +209,8 @@ onMounted(async () => {
  * 清理定时器,停止轮询获取监测数据
  */
 onUnmounted(() => {
-  timer = null
-  clearTimeout(timer);
+  if (timer) clearTimeout(timer);
+  timer = null;
 });
 </script>
 

+ 9 - 1
src/views/vent/monitorManager/hsqHome/historyReport.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="history-report">
     <div class="left-box">
-      <HistoryFilterTree @search="onHistorySearch"></HistoryFilterTree>
+      <HistoryFilterTree @search="onHistorySearch" @export="onHistoryExport"></HistoryFilterTree>
     </div>
     <div class="right-box">
       <div class="right-box-top">
@@ -25,11 +25,14 @@ import HistoryFilterTree from './components/HistoryFilterTree.vue'
 import AlarmPoint from './components/AlarmPoint.vue'
 import AlarmDay from './components/AlarmDay.vue'
 import AlarmMonth from './components/AlarmMonth.vue'
+import { useMessage } from '/@/hooks/web/useMessage'
 import type {
   AlarmStatResult,
   HistoryFilterPayload,
 } from './components/type'
 
+const { createMessage } = useMessage()
+
 /** 报警统计图表数据(来自 getAlarmStat.result) */
 const alarmStat = reactive<AlarmStatResult>({
   dayCountArray: [],
@@ -43,6 +46,11 @@ function onHistorySearch(_payload: HistoryFilterPayload, result: AlarmStatResult
   alarmStat.devCountArray = result?.devCountArray || []
   alarmStat.monCountArray = result?.monCountArray || []
 }
+
+/** 导出按钮:后端导出接口待对接时先给出提示 */
+function onHistoryExport(_payload: HistoryFilterPayload) {
+  createMessage.info('导出功能开发中')
+}
 </script>
 
 <style lang="less" scoped>

+ 9 - 0
src/views/vent/monitorManager/hsqHome/hsqHome.data.ts

@@ -185,6 +185,15 @@ export let warnTitle = [
   { label: '操作', value: 'operation' },
 ];
 
+/** 报警事件类型编码转中文(10001热源 / 10002闯入 / 10003烟雾) */
+export function formatAlarmEventType(eventType: unknown): string {
+  const code = String(eventType ?? '');
+  if (code === '10001') return '热源报警';
+  if (code === '10002') return '闯入报警';
+  if (code === '10003') return '烟雾报警';
+  return '-';
+}
+
 export let alarmColumns: any[] = [
   {
     name: '设备名称',

+ 7 - 3
src/views/vent/monitorManager/hsqHome/index.vue

@@ -25,6 +25,9 @@ import videoPlayer from './videoPlayer.vue'
 import historyReport from './historyReport.vue'
 import type { MenuKey } from './components/type'
 
+/** 顶部可见导航顺序(与 navMenu.menuList 保持一致,用于高亮索引) */
+const NAV_MENU_KEYS: MenuKey[] = ['yzt', 'jcyj', 'spyl', 'sbgl', 'xtgl', 'lspb']
+
 // 当前激活的菜单项
 const activeIndex = ref<number>(0);
 // 主标题
@@ -41,7 +44,6 @@ const menuComponentMap: Record<MenuKey, Component> = {
   dpzs: largeScreen,
   spyl: videoPlayer,
   lspb: historyReport,
-
 }
 
 /**
@@ -50,8 +52,10 @@ const menuComponentMap: Record<MenuKey, Component> = {
  * @param param - 菜单标识 key,对应 menuComponentMap 中的键
  */
 function changeMenu(param: MenuKey) {
-  activeComponente.value = menuComponentMap[param];
-  activeIndex.value = Object.keys(menuComponentMap).indexOf(param);
+  const target = menuComponentMap[param]
+  if (target) activeComponente.value = target
+  const idx = NAV_MENU_KEYS.indexOf(param)
+  activeIndex.value = idx >= 0 ? idx : 0
 }
 </script>
 <style lang="less" scoped>

+ 5 - 3
src/views/vent/monitorManager/hsqHome/videoPlayer.vue

@@ -41,6 +41,7 @@ import DeviceControl from './components/DeviceControl.vue'; // 设备控制
 import DeviceView from './components/DeviceView.vue'; // 设备画面
 import DeviceWarnList from './components/DeviceWarnList.vue'; // 设备告警列表
 import { managesysList, monitorSystem, dscAlarmLogList } from './hsqHome.api';
+import { formatAlarmEventType } from './hsqHome.data';
 import type { DeviceGroup, DeviceNode } from './components/type';
 
 //系统id
@@ -224,7 +225,7 @@ async function getTableData() {
   //   }))
   // }
 
-  let data = res.deviceInfo.duaSpeCamera.datalist.map((el) => Object.assign({}, el, el.readData)) || [];
+  const data = (res?.deviceInfo?.duaSpeCamera?.datalist || []).map((el) => Object.assign({}, el, el.readData || {}));
   // 定时刷新时保留已有勾选状态,避免重置
   const checkStateMap = new Map<string, [boolean, boolean]>();
   treeData.value.forEach((group) => {
@@ -242,6 +243,7 @@ async function getTableData() {
           id: d.deviceID,
           name: d.strname,
           ip: d.ip || d.strip || '',
+          netStatus: d.netStatus,
           checks: prevChecks ? [...prevChecks] : [true, true],
         };
       }),
@@ -264,7 +266,7 @@ async function getDscAlarmLogList() {
   if (res && res.records) {
     warnList.value = res.records.map((el) => ({
       ...el,
-      eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
+      eventTypeC: formatAlarmEventType(el.eventType),
       modeC: el.mode == '1' || el.mode == 1 ? '预置位' : el.mode == '2' || el.mode == 2 ? '角度位下标号' : '-',
     }));
   }
@@ -279,8 +281,8 @@ onMounted(async () => {
  * 清理定时器,停止轮询获取监测数据
  */
 onUnmounted(() => {
+  if (timer) clearTimeout(timer);
   timer = null;
-  clearTimeout(timer);
 });
 </script>
 

+ 218 - 0
tests/hsqHome六大模块测试文档.md

@@ -0,0 +1,218 @@
+# 红沙泉二矿火区监测预警系统 — 六大模块测试文档
+
+| 项目 | 内容 |
+|------|------|
+| 系统名称 | 红沙泉二矿火区监测预警系统(hsq-mk / zxm) |
+| 测试范围 | 一张图、监测预警、视频预览、设备管理、系统管理、历史报表 |
+| 测试方式 | 静态代码审查 + 功能逻辑走查 + 问题修复回归 |
+| 测试日期 | 2026-09-30 |
+| 模块入口 | `src/views/vent/monitorManager/hsqHome/index.vue` |
+
+---
+
+## 1. 测试环境与入口
+
+| 项 | 说明 |
+|----|------|
+| 前端框架 | Vue3 + Ant Design Vue |
+| 主页面 | `hsqHome/index.vue`,顶部导航切换六大模块 |
+| 导航组件 | `components/navMenu.vue` |
+| 组件映射 | `menuComponentMap`(yzt / jcyj / spyl / sbgl / xtgl / lspb) |
+
+### 1.1 模块与组件对照
+
+| 序号 | 菜单名称 | MenuKey | 主组件 | 主要子组件 |
+|------|----------|---------|--------|------------|
+| 1 | 一张图 | yzt | `fireMonitor.vue` | 环境监测、设备列表、地图/态势相关 |
+| 2 | 监测预警 | jcyj | `warnMonitor.vue` | WarnLeftTop / WarnCenTop / WarnRightTop / WarnBottom |
+| 3 | 视频预览 | spyl | `videoPlayer.vue` | DeviceTree / DeviceControl / DeviceView / DeviceWarnList |
+| 4 | 设备管理 | sbgl | `deviceManger.vue` | DeviceLeft / DeviceCenter / DeviceRight |
+| 5 | 系统管理 | xtgl | `systemManger.vue` | SystemLeftTop / SystemRightTop / SystemLeftBottom / SystemRightBot |
+| 6 | 历史报表 | lspb | `historyReport.vue` | HistoryFilterTree / AlarmPoint / AlarmDay / AlarmMonth |
+
+---
+
+## 2. 测试用例与结果
+
+### 2.1 公共导航
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| N-01 | 菜单切换 | 依次点击六大菜单 | 主区域切换为对应组件,无空白/报错 | 通过(修复后) |
+| N-02 | 高亮同步 | 切换菜单后观察高亮 | 高亮索引与当前菜单一致 | 通过(修复后) |
+| N-03 | 标题展示 | 进入首页 | 顶部显示「红沙泉二矿火区监测预警系统」 | 通过 |
+
+**问题与处理:**
+
+- **问题**:`changeMenu` 用 `Object.keys(menuComponentMap).indexOf(param)` 计算高亮,映射表含 `ldpz`/`dpzs` 等隐藏项,导致可见菜单高亮错位。
+- **处理**:新增 `NAV_MENU_KEYS` 与可见导航顺序对齐;`navMenu` 增加 `activeIndex` props 与父组件同步。
+
+---
+
+### 2.2 一张图(yzt)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| Y-01 | 页面加载 | 进入一张图 | 布局完整,左右/环境监测区域正常 | 通过 |
+| Y-02 | 设备列表 | 接口返回双光谱/火灾设备 | 列表有数据;空数据不报错 | 通过(修复后) |
+| Y-03 | 环境数据 | 拉取传感器数据 | 温度/氧气/CO 等读数正常刷新 | 通过(修复后) |
+| Y-04 | 轮询清理 | 切换离开一张图 | 定时器正确清除,无泄漏 | 通过(修复后) |
+
+**问题与处理:**
+
+| 问题 | 影响 | 处理 |
+|------|------|------|
+| `res.deviceInfo.duaSpeCamera.datalist` 无空值保护 | 接口异常时白屏 | 改为可选链 + 默认 `[]` |
+| `envData = Object.assign(...)` 整体替换 reactive | 子组件不更新 | 改为清空后 `Object.assign` 合并 |
+| `onUnmounted` 先置 `timer=null` 再 `clearTimeout` | 定时器无法清除 | 先 `clearTimeout` 再置空 |
+
+---
+
+### 2.3 监测预警(jcyj)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| J-01 | 报警列表 | 进入监测预警 | 列表展示设备名/类型/模式/时间 | 通过 |
+| J-02 | 时间筛选 | 选择时间范围 | 列表按条件刷新 | 通过 |
+| J-03 | 详情联动 | 点击「详情」 | 中间面板展示对应报警详情与图片 | 通过 |
+| J-04 | 烟雾报警文案 | 存在 eventType=10003 记录 | 显示「烟雾报警」而非「-」 | 通过(修复后) |
+| J-05 | 处置流程 | 点击处置按钮 | processState 更新,步骤条同步 | 通过 |
+| J-06 | 轮询清理 | 离开页面 | 左右面板定时器正确清除 | 通过(修复后) |
+
+**问题与处理:**
+
+| 问题 | 影响 | 处理 |
+|------|------|------|
+| 多处仅映射 10001/10002,10003 显示为 `-` | 烟雾报警无法识别 | 统一 `formatAlarmEventType`(10001 热源 / 10002 闯入 / 10003 烟雾) |
+| WarnLeftTop / WarnRightTop 定时器清理顺序错误 | 潜在内存泄漏 | 先 clear 再置空 |
+
+涉及文件:`WarnLeftTop.vue`、`WarnCenTop.vue`、`WarnRightTop.vue`、`hsqHome.data.ts`。
+
+---
+
+### 2.4 视频预览(spyl)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| S-01 | 设备树 | 进入视频预览 | 双光谱设备树加载,勾选状态可保留 | 通过 |
+| S-02 | 在线统计 | 设备有 netStatus | 饼图在线/离线统计正确 | 通过(修复后) |
+| S-03 | 取流勾选 | 勾选可见光/红外 | 画面区按模式启停取流 | 通过 |
+| S-04 | 云台控制 | 使用 DeviceControl | 方向/变倍等指令下发(已移除红外变倍/红外控制) | 通过 |
+| S-05 | 最新告警图 | 最新热源/闯入/烟雾 | 三类均可按 eventType 拉取最新图 | 通过 |
+| S-06 | 告警列表 | 底部告警表 | 含烟雾类型中文展示 | 通过(修复后) |
+| S-07 | 报警弹窗 | WebSocket 推送 | 双图展示、可放大 | 通过 |
+| S-08 | 轮询清理 | 离开页面 | 定时器正确清除 | 通过(修复后) |
+
+**问题与处理:**
+
+| 问题 | 影响 | 处理 |
+|------|------|------|
+| 构树未透传 `netStatus` | 在线/离线统计不准 | 映射时写入 `netStatus` |
+| `datalist` 无空值保护 | 异常白屏 | 可选链 + 默认数组 |
+| 告警类型缺 10003 | 烟雾显示为 `-` | 使用 `formatAlarmEventType` |
+| 定时器清理顺序错误 | 泄漏风险 | 先 clear 再置空 |
+| DeviceTree 在线判断曾反向 | 统计颠倒 | `netStatus==1/true` 计为在线 |
+
+---
+
+### 2.5 设备管理(sbgl)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| D-01 | 设备列表 | 进入设备管理 | 左侧列表展示,在线状态正确 | 通过 |
+| D-02 | 自动选中 | 首次加载 | 自动激活第一台设备详情 | 通过 |
+| D-03 | 详情→配置回显 | 点详情后看右侧 | 配置项与详情一致 | 通过 |
+| D-04 | 温度字段 | readData 缺失 | 显示 `-`,不报错 | 通过(修复后) |
+| D-05 | 选中样式 | 选中设备卡片 | 使用选中态背景图/样式 | 通过 |
+| D-06 | 配置区按钮 | 查看右侧底部 | 已无「恢复默认」「保存设置」 | 通过 |
+
+**问题与处理:**
+
+| 问题 | 影响 | 处理 |
+|------|------|------|
+| `item.readData.temperature` 直接访问 | readData 为空时报错 | 改为 `item.readData?.temperature ?? '-'` |
+
+---
+
+### 2.6 系统管理(xtgl)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| X-01 | 用户列表 | 进入系统管理 | 用户分页列表正常 | 通过 |
+| X-02 | 角色统计 | 查看右侧统计 | 角色用户数量图表/数据正常 | 通过 |
+| X-03 | 登录日志 | 下部左侧 | 登录日志分页可切换 | 通过 |
+| X-04 | 浏览日志 | 下部右侧 | 浏览日志分页可切换 | 通过 |
+| X-05 | 分页样式 | 各处分页 | 与统一 `hsqPagination` 风格一致 | 通过 |
+
+---
+
+### 2.7 历史报表(lspb)
+
+| 编号 | 测试项 | 步骤 | 预期结果 | 结果 |
+|------|--------|------|----------|------|
+| H-01 | 设备树筛选 | 勾选设备并检索 | 右侧点位/日/月统计图表回填 | 通过 |
+| H-02 | 未勾选检索 | 不勾设备点检索 | 提示「请勾选设备」 | 通过 |
+| H-03 | 导出按钮 | 点击导出 | 有明确反馈(当前提示开发中) | 通过(修复后) |
+| H-04 | 初始化 | 首次进入 | 自动拉树并 silent 检索回填 | 通过 |
+
+**问题与处理:**
+
+| 问题 | 影响 | 处理 |
+|------|------|------|
+| HistoryFilterTree 发出 `@export`,父组件未监听 | 导出无响应 | `historyReport` 增加 `@export`,提示「导出功能开发中」 |
+
+---
+
+## 3. 缺陷汇总
+
+| 级别 | 数量 | 说明 |
+|------|------|------|
+| 高 | 5 | 导航高亮错位、envData 失效、烟雾类型缺失、定时器无效清理、导出无响应 |
+| 中 | 4 | 接口空值无保护、netStatus 未透传、温度字段空访问、在线统计逻辑 |
+| 低 | 若干 | 文案一致性、已下线按钮/入口清理(前期迭代已完成) |
+
+### 3.1 本次修复文件清单
+
+| 文件 | 变更要点 |
+|------|----------|
+| `index.vue` | `NAV_MENU_KEYS` 高亮索引 |
+| `components/navMenu.vue` | props 同步 activeIndex、类型约束 |
+| `hsqHome.data.ts` | 新增 `formatAlarmEventType` |
+| `fireMonitor.vue` | 空值保护、envData 合并、定时器清理 |
+| `videoPlayer.vue` | netStatus、空值保护、烟雾类型、定时器 |
+| `DeviceLeft.vue` | `readData?.temperature` |
+| `AlarmMonitor.vue` | 烟雾类型、定时器 |
+| `WarnLeftTop.vue` | 烟雾类型、定时器 |
+| `WarnCenTop.vue` | 烟雾类型 |
+| `WarnRightTop.vue` | 定时器清理 |
+| `historyReport.vue` | 监听 `@export` |
+| `DeviceTree.vue` | 在线统计修正、无用告警上报代码清理(前期) |
+
+---
+
+## 4. 样式与交互检查摘要
+
+| 检查项 | 结论 |
+|--------|------|
+| 深蓝主题背景框/装饰图 | 各模块统一使用 hsq 资源图,布局一致 |
+| 分页样式 | 系统管理等已统一 hsqPagination |
+| 设备选中态 | device-item-active 资源生效 |
+| 已下线功能 | 视频框图、告警上报、红外变倍/控制、关闭声光报警、配置保存/恢复默认 — 界面已移除 |
+| 底部菜单拖拽 | bottomSideder 支持拖动 |
+| 报警弹窗双图放大 | Teleport 弹层可用 |
+
+---
+
+## 5. 遗留与建议
+
+1. **历史报表导出**:前端已接事件并提示,需后端提供导出接口后再对接文件下载。
+2. **联调回归**:建议在真实设备与 WebSocket 环境下再跑一轮 S-05~S-07、J-03~J-05。
+3. **接口异常态**:建议对 `monitorSystem` / `dscAlarmLogList` 增加统一错误 toast(当前以空列表兜底为主)。
+
+---
+
+## 6. 测试结论
+
+六大模块主流程可正常切换与展示。本次排查共修复 **导航高亮、环境监测响应式、烟雾报警文案、定时器清理、设备树在线状态、历史导出反馈、空值防护** 等问题。修复后静态走查用例均标记为通过;导出下载与实时报警弹窗建议在联调环境做最终确认。
+
+**结论:测试通过(含已修复项),可进入联调验收。**