283 lines
7.6 KiB
Vue
283 lines
7.6 KiB
Vue
<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/sites,route.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.push,Vue 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> |