123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- <template>
- <a-layout style="min-height: 100vh">
- <a-layout-sider
- v-model:collapsed="state.collapsed"
- collapsible
- :theme="theme"
- class="layout-sider"
- @breakpoint="onBreakpoint"
- >
- <div :class="['logoWrap', theme === 'light' ? 'light' : 'dark']">
- <div class="logo">
- <slot name="logo"><img src="@/assets/logo.png" alt="" /></slot>
- </div>
- <div class="text">雷能信息后台管理</div>
- </div>
- <!-- <a-menu
- v-model:selectedKeys="state.selectedKeys"
- mode="inline"
- :theme="theme"
- :inline-collapsed="state.collapsed"
- :items="menus"
- @click="clickMenuItemHandler"
- ></a-menu> -->
- <SideMenu :collapsed="collapsed" />
- </a-layout-sider>
- <a-layout class="layout-content">
- <a-layout-header>
- <slot name="header">
- <base-weather class="weather" mode="text"></base-weather>
- <div class="smartScreen" @click="openSmartScreen">智慧大屏</div>
- <time-now class="timeNow"></time-now>
- <SyncOutlined class="refresh" @click="refresh" />
- <user-dropdown class="userDropdown"></user-dropdown>
- </slot>
- </a-layout-header>
- <a-layout-content>
- <slot name="breadcrumb">
- <a-page-header
- v-if="routes && routes.length > 1"
- :title="routes[routes.length - 1]?.breadcrumbName"
- :breadcrumb="breadcrumbConfig"
- @back="backHandler"
- >
- <template #backIcon>
- <div
- :style="{
- pointerEvents: !canGoBack ? 'none' : 'auto',
- cursor: !canGoBack ? 'none' : 'pointer',
- color: !canGoBack ? 'rgba(0, 0, 0, 0.25)' : 'inherit',
- }"
- >
- <ArrowLeftOutlined />
- </div>
- </template>
- </a-page-header>
- </slot>
- <slot></slot>
- </a-layout-content>
- <a-layout-footer>
- <slot name="footer">
- <span>合肥雷能信息技术有限公司 © 2025 All Rights Reserved.</span>
- <span> 版本号:{{ version }}</span>
- <!-- <span> 构建时间:{{ buildTime }}</span> -->
- </slot>
- </a-layout-footer>
- </a-layout>
- <alert-modal v-model:open="openAlertModal" :data="alertModalState"></alert-modal>
- </a-layout>
- </template>
- <script setup lang="ts">
- import { ref, reactive, watchEffect, computed, h, onUnmounted, onMounted } from 'vue'
- import userDropdown from './components/userDropdown/index.vue'
- // import { menus } from '@/const/menus'
- import { useRoute, useRouter } from 'vue-router'
- import timeNow from './components/timeNow/index.vue'
- import type { Route } from 'ant-design-vue/es/breadcrumb/Breadcrumb'
- import mqtt, { MqttClient } from 'mqtt'
- import { useUserStore } from '@/stores/user'
- import AlertModal from './components/alertModal/index.vue'
- import { ArrowLeftOutlined, SyncOutlined } from '@ant-design/icons-vue'
- import SideMenu from './components/sideMenu/index.vue'
- const userStore = useUserStore()
- const userId = ref(userStore?.userInfo?.userId || '')
- const version = __APP_VERSION__
- // const buildTime = __BUILD_TIME__
- const emit = defineEmits(['refresh'])
- // 刷新
- const refresh = () => {
- emit('refresh')
- }
- const collapsed = ref<boolean>(false)
- const onBreakpoint = (broken: boolean) => {
- if (broken) {
- collapsed.value = true
- }
- }
- // 需要缓存的组件列表
- // const keepAliveComponents = computed(() => {
- // return route.matched.filter((item) => item.meta.keepAlive).map((item) => item.name as string)
- // })
- let mqttClient: MqttClient | null = null
- // let mqttTimeout: number | null = null
- // const MQTT_TIMEOUT_MS = 10000 // 10秒
- const resetMqttTimeout = () => {
- console.log('🚀关闭MQTT连接')
- closeMqtt()
- // if (mqttTimeout) clearTimeout(mqttTimeout)
- // mqttTimeout = window.setTimeout(() => {
- // console.log('MQTT超时未收到新消息')
- // closeMqtt()
- // }, MQTT_TIMEOUT_MS)
- }
- const MqttData = ref()
- const initMqttManager = async () => {
- try {
- const mqttConfig = {
- host: import.meta.env.VITE_MQTT_HOST_ALARM,
- username: import.meta.env.VITE_MQTT_USERNAME,
- password: import.meta.env.VITE_MQTT_PASSWORD,
- clientId: `web_mqtt_cmd${Math.random().toString(16).slice(2)}`,
- }
- mqttClient = mqtt.connect(mqttConfig.host, {
- clientId: mqttConfig.clientId,
- username: mqttConfig.username,
- password: mqttConfig.password,
- will: {
- // ✅ 添加遗嘱消息配置
- topic: '/mps/client/connect',
- payload: JSON.stringify({
- userId: userId.value,
- deviceType: 'wb',
- msgType: 'disconnect',
- }),
- qos: 2,
- retain: false,
- },
- })
- mqttClient.on('connect', () => {
- console.log('MQTT已连接')
- const messageData = {
- userId: userId.value, // 用户ID
- deviceType: 'wb', // 设备类型
- msgType: 'connect', // 消息类型
- }
- // 发布消息
- mqttClient?.publish('/mps/client/connect', JSON.stringify(messageData), { qos: 2 }, (err) => {
- if (err) console.error('发布失败', err)
- else console.log('✅ 已发送参数:', messageData)
- })
- // 订阅所有主题
- mqttClient?.subscribe(`/mps/client/connect/`, { qos: 2 }, (err) => {
- if (err) {
- console.error('MQTT订阅失败', err)
- } else {
- console.log(`🔥已订阅主题 /mps/client/connect`)
- }
- })
- mqttClient?.subscribe(`/mps/wb_${userStore.userInfo?.userId}/notice`, { qos: 2 }, (err) => {
- if (err) {
- console.error('MQTT订阅失败', err)
- } else {
- console.log(`🔥已订阅主题 /mps/wb_${userStore.userInfo?.userId}/notice`)
- }
- })
- })
- mqttClient.on('error', (err) => {
- console.error('MQTT连接错误', err)
- })
- mqttClient.on('message', (topic: string, message: Uint8Array) => {
- resetMqttTimeout()
- try {
- const data = JSON.parse(message.toString())
- console.log('👏👏收到MQTT消息', topic, data)
- MqttData.value = data
- } catch (e) {
- console.error('👏👏MQTT消息解析失败', e)
- }
- })
- } catch (error) {
- console.error('MQTT连接失败:', error)
- }
- }
- initMqttManager()
- // 报警弹窗
- const openAlertModal = ref(false)
- const alertModalState = reactive({
- devName: '',
- tenantName: '',
- clientId: '',
- eventListId: '',
- })
- watchEffect(() => {
- const { msgType, event, devName, tenantName, clientId, eventListId } = MqttData.value || {}
- if (msgType === 'fall' && event === 'fall_confirmed') {
- alertModalState.devName = devName
- alertModalState.tenantName = tenantName
- alertModalState.clientId = clientId
- alertModalState.eventListId = eventListId
- openAlertModal.value = true
- }
- // 退出连接,取消mqtt连接
- if (!userStore.userInfo.tokenValue) {
- console.log('🚀🚀🚀退出连接,取消mqtt连接')
- closeMqtt()
- }
- })
- const closeMqtt = () => {
- mqttClient?.end(true)
- }
- onUnmounted(() => {
- resetMqttTimeout()
- })
- defineOptions({
- name: 'baseLayout',
- })
- const route = useRoute()
- const router = useRouter()
- const theme = ref('dark') // 主题 dark / light
- const state = reactive({
- collapsed: false,
- selectedKeys: [route.name],
- })
- const routes = computed(() => {
- const currentRoute = router.currentRoute.value
- const items = []
- if (currentRoute.matched) {
- currentRoute.matched.forEach((matchedRoute) => {
- if (matchedRoute.meta?.title) {
- items.push({
- breadcrumbName: matchedRoute.meta.title,
- path: matchedRoute.path,
- })
- }
- })
- }
- if (items.length === 0 && currentRoute.meta?.title) {
- items.push({
- breadcrumbName: currentRoute.meta.title,
- path: currentRoute.path,
- })
- }
- return items
- })
- const breadcrumbConfig = computed(() => {
- return {
- routes: routes.value,
- itemRender,
- }
- })
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const itemRender = (options: { route: Route; params: any; routes: Route[]; paths: string[] }) => {
- console.log('itemRender', options)
- if (!Array.isArray(options.routes)) {
- return h('span', options.route.breadcrumbName || '')
- }
- const currentIndex = routes.value.indexOf(options.route)
- if (currentIndex === -1 || currentIndex === routes.value.length - 1) {
- return h(
- 'span',
- {
- class: 'current-breadcrumb',
- },
- options.route.breadcrumbName
- )
- }
- return h(
- 'a',
- {
- class: 'breadcrumb-link',
- onClick: (e) => {
- console.log('🚀customRouterLinkRende onClickr🚀', e)
- e.preventDefault()
- if (options.route.path) {
- router.push(options.route.path)
- }
- },
- },
- options.route.breadcrumbName
- )
- }
- const clickedMenuItem = ref('')
- watchEffect(() => {
- // if (routes.value.length > 0 && routes.value[0].path) {
- // state.selectedKeys = [routes.value[0].path.split('/')[1] || 'home']
- // }
- state.selectedKeys = [clickedMenuItem.value]
- })
- // const clickMenuItemHandler = ({ key, keyPath }: { key: string; keyPath: string[] }) => {
- // console.log('🚀clickMenuItemHandler🚀', key, keyPath)
- // clickedMenuItem.value = key
- // if (route.name !== key) {
- // router.push({ name: key })
- // }
- // }
- const canGoBack = ref(false)
- const navigationHistory = ref<string[]>([])
- let isBackNavigation = false
- onMounted(() => {
- // 初始化导航历史
- navigationHistory.value = [router.currentRoute.value.path]
- canGoBack.value = false
- })
- // 监听路由变化,更新导航历史和canGoBack状态
- watchEffect(() => {
- const currentPath = router.currentRoute.value.path
- if (isBackNavigation) {
- // 如果是返回导航,不添加到历史记录
- isBackNavigation = false
- } else {
- // 确保只添加新的路径
- if (navigationHistory.value[navigationHistory.value.length - 1] !== currentPath) {
- navigationHistory.value.push(currentPath)
- }
- }
- // 当历史记录长度大于1时可以返回
- canGoBack.value = navigationHistory.value.length > 1
- })
- const backHandler = async () => {
- if (navigationHistory.value.length > 1) {
- // 移除当前路径
- navigationHistory.value.pop()
- // 设置返回导航标志
- isBackNavigation = true
- // 返回到上一个路径
- const prevPath = navigationHistory.value[navigationHistory.value.length - 1]
- await router.push(prevPath)
- }
- }
- // 打开智慧大屏
- const openSmartScreen = () => {
- window.open('/dashboard', '_blank')
- }
- </script>
- <style scoped lang="less">
- .layout-sider {
- position: relative;
- background-color: #1a3a5f;
- :deep(.ant-layout-sider-children) {
- position: sticky;
- top: 0;
- height: auto;
- background-color: #1a3a5f;
- .ant-menu {
- background-color: #1a3a5f;
- }
- }
- :deep(.ant-layout-sider-trigger) {
- background-color: #1a3a5f;
- }
- }
- .logoWrap {
- height: 32px;
- margin: 16px;
- display: flex;
- align-items: center;
- overflow: hidden;
- transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- color: #fff;
- .logo {
- width: 45px;
- height: 32px;
- flex-shrink: 0;
- font-size: 20px;
- display: flex;
- justify-content: center;
- align-items: center;
- img {
- width: 100%;
- height: 100%;
- }
- }
- .text {
- margin-left: 10px;
- flex-grow: 1;
- opacity: 1;
- transition:
- opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
- width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- }
- }
- .light {
- color: #141414;
- }
- .dark {
- color: #fff;
- }
- .layout-content {
- .ant-layout-header {
- background-color: #1a3a5f;
- color: #fff;
- padding: 0 32px;
- text-align: right;
- display: flex;
- justify-content: flex-end;
- position: sticky;
- top: 0;
- z-index: 1000;
- .smartScreen {
- cursor: pointer;
- color: #eee;
- font-size: 1.2em;
- font-weight: bold;
- background: linear-gradient(to right, #ff6ec4, #f9d423, #00c9ff, #92fe9d);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- text-shadow: 0 0 2px rgba(255, 255, 255, 0.3);
- }
- .refresh {
- margin-right: 14px;
- font-size: 18px;
- font-weight: 600;
- }
- }
- .ant-layout-content {
- margin: 16px;
- }
- .ant-layout-footer {
- text-align: center;
- background-color: transparent;
- color: #4774a7;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- }
- .ant-layout-sider-collapsed & {
- .text {
- opacity: 0;
- width: 0;
- margin-left: 0;
- }
- }
- .site-layout .site-layout-background {
- background: #101830b3;
- }
- [data-theme='dark'] .site-layout .site-layout-background {
- background: #101830b3;
- }
- </style>
|