优化左边栏折叠效果

This commit is contained in:
jingrow 2025-12-28 01:43:52 +08:00
parent c2af4a2623
commit afda9db3bd
2 changed files with 294 additions and 131 deletions

View File

@ -1,10 +1,22 @@
<template>
<div class="relative flex h-full flex-col">
<n-layout class="h-full" has-sider>
<AppSidebar
<n-layout-sider
v-if="!isSignupFlow && !$isMobile && !isHideSidebar && $session.user"
bordered
collapse-mode="width"
:collapsed-width="64"
:width="240"
v-model:collapsed="sidebarCollapsed"
/>
:show-trigger="true"
:responsive="true"
:breakpoint="768"
class="app-sidebar-sider"
@collapse="sidebarCollapsed = true"
@expand="sidebarCollapsed = false"
>
<AppSidebar :collapsed="sidebarCollapsed" />
</n-layout-sider>
<n-layout class="h-full">
<div class="w-full h-full overflow-auto" id="scrollContainer">
<MobileNav
@ -34,7 +46,7 @@
<script setup>
import { defineAsyncComponent, computed, watch, ref, provide, onMounted } from 'vue';
import { NLayout } from 'naive-ui';
import { NLayout, NLayoutSider } from 'naive-ui';
import { Toaster } from 'vue-sonner';
import { dialogs } from './utils/components';
import { useRoute } from 'vue-router';
@ -109,3 +121,40 @@ provide('session', session);
</script>
<style src="../src/assets/style.css"></style>
<style>
/* 侧边栏整体样式优化 */
.app-sidebar-sider {
background: #fafafa !important;
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
/* 触发器样式优化 */
.app-sidebar-sider .n-layout-sider-trigger {
background: #fff !important;
border: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
transition: all 0.2s ease !important;
width: 32px !important;
height: 32px !important;
border-radius: 50% !important;
right: -16px !important;
}
.app-sidebar-sider .n-layout-sider-trigger:hover {
background: #f5f5f5 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.app-sidebar-sider .n-layout-sider-trigger .n-base-icon {
color: #666;
font-size: 14px;
}
/* 确保侧边栏内容正确显示 */
.app-sidebar-sider .n-layout-sider-scroll-container {
height: 100%;
overflow: hidden;
}
</style>

View File

@ -1,87 +1,66 @@
<template>
<n-layout-sider
:collapsed="collapsed"
:collapsed-width="64"
:width="220"
:show-trigger="true"
:collapsible="true"
:trigger-style="{ position: 'absolute', right: '-20px' }"
:collapsed-trigger-style="{ position: 'absolute', right: '-20px' }"
class="h-screen"
@collapse="handleCollapse"
@expand="handleExpand"
>
<div style="display: block; height: 100%;">
<!-- 用户信息区域 -->
<div class="p-2">
<n-dropdown
:options="dropdownOptions"
:show-arrow="true"
trigger="click"
@select="handleDropdownSelect"
<div class="sidebar-container">
<!-- 用户信息区域 -->
<div class="sidebar-header" :class="{ 'sidebar-header-collapsed': collapsed }">
<n-dropdown
:options="dropdownOptions"
:show-arrow="true"
trigger="click"
@select="handleDropdownSelect"
>
<div
class="user-info"
:class="{ 'user-info-collapsed': collapsed }"
>
<div
class="flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-gray-200"
:class="{ 'justify-center': collapsed }"
>
<JLogo class="h-8 w-8 shrink-0 rounded" />
<div
v-if="!collapsed"
class="ml-2 flex flex-1 flex-col overflow-hidden"
>
<div class="text-base font-medium leading-none text-gray-900">
今果 Jingrow
</div>
<n-tooltip>
<template #trigger>
<div
class="mt-1 overflow-hidden text-ellipsis whitespace-nowrap pb-1 text-sm leading-none text-gray-700"
>
{{ teamUserText }}
</div>
</template>
{{ teamUserText }}
</n-tooltip>
</div>
<JLogo class="logo-icon" />
<div v-if="!collapsed" class="user-info-text">
<div class="brand-name">今果 Jingrow</div>
<n-tooltip>
<template #trigger>
<div class="user-name">
{{ teamUserText }}
</div>
</template>
{{ teamUserText }}
</n-tooltip>
</div>
</n-dropdown>
</div>
<!-- 导航菜单 -->
<div class="flex-1 overflow-auto">
<NavigationItems>
<template v-slot="{ navigation }">
<n-menu
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="24"
:options="convertToMenuOptions(navigation)"
:value="activeKey"
:router="false"
@update:value="handleMenuSelect"
class="app-sidebar-menu"
/>
</template>
</NavigationItems>
</div>
</div>
</n-dropdown>
</div>
<!-- 切换团队对话框 -->
<SwitchTeamDialog2 v-model="showTeamSwitcher" />
</n-layout-sider>
<!-- 导航菜单 -->
<div class="sidebar-menu">
<NavigationItems>
<template v-slot="{ navigation }">
<n-menu
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="20"
:options="convertToMenuOptions(navigation)"
:value="activeKey"
:router="false"
@update:value="handleMenuSelect"
class="app-sidebar-menu"
/>
</template>
</NavigationItems>
</div>
</div>
<!-- 切换团队对话框 -->
<SwitchTeamDialog2 v-model="showTeamSwitcher" />
</template>
<script>
import { defineAsyncComponent, computed, ref, h, getCurrentInstance } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { NLayoutSider, NMenu, NDropdown, NTooltip, NIcon } from 'naive-ui';
import { NMenu, NDropdown, NTooltip, NIcon } from 'naive-ui';
import NavigationItems from './NavigationItems.vue';
import JLogo from '@/components/icons/JLogo.vue';
export default {
name: 'AppSidebar',
components: {
NLayoutSider,
NMenu,
NDropdown,
NTooltip,
@ -177,11 +156,6 @@ export default {
return items
.filter((item) => item.condition !== false)
.map((item) => {
console.log(`[AppSidebar] Processing "${item.name}":`, {
hasIcon: !!item.icon,
iconType: typeof item.icon,
collapsed: collapsed.value
});
// item.icon h()
// Naive UI n-menu icon
let iconComponent = undefined;
@ -189,16 +163,6 @@ export default {
iconComponent = () => {
try {
const iconVNode = item.icon();
// VNode
console.log(`[AppSidebar] Icon render for "${item.name}":`, {
hasVNode: !!iconVNode,
vnodeType: iconVNode?.type,
vnodeTypeName: iconVNode?.type?.name || iconVNode?.type?.__name || iconVNode?.type,
vnodeProps: iconVNode?.props,
vnodeChildren: iconVNode?.children,
shapeFlag: iconVNode?.shapeFlag,
collapsed: collapsed.value
});
// VNode props size
if (iconVNode && !iconVNode.props) {
@ -214,14 +178,7 @@ export default {
}
// NIcon
const wrapped = h(NIcon, null, { default: () => iconVNode });
console.log(`[AppSidebar] Wrapped icon for "${item.name}":`, {
hasWrapped: !!wrapped,
wrappedType: wrapped?.type,
wrappedChildren: wrapped?.children,
wrappedProps: wrapped?.props
});
return wrapped;
return h(NIcon, null, { default: () => iconVNode });
} catch (e) {
console.error(`[AppSidebar] Icon error for "${item.name}":`, e);
return null;
@ -296,17 +253,6 @@ export default {
});
};
//
const handleCollapse = () => {
console.log('[AppSidebar] handleCollapse called');
emit('update:collapsed', true);
};
//
const handleExpand = () => {
console.log('[AppSidebar] handleExpand called');
emit('update:collapsed', false);
};
//
const teamUserText = computed(() => {
@ -325,8 +271,6 @@ export default {
teamUserText,
handleDropdownSelect,
handleMenuSelect,
handleCollapse,
handleExpand,
convertToMenuOptions,
};
},
@ -334,42 +278,212 @@ export default {
</script>
<style scoped>
/* 全局覆盖,确保优先级最高 */
/* 确保菜单图标在折叠状态下正确显示 */
/* 注意:不要覆盖 Naive UI 的默认 display 样式,只调整必要的属性 */
/* 侧边栏容器 */
.sidebar-container {
display: block;
height: 100%;
background: #fafafa;
}
/* 用户信息区域 */
.sidebar-header {
padding: 16px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
transition: padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-header-collapsed {
padding: 16px 8px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.user-info:hover {
background: rgba(0, 0, 0, 0.04);
}
.user-info-collapsed {
justify-content: center;
padding: 8px;
gap: 0;
}
.user-info-collapsed .logo-icon {
margin: 0 auto;
}
.logo-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.user-info-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.brand-name {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.2;
}
.user-name {
font-size: 13px;
color: #666;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 菜单区域 */
.sidebar-menu {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}
/* 菜单项样式优化 */
:deep(.app-sidebar-menu .n-menu-item) {
margin: 1px 8px;
}
:deep(.app-sidebar-menu .n-menu-item-content) {
border-radius: 8px;
padding: 10px 12px;
margin: 0;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #4a5568;
line-height: 1.5;
position: relative;
}
:deep(.app-sidebar-menu .n-menu-item-content:hover) {
background: rgba(0, 0, 0, 0.04);
}
:deep(.app-sidebar-menu .n-menu-item-content.n-menu-item-content--selected) {
background: rgba(24, 160, 88, 0.1);
color: #18a058;
font-weight: 500;
}
:deep(.app-sidebar-menu .n-menu-item-content.n-menu-item-content--selected::before) {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background: #18a058;
border-radius: 0 2px 2px 0;
}
/* 折叠状态下的菜单项 */
:deep(.app-sidebar-menu.n-menu--collapsed .n-menu-item) {
margin: 2px 8px;
}
:deep(.app-sidebar-menu.n-menu--collapsed .n-menu-item-content) {
justify-content: center;
padding: 10px;
margin: 0;
border-radius: 8px;
}
:deep(.app-sidebar-menu.n-menu--collapsed .n-menu-item-content.n-menu-item-content--selected::before) {
display: none;
}
/* 图标样式优化 - 更轻量优雅 */
:deep(.app-sidebar-menu .n-icon) {
color: #8b8e95;
transition: color 0.2s ease;
font-size: 18px;
}
/* 确保图标 SVG 可见 */
:deep(.app-sidebar-menu .n-icon svg) {
width: 100%;
height: 100%;
width: 18px;
height: 18px;
stroke-width: 1.2;
display: block;
}
/* 修复 n-layout-sider 的 flex 布局导致图标不显示的问题 */
/* 根源Naive UI 的 .n-layout-sider 默认是 display: flex这会导致内部图标无法正确渲染 */
/* 解决方案:直接覆盖 .n-layout-sider 的 display 属性,使用更高优先级的选择器 */
</style>
<style>
/* 全局样式,不使用 scoped确保能覆盖 Naive UI 的默认样式 */
/* 修复:覆盖 .n-layout-sider 的 display: flex这是导致图标不显示的根本原因 */
.n-layout-sider {
display: block !important;
/* 折叠状态下的图标 */
:deep(.app-sidebar-menu.n-menu--collapsed .n-icon) {
font-size: 20px;
}
.n-layout-sider .n-layout-sider-scroll-container {
display: block !important;
:deep(.app-sidebar-menu.n-menu--collapsed .n-icon svg) {
width: 20px;
height: 20px;
stroke-width: 1.2;
}
/* 确保所有内部容器都是 block避免 flex 布局影响图标 */
.n-layout-sider > * {
display: block !important;
/* 选中和悬停状态的图标颜色 */
:deep(.app-sidebar-menu .n-menu-item-content.n-menu-item-content--selected .n-icon) {
color: #18a058;
}
:deep(.app-sidebar-menu .n-menu-item-content:hover .n-icon) {
color: #18a058;
}
/* 菜单标签样式 */
:deep(.app-sidebar-menu .n-menu-item-content__icon) {
margin-right: 12px;
}
:deep(.app-sidebar-menu.n-menu--collapsed .n-menu-item-content__icon) {
margin-right: 0;
}
/* 子菜单样式 */
:deep(.app-sidebar-menu .n-submenu) {
margin: 2px 8px;
}
:deep(.app-sidebar-menu .n-submenu .n-submenu-children) {
padding-left: 0;
}
:deep(.app-sidebar-menu .n-submenu .n-menu-item-content) {
padding-left: 36px;
}
/* 滚动条样式 */
.sidebar-menu::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
.sidebar-menu::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.15);
}
</style>
<style scoped>
</style>