开启 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,16 +1,30 @@
'use client'
import {useEffect, useRef, useState} from 'react'
import {useState} from 'react'
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
import {useForm} from 'react-hook-form'
import {useForm, useFormContext} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import * as z from 'zod'
import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import {sendSMS} from '@/actions/verify'
import {updatePassword} from '@/actions/user'
import SendMsg from '@/components/send-msg'
// 表单验证规则
const schema = z.object({
phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`),
captcha: z.string().nonempty('请输入验证码'),
code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`),
password: z.string().min(6, `密码至少6位`),
confirm_password: z.string(),
}).refine(data => data.password === data.confirm_password, {
message: '两次输入的密码不一致',
path: ['confirm_password'],
})
type Schema = z.infer<typeof schema>
interface ChangePasswordDialogProps {
triggerClassName?: string
@@ -33,20 +47,6 @@ export function ChangePasswordDialog({
const actualOpen = open !== undefined ? open : internalOpen
const actualOnOpenChange = onOpenChange || setInternalOpen
// 表单验证规则
const schema = z.object({
phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`),
captcha: z.string().nonempty('请输入验证码'),
code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`),
password: z.string().min(6, `密码至少6位`),
confirm_password: z.string(),
}).refine(data => data.password === data.confirm_password, {
message: '两次输入的密码不一致',
path: ['confirm_password'],
})
type Schema = z.infer<typeof schema>
// 表单初始化
const form = useForm<Schema>({
resolver: zodResolver(
@@ -92,52 +92,6 @@ export function ChangePasswordDialog({
}
})
// 验证码相关状态
const [captchaUrl, setCaptchaUrl] = useState(`/captcha?t=${new Date().getTime()}`)
const [captchaWait, setCaptchaWait] = useState(0)
const interval = useRef<NodeJS.Timeout>(null)
// 刷新验证码
const refreshCaptcha = () => {
setCaptchaUrl(`/captcha?t=${new Date().getTime()}`)
}
// 发送短信验证码
const sendVerifier = async () => {
const result = await form.trigger(['phone', 'captcha'])
if (!result) return
const {phone, captcha} = form.getValues()
const resp = await sendSMS({phone, captcha})
if (!resp.success) {
toast.error(resp.message)
refreshCaptcha()
return
}
setCaptchaWait(60)
interval.current = setInterval(() => {
setCaptchaWait((wait) => {
if (wait <= 1) {
clearInterval(interval.current!)
return 0
}
return wait - 1
})
}, 1000)
toast.success(`验证码已发送,请注意查收`)
}
// 清理定时器
useEffect(() => {
return () => {
if (interval.current) {
clearInterval(interval.current)
}
}
}, [])
return (
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
<DialogTrigger asChild>
@@ -155,29 +109,15 @@ export function ChangePasswordDialog({
)}
</FormField>
{/* 图形验证码 */}
<FormField<Schema> name="captcha" label="验证码">
{({field}) => (
<div className="flex gap-4">
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
<Button className="p-0 bg-transparent" onClick={refreshCaptcha} type="button">
<img src={captchaUrl} alt="验证码" className="h-10"/>
</Button>
</div>
)}
</FormField>
{/* 短信验证码 */}
<FormField<Schema> name="code" label="短信令牌" className="flex-auto">
{({field}) => (
<div className="flex gap-4">
<div className="flex gap-4 items-end">
<FormField<Schema> name="code" label="验证码" className="flex-auto">
{({field}) => (
<Input {...field} placeholder="请输入验证码" autoComplete="one-time-code"/>
<Button theme="outline" type="button" className="w-36" onClick={() => sendVerifier()}>
{captchaWait > 0 ? `重新发送(${captchaWait})` : `获取短信令牌`}
</Button>
</div>
)}
</FormField>
)}
</FormField>
<SendMsgByPhone/>
</div>
{/* 新密码 */}
<FormField<Schema> name="password" label="新密码" className="flex-auto">
@@ -212,3 +152,9 @@ export function ChangePasswordDialog({
</Dialog>
)
}
function SendMsgByPhone() {
const form = useFormContext<Schema>()
const phone = form.watch('phone')
return <SendMsg phone={phone}/>
}

View File

@@ -1,11 +1,9 @@
'use client'
import {Button} from '@/components/ui/button'
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {useRouter} from 'next/navigation'
import {useState} from 'react'
import Link from 'next/link'
interface RealnameAuthDialogProps {
hasAuthenticated: boolean
triggerClassName?: string
open?: boolean
defaultOpen?: boolean
@@ -14,23 +12,16 @@ interface RealnameAuthDialogProps {
}
export function RealnameAuthDialog({
hasAuthenticated,
triggerClassName,
open,
defaultOpen,
onOpenChange,
onSuccess,
}: RealnameAuthDialogProps) {
const [internalOpen, setInternalOpen] = useState(defaultOpen || false)
const router = useRouter()
const actualOpen = open !== undefined ? open : internalOpen
const actualOnOpenChange = onOpenChange || setInternalOpen
if (hasAuthenticated) {
return null
}
return (
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
<DialogTrigger asChild>

View File

@@ -20,7 +20,7 @@ import {Combobox} from '@/components/ui/combobox'
import cities from './_assets/cities.json'
import ExtractDocs from '@/docs/extract.mdx'
import Link from 'next/link'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
const schema = z.object({
resource: z.number({required_error: '请选择套餐'}),

View File

@@ -14,7 +14,7 @@ export default function Purchase() {
const tab = params.get('type') as TabType || 'short'
const updateTab = async (tab: string) => {
const updateTab = (tab: string) => {
const newParams = new URLSearchParams(params)
newParams.set('type', tab)
router.push(`${path}?${newParams.toString()}`)

View File

@@ -1,5 +1,5 @@
'use client'
import {useContext, useMemo} from 'react'
import {Suspense, use, useContext, useMemo} from 'react'
import {PurchaseFormContext} from '@/components/composites/purchase/short/form'
import {RadioGroup} from '@/components/ui/radio-group'
import {FormField} from '@/components/ui/form'
@@ -8,7 +8,7 @@ import Image from 'next/image'
import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg'
import balance from '../_assets/balance.svg'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import Pay from '@/components/composites/purchase/pay'
import {buttonVariants} from '@/components/ui/button'
@@ -99,73 +99,89 @@ export default function Right() {
{price}
</span>
</p>
{profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
<div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile?.balance}</span>
<RechargeModal/>
</p>
</div>
<FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
<Pay
method={method}
amount={price}
resource={{
type: 2,
long: {
mode: Number(mode),
live: Number(live),
daily_limit: dailyLimit,
expire: Number(expire),
quota: quota,
},
}}/>
</>
) : (
<Link href="/login" className={buttonVariants()}>
</Link>
)}
<Suspense>
<BalanceOrLogin {...{method, price, mode, live, quota, expire, dailyLimit}}/>
</Suspense>
</Card>
)
}
function BalanceOrLogin(props: {
method: 'wechat' | 'alipay' | 'balance'
price: string
mode: string
live: string
quota: number
expire: string
dailyLimit: number
}) {
const profile = use(useProfileStore(store => store.profile))
return profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
<div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile?.balance}</span>
<RechargeModal/>
</p>
</div>
<FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
<Pay
method={props.method}
balance={profile.balance}
amount={props.price}
resource={{
type: 2,
long: {
mode: Number(props.mode),
live: Number(props.live),
daily_limit: props.dailyLimit,
expire: Number(props.expire),
quota: props.quota,
},
}}/>
</>
) : (
<Link href="/login" className={buttonVariants()}>
</Link>
)
}

View File

@@ -4,7 +4,7 @@ import {Button} from '@/components/ui/button'
import balance from './_assets/balance.svg'
import Image from 'next/image'
import {useState} from 'react'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
import {Alert, AlertTitle} from '@/components/ui/alert'
import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
@@ -18,13 +18,16 @@ import {PaymentProps} from '@/components/composites/payment/type'
import {usePlatformType} from '@/lib/hooks'
export type PayProps = {
method: 'alipay' | 'wechat' | 'balance'
amount: string
resource: Parameters<typeof createResource>[0]
}
} & ({
method: 'alipay' | 'wechat'
} | {
method: 'balance'
balance: number
})
export default function Pay(props: PayProps) {
const profile = useProfileStore(store => store.profile)
const refreshProfile = useProfileStore(store => store.refreshProfile)
const [open, setOpen] = useState(false)
const [trade, setTrade] = useState<PaymentProps | null>(null)
@@ -102,7 +105,7 @@ export default function Pay(props: PayProps) {
}
}
const balanceEnough = profile && profile.balance >= Number(props.amount)
const balanceEnough = balance >= Number(props.amount)
return (
<>
@@ -121,49 +124,43 @@ export default function Pay(props: PayProps) {
</DialogTitle>
</DialogHeader>
{profile && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg">
{profile.balance}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg text-accent">
-
{props.amount}
</span>
</div>
<hr className="my-2"/>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className={`text-lg ${balanceEnough ? 'text-done' : 'text-fail'}`}>
{(profile.balance - Number(props.amount)).toFixed(2)}
</span>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg">
{balance}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg text-accent">
- {props.amount}
</span>
</div>
<hr className="my-2"/>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className={`text-lg ${balanceEnough ? 'text-done' : 'text-fail'}`}>
{(balance - Number(props.amount)).toFixed(2)}
</span>
</div>
{balanceEnough ? (
<Alert variant="done">
<AlertTitle>
</AlertTitle>
</Alert>
) : (
<Alert variant="fail">
<AlertTitle>
</AlertTitle>
</Alert>
)}
</div>
)}
{balanceEnough ? (
<Alert variant="done">
<AlertTitle>
</AlertTitle>
</Alert>
) : (
<Alert variant="fail">
<AlertTitle>
</AlertTitle>
</Alert>
)}
</div>
<DialogFooter>
<Button

View File

@@ -1,5 +1,5 @@
'use client'
import {useMemo} from 'react'
import {Suspense, use, useMemo} from 'react'
import {Schema} from '@/components/composites/purchase/short/form'
import {RadioGroup} from '@/components/ui/radio-group'
import {FormField} from '@/components/ui/form'
@@ -8,7 +8,7 @@ import Image from 'next/image'
import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg'
import balance from '../_assets/balance.svg'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import {buttonVariants} from '@/components/ui/button'
import Link from 'next/link'
@@ -18,8 +18,6 @@ import {useFormContext} from 'react-hook-form'
import {Card} from '@/components/ui/card'
export default function Right() {
const profile = useProfileStore(store => store.profile)
const form = useFormContext<Schema>()
const method = form.watch('pay_type')
@@ -93,73 +91,89 @@ export default function Right() {
{price}
</span>
</p>
{profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
<div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile?.balance}</span>
<RechargeModal/>
</p>
</div>
<FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
<Pay
method={method}
amount={price}
resource={{
type: 1,
short: {
mode: Number(mode),
live: Number(live),
quota: quota,
expire: Number(expire),
daily_limit: dailyLimit,
},
}}/>
</>
) : (
<Link href="/login" className={buttonVariants()}>
</Link>
)}
<Suspense>
<BalanceOrLogin {...{method, price, mode, live, quota, expire, dailyLimit}}/>
</Suspense>
</Card>
)
}
function BalanceOrLogin(props: {
method: 'wechat' | 'alipay' | 'balance'
price: string
mode: string
live: string
quota: number
expire: string
dailyLimit: number
}) {
const profile = use(useProfileStore(store => store.profile))
return profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
<div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile.balance}</span>
<RechargeModal/>
</p>
</div>
<FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
<Pay
method={props.method}
balance={profile.balance}
amount={props.price}
resource={{
type: 1,
short: {
mode: Number(props.mode),
live: Number(props.live),
quota: props.quota,
expire: Number(props.expire),
daily_limit: props.dailyLimit,
},
}}/>
</>
) : (
<Link href="/login" className={buttonVariants()}>
</Link>
)
}

View File

@@ -16,7 +16,7 @@ import {zodResolver} from '@hookform/resolvers/zod'
import {toast} from 'sonner'
import {useState} from 'react'
import {RechargeComplete, RechargePrepare} from '@/actions/user'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
import {merge} from '@/lib/utils'
import {
TradePlatform,

View File

@@ -1,5 +1,5 @@
'use client'
import {useProfileStore} from '@/components/stores-provider'
import {useProfileStore} from '@/components/stores/profile'
import {Button} from '@/components/ui/button'
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
import {LogOutIcon, UserIcon} from 'lucide-react'
@@ -8,38 +8,27 @@ import {logout} from '@/actions/auth'
import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'
import {User} from '@/lib/models'
type UserCenterProps = {
export default function UserCenter(props: {
profile: User
}
export default function UserCenter(props: UserCenterProps) {
}) {
const router = useRouter()
// 登录控制
const pathname = usePathname()
const refreshProfile = useProfileStore(store => store.refreshProfile)
const isAdminPage = pathname.startsWith('/admin') // 判断是否在后台页面
const displayedName = !isAdminPage
? props.profile.username || props.profile.phone.substring(0, 3) + '****' + props.profile.phone.substring(7) || '用户'
: '进入控制台'
const doLogout = async () => {
const resp = await logout()
if (resp.success) {
refreshProfile().then()
refreshProfile()
router.replace('/')
}
}
// 展示与跳转
const pathname = usePathname()
const isAdminPage = pathname.startsWith('/admin') // 判断是否在后台页面
const displayName = () => {
const {username, email, phone} = props.profile
switch (true) {
case !!username: return username
case !!phone: return `${phone.substring(0, 3)}****${phone.substring(7)}`
case !!email: return email
default: return null
}
}
const handleAvatarClick = () => {
const toAdminPage = () => {
if (!isAdminPage) {
router.push('/admin')
}
@@ -51,17 +40,13 @@ export default function UserCenter(props: UserCenterProps) {
<Button
theme="ghost"
className="flex gap-2 items-center h-12"
onClick={handleAvatarClick}
onClick={toAdminPage}
>
<Avatar>
<AvatarImage src={props.profile.avatar} alt="avatar"/>
<AvatarFallback className="bg-primary-muted"><UserIcon/></AvatarFallback>
</Avatar>
{isAdminPage ? (
<span>{displayName() || '用户'}</span>
) : (
<span></span>
)}
<span>{displayedName}</span>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-36 p-1" align="end">

View File

@@ -0,0 +1,48 @@
'use client'
import {useProfileStore} from '@/components/stores/profile'
import {useRouter} from 'next/navigation'
import {Suspense, use} from 'react'
export default function FreeTrial(props: {
className: string
}) {
return (
<Suspense fallback={<Pending className={props.className}/>} >
<Resolved className={props.className}/>
</Suspense>
)
}
function Resolved(props: {
className: string
}) {
const router = useRouter()
const profile = use(useProfileStore(store => store.profile))
return (
<button
className={props.className}
onClick={async () => {
router.push(profile ? '/admin/purchase' : '/product')
}}
>
</button>
)
}
function Pending(props: {
className: string
}) {
const router = useRouter()
return (
<button
className={props.className}
onClick={async () => {
router.push('/product')
}}
>
</button>
)
}

View File

@@ -0,0 +1,83 @@
'use client'
import {useEffect, useRef, useState} from 'react'
import Cap from '@cap.js/widget'
import {sendSMS} from '@/actions/verify'
import {toast} from 'sonner'
import {Button} from '@/components/ui/button'
export default function SendMsg(props: {
phone: string
}) {
const [countdown, setCountdown] = useState(0)
const [progress, setProgress] = useState(0)
const cap = useRef(new Cap({apiEndpoint: '/'}))
cap.current.addEventListener('solve', (event) => {
console.log('captcha solve', event)
})
cap.current.addEventListener('error', (event) => {
console.error('captcha error', event)
})
cap.current.addEventListener('reset', (event) => {
console.log('captcha reset', event)
})
cap.current.addEventListener('progress', (event) => {
setProgress(event.detail.progress)
})
// 计时
useEffect(() => {
const interval = setInterval(() => {
if (countdown > 0) {
setCountdown(countdown - 1)
}
}, 1000)
return () => clearInterval(interval)
}, [countdown])
// 发送验证码
const sendCode = async () => {
try {
// 检查手机号
const valid = /^1\d{10}$/.test(props.phone)
if (!valid) {
throw new Error('请输入正确的手机号')
}
// 完成挑战
const result = await cap.current.solve()
if (!result.success || !cap.current.token) {
throw new Error('人机验证失败')
}
// 发送验证码
const resp = await sendSMS({
phone: props.phone,
captcha: cap.current.token,
})
if (!resp.success) {
throw new Error(`验证码发送失败: ${resp.message}`)
}
setCountdown(60)
toast.success('验证码已发送')
}
catch (e) {
toast.error('验证码发送失败', {
description: (e as Error).message,
})
}
}
return (
<Button
type="button"
theme="outline"
className="whitespace-nowrap h-10"
disabled={countdown > 0}
onClick={sendCode}
>
{cap.current.token ? '1' : '0'}
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
</Button>
)
}

View File

@@ -1,64 +0,0 @@
'use client'
import {createContext, ReactNode, useContext, useEffect, useMemo, useRef, useState} from 'react'
import {StoreApi} from 'zustand/vanilla'
import {useStore} from 'zustand/react'
import {createProfileStore, ProfileStore} from '@/lib/stores/profile'
import {createLayoutStore, LayoutStore} from '@/lib/stores/layout'
import {ClientStore, createClientStore} from '@/lib/stores/client'
const StoreContext = createContext<{
profile?: StoreApi<ProfileStore>
layout?: StoreApi<LayoutStore>
client?: StoreApi<ClientStore>
}>({})
export function useProfileStore<T>(selector: (store: ProfileStore) => T) {
const profile = useContext(StoreContext).profile
if (!profile) {
throw new Error('useProfileStore must be used within a StoreProvider')
}
return useStore(profile, selector)
}
export function useLayoutStore<T>(selector: (store: LayoutStore) => T) {
const layout = useContext(StoreContext).layout
if (!layout) {
throw new Error('useLayoutStore must be used within a StoreProvider')
}
return useStore(layout, selector)
}
export function useClientStore<T>(selector: (store: ClientStore) => T) {
const client = useContext(StoreContext).client
if (!client) {
throw new Error('useClientStore must be used within a StoreProvider')
}
return useStore(client, selector)
}
export type ProfileProviderProps = {
children: ReactNode
}
export default function StoresProvider(props: ProfileProviderProps) {
console.log('init stores')
const [profile] = useState(createProfileStore())
const [layout] = useState(createLayoutStore())
const [client] = useState(createClientStore())
const refreshProfile = useStore(profile, store => store.refreshProfile)
useEffect(() => {
refreshProfile()
}, [refreshProfile])
return (
<StoreContext.Provider value={{
profile,
layout,
client,
}}>
{props.children}
</StoreContext.Provider>
)
}

View File

@@ -0,0 +1,65 @@
'use client'
import {createStore, StoreApi} from 'zustand/vanilla'
import {persist} from 'zustand/middleware'
import {createContext, ReactNode, useContext, useState} from 'react'
import {useStore} from 'zustand/react'
// store
export type ClientState = {
breakpoint: {
sm: boolean
md: boolean
lg: boolean
xl: boolean
}
}
export type ClientActions = {
setBreakpoints: (breakpoints: Partial<ClientState['breakpoint']>) => void
}
export type ClientStore = ClientState & ClientActions
export const createClientStore = () => {
return createStore<ClientStore>()(persist(
setState => ({
breakpoint: {
sm: false,
md: false,
lg: false,
xl: false,
},
setBreakpoints: breakpoints => setState(state => ({
breakpoint: {
...state.breakpoint,
...breakpoints,
},
})),
}),
{
name: 'client-store',
partialize: state => ({
device: state.breakpoint,
}),
},
))
}
// provider
const ClientStoreContext = createContext<StoreApi<ClientStore> | null>(null)
export const ClientStoreProvider = (props: {children: ReactNode}) => {
const [store] = useState(() => createClientStore())
return (
<ClientStoreContext.Provider value={store}>
{props.children}
</ClientStoreContext.Provider>
)
}
// consumer
export function useClientStore<T>(selector: (state: ClientStore) => T) {
const store = useContext(ClientStoreContext)
if (!store) {
throw new Error('ClientStoreContext 没有正确初始化')
}
return useStore(store, selector)
}

View File

@@ -0,0 +1,56 @@
'use client'
import {createStore, StoreApi} from 'zustand/vanilla'
import {persist} from 'zustand/middleware'
import {createContext, ReactNode, useContext, useState} from 'react'
import {useStore} from 'zustand/react'
// store
export type LayoutState = {
navbar: boolean
}
export type LayoutActions = {
toggleNavbar: () => void
setNavbar: (navbar: boolean) => void
}
export type LayoutStore = LayoutState & LayoutActions
export const createLayoutStore = () => {
return createStore<LayoutStore>()(persist(
setState => ({
navbar: false,
toggleNavbar: () => setState((state) => {
return {navbar: !state.navbar}
}),
setNavbar: navbar => setState((_) => {
return {navbar}
}),
}),
{
name: 'layout-store',
partialize: state => ({
navbar: state.navbar,
}),
},
))
}
// provider
const LayoutStoreContext = createContext<StoreApi<LayoutStore> | null>(null)
export const LayoutStoreProvider = (props: {children: ReactNode}) => {
const [store] = useState(() => createLayoutStore())
return (
<LayoutStoreContext.Provider value={store}>
{props.children}
</LayoutStoreContext.Provider>
)
}
// consumer
export function useLayoutStore<T>(selector: (state: LayoutStore) => T) {
const context = useContext(LayoutStoreContext)
if (!context) {
throw new Error('LayoutStoreContext 没有正确初始化')
}
return useStore(context, selector)
}

View File

@@ -0,0 +1,48 @@
'use client'
import {User} from '@/lib/models'
import {createStore, StoreApi} from 'zustand/vanilla'
import {getProfile} from '@/actions/auth'
import {createContext, useContext, useState} from 'react'
import {useStore} from 'zustand/react'
// store
export type ProfileState = {
profile: Promise<User | null>
}
export type ProfileActions = {
refreshProfile: () => void
}
export type ProfileStore = ProfileState & ProfileActions
export function createProfileStore(profile: Promise<User | null>) {
return createStore<ProfileStore>()(set => ({
profile,
refreshProfile: () => {
set({profile: getProfile().then(resp => resp.success ? resp.data : null)})
},
}))
}
// provider
export const ProfileStoreContext = createContext<StoreApi<ProfileStore> | null>(null)
export const ProfileStoreProvider = (props: {
profile: Promise<User | null>
children: React.ReactNode
}) => {
const [store] = useState(() => createProfileStore(props.profile))
return (
<ProfileStoreContext.Provider value={store}>
{props.children}
</ProfileStoreContext.Provider>
)
}
// consumer
export function useProfileStore<T>(selector: (state: ProfileStore) => T) {
const context = useContext(ProfileStoreContext)
if (!context) {
throw new Error('ProfileStoreContext 没有正确初始化')
}
return useStore(context, selector)
}