开启 ppr 优化渲染性能
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 31.22 27.99" style="enable-background:new 0 0 31.22 27.99;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="4.0657" y1="0.5307" x2="27.1835" y2="23.1816">
|
||||
<stop offset="0" style="stop-color:#2470F9"/>
|
||||
<stop offset="1" style="stop-color:#15BFFF"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M26.68,14.05c0,0,4.72-7.37,0.03-14.05c0,0-6.79,2.75-8.62,10.61h-4.72c0,0-2.1-8.65-8.78-10.61
|
||||
c0,0-3.84,4.91-0.22,13.99L0,19.26c0,0,8.65,0.79,13.1,7.08c0,0-0.28-4.98-1.33-6.55c0,0-4.01-0.96-3.91-5.24
|
||||
c0,0,6.55,0.35,6.81,13.46h2c0,0-0.9-10.44,6.94-13.52c0,0,1.03,3.21-3.95,5.31c0,0-1.77,3.82-1.58,6.5c0,0,5.16-5.92,13.19-7.26
|
||||
C31.26,19.02,28.54,16.11,26.68,14.05z M7.85,8.41l-0.48,2.58C5.84,9.08,6.42,4.87,6.42,4.87c3.34,1.62,3.34,5.45,3.34,5.45
|
||||
L7.85,8.41z M24.56,10.99l-0.48-2.58l-1.91,1.91c0,0,0-3.82,3.34-5.45C25.52,4.87,26.09,9.08,24.56,10.99z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 60 27.99" style="enable-background:new 0 0 60 27.99;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#2470F9;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M15.62,2.94v0.77h-2.49v0.99h-2.21V3.71H7.19v0.99H4.97V3.71H2.48V2.94h2.49V2.03h2.21v0.91h3.73V2.03h2.21
|
||||
v0.91H15.62z M14.89,14.21h0.73v0.78H2.48v-0.78h0.74v-2.59c0-0.57,0.28-0.85,0.85-0.85h9.96c0.57,0,0.85,0.28,0.85,0.85V14.21z
|
||||
M5.42,9.49H3.21V5.77h2.21V9.49z M5.44,14.21h0.94v-2.66H5.65c-0.14,0-0.21,0.07-0.21,0.21V14.21z M8.35,5.23v4.8H6.14v-4.8H8.35z
|
||||
M9.53,11.54H8.59v2.66h0.94V11.54z M12.06,5.23l-0.29,1.19h3.29V7.2h-0.9l0.76,2.83h-2.21L11.95,7.2h-0.38l-0.71,2.83H8.64
|
||||
l1.22-4.8H12.06z M12.68,11.75c0-0.14-0.07-0.21-0.21-0.21h-0.73v2.66h0.94V11.75z"/>
|
||||
<path class="st0" d="M20,4.95c0.07,0.21,0.1,0.43,0.1,0.69v8.52c0,0.67-0.34,1.01-0.99,1.01h-2.13l-0.42-0.77h1
|
||||
c0.22,0,0.34-0.11,0.34-0.35v-3.96l-1.4,0.8v-0.85l1.4-0.8V5.3l-1.4,0.8V5.24l1.2-0.69l-0.73-2.56h2.2l0.42,1.47L21,2.65V3.5
|
||||
l-1.22,0.7L20,4.95z M29.63,14.39v0.78l-1.61-0.52c-0.46-0.15-0.71-0.48-0.74-0.98l-0.8-10.37h-0.85l-0.41,10.63
|
||||
c0,0.18,0.08,0.22,0.24,0.15l0.46-0.18l-0.34-5.14h1.04l0.42,6.4H26l-0.04-0.55l-1.37,0.45c-0.28,0.1-0.52,0.07-0.73-0.07
|
||||
c-0.21-0.14-0.32-0.36-0.31-0.66l0.46-11.04h-0.5c-0.14,0-0.21,0.07-0.21,0.21v9.51c0,0.35-0.14,0.62-0.42,0.8l-2.12,1.35v-0.95
|
||||
l0.38-0.25c0.1-0.07,0.14-0.15,0.14-0.27V3.38c0-0.57,0.28-0.85,0.85-0.85h4.5c0.15,0,0.27-0.07,0.34-0.21l0.17-0.32h2.21L29,2.73
|
||||
c-0.13,0.29-0.35,0.48-0.66,0.55l0.83,10.7c0.01,0.17,0.1,0.28,0.22,0.32L29.63,14.39z"/>
|
||||
<path class="st0" d="M32.76,6.67h1.81v8.49h-2.21v-8.4l-1.88,0.42V6.39l1.11-0.24c0.24-0.04,0.41-0.18,0.52-0.42l1.67-3.7h2.24
|
||||
l-1.6,3.66c-0.18,0.46-0.53,0.74-1.02,0.84L32.76,6.67z M43.62,14.33v0.83l-3.28-0.56c-0.74-0.11-1.15-0.55-1.25-1.29L38.1,6.18
|
||||
h-2.61V5.4h2.49l-0.48-3.36h2.21l0.49,3.36h1.01c0.14,0,0.21-0.07,0.21-0.21V2.43h2.07v2.9c0,0.57-0.28,0.85-0.85,0.85h-2.33
|
||||
l1.04,7.24c0.04,0.36,0.25,0.57,0.6,0.62L43.62,14.33z"/>
|
||||
<path class="st0" d="M44.48,2.3h4.82v0.78H48v4.54h1.16v0.78H48v5.04l1.3-0.25v0.85l-4.82,0.92v-0.85l1.3-0.25V8.41h-1.16V7.62
|
||||
h1.16V3.08h-1.3V2.3z M57.62,14.23v0.8h-8.04v-0.8h2.91v-2.21h-2.66v-0.8h2.66V9.08h-1.81c-0.57,0-0.85-0.28-0.85-0.85V3.01
|
||||
c0-0.57,0.28-0.85,0.85-0.85h5.83c0.57,0,0.85,0.28,0.85,0.85v5.21c0,0.57-0.28,0.85-0.85,0.85h-1.81v2.14h2.66v0.8h-2.66v2.21
|
||||
H57.62z M52.16,2.94c-0.14,0-0.21,0.07-0.21,0.21v2.06h0.73V2.94H52.16z M51.95,8.08c0,0.14,0.07,0.21,0.21,0.21h0.52V6.01h-0.73
|
||||
V8.08z M55.25,3.15c0-0.14-0.07-0.21-0.21-0.21h-0.52v2.27h0.73V3.15z M55.04,8.29c0.14,0,0.21-0.07,0.21-0.21V6.01h-0.73v2.28
|
||||
H55.04z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M0.76,20.25c0.41,1.49,0.65,2.35,0.7,2.63h0.01c0.05-0.26,0.31-1.12,0.76-2.63h0.41
|
||||
c0.52,1.79,0.68,2.31,0.73,2.56h0.01c0.08-0.33,0.21-0.78,0.73-2.56h0.42l-0.94,3.02H3.14c-0.31-1.08-0.66-2.26-0.72-2.56H2.42
|
||||
c-0.06,0.31-0.37,1.32-0.74,2.56H1.22l-0.88-3.02H0.76z"/>
|
||||
<path class="st0" d="M6.25,20.25c0.41,1.49,0.65,2.35,0.7,2.63h0.01c0.05-0.26,0.31-1.12,0.76-2.63h0.41
|
||||
c0.52,1.79,0.68,2.31,0.73,2.56h0.01c0.08-0.33,0.21-0.78,0.73-2.56h0.42l-0.94,3.02H8.63c-0.31-1.08-0.66-2.26-0.72-2.56H7.91
|
||||
c-0.06,0.31-0.37,1.32-0.74,2.56H6.71l-0.88-3.02H6.25z"/>
|
||||
<path class="st0" d="M11.74,20.25c0.41,1.49,0.65,2.35,0.7,2.63h0.01c0.05-0.26,0.31-1.12,0.76-2.63h0.41
|
||||
c0.52,1.79,0.68,2.31,0.73,2.56h0.01c0.08-0.33,0.21-0.78,0.73-2.56h0.42l-0.94,3.02h-0.44c-0.31-1.08-0.66-2.26-0.72-2.56H13.4
|
||||
c-0.06,0.31-0.37,1.32-0.74,2.56H12.2l-0.88-3.02H11.74z"/>
|
||||
<path class="st0" d="M16.7,23.27v-0.82h0.41v0.82H16.7z"/>
|
||||
<path class="st0" d="M18.93,23.27v-4.41h0.4v4.41H18.93z"/>
|
||||
<path class="st0" d="M23.41,22.66c0,0.27,0.02,0.52,0.04,0.61h-0.39c-0.02-0.07-0.04-0.21-0.05-0.43c-0.1,0.21-0.36,0.5-0.96,0.5
|
||||
c-0.71,0-1.01-0.46-1.01-0.91c0-0.67,0.52-0.97,1.42-0.97c0.23,0,0.43,0,0.55,0v-0.29c0-0.29-0.09-0.65-0.72-0.65
|
||||
c-0.56,0-0.65,0.29-0.71,0.53h-0.39c0.04-0.37,0.27-0.87,1.12-0.87c0.71,0,1.11,0.3,1.11,0.97V22.66z M23.01,21.76
|
||||
c-0.11,0-0.36,0-0.54,0c-0.64,0-1.01,0.17-1.01,0.65c0,0.35,0.25,0.59,0.65,0.59c0.8,0,0.91-0.54,0.91-1.14V21.76z"/>
|
||||
<path class="st0" d="M25.38,21.01c0-0.26,0-0.52-0.01-0.76h0.39c0.01,0.09,0.02,0.45,0.02,0.54c0.13-0.29,0.37-0.61,0.98-0.61
|
||||
c0.55,0,1.01,0.32,1.01,1.14v1.95h-0.4v-1.89c0-0.5-0.2-0.83-0.7-0.83c-0.65,0-0.89,0.52-0.89,1.16v1.57h-0.4V21.01z"/>
|
||||
<path class="st0" d="M30.13,18.86v1.87c0.16-0.29,0.45-0.55,0.97-0.55c0.45,0,1.02,0.24,1.02,1.17v1.92h-0.4v-1.85
|
||||
c0-0.56-0.25-0.87-0.72-0.87c-0.57,0-0.87,0.36-0.87,1.05v1.67h-0.4v-4.41H30.13z"/>
|
||||
<path class="st0" d="M36.38,22.39c0,0.29,0,0.74,0.01,0.88H36c-0.01-0.08-0.02-0.27-0.02-0.49c-0.14,0.35-0.44,0.56-0.95,0.56
|
||||
c-0.47,0-0.99-0.21-0.99-1.1v-1.99h0.4v1.9c0,0.42,0.13,0.82,0.69,0.82c0.62,0,0.86-0.35,0.86-1.16v-1.56h0.4V22.39z"/>
|
||||
<path class="st0" d="M38.35,18.86h0.4v0.62h-0.4V18.86z M38.35,20.25h0.4v3.02h-0.4V20.25z"/>
|
||||
<path class="st0" d="M41.12,22.85v1.61h-0.4v-3.48c0-0.25,0-0.51-0.01-0.74h0.39c0.01,0.11,0.01,0.29,0.01,0.51
|
||||
c0.18-0.34,0.5-0.58,1.03-0.58c0.71,0,1.21,0.59,1.21,1.5c0,1.08-0.58,1.66-1.31,1.66C41.53,23.34,41.26,23.12,41.12,22.85z
|
||||
M42.93,21.7c0-0.67-0.32-1.16-0.88-1.16c-0.68,0-0.96,0.43-0.96,1.21c0,0.76,0.22,1.23,0.92,1.23
|
||||
C42.61,22.98,42.93,22.49,42.93,21.7z"/>
|
||||
<path class="st0" d="M44.93,23.27v-0.82h0.41v0.82H44.93z"/>
|
||||
<path class="st0" d="M49.44,22.36c-0.14,0.53-0.49,0.98-1.21,0.98c-0.79,0-1.32-0.58-1.32-1.57c0-0.84,0.46-1.59,1.35-1.59
|
||||
c0.81,0,1.12,0.58,1.17,0.99h-0.4c-0.08-0.33-0.3-0.63-0.77-0.63c-0.59,0-0.93,0.5-0.93,1.23c0,0.7,0.32,1.23,0.9,1.23
|
||||
c0.41,0,0.66-0.21,0.8-0.62H49.44z"/>
|
||||
<path class="st0" d="M53.68,21.75c0,0.85-0.48,1.59-1.39,1.59c-0.84,0-1.35-0.67-1.35-1.58c0-0.88,0.49-1.58,1.38-1.58
|
||||
C53.14,20.18,53.68,20.81,53.68,21.75z M51.35,21.76c0,0.69,0.36,1.23,0.96,1.23c0.61,0,0.95-0.5,0.95-1.23
|
||||
c0-0.7-0.34-1.23-0.96-1.23C51.67,20.53,51.35,21.05,51.35,21.76z"/>
|
||||
<path class="st0" d="M55.41,21.03c0-0.26,0-0.54-0.01-0.78h0.39c0.01,0.1,0.02,0.34,0.02,0.5c0.13-0.29,0.4-0.58,0.89-0.58
|
||||
c0.42,0,0.74,0.21,0.86,0.57c0.16-0.3,0.45-0.57,0.97-0.57c0.48,0,0.95,0.28,0.95,1.11v1.98h-0.4v-1.94c0-0.38-0.14-0.79-0.66-0.79
|
||||
c-0.54,0-0.78,0.43-0.78,0.99v1.74h-0.39v-1.92c0-0.42-0.12-0.8-0.64-0.8c-0.54,0-0.8,0.46-0.8,1.06v1.66h-0.4V21.03z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
import UserCenter from '@/components/composites/user-center'
|
||||
import {User} from '@/lib/models'
|
||||
|
||||
export type HeaderProps = {
|
||||
profile: User
|
||||
}
|
||||
|
||||
export default function Header(props: HeaderProps) {
|
||||
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 */}
|
||||
<div className="flex-none flex items-center justify-end pr-4">
|
||||
<UserCenter profile={props.profile}/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
import {ReactNode} from 'react'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
type AdminLayoutProps = {
|
||||
navbar: ReactNode
|
||||
header: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Layout(props: AdminLayoutProps) {
|
||||
const navbar = useLayoutStore(store => store.navbar)
|
||||
const setNevBar = useLayoutStore(store => store.setNavbar)
|
||||
|
||||
return (
|
||||
<div className="relative h-dvh overflow-hidden">
|
||||
|
||||
{/* 结构 */}
|
||||
<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]`,
|
||||
)}
|
||||
>
|
||||
<div className="col-start-1 row-start-1 row-span-2 bg-card overflow-hidden relative z-20">
|
||||
{props.navbar}
|
||||
</div>
|
||||
|
||||
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
|
||||
{props.header}
|
||||
</div>
|
||||
|
||||
<svg className="col-start-2 row-start-2 w-full h-full z-20 pointer-events-none" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<mask id="top-left-rounded-mask">
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<circle cx="16" cy="16" r="16" fill="black"/>
|
||||
<rect x="16" y="0" width="100%" height="32" fill="black"/>
|
||||
<rect x="0" y="16" width="32" height="100%" fill="black"/>
|
||||
<rect x="16" y="16" width="100%" height="100%" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" className="fill-card" mask="url(#top-left-rounded-mask)"/>
|
||||
</svg>
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,138 @@
|
||||
'use client'
|
||||
import {ComponentProps, ReactNode, useState} from 'react'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import Link from 'next/link'
|
||||
import {ReactNode, Suspense, use, useState} from 'react'
|
||||
import Image from 'next/image'
|
||||
import logoAvatar from '../_assets/logo-avatar.svg'
|
||||
import logoText from '../_assets/logo-text.svg'
|
||||
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'
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'
|
||||
import {UserRound} from 'lucide-react'
|
||||
import {UserRoundPen} from 'lucide-react'
|
||||
import {IdCard} from 'lucide-react'
|
||||
import {LockKeyhole} from 'lucide-react'
|
||||
import {Wallet} from 'lucide-react'
|
||||
import {ShoppingCart} from 'lucide-react'
|
||||
import {Package} from 'lucide-react'
|
||||
import {HardDriveUpload} from 'lucide-react'
|
||||
import {Eye} from 'lucide-react'
|
||||
import {Archive} from 'lucide-react'
|
||||
import {ArchiveRestore} from 'lucide-react'
|
||||
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'
|
||||
import {User} from '@/lib/models'
|
||||
|
||||
export type NavbarProps = {} & ComponentProps<'nav'>
|
||||
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 default function Navbar(props: NavbarProps) {
|
||||
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))
|
||||
if (!profile) throw new Error('登录状态异常')
|
||||
return (
|
||||
<>
|
||||
<RealnameAuthDialog
|
||||
triggerClassName="hidden"
|
||||
defaultOpen={!profile.id_token}
|
||||
/>
|
||||
<ChangePasswordDialog
|
||||
triggerClassName="hidden"
|
||||
defaultOpen={!profile.has_password}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 */}
|
||||
<div className="flex-none flex items-center justify-end pr-4">
|
||||
<Suspense>
|
||||
<HeaderUserCenter/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function HeaderUserCenter() {
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
if (!profile) throw new Error('登录状态异常')
|
||||
return <UserCenter profile={profile}/>
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const navbar = useLayoutStore(store => store.navbar)
|
||||
|
||||
return (
|
||||
@@ -9,9 +9,9 @@ import zod from 'zod'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {Identify} from '@/actions/user'
|
||||
import {toast} from 'sonner'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {ReactNode, Suspense, use, useEffect, useRef, useState} from 'react'
|
||||
import * as qrcode from 'qrcode'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {useProfileStore} from '@/components/stores/profile'
|
||||
import {merge} from '@/lib/utils'
|
||||
import banner from './_assets/banner.webp'
|
||||
import personal from './_assets/personal.webp'
|
||||
@@ -123,63 +123,60 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
平台不会收集您的个人信息,您的信息仅用于账户安全认证
|
||||
</p>
|
||||
</div>
|
||||
{profile?.id_token ? (
|
||||
<p className="flex gap-2 items-center">
|
||||
<CheckCircleIcon className="text-done"/>
|
||||
<span>已完成实名认证</span>
|
||||
</p>
|
||||
) : (
|
||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full">去认证</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{step === 'form' ? `实名认证` : `扫码完成认证`}
|
||||
</DialogTitle>
|
||||
{step === 'form' && (
|
||||
<Form form={form} handler={handler} className="flex flex-col gap-4">
|
||||
<FormField<Schema> name="name" label="姓名">
|
||||
{({id, field}) => (
|
||||
<input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入姓名"
|
||||
className="border rounded p-2 w-full"
|
||||
autoComplete="name"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField<Schema> name="iden_no" label="身份证号">
|
||||
{({id, field}) => (
|
||||
<input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入身份证号"
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<DialogFooter>
|
||||
<Button type="submit" className="w-full mt-4">提交</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
)}
|
||||
{step === 'scan' && (
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<canvas ref={canvas} width={256} height={256}/>
|
||||
<p className="text-sm text-gray-600">请扫码完成认证</p>
|
||||
<Button onClick={async () => {
|
||||
await refreshProfile()
|
||||
setOpenDialog(false)
|
||||
}}>
|
||||
已完成认证
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<Suspense>
|
||||
<IfNotIdentofy>
|
||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full">去认证</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{step === 'form' ? `实名认证` : `扫码完成认证`}
|
||||
</DialogTitle>
|
||||
{step === 'form' && (
|
||||
<Form form={form} handler={handler} className="flex flex-col gap-4">
|
||||
<FormField<Schema> name="name" label="姓名">
|
||||
{({id, field}) => (
|
||||
<input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入姓名"
|
||||
className="border rounded p-2 w-full"
|
||||
autoComplete="name"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField<Schema> name="iden_no" label="身份证号">
|
||||
{({id, field}) => (
|
||||
<input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入身份证号"
|
||||
className="border rounded p-2 w-full"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<DialogFooter>
|
||||
<Button type="submit" className="w-full mt-4">提交</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
)}
|
||||
{step === 'scan' && (
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<canvas ref={canvas} width={256} height={256}/>
|
||||
<p className="text-sm text-gray-600">请扫码完成认证</p>
|
||||
<Button onClick={async () => {
|
||||
await refreshProfile()
|
||||
setOpenDialog(false)
|
||||
}}>
|
||||
已完成认证
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</IfNotIdentofy>
|
||||
</Suspense>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,3 +232,15 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
function IfNotIdentofy(props: {children: ReactNode}) {
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
return !profile?.id_token
|
||||
? props.children
|
||||
: (
|
||||
<p className="flex gap-2 items-center">
|
||||
<CheckCircleIcon className="text-done"/>
|
||||
<span>已完成实名认证</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
import {ReactNode} from 'react'
|
||||
import Header from './_client/header'
|
||||
import Navbar from './_client/navbar'
|
||||
import Layout from './_client/layout'
|
||||
import {getProfile} from '@/actions/auth'
|
||||
import {redirect} from 'next/navigation'
|
||||
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
|
||||
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
|
||||
import {Shell, Content, Header, Navbar, Mask} from './clients'
|
||||
|
||||
export type AdminLayoutProps = {
|
||||
export default function Template(props: {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default async function AdminLayout(props: AdminLayoutProps) {
|
||||
const resp = await getProfile()
|
||||
const profile = resp.success ? resp.data : null
|
||||
|
||||
if (!profile) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
}) {
|
||||
return (
|
||||
<Layout
|
||||
navbar={<Navbar/>}
|
||||
header={<Header profile={profile}/>}
|
||||
>
|
||||
{props.children}
|
||||
<RealnameAuthDialog
|
||||
hasAuthenticated={!!profile.id_token}
|
||||
triggerClassName="hidden"
|
||||
defaultOpen={!profile.id_token}
|
||||
/>
|
||||
<ChangePasswordDialog
|
||||
triggerClassName="hidden"
|
||||
defaultOpen={!profile.has_password}
|
||||
/>
|
||||
</Layout>
|
||||
<div className="relative h-dvh overflow-hidden">
|
||||
{/* 外壳 */}
|
||||
<Shell>
|
||||
<div className="col-start-1 row-start-1 row-span-2 bg-card overflow-hidden relative z-20">
|
||||
<Navbar/>
|
||||
</div>
|
||||
|
||||
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
|
||||
<Header/>
|
||||
</div>
|
||||
|
||||
<svg className="col-start-2 row-start-2 w-full h-full z-20 pointer-events-none" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<mask id="top-left-rounded-mask">
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<circle cx="16" cy="16" r="16" fill="black"/>
|
||||
<rect x="16" y="0" width="100%" height="32" fill="black"/>
|
||||
<rect x="0" y="16" width="32" height="100%" fill="black"/>
|
||||
<rect x="16" y="16" width="100%" height="100%" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" className="fill-card" mask="url(#top-left-rounded-mask)"/>
|
||||
</svg>
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<Mask/>
|
||||
</Shell>
|
||||
|
||||
{/* 内容 */}
|
||||
<Content>
|
||||
{props.children}
|
||||
</Content>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import {update} from '@/actions/user'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {useProfileStore} from '@/components/stores/profile'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {Input} from '@/components/ui/input'
|
||||
|
||||
@@ -57,7 +57,7 @@ export default async function ProfilePage(props: ProfilePageProps) {
|
||||
<>
|
||||
<p className="text-sm">为了保障您的账户安全和正常使用服务,请您尽快完成实名认证</p>
|
||||
<RealnameAuthDialog
|
||||
hasAuthenticated={!!user.id_token}
|
||||
defaultOpen={!user.id_token}
|
||||
triggerClassName="w-24"
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Purchase, {TabType} from '@/components/composites/purchase'
|
||||
import Page from '@/components/page'
|
||||
import {Suspense} from 'react'
|
||||
|
||||
export type PurchasePageProps = {
|
||||
searchParams?: Promise<{
|
||||
@@ -10,7 +11,9 @@ export type PurchasePageProps = {
|
||||
export default async function PurchasePage(props: PurchasePageProps) {
|
||||
return (
|
||||
<Page className="flex-col">
|
||||
<Purchase/>
|
||||
<Suspense>
|
||||
<Purchase/>
|
||||
</Suspense>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {ReactNode, useEffect, useState} from 'react'
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||
@@ -8,24 +8,27 @@ import DatePicker from '@/components/date-picker'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Box, Eraser, Search, Timer} from 'lucide-react'
|
||||
import DataTable from '@/components/data-table'
|
||||
import {format, intlFormatDistance, isAfter} from 'date-fns'
|
||||
import {format, isAfter, isSameDay} from 'date-fns'
|
||||
import {useStatus} from '@/lib/states'
|
||||
import {ExtraResp, PageRecord} from '@/lib/api'
|
||||
import {Resource} from '@/lib/models'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
import {listResourceShort} from '@/actions/resource'
|
||||
import zod from 'zod'
|
||||
import {useSearchParams} from 'next/navigation'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
|
||||
export type ShortResourceProps = {
|
||||
}
|
||||
const filterSchema = zod.object({
|
||||
resource_no: zod.string().optional().default(''),
|
||||
type: zod.enum(['expire', 'quota', 'all']).default('all'),
|
||||
create_after: zod.date().optional(),
|
||||
create_before: zod.date().optional(),
|
||||
expire_after: zod.date().optional(),
|
||||
expire_before: zod.date().optional(),
|
||||
})
|
||||
|
||||
export default function ShortResource(props: ShortResourceProps) {
|
||||
// ======================
|
||||
// 查询
|
||||
// ======================
|
||||
type FilterSchema = zod.infer<typeof filterSchema>
|
||||
|
||||
export default function ShortResource() {
|
||||
const [status, setStatus] = useStatus()
|
||||
const [data, setData] = useState<ExtraResp<typeof listResourceShort>>({
|
||||
page: 1,
|
||||
@@ -34,7 +37,30 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
list: [],
|
||||
})
|
||||
|
||||
const refresh = async (page: number, size: number) => {
|
||||
const params = useSearchParams()
|
||||
let paramType = params.get('type')
|
||||
if (paramType !== 'all' && paramType !== 'expire' && paramType !== 'quota') {
|
||||
paramType = 'all'
|
||||
}
|
||||
|
||||
// 筛选表单
|
||||
const form = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
resource_no: params.get('resource_no') || '',
|
||||
type: paramType as 'expire' | 'quota' | 'all',
|
||||
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
|
||||
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
|
||||
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
|
||||
expire_before: params.get('expire_before') ? new Date(params.get('expire_before')!) : undefined,
|
||||
},
|
||||
})
|
||||
const handler = form.handleSubmit(async (value: FilterSchema) => {
|
||||
await refresh(1, data.size)
|
||||
})
|
||||
|
||||
// 查询
|
||||
const refresh = useCallback(async (page: number, size: number) => {
|
||||
setStatus('load')
|
||||
try {
|
||||
const type = {
|
||||
@@ -63,54 +89,19 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
setStatus('done')
|
||||
}
|
||||
else {
|
||||
throw new Error('Failed to load short resource')
|
||||
throw new Error(`Failed to load short resource`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
setStatus('fail')
|
||||
}
|
||||
}
|
||||
}, [form, setStatus])
|
||||
|
||||
useEffect(() => {
|
||||
refresh(1, 10).then()
|
||||
}, [])
|
||||
}, [refresh])
|
||||
|
||||
// ======================
|
||||
// 筛选
|
||||
// ======================
|
||||
|
||||
const filterSchema = zod.object({
|
||||
resource_no: zod.string().optional().default(''),
|
||||
type: zod.enum(['expire', 'quota', 'all']).default('all'),
|
||||
create_after: zod.date().optional(),
|
||||
create_before: zod.date().optional(),
|
||||
expire_after: zod.date().optional(),
|
||||
expire_before: zod.date().optional(),
|
||||
})
|
||||
|
||||
type FilterSchema = zod.infer<typeof filterSchema>
|
||||
|
||||
const params = useSearchParams()
|
||||
let paramType = params.get('type')
|
||||
if (paramType != 'all' && paramType != 'expire' && paramType != 'quota') {
|
||||
paramType = 'all'
|
||||
}
|
||||
|
||||
const form = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
resource_no: params.get('resource_no') || '',
|
||||
type: paramType as 'expire' | 'quota' | 'all',
|
||||
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
|
||||
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
|
||||
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
|
||||
expire_before: params.get('expire_before') ? new Date(params.get('expire_before')!) : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const handler = form.handleSubmit(async (value: FilterSchema) => {
|
||||
await refresh(1, data.size)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -166,7 +157,7 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm">最后使用时间</Label>
|
||||
<Label className="text-sm">到期时间</Label>
|
||||
<div className="flex items-center">
|
||||
<FormField name="expire_after">
|
||||
{({field}) => (
|
||||
@@ -253,11 +244,7 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: 'live', header: `IP 时效`, cell: ({row}) => (
|
||||
<span>
|
||||
{row.original.short.live / 60}
|
||||
{' '}
|
||||
分钟
|
||||
</span>
|
||||
<span>{row.original.short.live / 60}分钟</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -270,15 +257,12 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
: <span className="text-red-500">过期</span>}
|
||||
<span>|</span>
|
||||
<span>
|
||||
今日限额: {row.original.short.last_at
|
||||
&& new Date(row.original.short.last_at).toDateString() === new Date().toDateString()
|
||||
{row.original.short.last_at && isSameDay(row.original.short.expire_at, new Date())
|
||||
? row.original.short.daily
|
||||
: 0}/{row.original.short.quota}
|
||||
: 0
|
||||
}/{row.original.short.quota}
|
||||
</span>
|
||||
<span>|</span>
|
||||
<span>
|
||||
{intlFormatDistance(row.original.short.expire_at, new Date())}到期
|
||||
</span>
|
||||
</div>
|
||||
) : row.original.short.type === 2 ? (
|
||||
<div className="flex gap-1">
|
||||
@@ -301,28 +285,12 @@ export default function ShortResource(props: ShortResourceProps) {
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_at',
|
||||
header: '最近使用时间',
|
||||
cell: ({row}) => {
|
||||
const lastAt = row.original.short.last_at
|
||||
if (!lastAt) {
|
||||
return '暂未使用'
|
||||
}
|
||||
return format(lastAt, 'yyyy-MM-dd HH:mm')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at', header: '开通时间', cell: ({row}) => (
|
||||
format(row.getValue('created_at'), 'yyyy-MM-dd HH:mm')
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'action', header: `操作`, cell: item => (
|
||||
<div className="flex gap-2">
|
||||
-
|
||||
</div>
|
||||
),
|
||||
header: '最近使用时间', cell: ({row}) => row.original.short.last_at
|
||||
? format(row.original.short.last_at, 'yyyy-MM-dd HH:mm')
|
||||
: '-',
|
||||
},
|
||||
{header: '开通时间', cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm')},
|
||||
{header: '到期时间', cell: ({row}) => format(row.original.short.expire_at, 'yyyy-MM-dd HH:mm')},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@ import Page from '@/components/page'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||||
import ShortResource from '@/app/admin/resources/_client/short'
|
||||
import LongResource from '@/app/admin/resources/_client/long'
|
||||
import {Suspense} from 'react'
|
||||
|
||||
export default async function ResourcesPage() {
|
||||
// ======================
|
||||
@@ -16,10 +17,14 @@ export default async function ResourcesPage() {
|
||||
<TabsTrigger value="long" className="w-30 h-9 data-[state=active]:bg-primary-muted text-base rounded-md">长效套餐</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="short" className="flex flex-col gap-4">
|
||||
<ShortResource/>
|
||||
<Suspense>
|
||||
<ShortResource/>
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
<TabsContent value="long" className="flex flex-col gap-4">
|
||||
<LongResource/>
|
||||
<Suspense>
|
||||
<LongResource/>
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Page>
|
||||
|
||||
Reference in New Issue
Block a user