wip: 临时提交
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore, useSettingsStore } from "@/store";
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { ThemeMode } from "@/enums/settings/theme.enum";
|
||||
import { ComponentSize } from "@/enums/settings/layout.enum";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import logo from "@/assets/logo.png";
|
||||
|
||||
defineProps({
|
||||
@@ -40,56 +40,36 @@ defineProps({
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// 全局样式:针对顶部布局和混合布局的特殊处理
|
||||
// 顶部布局和混合布局的特殊处理
|
||||
.layout-top,
|
||||
.layout-mix {
|
||||
.logo {
|
||||
// 在顶部和混合布局中,移除背景色,使其透明
|
||||
background-color: transparent !important;
|
||||
|
||||
.title {
|
||||
// 确保标题颜色适配当前主题
|
||||
color: var(--menu-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部布局的响应式宽度控制
|
||||
.layout-top {
|
||||
.layout__header-left .logo {
|
||||
flex-shrink: 0; // 防止Logo被压缩
|
||||
width: $sidebar-width; // 默认宽度:显示logo+文字
|
||||
margin-right: 20px;
|
||||
|
||||
// 中屏设备优化(800px-1100px):适度缩小但保持显示文字
|
||||
@media (min-width: 768px) and (max-width: 1100px) {
|
||||
width: 180px; // 缩小到180px,为菜单腾出空间
|
||||
margin-right: 16px; // 减少右边距
|
||||
}
|
||||
|
||||
// 小屏设备:只显示logo,使用收缩宽度
|
||||
@media (max-width: 767px) {
|
||||
width: $sidebar-width-collapsed; // 只显示logo:54px
|
||||
margin-right: 12px; // 减少右边距
|
||||
}
|
||||
// 宽屏时:openSidebar 状态下显示完整Logo+文字
|
||||
.openSidebar {
|
||||
&.layout-top .layout__header-left .logo,
|
||||
&.layout-mix .layout__header-logo .logo {
|
||||
width: $sidebar-width; // 210px,显示logo+文字
|
||||
}
|
||||
}
|
||||
|
||||
// 混合布局的响应式宽度控制
|
||||
.layout-mix {
|
||||
.layout__header-logo .logo {
|
||||
flex-shrink: 0;
|
||||
width: $sidebar-width; // 默认宽度:显示logo+文字
|
||||
// 窄屏时:hideSidebar 状态下只显示Logo图标
|
||||
.hideSidebar {
|
||||
&.layout-top .layout__header-left .logo,
|
||||
&.layout-mix .layout__header-logo .logo {
|
||||
width: $sidebar-width-collapsed; // 54px,只显示logo
|
||||
}
|
||||
|
||||
// 中屏设备优化(800px-1100px):适度缩小但保持显示文字
|
||||
@media (min-width: 768px) and (max-width: 1100px) {
|
||||
width: 180px; // 缩小到180px,为菜单腾出空间
|
||||
}
|
||||
|
||||
// 小屏设备:只显示logo,使用收缩宽度
|
||||
@media (max-width: 767px) {
|
||||
width: $sidebar-width-collapsed; // 只显示logo:54px
|
||||
}
|
||||
// 隐藏文字,只显示图标
|
||||
.logo .title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<el-menu
|
||||
ref="menuRef"
|
||||
:default-active="currentRoute.path"
|
||||
:default-active="activeMenuIndex"
|
||||
:collapse="!appStore.sidebar.opened"
|
||||
:background-color="
|
||||
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
|
||||
@@ -37,7 +37,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, PropType } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import path from "path-browserify";
|
||||
import type { MenuInstance } from "element-plus";
|
||||
@@ -79,6 +78,83 @@ const theme = computed(() => settingsStore.theme);
|
||||
// 获取浅色主题下的侧边栏配色方案
|
||||
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
|
||||
|
||||
// 计算当前激活的菜单项
|
||||
const activeMenuIndex = computed(() => {
|
||||
const currentPath = currentRoute.path;
|
||||
|
||||
// 如果路由设置了 activeMenu,优先使用
|
||||
if (currentRoute.meta?.activeMenu) {
|
||||
return currentRoute.meta.activeMenu as string;
|
||||
}
|
||||
|
||||
// 在水平模式下(顶部布局),需要找到匹配的顶级菜单
|
||||
if (props.menuMode === "horizontal") {
|
||||
// 首先尝试简单的路径前缀匹配
|
||||
const pathSegments = currentPath.split("/").filter(Boolean);
|
||||
if (pathSegments.length > 0) {
|
||||
const topLevelPath = `/${pathSegments[0]}`;
|
||||
|
||||
// 检查是否有菜单项匹配这个顶级路径
|
||||
const matchingMenu = props.data.find((menu) => {
|
||||
const menuPath = resolveFullPath(menu.path);
|
||||
return menuPath === topLevelPath;
|
||||
});
|
||||
|
||||
if (matchingMenu) {
|
||||
console.log("🎯 Top menu matched:", topLevelPath, "for route:", currentPath);
|
||||
return topLevelPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果简单匹配失败,使用详细匹配
|
||||
const findMatchingTopMenu = (menus: RouteRecordRaw[], targetPath: string): string | null => {
|
||||
for (const menu of menus) {
|
||||
const menuPath = resolveFullPath(menu.path);
|
||||
|
||||
// 精确匹配
|
||||
if (targetPath === menuPath) {
|
||||
return menuPath;
|
||||
}
|
||||
|
||||
// 路径前缀匹配(子路径匹配父菜单)
|
||||
if (targetPath.startsWith(menuPath + "/")) {
|
||||
return menuPath;
|
||||
}
|
||||
|
||||
// 如果有子菜单,检查子菜单是否匹配
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const hasMatchingChild = menu.children.some((child) => {
|
||||
// 对于子菜单,需要正确解析路径
|
||||
let childPath;
|
||||
if (child.path.startsWith("/")) {
|
||||
// 如果子路径是绝对路径,直接使用
|
||||
childPath = child.path;
|
||||
} else {
|
||||
// 如果是相对路径,基于父菜单路径解析
|
||||
childPath = path.resolve(menuPath, child.path);
|
||||
}
|
||||
return targetPath === childPath || targetPath.startsWith(childPath + "/");
|
||||
});
|
||||
|
||||
if (hasMatchingChild) {
|
||||
return menuPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const matchedMenu = findMatchingTopMenu(props.data, currentPath);
|
||||
if (matchedMenu) {
|
||||
console.log("🎯 Detailed menu matched:", matchedMenu, "for route:", currentPath);
|
||||
return matchedMenu;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回当前路径
|
||||
return currentPath;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取完整路径
|
||||
*
|
||||
@@ -93,6 +169,11 @@ function resolveFullPath(routePath: string) {
|
||||
return props.basePath;
|
||||
}
|
||||
|
||||
// 如果 basePath 为空(顶部布局),直接返回 routePath
|
||||
if (!props.basePath || props.basePath === "") {
|
||||
return routePath;
|
||||
}
|
||||
|
||||
// 解析路径,生成完整的绝对路径
|
||||
return path.resolve(props.basePath, routePath);
|
||||
}
|
||||
@@ -127,4 +208,54 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听激活菜单变化,为包含激活子菜单的父菜单添加样式类
|
||||
*/
|
||||
watch(
|
||||
() => activeMenuIndex.value,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateParentMenuStyles();
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新父菜单样式 - 为包含激活子菜单的父菜单添加 has-active-child 类
|
||||
*/
|
||||
function updateParentMenuStyles() {
|
||||
if (!menuRef.value?.$el) return;
|
||||
|
||||
const menuEl = menuRef.value.$el as HTMLElement;
|
||||
|
||||
// 移除所有现有的 has-active-child 类
|
||||
const allSubMenus = menuEl.querySelectorAll(".el-sub-menu");
|
||||
allSubMenus.forEach((subMenu) => {
|
||||
subMenu.classList.remove("has-active-child");
|
||||
});
|
||||
|
||||
// 查找当前激活的菜单项
|
||||
const activeMenuItem = menuEl.querySelector(".el-menu-item.is-active");
|
||||
if (activeMenuItem) {
|
||||
// 向上查找父级 el-sub-menu 元素
|
||||
let parent = activeMenuItem.parentElement;
|
||||
while (parent && parent !== menuEl) {
|
||||
if (parent.classList.contains("el-sub-menu")) {
|
||||
parent.classList.add("has-active-child");
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载后立即更新父菜单样式
|
||||
*/
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateParentMenuStyles();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -21,13 +21,17 @@
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item v-for="menuItem in processedTopMenus" :key="menuItem.path" :index="menuItem.path">
|
||||
<MenuItemTitle v-if="menuItem.meta" :icon="menuItem.meta.icon" :title="menuItem.meta.title" />
|
||||
<MenuItemContent
|
||||
v-if="menuItem.meta"
|
||||
:icon="menuItem.meta.icon"
|
||||
:title="menuItem.meta.title"
|
||||
/>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MenuItemTitle from "./components/MenuItemTitle.vue";
|
||||
import MenuItemContent from "./components/MenuItemContent.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "MixTopMenu",
|
||||
@@ -81,14 +85,25 @@ const processedTopMenus = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 获取当前路由路径的顶部菜单路径
|
||||
const activeTopMenuPath =
|
||||
useRoute().path.split("/").filter(Boolean).length > 1
|
||||
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/"
|
||||
: "/";
|
||||
const route = useRoute();
|
||||
|
||||
// 设置当前激活的顶部菜单路径
|
||||
appStore.activeTopMenu(activeTopMenuPath);
|
||||
// 获取当前路由路径的顶部菜单路径
|
||||
const getActiveTopMenuPath = () => {
|
||||
const pathSegments = route.path.split("/").filter(Boolean);
|
||||
return pathSegments.length > 0 ? `/${pathSegments[0]}` : "/";
|
||||
};
|
||||
|
||||
// 监听路由变化,更新活跃的顶部菜单
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
const newActiveTopMenuPath = getActiveTopMenuPath();
|
||||
if (newActiveTopMenuPath !== appStore.activeTopMenuPath) {
|
||||
appStore.activeTopMenu(newActiveTopMenuPath);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
|
||||
@@ -105,7 +120,11 @@ const handleMenuSelect = (routePath: string) => {
|
||||
*/
|
||||
function activateFirstLevelMenu(routePath: string) {
|
||||
permissionStore.updateSideMenu(routePath); // 更新左侧菜单
|
||||
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
|
||||
|
||||
// 使用 nextTick 确保侧边菜单更新完成后再跳转
|
||||
nextTick(() => {
|
||||
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,22 +134,41 @@ function activateFirstLevelMenu(routePath: string) {
|
||||
const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => {
|
||||
if (menus.length === 0) return;
|
||||
|
||||
const [firstMenu] = menus;
|
||||
// 查找第一个可访问的菜单项
|
||||
const findFirstAccessibleRoute = (routes: RouteRecordRaw[]): RouteRecordRaw | null => {
|
||||
for (const route of routes) {
|
||||
// 跳过隐藏的菜单项
|
||||
if (route.meta?.hidden) continue;
|
||||
|
||||
// 如果第一个菜单有子菜单,递归跳转到第一个子菜单
|
||||
if (firstMenu.children && firstMenu.children.length > 0) {
|
||||
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]);
|
||||
} else if (firstMenu.name) {
|
||||
// 如果有子菜单,递归查找
|
||||
if (route.children && route.children.length > 0) {
|
||||
const childRoute = findFirstAccessibleRoute(route.children);
|
||||
if (childRoute) return childRoute;
|
||||
} else if (route.name && route.path) {
|
||||
// 找到第一个有名称和路径的菜单项
|
||||
return route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const firstRoute = findFirstAccessibleRoute(menus);
|
||||
|
||||
if (firstRoute && firstRoute.name) {
|
||||
console.log("🎯 Navigating to first menu:", firstRoute.name, firstRoute.path);
|
||||
router.push({
|
||||
name: firstMenu.name,
|
||||
name: firstRoute.name,
|
||||
query:
|
||||
typeof firstMenu.meta?.params === "object"
|
||||
? (firstMenu.meta.params as LocationQueryRaw)
|
||||
typeof firstRoute.meta?.params === "object"
|
||||
? (firstRoute.meta.params as LocationQueryRaw)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 当前激活的顶部菜单路径
|
||||
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
|
||||
|
||||
onMounted(() => {
|
||||
topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
:index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||
>
|
||||
<MenuItemTitle
|
||||
<MenuItemContent
|
||||
v-if="onlyOneChild.meta"
|
||||
:icon="onlyOneChild.meta.icon || item.meta?.icon"
|
||||
:title="onlyOneChild.meta.title"
|
||||
/>
|
||||
@@ -33,7 +34,7 @@
|
||||
<!--【非叶子节点】显示含多个子节点的父菜单,或始终显示的单子节点 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||
<template #title>
|
||||
<MenuItemTitle v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
|
||||
<MenuItemContent v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
|
||||
</template>
|
||||
|
||||
<MenuItem
|
||||
@@ -48,7 +49,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MenuItemTitle from "./MenuItemTitle.vue";
|
||||
import MenuItemContent from "./MenuItemContent.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "MenuItem",
|
||||
@@ -140,12 +141,6 @@ function resolvePath(routePath: string) {
|
||||
.submenu-title-noDropdown {
|
||||
position: relative;
|
||||
|
||||
.el-tooltip {
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
@@ -195,4 +190,41 @@ html.sidebar-color-blue {
|
||||
background-color: $menu-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// 父菜单激活状态样式 - 当子菜单激活时,父菜单显示激活状态
|
||||
.el-sub-menu {
|
||||
// 当父菜单包含激活子菜单时的样式
|
||||
&.has-active-child .el-sub-menu__title {
|
||||
color: var(--el-color-primary) !important;
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题下的父菜单激活状态
|
||||
html.dark & {
|
||||
&.has-active-child .el-sub-menu__title {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
background-color: rgba(64, 128, 255, 0.15) !important;
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深蓝色侧边栏配色下的父菜单激活状态
|
||||
html.sidebar-color-blue & {
|
||||
&.has-active-child .el-sub-menu__title {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
background-color: rgba(64, 128, 255, 0.2) !important;
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<!-- 菜单图标 -->
|
||||
<template v-if="icon">
|
||||
<el-icon v-if="isElIcon" class="el-icon">
|
||||
<el-icon v-if="isElIcon" class="menu-icon">
|
||||
<component :is="iconComponent" />
|
||||
</el-icon>
|
||||
<div v-else :class="`i-svg:${icon}`" />
|
||||
<div v-else :class="`i-svg:${icon}`" class="menu-icon" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="i-svg:menu" />
|
||||
<div class="i-svg:menu menu-icon" />
|
||||
</template>
|
||||
<!-- 菜单标题 -->
|
||||
<span v-if="title" class="ml-1">{{ translateRouteTitle(title) }}</span>
|
||||
<span v-if="title" class="menu-title ml-1">{{ translateRouteTitle(title) }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -26,28 +26,15 @@ const iconComponent = computed(() => props.icon?.replace("el-icon-", ""));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-icon {
|
||||
width: 14px !important;
|
||||
margin-right: 0 !important;
|
||||
.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;
|
||||
}
|
||||
|
||||
[class^="i-svg:"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: currentcolor !important;
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.el-sub-menu,
|
||||
.el-menu-item {
|
||||
.el-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
[class^="i-svg:"] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -62,7 +62,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import defaultSettings from "@/settings";
|
||||
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";
|
||||
|
||||
@@ -146,15 +146,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { LayoutMode } from "@/enums/settings/layout.enum";
|
||||
import { ThemeMode } from "@/enums/settings/theme.enum";
|
||||
import { SidebarColor } from "@/enums/settings/theme.enum";
|
||||
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
|
||||
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
|
||||
import { themeColorPresets } from "@/settings";
|
||||
|
||||
// 按钮图标 - 使用markRaw避免响应式警告
|
||||
// 按钮图标
|
||||
const copyIcon = markRaw(DocumentCopy);
|
||||
const resetIcon = markRaw(RefreshLeft);
|
||||
|
||||
@@ -175,18 +173,8 @@ const layoutOptions: LayoutOption[] = [
|
||||
{ value: LayoutMode.MIX, label: t("settings.mixLayout"), className: "mix" },
|
||||
];
|
||||
|
||||
// 颜色预设
|
||||
const colorPresets = [
|
||||
"#4080FF",
|
||||
"#626AEF",
|
||||
"#ff4500",
|
||||
"#ff8c00",
|
||||
"#00ced1",
|
||||
"#1e90ff",
|
||||
"#c71585",
|
||||
"rgb(255, 120, 0)",
|
||||
"hsva(120, 40, 94)",
|
||||
];
|
||||
// 使用统一的颜色预设配置
|
||||
const colorPresets = themeColorPresets;
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { computed } from "vue";
|
||||
import { useAppStore, useSettingsStore } from "@/store";
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
|
||||
/**
|
||||
* 布局相关的通用逻辑
|
||||
@@ -49,14 +48,6 @@ export function useLayout() {
|
||||
appStore.closeSideBar();
|
||||
}
|
||||
|
||||
// 暂时注释掉这个逻辑,避免影响手动操作
|
||||
// 监听路由变化,在移动端自动关闭侧边栏
|
||||
// watchEffect(() => {
|
||||
// if (appStore.device === "mobile" && appStore.sidebar.opened) {
|
||||
// appStore.closeSideBar();
|
||||
// }
|
||||
// });
|
||||
|
||||
return {
|
||||
currentLayout,
|
||||
isSidebarOpen,
|
||||
|
||||
@@ -15,7 +15,7 @@ import TopLayout from "./views/TopLayout.vue";
|
||||
import MixLayout from "./views/MixLayout.vue";
|
||||
import Settings from "./components/Settings/index.vue";
|
||||
import { LayoutMode } from "@/enums/settings/layout.enum";
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
|
||||
const { currentLayout } = useLayout();
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ console.log("🔍 LeftLayout - isMobile:", isMobile.value);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端样式 - 注意这里需要正确应用到父元素 */
|
||||
/* 移动端样式 */
|
||||
.mobile {
|
||||
.layout__sidebar {
|
||||
width: $sidebar-width !important;
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { useLayout } from "../composables/useLayout";
|
||||
@@ -136,21 +135,7 @@ console.log("🎨 MixLayout rendered");
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $sidebar-width; // 默认宽度:显示logo+文字
|
||||
height: 100%;
|
||||
|
||||
// 小屏设备:只显示logo,使用收缩宽度
|
||||
@media (max-width: 767px) {
|
||||
width: $sidebar-width-collapsed; // 只显示logo:54px
|
||||
}
|
||||
|
||||
:deep(.logo) {
|
||||
height: 100%;
|
||||
|
||||
a {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
@@ -177,20 +162,6 @@ console.log("🎨 MixLayout rendered");
|
||||
line-height: $navbar-height;
|
||||
border-bottom: none;
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1200px) {
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
border-bottom: 2px solid var(--el-color-primary);
|
||||
@@ -205,14 +176,6 @@ console.log("🎨 MixLayout rendered");
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1200px) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +213,7 @@ console.log("🎨 MixLayout rendered");
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
background-color: var(--menu-background);
|
||||
box-shadow: 0 0 6px -2px var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,18 +66,11 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
||||
align-items: center;
|
||||
min-width: 0; // 允许flex收缩
|
||||
height: 100%;
|
||||
overflow: hidden; // 防止溢出
|
||||
|
||||
// Logo 样式 - 使用SCSS变量管理宽度
|
||||
// Logo样式由AppLogo组件的全局样式控制
|
||||
:deep(.logo) {
|
||||
flex-shrink: 0; // 防止Logo被压缩
|
||||
width: $sidebar-width; // 默认宽度:显示logo+文字
|
||||
height: $navbar-height;
|
||||
|
||||
// 小屏设备:只显示logo,使用收缩宽度
|
||||
@media (max-width: 768px) {
|
||||
width: $sidebar-width-collapsed; // 只显示logo:54px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +82,7 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
// 限制菜单高度
|
||||
// 菜单样式
|
||||
:deep(.el-menu--horizontal) {
|
||||
flex: 1;
|
||||
min-width: 0; // 允许菜单收缩
|
||||
@@ -102,17 +95,6 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
||||
.el-menu-item {
|
||||
height: $navbar-height;
|
||||
line-height: $navbar-height;
|
||||
|
||||
// 响应式菜单项
|
||||
@media (min-width: 768px) and (max-width: 1200px) {
|
||||
padding: 0 12px; // 中屏设备减少内边距
|
||||
font-size: 14px; // 稍微缩小字体
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 0 8px; // 小屏设备进一步减少内边距
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu {
|
||||
@@ -121,6 +103,11 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
||||
line-height: $navbar-height;
|
||||
}
|
||||
}
|
||||
|
||||
// 修复子菜单弹出位置
|
||||
.el-menu--popup {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const { pkg } = __APP_INFO__;
|
||||
// 检查用户的操作系统是否使用深色模式
|
||||
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
export const defaultSettings: AppSettings = {
|
||||
// 系统Title
|
||||
title: pkg.name,
|
||||
// 系统版本
|
||||
@@ -24,7 +24,7 @@ const defaultSettings: AppSettings = {
|
||||
size: ComponentSize.DEFAULT,
|
||||
// 语言
|
||||
language: LanguageEnum.ZH_CN,
|
||||
// 主题颜色
|
||||
// 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
|
||||
themeColor: "#4080FF",
|
||||
// 是否显示水印
|
||||
showWatermark: false,
|
||||
@@ -34,4 +34,17 @@ const defaultSettings: AppSettings = {
|
||||
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
|
||||
};
|
||||
|
||||
export default defaultSettings;
|
||||
// 主题色预设 - 经典配色方案
|
||||
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
|
||||
export const themeColorPresets = [
|
||||
"#4080FF", // Arco Design 蓝 - 现代感强
|
||||
"#1890FF", // Ant Design 蓝 - 经典商务
|
||||
"#409EFF", // Element Plus 蓝 - 清新自然
|
||||
"#FA8C16", // 活力橙 - 温暖友好
|
||||
"#722ED1", // 优雅紫 - 高端大气
|
||||
"#13C2C2", // 青色 - 科技感
|
||||
"#52C41A", // 成功绿 - 活力清新
|
||||
"#F5222D", // 警示红 - 醒目强烈
|
||||
"#2F54EB", // 深蓝 - 稳重专业
|
||||
"#EB2F96", // 品红 - 时尚个性
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
|
||||
// 导入 Element Plus 中英文语言包
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
|
||||
import type { LayoutMode } from "@/enums/settings/layout.enum";
|
||||
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
|
||||
|
||||
@@ -10,6 +10,67 @@
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
// 混合布局左侧菜单的hover样式
|
||||
.layout-mix .layout__sidebar--left .el-menu {
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// 极简白主题:使用浅灰色背景
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
// 极简白主题:使用浅灰色背景
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题或深蓝色侧边栏配色下的左侧菜单hover样式
|
||||
html.dark .layout-mix .layout__sidebar--left .el-menu,
|
||||
html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu {
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// 深色背景:使用CSS变量
|
||||
background-color: var(--menu-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
// 深色背景:使用CSS变量
|
||||
background-color: var(--menu-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 窄屏时隐藏菜单文字,只显示图标
|
||||
.hideSidebar {
|
||||
// Top布局和Mix布局的水平菜单
|
||||
&.layout-top .layout__header .el-menu--horizontal,
|
||||
&.layout-mix .layout__header .el-menu--horizontal {
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
.menu-title,
|
||||
span:not([class*="i-svg"]):not(.el-icon) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mix布局的左侧菜单
|
||||
&.layout-mix .layout__sidebar--left .el-menu {
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
.menu-title,
|
||||
span:not([class*="i-svg"]):not(.el-icon) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全局搜索区域样式
|
||||
.search-container {
|
||||
padding: 18px 16px 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
$colors: (
|
||||
"primary": (
|
||||
// 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
|
||||
"base": #4080ff,
|
||||
),
|
||||
"success": (
|
||||
|
||||
@@ -77,6 +77,12 @@ export function applyTheme(colors: Record<string, string>) {
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
el.style.setProperty(`--el-color-${key}`, value);
|
||||
});
|
||||
|
||||
// 确保主题色立即生效,强制重新渲染
|
||||
requestAnimationFrame(() => {
|
||||
// 触发样式重新计算
|
||||
el.style.setProperty("--theme-update-trigger", Date.now().toString());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,22 +86,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<el-divider>
|
||||
<el-text size="small">{{ t("login.otherLoginMethods") }}</el-text>
|
||||
</el-divider>
|
||||
<div class="flex-center gap-x-5 w-full text-[var(--el-text-color-secondary)]">
|
||||
<CommonWrapper>
|
||||
<div text-20px class="i-svg:wechat" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:qq" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:github" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:gitee" />
|
||||
</CommonWrapper>
|
||||
<div class="third-party-login">
|
||||
<div class="divider-container">
|
||||
<div class="divider-line"></div>
|
||||
<span class="divider-text">{{ t("login.otherLoginMethods") }}</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<div class="flex-center gap-x-5 w-full text-[var(--el-text-color-secondary)]">
|
||||
<CommonWrapper>
|
||||
<div text-20px class="i-svg:wechat" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:qq" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:github" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:gitee" />
|
||||
</CommonWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -257,3 +261,26 @@ function toOtherForm(type: "register" | "resetPwd") {
|
||||
emit("update:modelValue", type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.third-party-login {
|
||||
.divider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--el-border-color-light), transparent);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import logo from "@/assets/logo.png";
|
||||
import defaultSettings from "@/settings";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
||||
import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user