refactor: ♻️ 重构优化导航栏代码

This commit is contained in:
Ray.Hao
2025-05-22 14:33:28 +08:00
parent e90775fae9
commit 54b2164d25
8 changed files with 281 additions and 255 deletions

View File

@@ -1,83 +1,81 @@
<template>
<div class="notification">
<el-dropdown class="h-full flex-center" trigger="click">
<el-badge v-if="noticeList.length > 0" :offset="[0, 15]" :value="noticeList.length" :max="99">
<div class="i-svg:bell" />
</el-badge>
<el-dropdown trigger="click">
<el-badge v-if="noticeList.length > 0" :value="noticeList.length" :max="99">
<div class="i-svg:bell" />
</el-badge>
<div v-else class="i-svg:bell" />
<div v-else class="i-svg:bell" />
<template #dropdown>
<div class="p-5">
<template v-if="noticeList.length > 0">
<div v-for="(item, index) in noticeList" :key="index" class="w-500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-text
size="small"
class="w-200px cursor-pointer !ml-2 !flex-1"
truncated
@click="handleReadNotice(item.id)"
>
{{ item.title }}
</el-text>
<template #dropdown>
<div class="p-5">
<template v-if="noticeList.length > 0">
<div v-for="(item, index) in noticeList" :key="index" class="w-500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-text
size="small"
class="w-200px cursor-pointer !ml-2 !flex-1"
truncated
@click="handleReadNotice(item.id)"
>
{{ item.title }}
</el-text>
<div class="text-xs text-gray">
{{ item.publishTime }}
</div>
<div class="text-xs text-gray">
{{ item.publishTime }}
</div>
</div>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" underline="never" @click="handleViewMoreNotice">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="noticeList.length > 0"
type="primary"
underline="never"
@click="handleMarkAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</template>
<template v-else>
<div class="flex-center h-150px w-350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
</div>
</template>
</el-dropdown>
<el-dialog
v-model="noticeDialogVisible"
:title="noticeDetail?.title ?? '通知详情'"
width="800px"
custom-class="notification-detail"
>
<div v-if="noticeDetail" class="p-x-20px">
<div class="flex-y-center mb-16px text-13px text-color-secondary">
<span class="flex-y-center">
<el-icon><User /></el-icon>
{{ noticeDetail.publisherName }}
</span>
<span class="ml-2 flex-y-center">
<el-icon><Timer /></el-icon>
{{ noticeDetail.publishTime }}
</span>
</div>
<div class="max-h-60vh pt-16px mb-24px overflow-y-auto border-t border-solid border-color">
<div v-html="noticeDetail.content"></div>
</div>
</div>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" underline="never" @click="handleViewMoreNotice">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="noticeList.length > 0"
type="primary"
underline="never"
@click="handleMarkAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</template>
<template v-else>
<div class="flex-center h-150px w-350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
</div>
</el-dialog>
</div>
</template>
</el-dropdown>
<el-dialog
v-model="noticeDialogVisible"
:title="noticeDetail?.title ?? '通知详情'"
width="800px"
custom-class="notification-detail"
>
<div v-if="noticeDetail" class="p-x-20px">
<div class="flex-y-center mb-16px text-13px text-color-secondary">
<span class="flex-y-center">
<el-icon><User /></el-icon>
{{ noticeDetail.publisherName }}
</span>
<span class="ml-2 flex-y-center">
<el-icon><Timer /></el-icon>
{{ noticeDetail.publishTime }}
</span>
</div>
<div class="max-h-60vh pt-16px mb-24px overflow-y-auto border-t border-solid border-color">
<div v-html="noticeDetail.content"></div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
@@ -162,14 +160,4 @@ onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.notification {
:deep(.el-badge) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
}
</style>
<style lang="scss" scoped></style>

View File

@@ -2,9 +2,7 @@
<!-- 布局大小 -->
<el-tooltip :content="t('sizeSelect.tooltip')" effect="dark" placement="bottom">
<el-dropdown trigger="click" @command="handleSizeChange">
<div>
<div class="i-svg:size" />
</div>
<div class="i-svg:size" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item

View File

