lxh 3 долоо хоног өмнө
parent
commit
436b23d385
35 өөрчлөгдсөн 1771 нэмэгдсэн , 654 устгасан
  1. 1 1
      .env
  2. 2 7
      build/vite/plugin/index.ts
  3. 16 1
      src/api/sys/user.ts
  4. 3 1
      src/components/registerGlobComp.ts
  5. 19 6
      src/components/vent/camera/HlsPlayer.vue
  6. 21 5
      src/components/vent/camera/LivePlayerWrapper.vue
  7. 19 5
      src/components/vent/camera/createPlayer.vue
  8. 6 2
      src/hooks/component/createPlayer.ts
  9. 31 5
      src/hooks/system/useCameraPianation.ts
  10. 19 19
      src/store/modules/user.ts
  11. 188 0
      src/views/sys/login/Login-hsq-dd.vue
  12. 196 0
      src/views/sys/login/Login-hsq-pt.vue
  13. 70 181
      src/views/sys/login/Login.vue
  14. 1 1
      src/views/sys/login/login1.vue
  15. 36 41
      src/views/vent/monitorManager/hsqHome/components/AlarmMonitor.vue
  16. 1 1
      src/views/vent/monitorManager/hsqHome/components/CommonTable.vue
  17. 4 4
      src/views/vent/monitorManager/hsqHome/components/DeviceCenter.vue
  18. 1 1
      src/views/vent/monitorManager/hsqHome/components/DeviceLeft.vue
  19. 57 11
      src/views/vent/monitorManager/hsqHome/components/DeviceTree.vue
  20. 178 37
      src/views/vent/monitorManager/hsqHome/components/DeviceView.vue
  21. 6 4
      src/views/vent/monitorManager/hsqHome/components/HistoryLine.vue
  22. 92 4
      src/views/vent/monitorManager/hsqHome/components/SystemLeftBottom.vue
  23. 87 2
      src/views/vent/monitorManager/hsqHome/components/SystemLeftTop.vue
  24. 154 65
      src/views/vent/monitorManager/hsqHome/components/WarnBottom.vue
  25. 195 40
      src/views/vent/monitorManager/hsqHome/components/WarnCenTop.vue
  26. 64 33
      src/views/vent/monitorManager/hsqHome/components/WarnLeftTop.vue
  27. 40 47
      src/views/vent/monitorManager/hsqHome/components/WarnRightTop.vue
  28. 1 1
      src/views/vent/monitorManager/hsqHome/components/navMenu.vue
  29. 5 22
      src/views/vent/monitorManager/hsqHome/fireMonitor.vue
  30. 39 19
      src/views/vent/monitorManager/hsqHome/hsqHome.data.ts
  31. 1 1
      src/views/vent/monitorManager/hsqHome/index.vue
  32. 71 10
      src/views/vent/monitorManager/hsqHome/systemManger.vue
  33. 130 38
      src/views/vent/monitorManager/hsqHome/videoPlayer.vue
  34. 15 38
      src/views/vent/monitorManager/hsqHome/warnMonitor.vue
  35. 2 1
      vite.config.ts

+ 1 - 1
.env

@@ -2,7 +2,7 @@
 VITE_PORT = 3100
 
 #  网站标题
-VITE_GLOB_APP_TITLE = 红沙泉露天煤矿监控系统
+VITE_GLOB_APP_TITLE = 红沙泉二矿火区监测预警系统
 
 VITE_GLOB_AP_LOGO = 
 

+ 2 - 7
build/vite/plugin/index.ts

@@ -20,7 +20,8 @@ import { configServerPlugin } from './serveStatic';
 // import OptimizationPersist from 'vite-plugin-optimize-persist';
 import { resolve } from 'path';
 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
-import topLevelAwait from 'vite-plugin-top-level-await';
+// 注意:vite-plugin-top-level-await 与当前 @swc/core@1.16.x 不兼容(missing field type)
+// 已改为 build.target=es2022 原生支持 top-level await,故不再启用该插件
 
 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
   const { VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
@@ -45,12 +46,6 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
       svgoOptions: false,
       customDomId: '__webtopo__svgpreview__icons__dom__',
     }),
-    topLevelAwait({
-      // The export name of top-level await promise for each chunk module
-      promiseExportName: '__tla',
-      // The function to generate import names of top-level await promise in each chunk module
-      promiseImportName: (i) => `__tla_${i}`,
-    }),
   ];
 
   vitePlugins.push(UnoCSS({ presets: [presetUno(), presetTypography()] }));

+ 16 - 1
src/api/sys/user.ts

@@ -12,7 +12,7 @@ import { PageEnum } from '/@/enums/pageEnum';
 const { createErrorModal } = useMessage();
 enum Api {
   Login = '/sys/login',
-  //Login='/sys/sso/hsqSsoLogin',
+  LoginDd='/sys/sso/hsqSsoLogin',
   phoneLogin = '/sys/phoneLogin',
   Logout = '/sys/logout',
   autoLogin = '/sys/autoLogin',
@@ -64,6 +64,21 @@ export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal',
   );
 }
 
+/**
+ * 单点登录
+ */
+export function loginDdApi(params:any, mode: ErrorMessageMode = 'modal', successMode: SuccessMessageMode = 'success') {
+  return defHttp.get<LoginResultModel>(
+    {
+      url: Api.LoginDd,
+      params,
+    },
+    {
+      successMessageMode: successMode,
+      errorMessageMode: mode,
+    }
+  );
+}
 /**
  * @description: user auto login api
  */

+ 3 - 1
src/components/registerGlobComp.ts

@@ -57,6 +57,7 @@ import {
   Skeleton,
   Cascader,
   Rate,
+  Pagination,
 } from 'ant-design-vue';
 const compList = [AntButton.Group, Icon, AIcon, JUploadButton];
 
@@ -121,6 +122,7 @@ export function registerGlobComp(app: App) {
     .use(DragEngine)
     .use(Skeleton)
     .use(Cascader)
-    .use(Rate);
+    .use(Rate)
+    .use(Pagination);
   console.log("注册antd组件完成!");
 }

+ 19 - 6
src/components/vent/camera/HlsPlayer.vue

@@ -9,7 +9,14 @@
   import { useDrag } from '/@/hooks/event/useDrag';
   import videojs from 'video.js';
 
-  const props = defineProps<{ src: string }>();
+  const props = withDefaults(
+    defineProps<{
+      src: string
+      /** 是否自动播放,默认 true,保持原有行为 */
+      autoplay?: boolean
+    }>(),
+    { autoplay: true }
+  );
   const videoEl = ref<HTMLVideoElement | null>(null);
   let hls: Hls | null = null;
   let flv: any = null;
@@ -24,22 +31,27 @@
       hls = new Hls();
       hls.loadSource(props.src);
       hls.attachMedia(video);
+      if (props.autoplay) {
+        hls.on(Hls.Events.MANIFEST_PARSED, () => {
+          video.play?.().catch(() => {});
+        });
+      }
     } else if (props.src.startsWith('rtsp://')) {
       video?.setAttribute('class', 'liveVideo');
       video?.setAttribute('muted', 'muted');
-      video.autoplay = true;
+      video.autoplay = props.autoplay;
       rtspServer = new window['WebRtcStreamer'](
         videoEl.value,
         VUE_APP_URL.webRtcUrl.startsWith('/') ? location.protocol + VUE_APP_URL.webRtcUrl : VUE_APP_URL.webRtcUrl
       );
       await rtspServer.connect(props.src, null, 'rtptransport=tcp&timeout=60');
-      if (rtspServer && video.play) video.play();
+      if (props.autoplay && rtspServer && video.play) video.play();
     } else if (props.src.includes('.flv') && video.canPlayType('application/x-mpegURL')) {
       flv = videojs(
         video,
         {
           controls: true,
-          autoplay: true,
+          autoplay: props.autoplay,
           preload: 'auto',
           sources: [
             {
@@ -50,12 +62,13 @@
         },
         () => {
           videojs.log('播放器准备好了!');
-          flv.play();
+          if (props.autoplay) flv.play();
         }
       );
-      flv.play();
+      if (props.autoplay) flv.play();
     } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
       video.src = props.src;
+      if (props.autoplay) video.play?.().catch(() => {});
     }
   });
 

+ 21 - 5
src/components/vent/camera/LivePlayerWrapper.vue

@@ -1,17 +1,32 @@
 <template>
   <div ref="videoEl" style="width: 100%; height: 100%">
-    <live-player ref="player" :video-url="src" live stretch autoplay muted controls style="width: 100%; height: 100%" @error="handleError" />
+    <live-player
+      ref="player"
+      :video-url="src"
+      live
+      stretch
+      :autoplay="autoplay"
+      muted
+      controls
+      style="width: 100%; height: 100%"
+      @error="handleError"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
   import LivePlayer from '@liveqing/liveplayer-v3';
   import { useDrag } from '/@/hooks/event/useDrag';
-  import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
+  import { onBeforeUnmount, onMounted, ref } from 'vue';
   // 注册组件
-  defineProps<{
-    src: string;
-  }>();
+  const props = withDefaults(
+    defineProps<{
+      src: string
+      /** 是否自动播放,默认 true,保持原有行为 */
+      autoplay?: boolean
+    }>(),
+    { autoplay: true }
+  );
 
   const videoEl = ref<HTMLElement | null>(null);
 
