左边栏基于naive ui重构

This commit is contained in:
jingrow 2025-12-28 01:12:59 +08:00
parent a61e776d3a
commit 4f27721cd1
2 changed files with 281 additions and 83 deletions

View File

@ -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',

View File

@ -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/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 { 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>