@@ -0,0 +1,178 @@
<template>
<div :class="['navbar-actions', navbarActionsClass]">
<!-- 桌面端工具项 -->
<template v-if="isDesktop">
<!-- 搜索 -->
<div class="navbar-actions__item">
<MenuSearch />
</div>
<!-- 全屏 -->
<div class="navbar-actions__item">
<Fullscreen />
</div>
<!-- 布局大小 -->
<div class="navbar-actions__item">
<SizeSelect />
</div>
<!-- 语言选择 -->
<div class="navbar-actions__item">
<LangSelect />
</div>
<!-- 通知 -->
<div class="navbar-actions__item">
<Notification />
</div>
</template>
<!-- 用户菜单 -->
<div class="navbar-actions__item">
<el-dropdown trigger="click">
<div class="user-profile">
<img class="user-profile__avatar" :src="userStore.userInfo.avatar" />
<span class="user-profile__name">{{ userStore.userInfo.username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleProfileClick">
{{ t("navbar.profile") }}
</el-dropdown-item>
<el-dropdown-item divided @click="logout">
{{ t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<!-- 系统设置 -->
<div
v-if="defaultSettings.showSettings"
class="navbar-actions__item"
@click="settingStore.settingsVisible = true"
>
<div class="i-svg:setting" />
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from "element-plus";
import defaultSettings from "@/settings";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { useAppStore, useSettingsStore, useUserStore } from "@/store";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
const { t } = useI18n();
const appStore = useAppStore();
const settingStore = useSettingsStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
// 是否为桌面设备
const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP);
/**
* 打开个人中心页面
*/
function handleProfileClick() {
router.push({ name: "Profile" });
}
// 根据主题和侧边栏配色方案选择样式类
const navbarActionsClass = computed(() => {
// 暗黑主题
if (settingStore.theme === ThemeMode.DARK) {
return "navbar-actions--white-text";
}
// 经典蓝侧边栏
if (settingStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
return "navbar-actions--white-text";
}
});
/**
* 退出登录
*/
function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
lockScroll: false,
}).then(() => {
userStore.logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped>
.navbar-actions {
display: flex;
align-items: center;
height: 100%;
&__item {
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: $navbar-height;
text-align: center;
cursor: pointer;
:deep([class^="i-svg:"]) {
color: var(--el-text-color);
&:hover {
color: var(--el-text-color-primary);
}
}
&:hover {
color: var(--el-text-color-primary);
background: rgb(0 0 0 / 10%);
}
}
.user-profile {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 13px;
&__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
&__name {
margin-left: 10px;
}
}
}
.layout-top,
.layout-mix {
.user-profile__name {
color: #fff !important;
}
}
.layout-top .navbar-actions--white-text :deep([class^="i-svg:"]),
.layout-mix .navbar-actions--white-text :deep([class^="i-svg:"]) {
color: #fff !important;
}
</style>

View File

@@ -1,150 +0,0 @@
<template>
<div :class="['navbar__right', navbarRightClass]">
<!-- 桌面端显示 -->
<template v-if="isDesktop">
<!-- 搜索 -->
<MenuSearch />
<!-- 全屏 -->
<Fullscreen />
<!-- 布局大小 -->
<SizeSelect />
<!-- 语言选择 -->
<LangSelect />
<!-- 通知下拉 -->
<Notification />
</template>
<!-- 用户头像个人中心退出登录等 -->
<el-dropdown trigger="click">
<div class="user-profile">
<img class="user-profile__avatar" :src="userStore.userInfo.avatar" />
<span class="user-profile__name">{{ userStore.userInfo.username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleProfileClick">
{{ t("navbar.profile") }}
</el-dropdown-item>
<el-dropdown-item divided @click="logout">
{{ t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 设置面板 -->
<div v-if="defaultSettings.showSettings" @click="settingStore.settingsVisible = true">
<div class="i-svg:setting" />
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n();
import defaultSettings from "@/settings";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { useAppStore, useSettingsStore, useUserStore } from "@/store";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
const appStore = useAppStore();
const settingStore = useSettingsStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP);
/**
* 打开个人中心页面
*/
function handleProfileClick() {
router.push({ name: "Profile" });
}
// 根据主题和侧边栏配色方案选择 navbar 右侧的样式类
const navbarRightClass = computed(() => {
// 如果暗黑主题
if (settingStore.theme === ThemeMode.DARK) {
return "navbar__right--white";
}
// 如果侧边栏是经典蓝
if (settingStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
return "navbar__right--white";
}
});
/**
* 退出登录
*/
function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
lockScroll: false,
}).then(() => {
userStore.logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped>
.navbar__right {
display: flex;
align-items: center;
justify-content: center;
& > * {
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: $navbar-height;
color: var(--el-text-color);
text-align: center;
cursor: pointer;
&:hover {
background: rgb(0 0 0 / 10%);
}
}
.user-profile {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 13px;
&__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
&__name {
margin-left: 10px;
}
}
}
.layout-top .navbar__right--white > *,
.layout-mix .navbar__right--white > * {
color: #fff;
}
.layout-top .navbar__right--white :deep([class^="i-svg:"]),
.layout-mix .navbar__right--white :deep([class^="i-svg:"]) {
color: #fff !important;
}
.dark .navbar__right > *:hover {
color: #ccc;
}
</style>

View File

@@ -1,25 +1,30 @@
<template>
<div class="navbar">
<div class="navbar__left">
<!-- 展开/收缩菜单 -->
<div class="flex-y-center">
<!-- 菜单折叠按钮 -->
<Hamburger :is-active="isSidebarOpened" @toggle-click="toggleSideBar" />
<!-- 面包屑 -->
<!-- 面包屑导航 -->
<Breadcrumb />
</div>
<!-- 导航栏右侧 -->
<NavbarRight />
<!-- 导航栏操作区域 -->
<div class="navbar__actions">
<NavbarActions />
</div>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from "@/store";
import Hamburger from "@/components/Hamburger/index.vue";
import Breadcrumb from "@/components/Breadcrumb/index.vue";
import NavbarActions from "./components/NavbarActions.vue";
const appStore = useAppStore();
// 侧边栏是否打开
// 侧边栏展开状态
const isSidebarOpened = computed(() => appStore.sidebar.opened);
// 展开/收缩菜单
// 切换侧边栏展开/折叠状态
function toggleSideBar() {
appStore.toggleSidebar();
}
@@ -32,7 +37,7 @@ function toggleSideBar() {
height: $navbar-height;
background: var(--el-bg-color);
&__left {
&__actions {
display: flex;
align-items: center;
}

View File

@@ -4,7 +4,7 @@
<div v-if="layout == LayoutMode.MIX" class="flex w-full">
<SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" />
<SidebarMixTopMenu class="flex-1" />
<NavbarRight />
<NavbarActions />
</div>
<!-- 顶部布局 || 左侧布局 -->
@@ -15,16 +15,16 @@
</el-scrollbar>
<!-- 顶部导航 -->
<NavbarRight v-if="layout == LayoutMode.TOP" />
<NavbarActions v-if="layout == LayoutMode.TOP" />
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import NavbarRight from "../NavBar/components/NavbarRight.vue";
import NavbarActions from "../NavBar/components/NavbarActions.vue";
const appStore = useAppStore();
const settingsStore = useSettingsStore();

View File

@@ -71,10 +71,11 @@ declare module "vue" {
Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"];
IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"];
LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"];
LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"];
MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"];
MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"];
NavbarActions: (typeof import("./../layout/components/NavBar/components/NavbarActions.vue"))["default"];
Notification: (typeof import("./../components/Notification/index.vue"))["default"];
LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"];
PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"];
PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"];
PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"];

View File

@@ -122,9 +122,12 @@ const route = useRoute();
onMounted(() => getCaptcha());
const loginFormRef = ref<FormInstance>();
const loading = ref(false); // 按钮 loading 状态
const isCapsLock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
const loading = ref(false);
// 是否大写锁定
const isCapsLock = ref(false);
// 验证码图片Base64字符串
const captchaBase64 = ref();
// 记住我
const rememberMe = Auth.getRememberMe();
const loginFormData = ref<LoginFormData>({
@@ -178,7 +181,9 @@ function getCaptcha() {
.finally(() => (codeLoading.value = false));
}
// 登录提交处理
/**
* 登录提交
*/
async function handleLoginSubmit() {
try {
// 1. 表单验证
@@ -211,8 +216,9 @@ async function handleLoginSubmit() {
/**
* 解析重定向目标
*
* @param query 路由查询参数
* @returns 标准化后的路由地址对象
* @returns 标准化后的路由地址
*/
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
// 默认跳转路径