| | |
| | | <template> |
| | | <div class="navbar" :class="'nav' + settingsStore.navType"> |
| | | <div class="navbar"> |
| | | <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> |
| | | <breadcrumb v-if="settingsStore.navType == 1" id="breadcrumb-container" class="breadcrumb-container" /> |
| | | <top-nav v-if="settingsStore.navType == 2" id="topmenu-container" class="topmenu-container" /> |
| | | <template v-if="settingsStore.navType == 3"> |
| | | <logo v-show="settingsStore.sidebarLogo" :collapse="false"></logo> |
| | | <top-bar id="topbar-container" class="topbar-container" /> |
| | | </template> |
| | | |
| | | <div class="center-menu"> |
| | | <span class="label">LIMS实验室管理系统</span> |
| | | </div> |
| | | <div class="right-menu"> |
| | | <template v-if="appStore.device !== 'mobile'"> |
| | | <header-search id="header-search" class="right-menu-item" /> |
| | | |
| | | <el-tooltip content="源码地址" effect="dark" placement="bottom"> |
| | | <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> |
| | | </el-tooltip> |
| | | |
| | | <el-tooltip content="文档地址" effect="dark" placement="bottom"> |
| | | <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" /> |
| | | </el-tooltip> |
| | | |
| | | <screenfull id="screenfull" class="right-menu-item hover-effect" /> |
| | | |
| | | <el-tooltip content="主题模式" effect="dark" placement="bottom"> |
| | | <div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme"> |
| | | <svg-icon v-if="settingsStore.isDark" icon-class="sunny" /> |
| | | <svg-icon v-if="!settingsStore.isDark" icon-class="moon" /> |
| | | <div class="avatar-wrapper"> |
| | | <el-avatar shape="square" :size="30">{{ avatarText }}</el-avatar> |
| | | <span class="userName">{{ userStore.nickName }}</span> |
| | | <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> |
| | | <div class="avatar-wrapper"> |
| | | <lims-icon name="el-icon-caret-bottom" /> |
| | | </div> |
| | | </el-tooltip> |
| | | |
| | | <el-tooltip content="布局大小" effect="dark" placement="bottom"> |
| | | <size-select id="size-select" class="right-menu-item hover-effect" /> |
| | | </el-tooltip> |
| | | |
| | | <el-tooltip content="消息通知" effect="dark" placement="bottom"> |
| | | <header-notice id="header-notice" class="right-menu-item hover-effect" /> |
| | | </el-tooltip> |
| | | </template> |
| | | |
| | | <el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" /> |
| | | <span class="user-nickname"> {{ userStore.nickName }} </span> |
| | | </div> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <router-link to="/user/profile"> |
| | | <el-dropdown-item>个人中心</el-dropdown-item> |
| | | </router-link> |
| | | <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings"> |
| | | <span>布局设置</span> |
| | | </el-dropdown-item> |
| | | <el-dropdown-item command="lockScreen"> |
| | | <span>锁定屏幕</span> |
| | | </el-dropdown-item> |
| | | <el-dropdown-item divided command="logout"> |
| | | <span>退出登录</span> |
| | | </el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <router-link to="/user/profile"> |
| | | <el-dropdown-item>个人中心</el-dropdown-item> |
| | | </router-link> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <img class="logoout" src="@/assets/images/logoout.png" @click="logout" title="退出账号" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ElMessageBox } from 'element-plus' |
| | | import Breadcrumb from '@/components/Breadcrumb' |
| | | import TopNav from './TopNav' |
| | | import TopBar from './TopBar' |
| | | import Logo from './Sidebar/Logo' |
| | | import Hamburger from '@/components/Hamburger' |
| | | import Screenfull from '@/components/Screenfull' |
| | | import SizeSelect from '@/components/SizeSelect' |
| | | import HeaderSearch from '@/components/HeaderSearch' |
| | | import RuoYiGit from '@/components/RuoYi/Git' |
| | | import RuoYiDoc from '@/components/RuoYi/Doc' |
| | | import useAppStore from '@/store/modules/app' |
| | | import useUserStore from '@/store/modules/user' |
| | | import useLockStore from '@/store/modules/lock' |
| | | import useSettingsStore from '@/store/modules/settings' |
| | | import HeaderNotice from './HeaderNotice' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const appStore = useAppStore() |
| | | const userStore = useUserStore() |
| | | const lockStore = useLockStore() |
| | | const settingsStore = useSettingsStore() |
| | | |
| | | const avatarText = computed(() => (userStore.nickName || '').substring(0, 1)) |
| | | |
| | | function toggleSideBar() { |
| | | appStore.toggleSideBar() |
| | | } |
| | | |
| | | function handleCommand(command) { |
| | | switch (command) { |
| | | case "setLayout": |
| | | setLayout() |
| | | break |
| | | case "lockScreen": |
| | | lockScreen() |
| | | break |
| | | case "logout": |
| | | logout() |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | function logout() { |
| | |
| | | }) |
| | | }).catch(() => { }) |
| | | } |
| | | |
| | | const emits = defineEmits(['setLayout']) |
| | | function setLayout() { |
| | | emits('setLayout') |
| | | } |
| | | |
| | | function lockScreen() { |
| | | const currentPath = route.fullPath |
| | | lockStore.lockScreen(currentPath) |
| | | router.push('/lock') |
| | | } |
| | | |
| | | async function toggleTheme(event) { |
| | | const x = event?.clientX || window.innerWidth / 2 |
| | | const y = event?.clientY || window.innerHeight / 2 |
| | | const wasDark = settingsStore.isDark |
| | | |
| | | const isReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches |
| | | const isSupported = document.startViewTransition && !isReducedMotion |
| | | |
| | | if (!isSupported) { |
| | | settingsStore.toggleTheme() |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const transition = document.startViewTransition(async () => { |
| | | await new Promise((resolve) => setTimeout(resolve, 10)) |
| | | settingsStore.toggleTheme() |
| | | await nextTick() |
| | | }) |
| | | await transition.ready |
| | | |
| | | const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y)) |
| | | const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`] |
| | | document.documentElement.animate( |
| | | { |
| | | clipPath: !wasDark ? [...clipPath].reverse() : clipPath |
| | | }, { |
| | | duration: 650, |
| | | easing: "cubic-bezier(0.4, 0, 0.2, 1)", |
| | | fill: "forwards", |
| | | pseudoElement: !wasDark ? "::view-transition-old(root)" : "::view-transition-new(root)" |
| | | } |
| | | ) |
| | | await transition.finished |
| | | } catch (error) { |
| | | console.warn("View transition failed, falling back to immediate toggle:", error) |
| | | settingsStore.toggleTheme() |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang='scss' scoped> |
| | | .navbar.nav3 { |
| | | .hamburger-container { |
| | | display: none !important; |
| | | } |
| | | } |
| | | |
| | | <style lang="scss" scoped> |
| | | .navbar { |
| | | height: 50px; |
| | | overflow: hidden; |
| | | position: relative; |
| | | background: var(--navbar-bg); |
| | | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); |
| | | background: var(--lims-surface); |
| | | border-bottom: 1px solid var(--lims-divider); |
| | | box-shadow: none; |
| | | position: fixed; /* 将头部固定 */ |
| | | top: 0; /* 在顶部固定 */ |
| | | width: 100%; /* 宽度100%,覆盖整个视口 */ |
| | | z-index: 1000; /* 确保头部在其他内容之上 */ |
| | | display: flex; |
| | | align-items: center; |
| | | // padding: 0 8px; |
| | | box-sizing: border-box; |
| | | justify-content: space-between; |
| | | |
| | | .hamburger-container { |
| | | line-height: 46px; |
| | | height: 100%; |
| | | cursor: pointer; |
| | | transition: background 0.3s; |
| | | transition: background 0.2s; |
| | | -webkit-tap-highlight-color: transparent; |
| | | display: flex; |
| | | align-items: center; |
| | | flex-shrink: 0; |
| | | margin-right: 8px; |
| | | margin-left: 8px; |
| | | border-radius: var(--lims-radius-sm); |
| | | |
| | | &:hover { |
| | | background: rgba(0, 0, 0, 0.025); |
| | | background: var(--lims-surface-hover); |
| | | } |
| | | } |
| | | |
| | | .breadcrumb-container { |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .topmenu-container { |
| | | position: absolute; |
| | | left: 50px; |
| | | } |
| | | |
| | | .topbar-container { |
| | | flex: 1; |
| | | min-width: 0; |
| | | .center-menu { |
| | | height: 100%; |
| | | line-height: 50px; |
| | | margin-right: auto; |
| | | margin-left: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | overflow: hidden; |
| | | margin-left: 8px; |
| | | |
| | | /* 标题左侧的主色细条,呼应弹窗标题的视觉语言 */ |
| | | &::before { |
| | | content: ''; |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: var(--el-color-primary); |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .label { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: var(--lims-text); |
| | | letter-spacing: 0.2px; |
| | | } |
| | | } |
| | | |
| | | .right-menu { |
| | |
| | | line-height: 50px; |
| | | display: flex; |
| | | align-items: center; |
| | | margin-left: auto; |
| | | |
| | | &:focus { |
| | | outline: none; |
| | | } |
| | | |
| | | .avatar-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | :deep(.el-avatar) { |
| | | background: var(--el-color-primary); |
| | | border-radius: var(--lims-radius-sm); |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .userName { |
| | | font-size: 14px; |
| | | color: var(--lims-text-2); |
| | | margin-left: 10px; |
| | | } |
| | | .logoout { |
| | | margin: 0 40px; |
| | | cursor: pointer; |
| | | width: 14px; |
| | | opacity: 0.6; |
| | | transition: opacity 0.2s; |
| | | |
| | | &:hover { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .right-menu-item { |
| | |
| | | padding: 0 8px; |
| | | height: 100%; |
| | | font-size: 18px; |
| | | color: #5a5e66; |
| | | color: var(--lims-text-2); |
| | | vertical-align: text-bottom; |
| | | |
| | | &.hover-effect { |
| | | cursor: pointer; |
| | | transition: background 0.3s; |
| | | transition: background 0.2s; |
| | | |
| | | &:hover { |
| | | background: rgba(0, 0, 0, 0.025); |
| | | } |
| | | } |
| | | |
| | | &.theme-switch-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | svg { |
| | | transition: transform 0.3s; |
| | | |
| | | &:hover { |
| | | transform: scale(1.15); |
| | | } |
| | | background: var(--lims-surface-hover); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .avatar-container { |
| | | margin-right: 0px; |
| | | padding-right: 0px; |
| | | |
| | | .avatar-wrapper { |
| | | margin-top: 10px; |
| | | right: 8px; |
| | | position: relative; |
| | | |
| | | .user-avatar { |
| | | cursor: pointer; |
| | | width: 30px; |
| | | height: 30px; |
| | | margin-right: 8px; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | .user-nickname{ |
| | | position: relative; |
| | | left: 0px; |
| | | bottom: 10px; |
| | | font-size: 14px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | i { |
| | | cursor: pointer; |
| | | position: absolute; |
| | | right: -20px; |
| | | top: 25px; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | } |