/admin/profile 页面从服务端获取用户数据

This commit is contained in:
2025-11-20 18:01:54 +08:00
parent 9f29e35b53
commit 4dd7b834f3
2 changed files with 205 additions and 210 deletions

View File

@@ -0,0 +1,189 @@
'use client'
import {update} from '@/actions/user'
import {useProfileStore} from '@/components/stores-provider'
import {Button} from '@/components/ui/button'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
import {User} from '@/lib/models'
import {merge} from '@/lib/utils'
import {zodResolver} from '@hookform/resolvers/zod'
import {useEffect, useRef} from 'react'
import {useForm} from 'react-hook-form'
import {toast} from 'sonner'
import z from 'zod'
import * as qrcode from 'qrcode'
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'
import {QrCodeIcon} from 'lucide-react'
export function Aftersale(props: {
profile: User
}) {
const admin = props.profile.admin_id
const canvasRef = useRef(null)
useEffect(() => {
if (admin && canvasRef.current) {
qrcode.toCanvas(canvasRef.current, String(admin), {
width: 180,
margin: 0,
}).catch((err) => {
console.error(err)
})
}
}, [admin])
return (
<Card className="flex-none max-sm:flex-col">
<CardHeader>
<CardTitle>
<QrCodeIcon size={18}/>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<p className="text-weak text-sm">
1.100+IP代理资源免费测试使
</p>
<p className="text-weak text-sm">
2.线
</p>
<p className="text-weak text-sm">
3.1V1专属售后答疑7*24线
</p>
</div>
<div className="flex flex-col gap-4 items-center">
<p></p>
<div>
<canvas ref={canvasRef} width="180" height="180" className="mx-auto bg-muted"/>
</div>
<p className="text-xs text-weak">
<br/>
</p>
</div>
</CardContent>
</Card>
)
}
export function BasicForm(props: {
profile: User
}) {
const schema = z.object({
username: z.string(),
email: z.string().email('请输入正确的邮箱'),
contact_qq: z.string(),
contact_wechat: z.string(),
})
type Schema = z.infer<typeof schema>
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
username: props.profile.username || '',
email: props.profile.email || '',
contact_qq: props.profile.contact_qq || '',
contact_wechat: props.profile.contact_wechat || '',
},
})
const handler = form.handleSubmit(async (value) => {
try {
const resp = await update(value)
if (!resp.success) {
throw new Error(resp.message)
}
await refreshProfile()
toast.success(`保存成功`)
}
catch (e) {
console.error(e)
toast.error(`保存失败`, {
description: e instanceof Error ? e.message : String(e),
})
}
})
const refreshProfile = useProfileStore(store => store.refreshProfile)
return (
<div className="flex flex-col gap-4 flex-1 w-full">
<h3 className="font-normal"></h3>
<Form
form={form}
handler={handler}
className={merge(
`grid grid-cols-2 gap-4 max-md:grid-cols-1 items-start`,
)}
>
<FormField<Schema>
name="username"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入用户名" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="email"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入邮箱" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="contact_qq"
label={<span className="w-full flex justify-end">QQ</span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入QQ号" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="contact_wechat"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入微信号" className="w-48 max-xl:w-full"/>
)}
</FormField>
<div className="flex justify-end gap-4 md:col-span-2 justify-self-stretch">
<Button
theme="outline"
type="button"
onClick={() => form.reset({
username: props.profile.username || '',
email: props.profile.email || '',
contact_qq: props.profile.contact_qq || '',
contact_wechat: props.profile.contact_wechat || '',
})}>
</Button>
<Button></Button>
</div>
</Form>
</div>
)
}

View File

