Files
education/frontend/src/layout/components/Navbar.vue
T
2026-08-26 09:52:26 +08:00

407 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="navbar" :class="[{ 'header-mode': headerMode }]">
<hamburger v-if="!headerMode" id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb v-if="!headerMode" id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<template v-if="device!=='mobile'">
<el-tooltip content="菜单搜索" effect="dark" placement="bottom">
<div id="header-search" class="right-menu-item icon-btn" @click="openSearch">
<search ref="searchRef" />
</div>
</el-tooltip>
<el-tooltip content="消息通知" effect="dark" placement="bottom">
<header-notice id="header-notice" class="right-menu-item icon-btn" />
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item" trigger="click" @command="handleCommand">
<div class="avatar-wrapper">
<div class="avatar-box">
<img :src="avatar" class="user-avatar">
<span class="online-dot" title="在线"></span>
</div>
<div class="user-info">
<span class="user-nickname">{{ nickName }}</span>
<span class="user-role">{{ userRoleLabel }}</span>
</div>
<i class="el-icon-arrow-down user-caret"></i>
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown-menu">
<router-link to="/user/profile">
<el-dropdown-item icon="el-icon-user">个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item icon="el-icon-lock" @click.native="lockScreen">
<span>锁定屏幕</span>
</el-dropdown-item>
<el-dropdown-item divided icon="el-icon-switch-button" @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import Search from '@/components/HeaderSearch'
import HeaderNotice from './HeaderNotice'
export default {
components: {
Breadcrumb,
Hamburger,
Screenfull,
Search,
HeaderNotice
},
props: {
// headerMode=true 时,Navbar 作为顶部全宽 Header 右侧功能区,仅渲染 right-menu
headerMode: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device',
'nickName',
'roles'
]),
// 用户角色标签:根据 roles 数组智能映射为友好文本
userRoleLabel() {
const roleMap = {
admin: '系统管理员',
teacher: '教师用户',
student: '学生用户',
ROLE_DEFAULT: '普通用户'
}
const r = (this.roles && this.roles[0]) || ''
// 直接匹配或去除 ROLE_ 前缀后再匹配
return roleMap[r] || roleMap[String(r).replace(/^ROLE_/, '')] || '教学管理系统'
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
handleCommand(command) {
// 预留:下拉菜单 command 事件入口
},
openSearch() {
if (this.$refs.searchRef && typeof this.$refs.searchRef.click === 'function') {
this.$refs.searchRef.click()
}
},
lockScreen() {
const currentPath = this.$route.fullPath
this.$store.dispatch('lock/lockScreen', currentPath).then(() => {
this.$router.push('/lock')
})
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index'
})
}).catch(() => {})
},
}
}
</script>
<style lang="scss" scoped>
.navbar.nav3 {
.hamburger-container {
display: none !important;
}
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
display: flex;
align-items: center;
// padding: 0 8px;
box-sizing: border-box;
&.header-mode {
height: 100%;
background: transparent;
box-shadow: none;
padding-right: 8px;
/* 深色顶栏下右侧功能区图标与文字改为白色 */
.right-menu-item {
color: rgba(255, 255, 255, 0.95);
&.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.10);
transition: background-color 0.18s ease, border-color 0.18s ease;
cursor: pointer;
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.28);
}
&:active {
background: rgba(255, 255, 255, 0.21);
}
.svg-icon {
width: 18px;
height: 18px;
}
}
.el-dropdown-link {
color: rgba(255, 255, 255, 0.95);
}
}
/* 通过深选择器让 .icon-btn 样式同时穿透到子组件(如 HeaderNotice)的根元素 */
::v-deep .icon-btn {
display: inline-flex !important;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.10);
transition: background-color 0.18s ease, border-color 0.18s ease;
cursor: pointer;
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.28);
}
&:active {
background: rgba(255, 255, 255, 0.21);
}
.svg-icon {
width: 18px;
height: 18px;
color: #fff;
}
}
.avatar-container .avatar-wrapper .user-nickname {
color: #fff;
}
}
.hamburger-container {
line-height: 46px;
height: 100%;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
flex-shrink: 0;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.right-menu {
height: 100%;
line-height: 50px;
display: flex;
align-items: center;
margin-left: auto;
gap: 6px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
padding: 0;
line-height: normal;
&.icon-btn {
position: relative;
}
.el-dropdown-link {
font-size: 14px;
color: #5a5e66;
cursor: pointer;
}
}
.avatar-container {
margin-right: 0;
padding: 0 6px 0 10px;
height: 100%;
border-radius: 8px;
cursor: pointer;
&.is-active .user-caret {
transform: rotate(180deg);
}
.avatar-wrapper {
display: flex;
align-items: center;
position: relative;
gap: 10px;
.avatar-box {
position: relative;
width: 36px;
height: 36px;
border-radius: 8px;
padding: 2px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.user-avatar {
cursor: pointer;
width: 100%;
height: 100%;
border-radius: 6px;
object-fit: cover;
background: var(--edu-green-light);
display: block;
}
.online-dot {
position: absolute;
right: 0;
bottom: 0;
width: 10px;
height: 10px;
border-radius: 50%;
background: #2ecc71;
border: 2px solid #fff;
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.6);
animation: onlinePulse 2s infinite;
}
@keyframes onlinePulse {
0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.55); }
70% { box-shadow: 0 0 0 6px rgba(46, 204, 113, 0); }
100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); }
}
.user-info {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.15;
min-width: 0;
}
.user-nickname {
font-size: 14px;
font-weight: 600;
color: #fff;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
letter-spacing: 0.3px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.user-role {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
margin-top: 2px;
letter-spacing: 0.2px;
}
.user-caret {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
margin-left: 2px;
transition: transform 0.2s ease;
}
}
}
}
}
/* 用户下拉菜单 - 提升视觉品质(非 scoped,因为 el-dropdown-menu 渲染到 body) */
.user-dropdown-menu {
border-radius: 10px !important;
border: 1px solid #ebeef5 !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06) !important;
padding: 6px !important;
overflow: hidden;
.el-dropdown-menu__item {
border-radius: 6px;
font-size: 13px;
color: #303133;
line-height: 36px;
padding: 0 14px;
transition: background 0.2s, color 0.2s;
i {
font-size: 14px;
color: var(--edu-green-primary);
margin-right: 8px;
}
&:not(.is-disabled):hover,
&:not(.is-disabled):focus {
background: var(--edu-green-light);
color: var(--edu-green-primary);
outline: none;
}
}
}
</style>