左边栏基于naive ui重构
This commit is contained in:
parent
a61e776d3a
commit
4f27721cd1
@ -1,14 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex h-full flex-col">
|
<div class="relative flex h-full flex-col">
|
||||||
<div class="h-full flex-1">
|
<n-layout class="h-full" has-sider>
|
||||||
<div class="flex h-full">
|
<AppSidebar
|
||||||
<div
|
v-if="!isSignupFlow && !$isMobile && !isHideSidebar && $session.user"
|
||||||
v-if="!isSignupFlow && !$isMobile && !isHideSidebar"
|
v-model:collapsed="sidebarCollapsed"
|
||||||
class="relative block min-h-0 flex-shrink-0 overflow-hidden hover:overflow-auto"
|
/>
|
||||||
>
|
<n-layout class="h-full">
|
||||||
<AppSidebar v-if="$session.user" />
|
<div class="w-full h-full overflow-auto" id="scrollContainer">
|
||||||
</div>
|
|
||||||
<div class="w-full overflow-auto" id="scrollContainer">
|
|
||||||
<MobileNav
|
<MobileNav
|
||||||
v-if="!isSignupFlow && $isMobile && !isHideSidebar && $session.user"
|
v-if="!isSignupFlow && $isMobile && !isHideSidebar && $session.user"
|
||||||
/>
|
/>
|
||||||
@ -27,15 +25,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</n-layout>
|
||||||
</div>
|
</n-layout>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
<component v-for="dialog in dialogs" :is="dialog" :key="dialog.id" />
|
<component v-for="dialog in dialogs" :is="dialog" :key="dialog.id" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineAsyncComponent, computed, watch, ref, provide } from 'vue';
|
import { defineAsyncComponent, computed, watch, ref, provide, onMounted } from 'vue';
|
||||||
|
import { NLayout } from 'naive-ui';
|
||||||
import { Toaster } from 'vue-sonner';
|
import { Toaster } from 'vue-sonner';
|
||||||
import { dialogs } from './utils/components';
|
import { dialogs } from './utils/components';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@ -52,6 +51,20 @@ const MobileNav = defineAsyncComponent(
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const team = getTeam();
|
const team = getTeam();
|
||||||
|
|
||||||
|
// 侧边栏折叠状态
|
||||||
|
const sidebarCollapsed = ref(false);
|
||||||
|
|
||||||
|
// 响应式:在移动端默认折叠
|
||||||
|
onMounted(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
sidebarCollapsed.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
});
|
||||||
|
|
||||||
const isHideSidebar = computed(() => {
|
const isHideSidebar = computed(() => {
|
||||||
const alwaysHideSidebarRoutes = [
|
const alwaysHideSidebarRoutes = [
|
||||||
'Site Login',
|
'Site Login',
|
||||||
|
|||||||
@ -1,98 +1,283 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<n-layout-sider
|
||||||
class="relative flex min-h-screen w-[220px] flex-col border-r bg-gray-50"
|
: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="p-2">
|
<div class="flex h-full flex-col">
|
||||||
<Dropdown
|
<!-- 用户信息区域 -->
|
||||||
:options="[
|
<div class="p-2">
|
||||||
{
|
<n-dropdown
|
||||||
label: '切换团队',
|
:options="dropdownOptions"
|
||||||
icon: 'command',
|
:show-arrow="true"
|
||||||
condition: () =>
|
trigger="click"
|
||||||
$team?.pg?.valid_teams?.length > 1 || $team?.pg?.is_desk_user,
|
@select="handleDropdownSelect"
|
||||||
onClick: () => (showTeamSwitcher = true)
|
>
|
||||||
},
|
<div
|
||||||
{
|
class="flex cursor-pointer items-center rounded-md px-2 py-2 transition-colors hover:bg-gray-200"
|
||||||
label: '退出登录',
|
:class="{ 'justify-center': collapsed }"
|
||||||
icon: 'log-out',
|
|
||||||
onClick: $session.logout.submit
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<template v-slot="{ open }">
|
|
||||||
<button
|
|
||||||
class="flex w-[204px] items-center rounded-md px-2 py-2 text-left"
|
|
||||||
:class="open ? 'bg-white shadow-sm' : 'hover:bg-gray-200'"
|
|
||||||
>
|
>
|
||||||
<JLogo class="mb-1 h-8 w-8 shrink-0 rounded" />
|
<JLogo class="h-8 w-8 shrink-0 rounded" />
|
||||||
<div class="ml-2 flex flex-1 flex-col overflow-hidden">
|
<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">
|
<div class="text-base font-medium leading-none text-gray-900">
|
||||||
今果 Jingrow
|
今果 Jingrow
|
||||||
</div>
|
</div>
|
||||||
<Tooltip :text="$team?.pg?.user || null">
|
<n-tooltip>
|
||||||
<div
|
<template #trigger>
|
||||||
class="mt-1 hidden overflow-hidden text-ellipsis whitespace-nowrap pb-1 text-sm leading-none text-gray-700 sm:inline"
|
<div
|
||||||
>
|
class="mt-1 overflow-hidden text-ellipsis whitespace-nowrap pb-1 text-sm leading-none text-gray-700"
|
||||||
{{ $team?.get.loading ? '加载中...' : $team?.pg?.user }}
|
>
|
||||||
</div>
|
{{ teamUserText }}
|
||||||
</Tooltip>
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ teamUserText }}
|
||||||
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<FeatherIcon
|
</div>
|
||||||
name="chevron-down"
|
</n-dropdown>
|
||||||
class="ml-auto h-5 w-5 text-gray-700"
|
</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"
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<nav class="px-2">
|
|
||||||
<NavigationItems>
|
|
||||||
<template v-slot="{ navigation }">
|
|
||||||
<template v-for="(item, i) in navigation" :key="item.name">
|
|
||||||
<AppSidebarItemGroup v-if="item.children" :item="item" />
|
|
||||||
<AppSidebarItem class="mt-0.5" v-else :item="item" />
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</NavigationItems>
|
||||||
</NavigationItems>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
<!-- TODO: update component name after dashboard-beta merges -->
|
|
||||||
|
<!-- 切换团队对话框 -->
|
||||||
<SwitchTeamDialog2 v-model="showTeamSwitcher" />
|
<SwitchTeamDialog2 v-model="showTeamSwitcher" />
|
||||||
</div>
|
</n-layout-sider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent, computed, ref, h, getCurrentInstance } from 'vue';
|
||||||
import AppSidebarItem from './AppSidebarItem.vue';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { Tooltip } from 'jingrow-ui';
|
import { NLayoutSider, NMenu, NDropdown, NTooltip } from 'naive-ui';
|
||||||
import NavigationItems from './NavigationItems.vue';
|
import NavigationItems from './NavigationItems.vue';
|
||||||
import AppSidebarItemGroup from './AppSidebarItemGroup.vue';
|
import JLogo from '@/components/icons/JLogo.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppSidebar',
|
name: 'AppSidebar',
|
||||||
components: {
|
components: {
|
||||||
AppSidebarItem,
|
NLayoutSider,
|
||||||
AppSidebarItemGroup,
|
NMenu,
|
||||||
|
NDropdown,
|
||||||
|
NTooltip,
|
||||||
|
NavigationItems,
|
||||||
|
JLogo,
|
||||||
SwitchTeamDialog2: defineAsyncComponent(() =>
|
SwitchTeamDialog2: defineAsyncComponent(() =>
|
||||||
import('./SwitchTeamDialog.vue')
|
import('./SwitchTeamDialog.vue')
|
||||||
),
|
),
|
||||||
Tooltip,
|
|
||||||
NavigationItems
|
|
||||||
},
|
},
|
||||||
data() {
|
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 {
|
return {
|
||||||
showTeamSwitcher: false
|
showTeamSwitcher,
|
||||||
|
collapsed,
|
||||||
|
dropdownOptions,
|
||||||
|
activeKey,
|
||||||
|
teamUserText,
|
||||||
|
handleDropdownSelect,
|
||||||
|
handleMenuSelect,
|
||||||
|
handleCollapse,
|
||||||
|
handleExpand,
|
||||||
|
convertToMenuOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
support() {
|
|
||||||
window.open('https://jingrow.com/support', '_blank');
|
|
||||||
},
|
|
||||||
feedback() {
|
|
||||||
window.open(
|
|
||||||
'https://jingrow.com/feedback/new',
|
|
||||||
'_blank'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user