jcloud/dashboard/src2/components/AppSidebar.vue

396 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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 }"
>
<JLogo class="logo-icon" @click.stop="goToHome" />
<div v-if="!collapsed" class="user-info-text">
<div class="brand-name">Jingrow</div>
<div class="user-name">
{{ teamUserText }}
</div>
</div>
<n-icon v-if="!collapsed" class="chevron-icon">
<ChevronDown />
</n-icon>
</div>
</n-dropdown>
</div>
<!-- 导航菜单 -->
<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 { NMenu, NDropdown, NIcon } from 'naive-ui';
import NavigationItems from './NavigationItems.vue';
import JLogo from '@/components/icons/JLogo.vue';
import ChevronDown from '~icons/lucide/chevron-down';
export default {
name: 'AppSidebar',
components: {
NMenu,
NDropdown,
NIcon,
NavigationItems,
JLogo,
ChevronDown,
SwitchTeamDialog2: defineAsyncComponent(() =>
import('./SwitchTeamDialog.vue')
),
},
props: {
collapsed: {
type: Boolean,
default: false,
},
},
emits: ['update:collapsed', 'menu-select'],
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance();
const showTeamSwitcher = ref(false);
// 简化:直接使用 props响应式处理由父组件负责
const collapsed = computed(() => props.collapsed);
// 下拉菜单选项
const dropdownOptions = computed(() => {
const options = [];
// 通过 getCurrentInstance 访问全局属性
const team = instance?.appContext.config.globalProperties.$team?.pg;
if (
team &&
(team.valid_teams?.length > 1 || team.is_desk_user)
) {
options.push({
label: '切换团队',
key: 'switch-team',
});
}
options.push({
label: '退出登录',
key: 'logout',
});
return options;
});
// 处理下拉菜单选择
const handleDropdownSelect = (key) => {
if (key === 'switch-team') {
showTeamSwitcher.value = true;
} else if (key === 'logout') {
const session = instance?.appContext.config.globalProperties.$session;
if (session) {
session.logout.submit();
}
}
};
// 转换导航项为菜单选项
const convertToMenuOptions = (items) => {
if (!items || !Array.isArray(items)) {
console.warn('[AppSidebar] convertToMenuOptions: invalid items', items);
return [];
}
return items
.filter((item) => item.condition !== false)
.map((item) => {
// 处理图标:简化处理,直接使用渲染函数
// Naive UI 的 NMenu 可以直接接受渲染函数作为 icon
let iconComponent = undefined;
if (item.icon) {
iconComponent = () => {
try {
const iconVNode = item.icon();
// 用 NIcon 包装以保持样式一致性
return h(NIcon, { size: 18 }, { default: () => iconVNode });
} catch (e) {
console.error(`[AppSidebar] Icon error for "${item.name}":`, e);
return null;
}
};
}
// 处理路由NavigationItems 中的 route 已经是相对于 /dashboard/ 的路径(如 '/sites'
// 路由配置的 base 是 '/dashboard/',所以直接使用 item.route 即可
let routePath = item.route || item.name;
const option = {
label: item.name,
key: routePath,
icon: iconComponent,
disabled: item.disabled,
};
// 处理子菜单
if (item.children && item.children.length > 0) {
option.children = convertToMenuOptions(item.children);
}
// 处理徽章
if (item.badge) {
try {
// item.badge 是一个函数,调用它获取徽章组件
const badgeResult = item.badge();
if (badgeResult) {
option.label = () => h('div', {
class: 'flex items-center justify-between w-full'
}, [
h('span', item.name),
badgeResult,
]);
}
} catch (e) {
// 徽章处理失败,使用默认标签
console.warn('Badge rendering failed:', e);
}
}
return option;
});
};
// 当前激活的菜单项
const activeKey = computed(() => {
// 路由配置的 base 是 '/dashboard/'route.path 返回的是相对于 base 的路径
// 例如:如果访问 /dashboard/sitesroute.path 应该是 '/sites'
let path = route.path;
// 确保路径格式正确(以 / 开头)
if (!path.startsWith('/')) {
path = '/' + path;
}
// 如果路径是 /dashboard 或 /,统一处理
if (path === '/dashboard' || path === '/') {
return path;
}
return path;
});
// 处理菜单选择
// 注意:由于路由有 base 路径 '/dashboard/',不能使用 router="true"
// 需要手动处理路由导航
const handleMenuSelect = (key) => {
// 路由配置的 base 是 '/dashboard/'key 已经是相对于 base 的路径(如 '/sites'
// 直接使用 router.pushVue Router 会自动处理 base
router.push(key).catch((err) => {
// 忽略导航重复的错误
if (err.name !== 'NavigationDuplicated') {
console.error('Navigation error:', err);
}
});
// 发出菜单选择事件,用于移动端自动关闭侧边栏
emit('menu-select');
};
// 团队用户文本
const teamUserText = computed(() => {
const team = instance?.appContext.config.globalProperties.$team;
if (team?.get?.loading) {
return '加载中...';
}
return team?.pg?.user || '';
});
// 跳转到首页
const goToHome = () => {
router.push('/').catch((err) => {
if (err.name !== 'NavigationDuplicated') {
console.error('Navigation error:', err);
}
});
};
return {
showTeamSwitcher,
collapsed,
dropdownOptions,
activeKey,
teamUserText,
handleDropdownSelect,
handleMenuSelect,
convertToMenuOptions,
goToHome,
};
},
};
</script>
<style scoped>
.sidebar-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 100%;
background: #fafafa;
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-header {
padding: 16px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
transition: padding 0.25s 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: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
padding 0.25s cubic-bezier(0.4, 0, 0.2, 1),
gap 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.user-info:hover {
background: rgba(0, 0, 0, 0.04);
}
.user-info:active {
background: rgba(0, 0, 0, 0.06);
}
.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;
cursor: pointer;
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.logo-icon:hover {
opacity: 0.8;
}
.user-info-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
opacity: 1;
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1),
max-width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
max-width: 200px;
overflow: hidden;
}
.user-info-collapsed .user-info-text {
opacity: 0;
max-width: 0;
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
max-width 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.brand-name {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.2;
transition: opacity 0.2s ease;
}
.user-name {
font-size: 13px;
color: #666;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity 0.2s ease;
}
.chevron-icon {
font-size: 16px;
color: #8b8e95;
margin-left: auto;
flex-shrink: 0;
transition: color 0.2s ease;
}
.user-info:hover .chevron-icon {
color: #4a5568;
}
.sidebar-menu {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}
:deep(.app-sidebar-menu .n-icon) {
color: #8b8e95;
}
.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>