mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 02:26:36 +08:00
prettier
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import {usePathname, useRouter} from "next/navigation"
|
||||
import {APP_BUILD_DATE, APP_VERSION} from "@/lib/app-info"
|
||||
import {toast} from "sonner"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Spinner} from "@/components/ui/spinner"
|
||||
import {AnimateIcon} from "@/components/animate-ui/icons/icon"
|
||||
import {ChevronLeft} from "@/components/animate-ui/icons/chevron-left"
|
||||
import {ChevronRight} from "@/components/animate-ui/icons/chevron-right"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { APP_BUILD_DATE, APP_VERSION } from '@/lib/app-info';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { AnimateIcon } from '@/components/animate-ui/icons/icon';
|
||||
import { ChevronLeft } from '@/components/animate-ui/icons/chevron-left';
|
||||
import { ChevronRight } from '@/components/animate-ui/icons/chevron-right';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {getCurrentTheme} from "@/components/layout/avater-style/registry"
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { getCurrentTheme } from '@/components/layout/avater-style/registry';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from '@/components/ui/sidebar';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bell,
|
||||
@@ -57,44 +57,55 @@ import {
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
UserRound,
|
||||
} from "lucide-react"
|
||||
} from 'lucide-react';
|
||||
|
||||
import {useUser} from "@/contexts/user-context"
|
||||
import {usePublicConfig} from "@/hooks/use-public-config"
|
||||
import {OpenFlareSidebarMenu} from "@/components/layout/openflare-sidebar-menu"
|
||||
import {openflareSidebarNav} from "@/lib/navigation/openflare-nav"
|
||||
import { useUser } from '@/contexts/user-context';
|
||||
import { usePublicConfig } from '@/hooks/use-public-config';
|
||||
import { OpenFlareSidebarMenu } from '@/components/layout/openflare-sidebar-menu';
|
||||
import { openflareSidebarNav } from '@/lib/navigation/openflare-nav';
|
||||
|
||||
/* 导航数据 */
|
||||
const data = {
|
||||
admin: [
|
||||
{ title: "用户管理", url: "/admin/users", icon: UserRound },
|
||||
{ title: "任务管理", url: "/admin/tasks", icon: Layers },
|
||||
{ title: "存储管理", url: "/admin/files", icon: FolderOpen },
|
||||
{ title: "数据管理", url: "/admin/database", icon: Database },
|
||||
{ title: "通知推送", url: "/admin/push", icon: Bell },
|
||||
{ title: "系统日志", url: "/admin/logs", icon: Terminal },
|
||||
{ title: "系统配置", url: "/admin/system", icon: ShieldCheck },
|
||||
{ title: "系统设置", url: "/admin/settings", icon: Settings },
|
||||
{ title: '用户管理', url: '/admin/users', icon: UserRound },
|
||||
{ title: '任务管理', url: '/admin/tasks', icon: Layers },
|
||||
{ title: '存储管理', url: '/admin/files', icon: FolderOpen },
|
||||
{ title: '数据管理', url: '/admin/database', icon: Database },
|
||||
{ title: '通知推送', url: '/admin/push', icon: Bell },
|
||||
{ title: '系统日志', url: '/admin/logs', icon: Terminal },
|
||||
{ title: '系统配置', url: '/admin/system', icon: ShieldCheck },
|
||||
{ title: '系统设置', url: '/admin/settings', icon: Settings },
|
||||
],
|
||||
document: [
|
||||
{ title: "使用文档", url: "https://open-flare.pages.dev/", icon: FileText, external: true },
|
||||
{
|
||||
title: '使用文档',
|
||||
url: 'https://open-flare.pages.dev/',
|
||||
icon: FileText,
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
function parseMenuDisplayConfig(raw: string | undefined): Record<string, boolean> {
|
||||
if (!raw) return {}
|
||||
function parseMenuDisplayConfig(
|
||||
raw: string | undefined,
|
||||
): Record<string, boolean> {
|
||||
if (!raw) return {};
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
||||
return {};
|
||||
|
||||
return Object.entries(parsed).reduce<Record<string, boolean>>((result, [key, value]) => {
|
||||
if (typeof value === "boolean") {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
return Object.entries(parsed).reduce<Record<string, boolean>>(
|
||||
(result, [key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{},
|
||||
);
|
||||
} catch {
|
||||
return {}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,128 +120,147 @@ function parseMenuDisplayConfig(raw: string | undefined): Record<string, boolean
|
||||
* @returns {React.ReactNode} 应用侧边栏组件
|
||||
*/
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { toggleSidebar, state, isMobile, setOpenMobile } = useSidebar()
|
||||
const {user, logout, loading: userLoading} = useUser()
|
||||
const { config } = usePublicConfig()
|
||||
const [showLogoutDialog, setShowLogoutDialog] = React.useState(false)
|
||||
const [isLoggingOut, setIsLoggingOut] = React.useState(false)
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { toggleSidebar, state, isMobile, setOpenMobile } = useSidebar();
|
||||
const { user, logout, loading: userLoading } = useUser();
|
||||
const { config } = usePublicConfig();
|
||||
const [showLogoutDialog, setShowLogoutDialog] = React.useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = React.useState(false);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const currentTheme = getCurrentTheme()
|
||||
const currentTheme = getCurrentTheme();
|
||||
const {
|
||||
MainDecoration,
|
||||
InteractionDecoration,
|
||||
BackgroundEffect,
|
||||
smallMainDecorationConfig,
|
||||
largeMainDecorationConfig,
|
||||
interactionDecorationConfig
|
||||
} = currentTheme
|
||||
const hasAvatarDecoration = currentTheme.key !== "none"
|
||||
interactionDecorationConfig,
|
||||
} = currentTheme;
|
||||
const hasAvatarDecoration = currentTheme.key !== 'none';
|
||||
|
||||
const [showEffect, setShowEffect] = React.useState(false)
|
||||
const [userMenuOpen, setUserMenuOpen] = React.useState(false)
|
||||
const userMenuLastToggle = React.useRef(0)
|
||||
const [showEffect, setShowEffect] = React.useState(false);
|
||||
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
|
||||
const userMenuLastToggle = React.useRef(0);
|
||||
|
||||
const handleUserMenuOpenChange = React.useCallback((open: boolean) => {
|
||||
const now = Date.now()
|
||||
if (now - userMenuLastToggle.current < 65) return
|
||||
userMenuLastToggle.current = now
|
||||
setUserMenuOpen(open)
|
||||
}, [])
|
||||
const now = Date.now();
|
||||
if (now - userMenuLastToggle.current < 65) return;
|
||||
userMenuLastToggle.current = now;
|
||||
setUserMenuOpen(open);
|
||||
}, []);
|
||||
|
||||
const handleCloseSidebar = React.useCallback(() => {
|
||||
if (isMobile) {
|
||||
setOpenMobile(false)
|
||||
setOpenMobile(false);
|
||||
}
|
||||
}, [isMobile, setOpenMobile])
|
||||
}, [isMobile, setOpenMobile]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true)
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await logout()
|
||||
setShowLogoutDialog(false)
|
||||
await logout();
|
||||
setShowLogoutDialog(false);
|
||||
} catch (error) {
|
||||
toast.error("登出失败", {
|
||||
description: error instanceof Error ? error.message : "登出时发生错误,请重试"
|
||||
})
|
||||
setIsLoggingOut(false)
|
||||
toast.error('登出失败', {
|
||||
description:
|
||||
error instanceof Error ? error.message : '登出时发生错误,请重试',
|
||||
});
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const adminFiltered = React.useMemo(() => {
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config)
|
||||
return data.admin.filter((item) => displayConfig[item.url] !== false)
|
||||
}, [config])
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config);
|
||||
return data.admin.filter((item) => displayConfig[item.url] !== false);
|
||||
}, [config]);
|
||||
|
||||
const documentFiltered = React.useMemo(() => {
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config)
|
||||
return data.document.filter((item) => displayConfig[item.url] !== false)
|
||||
}, [config])
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config);
|
||||
return data.document.filter((item) => displayConfig[item.url] !== false);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasAvatarDecoration && showEffect && BackgroundEffect && <BackgroundEffect />}
|
||||
<Sidebar collapsible="icon" {...props} className="px-2 relative border-r border-border/40 group-data-[collapsible=icon]">
|
||||
{hasAvatarDecoration && showEffect && BackgroundEffect && (
|
||||
<BackgroundEffect />
|
||||
)}
|
||||
<Sidebar
|
||||
collapsible='icon'
|
||||
{...props}
|
||||
className='px-2 relative border-r border-border/40 group-data-[collapsible=icon]'
|
||||
>
|
||||
<Button
|
||||
onClick={toggleSidebar}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-1/2 -right-6 w-2 h-4 text-muted-foreground hover:bg-background hidden md:flex"
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='absolute top-1/2 -right-6 w-2 h-4 text-muted-foreground hover:bg-background hidden md:flex'
|
||||
>
|
||||
{state === "expanded" ? (
|
||||
{state === 'expanded' ? (
|
||||
<AnimateIcon animateOnHover>
|
||||
<ChevronLeft className="size-6" />
|
||||
<ChevronLeft className='size-6' />
|
||||
</AnimateIcon>
|
||||
) : (
|
||||
<AnimateIcon animateOnHover>
|
||||
<ChevronRight className="size-6" />
|
||||
<ChevronRight className='size-6' />
|
||||
</AnimateIcon>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<SidebarHeader className="py-4 md:-ml-2">
|
||||
<DropdownMenu open={userMenuOpen} onOpenChange={handleUserMenuOpenChange}>
|
||||
<SidebarHeader className='py-4 md:-ml-2'>
|
||||
<DropdownMenu
|
||||
open={userMenuOpen}
|
||||
onOpenChange={handleUserMenuOpenChange}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-12 hover:bg-accent group-data-[collapsible=icon]:ml-2"
|
||||
variant='ghost'
|
||||
className='flex h-12 hover:bg-accent group-data-[collapsible=icon]:ml-2'
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<div className="relative overflow-visible">
|
||||
{hasAvatarDecoration && <MainDecoration className={smallMainDecorationConfig?.className} />}
|
||||
<Avatar className="size-6 rounded relative z-10">
|
||||
<AvatarImage
|
||||
src={user?.avatar_url}
|
||||
alt={user?.nickname}
|
||||
<div className='relative overflow-visible'>
|
||||
{hasAvatarDecoration && (
|
||||
<MainDecoration
|
||||
className={smallMainDecorationConfig?.className}
|
||||
/>
|
||||
<AvatarFallback className="rounded bg-muted text-sm">
|
||||
{user?.nickname?.charAt(0)?.toUpperCase() || "L"}
|
||||
)}
|
||||
<Avatar className='size-6 rounded relative z-10'>
|
||||
<AvatarImage src={user?.avatar_url} alt={user?.nickname} />
|
||||
<AvatarFallback className='rounded bg-muted text-sm'>
|
||||
{user?.nickname?.charAt(0)?.toUpperCase() || 'L'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex flex-col items-start flex-1 text-left group-data-[collapsible=icon]:hidden">
|
||||
<span className="text-xs font-medium truncate w-full text-left ml-2">
|
||||
{user?.nickname || user?.username || "Unknown User"}
|
||||
<div className='flex flex-col items-start flex-1 text-left group-data-[collapsible=icon]:hidden'>
|
||||
<span className='text-xs font-medium truncate w-full text-left ml-2'>
|
||||
{user?.nickname || user?.username || 'Unknown User'}
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-muted-foreground/100 truncate w-full text-left ml-2">
|
||||
{user?.is_admin ? "系统管理员" : "普通用户"}
|
||||
<span className='text-[11px] font-medium text-muted-foreground/100 truncate w-full text-left ml-2'>
|
||||
{user?.is_admin ? '系统管理员' : '普通用户'}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="size-4 text-muted-foreground ml-auto group-data-[collapsible=icon]:hidden" />
|
||||
<ChevronDown className='size-4 text-muted-foreground ml-auto group-data-[collapsible=icon]:hidden' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className={isMobile || state === "collapsed" ? "ml-1 w-68 z-[200]" : "w-64 z-[200]"}
|
||||
align="start"
|
||||
side={isMobile || state === "collapsed" ? "bottom" : "right"}
|
||||
className={
|
||||
isMobile || state === 'collapsed'
|
||||
? 'ml-1 w-68 z-[200]'
|
||||
: 'w-64 z-[200]'
|
||||
}
|
||||
align='start'
|
||||
side={isMobile || state === 'collapsed' ? 'bottom' : 'right'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-col items-center mb-4 gap-1 pt-4">
|
||||
<div className='flex flex-col items-center mb-4 gap-1 pt-4'>
|
||||
<div
|
||||
className={`relative w-32 h-16 flex items-center justify-center overflow-visible select-none transition-transform ${hasAvatarDecoration ? "cursor-pointer active:scale-95" : ""}`}
|
||||
onClick={hasAvatarDecoration ? () => setShowEffect(!showEffect) : undefined}
|
||||
className={`relative w-32 h-16 flex items-center justify-center overflow-visible select-none transition-transform ${hasAvatarDecoration ? 'cursor-pointer active:scale-95' : ''}`}
|
||||
onClick={
|
||||
hasAvatarDecoration
|
||||
? () => setShowEffect(!showEffect)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{hasAvatarDecoration && (
|
||||
<InteractionDecoration
|
||||
@@ -238,79 +268,103 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
className={interactionDecorationConfig?.className}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10">
|
||||
{hasAvatarDecoration && <MainDecoration className={largeMainDecorationConfig?.className} />}
|
||||
<Avatar className="size-14 rounded-full pointer-events-none">
|
||||
<div className='relative z-10'>
|
||||
{hasAvatarDecoration && (
|
||||
<MainDecoration
|
||||
className={largeMainDecorationConfig?.className}
|
||||
/>
|
||||
)}
|
||||
<Avatar className='size-14 rounded-full pointer-events-none'>
|
||||
<AvatarImage
|
||||
src={user?.avatar_url}
|
||||
alt={user?.nickname}
|
||||
/>
|
||||
<AvatarFallback className="rounded-full bg-transparent text-lg">
|
||||
{user?.nickname?.charAt(0)?.toUpperCase() || "L"}
|
||||
<AvatarFallback className='rounded-full bg-transparent text-lg'>
|
||||
{user?.nickname?.charAt(0)?.toUpperCase() || 'L'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-base font-semibold truncate mt-2">
|
||||
<span className='text-base font-semibold truncate mt-2'>
|
||||
{userLoading
|
||||
? "加载中..."
|
||||
: user?.nickname || user?.username || "未登录"}
|
||||
? '加载中...'
|
||||
: user?.nickname || user?.username || '未登录'}
|
||||
</span>
|
||||
<span className="text-xs font-base text-muted-foreground">
|
||||
{userLoading ? "正在同步账户" : user?.is_admin ? "系统管理员" : "普通用户"}
|
||||
<span className='text-xs font-base text-muted-foreground'>
|
||||
{userLoading
|
||||
? '正在同步账户'
|
||||
: user?.is_admin
|
||||
? '系统管理员'
|
||||
: '普通用户'}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
router.push("/settings/profile")
|
||||
handleCloseSidebar()
|
||||
}}>
|
||||
<UserRound className="mr-2 size-4" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push('/settings/profile');
|
||||
handleCloseSidebar();
|
||||
}}
|
||||
>
|
||||
<UserRound className='mr-2 size-4' />
|
||||
<span>我的资料</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
router.push("/settings")
|
||||
handleCloseSidebar()
|
||||
}}>
|
||||
<Settings className="mr-2 size-4" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push('/settings');
|
||||
handleCloseSidebar();
|
||||
}}
|
||||
>
|
||||
<Settings className='mr-2 size-4' />
|
||||
<span>设置</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-2" />
|
||||
<DropdownMenuItem onClick={() => {
|
||||
window.open("https://open-flare.pages.dev/", "_blank", "noopener,noreferrer")
|
||||
handleCloseSidebar()
|
||||
}}>
|
||||
<FileQuestionMark className="mr-2 size-4" />
|
||||
<DropdownMenuSeparator className='my-2' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://open-flare.pages.dev/',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
handleCloseSidebar();
|
||||
}}
|
||||
>
|
||||
<FileQuestionMark className='mr-2 size-4' />
|
||||
<span>使用帮助</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive hover:bg-destructive/50" onClick={() => setShowLogoutDialog(true)}>
|
||||
<LogOut className="mr-2 size-4 text-destructive" />
|
||||
<DropdownMenuItem
|
||||
className='text-destructive hover:bg-destructive/50'
|
||||
onClick={() => setShowLogoutDialog(true)}
|
||||
>
|
||||
<LogOut className='mr-2 size-4 text-destructive' />
|
||||
<span>退出登录</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="group-data-[collapsible=icon]">
|
||||
<SidebarContent className='group-data-[collapsible=icon]'>
|
||||
{openflareSidebarNav.length > 0 && (
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupContent className="py-1">
|
||||
<SidebarGroup className='py-0'>
|
||||
<SidebarGroupContent className='py-1'>
|
||||
<OpenFlareSidebarMenu onNavigate={handleCloseSidebar} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{user?.is_admin && adminFiltered.length > 0 && (
|
||||
<SidebarGroup className="py-0 pt-4">
|
||||
<SidebarGroupLabel className="text-xs font-normal text-muted-foreground">
|
||||
<SidebarGroup className='py-0 pt-4'>
|
||||
<SidebarGroupLabel className='text-xs font-normal text-muted-foreground'>
|
||||
管理
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent className="py-1">
|
||||
<SidebarMenu className="gap-1">
|
||||
<SidebarGroupContent className='py-1'>
|
||||
<SidebarMenu className='gap-1'>
|
||||
{adminFiltered.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={pathname === item.url || pathname.startsWith(`${item.url}/`)}
|
||||
isActive={
|
||||
pathname === item.url ||
|
||||
pathname.startsWith(`${item.url}/`)
|
||||
}
|
||||
asChild
|
||||
>
|
||||
<Link href={item.url} onClick={handleCloseSidebar}>
|
||||
@@ -326,12 +380,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
)}
|
||||
|
||||
{documentFiltered.length > 0 && (
|
||||
<SidebarGroup className="py-0 pt-4">
|
||||
<SidebarGroupLabel className="text-xs font-normal text-muted-foreground">
|
||||
<SidebarGroup className='py-0 pt-4'>
|
||||
<SidebarGroupLabel className='text-xs font-normal text-muted-foreground'>
|
||||
文档库
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent className="py-1">
|
||||
<SidebarMenu className="gap-1">
|
||||
<SidebarGroupContent className='py-1'>
|
||||
<SidebarMenu className='gap-1'>
|
||||
{documentFiltered.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
@@ -340,15 +394,20 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
asChild
|
||||
>
|
||||
{item.external ? (
|
||||
<Link href={item.url} target="_blank" rel="noopener noreferrer" onClick={handleCloseSidebar}>
|
||||
<Link
|
||||
href={item.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
onClick={handleCloseSidebar}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<ArrowUpRight className="size-3 text-muted-foreground" />
|
||||
<span className='flex-1'>{item.title}</span>
|
||||
<ArrowUpRight className='size-3 text-muted-foreground' />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={item.url} onClick={handleCloseSidebar}>
|
||||
{item.icon && <item.icon />}
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<span className='flex-1'>{item.title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
@@ -358,33 +417,37 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="mt-auto px-3 py-3 group-data-[collapsible=icon]:hidden">
|
||||
<div className="border-t border-border/60 pt-3 text-[11px] leading-5 text-muted-foreground">
|
||||
<SidebarFooter className='mt-auto px-3 py-3 group-data-[collapsible=icon]:hidden'>
|
||||
<div className='border-t border-border/60 pt-3 text-[11px] leading-5 text-muted-foreground'>
|
||||
<div>Version {APP_VERSION}</div>
|
||||
{APP_BUILD_DATE && <div>Build At {APP_BUILD_DATE}</div>}
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
<AlertDialog open={showLogoutDialog} onOpenChange={(open) => !isLoggingOut && setShowLogoutDialog(open)}>
|
||||
<AlertDialog
|
||||
open={showLogoutDialog}
|
||||
onOpenChange={(open) => !isLoggingOut && setShowLogoutDialog(open)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认登出</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{isLoggingOut ? '正在登出,请稍候...' : '您确定要登出当前账户吗?'}
|
||||
{isLoggingOut
|
||||
? '正在登出,请稍候...'
|
||||
: '您确定要登出当前账户吗?'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoggingOut}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleLogout} disabled={isLoggingOut}>
|
||||
{isLoggingOut && <Spinner className="mr-2" />}
|
||||
{isLoggingOut && <Spinner className='mr-2' />}
|
||||
{isLoggingOut ? '登出中...' : '确认登出'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user