2025-04-25 16:24:04 +08:00
|
|
|
|
'use client'
|
2025-12-11 14:10:52 +08:00
|
|
|
|
import {ReactNode, Suspense, use, useState} from 'react'
|
2025-04-25 16:24:04 +08:00
|
|
|
|
import Image from 'next/image'
|
2025-12-11 14:10:52 +08:00
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
|
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
|
|
|
|
|
|
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
|
|
|
|
|
|
import UserCenter from '@/components/composites/user-center'
|
|
|
|
|
|
import {Button} from '@/components/ui/button'
|
2025-05-06 15:49:02 +08:00
|
|
|
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'
|
2025-12-11 14:10:52 +08:00
|
|
|
|
import {Archive, ArchiveRestore, Eye, HardDriveUpload, IdCard, LockKeyhole, Package, PanelLeftCloseIcon, PanelLeftOpenIcon, ShoppingCart, UserRound, UserRoundPen, Wallet} from 'lucide-react'
|
|
|
|
|
|
import {merge} from '@/lib/utils'
|
|
|
|
|
|
import logoAvatar from '@/assets/logo-avatar.svg'
|
|
|
|
|
|
import logoText from '@/assets/logo-text.svg'
|
|
|
|
|
|
import {useLayoutStore} from '@/components/stores/layout'
|
|
|
|
|
|
import {useProfileStore} from '@/components/stores/profile'
|
|
|
|
|
|
|
|
|
|
|
|
export function Shell(props: {
|
|
|
|
|
|
children: ReactNode
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const navbar = useLayoutStore(store => store.navbar)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
data-expand={navbar}
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`transition-[grid-template-columns] duration-300 ease-in-out`,
|
|
|
|
|
|
`w-full h-full grid`,
|
|
|
|
|
|
`grid-rows-[64px_1fr]`,
|
|
|
|
|
|
`data-[expand=true]:grid-cols-[200px_1fr]`,
|
|
|
|
|
|
`data-[expand=false]:grid-cols-[0px_1fr]`,
|
|
|
|
|
|
`md:data-[expand=false]:grid-cols-[64px_1fr]`,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{props.children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Mask() {
|
|
|
|
|
|
const navbar = useLayoutStore(store => store.navbar)
|
|
|
|
|
|
const setNevBar = useLayoutStore(store => store.setNavbar)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
data-expand={navbar}
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`lg:hidden`,
|
|
|
|
|
|
`transition-opacity duration-300 ease-in-out`,
|
|
|
|
|
|
`col-start-1 row-start-1 col-span-2 row-span-2 bg-black/50 z-10`,
|
|
|
|
|
|
`data-[expand=true]:opacity-100 data-[expand=false]:opacity-0`,
|
|
|
|
|
|
`data-[expand=true]:pointer-events-auto data-[expand=false]:pointer-events-none`,
|
|
|
|
|
|
)}
|
|
|
|
|
|
onClick={() => setNevBar(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Content(props: {children: ReactNode}) {
|
|
|
|
|
|
const navbar = useLayoutStore(store => store.navbar)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
data-expand={navbar}
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`transition-[margin] duration-300 ease-in-out`,
|
|
|
|
|
|
`absolute inset-0 overflow-hidden`,
|
|
|
|
|
|
`mt-16`,
|
|
|
|
|
|
`md:ml-16`,
|
|
|
|
|
|
`lg:data-[expand=true]:ml-[200px]`,
|
|
|
|
|
|
)}>
|
|
|
|
|
|
{props.children}
|
|
|
|
|
|
<Suspense>
|
|
|
|
|
|
<ContentResolved/>
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
function ContentResolved() {
|
|
|
|
|
|
const profile = use(useProfileStore(store => store.profile))
|
2026-02-27 15:03:17 +08:00
|
|
|
|
if (profile)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<RealnameAuthDialog
|
|
|
|
|
|
triggerClassName="hidden"
|
|
|
|
|
|
defaultOpen={!profile.id_token}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ChangePasswordDialog
|
|
|
|
|
|
triggerClassName="hidden"
|
|
|
|
|
|
defaultOpen={!profile.has_password}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
2025-12-11 14:10:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Header() {
|
|
|
|
|
|
const navbar = useLayoutStore(store => store.navbar)
|
|
|
|
|
|
const toggleNavbar = useLayoutStore(store => store.toggleNavbar)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<header className={merge(
|
|
|
|
|
|
`flex-none h-16 overflow-hidden`,
|
|
|
|
|
|
`flex items-stretch`,
|
|
|
|
|
|
)}>
|
|
|
|
|
|
{/* left */}
|
|
|
|
|
|
<div className="flex-auto flex items-center gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
theme="ghost"
|
|
|
|
|
|
className="w-9 h-9 ml-4 md:ml-0"
|
|
|
|
|
|
onClick={toggleNavbar}>
|
|
|
|
|
|
{navbar
|
|
|
|
|
|
? <PanelLeftCloseIcon/>
|
|
|
|
|
|
: <PanelLeftOpenIcon/>
|
|
|
|
|
|
}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<span className="max-md:hidden">
|
|
|
|
|
|
欢迎来到,蓝狐代理
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* right */}
|
2026-03-12 15:46:54 +08:00
|
|
|
|
<div className="flex-none flex items-center justify-end pr-4 max-md:hidden gap-2">
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/"
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`flex-none h-16 flex items-center justify-center`,
|
|
|
|
|
|
)}>
|
|
|
|
|
|
返回首页
|
|
|
|
|
|
</Link>
|
2025-12-11 14:10:52 +08:00
|
|
|
|
<Suspense>
|
|
|
|
|
|
<HeaderUserCenter/>
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function HeaderUserCenter() {
|
|
|
|
|
|
const profile = use(useProfileStore(store => store.profile))
|
2026-02-27 15:03:17 +08:00
|
|
|
|
if (profile) return <UserCenter profile={profile}/>
|
2025-12-11 14:10:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Navbar() {
|
2025-04-25 16:24:04 +08:00
|
|
|
|
const navbar = useLayoutStore(store => store.navbar)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<nav
|
|
|
|
|
|
data-expand={navbar}
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`transition-[flex-basis] duration-300 ease-in-out`,
|
2025-06-09 16:55:44 +08:00
|
|
|
|
`h-full flex flex-col overflow-hidden group`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
`data-[expand=true]:basis-52 data-[expand=false]:basis-16`,
|
2025-04-26 17:20:21 +08:00
|
|
|
|
)}>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
{/* logo */}
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/"
|
|
|
|
|
|
className={merge(
|
2025-11-20 18:32:43 +08:00
|
|
|
|
`flex-none h-16 flex items-center justify-center`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
)}>
|
|
|
|
|
|
<Image src={logoAvatar} alt="logo" className="w-10 h-10 object-contain"/>
|
|
|
|
|
|
<Image
|
|
|
|
|
|
src={logoText}
|
|
|
|
|
|
alt="logo"
|
|
|
|
|
|
className={merge(
|
|
|
|
|
|
`h-10 translate-1 object-cover object-left`,
|
|
|
|
|
|
`transition-[opacity,width] duration-[200ms,300ms] ease-in-out`,
|
|
|
|
|
|
`group-data-[expand=true]:delay-[100ms,0ms]`,
|
|
|
|
|
|
`group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
|
|
|
|
|
|
`group-data-[expand=true]:w-[85px] group-data-[expand=false]:w-0`,
|
|
|
|
|
|
)}/>
|
2025-04-26 17:20:21 +08:00
|
|
|
|
</Link>
|
2025-04-25 16:24:04 +08:00
|
|
|
|
|
|
|
|
|
|
{/* routes */}
|
|
|
|
|
|
<section className={merge(
|
2025-04-26 17:20:21 +08:00
|
|
|
|
`transition-[padding] duration-300 ease-in-out`,
|
2025-06-09 16:55:44 +08:00
|
|
|
|
`flex-auto overflow-x-hidden overflow-y-auto pb-4`,
|
2025-04-26 17:20:21 +08:00
|
|
|
|
`group-data-[expand=true]:px-4 group-data-[expand=false]:px-3`,
|
2025-04-25 16:24:04 +08:00
|
|
|
|
)}>
|
2025-05-06 15:49:02 +08:00
|
|
|
|
<TooltipProvider>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<NavItem href="/admin" icon={<UserRound size={20}/>} label="账户总览" expand={navbar}/>
|
2025-12-18 12:25:52 +08:00
|
|
|
|
<NavTitle label="快速开始"/>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<NavItem href="/admin/identify" icon={<IdCard size={20}/>} label="实名认证" expand={navbar}/>
|
|
|
|
|
|
<NavItem href="/admin/whitelist" icon={<LockKeyhole size={20}/>} label="白名单" expand={navbar}/>
|
|
|
|
|
|
<NavItem href="/admin/purchase" icon={<ShoppingCart size={20}/>} label="购买套餐" expand={navbar}/>
|
|
|
|
|
|
<NavItem href="/admin/extract" icon={<HardDriveUpload size={20}/>} label="提取 IP" expand={navbar}/>
|
2025-12-18 12:25:52 +08:00
|
|
|
|
<NavTitle label="个人中心"/>
|
|
|
|
|
|
<NavItem href="/admin/profile" icon={<UserRoundPen size={20}/>} label="基本信息" expand={navbar}/>
|
|
|
|
|
|
<NavItem href="/admin/bills" icon={<Wallet size={20}/>} label="我的账单" expand={navbar}/>
|
|
|
|
|
|
<NavTitle label="资源管理"/>
|
|
|
|
|
|
<NavItem href="/admin/resources" icon={<Package size={20}/>} label="我的套餐" expand={navbar}/>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<NavItem href="/admin/channels" icon={<Eye size={20}/>} label="IP 管理" expand={navbar}/>
|
2025-12-19 15:15:54 +08:00
|
|
|
|
<NavItem href="/admin/record" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
|
2025-12-18 12:25:52 +08:00
|
|
|
|
{/* <NavTitle label="数据统计"/>
|
|
|
|
|
|
<NavItem href="/admin" icon={<ArchiveRestore size={20}/>} label="使用记录" expand={navbar}/> */}
|
2025-05-06 15:49:02 +08:00
|
|
|
|
</TooltipProvider>
|
2025-04-25 16:24:04 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function NavTitle(props: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<p className={merge(
|
2025-04-26 17:20:21 +08:00
|
|
|
|
`transition-[height] duration-300 ease-in-out`,
|
2025-04-25 16:24:04 +08:00
|
|
|
|
`text-sm text-gray-500 whitespace-nowrap flex items-center relative`,
|
|
|
|
|
|
`group-data-[expand=true]:h-9`,
|
|
|
|
|
|
`group-data-[expand=false]:h-4`,
|
|
|
|
|
|
)}>
|
|
|
|
|
|
<span className={merge(
|
2025-11-20 18:32:43 +08:00
|
|
|
|
`transition-opacity duration-200 ease-in-out absolute mx-4`,
|
2025-04-26 17:20:21 +08:00
|
|
|
|
`group-data-[expand=true]:delay-100 group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
)}>
|
|
|
|
|
|
{props.label}
|
|
|
|
|
|
</span>
|
2025-04-26 14:18:08 +08:00
|
|
|
|
<span className={merge(
|
2025-11-20 18:32:43 +08:00
|
|
|
|
`transition-opacity duration-200 ease-in-out absolute w-full border-b block`,
|
2025-04-26 17:20:21 +08:00
|
|
|
|
`group-data-[expand=false]:delay-100 group-data-[expand=false]:opacity-100 group-data-[expand=true]:opacity-0`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
)}>
|
|
|
|
|
|
</span>
|
2025-04-25 16:24:04 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function NavItem(props: {
|
|
|
|
|
|
href: string
|
|
|
|
|
|
icon?: ReactNode
|
|
|
|
|
|
label: string
|
2025-05-06 15:49:02 +08:00
|
|
|
|
expand?: boolean
|
2025-04-25 16:24:04 +08:00
|
|
|
|
}) {
|
2025-05-06 15:49:02 +08:00
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenChange = (open: boolean) => {
|
|
|
|
|
|
if (!props.expand) {
|
|
|
|
|
|
setOpen(open)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 11:17:38 +08:00
|
|
|
|
const setNavbar = useLayoutStore(store => store.setNavbar)
|
|
|
|
|
|
const closeNavBarIfMobile = () => {
|
|
|
|
|
|
if (window.innerWidth < 768) {
|
|
|
|
|
|
setNavbar(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-25 16:24:04 +08:00
|
|
|
|
return (
|
2025-05-06 15:49:02 +08:00
|
|
|
|
<Tooltip open={open} onOpenChange={handleOpenChange}>
|
|
|
|
|
|
<TooltipTrigger asChild>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<Link
|
|
|
|
|
|
className={merge(
|
2025-11-20 18:32:43 +08:00
|
|
|
|
`transition-[padding,background] duration-300 ease-in-out`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
`flex items-center rounded-md gap-2 whitespace-nowrap`,
|
|
|
|
|
|
`hover:bg-gray-100`,
|
|
|
|
|
|
`group-data-[expand=true]:px-4`,
|
|
|
|
|
|
)}
|
2025-06-09 11:17:38 +08:00
|
|
|
|
href={props.href}
|
|
|
|
|
|
onClick={closeNavBarIfMobile}
|
|
|
|
|
|
>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<span className="flex-none w-10 h-10 flex items-center justify-center">{props.icon}</span>
|
2025-05-06 15:49:02 +08:00
|
|
|
|
<span className={merge(
|
|
|
|
|
|
`flex-auto`,
|
|
|
|
|
|
`transition-[width,opacity] duration-300 ease-in-out`,
|
|
|
|
|
|
`group-data-[expand=true]:w-auto group-data-[expand=true]:opacity-100`,
|
|
|
|
|
|
`group-data-[expand=false]:w-0 group-data-[expand=false]:opacity-0`,
|
2025-06-07 11:49:57 +08:00
|
|
|
|
)}>
|
|
|
|
|
|
{props.label}
|
|
|
|
|
|
</span>
|
2025-05-06 15:49:02 +08:00
|
|
|
|
</Link>
|
|
|
|
|
|
</TooltipTrigger>
|
2025-06-07 11:49:57 +08:00
|
|
|
|
<TooltipContent side="right" sideOffset={16}>
|
2025-05-06 15:49:02 +08:00
|
|
|
|
<p>{props.label}</p>
|
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
|
</Tooltip>
|
2025-04-25 16:24:04 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|