index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <template>
  2. <a-layout style="min-height: 100vh">
  3. <a-layout-sider
  4. v-model:collapsed="state.collapsed"
  5. collapsible
  6. :theme="theme"
  7. class="layout-sider"
  8. >
  9. <div :class="['logoWrap', theme === 'light' ? 'light' : 'dark']">
  10. <div class="logo">
  11. <slot name="logo"><img src="@/assets/logo.png" alt="" /></slot>
  12. </div>
  13. <div class="text">雷能信息后台管理</div>
  14. </div>
  15. <a-menu
  16. v-model:selectedKeys="state.selectedKeys"
  17. mode="inline"
  18. :theme="theme"
  19. :inline-collapsed="state.collapsed"
  20. :items="menus"
  21. @click="clickMenuItemHandler"
  22. ></a-menu>
  23. </a-layout-sider>
  24. <a-layout class="layout-content">
  25. <a-layout-header>
  26. <slot name="header">
  27. <base-weather class="weather" mode="text"></base-weather>
  28. <time-now class="timeNow"></time-now>
  29. <user-dropdown class="userDropdown"></user-dropdown>
  30. </slot>
  31. </a-layout-header>
  32. <a-layout-content>
  33. <slot name="breadcrumb">
  34. <a-page-header
  35. v-if="routes && routes.length > 1"
  36. :title="routes[routes.length - 1]?.breadcrumbName"
  37. :breadcrumb="breadcrumbConfig"
  38. @back="backHandler"
  39. >
  40. <template #backIcon>
  41. <div
  42. :style="{
  43. pointerEvents: !canGoBack ? 'none' : 'auto',
  44. cursor: !canGoBack ? 'none' : 'pointer',
  45. color: !canGoBack ? 'rgba(0, 0, 0, 0.25)' : 'inherit',
  46. }"
  47. >
  48. <ArrowLeftOutlined />
  49. </div>
  50. </template>
  51. </a-page-header>
  52. </slot>
  53. <slot></slot>
  54. </a-layout-content>
  55. <a-layout-footer>
  56. <slot name="footer">合肥雷能信息技术有限公司 © 2025 All Rights Reserved.</slot>
  57. </a-layout-footer>
  58. </a-layout>
  59. <alert-modal v-model:open="openAlertModal" :data="alertModalState"></alert-modal>
  60. </a-layout>
  61. </template>
  62. <script setup lang="ts">
  63. import { ref, defineOptions, reactive, watchEffect, computed, h, onUnmounted, onMounted } from 'vue'
  64. import userDropdown from './components/userDropdown/index.vue'
  65. import { menus } from '@/const/menus'
  66. import { useRoute, useRouter } from 'vue-router'
  67. import timeNow from './components/timeNow/index.vue'
  68. import type { Route } from 'ant-design-vue/es/breadcrumb/Breadcrumb'
  69. import mqtt, { MqttClient } from 'mqtt'
  70. import { useUserStore } from '@/stores/user'
  71. import AlertModal from './components/alertModal/index.vue'
  72. import { ArrowLeftOutlined } from '@ant-design/icons-vue'
  73. const userStore = useUserStore()
  74. const userId = ref(userStore?.userInfo?.userId || '')
  75. let mqttClient: MqttClient | null = null
  76. let mqttTimeout: number | null = null
  77. const MQTT_TIMEOUT_MS = 10000 // 10秒
  78. const resetMqttTimeout = () => {
  79. if (mqttTimeout) clearTimeout(mqttTimeout)
  80. mqttTimeout = window.setTimeout(() => {
  81. console.log('MQTT超时未收到新消息')
  82. closeMqtt()
  83. }, MQTT_TIMEOUT_MS)
  84. }
  85. const MqttData = ref()
  86. const initMqttManager = async () => {
  87. try {
  88. mqttClient = mqtt.connect('wss://radar-power.cn:8084/mqtt', {
  89. clientId: 'mqtt_vue_test' + Math.random().toString(16).substring(2, 8),
  90. username: 'admin',
  91. password: 'public',
  92. will: {
  93. // ✅ 添加遗嘱消息配置
  94. topic: '/mps/client/connect',
  95. payload: JSON.stringify({
  96. userId: userId.value,
  97. deviceType: 'wb',
  98. msgType: 'disconnect',
  99. }),
  100. qos: 2,
  101. retain: false,
  102. },
  103. })
  104. mqttClient.on('connect', () => {
  105. console.log('MQTT已连接')
  106. const messageData = {
  107. userId: userId.value, // 用户ID
  108. deviceType: 'wb', // 设备类型
  109. msgType: 'connect', // 消息类型
  110. }
  111. // 发布消息
  112. mqttClient?.publish('/mps/client/connect', JSON.stringify(messageData), { qos: 2 }, (err) => {
  113. if (err) console.error('发布失败', err)
  114. else console.log('✅ 已发送参数:', messageData)
  115. })
  116. // 订阅所有主题
  117. mqttClient?.subscribe(`/mps/client/connect/`, (err) => {
  118. if (err) {
  119. console.error('MQTT订阅失败', err)
  120. } else {
  121. console.log(`🔥已订阅主题 /mps/client/connect`)
  122. }
  123. })
  124. mqttClient?.subscribe(`/mps/wb_${userStore.userInfo?.userId}/notice`, (err) => {
  125. if (err) {
  126. console.error('MQTT订阅失败', err)
  127. } else {
  128. console.log(`🔥已订阅主题 /mps/wb_${userStore.userInfo?.userId}/notice`)
  129. }
  130. })
  131. })
  132. mqttClient.on('error', (err) => {
  133. console.error('MQTT连接错误', err)
  134. })
  135. mqttClient.on('message', (topic: string, message: Uint8Array) => {
  136. resetMqttTimeout()
  137. try {
  138. const data = JSON.parse(message.toString())
  139. console.log('👏👏收到MQTT消息', topic, data)
  140. MqttData.value = data
  141. } catch (e) {
  142. console.error('👏👏MQTT消息解析失败', e)
  143. }
  144. })
  145. } catch (error) {
  146. console.error('MQTT连接失败:', error)
  147. }
  148. }
  149. initMqttManager()
  150. // 报警弹窗
  151. const openAlertModal = ref(false)
  152. const alertModalState = reactive({
  153. devName: '',
  154. tenantName: '',
  155. clientId: '',
  156. })
  157. watchEffect(() => {
  158. const { msgType, event, devName, tenantName, clientId } = MqttData.value || {}
  159. if (msgType === 'fall' && event === 'fall_confirmed') {
  160. alertModalState.devName = devName
  161. alertModalState.tenantName = tenantName
  162. alertModalState.clientId = clientId
  163. openAlertModal.value = true
  164. }
  165. // 退出连接,取消mqtt连接
  166. if (!userStore.userInfo.tokenValue) {
  167. console.log('🚀🚀🚀退出连接,取消mqtt连接')
  168. closeMqtt()
  169. }
  170. })
  171. const closeMqtt = () => {
  172. mqttClient?.end(true)
  173. }
  174. onUnmounted(() => {
  175. resetMqttTimeout()
  176. })
  177. defineOptions({
  178. name: 'baseLayout',
  179. })
  180. const route = useRoute()
  181. const router = useRouter()
  182. const theme = ref('dark') // 主题 dark / light
  183. const state = reactive({
  184. collapsed: false,
  185. selectedKeys: [route.name],
  186. })
  187. const routes = computed(() => {
  188. const currentRoute = router.currentRoute.value
  189. const items = []
  190. if (currentRoute.matched) {
  191. currentRoute.matched.forEach((matchedRoute) => {
  192. if (matchedRoute.meta?.title) {
  193. items.push({
  194. breadcrumbName: matchedRoute.meta.title,
  195. path: matchedRoute.path,
  196. })
  197. }
  198. })
  199. }
  200. if (items.length === 0 && currentRoute.meta?.title) {
  201. items.push({
  202. breadcrumbName: currentRoute.meta.title,
  203. path: currentRoute.path,
  204. })
  205. }
  206. return items
  207. })
  208. const breadcrumbConfig = computed(() => {
  209. return {
  210. routes: routes.value,
  211. itemRender,
  212. }
  213. })
  214. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  215. const itemRender = (options: { route: Route; params: any; routes: Route[]; paths: string[] }) => {
  216. console.log('itemRender', options)
  217. if (!Array.isArray(options.routes)) {
  218. return h('span', options.route.breadcrumbName || '')
  219. }
  220. const currentIndex = routes.value.indexOf(options.route)
  221. if (currentIndex === -1 || currentIndex === routes.value.length - 1) {
  222. return h(
  223. 'span',
  224. {
  225. class: 'current-breadcrumb',
  226. },
  227. options.route.breadcrumbName
  228. )
  229. }
  230. return h(
  231. 'a',
  232. {
  233. class: 'breadcrumb-link',
  234. onClick: (e) => {
  235. console.log('🚀customRouterLinkRende onClickr🚀', e)
  236. e.preventDefault()
  237. if (options.route.path) {
  238. router.push(options.route.path)
  239. }
  240. },
  241. },
  242. options.route.breadcrumbName
  243. )
  244. }
  245. watchEffect(() => {
  246. if (routes.value.length > 0 && routes.value[0].path) {
  247. state.selectedKeys = [routes.value[0].path.split('/')[1] || 'home']
  248. }
  249. })
  250. const clickMenuItemHandler = ({ key, keyPath }: { key: string; keyPath: string[] }) => {
  251. console.log('🚀clickMenuItemHandler🚀', key, keyPath)
  252. if (route.name !== key) {
  253. router.push({ name: key })
  254. }
  255. }
  256. const canGoBack = ref(false)
  257. const navigationHistory = ref<string[]>([])
  258. let isBackNavigation = false
  259. onMounted(() => {
  260. // 初始化导航历史
  261. navigationHistory.value = [router.currentRoute.value.path]
  262. canGoBack.value = false
  263. })
  264. // 监听路由变化,更新导航历史和canGoBack状态
  265. watchEffect(() => {
  266. const currentPath = router.currentRoute.value.path
  267. if (isBackNavigation) {
  268. // 如果是返回导航,不添加到历史记录
  269. isBackNavigation = false
  270. } else {
  271. // 确保只添加新的路径
  272. if (navigationHistory.value[navigationHistory.value.length - 1] !== currentPath) {
  273. navigationHistory.value.push(currentPath)
  274. }
  275. }
  276. // 当历史记录长度大于1时可以返回
  277. canGoBack.value = navigationHistory.value.length > 1
  278. })
  279. const backHandler = async () => {
  280. if (navigationHistory.value.length > 1) {
  281. // 移除当前路径
  282. navigationHistory.value.pop()
  283. // 设置返回导航标志
  284. isBackNavigation = true
  285. // 返回到上一个路径
  286. const prevPath = navigationHistory.value[navigationHistory.value.length - 1]
  287. await router.push(prevPath)
  288. }
  289. }
  290. </script>
  291. <style scoped lang="less">
  292. .layout-sider {
  293. position: relative;
  294. background-color: #1a3a5f;
  295. :deep(.ant-layout-sider-children) {
  296. position: sticky;
  297. top: 0;
  298. height: auto;
  299. background-color: #1a3a5f;
  300. .ant-menu {
  301. background-color: #1a3a5f;
  302. }
  303. }
  304. :deep(.ant-layout-sider-trigger) {
  305. background-color: #1a3a5f;
  306. }
  307. }
  308. .logoWrap {
  309. height: 32px;
  310. margin: 16px;
  311. display: flex;
  312. align-items: center;
  313. overflow: hidden;
  314. transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  315. color: #fff;
  316. .logo {
  317. width: 45px;
  318. height: 32px;
  319. flex-shrink: 0;
  320. font-size: 20px;
  321. display: flex;
  322. justify-content: center;
  323. align-items: center;
  324. img {
  325. width: 100%;
  326. height: 100%;
  327. }
  328. }
  329. .text {
  330. margin-left: 10px;
  331. flex-grow: 1;
  332. opacity: 1;
  333. transition:
  334. opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
  335. width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  336. overflow: hidden;
  337. text-overflow: ellipsis;
  338. display: -webkit-box;
  339. -webkit-box-orient: vertical;
  340. -webkit-line-clamp: 2;
  341. }
  342. }
  343. .light {
  344. color: #141414;
  345. }
  346. .dark {
  347. color: #fff;
  348. }
  349. .layout-content {
  350. .ant-layout-header {
  351. background-color: #1a3a5f;
  352. color: #fff;
  353. padding: 0 32px;
  354. text-align: right;
  355. display: flex;
  356. justify-content: flex-end;
  357. position: sticky;
  358. top: 0;
  359. z-index: 1000;
  360. }
  361. .ant-layout-content {
  362. margin: 16px;
  363. }
  364. .ant-layout-footer {
  365. text-align: center;
  366. background-color: transparent;
  367. color: #4774a7;
  368. }
  369. }
  370. .ant-layout-sider-collapsed & {
  371. .text {
  372. opacity: 0;
  373. width: 0;
  374. margin-left: 0;
  375. }
  376. }
  377. .site-layout .site-layout-background {
  378. background: #101830b3;
  379. }
  380. [data-theme='dark'] .site-layout .site-layout-background {
  381. background: #101830b3;
  382. }
  383. </style>