@@ -22,6 +37,7 @@
   let timer: NodeJS.Timeout | null = null;
 
   function handleError(err) {
+    if (!props.autoplay) return;
     if (retryCount < maxRetry) {
       retryCount++;
       console.log(`第 ${retryCount} 次重试...`);

+ 19 - 5
src/components/vent/camera/createPlayer.vue

@@ -1,33 +1,47 @@
 <template>
   <div id="LivePlayerBox">
-    <component v-for="(item, index) in streamList" :key="index" :is="item.zj" :src="item.src" class="liveVideo" />
+    <component
+      v-for="(item, index) in streamList"
+      :key="index"
+      :is="item.zj"
+      :src="item.src"
+      :autoplay="item.autoplay"
+      class="liveVideo"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, h, onBeforeMount, onUnmounted, onBeforeUnmount } from 'vue';
+  import { ref, onMounted, onBeforeUnmount } from 'vue';
   import HlsPlayer from '@/components/vent/camera/HlsPlayer.vue';
   import LivePlayerWrapper from '@/components/vent/camera/LivePlayerWrapper.vue';
 
   const props = defineProps<{
     srcList: any[];
+    /** 自动播放起始下标,配合 autoPlayCount 使用,默认 0 */
+    autoPlayStart?: number;
+    /** 自动播放数量:未传则全部自动播放 */
+    autoPlayCount?: number;
   }>();
 
   const streamList = ref<any[]>([]);
 
   onMounted(() => {
     const componentList: any[] = [];
+    const start = props.autoPlayStart ?? 0;
     for (let i = 0; i < props.srcList.length; i++) {
       const stream = props.srcList[i];
       if (stream.addr.includes('0.0.0.0')) {
         stream.addr = stream.addr.replace('0.0.0.0', window.location.hostname);
       }
+      const autoplay =
+        props.autoPlayCount == null ? true : i >= start && i < start + props.autoPlayCount;
       if (stream.addr.toLowerCase().includes('.m3u8') || stream.addr.toLowerCase().startsWith('rtsp://')) {
-        componentList.push({ zj: HlsPlayer, src: stream.addr });
+        componentList.push({ zj: HlsPlayer, src: stream.addr, autoplay });
       } else if (stream.addr.startsWith('rtmp://')) {
-        componentList.push({ zj: LivePlayerWrapper, src: stream.addr });
+        componentList.push({ zj: LivePlayerWrapper, src: stream.addr, autoplay });
       } else {
-        componentList.push({ zj: LivePlayerWrapper, src: stream.addr });
+        componentList.push({ zj: LivePlayerWrapper, src: stream.addr, autoplay });
       }
     }
     streamList.value = componentList;

+ 6 - 2
src/hooks/component/createPlayer.ts

@@ -1,6 +1,10 @@
 import { h, VNode } from 'vue';
 import createPlayer from '@/components/vent/camera/createPlayer.vue';
 
-export function createPlayerVNode(streamList: []): VNode {
-  return h(createPlayer, { srcList: streamList });
+export function createPlayerVNode(
+  streamList: [],
+  autoPlayCount?: number,
+  autoPlayStart?: number
+): VNode {
+  return h(createPlayer, { srcList: streamList, autoPlayCount, autoPlayStart });
 }

+ 31 - 5
src/hooks/system/useCameraPianation.ts

@@ -21,6 +21,25 @@ export function useCamera() {
   const videoParentDomList: (HTMLElement | [string, { name: string; addr: string; cameraRate: number; devicekind: string }])[] = [];
   let Total = ref(0);
 
+  /** name 排序权重:可见光在前,热成像在后,其余居中 */
+  function getCameraNameOrder(name?: string) {
+    const n = name || ''
+    if (n.includes('可见光')) return 0
+    if (n.includes('热成像')) return 2
+    return 1
+  }
+
+  /** 按 name 过滤排序,保持同组内原有相对顺序 */
+  function sortCamerasByName(list: any[]) {
+    return list
+      .map((item, index) => ({ item, index }))
+      .sort((a, b) => {
+        const orderDiff = getCameraNameOrder(a.item?.name) - getCameraNameOrder(b.item?.name)
+        return orderDiff !== 0 ? orderDiff : a.index - b.index
+      })
+      .map(({ item }) => item)
+  }
+
   async function getCamera(deviceid, parentPlayerDom, renderPlayer, pagination, cameraData?, devKind?, isCustom = false) {
     await removeCameraRef(parentPlayerDom, renderPlayer);
     let res;
@@ -30,7 +49,7 @@ export function useCamera() {
       res = await cameraList({ sysId: deviceid, devKind, pageNo: pagination.current, pageSize: pagination.pageSize });
     }
     Total.value = !cameraData ? res.total : 0;
-    const cameras: [] = !cameraData ? res.records || [] : cameraData;
+    const cameras: any[] = sortCamerasByName(!cameraData ? res.records || [] : cameraData);
     console.log(cameras, 'cameras===');
     const cameraAddrs: any[] = [],
       cameraNames: any[] = [];
@@ -72,10 +91,17 @@ export function useCamera() {
         cameraAddrs.push({ name: '摄像头' + i, addr: addrs[i] });
       }
     }
+    // 地址解析完成后再次按 name 校正顺序(可见光在前,热成像在后)
+    const sortedCameraAddrs = sortCamerasByName(cameraAddrs);
     if (isCustom) {
-      return cameraAddrs;
+      return sortedCameraAddrs;
     } else {
-      await deviceCameraInit1(cameraAddrs, parentPlayerDom);
+      await deviceCameraInit1(
+        sortedCameraAddrs,
+        parentPlayerDom,
+        pagination?.autoPlayCount,
+        pagination?.autoPlayStart
+      );
     }
   }
 
@@ -362,8 +388,8 @@ export function useCamera() {
     });
   }
 
-  function deviceCameraInit1(cameraAddrs, player) {
-    const vnode = createPlayerVNode(cameraAddrs);
+  function deviceCameraInit1(cameraAddrs, player, autoPlayCount?: number, autoPlayStart?: number) {
+    const vnode = createPlayerVNode(cameraAddrs, autoPlayCount, autoPlayStart);
     render(vnode, player.value);
   }
 

+ 19 - 19
src/store/modules/user.ts

@@ -6,7 +6,7 @@ import { PageEnum } from '/@/enums/pageEnum';
 import { ROLES_KEY, TOKEN_KEY, USER_INFO_KEY, LOGIN_INFO_KEY, DB_DICT_DATA_KEY, TENANT_ID, PWD_KEY } from '/@/enums/cacheEnum';
 import { getAuthCache, setAuthCache } from '/@/utils/auth';
 import { AutoLoginParams, GetUserInfoModel, LoginParams, ThirdLoginParams } from '/@/api/sys/model/userModel';
-import { autoLoginApi, doLogout, getUserInfo, loginApi, phoneLoginApi, thirdLogin } from '/@/api/sys/user';
+import { autoLoginApi, doLogout, getUserInfo, loginApi,loginDdApi, phoneLoginApi, thirdLogin } from '/@/api/sys/user';
 import { useI18n } from '/@/hooks/web/useI18n';
 import { useMessage } from '/@/hooks/web/useMessage';
 import { router } from '/@/router';
@@ -162,24 +162,24 @@ export const useUserStore = defineStore({
       }
     },
 
-    //async login(
-    //  params: any): Promise<GetUserInfoModel | null> {
-    //  try {
-    //    const { goHome = true, mode = 'none', successMode, ...loginParams } = params;
-    //    // 进行加密
-    //    //const encryption = new AesEncryption({ key: loginCipher.key, iv: loginCipher.iv });
-    //    //loginParams.password = encryption.encryptByAES(loginParams.password);
-    //    //const data = await loginApi(loginParams, mode, successMode);
-    //    const data = await loginApi(loginParams);
-    //    const { token, userInfo } = data;
-    //    // save token
-    //    this.setToken(token);
-    //    // this.setTenant(userInfo.loginTenantId);
-    //    return this.afterLoginAction(goHome, data);
-    //  } catch (error) {
-    //    return Promise.reject(error);
-    //  }
-    //},
+    async loginDd(
+      params: any): Promise<GetUserInfoModel | null> {
+      try {
+        const { goHome = true, mode = 'none', successMode, ...loginParams } = params;
+        // 进行加密
+        //const encryption = new AesEncryption({ key: loginCipher.key, iv: loginCipher.iv });
+        //loginParams.password = encryption.encryptByAES(loginParams.password);
+        //const data = await loginDdApi(loginParams, mode, successMode);
+        const data = await loginDdApi(loginParams);
+        const { token, userInfo } = data;
+        // save token
+        this.setToken(token);
+        // this.setTenant(userInfo.loginTenantId);
+        return this.afterLoginAction(goHome, data);
+      } catch (error) {
+        return Promise.reject(error);
+      }
+    },
     /**
      * 扫码登录事件
      */

+ 188 - 0
src/views/sys/login/Login-hsq-dd.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="login-container">
+    <!-- 登录中加载态:Ant Design Spin + 自定义青色点阵,风格对齐设计稿 -->
+    <div class="login-loading">
+      <Spin :spinning="loading" size="large">
+        <template #indicator>
+          <div class="dot-spinner" aria-hidden="true">
+            <span v-for="i in 8" :key="i" class="dot" :style="dotStyle(i)" />
+          </div>
+        </template>
+        <div class="spin-holder" />
+      </Spin>
+      <div v-show="loading" class="loading-text">登录中...</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, toRaw, reactive } from 'vue'
+import { Spin } from 'ant-design-vue'
+import { useUserStore } from '/@/store/modules/user';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { getCodeInfo } from '/@/api/sys/user';
+
+const props = defineProps({
+  queryParam: {
+    type: Object,
+    default: () => {},
+  },
+})
+
+
+
+/** 登录加载状态,对接登录接口后可在请求前后切换 */
+const loading = ref(true)
+const { t } = useI18n();
+  const { notification, createErrorModal } = useMessage();
+const userStore = useUserStore();
+ 
+   const randCodeData = reactive({
+    randCodeImage: '',
+    requestCodeSuccess: false,
+    checkKey: null,
+  });
+
+function dotStyle(i: number) {
+  const index = i - 1
+  const size = 10 - index * 0.75
+  return {
+    transform: `rotate(${index * 45}deg) translateY(-22px)`,
+    opacity: Math.max(0.25, 1 - index * 0.1),
+    width: `${size}px`,
+    height: `${size}px`,
+    margin: `${-size / 2}px 0 0 ${-size / 2}px`,
+  }
+}
+
+async function handleLogin(params:any) {
+  try {
+    const { userInfo } = await userStore.loginDd(
+      toRaw({
+        ticket: params.ticket,
+        type: params.type,
+      })
+    );
+    if (userInfo) {
+      loading.value = false
+      notification.success({
+        message: t('sys.login.loginSuccessTitle'),
+        description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
+        duration: 3,
+      });
+    }
+  } catch (error) {
+    //update-begin-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
+    handleChangeCheckCode();
+    //update-end-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
+  }
+}
+
+function handleChangeCheckCode() {
+    //TODO 兼容mock和接口,暂时这样处理
+    randCodeData.checkKey = 1629428467008; //new Date().getTime();
+    getCodeInfo(randCodeData.checkKey).then((res) => {
+      randCodeData.randCodeImage = res;
+      randCodeData.requestCodeSuccess = true;
+    });
+  }
+
+  watch(
+  () => props.queryParam,
+  (type) => {
+    if(JSON.stringify(type) === '{}') return
+    // 根据 type 处理业务逻辑
+    console.log(type,'type===1')
+    handleLogin({ticket: type.ticket, type: type.type})
+  },
+  { immediate: true }
+)
+
+  //初始化验证码
+  onMounted(() => {
+    handleChangeCheckCode();
+  });
+</script>
+
+<style lang="less" scoped>
+.login-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background: url('@/assets/images/home-container/configurable/hsq/login-bg.png') no-repeat;
+  background-size: 100% 100%;
+  overflow: hidden;
+}
+
+.login-loading {
+  position: absolute;
+  inset: 0;
+  z-index: 10;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  :deep(.ant-spin-nested-loading) {
+    width: auto;
+  }
+
+  :deep(.ant-spin) {
+    max-height: none;
+    color: #00e5ff;
+  }
+
+  :deep(.ant-spin-dot) {
+    display: none !important;
+  }
+
+  :deep(.ant-spin-container) {
+    transition: none;
+  }
+
+  // .spin-holder {
+  //   width: 56px;
+  //   height: 56px;
+  // }
+}
+
+.dot-spinner {
+  position: relative;
+  width: 56px;
+  height: 56px;
+  animation: spin-rotate 1s linear infinite;
+
+  .dot {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    border-radius: 50%;
+    background: #00e5ff;
+    box-shadow:
+      0 0 4px rgba(0, 229, 255, 1),
+      0 0 10px rgba(0, 200, 255, 0.7);
+  }
+}
+
+.loading-text {
+  margin-top: 50px;
+  margin-left: 18px;
+  color: #fff;
+  font-size: 16px;
+  line-height: 1;
+  letter-spacing: 2px;
+  font-weight: 400;
+  user-select: none;
+}
+
+@keyframes spin-rotate {
+  from {
+    transform: rotate(0deg);
+  }
+
+  to {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 196 - 0
src/views/sys/login/Login-hsq-pt.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="Login">
+    <div class="login-container">
+      <div class="login-title">红沙泉二矿火区监测预警系统</div>
+      <div class="login-info">
+        <a-form :model="loginForm" name="basic" :rules="getFormRules" autocomplete="off" ref="formRef" @keypress.enter="handleLogin">
+          <a-form-item name="account">
+            <a-input v-model:value="loginForm.account" placeholder="请输入用户姓名" style="margin-bottom: 25px">
+              <template #prefix>
+                <user-outlined type="user" />
+              </template>
+            </a-input>
+          </a-form-item>
+          <a-form-item name="password">
+            <a-input-password v-model:value="loginForm.password" placeholder="请输入密码">
+              <template #prefix>
+                <LockOutlined type="user" />
+              </template>
+            </a-input-password>
+          </a-form-item>
+          <a-form-item>
+            <div class="pass-tip">忘记密码</div>
+          </a-form-item>
+        </a-form>
+      </div>
+      <div class="login-btn">
+        <a-button type="primary" style="width: 100%; height: 100%" @click="handleLogin">登录</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, toRaw, onMounted } from 'vue';
+  import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
+  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+  import { useUserStore } from '/@/store/modules/user';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { getCodeInfo } from '/@/api/sys/user';
+
+  const formRef = ref();
+  let loginForm = reactive({
+    account: 'hsq',
+    password: 'hsq123hsq',
+    inputCode: '',
+  });
+  const { validForm } = useFormValid(formRef);
+  const { getFormRules } = useFormRules();
+  const userStore = useUserStore();
+  const randCodeData = reactive({
+    randCodeImage: '',
+    requestCodeSuccess: false,
+    checkKey: null,
+  });
+  const { notification, createErrorModal } = useMessage();
+  const { t } = useI18n();
+
+  async function handleLogin() {
+    const data = await validForm();
+    if (!data) return;
+    try {
+      const { userInfo } = await userStore.login(
+        toRaw({
+          password: data.password,
+          username: data.account,
+          captcha: data.inputCode,
+          checkKey: randCodeData.checkKey,
+          mode: 'none', //不要默认的错误提示
+        })
+      );
+      if (userInfo) {
+        notification.success({
+          message: t('sys.login.loginSuccessTitle'),
+          description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
+          duration: 3,
+        });
+      }
+    } catch (error) {
+      //update-begin-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
+      handleChangeCheckCode();
+      //update-end-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
+    }
+  }
+
+  function handleChangeCheckCode() {
+    loginForm.inputCode = '';
+    //TODO 兼容mock和接口,暂时这样处理
+    randCodeData.checkKey = 1629428467008; //new Date().getTime();
+    getCodeInfo(randCodeData.checkKey).then((res) => {
+      randCodeData.randCodeImage = res;
+      randCodeData.requestCodeSuccess = true;
+    });
+  }
+  //初始化验证码
+  onMounted(() => {
+    handleChangeCheckCode();
+  });
+</script>
+
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+
+  @{theme-deepblue} {
+    .Login {
+      --image-login-bg: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/login-bg.png');
+      --image-login-bd: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/login-border.png');
+    }
+  }
+
+  .Login {
+    --image-login-bg: url('@/assets/images/gasInjection/login-bg.png');
+    --image-login-bd: url('@/assets/images/gasInjection/login-border.png');
+    position: relative;
+    width: 100%;
+    height: 100%;
+    background: var(--image-login-bg) no-repeat;
+    background-size: 100% 100%;
+
+    .login-container {
+      position: absolute;
+      width: 460px;
+      height: 600px;
+      right: 155px;
+      top: 50%;
+      transform: translate(0, -50%);
+      background: var(--image-login-bd) no-repeat;
+      background-size: 100% 100%;
+      padding: 60px;
+      box-sizing: border-box;
+    }
+
+    .login-title {
+      width: 100%;
+      height: 100px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+      font-size: 24px;
+      // font-family: 'douyuFont';
+      font-weight: bolder;
+      letter-spacing: 2px;
+    }
+
+    .login-info {
+      padding: 40px 0px 55px 0px;
+    }
+
+    .pass-tip {
+      width: 100%;
+      display: flex;
+      justify-content: flex-end;
+      color: #fff;
+      margin: 10px 0px;
+    }
+
+    .login-btn {
+      width: 100%;
+      height: 40px;
+    }
+  }
+
+  ::v-deep .zxm-input-affix-wrapper {
+    padding: 8px 11px !important;
+    color: #fff !important;
+    background-color: #0f4a6f !important;
+    border: 1px solid #2394d3 !important;
+  }
+
+  ::v-deep .zxm-input-prefix {
+    margin-right: 10px;
+  }
+
+  ::v-deep .anticon {
+    color: #fff;
+  }
+
+  ::v-deep .zxm-input {
+    height: 30px;
+    background-color: #0f4a73;
+    color: #fff;
+  }
+
+  ::v-deep .zxm-form-item-has-error :not(.zxm-input-disabled):not(.zxm-input-borderless).zxm-input {
+    background-color: #0f4a73 !important;
+  }
+
+  ::v-deep .zxm-btn-primary {
+    background: #2cb6ff !important;
+  }
+
+  ::v-deep .zxm-btn-primary:hover {
+    background: #0580c2 !important;
+  }
+</style>

+ 70 - 181
src/views/sys/login/Login.vue

@@ -1,196 +1,85 @@
 <template>
-  <div class="Login">
-    <div class="login-container">
-      <div class="login-title">红沙泉露天煤矿智能管控系统</div>
-      <div class="login-info">
-        <a-form :model="loginForm" name="basic" :rules="getFormRules" autocomplete="off" ref="formRef" @keypress.enter="handleLogin">
-          <a-form-item name="account">
-            <a-input v-model:value="loginForm.account" placeholder="请输入用户姓名" style="margin-bottom: 25px">
-              <template #prefix>
-                <user-outlined type="user" />
-              </template>
-            </a-input>
-          </a-form-item>
-          <a-form-item name="password">
-            <a-input-password v-model:value="loginForm.password" placeholder="请输入密码">
-              <template #prefix>
-                <LockOutlined type="user" />
-              </template>
-            </a-input-password>
-          </a-form-item>
-          <a-form-item>
-            <div class="pass-tip">忘记密码</div>
-          </a-form-item>
-        </a-form>
-      </div>
-      <div class="login-btn">
-        <a-button type="primary" style="width: 100%; height: 100%" @click="handleLogin">登录</a-button>
-      </div>
-    </div>
+  <div class="login-containers">
+    <component :is="currentComponent" :queryParam="queryString" />
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, toRaw, onMounted } from 'vue';
-  import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
-  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
-  import { useUserStore } from '/@/store/modules/user';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { getCodeInfo } from '/@/api/sys/user';
-
-  const formRef = ref();
-  let loginForm = reactive({
-    account: 'hsq',
-    password: 'hsq123hsq',
-    inputCode: '',
-  });
-  const { validForm } = useFormValid(formRef);
-  const { getFormRules } = useFormRules();
-  const userStore = useUserStore();
-  const randCodeData = reactive({
-    randCodeImage: '',
-    requestCodeSuccess: false,
-    checkKey: null,
-  });
-  const { notification, createErrorModal } = useMessage();
-  const { t } = useI18n();
-
-  async function handleLogin() {
-    const data = await validForm();
-    if (!data) return;
-    try {
-      const { userInfo } = await userStore.login(
-        toRaw({
-          password: data.password,
-          username: data.account,
-          captcha: data.inputCode,
-          checkKey: randCodeData.checkKey,
-          mode: 'none', //不要默认的错误提示
-        })
-      );
-      if (userInfo) {
-        notification.success({
-          message: t('sys.login.loginSuccessTitle'),
-          description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
-          duration: 3,
-        });
-      }
-    } catch (error) {
-      //update-begin-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
-      handleChangeCheckCode();
-      //update-end-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
-    }
+import { ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import LoginHsqDd from './Login-hsq-dd.vue'
+import LoginHsqPt from './Login-hsq-pt.vue'
+
+interface LoginQueryString {
+  type: string
+  ticket: string
+}
+
+
+const currentComponent = ref<any>(null)
+const route = useRoute()
+const queryString = ref<LoginQueryString>({
+  type: '',
+  ticket: '',
+})
+
+/** 从 obj(含 redirect)中截取 type、ticket */
+function extractTypeTicket(obj: Record<string, unknown>): LoginQueryString {
+  let type = ''
+  let ticket = ''
+
+  if (obj.type != null && obj.type !== '') {
+    type = String(obj.type)
   }
-
-  function handleChangeCheckCode() {
-    loginForm.inputCode = '';
-    //TODO 兼容mock和接口,暂时这样处理
-    randCodeData.checkKey = 1629428467008; //new Date().getTime();
-    getCodeInfo(randCodeData.checkKey).then((res) => {
-      randCodeData.randCodeImage = res;
-      randCodeData.requestCodeSuccess = true;
-    });
+  if (obj.ticket != null && obj.ticket !== '') {
+    ticket = String(obj.ticket)
   }
-  //初始化验证码
-  onMounted(() => {
-    handleChangeCheckCode();
-  });
-</script>
 
-<style lang="less" scoped>
-  @import '/@/design/theme.less';
+  const redirect = obj.redirect
+  if (typeof redirect === 'string' && redirect) {
+    const queryPart = redirect.includes('?')
+      ? redirect.split('?').slice(1).join('?')
+      : redirect.replace(/^\//, '')
 
-  @{theme-deepblue} {
-    .Login {
-      --image-login-bg: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/login-bg.png');
-      --image-login-bd: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/login-border.png');
+    if (queryPart.includes('=')) {
+      const searchParams = new URLSearchParams(queryPart)
+      if (!type && searchParams.get('type')) {
+        type = searchParams.get('type')!
+      }
+      if (!ticket && searchParams.get('ticket')) {
+        ticket = searchParams.get('ticket')!
+      }
     }
   }
 
-  .Login {
-    --image-login-bg: url('@/assets/images/gasInjection/login-bg.png');
-    --image-login-bd: url('@/assets/images/gasInjection/login-border.png');
-    position: relative;
-    width: 100%;
-    height: 100%;
-    background: var(--image-login-bg) no-repeat;
-    background-size: 100% 100%;
-
-    .login-container {
-      position: absolute;
-      width: 460px;
-      height: 600px;
-      right: 155px;
-      top: 50%;
-      transform: translate(0, -50%);
-      background: var(--image-login-bd) no-repeat;
-      background-size: 100% 100%;
-      padding: 60px;
-      box-sizing: border-box;
-    }
-
-    .login-title {
-      width: 100%;
-      height: 100px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      color: #fff;
-      font-size: 26px;
-      // font-family: 'douyuFont';
-      font-weight: bolder;
-      letter-spacing: 2px;
-    }
-
-    .login-info {
-      padding: 40px 0px 55px 0px;
-    }
-
-    .pass-tip {
-      width: 100%;
-      display: flex;
-      justify-content: flex-end;
-      color: #fff;
-      margin: 10px 0px;
-    }
-
-    .login-btn {
-      width: 100%;
-      height: 40px;
+  return { type, ticket }
+}
+
+watch(
+  () => route.query,
+  (query) => {
+   
+    queryString.value = extractTypeTicket(query)
+    console.log(queryString.value,'queryString.value===')
+    console.log(query,'query===')
+
+    const redirect = typeof query.redirect == 'string' ? query.redirect : ''
+    // 根据 type 处理业务逻辑
+    if (redirect.indexOf('type') !== -1 || redirect.indexOf('ticket') !== -1) {
+      currentComponent.value = LoginHsqDd
+    } else {
+      currentComponent.value = LoginHsqPt
     }
-  }
-
-  ::v-deep .zxm-input-affix-wrapper {
-    padding: 8px 11px !important;
-    color: #fff !important;
-    background-color: #0f4a6f !important;
-    border: 1px solid #2394d3 !important;
-  }
-
-  ::v-deep .zxm-input-prefix {
-    margin-right: 10px;
-  }
-
-  ::v-deep .anticon {
-    color: #fff;
-  }
-
-  ::v-deep .zxm-input {
-    height: 30px;
-    background-color: #0f4a73;
-    color: #fff;
-  }
-
-  ::v-deep .zxm-form-item-has-error :not(.zxm-input-disabled):not(.zxm-input-borderless).zxm-input {
-    background-color: #0f4a73 !important;
-  }
-
-  ::v-deep .zxm-btn-primary {
-    background: #2cb6ff !important;
-  }
+  },
+  { immediate: true }
+)
+</script>
 
-  ::v-deep .zxm-btn-primary:hover {
-    background: #0580c2 !important;
-  }
+<style lang="less" scoped>
+.login-containers {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
 </style>

+ 1 - 1
src/views/sys/login/login1.vue

@@ -10,7 +10,7 @@
       <div class="top-bg"></div>
       <div class="login-icon"></div>
       <!-- <div class="title">{{ title }}</div> -->
-      <div class="title">红沙泉露天煤矿监管系统</div>
+      <div class="title">红沙泉二矿火区监测预警系统</div>
     </div>
 
     <div class="flex center">

+ 36 - 41
src/views/vent/monitorManager/hsqHome/components/AlarmMonitor.vue

@@ -21,7 +21,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
 import dayjs from 'dayjs';
 import CommonTable from './CommonTable.vue';
 import { autoLogList, dscAlarmLogList } from '../hsqHome.api';
@@ -30,13 +30,6 @@ import {alarmColumns} from '../hsqHome.data'
 const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 
 let props = defineProps({
-  // /**
-  //  * 顶部卡片配置项
-  //  * 每项包含 label(显示名称)和 value(对应 cardData 中的 key)
-  //  */
-  // option: {
-  //   type: Array as any
-  // },
   /**
    * 卡片数据对象
    * key 对应 option 中每项的 value,value 为实际展示数值
@@ -49,40 +42,34 @@ let props = defineProps({
   }
 })
 
-/** 时间范围:[开始时间, 结束时间],默认近 7 天至当前时间 */
+/** 时间范围:[开始时间, 结束时间],默认当天 0 点至当前时间 */
 const timeRange = ref<[string, string]>([
-  dayjs().subtract(7, 'day').format(DATE_FORMAT),
+  dayjs().startOf('day').format(DATE_FORMAT),
   dayjs().format(DATE_FORMAT),
 ])
 
 // 表格数据源:从后端获取报警日志列表数据
 let tableData = ref<any[]>([])
 
-// /**
-//  * 获取报警日志列表数据
-//  *
-//  * 调用 autoLogList API,按所选开始/结束时间查询,
-//  * 将返回的报警日志数据填充到表格中用于展示
-//  *
-//  * @returns {Promise<void>}
-//  */
-// async function getAlarmData() {
-//   let res = await autoLogList({
-//     pageNum: 1,
-//     pageSize: 100,
-//     sysId: '2074416476136718337',
-//     startTime: timeRange.value?.[0],
-//     endTime: timeRange.value?.[1],
-//   });
-//   tableData.value = res.records.map(el=>{
-//     return {
-//       ...el,
-//       // 将数字类型的报警等级映射为中文描述
-//       // 101:低风险  102:一般风险  103:较大风险  104:重大风险  201:报警
-//       alarmLevelC: el.alarmLevel == 101 ? '低风险' : el.alarmLevel == 102 ? '一般风险' : el.alarmLevel == 103 ? '较大风险' : el.alarmLevel == 104 ? '重大风险' : el.alarmLevel == 201 ? '报警' : '低风险'  
-//     }
-//   });
-// }
+/** 定时获取监测数据定时器 */
+let timer: null | NodeJS.Timeout = null;
+
+/**
+ * 定时获取监测数据
+ * 通过定时器循环调用数据获取方法,更新表格数据
+ *
+ * @param flag - 是否立即执行(首次调用时传true跳过延时)
+ */
+function getMonitor(flag?: boolean) {
+  timer = setTimeout(async () => {
+    await getDscAlarmLogList();
+   
+    if (timer) {
+      timer = null;
+    }
+    getMonitor();
+  }, flag ? 0 : 10000);
+}
 
 /**
  * 获取双光谱摄像机报警记录-分页列表查询
@@ -92,20 +79,28 @@ let tableData = ref<any[]>([])
 async function getDscAlarmLogList() {
   const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000, startTime: timeRange.value?.[0], endTime: timeRange.value?.[1] });
   if (res && res.records) {
-    tableData.value = res.records.map(el=>{
-      return {
+    tableData.value = res.records
+      .map(el => ({
         ...el,
         // 将数字类型的报警等级映射为中文描述
-        eventTypeC: el.eventType == 10001 ? '热源报警' : el.eventType ==   10002	 ? '闯入报警' : '-'  
-      }
-    })
+        eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
+      }))
+      .sort((a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf())
   }
 }
 
 // 组件挂载后立即获取报警日志数据
 onMounted(() => {
-  getDscAlarmLogList();
+  getMonitor(true);
 })
+/**
+ * 组件卸载前
+ * 清理定时器,停止轮询获取监测数据
+ */
+onUnmounted(() => {
+  timer = null
+  clearTimeout(timer);
+});
 </script>
 
 <style lang="less" scoped>

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

@@ -178,7 +178,7 @@ function get(o, p) {
 
     .table__content_list_row {
       width: 100%;
-      height: 30px;
+      height: 46px;
       display: flex;
       justify-content: space-between;
       align-items: center;

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

@@ -149,9 +149,9 @@ let props = defineProps({
 
 let detailDatas = ref<any>({})
 let tableData = ref<any[]>([])
-/** 时间范围:[开始时间, 结束时间],默认近 7 天至当前时间 */
+/** 时间范围:[开始时间, 结束时间],默认当天 0 点至当前时间 */
 const timeRange = ref<[string, string]>([
-  dayjs().subtract(7, 'day').format(DATE_FORMAT),
+  dayjs().startOf('day').format(DATE_FORMAT),
   dayjs().format(DATE_FORMAT),
 ])
 
@@ -171,7 +171,7 @@ function getMonitor(flag?: boolean) {
       timer = null;
     }
     getMonitor();
-  }, flag ? 0 : 30000);
+  }, flag ? 0 : 10000);
 }
 
 /**
@@ -184,7 +184,7 @@ async function getDscAlarmLogList() {
   if (res && res.records) {
     tableData.value = res.records.map(el => ({
       ...el,
-      eventTypeC: el.eventType === 10001 ? '热源报警' : el.eventType==10002 ? '闯入报警' : '',
+      eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
     }))
   }
 }

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

@@ -119,7 +119,7 @@ async function getMonitor(flag?: boolean) {
     }
     // 递归调用自身,形成定时轮询机制
     getMonitor();
-  }, flag ? 0 : 30000); // 首次立即执行,之后每隔30秒执行一次
+  }, flag ? 0 : 10000); // 首次立即执行,之后每隔30秒执行一次
 }
 
 /**

+ 57 - 11
src/views/vent/monitorManager/hsqHome/components/DeviceTree.vue

@@ -43,7 +43,12 @@
             </span>
             <span class="device-name" :title="device.name">{{ device.name }}</span>
             <div class="device-checks" @click.stop>
-              <label v-for="(checked, cIndex) in device.checks" :key="cIndex" class="check-item">
+              <label
+                v-for="(checked, cIndex) in device.checks"
+                :key="cIndex"
+                class="check-item"
+                :data-tip="checkTips[cIndex]"
+              >
                 <input type="checkbox" :checked="checked"
                   @change="toggleCheck(device, cIndex, ($event.target as HTMLInputElement).checked)" />
                 <span class="check-box">
@@ -71,18 +76,13 @@ interface DeviceNode {
   id: string
   name: string
   /** 三个勾选状态:如显示/联动/告警等 */
-  checks: [boolean, boolean, boolean]
-}
-/** 设备分组节点数据结构 */
-interface GroupNode {
-  id: string
-  title: string
-  children: DeviceNode[]
+  checks: [boolean, boolean, ]
 }
 
+
 let props=defineProps({
   treeData: {
-    type: Array as PropType<GroupNode[]>,
+    type: Array as any,
     default: () => [],
   },
 })
@@ -91,11 +91,12 @@ let props=defineProps({
 const emit = defineEmits<{
   // (e: 'save', data: GroupNode[]): void
   (e: 'select', device: DeviceNode): void
-  (e: 'checkChange', payload: { device: DeviceNode; index: number; checked: boolean }): void
+  (e: 'checkChange', payload: any): void
   (e: 'tabChange', key: string): void
 }>()
 
-
+/** 多选框提示:第一个可见光取流,第二个红外取流 */
+const checkTips = ['可见光取流', '红外取流'] as const
 
 /** 当前激活的 Tab */
 const activeTab = ref('device')
@@ -384,6 +385,7 @@ watch(activeTab, (key) => {
 
 // 自定义勾选框样式
 .check-item {
+  position: relative;
   display: inline-flex;
   cursor: pointer;
   margin: 0;
@@ -408,6 +410,50 @@ watch(activeTab, (key) => {
     background: var(--dt-accent);
     border-color: var(--dt-accent);
   }
+
+  /* 悬停文字提示,风格对齐设备树(左侧弹出,避免被滚动容器裁切) */
+  &::after {
+    content: attr(data-tip);
+    position: absolute;
+    right: calc(100% + 8px);
+    top: 50%;
+    transform: translateY(-50%);
+    padding: 3px 8px;
+    white-space: nowrap;
+    font-size: 12px;
+    line-height: 1.4;
+    color: #e8f4ff;
+    background: rgba(7, 35, 58, 0.95);
+    border: 1px solid rgba(0, 183, 255, 0.65);
+    border-radius: 2px;
+    box-shadow: 0 0 8px rgba(0, 183, 255, 0.35);
+    pointer-events: none;
+    opacity: 0;
+    visibility: hidden;
+    transition: opacity 0.15s ease;
+    z-index: 20;
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    right: calc(100% - 2px);
+    top: 50%;
+    transform: translateY(-50%);
+    border: 5px solid transparent;
+    border-left-color: rgba(0, 183, 255, 0.65);
+    pointer-events: none;
+    opacity: 0;
+    visibility: hidden;
+    transition: opacity 0.15s ease;
+    z-index: 20;
+  }
+
+  &:hover::after,
+  &:hover::before {
+    opacity: 1;
+    visibility: visible;
+  }
 }
 
 // 隐藏滚动条

+ 178 - 37
src/views/vent/monitorManager/hsqHome/components/DeviceView.vue

@@ -13,25 +13,39 @@
     <div class="view-grid">
       <div class="view-cell" @click="selectChannel">
         <div class="media-wrap">
-
-          <div v-if="renderPlayer" ref="playerRef" style="
-     display: flex;
-      width: 100%;
-      height: 100%;
-      overflow-y: auto;
-      pointer-events: none;
-    ">
-          </div>
-
+          <div
+            v-if="renderPlayer"
+            ref="playerRef"
+            style="
+              display: flex;
+              width: 100%;
+              height: 100%;
+              overflow: hidden;
+              pointer-events: none;
+            "
+          ></div>
         </div>
       </div>
     </div>
+
+    <div class="view-pagination">
+      <a-pagination
+        size="small"
+        :current="pagination.current"
+        :page-size="pagination.pageSize"
+        :total="total"
+        :show-size-changer="false"
+        :show-less-items="true"
+        :hide-on-single-page="false"
+        @change="onPageChange"
+      />
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { onBeforeUnmount, onMounted, ref } from 'vue'
-import { useCamera } from '/@/hooks/system/useCamera';
+import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
+import { useCamera } from '/@/hooks/system/useCameraPianation'
 
 
 /** 操作按钮数据结构 */
@@ -53,9 +67,19 @@ const emit = defineEmits<{
   (e: 'select', channel: any): void
 }>()
 
-const { getCamera, removeCamera } = useCamera();
-const playerRef = ref();
-const renderPlayer = ref(true);
+const { getCamera, removeCamera, Total: total } = useCamera()
+const playerRef = ref()
+const renderPlayer = ref(true)
+/** 网格分页:2×4,每页 8 路画面;autoPlayStart/Count 控制自动播放区间 */
+const pagination = reactive<{
+  current: number
+  pageSize: number
+  autoPlayStart?: number
+  autoPlayCount?: number
+}>({
+  current: 1,
+  pageSize: 8,
+})
 /** 顶部操作按钮列表 */
 const actions: ActionItem[] = [
   { key: 'heat', label: '最新热源' },
@@ -68,6 +92,8 @@ const actions: ActionItem[] = [
 const activeAction = ref('')
 /** 当前选中的通道 ID */
 const selectedId = ref('')
+/** 取流播放模式:visible 前4播后4停;infrared 前4停后4播 */
+const streamPlayMode = ref<'visible' | 'infrared' | null>(null)
 
 /** 点击操作按钮,触发 action 事件 */
 function onAction(key: string) {
@@ -81,13 +107,90 @@ function selectChannel(item) {
   emit('select', item)
 }
 
+/** 同步分页上的自动播放区间 */
+function syncPaginationPlayRange() {
+  if (streamPlayMode.value === 'visible') {
+    pagination.autoPlayStart = 0
+    pagination.autoPlayCount = 4
+  } else if (streamPlayMode.value === 'infrared') {
+    pagination.autoPlayStart = 4
+    pagination.autoPlayCount = 4
+  } else {
+    pagination.autoPlayStart = undefined
+    pagination.autoPlayCount = undefined
+  }
+}
+
+/** 按当前取流模式校正播放/停止状态 */
+function applyStreamPlayState() {
+  if (!streamPlayMode.value) return
+  const root = playerRef.value as HTMLElement | undefined
+  if (!root) return
+  const videos = Array.from(root.querySelectorAll('video')) as HTMLVideoElement[]
+  videos.forEach((video, index) => {
+    const shouldPlay = streamPlayMode.value === 'visible' ? index < 4 : index >= 4
+    if (shouldPlay) {
+      video.play?.().catch(() => {})
+    } else {
+      video.pause?.()
+    }
+  })
+}
+
+/** 手动勾选可见光取流:前4播放,后4停止 */
+async function enableVisibleLightPlayMode() {
+  streamPlayMode.value = 'visible'
+  syncPaginationPlayRange()
+  await nextTick()
+  applyStreamPlayState()
+}
+
+/** 手动勾选红外取流:前4停止,后4播放 */
+async function enableInfraredPlayMode() {
+  streamPlayMode.value = 'infrared'
+  syncPaginationPlayRange()
+  await nextTick()
+  applyStreamPlayState()
+}
+
+/** 加载当前页监控画面 */
+async function loadCameraList() {
+  syncPaginationPlayRange()
+  await getCamera(props.deviceId, playerRef, renderPlayer, pagination)
+  if (streamPlayMode.value) {
+    await nextTick()
+    // 等待播放器挂载完成后再校正播放状态
+    setTimeout(() => applyStreamPlayState(), 300)
+  }
+}
+
+/** 分页切换 */
+async function onPageChange(page: number) {
+  pagination.current = page
+  await loadCameraList()
+}
+
 onMounted(async () => {
-  await getCamera(props.deviceId, playerRef, renderPlayer);
-});
+  await loadCameraList()
+})
+
+watch(
+  () => props.deviceId,
+  async () => {
+    pagination.current = 1
+    await loadCameraList()
+  }
+)
 
 onBeforeUnmount(() => {
   removeCamera(playerRef);
 });
+
+defineExpose({
+  enableVisibleLightPlayMode,
+  enableInfraredPlayMode,
+  applyStreamPlayState,
+})
 </script>
 
 <style lang="less" scoped>
@@ -149,30 +252,68 @@ onBeforeUnmount(() => {
 // 2×4 监控画面网格
 .view-grid {
   flex: 1;
+  min-height: 0;
   display: flex;
-  // flex-wrap: wrap;
-  // gap: 15px 10px;
-  // justify-content: flex-start;
-  // align-items: flex-start;
-  overflow-y: auto;
+  overflow: hidden;
+}
+
+.view-cell {
+  flex: 1;
+  min-height: 0;
+  cursor: pointer;
+}
+
+.media-wrap {
+  height: 100%;
+  box-sizing: border-box;
+}
 
+.view-pagination {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  flex-shrink: 0;
+  height: 36px;
+  padding: 0 4px;
 }
 
-// // 单个监控画面单元格
-// .view-cell {
-//   width: calc(25% - 10px);
-//   height: 220px;
-//   cursor: pointer;
-//   // background: var(--image-bg) no-repeat center;
-//   // background-size: 100% 100%;
-// }
+:deep(.zxm-pagination) {
+  color: #cfe9ff;
 
-// // 媒体内容包裹层
-// .media-wrap {
-//   height: 100%;
-//   padding: 10px 10px 12px 10px;
-//   box-sizing: border-box;
-// }
+  .zxm-pagination-item,
+  .zxm-pagination-prev,
+  .zxm-pagination-next,
+  .zxm-pagination-jump-prev,
+  .zxm-pagination-jump-next {
+    min-width: 26px;
+    height: 26px;
+    line-height: 24px;
+    border: 1px solid rgba(58, 225, 255, 0.45);
+    background: rgba(16, 64, 100, 0.85);
+    color: #cfe9ff;
+  }
+
+  .zxm-pagination-item a,
+  .zxm-pagination-prev button,
+  .zxm-pagination-next button {
+    color: #cfe9ff;
+  }
+
+  .zxm-pagination-item-active {
+    border-color: #01fefc;
+    background: rgba(32, 166, 169, 0.85);
+
+    a {
+      color: #fff;
+    }
+  }
+
+  .zxm-pagination-disabled .zxm-pagination-item-link,
+  .zxm-pagination-disabled button {
+    color: rgba(207, 233, 255, 0.35);
+    border-color: rgba(58, 225, 255, 0.2);
+  }
+}
 
 
 // 无画面占位提示
@@ -192,7 +333,7 @@ onBeforeUnmount(() => {
 
   .liveVideo {
     width: 349px !important;
-    height: 252px !important;
+    height: 220px !important;
     padding: 12px 8px !important;
     align-self: auto !important;
     // margin: 8px !important;

+ 6 - 4
src/views/vent/monitorManager/hsqHome/components/HistoryLine.vue

@@ -34,8 +34,9 @@ const deviceType = ref('')
 /** 实时温度曲线图表数据 */
 let chartData = ref<any[]>([])
 /** 时间范围:[开始时间, 结束时间],默认近 7 天至当前时间 */
+/** 时间范围:[开始时间, 结束时间],默认当天 0 点至当前时间 */
 const timeRange = ref<[string, string]>([
-  dayjs().subtract(30, 'day').format(DATE_FORMAT),
+  dayjs().startOf('day').format(DATE_FORMAT),
   dayjs().format(DATE_FORMAT),
 ])
 
@@ -46,9 +47,10 @@ function onDeviceChange(value: string) {
   const item = props.deviceOptions.find((d) => d.value === value)
   currentDevice.value = value
   deviceType.value = item?.devicetype
-  if (item) {
-    // emit('deviceChange', item)
-  }
+  getChartData()
+  // if (item) {
+  //   // emit('deviceChange', item)
+  // }
 }
 
 /** 获取历史温度曲线数据 */

+ 92 - 4
src/views/vent/monitorManager/hsqHome/components/SystemLeftBottom.vue

@@ -20,6 +20,18 @@
           </div>
         </div>
       </div>
+      <div v-if="showPagination" class="table-pagination">
+        <a-pagination
+          size="small"
+          :current="pageNo"
+          :page-size="pageSize"
+          :total="total"
+          :show-size-changer="false"
+          :show-less-items="true"
+          :hide-on-single-page="false"
+          @change="onPageChange"
+        />
+      </div>
 
     </div>
   </div>
@@ -49,15 +61,39 @@ let props = defineProps({
   titleOption: {
     type: Array as PropType<BtnOption[]>,
     default: () => [],
-  }
+  },
+  showPagination: {
+    type: Boolean,
+    default: false,
+  },
+  pageNo: {
+    type: Number,
+    default: 1,
+  },
+  pageSize: {
+    type: Number,
+    default: 10,
+  },
+  total: {
+    type: Number,
+    default: 0,
+  },
 })
 let activeIndex = ref(0)
-let $emit=defineEmits(['changeTab'])
+const emit = defineEmits<{
+  (e: 'changeTab', value?: string | number): void
+  (e: 'pageChange', pageNo: number, pageSize: number): void
+}>()
 
 //tab选项切换
 function handleClick(item, index) {
   activeIndex.value = index
-  $emit('changeTab',item.value)
+  emit('changeTab', item.value)
+}
+
+/** 分页切换 */
+function onPageChange(page: number, size: number) {
+  emit('pageChange', page, size)
 }
 </script>
 
@@ -129,6 +165,9 @@ function handleClick(item, index) {
 
   .list-content {
     height: calc(100% - 40px);
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
   }
 
   .content-title {
@@ -174,10 +213,59 @@ function handleClick(item, index) {
   }
 
   .table-content {
-    height: calc(100% - 40px);
+    flex: 1;
+    min-height: 0;
     overflow-y: auto;
   }
 
+  .table-pagination {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    flex-shrink: 0;
+    height: 36px;
+    margin-top: 6px;
+    padding: 0 4px;
+  }
+
+  :deep(.zxm-pagination) {
+    color: #cfe9ff;
+
+    .zxm-pagination-item,
+    .zxm-pagination-prev,
+    .zxm-pagination-next,
+    .zxm-pagination-jump-prev,
+    .zxm-pagination-jump-next {
+      min-width: 26px;
+      height: 26px;
+      line-height: 24px;
+      border: 1px solid rgba(58, 225, 255, 0.45);
+      background: rgba(16, 64, 100, 0.85);
+      color: #cfe9ff;
+    }
+
+    .zxm-pagination-item a,
+    .zxm-pagination-prev button,
+    .zxm-pagination-next button {
+      color: #cfe9ff;
+    }
+
+    .zxm-pagination-item-active {
+      border-color: #01fefc;
+      background: rgba(32, 166, 169, 0.85);
+
+      a {
+        color: #fff;
+      }
+    }
+
+    .zxm-pagination-disabled .zxm-pagination-item-link,
+    .zxm-pagination-disabled button {
+      color: rgba(207, 233, 255, 0.35);
+      border-color: rgba(58, 225, 255, 0.2);
+    }
+  }
+
   .content-item {
     display: flex;
     justify-content: space-between;

+ 87 - 2
src/views/vent/monitorManager/hsqHome/components/SystemLeftTop.vue

@@ -29,13 +29,25 @@
           </div>
         </div>
       </div>
+      <div class="table-pagination">
+        <a-pagination
+          size="small"
+          :current="pageNo"
+          :page-size="pageSize"
+          :total="total"
+          :show-size-changer="false"
+          :show-less-items="true"
+          :hide-on-single-page="false"
+          @change="onPageChange"
+        />
+      </div>
       <!-- </div> -->
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, type PropType } from 'vue';
+import { type PropType } from 'vue';
 import { useRouter } from 'vue-router'
 
 let props = defineProps({
@@ -47,8 +59,24 @@ let props = defineProps({
     type: Object as PropType<Record<string, any>>,
     default: () => ({}),
   },
+  pageNo: {
+    type: Number,
+    default: 1,
+  },
+  pageSize: {
+    type: Number,
+    default: 10,
+  },
+  total: {
+    type: Number,
+    default: 0,
+  },
 })
 
+const emit = defineEmits<{
+  (e: 'pageChange', pageNo: number, pageSize: number): void
+}>()
+
 // let searchData = ref('')
 let router = useRouter()
 
@@ -60,6 +88,11 @@ function handleDetail() {
   })
 }
 
+/** 分页切换 */
+function onPageChange(page: number, size: number) {
+  emit('pageChange', page, size)
+}
+
 </script>
 
 <style lang="less" scoped>
@@ -119,6 +152,9 @@ function handleDetail() {
 
   .list-content {
     height: calc(100% - 40px);
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
   }
 
   // .search-area {
@@ -189,10 +225,59 @@ function handleDetail() {
   }
 
   .table-content {
-    height: calc(100% - 40px);
+    flex: 1;
+    min-height: 0;
     overflow-y: auto;
   }
 
+  .table-pagination {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    flex-shrink: 0;
+    height: 36px;
+    margin-top: 6px;
+    padding: 0 4px;
+  }
+
+  :deep(.zxm-pagination) {
+    color: #cfe9ff;
+
+    .zxm-pagination-item,
+    .zxm-pagination-prev,
+    .zxm-pagination-next,
+    .zxm-pagination-jump-prev,
+    .zxm-pagination-jump-next {
+      min-width: 26px;
+      height: 26px;
+      line-height: 24px;
+      border: 1px solid rgba(58, 225, 255, 0.45);
+      background: rgba(16, 64, 100, 0.85);
+      color: #cfe9ff;
+    }
+
+    .zxm-pagination-item a,
+    .zxm-pagination-prev button,
+    .zxm-pagination-next button {
+      color: #cfe9ff;
+    }
+
+    .zxm-pagination-item-active {
+      border-color: #01fefc;
+      background: rgba(32, 166, 169, 0.85);
+
+      a {
+        color: #fff;
+      }
+    }
+
+    .zxm-pagination-disabled .zxm-pagination-item-link,
+    .zxm-pagination-disabled button {
+      color: rgba(207, 233, 255, 0.35);
+      border-color: rgba(58, 225, 255, 0.2);
+    }
+  }
+
   .content-item {
     display: flex;
     justify-content: space-between;

+ 154 - 65
src/views/vent/monitorManager/hsqHome/components/WarnBottom.vue

@@ -1,45 +1,38 @@
   <template>
     <div class="echart-bar-new">
       <div class="list-title">
-        报警趋势统计
+        温度趋势统计
       </div>
       <div ref="line" class="list-content"></div>
     </div>
   </template>
 
 <script setup lang="ts">
-import { nextTick, onMounted, ref } from 'vue'
+import { nextTick, ref, watch, type PropType } from 'vue'
 import * as echarts from 'echarts';
-import { dscAlarmLogList } from '../hsqHome.api';
+import { listdays } from '../hsqHome.api'
 import dayjs from 'dayjs';
 
-const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+let props = defineProps({
+  deviceData: {
+    type: Object,
+    default: () => { },
+  },
+  timeRange: {
+    type: Array as any,
+    default: () => [
+      dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    ],
+  },
+})
+
 //获取dom元素节点
 const line = ref<any>();
-/** 时间范围:[开始时间, 结束时间],默认近 7 天至当前时间 */
-const timeRange = ref<[string, string]>([
-  dayjs().subtract(7, 'day').format(DATE_FORMAT),
-  dayjs().format(DATE_FORMAT),
-])
 const xData = ref<any[]>([]);
-const yData = ref<any[]>([]);
-// 定时器引用,用于页面卸载时清理
-let timer: null | NodeJS.Timeout = null;
-/**
- * 定时获取监测数据
- * 
- * @param flag 是否立即执行(默认false,延迟30秒执行)
- */
-function getMonitor(flag?: boolean) {
-  timer = setTimeout(async () => {
-    await getDscAlarmLogList();
-    if (timer) {
-      timer = null;
-    }
-    // 递归调用实现持续轮询
-    getMonitor();
-  }, flag ? 0 : 10000);
-}
+const avgData = ref<any[]>([]);
+const maxData = ref<any[]>([]);
+const minData = ref<any[]>([]);
 
 function getOption() {
   nextTick(() => {
@@ -74,9 +67,9 @@ function getOption() {
         itemWidth: 18,
         icon: 'path://M0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z',
         data: [
-          {
-            name: '报警温度',
-          },
+          { name: '平均温度', },
+          { name: '最高温度', },
+          { name: '最低温度', },
         ],
       },
       xAxis: [
@@ -113,9 +106,9 @@ function getOption() {
       yAxis: [
         {
           boundaryGap: false,
-          name: '',
+          name: '温度(℃)',
           type: 'value',
-          max: 1,
+          max: 100,
           min: 0,
           axisLabel: {
             textStyle: {
@@ -125,7 +118,7 @@ function getOption() {
           nameTextStyle: {
             color: '#fff',
             fontSize: 12,
-            lineHeight: 5,
+            lineHeight: 1,
           },
           splitLine: {
             lineStyle: {
@@ -146,7 +139,7 @@ function getOption() {
       ],
       series: [
         {
-          name: '报警温度',
+          name: '最高温度',
           type: 'line',
           smooth: false,
           showSymbol: false,
@@ -161,32 +154,118 @@ function getOption() {
               color: '#02f2de',
             },
           },
-          areaStyle: {
+          // areaStyle: {
+          //   normal: {
+          //     color: new echarts.graphic.LinearGradient(
+          //       0,
+          //       0,
+          //       0,
+          //       1,
+          //       [
+          //         {
+          //           offset: 0,
+          //           color: 'rgba(2, 242, 222,0.8)',
+          //         },
+          //         {
+          //           offset: 0.2,
+          //           color: 'rgba(2, 242, 222,0.4)',
+          //         },
+          //         {
+          //           offset: 0.5,
+          //           color: 'rgba(2, 242, 222,0)',
+          //         },
+          //       ],
+          //       false
+          //     ),
+          //   },
+          // },
+          data: maxData.value || [],
+        },
+        {
+          name: '最低温度',
+          type: 'line',
+          smooth: false,
+          showSymbol: false,
+          zlevel: 3,
+          itemStyle: {
+            color: '#11d175',
+            borderColor: '#11d175',
+          },
+          lineStyle: {
             normal: {
-              color: new echarts.graphic.LinearGradient(
-                0,
-                0,
-                0,
-                1,
-                [
-                  {
-                    offset: 0,
-                    color: 'rgba(2, 242, 222,0.8)',
-                  },
-                  {
-                    offset: 0.5,
-                    color: 'rgba(2, 242, 222,0.4)',
-                  },
-                  {
-                    offset: 0.9,
-                    color: 'rgba(2, 242, 222,0)',
-                  },
-                ],
-                false
-              ),
+              width: 1,
+              color: '#11d175',
             },
           },
-          data: yData.value || [],
+          // areaStyle: {
+          //   normal: {
+          //     color: new echarts.graphic.LinearGradient(
+          //       0,
+          //       0,
+          //       0,
+          //       1,
+          //       [
+          //         {
+          //           offset: 0,
+          //           color: 'rgba(17, 209, 117,0.8)',
+          //         },
+          //         {
+          //           offset: 0.5,
+          //           color: 'rgba(17, 209, 117,0.4)',
+          //         },
+          //         {
+          //           offset: 0.9,
+          //           color: 'rgba(17, 209, 117,0)',
+          //         },
+          //       ],
+          //       false
+          //     ),
+          //   },
+          // },
+          data: minData.value || [],
+        },
+        {
+          name: '平均温度',
+          type: 'line',
+          smooth: false,
+          showSymbol: false,
+          zlevel: 3,
+          itemStyle: {
+            color: '#fbc014',
+            borderColor: '#fbc014',
+          },
+          lineStyle: {
+            normal: {
+              width: 1,
+              color: '#fbc014',
+            },
+          },
+          // areaStyle: {
+          //   normal: {
+          //     color: new echarts.graphic.LinearGradient(
+          //       0,
+          //       0,
+          //       0,
+          //       1,
+          //       [
+          //         {
+          //           offset: 0,
+          //           color: 'rgba(252, 197, 37,0.8)',
+          //         },
+          //         {
+          //           offset: 0.5,
+          //           color: 'rgba(252, 197, 37,0.4)',
+          //         },
+          //         {
+          //           offset: 0.9,
+          //           color: 'rgba(252, 197, 37,0)',
+          //         },
+          //       ],
+          //       false
+          //     ),
+          //   },
+          // },
+          data: avgData.value || [],
         },
       ],
     };
@@ -202,18 +281,28 @@ function getOption() {
  * 调用 dscAlarmLogList 接口获取双光谱摄像机报警记录分页列表,
  * 并将返回数据赋值给 warnList
  */
-async function getDscAlarmLogList() {
-  const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000, startTime: timeRange.value?.[0], endTime: timeRange.value?.[1] });
-  if (res && res.records) {
-    xData.value = res.records.map(el => el.startTime)
-    yData.value = res.records.map(el => el.tempFlag)
+async function getDscAlarmLogList(param) {
+  const [startTime, endTime] = props.timeRange
+  let res = await listdays({ ttime_begin: startTime, ttime_end: endTime, skip: '8', gdeviceids: param.deviceId, pageNo: 1, pageSize: 1000, strtype: 'duaSpeCamera__normal' })
+  if (res.datalist.records.length) {
+    xData.value = res.datalist.records.map(item => item.ttime);
+    avgData.value = res.datalist.records.map(item => item.readData.avg);
+    maxData.value = res.datalist.records.map(item => item.readData.max);
+    minData.value = res.datalist.records.map(item => item.readData.min);
     getOption();
   }
 }
 
-onMounted(async () => {
-  getMonitor(true);
-});
+watch(
+  () => [props.deviceData, props.timeRange] as const,
+  async ([newVal]) => {
+    if (newVal?.deviceId) {
+      await getDscAlarmLogList(newVal);
+    }
+  },
+  { immediate: true, deep: true }
+)
+
 </script>
 
 <style lang="less" scoped>

+ 195 - 40
src/views/vent/monitorManager/hsqHome/components/WarnCenTop.vue

@@ -1,44 +1,64 @@
 <template>
   <div class="mixed-warn">
     <div class="list-title">
-      <span>报警详情-</span>
-      <span style="margin-left: 5px;">{{ warnData.preset || '-' }}</span>
+      <span>报警详情</span>
+      <!-- <span style="margin-left: 5px;">{{ warnData.devId || '-' }}</span> -->
     </div>
     <div class="list-t">
       <div class="basic-box">
         <div class="box-btn">
-          <div v-for="(btn,index) in actionBtns" :key="btn.key"
-            :class="['baisc-btn', { 'baisc-btn-active': activeBtn === btn.key }]" @click="changeStep(btn)">
+          <div v-for="(btn, index) in actionBtns" :key="btn.key"
+            :class="['baisc-btn', { 'baisc-btn-active': activeBtn == btn.key }]" @click="changeStep(btn)">
             <div class="btn-item">{{ btn.label }}</div>
           </div>
         </div>
         <div class="echart-box">
-           <img  :src="urlStr + '/sys/common/static/' + warnData.picName" />
+          <div class="box-img">
+            <img
+              v-if="picUrl1"
+              :src="picUrl1"
+              title="双击放大预览"
+              @dblclick="openPreview(picUrl1)"
+            />
+          </div>
+          <div class="box-img">
+            <img
+              v-if="picUrl2"
+              :src="picUrl2"
+              title="双击放大预览"
+              @dblclick="openPreview(picUrl2)"
+            />
+          </div>
         </div>
       </div>
       <div class="basic-box">
         <basicBorder title="报警详情">
           <div class="warn-detail">
+             <div class="warn-item">
+              <div class="item-label">设备编号 : </div>
+              <div class="item-val text-val1">{{ warnData.devId || '-' }}</div>
+            </div>
             <div class="warn-item">
               <div class="item-label">报警设备 : </div>
               <div class="item-val text-val1">{{ warnData.deviceName || '-' }}</div>
             </div>
-            <div class="warn-item">
-              <div class="item-label">报警位置 : </div>
-              <div class="item-val text-val1"> {{ warnData.devicePos || '-' }}</div>
-            </div>
             <div class="warn-item">
               <div class="item-label">报警类型 : </div>
-              <div class="item-val text-val"> {{ warnData.eventType=='10001' ? '热源报警' : warnData.eventType=='10002' ? '闯入报警' : '-' }}</div>
+              <div class="item-val text-val"> {{ warnData.eventType == '10001' ? '热源报警' : warnData.eventType == '10002'
+                ?
+                '闯入报警' : '-' }}</div>
             </div>
             <div class="warn-item">
+              <div class="item-label">报警时间 : </div>
+              <div class="item-val text-val1"> {{ warnData.createTime || '-' }}</div>
+            </div>
+            <!-- <div class="warn-item">
               <div class="item-label">红外类型 : </div>
               <div class="item-val text-val"> {{ warnData.irModel || '-' }}</div>
-            </div>
+            </div> -->
             <div class="warn-item">
               <div class="item-label ">模式 : </div>
-              <!-- <div class="item-val text-val"> {{ `${warnDatas.temp}℃` || '-' }}</div> -->
-              <div class="item-val text-val"> {{ warnData.mode || '-' }}</div>
+              <div class="item-val text-val"> {{ warnData.mode=='1' ? '预置位' : warnData.mode=='2' ? '角度位下标号' : '-'  }}</div>
             </div>
             <div class="warn-item">
               <div class="item-label">红外水平像素数量 : </div>
@@ -56,10 +76,11 @@
               <div class="item-label">云台垂直角 : </div>
               <div class="item-val text-val1"> {{ warnData.ptzVerAngle || '-' }}</div>
             </div>
-             <div class="warn-item">
+            <!-- <div class="warn-item">
               <div class="item-label">测温标记 : </div>
               <div class="item-val text-val1"> {{ warnData.tempFlag || '-' }}</div>
-            </div>
+            </div> -->
+
           </div>
         </basicBorder>
       </div>
@@ -88,11 +109,26 @@
         </div>
       </basicBorder>
     </div>
+
+    <!-- 图片双击放大预览 -->
+    <Teleport to="body">
+      <div v-if="previewVisible" class="img-preview-mask" @click.self="closePreview">
+        <div class="img-preview-panel">
+          <div class="img-preview-header">
+            <span>图片预览</span>
+            <div class="img-preview-close" @click="closePreview">×</div>
+          </div>
+          <div class="img-preview-body">
+            <img :src="previewSrc" alt="预览图片" />
+          </div>
+        </div>
+      </div>
+    </Teleport>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 import basicBorder from './basicBorder.vue'
 import { picList, actionBtns } from '../hsqHome.data'
 import { useGlobSetting } from '/@/hooks/setting';
@@ -114,27 +150,70 @@ const globSetting = useGlobSetting();
 const baseUploadUrl = globSetting.uploadUrl;
 const { createMessage } = useMessage();
 let urlStr = '';
-  if (import.meta.env.PROD) {
-    urlStr = VUE_APP_URL.baseUrl;
-  } else {
-    urlStr = baseUploadUrl || '';
-  }
+if (import.meta.env.PROD) {
+  urlStr = VUE_APP_URL.baseUrl;
+} else {
+  urlStr = baseUploadUrl || '';
+}
 
-  /** 切换步骤 */
-  function changeStep(btn: any) {
-    currentStep.value = btn.key -1
-    activeBtn.value = btn.key
-    getUpdateWarnStatus()
-  }
+/** 预览弹窗显隐与图片地址 */
+const previewVisible = ref(false)
+const previewSrc = ref('')
+
+/** 重置处理流程 UI 状态(不调用接口) */
+function resetProcessFlow() {
+  currentStep.value = 0
+  activeBtn.value = ''
+}
 
-  /** 更新报警状态 */
-  async function getUpdateWarnStatus() {
-    let res = await updateProcessState({
-     id:props.warnData.id,
-     processState:currentStep.value,
-    })
-   createMessage.success(res.msg || '更新成功')
+/** 报警信息列表切换详情时,重置处理流程状态 */
+watch(
+  () => props.warnData?.id,
+  (id, prevId) => {
+    if (id !== prevId) {
+      resetProcessFlow()
+    }
   }
+)
+
+/** 拼接报警图片完整地址 */
+function buildPicUrl(picName?: string) {
+  if (!picName) return ''
+  const path = picName.indexOf('h') >= 0 ? picName.substring(picName.indexOf('h')) : picName
+  return `${urlStr}/sys/common/static/${path}`
+}
+
+const picUrl1 = computed(() => buildPicUrl(props.warnData?.picName))
+const picUrl2 = computed(() => buildPicUrl(props.warnData?.picName2))
+
+/** 打开图片预览 */
+function openPreview(src: string) {
+  if (!src) return
+  previewSrc.value = src
+  previewVisible.value = true
+}
+
+/** 关闭图片预览 */
+function closePreview() {
+  previewVisible.value = false
+  previewSrc.value = ''
+}
+
+/** 切换步骤 */
+function changeStep(btn: any) {
+  currentStep.value = btn.key - 1
+  activeBtn.value = btn.key
+  getUpdateWarnStatus()
+}
+
+/** 更新报警状态 */
+async function getUpdateWarnStatus() {
+  let res = await updateProcessState({
+    id: props.warnData.id,
+    processState: currentStep.value,
+  })
+  createMessage.success(res.msg || '更新成功')
+}
 
 </script>
 
@@ -230,13 +309,22 @@ let urlStr = '';
   }
 
   .echart-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
     height: calc(100% - 44px);
-    background-color: #114268;
-    // background: var(--image-box-bg7) no-repeat;
-    // background-size: 100% 100%;
-    img {
-      width: 100%;
-      height: 100%;
+
+    .box-img {
+      height: calc(50% - 2px);
+      background-color: #114268;
+      overflow: hidden;
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        cursor: zoom-in;
+      }
     }
   }
 
@@ -354,4 +442,71 @@ let urlStr = '';
 :deep(.zxm-steps-item-wait > .zxm-steps-item-container > .zxm-steps-item-tail::after) {
   background-color: rgba(28, 118, 172, .6) !important;
 }
+
+/* Teleport 到 body,需写在 .mixed-warn 外以保证样式生效 */
+.img-preview-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 3000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(9, 23, 44, 0.82);
+  backdrop-filter: blur(4px);
+}
+
+.img-preview-panel {
+  width: min(86vw, 1100px);
+  height: min(82vh, 760px);
+  display: flex;
+  flex-direction: column;
+  border: 1px solid rgba(11, 37, 66);
+  box-shadow: 0 0 20px 5px rgba(32, 131, 189, 0.7) inset, 0 0 24px rgba(1, 254, 252, 0.2);
+  background: rgba(9, 35, 58, 0.95);
+}
+
+.img-preview-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 36px;
+  padding: 0 14px;
+  color: #fff;
+  background: linear-gradient(to right, #134c77, transparent);
+  border-bottom: 1px solid rgba(41, 171, 241, 0.45);
+}
+
+.img-preview-close {
+  width: 28px;
+  height: 24px;
+  line-height: 20px;
+  text-align: center;
+  cursor: pointer;
+  color: #01fefc;
+  border: 1px solid #01fefc;
+  border-radius: 2px;
+  transition: background-color 0.2s, box-shadow 0.2s;
+
+  &:hover {
+    background-color: rgba(36, 132, 188, 0.55);
+    box-shadow: 0 0 6px rgba(1, 254, 252, 0.45);
+  }
+}
+
+.img-preview-body {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16px;
+  box-sizing: border-box;
+  background-color: #114268;
+  overflow: hidden;
+
+  img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+  }
+}
 </style>

+ 64 - 33
src/views/vent/monitorManager/hsqHome/components/WarnLeftTop.vue

@@ -1,7 +1,7 @@
 <!-- 报警信息左侧顶部组件:包含报警搜索、报警列表展示及详情查看功能 -->
 <template>
   <div class="search-table">
-      <div class="list-title"> 报警信息 </div>
+    <div class="list-title"> 报警信息 </div>
     <!-- 搜索区域:包含搜索输入框和筛选按钮 -->
     <div class="search-area">
       <div class="time-range">
@@ -21,17 +21,11 @@
         <div class="content-item" :class="{ 'content-item-active': activeIndex == index }"
           v-for="(item, index) in tableData" :key="index">
           <div class="item-text" v-for="(ite, ind) in warnTitle" :key="ind">
-            <!-- 设备名称列 -->
-            <div v-if="ite.value == 'deviceName'">{{ item[ite.value] }}</div>
-            <!-- 安装位置列 -->
-            <div v-if="ite.value == 'devicePos'">{{ item[ite.value] }}</div>
-            <!-- 报警类型列 -->
-            <div v-if="ite.value == 'eventType'">{{ item[ite.value] }}</div>
             <!-- 操作列:点击"详情"触发详情查看 -->
             <div v-if="ite.value == 'operation'">
-              <span class="text-look"
-                @click="handlerDetail(item, index)">详情</span>
+              <span class="text-look" @click="handlerDetail(item, index)">详情</span>
             </div>
+            <div v-else class="cell-value" :title="item[ite.value]">{{ item[ite.value] || '-' }}</div>
           </div>
         </div>
       </div>
@@ -40,22 +34,25 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch, type PropType } from 'vue'
+import { onMounted, onUnmounted, ref, watch, type PropType } from 'vue'
 import { warnTitle } from '../hsqHome.data'
 import dayjs from 'dayjs'
 import { dscAlarmLogList } from '../hsqHome.api'
 
 const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 let activeIndex = ref(0)
-/** 时间范围:[开始时间, 结束时间],默认近 7 天至当前时间 */
+/** 时间范围:[开始时间, 结束时间],默认当天 0 点至当前时间 */
 const timeRange = ref<[string, string]>([
-  dayjs().subtract(7, 'day').format(DATE_FORMAT),
+  dayjs().startOf('day').format(DATE_FORMAT),
   dayjs().format(DATE_FORMAT),
 ])
 // 告警列表数据
 let tableData = ref<any[]>([])
-// 定义事件:detail - 查看报警详情
-let $emit = defineEmits(['detail'])
+// 定义事件:detail - 查看报警详情;timeChange - 时间范围变更
+let $emit = defineEmits<{
+  (e: 'detail', item: any): void
+  (e: 'timeChange', range: [string, string]): void
+}>()
 
 
 // 定时器引用,用于页面卸载时清理
@@ -83,15 +80,16 @@ function getMonitor(flag?: boolean) {
  * 并将返回数据赋值给 warnList
  */
 async function getDscAlarmLogList() {
+  $emit('timeChange', [...timeRange.value])
   const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000, startTime: timeRange.value?.[0], endTime: timeRange.value?.[1] });
   if (res && res.records) {
-    tableData.value = res.records.map(el=>{
-      return {
+    tableData.value = res.records
+      .map(el => ({
         ...el,
         // 将数字类型的报警等级映射为中文描述
-        eventTypeC: el.eventType == 10001 ? '热源报警' : el.eventType ==   10002	 ? '闯入报警' : '-'  
-      }
-    })
+        eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
+      }))
+      .sort((a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf())
     $emit('detail', tableData.value[activeIndex.value])
   }
 }
@@ -106,6 +104,14 @@ onMounted(() => {
   getDscAlarmLogList();
   getMonitor(true);
 })
+/**
+ * 组件卸载前
+ * 清理定时器,停止轮询获取监测数据
+ */
+onUnmounted(() => {
+  timer = null
+  clearTimeout(timer);
+});
 
 </script>
 <style lang="less" scoped>
@@ -117,7 +123,7 @@ onMounted(() => {
 
 /* 整体容器:定义背景图片变量及基础布局 */
 .search-table {
-   --image-box-bg: url('@/assets/images/home-container/configurable/hsq/2-5.png');
+  --image-box-bg: url('@/assets/images/home-container/configurable/hsq/2-5.png');
   --image-box-bg1: url('@/assets/images/home-container/configurable/hsq/2-6.png');
   --image-box-bg2: url('@/assets/images/home-container/configurable/hsq/2-24.png');
   position: relative;
@@ -126,16 +132,17 @@ onMounted(() => {
   padding: 15px;
   box-sizing: border-box;
 }
+
 .list-title {
-    width: 177px;
-    height: 30px;
-    line-height: 22px;
-    color: #01fefc;
-    font-weight: bolder;
-    background: var(--image-box-bg) no-repeat;
-    background-size: 80% 100%;
-    margin-bottom: 5px;
-  }
+  width: 177px;
+  height: 30px;
+  line-height: 22px;
+  color: #01fefc;
+  font-weight: bolder;
+  background: var(--image-box-bg) no-repeat;
+  background-size: 80% 100%;
+  margin-bottom: 5px;
+}
 
 
 /* 搜索区域:输入框与筛选按钮水平排列 */
@@ -215,6 +222,10 @@ onMounted(() => {
   }
 
   &:nth-child(4) {
+    flex: 1.6;
+  }
+
+  &:nth-child(5) {
     flex: 1;
   }
 }
@@ -225,13 +236,13 @@ onMounted(() => {
   overflow-y: auto;
 }
 
-/* 数据行:奇偶行交替背景色 */
+/* 数据行:奇偶行交替背景色,增高以支持两行展示 */
 .content-item {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  //height: 36px;
-  padding: 4px 0px;
+  min-height: 52px;
+  padding: 6px 2px;
   box-sizing: border-box;
 
   &:nth-child(odd) {
@@ -245,7 +256,7 @@ onMounted(() => {
   /* 详情激活时的行选中高亮,边框色与详情按钮激活态一致 */
   &.content-item-active {
     background-color: rgba(36, 132, 188, 0.35) !important;
-    box-shadow: inset 0 0 0 1px #90cff3;
+    box-shadow: inset 0 0 0 1px #ffa500;
   }
 }
 
@@ -255,6 +266,9 @@ onMounted(() => {
   justify-content: center;
   align-items: center;
   text-align: center;
+  min-width: 0;
+  padding: 0 2px;
+  box-sizing: border-box;
 
   &:nth-child(1) {
     flex: 2;
@@ -269,9 +283,26 @@ onMounted(() => {
   }
 
   &:nth-child(4) {
+    flex: 1.6;
+  }
+
+  &:nth-child(5) {
     flex: 1;
   }
+}
 
+/* 单元格文本:最多两行展示,超出省略 */
+.cell-value {
+  display: -webkit-box;
+  width: 100%;
+  max-height: 40px;
+  line-height: 20px;
+  font-size: 13px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  word-break: break-word;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
 }
 
 /* "详情"操作按钮样式 */

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

@@ -12,10 +12,11 @@
         </div>
         <div class="table-content">
           <div class="content-item" v-for="(item, index) in tableData" :key="index">
-            <div class="item-text">{{ item.strusername }}</div>
-            <div class="item-text">{{ item.devicename }} </div>
-            <div class="item-text item-text1">{{ item.des }}</div>
-            <div class="item-text">{{ item.updateTime }} </div>
+            <div class="item-text" :title="item.username">{{ item.username }}</div>
+            <div class="item-text" :title="item.devicename">{{ item.devicename }}</div>
+            <div class="item-text item-text1" :title="item.strremark">{{ item.strremark }}</div>
+            <div class="item-text" :title="item.ip">{{ item.ip }}</div>
+            <div class="item-text" :title="item.updateTime">{{ item.updateTime }}</div>
           </div>
         </div>
       </div>
@@ -27,21 +28,11 @@
 <script setup lang="ts">
 import { onMounted, onUnmounted, reactive, ref } from 'vue';
 import {deviceSetLog} from '../hsqHome.api'
+import {navList,titleList} from '../hsqHome.data'
 
 //当前激活nav选项
 let activeIndex = ref(0)
 let devicetype=ref('duaSpeCamera')
-let navList = ref<any[]>([
-  { label: '光谱摄像机', value: 'duaSpeCamera' },
-  { label: '火灾探测器', value: 'imgFireDet' }
-])
-let titleList = ref<any[]>([
-  { label: '用户', value: 'strusername' },
-  { label: '操作设备', value: 'devicename' },
-  { label: '操作记录', value: 'des' },
-  { label: '登录IP', value: 'ip' },
-  { label: '时间', value: 'updateTime' },
-]);
 let tableData = ref<any[]>([]);
 let timer: null | NodeJS.Timeout = null;
 /**
@@ -56,7 +47,7 @@ function getMonitor(flag?: boolean) {
       timer = null;
     }
     getMonitor();
-  }, flag ? 0 : 30000);
+  }, flag ? 0 : 10000);
 }
 //nav选项切换
 function changeNav(item, index) {
@@ -161,45 +152,49 @@ onUnmounted(()=>{
   .basic-list-content {
     width: 100%;
     height: calc(100% - 42px);
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
   }
 
   .content-title {
-    display: flex;
-    justify-content: space-between;
+    display: grid;
+    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1.5fr);
+    column-gap: 4px;
     align-items: center;
+    flex-shrink: 0;
     height: 34px;
     background: var(--image-box-bg2) no-repeat;
     background-size: 100% 100%;
     margin-bottom: 6px;
+    padding: 0 4px;
+    box-sizing: border-box;
   }
 
   .title-item {
-    display: flex;
-    flex: 1;
-    justify-content: center;
-    align-items: center;
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    text-align: center;
     color: #01fefc;
-
-    &:nth-child(2) {
-      flex: 1.5;
-    }
-
-    &:nth-child(3) {
-      flex: 1.5;
-    }
   }
 
   .table-content {
-    height: calc(100% - 40px);
+    flex: 1;
+    min-height: 0;
     overflow-y: auto;
   }
 
   .content-item {
-    display: flex;
-    justify-content: space-between;
+    display: grid;
+    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1.5fr);
+    column-gap: 4px;
     align-items: center;
-    height: 36px;
-    font-size: 14px;
+    min-height: 52px;
+    font-size: 13px;
+    padding: 6px 4px;
+    box-sizing: border-box;
 
     &:nth-child(odd) {
       background-color: #0e3455;
@@ -211,18 +206,16 @@ onUnmounted(()=>{
   }
 
   .item-text {
-    display: flex;
-    flex: 1;
-    justify-content: center;
-    align-items: center;
-
-    &:nth-child(2) {
-      flex: 1.5;
-    }
-
-    &:nth-child(3) {
-      flex: 1.5;
-    }
+    min-width: 0;
+    text-align: center;
+    display: -webkit-box;
+    max-height: 40px;
+    line-height: 20px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-word;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
   }
 
   .item-text1 {

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

@@ -15,7 +15,7 @@ let menuList = ref<any[]>([
   { label: '监测预警', value: 'jcyj' },
   { label: '视频预览', value: 'spyl' },
   { label: '设备管理', value: 'sbgl' },
-  { label: '联动配置', value: 'ldpz' },
+  // { label: '联动配置', value: 'ldpz' },
   { label: '系统管理', value: 'xtgl' },
 ]);
 let $emit = defineEmits(['changeMenu']);

+ 5 - 22
src/views/vent/monitorManager/hsqHome/fireMonitor.vue

@@ -32,7 +32,7 @@
       <div class="box-top1">
         <CommonBd title="报警监控">
           <!-- <AlarmMonitor :option="alarmOption" :cardData="alarmData"></AlarmMonitor> -->
-          <AlarmMonitor ></AlarmMonitor>
+          <AlarmMonitor></AlarmMonitor>
         </CommonBd>
       </div>
       <div class="box-cen1">
@@ -41,7 +41,6 @@
         </CommonBd> -->
         <CommonBd title="历史温度曲线">
           <HistoryLine :deviceOptions="deviceOptions"></HistoryLine>
-          <!-- <CustomChart :chart-config="chartConfig" :chart-data="chartData"></CustomChart> -->
         </CommonBd>
       </div>
       <div class="box-bot1">
@@ -60,7 +59,7 @@ import CommonTable from './components/CommonTable.vue';
 import AlarmMonitor from './components/AlarmMonitor.vue';
 import HistoryLine from './components/HistoryLine.vue';
 import EnvironmentMonitor from './components/EnvironmentMonitor.vue';
-import QuickAction from './components/QuickAction.vue';
+// import QuickAction from './components/QuickAction.vue';
 import dwgViewer from '../../cad/dwgViewerTest.vue';
 import { option, chartConfig, deviceColumns, quickOption, btnOption, alarmOption, chartTempConfig, } from './hsqHome.data';
 import { managesysList, monitorSystem, deviceList, monitorDevice, getManagesysDeviceNum } from './hsqHome.api';
@@ -75,21 +74,12 @@ let tableData = ref<any[]>([])
 let tableDataGp = ref<any[]>([])
 /** 火灾探测器表格数据 */
 let tableDataHz = ref<any[]>([])
-// /** 实时温度曲线图表数据 */
-// let chartData = ref<any[]>([])
 /** 历史温度曲线设备选项配置 */
 let deviceOptions = ref<any[]>([])
 /** 环境监测设备选项配置 */
 let envOption = ref<any[]>([])
 /** 环境监测数据 */
 let envData = reactive({})
-
-/** 快捷操作面板数据 */
-let quickData = reactive({
-  time: '8天6小时32分18秒',
-  sjsx: '实时',
-  wlyc: '12s',
-})
 /** 定时获取监测数据定时器 */
 let timer: null | NodeJS.Timeout = null;
 
@@ -108,7 +98,7 @@ function getMonitor(flag?: boolean) {
       timer = null;
     }
     getMonitor();
-  }, flag ? 0 : 30000);
+  }, flag ? 0 : 10000);
 }
 
 /**
@@ -127,19 +117,13 @@ async function getManagesysDeviceNumTj() {
  */
 async function getTableData() {
   const res = await monitorSystem({ type: 'all', systemID: systemId.value, devicetype: '' });
-  tableData.value = [...res.deviceInfo.duaSpeCamera.datalist, ...res.deviceInfo.imgFireDet.datalist]
   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)) || [];
-  // chartData.value = tableData.value.map(v => ({
-  //   pos: v.strname,
-  //   value: v.readData.temperature
-  // }))
-  deviceOptions.value = tableData.value.map(v => ({
+  tableData.value = tableDataGp.value
+  deviceOptions.value = tableDataGp.value.map(v => ({
     label: v.strname,
     value: v.deviceID,
     devicetype: v.deviceType
   }))
-  // console.log(chartData.value, 'chartData.value')
 }
 
 /**
@@ -177,7 +161,6 @@ onMounted(async () => {
   getMonitor(true);
 });
 
-
 /**
  * 组件卸载前
  * 清理定时器,停止轮询获取监测数据

+ 39 - 19
src/views/vent/monitorManager/hsqHome/hsqHome.data.ts

@@ -67,7 +67,7 @@ export let deviceColumns: any[] = [
   {
     name: '温度(‌℃‌)',
     //prop: 'syswarnLevel',
-    prop: 'temperature'
+    prop: 'avg'
   },
 ]
 
@@ -117,16 +117,16 @@ export let chartTempConfig: any = {
 
 
 //export let envOption = [
-//  { label: '环境温度', value: 'tempHj', unit: '℃' },
-//  { label: '风速风向', value: 'fsfx', unit: 'm/s' },
-//  { label: '温度', value: 'temp', unit: '%' },
-//  { label: 'CO浓度', value: 'coval', unit: 'ppm' },
-//  { label: 'CO2浓度', value: 'co2val', unit: 'ppm' },
-//  { label: 'O2浓度', value: 'o2val', unit: '%' },
-//  { label: 'Ch4浓度', value: 'ch4val', unit: '%' },
-//  { label: '粉尘浓度', value: 'fcnd', unit: 'mg/m³' },
-//  { label: '烟雾指数', value: 'ywzs', unit: '' },
-//  { label: '地表温度', value: 'tempDb', unit: '℃' },
+// { label: '环境温度', value: 'tempHj', unit: '℃' },
+// { label: '风速风向', value: 'fsfx', unit: 'm/s' },
+// { label: '温度', value: 'temp', unit: '%' },
+// { label: 'CO浓度', value: 'coval', unit: 'ppm' },
+// { label: 'CO2浓度', value: 'co2val', unit: 'ppm' },
+// { label: 'O2浓度', value: 'o2val', unit: '%' },
+// { label: 'Ch4浓度', value: 'ch4val', unit: '%' },
+// { label: '粉尘浓度', value: 'fcnd', unit: 'mg/m³' },
+// { label: '烟雾指数', value: 'ywzs', unit: '' },
+// { label: '地表温度', value: 'tempDb', unit: '℃' },
 //]
 
 //用户列表
@@ -180,8 +180,9 @@ export let dayLoginConfig: any = {
 //报警列表表头
 export let warnTitle = [
   { label: '设备名称', value: 'deviceName' },
-  { label: '安装位置', value: 'devicePos' },
   { label: '报警类型', value: 'eventTypeC' },
+  { label: '模式', value: 'mode_dictText' },
+  { label: '时间', value: 'createTime' },
   { label: '操作', value: 'operation' },
 ]
 
@@ -190,14 +191,18 @@ export let alarmColumns: any[] = [
     name: '设备名称',
     prop: 'deviceName',
   },
-  {
-    name: '安装位置',
-    prop: 'devicePos',
-  },
   {
     name: '报警类型',
     prop: 'eventTypeC',
   },
+  {
+    name: '模式',
+    prop: 'mode_dictText',
+  },
+  {
+    name: '时间',
+    prop: 'createTime',
+  },
 ]
 export let dscAlarmLogColumns: any[] = [
   {
@@ -342,15 +347,18 @@ export const tabsPtz: TabItem[] = [
 export const lensControls: LensControl[] = [
   {
     key: 'zoom',
-    icon: `<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="10" cy="10" r="6" fill="none" stroke="#fff" stroke-width="1.6"/><path d="M14.5 14.5 L20 20" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><path d="M10 7.5 V12.5 M7.5 10 H12.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>`,
+    icon: `<svg viewBox="0 0 24 24" width="18"
+  height="18"><circle cx="10" cy="10" r="6" fill="none" stroke="#fff" stroke-width="1.6"/><path d="M14.5 14.5 L20 20" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><path d="M10 7.5 V12.5 M7.5 10 H12.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>`,
   },
   {
     key: 'focus',
-    icon: `<svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="7" width="12" height="10" rx="1.5" fill="none" stroke="#fff" stroke-width="1.5"/><circle cx="9" cy="12" r="2.5" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M15 9.5 L21 7 V17 L15 14.5 Z" fill="none" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>`,
+    icon: `<svg viewBox="0 0 24 24" width="18"
+  height="18"><rect x="3" y="7" width="12" height="10" rx="1.5" fill="none" stroke="#fff" stroke-width="1.5"/><circle cx="9" cy="12" r="2.5" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M15 9.5 L21 7 V17 L15 14.5 Z" fill="none" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>`,
   },
   {
     key: 'iris',
-    icon: `<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="8" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M12 4 L14.5 10.5 L21 12 L14.5 13.5 L12 20 L9.5 13.5 L3 12 L9.5 10.5 Z" fill="none" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
+    icon: `<svg viewBox="0 0 24 24" width="18"
+  height="18"><circle cx="12" cy="12" r="8" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M12 4 L14.5 10.5 L21 12 L14.5 13.5 L12 20 L9.5 13.5 L3 12 L9.5 10.5 Z" fill="none" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
   },
 ]
 
@@ -368,3 +376,15 @@ export const actionBtns = [
   { key: 4, label: '现场处置' },
   { key: 5, label: '确认解除' },
 ]
+
+export let navList =[
+  { label: '光谱摄像机', value: 'duaSpeCamera' },
+  { label: '火灾探测器', value: 'imgFireDet' }
+]
+export let titleList =[
+  { label: '用户', value: 'username' },
+  { label: '操作设备', value: 'devicename' },
+  { label: '操作记录', value: 'strremark' },
+  { label: '登录IP', value: 'ip' },
+  { label: '时间', value: 'updateTime' },
+]

+ 1 - 1
src/views/vent/monitorManager/hsqHome/index.vue

@@ -42,7 +42,7 @@ interface IComponentReference extends Ref<Component> { }
 // 当前激活的菜单项
 const activeIndex = ref<number>(0);
 // 主标题
-const mainTitle = ref<string>('红沙泉露天煤矿火灾监测预警系统');
+const mainTitle = ref<string>('红沙泉二矿火区监测预警系统');
 //当前激活界面
 const activeComponente = ref<Component>(fireMonitor) as IComponentReference;
 /** 菜单 key 与组件的映射表,用于根据导航菜单项动态切换显示组件 */

+ 71 - 10
src/views/vent/monitorManager/hsqHome/systemManger.vue

@@ -4,7 +4,14 @@
     <!-- 上部区域:用户列表 + 角色用户数量统计 -->
     <div class="basic-top-box">
       <div class="system-box-t">
-        <SystemLeftTop :option="optionUser" :data="listData"></SystemLeftTop>
+        <SystemLeftTop
+          :option="optionUser"
+          :data="listData"
+          :page-no="userPageNo"
+          :page-size="userPageSize"
+          :total="userTotal"
+          @page-change="handleUserPageChange"
+        ></SystemLeftTop>
       </div>
       <div class="system-box-t">
         <SystemRightTop :userInfo="userInfo"></SystemRightTop>
@@ -13,11 +20,29 @@
     <!-- 下部区域:登录日志、浏览日志 -->
     <div class="basic-bot-box">
       <div class="system-box-b">
-        <SystemLeftBottom title="登录日志"  :titleOption="titleOption" :data="logList" >
+        <SystemLeftBottom
+          title="登录日志"
+          :titleOption="titleOption"
+          :data="logList"
+          show-pagination
+          :page-no="logPageNo"
+          :page-size="logPageSize"
+          :total="logTotal"
+          @page-change="handleLogPageChange"
+        >
         </SystemLeftBottom>
       </div>
       <div class="system-box-b">
-          <SystemLeftBottom title="浏览日志" :titleOption="titleLookOption" :data="lookLogList" >
+        <SystemLeftBottom
+          title="浏览日志"
+          :titleOption="titleLookOption"
+          :data="lookLogList"
+          show-pagination
+          :page-no="lookLogPageNo"
+          :page-size="lookLogPageSize"
+          :total="lookLogTotal"
+          @page-change="handleLookLogPageChange"
+        >
         </SystemLeftBottom>
       </div>
       <!-- <div class="system-box-b">
@@ -40,10 +65,22 @@ import { optionUser, titleOption, titleLookOption } from './hsqHome.data.js'
 
 /** 用户列表数据,供左上角用户表格展示 */
 let listData = ref([])
+/** 用户列表分页 */
+const userPageNo = ref(1)
+const userPageSize = ref(10)
+const userTotal = ref(0)
 /** 登录日志列表(logType=1) */
 let logList = ref([])
+/** 登录日志分页 */
+const logPageNo = ref(1)
+const logPageSize = ref(10)
+const logTotal = ref(0)
 /** 浏览日志列表(logType=3) */
 let lookLogList = ref([])
+/** 浏览日志分页 */
+const lookLogPageNo = ref(1)
+const lookLogPageSize = ref(10)
+const lookLogTotal = ref(0)
 // let searchData = reactive({
 //   dayEnd: dayjs().format('YYYY-MM-DD') ,
 //   dayStart:dayjs().startOf('date').subtract(7, 'day').format('YYYY-MM-DD'),
@@ -52,21 +89,45 @@ let lookLogList = ref([])
 let userInfo = ref<any[]>([])
 
 /** 获取用户列表 */
-async function getUserList() {
-  let res = await userList({ order: 'desc', pageNo: 1, pageSize: 1000 })
+async function getUserList(pageNo = userPageNo.value, pageSize = userPageSize.value) {
+  const res = await userList({ order: 'desc', pageNo, pageSize })
   listData.value = res.records || []
+  userTotal.value = res.total ?? res.records?.length ?? 0
+  userPageNo.value = pageNo
+  userPageSize.value = pageSize
+}
+
+/** 用户列表分页切换 */
+function handleUserPageChange(pageNo: number, pageSize: number) {
+  getUserList(pageNo, pageSize)
 }
 
 /** 获取登录日志(logType: 1-登录 2-操作 3-浏览) */
-async function getLogList() {
-  let res = await LongList({ logType: 1, pageNo: 1, pageSize: 1000 })
+async function getLogList(pageNo = logPageNo.value, pageSize = logPageSize.value) {
+  const res = await LongList({ logType: 1, pageNo, pageSize })
   logList.value = res.records || []
+  logTotal.value = res.total ?? res.records?.length ?? 0
+  logPageNo.value = pageNo
+  logPageSize.value = pageSize
+}
+
+/** 登录日志分页切换 */
+function handleLogPageChange(pageNo: number, pageSize: number) {
+  getLogList(pageNo, pageSize)
 }
 
-/** 获取浏览日志 */
-async function getLookLogList() {
-  let res = await LongList({ logType: 3, pageNo: 1, pageSize: 1000 })
+/** 获取浏览日志(logType: 3-浏览) */
+async function getLookLogList(pageNo = lookLogPageNo.value, pageSize = lookLogPageSize.value) {
+  const res = await LongList({ logType: 3, pageNo, pageSize })
   lookLogList.value = res.records || []
+  lookLogTotal.value = res.total ?? res.records?.length ?? 0
+  lookLogPageNo.value = pageNo
+  lookLogPageSize.value = pageSize
+}
+
+/** 浏览日志分页切换 */
+function handleLookLogPageChange(pageNo: number, pageSize: number) {
+  getLookLogList(pageNo, pageSize)
 }
 // //日志选项切换
 // function changeTab(param) {

+ 130 - 38
src/views/vent/monitorManager/hsqHome/videoPlayer.vue

@@ -5,7 +5,7 @@
     <div class="left-box">
       <!-- 左上:设备树组件 -->
       <div class="basic-top-left">
-        <DeviceTree :treeData="treeData" />
+        <DeviceTree :treeData="treeData" @checkChange="onCheckChange" />
       </div>
       <!-- 左下:设备控制组件 -->
       <div class="basic-bottom-left">
@@ -16,7 +16,7 @@
     <div class="right-box">
       <!-- 右上:设备画面组件 -->
       <div class="basic-top-right">
-        <DeviceView :deviceId="deviceId" />
+        <DeviceView ref="deviceViewRef" :deviceId="deviceId" />
       </div>
       <!-- 右下:设备告警列表组件 -->
       <div class="basic-bottom-right">
@@ -27,7 +27,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { onMounted, onUnmounted, ref } from 'vue'
 
 // 子组件导入
 import DeviceTree from './components/DeviceTree.vue' // 设备树
@@ -60,9 +60,27 @@ const deviceOptions = ref<any[]>([])
 const warnList = ref<any[]>([])
 // 当前选中的设备ID
 const deviceId = ref('')
+/** 设备画面组件引用 */
+const deviceViewRef = ref<InstanceType<typeof DeviceView> | null>(null)
 /** 定时获取监测数据定时器 */
 let timer: null | NodeJS.Timeout = null;
 
+/**
+ * 设备树勾选变更
+ * - 可见光取流(index === 0):当前页前 4 路播放、后 4 路停止
+ * - 红外取流(index === 1):当前页前 4 路停止、后 4 路播放
+ */
+function onCheckChange(payload: { device: DeviceNode; index: number; checked: boolean }) {
+  console.log('设备树勾选变更', payload)
+  deviceId.value = payload.device.id || ''
+  if (!payload.checked) return
+  if (payload.index === 0) {
+    deviceViewRef.value?.enableVisibleLightPlayMode?.()
+  } else if (payload.index === 1) {
+    deviceViewRef.value?.enableInfraredPlayMode?.()
+  }
+}
+
 /**
  * 定时获取监测数据
  * 通过定时器循环调用数据获取方法,更新表格数据
@@ -77,7 +95,7 @@ function getMonitor(flag?: boolean) {
       timer = null;
     }
     getMonitor();
-  }, flag ? 0 : 30000);
+  }, flag ? 0 : 10000);
 }
 
 /**
@@ -99,40 +117,106 @@ async function getManagesysList() {
  */
 async function getTableData() {
   const res = await monitorSystem({ type: 'all', systemID: systemId.value, devicetype: '' });
-  if (res.deviceInfo.duaSpeCamera.datalist.length > 0 && res.deviceInfo.imgFireDet.datalist.length > 0) {
-    let data = [...res.deviceInfo.duaSpeCamera.datalist, ...res.deviceInfo.imgFireDet.datalist]
-    treeData.value = [
-      {
-        id: 'g1',
-        title: '双光谱摄像机',
-        children: res.deviceInfo.duaSpeCamera.datalist.map(d => {
-          return {
-            id: d.deviceID,
-            name: d.strname,
-            checks: [false, false, false]
-          }
-        })
-      },
-      {
-        id: 'g2',
-        title: '火灾探测器',
-        children: res.deviceInfo.imgFireDet.datalist.map(i => {
-          return {
-            id: i.deviceID,
-            name: i.strname,
-            checks: [false, false, false]
-          }
-        })
-      },
-    ]
-    console.log(treeData.value, 'treeData.value')
-    deviceOptions.value = data.map(d => ({
-      value: d.deviceID,
-      label: d.strname,
-      deviceType:d.deviceType
-    }))
-  }
+  // if (res.deviceInfo.duaSpeCamera.datalist.length > 0 && res.deviceInfo.imgFireDet.datalist.length > 0) {
+  //   let data = [...res.deviceInfo.duaSpeCamera.datalist, ...res.deviceInfo.imgFireDet.datalist]
+  //   treeData.value = [
+  //     {
+  //       id: 'g1',
+  //       title: '双光谱摄像机',
+  //       children: res.deviceInfo.duaSpeCamera.datalist.map(d => {
+  //         return {
+  //           id: d.deviceID,
+  //           name: d.strname,
+  //           checks: [false, false, false]
+  //         }
+  //       })
+  //     },
+  //     {
+  //       id: 'g2',
+  //       title: '火灾探测器',
+  //       children: res.deviceInfo.imgFireDet.datalist.map(i => {
+  //         return {
+  //           id: i.deviceID,
+  //           name: i.strname,
+  //           checks: [false, false, false]
+  //         }
+  //       })
+  //     },
+  //   ]
+  //   deviceOptions.value = data.map(d => ({
+  //     value: d.deviceID,
+  //     label: d.strname,
+  //     deviceType:d.deviceType
+  //   }))
+  // }else if(res.deviceInfo.duaSpeCamera.datalist.length){
+  //   let data = res.deviceInfo.duaSpeCamera.datalist.map(el => Object.assign({}, el, el.readData)) || [];
+  //   treeData.value = [
+  //     {
+  //       id: 'g1',
+  //       title: '双光谱摄像机',
+  //       children: data.map(d => {
+  //         return {
+  //           id: d.deviceID,
+  //           name: d.strname,
+  //           checks: [false, false, false]
+  //         }
+  //       })
+  //     },
+  //   ]
+  //   deviceOptions.value = data.map(d => ({
+  //     value: d.deviceID,
+  //     label: d.strname,
+  //     deviceType:d.deviceType
+  //   }))
+  // }else if(res.deviceInfo.imgFireDet.datalist.length){
+  //   let data = res.deviceInfo.imgFireDet.datalist.map(el => Object.assign({}, el, el.readData)) || [];
+  //   treeData.value = [
+  //     {
+  //       id: 'g2',
+  //       title: '火灾探测器',
+  //       children: data.map(i => {
+  //         return {
+  //           id: i.deviceID,
+  //           name: i.strname,
+  //           checks: [false, false, false]
+  //         }
+  //       })
+  //     },
+  //   ]
+  //   deviceOptions.value = data.map(d => ({
+  //     value: d.deviceID,
+  //     label: d.strname,
+  //     deviceType:d.deviceType
+  //   }))
+  // }
 
+  let data = res.deviceInfo.duaSpeCamera.datalist.map(el => Object.assign({}, el, el.readData)) || [];
+  // 定时刷新时保留已有勾选状态,避免重置
+  const checkStateMap = new Map<string, [boolean, boolean]>()
+  treeData.value.forEach((group) => {
+    group.children?.forEach((device) => {
+      checkStateMap.set(device.id, [!!device.checks?.[0], !!device.checks?.[1]])
+    })
+  })
+  treeData.value = [
+    {
+      id: 'g1',
+      title: '双光谱摄像机',
+      children: data.map(d => {
+        const prevChecks = checkStateMap.get(d.deviceID)
+        return {
+          id: d.deviceID,
+          name: d.strname,
+          checks: prevChecks ? [...prevChecks] : [false, false],
+        }
+      })
+    },
+  ]
+  deviceOptions.value = data.map(d => ({
+    value: d.deviceID,
+    label: d.strname,
+    deviceType: d.deviceType
+  }))
 }
 
 /**
@@ -145,7 +229,7 @@ async function getDscAlarmLogList() {
   if (res && res.records) {
     warnList.value = res.records.map(el => ({
       ...el,
-      eventTypeC: el.eventType === 10001 ? '热源报警' : el.eventType==10002 ? '闯入报警' : '',
+      eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
     }))
   }
 }
@@ -154,6 +238,14 @@ onMounted(async () => {
   await getManagesysList();
   getMonitor(true);
 })
+/**
+ * 组件卸载前
+ * 清理定时器,停止轮询获取监测数据
+ */
+onUnmounted(() => {
+  timer = null
+  clearTimeout(timer);
+});
 </script>
 
 <style lang="less" scoped>

+ 15 - 38
src/views/vent/monitorManager/hsqHome/warnMonitor.vue

@@ -5,7 +5,7 @@
     <div class="warn-top-box">
       <!-- 左侧面板:告警列表 -->
       <div class="baisc-box">
-        <WarnLeftTop  @detail="handlerDetail"></WarnLeftTop>
+        <WarnLeftTop @detail="handlerDetail" @time-change="handlerTimeChange"></WarnLeftTop>
       </div>
       <!-- 中间面板:告警详情展示 -->
       <div class="baisc-box">
@@ -18,33 +18,28 @@
     </div>
     <!-- 底部区域:告警趋势/历史记录 -->
     <div class="warn-bot-box">
-      <WarnBottom></WarnBottom>
+      <WarnBottom :deviceData="warnData" :timeRange="alarmTimeRange"></WarnBottom>
     </div>
   </div>
 </template>
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, computed, nextTick, onBeforeMount, watch } from 'vue';
+import { ref } from 'vue';
 import WarnLeftTop from './components/WarnLeftTop.vue';
 import WarnCenTop from './components/WarnCenTop.vue'
 import WarnRightTop from './components/WarnRightTop.vue'
 import WarnBottom from './components/WarnBottom.vue'
-import { autoLogList, managesysList } from './hsqHome.api'
+import dayjs from 'dayjs'
+// import { autoLogList, managesysList } from './hsqHome.api'
 
+const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 
 // 当前选中的告警详情数据
 let warnData = ref({})
-// // 系统ID,用于筛选数据
-// let systemId = ref('')
-
-
-// /**
-//  * 获取告警列表数据
-//  * 根据systemId筛选告警记录,最多返回100条
-//  */
-// async function getWarnList() {
-//   let res = await autoLogList({ pageNum: 1, pageSize: 100, sysId: systemId.value });
-//   tableData.value = res.records || []
-// }
+/** 报警信息模块所选时间范围,同步至温度趋势统计 */
+const alarmTimeRange = ref<[string, string]>([
+  dayjs().startOf('day').format(DATE_FORMAT),
+  dayjs().format(DATE_FORMAT),
+])
 
 /**
  * 处理告警详情点击事件
@@ -56,28 +51,10 @@ function handlerDetail(item: any) {
   warnData.value = item
 }
 
-/**
- * 获取系统列表并设置默认系统ID
- * 用于初始化时获取systemId,供后续接口调用使用
- */
-// async function getManagesysList() {
-//   const res = await managesysList({ strtype: 'sys_openair_fire', pagetype: 'normal' });
-//   if (res && res.records) {
-//     systemId.value = res.records[0].id
-//   }
-// }
-
-// 页面加载时初始化数据并启动定时轮询
-onMounted(async() => {
-  //  await getManagesysList();
-   
-});
-
-// 页面卸载时清理定时器,防止内存泄漏
-onUnmounted(() => {
-  // timer = null
-  // clearTimeout(timer);
-});
+/** 同步报警信息时间范围 */
+function handlerTimeChange(range: [string, string]) {
+  alarmTimeRange.value = range
+}
 </script>
 
 <style lang="less" scoped>

+ 2 - 1
vite.config.ts

@@ -73,7 +73,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     },
     build: {
       minify: 'esbuild',
-      target: 'es2020',
+      // es2022 原生支持 top-level await,避免 vite-plugin-top-level-await 与 @swc/core 不兼容
+      target: 'es2022',
       cssTarget: 'chrome80',
       outDir: OUTPUT_DIR,
       rollupOptions: {