index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. <script setup>
  2. import { defineProps, onMounted, ref, watch, onUnmounted, h } from 'vue';
  3. import { RouterView, useRoute, useRouter } from 'vue-router';
  4. import { SlackCircleFilled, FullscreenOutlined, CaretDownOutlined, VideoCameraFilled } from '@ant-design/icons-vue';
  5. import { findAllChannelsByUserAPI, getOrganCameraListAPI, getWeatherAPi } from '@/api/layout/index.js';
  6. import { useMechanismStore } from '@/store/index.js';
  7. import { toggle } from '@/utils/full.js';
  8. import { message, notification } from 'ant-design-vue';
  9. import $bus from '@/utils/mitt.js';
  10. const store = useMechanismStore();
  11. const router = useRouter();
  12. const route = useRoute();
  13. const isGrid = ref(localStorage.getItem('isGrid'));
  14. const routerList = ref({});
  15. const routerListShow = ref(false);
  16. const findAllChannelsByUserList = ref([]);
  17. const findAllChannelsByUserListShow = ref(false);
  18. const openPages = () => {
  19. routerListShow.value = !routerListShow.value;
  20. //routerList.value = router.getRoutes();
  21. routerList.value = [
  22. {
  23. meta: { title: '数据总览' },
  24. path: '/'
  25. },
  26. {
  27. meta: { title: '健康数据' },
  28. path: '/health_data'
  29. },
  30. {
  31. meta: { title: '报警数据' },
  32. path: '/alarm_data'
  33. },
  34. {
  35. meta: { title: '个人中心' },
  36. path: '/personal_center'
  37. },
  38. {
  39. meta: { title: '体征数据' },
  40. path: '/sign_data'
  41. }
  42. ];
  43. };
  44. const selectNowRouter = item => {
  45. routerListShow.value = false;
  46. router.push(item.path);
  47. };
  48. const title = ref(0);
  49. watch(store, val => {
  50. title.value = route.query.title ? route.query.title : '';
  51. });
  52. // 监听路由变化,处理URL参数中的机构信息
  53. watch(
  54. () => route.query,
  55. (newQuery, oldQuery) => {
  56. // 当tenantId或tenantName变化时,重新处理
  57. if (newQuery.tenantId && newQuery.tenantName) {
  58. handleTenantFromQuery();
  59. }
  60. },
  61. { immediate: false }
  62. );
  63. const findAllChannelsByUserParams = ref({
  64. pageNum: 1,
  65. pageSize: 20
  66. });
  67. const findAllChannelsByUserLoading = ref(false);
  68. const weather_info = ref({});
  69. // 获取天气
  70. function get_weatherinfofun() {
  71. getWeatherAPi().then(res => {
  72. console.log(res);
  73. if (res.data.weather) {
  74. weather_info.value = res.data.weather[0];
  75. }
  76. });
  77. }
  78. // 获取机构
  79. function findAllChannelsByUser() {
  80. if (findAllChannelsByUserLoading.value) return;
  81. findAllChannelsByUserLoading.value = true;
  82. findAllChannelsByUserAPI({}).then(res => {
  83. findAllChannelsByUserList.value = res.data;
  84. // if (!store.$state.mechanismData) {
  85. // console.log(findAllChannelsByUserList.value[0]);
  86. // selectFindAllChannelsByUser(findAllChannelsByUserList.value[0]);
  87. // }
  88. // if (res.data.length === 0) {
  89. // findAllChannelsByUserParams.value.pageNum;
  90. // }
  91. // findAllChannelsByUserList.value = findAllChannelsByUserList.value.concat(
  92. // res.data
  93. // );
  94. findAllChannelsByUserLoading.value = false;
  95. });
  96. }
  97. const openFindAllChannelsByUserList = () => {
  98. findAllChannelsByUserListShow.value = !findAllChannelsByUserListShow.value;
  99. };
  100. const selectFindAllChannelsByUser = item => {
  101. store.setMechanismData(item);
  102. console.log(item);
  103. findAllChannelsByUserListShow.value = false;
  104. const tentantId = item.tentantId;
  105. const title = item.tentantName;
  106. window.location.href = '/index/roomdetail?tenantId=' + tentantId + '&title=' + title;
  107. // router.push({
  108. // path: "/index/roomdetail",
  109. // query: {
  110. // tenantId: tentantId,
  111. // },
  112. // });
  113. };
  114. // 处理从URL参数接收的机构信息
  115. const handleTenantFromQuery = () => {
  116. const tenantId = route.query.tenantId;
  117. const tenantName = route.query.tenantName;
  118. // 如果URL中有tenantId和tenantName,说明是从外部系统传入的机构信息
  119. if (tenantId && tenantName) {
  120. localStorage.setItem('isGrid', 1);
  121. isGrid.value = 1;
  122. console.log('从URL接收机构信息:', { tenantId, tenantName });
  123. // 构造机构数据对象,格式与selectFindAllChannelsByUser使用的格式一致
  124. const tenantInfo = {
  125. tentantId: tenantId,
  126. tentantName: tenantName,
  127. groupFlag: 0
  128. };
  129. // 设置到store中
  130. store.setMechanismData(tenantInfo);
  131. // 更新title显示
  132. title.value = tenantName;
  133. // 如果当前不在roomdetail页面,则跳转到roomdetail页面
  134. if (route.path !== '/index/roomdetail') {
  135. router.push({
  136. path: '/index/roomdetail',
  137. query: {
  138. tenantId: tenantId,
  139. title: tenantName
  140. }
  141. });
  142. }
  143. } else {
  144. localStorage.setItem('isGrid', 0);
  145. isGrid.value = 0;
  146. }
  147. };
  148. function jump_indpage() {
  149. if (isGrid.value == 0) {
  150. routerListShow.value = false;
  151. router.push('/index');
  152. }
  153. }
  154. const findAllChannelsByUserListScroll = e => {
  155. if (e.target) {
  156. // 滚动容器的高度
  157. const containerHeight = e.target.clientHeight;
  158. // 滚动内容的总高度
  159. const contentHeight = e.target.scrollHeight - 1;
  160. // 当前滚动位置
  161. const scrollPosition = e.target.scrollTop;
  162. // 判断是否滚动到底部
  163. if (scrollPosition + containerHeight > contentHeight) {
  164. // 在此处可以触发加载更多内容或其他操作
  165. // findAllChannelsByUserParams.value.pageNum++;
  166. // findAllChannelsByUser();
  167. }
  168. }
  169. };
  170. // 远程视频
  171. const openVideo = () => {
  172. getOrganCameraListAPI({
  173. pageNum: 1, //页数
  174. pageSize: 10, //条数
  175. params: {
  176. orgId: store.channelId //机构ID(查询全部传null)
  177. }
  178. }).then(res => {
  179. //console.log(res.data[0].url);
  180. if (res.data.length) {
  181. window.open(res.data[0].url);
  182. } else {
  183. message.error('暂无视频');
  184. }
  185. });
  186. };
  187. onUnmounted(() => {
  188. // 清理所有定时器
  189. stopHeartbeat();
  190. if (reconnectTimeout) {
  191. clearTimeout(reconnectTimeout);
  192. reconnectTimeout = null;
  193. }
  194. // 正常关闭连接
  195. if (socket.value) {
  196. socket.value.close(1000, '页面关闭');
  197. socket.value = null;
  198. }
  199. });
  200. onMounted(() => {
  201. // 优先处理从URL参数传入的机构信息(外部系统传入)
  202. handleTenantFromQuery();
  203. findAllChannelsByUser();
  204. // get_weatherinfofun();
  205. title.value = route.query.title ? route.query.title : '';
  206. // 页面加载后自动连接
  207. setTimeout(() => {
  208. connect();
  209. }, 1000);
  210. // 页面可见性变化处理
  211. // 页面可见性变化处理
  212. document.addEventListener('visibilitychange', () => {
  213. if (document.hidden) {
  214. console.log('页面切换到后台');
  215. } else {
  216. console.log('页面回到前台');
  217. // 检查连接和心跳状态
  218. checkConnectionHealth();
  219. }
  220. });
  221. // 添加定期健康检查
  222. setInterval(() => {
  223. checkConnectionHealth();
  224. }, 30000); // 每30秒检查一次连接健康状态
  225. // 网络状态监测
  226. window.addEventListener('online', () => {
  227. if (!socket.value) {
  228. connect();
  229. }
  230. });
  231. window.addEventListener('offline', () => {
  232. console.log('网络连接断开');
  233. });
  234. });
  235. // websocket设备连接
  236. // 响应式数据
  237. const socket = ref(null);
  238. const isConnecting = ref(false);
  239. const connectionId = ref(null);
  240. const reconnectAttempts = ref(0);
  241. const maxReconnectAttempts = ref(10);
  242. const lastActivityTime = ref('-');
  243. const initTime = ref(new Date().toLocaleTimeString());
  244. const events = ref([]);
  245. // 定时器引用
  246. let heartbeatInterval = null;
  247. let heartbeatTimeout = null;
  248. let reconnectTimeout = null;
  249. let lastActivity = Date.now();
  250. const heartbeatIntervalTime = 25000; // 25秒发送一次心跳
  251. const heartbeatTimeoutTime = 10000; // 10秒心跳响应超时
  252. // 添加心跳状态响应式变量
  253. const heartbeatStatus = ref('normal'); // normal: 正常, waiting: 等待响应, timeout: 超时, expired: 过期
  254. const lastHeartbeatTime = ref(null); // 最后一次发送心跳的时间
  255. const lastHeartbeatAckTime = ref(null); // 最后一次收到心跳响应的时间
  256. // 连接websocket方法
  257. const connect = () => {
  258. // 连接已存在或连接中,跳过重复连接
  259. if (isConnecting.value || socket.value) {
  260. return;
  261. }
  262. isConnecting.value = true;
  263. // 连接中...
  264. // 清除之前的重连定时器
  265. if (reconnectTimeout) {
  266. clearTimeout(reconnectTimeout);
  267. reconnectTimeout = null;
  268. }
  269. try {
  270. const clientId = generateClientId();
  271. // 连接地址: ${wsUrl}
  272. const isGrid = localStorage.getItem('isGrid');
  273. const urlObj = {
  274. 0: import.meta.env.VITE_API_WSS_URL,
  275. 1: import.meta.env.VITE_API_WSS_URL_org
  276. };
  277. const wsUrl = urlObj[isGrid] + clientId;
  278. socket.value = new WebSocket(wsUrl);
  279. socket.value.onopen = handleOpen;
  280. socket.value.onmessage = handleMessage;
  281. socket.value.onclose = handleClose;
  282. socket.value.onerror = handleError;
  283. } catch (error) {
  284. // 连接创建错误: ${error.message}
  285. handleConnectionFailure();
  286. }
  287. };
  288. const sendMessage = message => {
  289. if (socket.value && socket.value.readyState === WebSocket.OPEN) {
  290. try {
  291. socket.value.send(JSON.stringify(message));
  292. lastActivity = Date.now();
  293. updateLastActivity();
  294. return true;
  295. } catch (error) {
  296. // 发送消息失败: ${error.message}
  297. return false;
  298. }
  299. } else {
  300. // 无法发送消息: WebSocket未连接
  301. return false;
  302. }
  303. };
  304. // 连接成功
  305. const handleOpen = event => {
  306. isConnecting.value = false;
  307. reconnectAttempts.value = 0;
  308. lastActivity = Date.now();
  309. // WebSocket连接成功
  310. // 启动心跳机制
  311. startHeartbeat();
  312. // 发送身份验证消息
  313. const isGrid = localStorage.getItem('isGrid');
  314. const objApiMonitor = {
  315. 0: 'instcare-bigscreen',
  316. 1: 'instcare-web-orgadmin'
  317. };
  318. let postData = {
  319. type: 'AUTH',
  320. clientType: objApiMonitor[isGrid],
  321. clientId: generateClientId(),
  322. timestamp: Date.now()
  323. };
  324. if (isGrid == 1) {
  325. postData.organizationId = Number(store.tentantId || 0);
  326. }
  327. console.log('postData', postData);
  328. sendMessage(postData);
  329. };
  330. // 获取到服务器发送的消息
  331. const handleMessage = event => {
  332. try {
  333. lastActivity = Date.now();
  334. updateLastActivity();
  335. const data = JSON.parse(event.data);
  336. processIncomingData(data);
  337. } catch (error) {
  338. console.error('消息解析错误:', error, '原始数据:', event.data);
  339. }
  340. };
  341. // 处理心跳响应
  342. const handleHeartbeatAck = data => {
  343. console.log('心跳消息', data);
  344. // 清除超时检测
  345. if (heartbeatTimeout) {
  346. clearTimeout(heartbeatTimeout);
  347. heartbeatTimeout = null;
  348. }
  349. heartbeatStatus.value = 'normal';
  350. lastHeartbeatAckTime.value = Date.now();
  351. lastActivity = Date.now();
  352. updateLastActivity();
  353. console.log('💓 心跳响应正常');
  354. };
  355. // 新增:连接健康检查
  356. const checkConnectionHealth = () => {
  357. if (!socket.value || socket.value.readyState !== WebSocket.OPEN) {
  358. return;
  359. }
  360. const now = Date.now();
  361. const timeSinceLastActivity = now - lastActivity;
  362. const timeSinceLastHeartbeat = now - (lastHeartbeatTime.value || 0);
  363. const totalTimeout = heartbeatIntervalTime + heartbeatTimeoutTime;
  364. console.log('🔍 连接健康检查:');
  365. console.log('最后活动:', Math.round(timeSinceLastActivity / 1000) + '秒前');
  366. console.log('最后心跳:', Math.round(timeSinceLastHeartbeat / 1000) + '秒前');
  367. console.log('心跳状态:', heartbeatStatus.value);
  368. // 如果超过总超时时间无活动,认为连接已死
  369. if (timeSinceLastActivity > totalTimeout + 10000) {
  370. // 额外给10秒缓冲
  371. console.error('🚨 连接长时间无活动,可能已断开');
  372. heartbeatStatus.value = 'expired';
  373. handleHeartbeatExpired();
  374. return;
  375. }
  376. // 如果心跳等待时间过长,发送测试消息
  377. if (heartbeatStatus.value === 'waiting' && timeSinceLastHeartbeat > heartbeatTimeoutTime + 5000) {
  378. console.warn('⚠️ 心跳响应延迟,发送测试消息');
  379. sendMessage({ type: 'PING', timestamp: now });
  380. }
  381. };
  382. const handleClose = event => {
  383. console.log(`WebSocket连接关闭: 代码 ${event.code}, 原因: ${event.reason || '未知'}`);
  384. isConnecting.value = false;
  385. socket.value = null;
  386. connectionId.value = null;
  387. heartbeatStatus.value = 'expired'; // 连接关闭时标记为过期
  388. // 停止心跳检测
  389. stopHeartbeat();
  390. // 清除可能存在的重连定时器
  391. if (reconnectTimeout) {
  392. clearTimeout(reconnectTimeout);
  393. reconnectTimeout = null;
  394. }
  395. // 判断是否需要重连(非正常关闭且未超过最大重连次数)
  396. if (event.code !== 1000 && reconnectAttempts.value < maxReconnectAttempts.value) {
  397. reconnectAttempts.value++;
  398. const delay = Math.min(3000 * Math.pow(1.5, reconnectAttempts.value - 1), 30000);
  399. console.log(`${Math.round(delay / 1000)}秒后尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts.value})`);
  400. reconnectTimeout = setTimeout(() => {
  401. // 检查是否已经重新连接
  402. if (!socket.value && !isConnecting.value) {
  403. connect();
  404. }
  405. }, delay);
  406. } else if (reconnectAttempts.value >= maxReconnectAttempts.value) {
  407. console.error('已达到最大重连次数,停止自动重连');
  408. heartbeatStatus.value = 'expired';
  409. }
  410. };
  411. const handleError = event => {
  412. console.error('WebSocket错误详情:', event);
  413. };
  414. // 修改心跳检测逻辑
  415. const startHeartbeat = () => {
  416. // 先停止可能存在的旧心跳
  417. stopHeartbeat();
  418. // 初始化心跳状态
  419. heartbeatStatus.value = 'normal';
  420. lastHeartbeatTime.value = Date.now();
  421. // 定时发送心跳
  422. heartbeatInterval = setInterval(() => {
  423. // 检查连接状态
  424. if (!socket.value || socket.value.readyState !== WebSocket.OPEN) {
  425. console.log('连接已断开,停止心跳');
  426. heartbeatStatus.value = 'expired';
  427. stopHeartbeat();
  428. return;
  429. }
  430. // 检查上次心跳是否过期(超过间隔+超时时间未收到响应)
  431. const now = Date.now();
  432. const timeSinceLastHeartbeat = now - lastHeartbeatTime.value;
  433. const totalTimeout = heartbeatIntervalTime + heartbeatTimeoutTime;
  434. if (lastHeartbeatTime.value && timeSinceLastHeartbeat > totalTimeout) {
  435. console.warn(`💔 心跳已过期,距离上次心跳${Math.round(timeSinceLastHeartbeat / 1000)}秒,触发重连`);
  436. heartbeatStatus.value = 'expired';
  437. handleHeartbeatExpired();
  438. return;
  439. }
  440. // 发送心跳
  441. heartbeatStatus.value = 'waiting';
  442. lastHeartbeatTime.value = now;
  443. let params = {
  444. type: 'HEARTBEAT',
  445. timestamp: now,
  446. clientTime: now
  447. };
  448. console.log('params', params);
  449. const success = sendMessage(params);
  450. if (success) {
  451. // 设置心跳超时检测
  452. if (heartbeatTimeout) {
  453. clearTimeout(heartbeatTimeout);
  454. }
  455. heartbeatTimeout = setTimeout(() => {
  456. console.warn('💔 心跳响应超时,连接可能已断开');
  457. heartbeatStatus.value = 'timeout';
  458. handleHeartbeatTimeout();
  459. }, heartbeatTimeoutTime);
  460. } else {
  461. console.warn('心跳发送失败,连接可能有问题');
  462. heartbeatStatus.value = 'timeout';
  463. handleHeartbeatTimeout();
  464. }
  465. }, heartbeatIntervalTime);
  466. };
  467. // 新增:处理心跳过期
  468. const handleHeartbeatExpired = () => {
  469. console.error('🚨 心跳已过期,关闭连接并重新连接');
  470. // 停止所有定时器
  471. stopHeartbeat();
  472. // 关闭当前连接
  473. if (socket.value) {
  474. socket.value.close(1000, '心跳过期');
  475. socket.value = null;
  476. }
  477. // 立即重新连接
  478. reconnectAttempts.value = 0; // 重置重连计数
  479. setTimeout(() => {
  480. if (!socket.value && !isConnecting.value) {
  481. console.log('开始心跳过期重连...');
  482. connect();
  483. }
  484. }, 1000);
  485. };
  486. // 新增:处理心跳超时
  487. const handleHeartbeatTimeout = () => {
  488. console.warn('⏰ 心跳响应超时,关闭连接触发重连');
  489. // 停止心跳检测
  490. stopHeartbeat();
  491. // 主动关闭连接触发重连机制
  492. if (socket.value) {
  493. socket.value.close(1000, '心跳响应超时');
  494. }
  495. };
  496. const stopHeartbeat = () => {
  497. if (heartbeatInterval) {
  498. clearInterval(heartbeatInterval);
  499. heartbeatInterval = null;
  500. }
  501. if (heartbeatTimeout) {
  502. clearTimeout(heartbeatTimeout);
  503. heartbeatTimeout = null;
  504. }
  505. };
  506. const processIncomingData = data => {
  507. if (!data || !data.type) {
  508. // 收到无效消息格式
  509. return;
  510. }
  511. switch (data.type) {
  512. case 'CONNECT_SUCCESS':
  513. handleConnectSuccess(data);
  514. break;
  515. case 'AUTH_SUCCESS':
  516. handleAuthSuccess(data);
  517. break;
  518. case 'SOS_ALERT':
  519. // 需要展示的数据
  520. handleSOSAlert(data);
  521. break;
  522. case 'DEVICE_DATA_UPDATE':
  523. handleDeviceData(data);
  524. break;
  525. case 'SYSTEM_STATS_UPDATE':
  526. handleStatsUpdate(data);
  527. break;
  528. case 'HEARTBEAT_ACK':
  529. handleHeartbeatAck(data);
  530. break;
  531. default:
  532. handleGenericMessage(data);
  533. }
  534. };
  535. const handleConnectSuccess = data => {
  536. console.log('WebSocket连接成功');
  537. // 连接成功,连接ID: ${connectionId.value}
  538. connectionId.value = data.connectionId;
  539. };
  540. const handleAuthSuccess = data => {
  541. console.log('身份验证成功');
  542. };
  543. const handleSOSAlert = alertData => {
  544. console.log('alertData', alertData);
  545. try {
  546. const alert = alertData.data || alertData;
  547. // 发送确认消息
  548. sendMessage({
  549. type: 'SOS_ACK',
  550. alertId: alertData.timestamp,
  551. timestamp: Date.now()
  552. });
  553. notification.warning({
  554. message: h('div', {
  555. style: {
  556. color: '#fff',
  557. // fontSize: '18px'
  558. fontSize: '0.1rem'
  559. },
  560. innerHTML: `
  561. <div>🚨🚨 SOS紧急预警 🚨🚨</div>
  562. `
  563. }),
  564. duration: 10,
  565. // duration: null,
  566. class: 'my-warning-notification',
  567. style: {
  568. background: 'linear-gradient(135deg, #1e4184 0%, #3469e3 100%) !important',
  569. border: '1px solid rgba(42, 157, 143, 0.3) !important',
  570. 'box-shadow': '0 4px 20px rgba(0, 0, 0, 0.5) !important',
  571. color: '#fff !important',
  572. width: '2rem !important'
  573. },
  574. description: h('div', {
  575. style: {
  576. color: '#fff',
  577. fontSize: '0.1rem'
  578. },
  579. innerHTML: `
  580. <div>院区名称: ${alert.organizationName || '未知'}</div>
  581. <div>长者姓名: ${alert.elderName || '未知'}</div>
  582. <div>长者房间: ${alert.roomName || '未知'}</div>
  583. <div>设备类型: ${alert.deviceType || '未知'}</div>
  584. <div>设备电量: ${alert.batteryLevel || '0%'}</div>
  585. <div>时间: ${new Date(alertData.timestamp).toLocaleString()}</div>
  586. `
  587. })
  588. });
  589. $bus.emit('roomDetails', alertData);
  590. // 重新获取信息
  591. // overviewStatistics()
  592. // <div>设备: ${alert.deviceId || '未知'}</div>
  593. // ${alert.location ? `<div>位置: 经度${alert.location.longitude?.toFixed(6)}, 纬度${alert.location.latitude?.toFixed(6)}</div>` : ''}
  594. } catch (error) {
  595. console.error('处理SOS告警错误:', error);
  596. }
  597. };
  598. const handleDeviceData = deviceData => {
  599. console.log('deviceData', deviceData);
  600. };
  601. const handleStatsUpdate = statsData => {
  602. console.log('statsData', statsData);
  603. };
  604. // 修改普通消息处理,不干扰心跳检测
  605. const handleGenericMessage = data => {
  606. console.log('收到普通消息:', data);
  607. // 这里只更新最后活动时间,但不影响心跳超时检测
  608. lastActivity = Date.now();
  609. updateLastActivity();
  610. };
  611. const updateLastActivity = () => {
  612. lastActivityTime.value = new Date().toLocaleTimeString();
  613. };
  614. const generateClientId = () => {
  615. const timestamp = Date.now();
  616. const random = Math.random().toString(36).substr(2, 9);
  617. return `monitor_${timestamp}_${random}`;
  618. };
  619. const handleConnectionFailure = () => {
  620. isConnecting.value = false;
  621. socket.value = null;
  622. // 连接创建失败也尝试重连
  623. if (reconnectAttempts.value < maxReconnectAttempts.value) {
  624. reconnectAttempts.value++;
  625. const delay = Math.min(3000 * Math.pow(1.5, reconnectAttempts.value - 1), 30000);
  626. reconnectTimeout = setTimeout(() => {
  627. if (!socket.value && !isConnecting.value) {
  628. connect();
  629. }
  630. }, delay);
  631. }
  632. };
  633. </script>
  634. <template>
  635. <div>
  636. <div class="weatherbox">
  637. <div class="lfet_box">
  638. <span>{{ weather_info.city }}</span>
  639. <span>{{ weather_info.weather }}</span>
  640. <!-- <span>风力 {{ weather_info.windpower }}级</span>
  641. <span>方向 {{ weather_info.winddirection }}</span> -->
  642. </div>
  643. <div class="rgiht_box">
  644. <!-- <span
  645. >{{ weather_info.temperature }}℃ 空气湿度{{
  646. weather_info.humidity_float
  647. }}</span
  648. > -->
  649. </div>
  650. </div>
  651. <div class="layout">
  652. <!-- <img src="../assets/img/common/bg3.png" alt="" /> -->
  653. <div class="title">
  654. <div class="flex title_item full_screen" @click="toggle">
  655. <FullscreenOutlined />
  656. <div class="title_item_text">全屏</div>
  657. </div>
  658. <div class="flex title_item meat_name" @click="openFindAllChannelsByUserList" v-if="isGrid == 0">
  659. <!--<div class="title_item_text">海珠颐年养老</div> -->
  660. <div class="title_item_text">{{ title ? title : store.name }}</div>
  661. <CaretDownOutlined />
  662. <div class="findAllChannelsByUser-list" v-show="findAllChannelsByUserListShow" @scroll="findAllChannelsByUserListScroll">
  663. <div v-for="item in findAllChannelsByUserList" :key="item">
  664. <div @click.stop="selectFindAllChannelsByUser(item)" v-if="item.tentantName">
  665. {{ item.tentantName }}
  666. </div>
  667. </div>
  668. <!-- <template v-for="item in findAllChannelsByUserList">
  669. <div
  670. @click.stop="selectFindAllChannelsByUser(item)"
  671. v-if="item.sysChannelOrg.orgName">
  672. {{ item.sysChannelOrg.orgName }}
  673. </div>
  674. </template> -->
  675. </div>
  676. </div>
  677. <div class="flex title_item title_item_info">
  678. <!--<SlackCircleFilled/>-->
  679. <div class="title_item_text" @click="jump_indpage">颐年智慧医养数字化平台</div>
  680. </div>
  681. <div class="flex title_item page_type" @click="openPages" v-if="isGrid == 0">
  682. <div class="title_item_text">{{ route.meta.title }}</div>
  683. <CaretDownOutlined />
  684. <div class="router-list" v-show="routerListShow">
  685. <div v-for="item in routerList" :key="item">
  686. <div @click.stop="selectNowRouter(item)" v-if="item.meta.title">
  687. {{ item.meta.title }}
  688. </div>
  689. </div>
  690. </div>
  691. </div>
  692. <div class="flex title_item video_telephone" @click="openVideo">
  693. <VideoCameraFilled />
  694. <div class="title_item_text">远程视频</div>
  695. </div>
  696. </div>
  697. <div class="content">
  698. <RouterView />
  699. </div>
  700. </div>
  701. </div>
  702. </template>
  703. <style lang="scss" scoped>
  704. @import '@/assets/css/layout.css';
  705. .weatherbox {
  706. width: 100%;
  707. z-index: 99;
  708. display: flex;
  709. justify-content: space-between;
  710. position: absolute;
  711. top: 0;
  712. padding: 14px 0;
  713. .lfet_box {
  714. display: flex;
  715. margin-left: 30px;
  716. span {
  717. margin-right: 20px;
  718. color: #f0f0f0;
  719. font-size: 14px;
  720. }
  721. }
  722. .rgiht_box {
  723. display: flex;
  724. margin-right: 30px;
  725. span {
  726. color: #f0f0f0;
  727. font-size: 14px;
  728. }
  729. }
  730. }
  731. .layout {
  732. background-image: url('../assets/img/common/bg3.png');
  733. background-repeat: no-repeat;
  734. background-size: cover;
  735. background-position: center;
  736. .title {
  737. // z-index: 50;
  738. // position: absolute;
  739. // top: 0;
  740. // right: 0;
  741. // left: 0;
  742. height: 120px;
  743. .title_item {
  744. position: absolute;
  745. font-family: MiSans, MiSans;
  746. font-weight: 600;
  747. font-size: 24px;
  748. color: #39e6ad;
  749. display: inline-block;
  750. line-height: 32px;
  751. span {
  752. font-size: 22px;
  753. }
  754. .title_item_text {
  755. margin: 0 5px;
  756. cursor: pointer;
  757. display: inline-block;
  758. }
  759. }
  760. .full_screen {
  761. top: 80px;
  762. left: 190px;
  763. &:hover {
  764. cursor: pointer;
  765. }
  766. }
  767. .meat_name {
  768. top: 65px;
  769. left: 400px;
  770. position: relative;
  771. &:hover {
  772. cursor: pointer;
  773. }
  774. .findAllChannelsByUser-list::-webkit-scrollbar {
  775. background-color: #f0f0f0;
  776. width: 6px;
  777. border-radius: 40px;
  778. height: 10px;
  779. }
  780. .findAllChannelsByUser-list::-webkit-scrollbar-thumb {
  781. background-color: #0284ff;
  782. border-radius: 40px;
  783. }
  784. .findAllChannelsByUser-list {
  785. position: absolute;
  786. top: 45px;
  787. width: 300px;
  788. height: 400px;
  789. z-index: 100;
  790. border-radius: 10px;
  791. background: #0a1a35;
  792. box-sizing: border-box;
  793. border: 1px solid #0284ff;
  794. overflow: auto;
  795. div {
  796. padding: 5px 10px;
  797. box-sizing: border-box;
  798. font-size: 16px;
  799. line-height: 18px;
  800. &:hover {
  801. background: rgba(41, 108, 220, 0.28);
  802. }
  803. }
  804. }
  805. }
  806. .title_item_info {
  807. top: 50px;
  808. left: 0;
  809. right: 0;
  810. width: fit-content;
  811. margin: auto;
  812. font-size: 39px;
  813. letter-spacing: 5px;
  814. span {
  815. font-size: 39px;
  816. }
  817. }
  818. .page_type {
  819. top: 65px;
  820. left: 1380px;
  821. .router-list::-webkit-scrollbar {
  822. background-color: #f0f0f0;
  823. width: 6px;
  824. border-radius: 40px;
  825. height: 10px;
  826. }
  827. .router-list::-webkit-scrollbar-thumb {
  828. background-color: #0284ff;
  829. border-radius: 40px;
  830. }
  831. .router-list {
  832. position: absolute;
  833. top: 45px;
  834. width: 130px;
  835. z-index: 900;
  836. border-radius: 10px;
  837. background: #0a1a35;
  838. box-sizing: border-box;
  839. border: 1px solid #0284ff;
  840. div {
  841. padding: 5px 10px;
  842. line-height: 18px;
  843. box-sizing: border-box;
  844. font-size: 16px;
  845. //border: 1px solid #fff;
  846. }
  847. }
  848. &:hover {
  849. cursor: pointer;
  850. }
  851. }
  852. .video_telephone {
  853. top: 80px;
  854. right: 190px;
  855. &:hover {
  856. cursor: pointer;
  857. }
  858. }
  859. }
  860. .content {
  861. position: absolute;
  862. top: 120px;
  863. left: 0;
  864. right: 0;
  865. bottom: 70px;
  866. z-index: 40;
  867. }
  868. }
  869. </style>