Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 7 additions & 38 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
<template>
<div :class="isDark ? 'dark' : ''">
<n-config-provider :theme="theme" :locale="naiveLocale" :theme-overrides="themeOverrides" :date-locale="dateLocale">
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-config-provider>
<div class="min-h-screen" :class="{ dark: isDark }">
<AppUiProvider>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</AppUiProvider>
</div>
</template>

<script lang="ts" setup>
import AppUiProvider from '@/components/providers/AppUiProvider.vue';
import { useColorMode } from '@vueuse/core';
import { darkTheme, dateZhCN, enUS, zhCN } from 'naive-ui';
import { computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { themeOverrides } from '~/config/theme';

const { system, store } = useColorMode();
const { locale } = useI18n();
Expand All @@ -28,30 +21,6 @@ const isDark = computed(() => {
return store.value === 'dark' || (store.value === 'auto' && system.value === 'dark');
});

const themeName = computed(() => {
return store.value === 'auto' ? system.value : store.value;
});

const theme = computed(() => {
if (isDark.value) {
return darkTheme;
}

return { name: themeName.value };
});

const naiveLocale = computed(() => {
// 安全地比较locale的值,避免TypeScript类型错误
const currentLocale = locale.value.toString();
return currentLocale === 'zh-CN' || currentLocale === 'zh' ? zhCN : enUS;
});

const dateLocale = computed(() => {
// 安全地比较locale的值,避免TypeScript类型错误
const currentLocale = locale.value.toString();
return currentLocale === 'zh-CN' || currentLocale === 'zh' ? dateZhCN : null;
});

// 监听语言变化
watch(locale, newLocale => {
console.log('Language changed to:', newLocale);
Expand Down
8 changes: 4 additions & 4 deletions auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// biome-ignore lint: disable
export {}
declare global {
const useDialog: typeof import('naive-ui')['useDialog']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useNotification: typeof import('naive-ui')['useNotification']
const useDialog: typeof import('~/composables/ui')['useDialog']
const useLoadingBar: typeof import('~/composables/ui')['useLoadingBar']
const useMessage: typeof import('~/composables/ui')['useMessage']
const useNotification: typeof import('~/composables/ui')['useNotification']
}
185 changes: 185 additions & 0 deletions components/AppSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<script setup lang="ts">
import { Icon } from '#components';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
SidebarTrigger,
useSidebar,
} from '@/components/ui/sidebar';
import LanguageSwitcher from '@/components/language-switcher.vue';
import ThemeSwitcher from '@/components/theme-switcher.vue';
import UserDropdown from '@/components/user-dropdown.vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, RouterLink } from 'vue-router';
import type { AppConfig, NavItem } from '~/types/app-config';
import { icon } from '~/utils/ui';

const appConfig = useAppConfig() as unknown as AppConfig;
const route = useRoute();
const { t } = useI18n();
const { state } = useSidebar();

const isCollapsed = computed(() => state.value === 'collapsed');

const navGroups = computed(() => {
const groups: NavItem[][] = [];
let current: NavItem[] = [];

for (const nav of appConfig.navs) {
if (nav.type === 'divider') {
if (current.length) {
groups.push(current);
current = [];
}
continue;
}

current.push(nav);
}

if (current.length) {
groups.push(current);
}

return groups;
});

const normalizeTo = (item: NavItem) => item.to || '/';

const isExternalLink = (item: NavItem) => Boolean(item.target) || /^https?:/i.test(item.to || '');

const isRouteActive = (item: NavItem): boolean => {
if (!item.to) {
if (item.children?.length) {
return item.children.some(child => isRouteActive(child));
}

return false;
}

if (item.children?.length) {
return item.children.some(child => isRouteActive(child));
}

if (isExternalLink(item)) {
return false;
}

return route.path.startsWith(item.to);
};

const renderLabel = (item: NavItem) => t(item.label);

const renderIcon = (item: NavItem) => {
if (!item.icon) {
return null;
}

return icon(item.icon);
};
</script>

<template>
<Sidebar collapsible="icon" class="bg-sidebar text-sidebar-foreground">
<SidebarHeader class="border-b border-sidebar-border px-4 py-3">
<div class="flex items-center gap-3">
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary text-primary-foreground font-bold">
<span v-if="isCollapsed">{{ appConfig.name.substring(0, 1) }}</span>
<img v-else src="~/assets/logo.svg" alt="RustFS" class="h-8" />
</div>
<div v-if="!isCollapsed" class="flex flex-col">
<span class="text-sm font-semibold leading-tight">{{ appConfig.name }}</span>
<span class="text-xs text-muted-foreground">{{ appConfig.description }}</span>
</div>
</div>
<SidebarTrigger class="ml-auto" />
</SidebarHeader>

<SidebarContent class="px-2 py-4">
<div class="flex flex-col gap-6">
<template v-for="(group, groupIndex) in navGroups" :key="groupIndex">
<SidebarMenu>
<SidebarMenuItem v-for="item in group" :key="item.label">
<component
v-if="item.children?.length"
:is="SidebarMenuButton"
:is-active="isRouteActive(item)"
class="items-start"
>
<component
:is="isExternalLink(item) ? 'a' : RouterLink"
:to="isExternalLink(item) ? undefined : normalizeTo(item)"
:href="isExternalLink(item) ? normalizeTo(item) : undefined"
:target="item.target"
class="flex flex-1 items-center gap-3"
>
<component :is="renderIcon(item)" v-if="item.icon" />
<span class="flex-1 text-sm font-medium">{{ renderLabel(item) }}</span>
</component>
</component>
<SidebarMenuSub v-if="item.children?.length">
<SidebarMenuSubItem v-for="child in item.children" :key="child.label">
<SidebarMenuSubButton
as-child
size="sm"
:is-active="isRouteActive(child)"
>
<component
:is="isExternalLink(child) ? 'a' : RouterLink"
:to="isExternalLink(child) ? undefined : normalizeTo(child)"
:href="isExternalLink(child) ? normalizeTo(child) : undefined"
:target="child.target"
class="flex flex-1 items-center gap-2"
>
<component :is="renderIcon(child)" v-if="child.icon" />
<span>{{ renderLabel(child) }}</span>
</component>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
<SidebarMenuButton v-else as-child :is-active="isRouteActive(item)">
<component
:is="isExternalLink(item) ? 'a' : RouterLink"
:to="isExternalLink(item) ? undefined : normalizeTo(item)"
:href="isExternalLink(item) ? normalizeTo(item) : undefined"
:target="item.target"
class="flex w-full items-center gap-3"
>
<component :is="renderIcon(item)" v-if="item.icon" />
<span class="flex-1 truncate text-sm font-medium">{{ renderLabel(item) }}</span>
<Icon
v-if="isExternalLink(item)"
name="ri:external-link-line"
class="h-3.5 w-3.5 text-muted-foreground"
/>
</component>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<div v-if="groupIndex !== navGroups.length - 1" class="border-t border-sidebar-border" />
</template>
</div>
</SidebarContent>

<SidebarFooter class="mt-auto border-t border-sidebar-border px-4 py-4">
<div class="flex flex-col gap-3">
<div class="grid grid-cols-1 gap-2" :class="{ 'grid-cols-1': isCollapsed, 'grid-cols-2': !isCollapsed }">
<LanguageSwitcher />
<ThemeSwitcher v-if="!isCollapsed" />
</div>
<UserDropdown :is-collapsed="isCollapsed" />
</div>
</SidebarFooter>
</Sidebar>
<SidebarRail />
</template>
Loading