开启 ppr 优化渲染性能

This commit is contained in:
2025-12-11 14:10:52 +08:00
parent 8fb6ba2f22
commit 5db63273bc
50 changed files with 2635 additions and 10426 deletions

View File

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

View File

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

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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 (

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

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

View File

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

View File

@@ -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>
)
}

View File

@@ -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')},
]}
/>
</>

View File

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