feat: 项目结构重构优化
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<router-link :key="+collapse" class="wh-full flex-center" to="/">
|
||||
<img :src="logo" class="w20px h20px" />
|
||||
<span v-if="!collapse" class="title">
|
||||
{{ defaultSettings.title }}
|
||||
{{ appConfig.title }}
|
||||
</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
@@ -12,8 +12,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defaultSettings } from "@/settings";
|
||||
import logo from "@/assets/logo.png";
|
||||
import { appConfig } from "@/settings";
|
||||
import logo from "@/assets/images/logo.png";
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
@@ -40,7 +40,7 @@ defineProps({
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// 顶部布局和混合布局的特殊处理
|
||||
// 顶部布局和混合布局的特殊处<EFBFBD>?
|
||||
.layout-top,
|
||||
.layout-mix {
|
||||
.logo {
|
||||
@@ -3,12 +3,12 @@
|
||||
<div class="flex-y-center">
|
||||
<!-- 菜单折叠按钮 -->
|
||||
<Hamburger :is-active="isSidebarOpened" @toggle-click="toggleSideBar" />
|
||||
<!-- 面包屑导航 -->
|
||||
<!-- é<EFBFBD>¢åŒ…屑导èˆ?-->
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<!-- 导航栏操作区域 -->
|
||||
<!-- 导航æ <EFBFBD>æ“<EFBFBD>作区åŸ?-->
|
||||
<div class="navbar__actions">
|
||||
<NavbarActions />
|
||||
<LayoutToolbar />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,19 +17,13 @@
|
||||
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() {
|
||||
console.log("🔄 Hamburger clicked! Current state:", isSidebarOpened.value);
|
||||
console.log("🔄 Device type:", appStore.device);
|
||||
appStore.toggleSidebar();
|
||||
console.log("🔄 New state:", appStore.sidebar.opened);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
size="380"
|
||||
@@ -48,6 +48,22 @@
|
||||
<span class="text-xs">{{ t("settings.showWatermark") }}</span>
|
||||
<el-switch v-model="settingsStore.showWatermark" />
|
||||
</div>
|
||||
|
||||
<div class="config-item flex-x-between">
|
||||
<span class="text-xs">灰色模式</span>
|
||||
<el-switch v-model="settingsStore.grayMode" />
|
||||
</div>
|
||||
|
||||
<div class="config-item flex-x-between">
|
||||
<span class="text-xs">色弱模式</span>
|
||||
<el-switch v-model="settingsStore.colorWeak" />
|
||||
</div>
|
||||
|
||||
<div v-if="aiSystemEnabled" class="config-item flex-x-between">
|
||||
<span class="text-xs">AI 助手</span>
|
||||
<el-switch v-model="settingsStore.userEnableAi" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isDark" class="config-item flex-x-between">
|
||||
<span class="text-xs">{{ t("settings.sidebarColorScheme") }}</span>
|
||||
<el-radio-group v-model="sidebarColor" @change="changeSidebarColor">
|
||||
@@ -65,7 +81,7 @@
|
||||
<section class="config-section">
|
||||
<el-divider>{{ t("settings.navigation") }}</el-divider>
|
||||
|
||||
<!-- 整合的布局选择器 -->
|
||||
<!-- 整合的布局选择 -->
|
||||
<div class="layout-select">
|
||||
<div class="layout-grid">
|
||||
<el-tooltip
|
||||
@@ -110,7 +126,7 @@
|
||||
<template #footer>
|
||||
<div class="action-buttons">
|
||||
<el-tooltip
|
||||
content="复制配置将生成当前设置的代码,覆盖 src/settings.ts 下的 defaultSettings 变量"
|
||||
content="复制配置将生成当前设置的代码,覆盖到 `src/settings.ts` 下的 `defaultSettings` 变量"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
@@ -145,12 +161,15 @@ import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
|
||||
const { t } = useI18n();
|
||||
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
|
||||
import { useSettingsStore } from "@/store";
|
||||
import { themeColorPresets } from "@/settings";
|
||||
import { themeColorPresets, appConfig } from "@/settings";
|
||||
|
||||
// 按钮图标
|
||||
const copyIcon = markRaw(DocumentCopy);
|
||||
const resetIcon = markRaw(RefreshLeft);
|
||||
|
||||
// AI 系统级开关
|
||||
const aiSystemEnabled = appConfig.aiEnabled;
|
||||
|
||||
// 加载状态
|
||||
const copyLoading = ref(false);
|
||||
const resetLoading = ref(false);
|
||||
@@ -168,8 +187,8 @@ const layoutOptions: LayoutOption[] = [
|
||||
{ value: LayoutMode.MIX, label: t("settings.mixLayout"), className: "mix" },
|
||||
];
|
||||
|
||||
// 使用统一的颜色预设配置
|
||||
const colorPresets = themeColorPresets;
|
||||
// 使用统一的颜色预设配置(复制为可变数组以兼容组件 prop)
|
||||
const colorPresets = [...themeColorPresets];
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
@@ -178,7 +197,9 @@ const sidebarColor = ref(settingsStore.sidebarColorScheme);
|
||||
|
||||
const selectedThemeColor = computed({
|
||||
get: () => settingsStore.themeColor,
|
||||
set: (value) => settingsStore.updateThemeColor(value),
|
||||
set: (value) => {
|
||||
settingsStore.themeColor = value;
|
||||
},
|
||||
});
|
||||
|
||||
const drawerVisible = computed({
|
||||
@@ -192,7 +213,7 @@ const drawerVisible = computed({
|
||||
* @param isDark 是否启用暗黑模式
|
||||
*/
|
||||
const handleThemeChange = (isDark: string | number | boolean) => {
|
||||
settingsStore.updateTheme(isDark ? ThemeMode.DARK : ThemeMode.LIGHT);
|
||||
settingsStore.theme = isDark ? ThemeMode.DARK : ThemeMode.LIGHT;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -201,7 +222,7 @@ const handleThemeChange = (isDark: string | number | boolean) => {
|
||||
* @param val 颜色方案名称
|
||||
*/
|
||||
const changeSidebarColor = (val: any) => {
|
||||
settingsStore.updateSidebarColorScheme(val);
|
||||
settingsStore.sidebarColorScheme = val;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -212,7 +233,7 @@ const changeSidebarColor = (val: any) => {
|
||||
const handleLayoutChange = (layout: LayoutMode) => {
|
||||
if (settingsStore.layout === layout) return;
|
||||
|
||||
settingsStore.updateLayout(layout);
|
||||
settingsStore.layout = layout;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -249,7 +270,7 @@ const handleResetSettings = async () => {
|
||||
try {
|
||||
settingsStore.resetSettings();
|
||||
|
||||
// 同步更新本地状态
|
||||
// 同步更新本地状态"
|
||||
isDark.value = settingsStore.theme === ThemeMode.DARK;
|
||||
sidebarColor.value = settingsStore.sidebarColorScheme;
|
||||
|
||||
@@ -319,7 +340,7 @@ const handleCloseDrawer = () => {
|
||||
|
||||
/* 设置内容区域 */
|
||||
.settings-content {
|
||||
height: calc(100vh - 120px); /* 减去头部和底部按钮的高度 */
|
||||
max-height: calc(100vh - 120px);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -340,7 +361,6 @@ const handleCloseDrawer = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 主题切换器优化 */
|
||||
.theme-switch {
|
||||
transform: scale(1.2);
|
||||
@@ -571,7 +591,6 @@ const handleCloseDrawer = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 复制配置对话框样式 */
|
||||
:deep(.copy-config-dialog) {
|
||||
.el-message-box__content {
|
||||
max-height: 400px;
|
||||
@@ -14,8 +14,8 @@
|
||||
@open="onMenuOpen"
|
||||
@close="onMenuClose"
|
||||
>
|
||||
<!-- 菜单项 -->
|
||||
<MenuItem
|
||||
<!-- è<EFBFBD>œå<EFBFBD>•é¡?-->
|
||||
<LayoutSidebarItem
|
||||
v-for="route in data"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
@@ -32,7 +32,7 @@ import type { RouteRecordRaw } from "vue-router";
|
||||
import { SidebarColor } from "@/enums/settings";
|
||||
import { useSettingsStore, useAppStore } from "@/store";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import MenuItem from "./components/MenuItem.vue";
|
||||
import LayoutSidebarItem from "./LayoutSidebarItem.vue";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -63,10 +63,10 @@ const expandedMenuIndexes = ref<string[]>([]);
|
||||
// 获取主题
|
||||
const theme = computed(() => settingsStore.theme);
|
||||
|
||||
// 获取浅色主题下的侧边栏配色方案
|
||||
// 获å<EFBFBD>–浅色主题下的侧边æ <EFBFBD>é…<EFBFBD>色方æ¡?
|
||||
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
|
||||
|
||||
// 菜单主题属性
|
||||
// è<EFBFBD>œå<EFBFBD>•主题属æ€?
|
||||
const menuThemeProps = computed(() => {
|
||||
const isDarkOrClassicBlue =
|
||||
theme.value === "dark" || sidebarColorScheme.value === SidebarColor.CLASSIC_BLUE;
|
||||
@@ -78,11 +78,11 @@ const menuThemeProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
// 计算当前激活的菜单项
|
||||
// 计算当å‰<EFBFBD>激活的è<EFBFBD>œå<EFBFBD>•é¡?
|
||||
const activeMenuPath = computed((): string => {
|
||||
const { meta, path } = currentRoute;
|
||||
|
||||
// 如果路由meta中设置了activeMenu,则使用它(用于处理一些特殊情况,如详情页)
|
||||
// 如果路由metaä¸è®¾ç½®äº†activeMenu,则使用它(用于处ç<EFBFBD>†ä¸€äº›ç‰¹æ®Šæƒ…况,如详情页ï¼?
|
||||
if (meta?.activeMenu && typeof meta.activeMenu === "string") {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
@@ -94,8 +94,8 @@ const activeMenuPath = computed((): string => {
|
||||
/**
|
||||
* 获取完整路径
|
||||
*
|
||||
* @param routePath 当前路由的相对路径 /user
|
||||
* @returns 完整的绝对路径 D://vue3-element-admin/system/user
|
||||
* @param routePath 当å‰<EFBFBD>路由的相对路å¾? /user
|
||||
* @returns 完整的ç»<EFBFBD>对路å¾?D://vue3-element-admin/system/user
|
||||
*/
|
||||
function resolveFullPath(routePath: string) {
|
||||
if (isExternal(routePath)) {
|
||||
@@ -143,8 +143,8 @@ watch(
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听菜单模式变化:当菜单模式切换为水平模式时,关闭所有展开的菜单项,
|
||||
* 避免在水平模式下菜单项显示错位。
|
||||
* 监å<EFBFBD>¬è<EFBFBD>œå<EFBFBD>•模å¼<EFBFBD>å<EFBFBD>˜åŒ–:当è<EFBFBD>œå<EFBFBD>•模å¼<EFBFBD>切æ<EFBFBD>¢ä¸ºæ°´å¹³æ¨¡å¼<EFBFBD>æ—¶ï¼Œå…³é—æ‰€æœ‰å±•开的è<EFBFBD>œå<EFBFBD>•项ï¼?
|
||||
* é<EFBFBD>¿å…<EFBFBD>在水平模å¼<EFBFBD>下è<EFBFBD>œå<EFBFBD>•项显示错ä½<EFBFBD>ã€?
|
||||
*/
|
||||
watch(
|
||||
() => props.menuMode,
|
||||
@@ -156,7 +156,7 @@ watch(
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听激活菜单变化,为包含激活子菜单的父菜单添加样式类
|
||||
* 监å<EFBFBD>¬æ¿€æ´»è<EFBFBD>œå<EFBFBD>•å<EFBFBD>˜åŒ–,为包å<EFBFBD>«æ¿€æ´»å<EFBFBD>è<EFBFBD>œå<EFBFBD>•的父è<EFBFBD>œå<EFBFBD>•æ·»åŠ æ ·å¼<EFBFBD>ç±?
|
||||
*/
|
||||
watch(
|
||||
() => activeMenuPath.value,
|
||||
@@ -169,7 +169,7 @@ watch(
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听路由变化,确保菜单能随TagsView切换而正确激活
|
||||
* 监å<EFBFBD>¬è·¯ç”±å<EFBFBD>˜åŒ–,确ä¿<EFBFBD>è<EFBFBD>œå<EFBFBD>•能éš<EFBFBD>TagsView切æ<EFBFBD>¢è€Œæ£ç¡®æ¿€æ´?
|
||||
*/
|
||||
watch(
|
||||
() => currentRoute.path,
|
||||
@@ -181,7 +181,7 @@ watch(
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新父菜单样式 - 为包含激活子菜单的父菜单添加 has-active-child 类
|
||||
* 更新父è<EFBFBD>œå<EFBFBD>•æ ·å¼?- 为包å<EFBFBD>«æ¿€æ´»å<EFBFBD>è<EFBFBD>œå<EFBFBD>•的父è<EFBFBD>œå<EFBFBD>•æ·»åŠ has-active-child ç±?
|
||||
*/
|
||||
function updateParentMenuStyles() {
|
||||
if (!menuRef.value?.$el) return;
|
||||
@@ -191,13 +191,13 @@ function updateParentMenuStyles() {
|
||||
const menuEl = menuRef.value?.$el as HTMLElement;
|
||||
if (!menuEl) return;
|
||||
|
||||
// 移除所有现有的 has-active-child 类
|
||||
// 移除所有现有的 has-active-child �
|
||||
const allSubMenus = menuEl.querySelectorAll(".el-sub-menu");
|
||||
allSubMenus.forEach((subMenu) => {
|
||||
subMenu.classList.remove("has-active-child");
|
||||
});
|
||||
|
||||
// 查找当前激活的菜单项
|
||||
// 查找当å‰<EFBFBD>激活的è<EFBFBD>œå<EFBFBD>•é¡?
|
||||
const activeMenuItem = menuEl.querySelector(".el-menu-item.is-active");
|
||||
|
||||
if (activeMenuItem) {
|
||||
@@ -210,12 +210,12 @@ function updateParentMenuStyles() {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
} else {
|
||||
// 水平模式下可能需要特殊处理
|
||||
// 水平模å¼<EFBFBD>下å<EFBFBD>¯èƒ½éœ€è¦<EFBFBD>特殊处ç<EFBFBD>?
|
||||
if (props.menuMode === "horizontal") {
|
||||
// 对于水平菜单,使用路径匹配来找到父菜单
|
||||
// 对于水平è<EFBFBD>œå<EFBFBD>•,使用路径匹é…<EFBFBD>æ<EFBFBD>¥æ‰¾åˆ°çˆ¶è<EFBFBD>œå<EFBFBD>?
|
||||
const currentPath = activeMenuPath.value;
|
||||
|
||||
// 查找所有父菜单项,检查哪个包含当前路径
|
||||
// 查找所有父è<EFBFBD>œå<EFBFBD>•项,检查哪个包å<EFBFBD>«å½“å‰<EFBFBD>è·¯å¾?
|
||||
allSubMenus.forEach((subMenu) => {
|
||||
const subMenuEl = subMenu as HTMLElement;
|
||||
const subMenuPath =
|
||||
@@ -239,7 +239,7 @@ function updateParentMenuStyles() {
|
||||
* 组件挂载后立即更新父菜单样式
|
||||
*/
|
||||
onMounted(() => {
|
||||
// 确保在组件挂载后更新样式,不依赖于异步操作
|
||||
// ç¡®ä¿<EFBFBD>在组件挂载å<EFBFBD>Žæ›´æ–°æ ·å¼<EFBFBD>,ä¸<EFBFBD>ä¾<EFBFBD>èµ–äºŽå¼‚æ¥æ“<EFBFBD>ä½?
|
||||
updateParentMenuStyles();
|
||||
});
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden">
|
||||
<!--【叶子节点】显示叶子节点或唯一子节点且父节点未配置始终显示 -->
|
||||
<template
|
||||
@@ -22,11 +22,12 @@
|
||||
:index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||
>
|
||||
<MenuItemContent
|
||||
v-if="onlyOneChild.meta"
|
||||
:icon="onlyOneChild.meta.icon || item.meta?.icon"
|
||||
:title="onlyOneChild.meta.title"
|
||||
/>
|
||||
<template v-if="onlyOneChild.meta">
|
||||
<MenuIcon :icon="onlyOneChild.meta.icon || item.meta?.icon" />
|
||||
<span v-if="onlyOneChild.meta.title" class="ml-1">
|
||||
{{ translateRouteTitle(onlyOneChild.meta.title) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</AppLink>
|
||||
</template>
|
||||
@@ -34,10 +35,15 @@
|
||||
<!--【非叶子节点】显示含多个子节点的父菜单,或始终显示的单子节点 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" :data-path="item.path" teleported>
|
||||
<template #title>
|
||||
<MenuItemContent v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
|
||||
<template v-if="item.meta">
|
||||
<MenuIcon :icon="item.meta.icon" />
|
||||
<span v-if="item.meta.title" class="ml-1">
|
||||
{{ translateRouteTitle(item.meta.title) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<MenuItem
|
||||
<LayoutSidebarItem
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
@@ -49,17 +55,39 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MenuItemContent from "./MenuItemContent.vue";
|
||||
import path from "path-browserify";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { isExternal } from "@/utils";
|
||||
import { translateRouteTitle } from "@/lang/utils";
|
||||
import { ElIcon } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
name: "MenuItem",
|
||||
name: "LayoutSidebarItem",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import path from "path-browserify";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
// 菜单图标组件
|
||||
const MenuIcon = defineComponent({
|
||||
props: { icon: String },
|
||||
setup(props) {
|
||||
const isElIcon = computed(() => props.icon?.startsWith("el-icon"));
|
||||
const iconName = computed(() => props.icon?.replace("el-icon-", ""));
|
||||
|
||||
import { isExternal } from "@/utils";
|
||||
return () => {
|
||||
if (!props.icon) {
|
||||
return h("div", { class: "i-svg:menu" });
|
||||
}
|
||||
|
||||
// Element Plus 图标
|
||||
if (isElIcon.value) {
|
||||
return h(ElIcon, null, () => h(resolveComponent(iconName.value!)));
|
||||
}
|
||||
|
||||
// SVG 图标
|
||||
return h("div", { class: `i-svg:${props.icon}` });
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
@@ -112,7 +140,7 @@ function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecord
|
||||
return true;
|
||||
}
|
||||
|
||||
// 无子节点时
|
||||
// 无子节点
|
||||
if (showingChildren.length === 0) {
|
||||
// 父节点设置为唯一显示节点,并标记为无子节点
|
||||
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||
@@ -137,7 +165,57 @@ function resolvePath(routePath: string) {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* 菜单图标统一样式 */
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
.el-icon {
|
||||
width: 1em !important;
|
||||
margin-right: 0 !important;
|
||||
font-size: 18px;
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
[class^="i-svg:"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
color: currentcolor !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 折叠状态下的图标样式 - 确保 SVG 图标不被压缩 */
|
||||
.el-menu--collapse {
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
[class^="i-svg:"] {
|
||||
width: 18px !important;
|
||||
min-width: 18px !important;
|
||||
height: 18px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* tooltip 弹出层中的图标 */
|
||||
.el-tooltip__trigger {
|
||||
[class^="i-svg:"] {
|
||||
width: 18px !important;
|
||||
min-width: 18px !important;
|
||||
height: 18px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* hideSidebar 状态下的图标 */
|
||||
.hideSidebar {
|
||||
[class^="i-svg:"] {
|
||||
width: 18px !important;
|
||||
min-width: 18px !important;
|
||||
height: 18px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.submenu-title-noDropdown {
|
||||
position: relative;
|
||||
|
||||
@@ -203,7 +281,7 @@ html.sidebar-color-blue {
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题下的父菜单激活状态
|
||||
// 深色主题下的父菜单激活状态"
|
||||
html.dark & {
|
||||
&.has-active-child > .el-sub-menu__title {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
@@ -215,7 +293,7 @@ html.sidebar-color-blue {
|
||||
}
|
||||
}
|
||||
|
||||
// 深蓝色侧边栏配色下的父菜单激活状态
|
||||
// 深蓝色侧边栏配色下的父菜单激活状态"
|
||||
html.sidebar-color-blue & {
|
||||
&.has-active-child > .el-sub-menu__title {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
@@ -227,4 +305,5 @@ html.sidebar-color-blue {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<!-- 水平滚动容器 -->
|
||||
<el-scrollbar
|
||||
@@ -70,7 +70,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter, type RouteRecordRaw } from "vue-router";
|
||||
import { resolve } from "path-browserify";
|
||||
import { translateRouteTitle } from "@/utils/i18n";
|
||||
import { translateRouteTitle } from "@/lang/utils";
|
||||
import { usePermissionStore, useTagsViewStore } from "@/store";
|
||||
|
||||
interface ContextMenu {
|
||||
@@ -91,7 +91,7 @@ const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
// 当前选中的标签
|
||||
const selectedTag = ref<TagView | null>(null);
|
||||
|
||||
// 右键菜单状态
|
||||
// 右键菜单状态"
|
||||
const contextMenu = reactive<ContextMenu>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
@@ -4,7 +4,7 @@
|
||||
<template v-if="isDesktop">
|
||||
<!-- 搜索 -->
|
||||
<div class="navbar-actions__item">
|
||||
<MenuSearch />
|
||||
<CommandPalette />
|
||||
</div>
|
||||
|
||||
<!-- 全屏 -->
|
||||
@@ -24,10 +24,10 @@
|
||||
|
||||
<!-- 通知 -->
|
||||
<div class="navbar-actions__item">
|
||||
<Notification />
|
||||
<NoticeDropdown />
|
||||
</div>
|
||||
|
||||
<!-- 租户选择(如果启用多租户) -->
|
||||
<!-- 租户选择(如果启用多租户)-->
|
||||
<div v-if="showTenantSelect" class="navbar-actions__item">
|
||||
<TenantSwitcher @change="handleTenantChange" />
|
||||
</div>
|
||||
@@ -60,11 +60,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 系统设置 -->
|
||||
<div
|
||||
v-if="defaultSettings.showSettings"
|
||||
class="navbar-actions__item"
|
||||
@click="handleSettingsClick"
|
||||
>
|
||||
<div v-if="defaults.showSettings" class="navbar-actions__item" @click="handleSettingsClick">
|
||||
<div class="i-svg:setting" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,18 +69,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { defaults } from "@/settings";
|
||||
import { DeviceEnum, SidebarColor, ThemeMode, LayoutMode } from "@/enums/settings";
|
||||
import { useAppStore, useSettingsStore, useUserStore } from "@/store";
|
||||
|
||||
// 导入子组件
|
||||
import MenuSearch from "@/components/MenuSearch/index.vue";
|
||||
import CommandPalette from "@/components/CommandPalette/index.vue";
|
||||
import Fullscreen from "@/components/Fullscreen/index.vue";
|
||||
import SizeSelect from "@/components/SizeSelect/index.vue";
|
||||
import LangSelect from "@/components/LangSelect/index.vue";
|
||||
import Notification from "@/components/Notification/index.vue";
|
||||
import NoticeDropdown from "@/components/NoticeDropdown/index.vue";
|
||||
import TenantSwitcher from "@/components/TenantSwitcher/index.vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant";
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
@@ -105,7 +101,7 @@ const showTenantSelect = computed(() => {
|
||||
if (tenantStore.tenantList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// 如果只有一个租户,也不显示(单租户模式,用户无感知)
|
||||
// 如果只有一个租户,也不显示(单租户模式,用户无感知<EFBFBD>?
|
||||
if (tenantStore.tenantList.length === 1) {
|
||||
return false;
|
||||
}
|
||||
@@ -132,20 +128,20 @@ function handleProfileClick() {
|
||||
router.push({ name: "Profile" });
|
||||
}
|
||||
|
||||
// 根据主题和侧边栏配色方案选择样式类
|
||||
// 根据主题和侧边栏配色方案选择样式<EFBFBD>?
|
||||
const navbarActionsClass = computed(() => {
|
||||
const { theme, sidebarColorScheme, layout } = settingStore;
|
||||
|
||||
// 暗黑主题下,所有布局都使用白色文字
|
||||
// 暗黑主题下,所有布局都使用白色文<EFBFBD>?
|
||||
if (theme === ThemeMode.DARK) {
|
||||
return "navbar-actions--white-text";
|
||||
}
|
||||
|
||||
// 明亮主题下
|
||||
// 明亮主题<EFBFBD>?
|
||||
if (theme === ThemeMode.LIGHT) {
|
||||
// 顶部布局和混合布局的顶部区域:
|
||||
// - 如果侧边栏是经典蓝色,使用白色文字
|
||||
// - 如果侧边栏是极简白色,使用深色文字
|
||||
// - 如果侧边栏是经典蓝色,使用白色文<EFBFBD>?
|
||||
// - 如果侧边栏是极简白色,使用深色文<EFBFBD>?
|
||||
if (layout === LayoutMode.TOP || layout === LayoutMode.MIX) {
|
||||
if (sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
|
||||
return "navbar-actions--white-text";
|
||||
@@ -159,7 +155,7 @@ const navbarActionsClass = computed(() => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* 退出登<EFBFBD>?
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||
@@ -193,7 +189,7 @@ function handleSettingsClick() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标准 */
|
||||
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标<EFBFBD>?*/
|
||||
height: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0 8px;
|
||||
@@ -201,7 +197,7 @@ function handleSettingsClick() {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
// 确保子元素居中
|
||||
// 确保子元素居<EFBFBD>?
|
||||
> * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -258,7 +254,7 @@ function handleSettingsClick() {
|
||||
}
|
||||
}
|
||||
|
||||
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局)
|
||||
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局<EFBFBD>?
|
||||
.navbar-actions--white-text {
|
||||
.navbar-actions__item {
|
||||
:deep([class^="i-svg:"]) {
|
||||
@@ -294,7 +290,7 @@ function handleSettingsClick() {
|
||||
}
|
||||
}
|
||||
|
||||
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局)
|
||||
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局<EFBFBD>?
|
||||
.navbar-actions--dark-text {
|
||||
.navbar-actions__item {
|
||||
:deep([class^="i-svg:"]) {
|
||||
@@ -1,187 +0,0 @@
|
||||
<!-- 混合布局顶部菜单 -->
|
||||
<template>
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:default-active="activeTopMenuPath"
|
||||
:background-color="
|
||||
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
|
||||
? variables['menu-background']
|
||||
: undefined
|
||||
"
|
||||
:text-color="
|
||||
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
|
||||
? variables['menu-text']
|
||||
: undefined
|
||||
"
|
||||
:active-text-color="
|
||||
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
|
||||
? variables['menu-active-text']
|
||||
: undefined
|
||||
"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item v-for="menuItem in processedTopMenus" :key="menuItem.path" :index="menuItem.path">
|
||||
<MenuItemContent
|
||||
v-if="menuItem.meta"
|
||||
:icon="menuItem.meta.icon"
|
||||
:title="menuItem.meta.title"
|
||||
/>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MenuItemContent from "./components/MenuItemContent.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "MixTopMenu",
|
||||
});
|
||||
|
||||
import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
|
||||
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
import { SidebarColor } from "@/enums/settings";
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 获取主题
|
||||
const theme = computed(() => settingsStore.theme);
|
||||
|
||||
// 获取浅色主题下的侧边栏配色方案
|
||||
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
|
||||
|
||||
// 顶部菜单列表
|
||||
const topMenus = ref<RouteRecordRaw[]>([]);
|
||||
|
||||
// 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题
|
||||
const processedTopMenus = computed(() => {
|
||||
return topMenus.value.map((route) => {
|
||||
// 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由
|
||||
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
|
||||
return route;
|
||||
}
|
||||
|
||||
// 过滤出非隐藏的子菜单
|
||||
const visibleChildren = route.children.filter((child) => !child.meta?.hidden);
|
||||
|
||||
// 如果只有一个非隐藏的子菜单,显示子菜单的信息
|
||||
if (visibleChildren.length === 1) {
|
||||
const onlyChild = visibleChildren[0];
|
||||
return {
|
||||
...route,
|
||||
meta: {
|
||||
...route.meta,
|
||||
title: onlyChild.meta?.title || route.meta?.title,
|
||||
icon: onlyChild.meta?.icon || route.meta?.icon,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 其他情况返回原路由
|
||||
return route;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
|
||||
* @param routePath 点击的菜单路径
|
||||
*/
|
||||
const handleMenuSelect = (routePath: string) => {
|
||||
updateMenuState(routePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新菜单状态 - 同时处理点击和路由变化情况
|
||||
* @param topMenuPath 顶级菜单路径
|
||||
* @param skipNavigation 是否跳过导航(路由变化时为true,点击菜单时为false)
|
||||
*/
|
||||
const updateMenuState = (topMenuPath: string, skipNavigation = false) => {
|
||||
// 不相同才更新,避免重复操作
|
||||
if (topMenuPath !== appStore.activeTopMenuPath) {
|
||||
appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单
|
||||
permissionStore.setMixLayoutSideMenus(topMenuPath); // 设置混合布局左侧菜单
|
||||
}
|
||||
|
||||
// 如果是点击菜单且状态已变更,才进行导航
|
||||
if (!skipNavigation) {
|
||||
navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus); // 跳转到左侧第一个菜单
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到左侧第一个可访问的菜单
|
||||
* @param menus 左侧菜单列表
|
||||
*/
|
||||
const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => {
|
||||
if (menus.length === 0) return;
|
||||
|
||||
const [firstMenu] = menus;
|
||||
|
||||
// 如果第一个菜单有子菜单,递归跳转到第一个子菜单
|
||||
if (firstMenu.children && firstMenu.children.length > 0) {
|
||||
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]);
|
||||
} else if (firstMenu.name) {
|
||||
router.push({
|
||||
name: firstMenu.name,
|
||||
query:
|
||||
typeof firstMenu.meta?.params === "object"
|
||||
? (firstMenu.meta.params as LocationQueryRaw)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前路由路径的顶部菜单路径
|
||||
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
|
||||
|
||||
onMounted(() => {
|
||||
topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden);
|
||||
// 初始化顶部菜单
|
||||
const currentTopMenuPath =
|
||||
useRoute().path.split("/").filter(Boolean).length > 1
|
||||
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/"
|
||||
: "/";
|
||||
appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单
|
||||
permissionStore.setMixLayoutSideMenus(currentTopMenuPath); // 设置混合布局左侧菜单
|
||||
});
|
||||
|
||||
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
(newPath) => {
|
||||
if (newPath) {
|
||||
// 提取顶级路径
|
||||
const topMenuPath =
|
||||
newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/";
|
||||
|
||||
// 使用公共方法更新菜单状态,但跳过导航(因为路由已经变化)
|
||||
updateMenuState(topMenuPath, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&--horizontal {
|
||||
height: $navbar-height !important;
|
||||
|
||||
// 确保菜单项垂直居中
|
||||
:deep(.el-menu-item) {
|
||||
height: 100%;
|
||||
line-height: $navbar-height;
|
||||
}
|
||||
|
||||
// 移除默认的底部边框
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<!-- 菜单图标 -->
|
||||
<template v-if="icon">
|
||||
<el-icon v-if="isElIcon" class="menu-icon">
|
||||
<component :is="iconComponent" />
|
||||
</el-icon>
|
||||
<div v-else :class="`i-svg:${icon}`" class="menu-icon" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="i-svg:menu menu-icon" />
|
||||
</template>
|
||||
<!-- 菜单标题 -->
|
||||
<span v-if="title" class="menu-title ml-1">{{ translateRouteTitle(title) }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { translateRouteTitle } from "@/utils/i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
icon?: string;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const isElIcon = computed(() => props.icon?.startsWith("el-icon"));
|
||||
const iconComponent = computed(() => props.icon?.replace("el-icon-", ""));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
color: currentcolor;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user