jcloud/dashboard/src2/components/AppSidebar.vue

283 lines
7.6 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>
<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 class="flex h-full flex-col">
<!-- 用户信息区域 -->
<div class="p-2">
<n-dropdown
:options="dropdownOptions"
:show-arrow="true"
trigger="click"
@select="handleDropdownSelect"
>
<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>
</div>
</n-dropdown>
</div>
<!-- 导航菜单 -->
<div class="flex-1 overflow-auto">
<NavigationItems>
<template v-slot="{ navigation }">
<n-menu
:collapsed="collapsed"
:collapsed-width="64"
:options="convertToMenuOptions(navigation)"
:value="activeKey"
:router="false"
@update:value="handleMenuSelect"
/>
</template>
</NavigationItems>
</div>
</div>
<!-- 切换团队对话框 -->
<SwitchTeamDialog2 v-model="showTeamSwitcher" />
</n-layout-sider>
</template>
<script>
import { defineAsyncComponent, computed, ref, h, getCurrentInstance } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { NLayoutSider, NMenu, NDropdown, NTooltip } from 'naive-ui';
import NavigationItems from './NavigationItems.vue';
import JLogo from '@/components/icons/JLogo.vue';
export default {
name: 'AppSidebar',
components: {
NLayoutSider,
NMenu,
NDropdown,
NTooltip,
NavigationItems,
JLogo,
SwitchTeamDialog2: defineAsyncComponent(() =>
import('./SwitchTeamDialog.vue')
),
},
props: {
collapsed: {
type: Boolean,
default: false,
},
},
emits: ['update:collapsed'],
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance();
const showTeamSwitcher = ref(false);
// 响应式:在移动端默认折叠
const isMobile = computed(() => {
if (typeof window === 'undefined') return false;
return window.innerWidth < 768;
});
const collapsed = computed({
get: () => {
// 如果 props 有值,使用 props否则根据屏幕大小判断
if (props.collapsed !== undefined) {
return props.collapsed;
}
return isMobile.value;
},
set: (value) => emit('update:collapsed', value),
});
// 监听窗口大小变化
if (typeof window !== 'undefined') {
const handleResize = () => {
if (window.innerWidth < 768 && !props.collapsed) {
emit('update:collapsed', true);
}
};
window.addEventListener('resize', handleResize);
}
// 下拉菜单选项
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',
icon: () => h('i', { class: 'i-lucide-command h-4 w-4' }),
});
}
options.push({
label: '退出登录',
key: 'logout',
icon: () => h('i', { class: 'i-lucide-log-out h-4 w-4' }),
});
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)) return [];
return items
.filter((item) => item.condition !== false)
.map((item) => {
// 处理图标item.icon 是一个函数,返回 h() 创建的组件
// Naive UI 的 n-menu 需要 icon 是一个渲染函数
let iconComponent = undefined;
if (item.icon) {
// item.icon 本身就是一个函数,直接使用
// 但需要确保它返回一个有效的 VNode
iconComponent = item.icon;
}
// 处理路由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;
});
// 处理菜单选择
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);
}
});
};
// 处理折叠
const handleCollapse = () => {
emit('update:collapsed', true);
};
// 处理展开
const handleExpand = () => {
emit('update:collapsed', false);
};
// 团队用户文本
const teamUserText = computed(() => {
const team = instance?.appContext.config.globalProperties.$team;
if (team?.get?.loading) {
return '加载中...';
}
return team?.pg?.user || '';
});
return {
showTeamSwitcher,
collapsed,
dropdownOptions,
activeKey,
teamUserText,
handleDropdownSelect,
handleMenuSelect,
handleCollapse,
handleExpand,
convertToMenuOptions,
};
},
};
</script>