使 menu UI 更加舒适
This commit is contained in:
1 parent
e8ceb5b46b
commit
f062fb1e29
1 file changed
+100
-11
+100
-11
@@ -8,7 +8,7 @@
|
|||||||
* - TabBar:多标签页切换栏
|
* - TabBar:多标签页切换栏
|
||||||
* - 头像下拉菜单:用户信息头部 + 个人中心 + 系统设置 + 主题切换 + 退出登录
|
* - 头像下拉菜单:用户信息头部 + 个人中心 + 系统设置 + 主题切换 + 退出登录
|
||||||
*/
|
*/
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
@@ -75,6 +75,38 @@ function handleMenuSelect(index: string): void {
|
|||||||
router.push(index)
|
router.push(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 折叠态下点击侧边栏任意位置(菜单项/子菜单标题),自动展开侧边栏 */
|
||||||
|
function handleSidebarClick(): void {
|
||||||
|
if (appStore.sidebarCollapsed) {
|
||||||
|
appStore.toggleSidebar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 菜单折叠:折叠时收起所有展开的二级菜单(配合 CSS 过渡实现平滑收回)===
|
||||||
|
const menuRef = ref()
|
||||||
|
|
||||||
|
/** 收起所有已展开的二级菜单 */
|
||||||
|
function closeAllSubMenus(): void {
|
||||||
|
const menu = menuRef.value
|
||||||
|
if (!menu) return
|
||||||
|
for (const m of userStore.menus) {
|
||||||
|
if (m.Children?.length) {
|
||||||
|
try {
|
||||||
|
menu.close(m.Path || m.Title)
|
||||||
|
} catch {
|
||||||
|
// 未展开的 sub-menu close 可能抛错,忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => appStore.sidebarCollapsed,
|
||||||
|
(collapsed) => {
|
||||||
|
if (collapsed) closeAllSubMenus()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
/** 头像下拉菜单命令统一处理 */
|
/** 头像下拉菜单命令统一处理 */
|
||||||
function handleUserCommand(cmd: string): void {
|
function handleUserCommand(cmd: string): void {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
@@ -226,11 +258,11 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
<aside class="gerp-sidebar" :class="{ collapsed: appStore.sidebarCollapsed }">
|
<aside class="gerp-sidebar" :class="{ collapsed: appStore.sidebarCollapsed }">
|
||||||
<el-scrollbar>
|
<el-scrollbar>
|
||||||
<el-menu
|
<el-menu
|
||||||
|
ref="menuRef"
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
:collapse="appStore.sidebarCollapsed"
|
|
||||||
:collapse-transition="true"
|
|
||||||
router
|
router
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
|
@click.capture="handleSidebarClick"
|
||||||
>
|
>
|
||||||
<template v-for="menu in userStore.menus" :key="menu.Path || menu.Title">
|
<template v-for="menu in userStore.menus" :key="menu.Path || menu.Title">
|
||||||
<!-- 含子菜单 -->
|
<!-- 含子菜单 -->
|
||||||
@@ -247,7 +279,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
>
|
>
|
||||||
<i v-if="child.Icon && isFaIcon(child.Icon)" :class="child.Icon" class="gerp-menu-icon" />
|
<i v-if="child.Icon && isFaIcon(child.Icon)" :class="child.Icon" class="gerp-menu-icon" />
|
||||||
<el-icon v-else-if="child.Icon"><component :is="child.Icon" /></el-icon>
|
<el-icon v-else-if="child.Icon"><component :is="child.Icon" /></el-icon>
|
||||||
<template #title>{{ child.Title }}</template>
|
<template #title><span>{{ child.Title }}</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
@@ -255,7 +287,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
<el-menu-item v-else :index="menu.Path || ''">
|
<el-menu-item v-else :index="menu.Path || ''">
|
||||||
<i v-if="menu.Icon && isFaIcon(menu.Icon)" :class="menu.Icon" class="gerp-menu-icon" />
|
<i v-if="menu.Icon && isFaIcon(menu.Icon)" :class="menu.Icon" class="gerp-menu-icon" />
|
||||||
<el-icon v-else-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
|
<el-icon v-else-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
|
||||||
<template #title>{{ menu.Title }}</template>
|
<template #title><span>{{ menu.Title }}</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
@@ -394,6 +426,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
|
|
||||||
.gerp-sidebar {
|
.gerp-sidebar {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-right: 1px solid var(--gerp-border-color);
|
border-right: 1px solid var(--gerp-border-color);
|
||||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -401,10 +434,6 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
width: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-scrollbar) {
|
:deep(.el-scrollbar) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -412,6 +441,64 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
:deep(.el-menu) {
|
:deep(.el-menu) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
// 宽度跟随侧边栏平滑收缩
|
||||||
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单文字:折叠/展开时平滑淡出/淡入 + max-width 收缩,icon 不受影响保持显示
|
||||||
|
// max-width + overflow hidden 让文字淡出时不占空间,避免 width 变小时文字换行叠到 icon 上
|
||||||
|
:deep(.el-menu-item span),
|
||||||
|
:deep(.el-sub-menu__title span) {
|
||||||
|
transition: opacity 0.25s ease, max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sub-menu 箭头:折叠时随文字一起淡出,展开时淡入
|
||||||
|
:deep(.el-sub-menu__icon-arrow) {
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级菜单容器:折叠时高度平滑收起,展开侧边栏后恢复
|
||||||
|
// min-width: 0 允许其跟随容器收缩(否则默认 min-width: 200px 会撑住 sub-menu 宽度)
|
||||||
|
:deep(.el-menu--inline) {
|
||||||
|
min-width: 0;
|
||||||
|
max-height: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠态:宽度变小 + 文字淡出收缩 + 箭头隐藏 + 二级菜单收起
|
||||||
|
// 注意:icon 保持与展开态完全相同的位置(padding/margin 不变),仅宽度收缩 + 文字淡出
|
||||||
|
&.collapsed {
|
||||||
|
width: 64px;
|
||||||
|
// 隐藏左侧指示条
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-menu-item span),
|
||||||
|
:deep(.el-sub-menu__title span) {
|
||||||
|
opacity: 0;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-sub-menu__icon-arrow) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-menu--inline) {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
// 折叠态隐藏版本号文字,设置按钮居中
|
||||||
|
.gerp-version {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.gerp-sidebar-footer {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单项:对齐原版 GERP 的左侧指示条 + hover/active 效果
|
// 菜单项:对齐原版 GERP 的左侧指示条 + hover/active 效果
|
||||||
@@ -419,7 +506,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin: 2px 8px;
|
margin: 2px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
// 左侧指示条(hover/active 时显示)
|
// 左侧指示条(hover/active 时显示)
|
||||||
&::before {
|
&::before {
|
||||||
@@ -459,7 +546,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
:deep(.el-sub-menu__title) {
|
:deep(.el-sub-menu__title) {
|
||||||
margin: 2px 8px;
|
margin: 2px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
@@ -503,6 +590,8 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
// margin 过渡让折叠/展开时图标右间距平滑变化(更平缓)
|
||||||
|
transition: margin 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 折叠态:图标居中,去除右边距
|
// 折叠态:图标居中,去除右边距
|
||||||
|
|||||||
Reference in new issue
Block a user