@@ -1,48 +1,27 @@
'use client'
import {useEffect, useRef} from 'react'
import Page from '@/components/page'
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
import {Form, FormField} from '@/components/ui/form'
import {Button} from '@/components/ui/button'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import * as z from 'zod'
import {useProfileStore} from '@/components/stores-provider'
import {toast} from 'sonner'
import {CheckCircle, QrCodeIcon} from 'lucide-react'
import * as qrcode from 'qrcode'
import {CheckCircle} from 'lucide-react'
import Image from 'next/image'
import banner from '@/app/admin/identify/_assets/banner.webp'
import {Input} from '@/components/ui/input'
import {merge} from '@/lib/utils'
import {User} from '@/lib/models'
import {update} from '@/actions/user'
import RechargeModal from '@/components/composites/recharge'
import {useRouter} from 'next/navigation'
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
import {getProfile} from '@/actions/auth'
import {Aftersale, BasicForm} from './clients'
export type ProfilePageProps = {}
export default function ProfilePage(props: ProfilePageProps) {
const router = useRouter()
const profile = useProfileStore(store => store.profile)
// ======================
// render
// ======================
if (!profile) {
export default async function ProfilePage(props: ProfilePageProps) {
const profile = await getProfile()
if (!profile.success) {
return (
<Page>
<div className="flex flex-col gap-4">
<h3 className="text-lg font-bold">...</h3>
<p className="text-sm text-gray-600"></p>
</div>
</Page>
)
}
const user = profile.data
return (
<Page className="lg:flex-row lg:items-stretch md:flex-col max-sm:flex-col">
<div className="flex-3/4 flex flex-col gap-4">
@@ -61,7 +40,7 @@ export default function ProfilePage(props: ProfilePageProps) {
<CardTitle className="font-normal"></CardTitle>
</CardHeader>
<CardContent className="flex-auto flex justify-between items-center px-8">
<p className="text-xl">{profile?.balance}</p>
<p className="text-xl">{user.balance}</p>
<RechargeModal classNames={{
trigger: `h-10 px-6`,
}}/>
@@ -73,12 +52,12 @@ export default function ProfilePage(props: ProfilePageProps) {
<CardTitle className="font-normal"></CardTitle>
</CardHeader>
<CardContent className="flex-auto flex justify-between items-center gap-4 px-4 pr-8">
{!profile?.id_token
{!user.id_token
? (
<>
<p className="text-sm">使</p>
<RealnameAuthDialog
hasAuthenticated={!!profile?.id_token}
hasAuthenticated={!!user.id_token}
triggerClassName="w-24"
/>
</>
@@ -86,8 +65,8 @@ export default function ProfilePage(props: ProfilePageProps) {
: (
<>
<p className="flex flex-col gap-1">
<span>{profile.name}</span>
<span className="text-sm">{profile.id_no}</span>
<span>{user.name}</span>
<span className="text-sm">{user.id_no}</span>
</p>
<p className="flex gap-1 items-center">
<CheckCircle className="text-done" size={18}/>
@@ -105,192 +84,19 @@ export default function ProfilePage(props: ProfilePageProps) {
<div className="flex flex-col gap-4">
<h3 className="font-normal"></h3>
<div className="flex gap-4 items-center">
<p>{profile.phone}</p>
<p>{user.phone}</p>
<ChangePasswordDialog triggerClassName="w-24 h-9"/>
</div>
</div>
{/* 基本信息 */}
<BasicForm profile={profile}/>
<BasicForm profile={user}/>
</div>
</div>
{/* 侧边栏:客服经理信息 */}
<Aftersale profile={profile}/>
<Aftersale profile={user}/>
</Page>
)
}
function Aftersale(props: {
profile: User
}) {
const admin = props.profile.admin_id
const canvasRef = useRef(null)
useEffect(() => {
if (admin && canvasRef.current) {
qrcode.toCanvas(canvasRef.current, String(admin), {
width: 180,
margin: 0,
}).catch((err) => {
console.error(err)
})
}
}, [admin])
return (
<Card className="flex-none max-sm:flex-col">
<CardHeader>
<CardTitle>
<QrCodeIcon size={18}/>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<p className="text-weak text-sm">
1.100+IP代理资源免费测试使
</p>
<p className="text-weak text-sm">
2.线
</p>
<p className="text-weak text-sm">
3.1V1专属售后答疑7*24线
</p>
</div>
<div className="flex flex-col gap-4 items-center">
<p></p>
<div>
<canvas ref={canvasRef} width="180" height="180" className="mx-auto bg-muted"/>
</div>
<p className="text-xs text-weak">
<br/>
</p>
</div>
</CardContent>
</Card>
)
}
function BasicForm(props: {
profile: User
}) {
const schema = z.object({
username: z.string(),
email: z.string().email('请输入正确的邮箱'),
contact_qq: z.string(),
contact_wechat: z.string(),
})
type Schema = z.infer<typeof schema>
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
username: props.profile.username || '',
email: props.profile.email || '',
contact_qq: props.profile.contact_qq || '',
contact_wechat: props.profile.contact_wechat || '',
},
})
const handler = form.handleSubmit(async (value) => {
try {
const resp = await update(value)
if (!resp.success) {
throw new Error(resp.message)
}
await refreshProfile()
toast.success(`保存成功`)
}
catch (e) {
console.error(e)
toast.error(`保存失败`, {
description: e instanceof Error ? e.message : String(e),
})
}
})
const refreshProfile = useProfileStore(store => store.refreshProfile)
return (
<div className="flex flex-col gap-4 flex-1 w-full">
<h3 className="font-normal"></h3>
<Form
form={form}
handler={handler}
className={merge(
`grid grid-cols-2 gap-4 max-md:grid-cols-1 items-start`,
)}
>
<FormField<Schema>
name="username"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入用户名" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="email"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入邮箱" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="contact_qq"
label={<span className="w-full flex justify-end">QQ</span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入QQ号" className="w-48 max-xl:w-full"/>
)}
</FormField>
<FormField<Schema>
name="contact_wechat"
label={<span className="w-full flex justify-end"></span>}
className="grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4"
classNames={{
message: `col-start-2`,
}}
>
{({field}) => (
<Input {...field} placeholder="请输入微信号" className="w-48 max-xl:w-full"/>
)}
</FormField>
<div className="flex justify-end gap-4 md:col-span-2 justify-self-stretch">
<Button
theme="outline"
type="button"
onClick={() => form.reset({
username: props.profile.username || '',
email: props.profile.email || '',
contact_qq: props.profile.contact_qq || '',
contact_wechat: props.profile.contact_wechat || '',
})}>
</Button>
<Button></Button>
</div>
</Form>
</div>
)
}