App.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <script lang="ts" setup>
  2. import { isDark } from '@/utils/is'
  3. import { useAppStore } from '@/store/modules/app'
  4. import { useDesign } from '@/hooks/web/useDesign'
  5. import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
  6. import routerSearch from '@/components/RouterSearch/index.vue'
  7. import { useWatermark } from '@/hooks/web/useWatermark'
  8. import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
  9. import { useWebSocket } from '@/utils/useWebSocket'
  10. import { amapReverseGeocode } from '@/utils/amapService'
  11. import { getAccessToken } from '@/utils/auth'
  12. import GlobalStatsNotification from '@/components/GlobalStatsNotification/index.vue'
  13. defineOptions({ name: 'APP' })
  14. // 使用响应式ref来存储token,并通过storage事件监听变化
  15. const getToken = ref(getAccessToken())
  16. const { getPrefixCls } = useDesign()
  17. const prefixCls = getPrefixCls('app')
  18. const appStore = useAppStore()
  19. const currentSize = computed(() => appStore.getCurrentSize)
  20. const greyMode = computed(() => appStore.getGreyMode)
  21. const { wsCache } = useCache()
  22. const { setWatermark } = useWatermark()
  23. // 监听sessionStorage的变化(包括同页面设置的情况)
  24. const handleStorageChange = (e: StorageEvent | null) => {
  25. if (!e || e.key === 'ACCESS_TOKEN') {
  26. getToken.value = getAccessToken()
  27. }
  28. }
  29. // 监听storage事件(跨标签页)
  30. window.addEventListener('storage', handleStorageChange)
  31. // 由于storage事件只在跨标签页时触发,同页面需要手动触发
  32. // 重写setItem方法以监听同页面的变化
  33. const originalSetItem = sessionStorage.setItem.bind(sessionStorage)
  34. sessionStorage.setItem = function(key: string, value: string) {
  35. originalSetItem(key, value)
  36. if (key === 'ACCESS_TOKEN') {
  37. // 手动触发更新
  38. getToken.value = getAccessToken()
  39. }
  40. }
  41. // 重写removeItem方法
  42. const originalRemoveItem = sessionStorage.removeItem.bind(sessionStorage)
  43. sessionStorage.removeItem = function(key: string) {
  44. originalRemoveItem(key)
  45. if (key === 'ACCESS_TOKEN') {
  46. getToken.value = getAccessToken()
  47. }
  48. }
  49. // 根据浏览器当前主题设置系统主题色
  50. const setDefaultTheme = () => {
  51. let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
  52. if (isDarkTheme === null) {
  53. isDarkTheme = isDark()
  54. }
  55. appStore.setIsDark(isDarkTheme)
  56. }
  57. setDefaultTheme()
  58. // 根据浏览器当前是否有设置水印
  59. const seMask = () => {
  60. if(appStore.getWaterMask){
  61. setWatermark(appStore.getWaterMask)
  62. }
  63. }
  64. seMask()
  65. // 全局统计信息弹窗
  66. const statsNotificationVisible = ref(false)
  67. const statsData = ref({
  68. successCount: 0,
  69. failureCount: 0,
  70. failureReasons: []
  71. })
  72. // 显示统计信息弹窗
  73. const showStatsNotification = (data: any) => {
  74. statsData.value = data
  75. statsNotificationVisible.value = true
  76. }
  77. // 关闭统计信息弹窗
  78. const closeStatsNotification = () => {
  79. statsNotificationVisible.value = false
  80. }
  81. // WebSocket 连接
  82. const wsUrl = import.meta.env.VITE_API_WSS_URL
  83. const { connect, disconnect, sendMessage } = useWebSocket({
  84. wsUrl,
  85. onSOSAlert: (alertData: any) => handleSOSAlert(alertData),
  86. onOrderAlert: (alertData: any) => showStatsNotification(alertData)
  87. })
  88. const handleSOSAlert = async (alertData: any) => {
  89. const alert = alertData.data || alertData
  90. sendMessage({
  91. type: 'SOS_ACK',
  92. alertId: alertData.timestamp,
  93. timestamp: Date.now()
  94. })
  95. // 获取地址信息
  96. let addressInfo = '位置信息获取中...'
  97. if (alert.longitude && alert.latitude) {
  98. addressInfo = await amapReverseGeocode(alert.longitude, alert.latitude)
  99. }
  100. ElNotification({
  101. title: '🚨🚨 SOS紧急预警 🚨🚨',
  102. customClass: 'my-warning-notification',
  103. message: h('div', {
  104. style: { color: '#fff' },
  105. innerHTML: `
  106. <div>院区名称: ${alert.organizationName || '未知'}</div>
  107. <div>长者姓名: ${alert.elderName || '未知'}</div>
  108. <div>长者房间: ${alert.roomName || '未知'}</div>
  109. <div>设备类型: ${alert.deviceType || '未知'}</div>
  110. <div>设备电量: ${alert.batteryLevel || '0%'}</div>
  111. <div>位置信息: ${addressInfo || ''}</div>
  112. <div>时间: ${new Date(alertData.timestamp).toLocaleString()}</div>
  113. `
  114. }),
  115. type: 'warning',
  116. duration: 10000
  117. })
  118. }
  119. onMounted(() => {
  120. // 检查今天是否已经执行过刷新
  121. const checkIfRefreshedToday = () => {
  122. const today = new Date().toDateString()
  123. const lastRefreshDate = localStorage.getItem('lastRefreshDate')
  124. return lastRefreshDate === today
  125. }
  126. // 标记今天已刷新
  127. const markRefreshedToday = () => {
  128. const today = new Date().toDateString()
  129. localStorage.setItem('lastRefreshDate', today)
  130. }
  131. // 计算到下一个15:52的时间间隔
  132. const calculateNextExecutionTime = () => {
  133. const now = new Date()
  134. const nextExecution = new Date()
  135. nextExecution.setHours(4, 0, 0, 0) //
  136. console.log('当前时间:', now.toLocaleString())
  137. console.log('初始执行时间:', nextExecution.toLocaleString())
  138. // 如果当前时间已经超过今天的15:52,则设置为明天的15:52
  139. if (now.getHours() > 4 || (now.getHours() === 4 && now.getMinutes() > 0) || (now.getHours() === 4 && now.getMinutes() === 0 && now.getSeconds() > 0)) {
  140. nextExecution.setDate(nextExecution.getDate() + 1)
  141. console.log('当前时间已过04:00,设置为明天执行')
  142. } else {
  143. console.log('设置为今天执行')
  144. }
  145. const timeDiff = nextExecution.getTime() - now.getTime()
  146. console.log('时间差(毫秒):', timeDiff)
  147. return timeDiff
  148. }
  149. // 设置定时器
  150. const scheduleRefresh = () => {
  151. const timeUntilNextExecution = calculateNextExecutionTime()
  152. const nextExecution = new Date(Date.now() + timeUntilNextExecution)
  153. console.log('定时器已设置,将在以下时间执行刷新:', nextExecution.toLocaleString())
  154. console.log('距离执行还有毫秒数:', timeUntilNextExecution)
  155. setTimeout(() => {
  156. console.log('定时任务触发,准备刷新页面')
  157. // 标记今天已刷新
  158. markRefreshedToday()
  159. // 执行页面刷新
  160. location.reload()
  161. // 递归调用,设置下一次执行
  162. scheduleRefresh()
  163. }, timeUntilNextExecution)
  164. }
  165. // 检查日期变化并重新设置定时器
  166. const checkDateChangeAndReschedule = () => {
  167. const now = new Date()
  168. console.log('当前时间:', now.toLocaleString())
  169. console.log('今天是否已刷新:', checkIfRefreshedToday())
  170. console.log('localStorage中的日期:', localStorage.getItem('lastRefreshDate'))
  171. // 如果localStorage中没有记录或者记录的日期不是今天,则需要重新设置定时器
  172. if (!checkIfRefreshedToday()) {
  173. console.log('设置新的定时器')
  174. scheduleRefresh()
  175. } else {
  176. console.log('今天已刷新,不设置定时器')
  177. }
  178. }
  179. // 初始检查和设置
  180. checkDateChangeAndReschedule()
  181. // 每小时检查一次日期变化,确保即使页面一直打开也能在第二天重新设置定时器
  182. setInterval(checkDateChangeAndReschedule, 120 * 60 * 1000) // 每小时检查一次
  183. })
  184. // 监听token变化
  185. // watch(
  186. // getToken,
  187. // (val) => {
  188. // console.log('getToken', val)
  189. // if(val){
  190. // setTimeout(() => {
  191. // console.log('开始连接WebSocket')
  192. // connect()
  193. // }, 1000)
  194. // }else {
  195. // console.log('断开WebSocket')
  196. // disconnect()
  197. // }
  198. // },
  199. // { immediate: true }
  200. // )
  201. onUnmounted(() => {
  202. // disconnect()
  203. // 清理storage事件监听器
  204. window.removeEventListener('storage', handleStorageChange)
  205. })
  206. </script>
  207. <template>
  208. <ConfigGlobal :size="currentSize">
  209. <RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
  210. <routerSearch />
  211. <GlobalStatsNotification
  212. :visible="statsNotificationVisible"
  213. :stats="statsData"
  214. @close="closeStatsNotification"
  215. />
  216. </ConfigGlobal>
  217. </template>
  218. <style lang="scss">
  219. $prefix-cls: #{$namespace}-app;
  220. .size {
  221. width: 100%;
  222. height: 100%;
  223. }
  224. html{
  225. font-size: 14px;
  226. }
  227. html,
  228. body {
  229. @extend .size;
  230. padding: 0 !important;
  231. margin: 0;
  232. overflow: hidden;
  233. #app {
  234. @extend .size;
  235. }
  236. }
  237. .#{$prefix-cls}-grey-mode {
  238. filter: grayscale(100%);
  239. }
  240. </style>