feat(layout): 优化头部通知组件并实现混合菜单布局
- 重构 HeaderNotice 组件样式,移除外层容器类名并调整图标尺寸 - 将消息通知组件从左侧移动到右侧菜单区域 - 添加 TopBar 组件支持混合菜单和顶部菜单模式 - 实现动态侧边栏显示逻辑,根据导航类型控制侧边栏显示 - 在 Settings 组件中完善菜单导航设置的逻辑判断 - 优化通知轮询机制,添加定时检查新通知功能 - 实现浏览器通知提醒功能,新消息时显示 toast 提示 - 调整权限管理中的路由处理逻辑,确保菜单正常加载
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="header-notice-trigger">
|
||||
<el-popover
|
||||
placement="bottom-end"
|
||||
:width="380"
|
||||
@@ -85,11 +85,9 @@
|
||||
</div>
|
||||
|
||||
<template #reference>
|
||||
<div class="notice-trigger-icon">
|
||||
<el-badge :value="noticeStore.unreadCount" :hidden="noticeStore.unreadCount === 0" :max="99">
|
||||
<el-icon :size="18"><Bell /></el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
<el-badge :value="noticeStore.unreadCount" :hidden="noticeStore.unreadCount === 0" :max="99">
|
||||
<el-icon><Bell /></el-icon>
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
@@ -264,22 +262,21 @@ onMounted(() => {
|
||||
:deep(p) { margin: 8px 0; }
|
||||
:deep(a) { color: #3B82F6; }
|
||||
}
|
||||
/* 触发器图标 */
|
||||
.notice-trigger-icon {
|
||||
.header-notice-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: background 0.3s;
|
||||
color: #606266;
|
||||
font-size: 18px;
|
||||
&:hover {
|
||||
background-color: #f6f6f6;
|
||||
color: #3B82F6;
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
class="left-action-item"
|
||||
/>
|
||||
</template>
|
||||
<!-- 消息通知 -->
|
||||
<HeaderNotice />
|
||||
<!-- 帮助中心按钮 -->
|
||||
<el-tooltip
|
||||
content="帮助中心"
|
||||
@@ -48,6 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-menu">
|
||||
<HeaderNotice />
|
||||
<div class="avatar-container">
|
||||
<div class="avatar-wrapper">
|
||||
<el-dropdown
|
||||
|
||||
@@ -169,18 +169,22 @@ function handleNavType(val) {
|
||||
/** 菜单导航设置 */
|
||||
watch(() => navType, val => {
|
||||
if (val.value == 1) {
|
||||
// 纯左侧菜单
|
||||
appStore.sidebar.opened = true
|
||||
appStore.toggleSideBarHide(false)
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||
}
|
||||
if (val.value == 2) {
|
||||
// 混合菜单:顶部显示一级菜单,左侧显示子菜单
|
||||
appStore.sidebar.opened = true
|
||||
appStore.toggleSideBarHide(false)
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||
}
|
||||
if (val.value == 3) {
|
||||
// 纯顶部菜单:隐藏侧边栏
|
||||
appStore.sidebar.opened = false
|
||||
appStore.toggleSideBarHide(true)
|
||||
}
|
||||
if ([1, 3].includes(val.value)) {
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||
}
|
||||
}, { immediate: true, deep: true }
|
||||
)
|
||||
|
||||
@@ -2,3 +2,4 @@ export { default as AppMain } from './AppMain'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Settings } from './Settings'
|
||||
export { default as TagsView } from './TagsView/index.vue'
|
||||
export { default as TopBar } from './TopBar'
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
@click="handleClickOutside"
|
||||
/>
|
||||
<!-- 左侧侧边栏 -->
|
||||
<sidebar v-if="!sidebar.hide" />
|
||||
<sidebar v-if="topNav !== 3 && !sidebar.hide" />
|
||||
<!-- 右侧主容器 -->
|
||||
<div class="main-wrapper">
|
||||
<!-- 顶部导航栏 -->
|
||||
<navbar @set-layout="setLayout" />
|
||||
<!-- 顶部菜单栏(混合菜单/顶部菜单模式) -->
|
||||
<TopBar v-if="topNav === 2 || topNav === 3" />
|
||||
<!-- 内容区 -->
|
||||
<div
|
||||
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide }"
|
||||
@@ -40,7 +42,7 @@
|
||||
<script setup>
|
||||
import {useWindowSize} from '@vueuse/core';
|
||||
import Sidebar from './components/Sidebar/index.vue';
|
||||
import {AppMain, Settings, TagsView, Navbar} from './components';
|
||||
import {AppMain, Settings, TagsView, Navbar, TopBar} from './components';
|
||||
import NoticePopup from '@/components/NoticePopup/index.vue';
|
||||
import Copyright from './components/Copyright/index.vue';
|
||||
|
||||
@@ -54,6 +56,7 @@ const sidebar = computed(() => useAppStore().sidebar);
|
||||
const device = computed(() => useAppStore().device);
|
||||
const needTagsView = computed(() => settingsStore.tagsView);
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const topNav = computed(() => settingsStore.topNav);
|
||||
|
||||
const { width } = useWindowSize();
|
||||
const WIDTH = 992; // refer to Bootstrap's responsive design
|
||||
|
||||
@@ -9,6 +9,7 @@ import useUserStore from '@/store/modules/user'
|
||||
import useLockStore from '@/store/modules/lock'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
import useNoticeStore from '@/store/modules/notice'
|
||||
|
||||
// 全局变量,用于控制公告弹窗只显示一次
|
||||
let hasShownNoticePopup = false
|
||||
@@ -54,6 +55,7 @@ router.beforeEach(async (to, from) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
useNoticeStore().startPolling()
|
||||
return { ...to, replace: true }
|
||||
} catch (err) {
|
||||
console.error('路由加载失败:', err)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { getUnreadCount, getUserNotices, markAsRead, markAllAsRead, listNoticeTop } from '@/api/system/notice'
|
||||
import { ElNotification } from 'element-plus'
|
||||
|
||||
let pollingTimer = null
|
||||
|
||||
const useNoticeStore = defineStore('notice', {
|
||||
state: () => ({
|
||||
unreadCount: 0,
|
||||
noticeList: [],
|
||||
readIds: new Set(),
|
||||
loaded: false
|
||||
loaded: false,
|
||||
lastUnreadCount: -1
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@@ -21,6 +25,82 @@ const useNoticeStore = defineStore('notice', {
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 启动轮询(登录后调用)
|
||||
startPolling() {
|
||||
this.stopPolling()
|
||||
// 立即拉一次
|
||||
this.fetchNotices()
|
||||
// 每 30 秒轮询未读数
|
||||
pollingTimer = setInterval(() => {
|
||||
this.checkNewNotices()
|
||||
}, 30000)
|
||||
},
|
||||
|
||||
// 停止轮询(退出登录时调用)
|
||||
stopPolling() {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer)
|
||||
pollingTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
// 检查是否有新通知
|
||||
async checkNewNotices() {
|
||||
try {
|
||||
const res = await getUnreadCount()
|
||||
const newCount = res.data || 0
|
||||
// 首次记录
|
||||
if (this.lastUnreadCount === -1) {
|
||||
this.lastUnreadCount = newCount
|
||||
this.unreadCount = newCount
|
||||
return
|
||||
}
|
||||
// 未读数增加 → 有新通知
|
||||
if (newCount > this.lastUnreadCount) {
|
||||
this.unreadCount = newCount
|
||||
this.lastUnreadCount = newCount
|
||||
// 刷新列表
|
||||
await this.fetchNotices(true)
|
||||
// 弹出浏览器通知
|
||||
const diff = newCount - this.lastUnreadCount + (newCount - this.lastUnreadCount)
|
||||
this.showNewNoticeToast()
|
||||
} else {
|
||||
this.unreadCount = newCount
|
||||
this.lastUnreadCount = newCount
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 新消息提示
|
||||
showNewNoticeToast() {
|
||||
try {
|
||||
ElNotification({
|
||||
title: '新消息',
|
||||
message: '您有新的通知/公告,请查看消息中心',
|
||||
type: 'info',
|
||||
duration: 4000,
|
||||
position: 'top-right'
|
||||
})
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 获取通知列表
|
||||
async fetchNotices(silent = false) {
|
||||
try {
|
||||
const res = await getUserNotices()
|
||||
this.noticeList = res.data || []
|
||||
this.loaded = true
|
||||
this.unreadCount = this.noticeList.filter(n => !n.readFlag || n.readFlag === '0').length
|
||||
this.lastUnreadCount = this.unreadCount
|
||||
} catch (e) {
|
||||
if (!silent) console.error('获取通知列表失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取未读数量
|
||||
async fetchUnreadCount() {
|
||||
try {
|
||||
@@ -31,42 +111,18 @@ const useNoticeStore = defineStore('notice', {
|
||||
}
|
||||
},
|
||||
|
||||
// 获取通知列表
|
||||
async fetchNotices() {
|
||||
try {
|
||||
const res = await getUserNotices()
|
||||
this.noticeList = res.data || []
|
||||
this.loaded = true
|
||||
// 计算未读数
|
||||
this.unreadCount = this.noticeList.filter(n => !n.readFlag || n.readFlag === '0').length
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 获取顶部通知列表
|
||||
async fetchTopNotices(query) {
|
||||
try {
|
||||
const res = await listNoticeTop(query)
|
||||
this.noticeList = res.data || []
|
||||
this.loaded = true
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 标记单条已读
|
||||
async markRead(noticeId) {
|
||||
try {
|
||||
await markAsRead(noticeId)
|
||||
this.readIds.add(noticeId)
|
||||
// 更新列表中的已读状态
|
||||
const item = this.noticeList.find(n => n.noticeId === noticeId)
|
||||
if (item) {
|
||||
item.isRead = true
|
||||
item.readFlag = '1'
|
||||
}
|
||||
this.unreadCount = Math.max(0, this.unreadCount - 1)
|
||||
this.lastUnreadCount = this.unreadCount
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
@@ -86,6 +142,7 @@ const useNoticeStore = defineStore('notice', {
|
||||
n.readFlag = '1'
|
||||
})
|
||||
this.unreadCount = 0
|
||||
this.lastUnreadCount = 0
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@@ -94,10 +151,12 @@ const useNoticeStore = defineStore('notice', {
|
||||
|
||||
// 重置状态
|
||||
reset() {
|
||||
this.stopPolling()
|
||||
this.unreadCount = 0
|
||||
this.noticeList = []
|
||||
this.readIds = new Set()
|
||||
this.loaded = false
|
||||
this.lastUnreadCount = -1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user