Files
web/src/app/admin/identify/page.tsx

254 lines
9.5 KiB
TypeScript
Raw Normal View History

'use client'
import {Button} from '@/components/ui/button'
import Image from 'next/image'
import Page from '@/components/page'
import {Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Form, FormField} from '@/components/ui/form'
import {useForm} from 'react-hook-form'
import zod from 'zod'
import {zodResolver} from '@hookform/resolvers/zod'
import {Identify} from '@/actions/user'
import {toast} from 'sonner'
2025-12-11 14:10:52 +08:00
import {ReactNode, Suspense, use, useEffect, useRef, useState} from 'react'
import * as qrcode from 'qrcode'
2025-12-11 14:10:52 +08:00
import {useProfileStore} from '@/components/stores/profile'
import {merge} from '@/lib/utils'
import banner from './_assets/banner.webp'
import personal from './_assets/personal.webp'
import step1 from './_assets/step1.webp'
import step2 from './_assets/step2.webp'
import step3 from './_assets/step3.webp'
2025-11-21 14:16:39 +08:00
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
import {CheckCircleIcon, WorkflowIcon} from 'lucide-react'
const schema = zod.object({
type: zod.enum([`personal`, `enterprise`], {errorMap: () => ({message: `请选择认证类型`})}).default('personal'),
name: zod.string().min(2, {message: `姓名至少2个字符`}),
iden_no: zod.string().length(18, {message: `身份证号码必须为18位`}),
})
type Schema = zod.infer<typeof schema>
export type IdentifyPageProps = {}
export default function IdentifyPage(props: IdentifyPageProps) {
// ======================
// 填写信息
// ======================
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
type: `personal`,
name: ``,
iden_no: ``,
},
})
const handler = form.handleSubmit(
async (value) => {
const resp = await Identify({
type: value.type === `personal` ? 1 : 2,
name: value.name,
iden_no: value.iden_no,
})
if (resp.success) {
form.reset()
if (!resp.data?.identified) {
setStep('scan')
setTarget(resp.data.target)
}
else {
toast.success('认证已完成')
}
}
else {
toast.error(resp.message || `认证失败:请稍后重试`)
}
},
)
// ======================
// 扫码认证
// ======================
const [step, setStep] = useState<'form' | 'scan'>('form')
const [target, setTarget] = useState('')
const [openDialog, setOpenDialog] = useState(false)
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvas.current && target) {
qrcode.toCanvas(canvas.current, target, {
width: 256,
margin: 0,
}, (error) => {
if (error) {
console.error(error)
toast.error(`二维码生成失败:请稍后重试`)
}
})
}
}, [target])
// ======================
// 用户数据
// ======================
const profile = useProfileStore(store => store.profile)
const refreshProfile = useProfileStore(store => store.refreshProfile)
2026-03-13 18:26:23 +08:00
// 重置认证流程
const handleDialogOpenChange = async (open: boolean) => {
setOpenDialog(open)
if (!open) {
setStep('form')
setTarget('')
await refreshProfile()
}
}
// ======================
// render
// ======================
return (
2025-06-10 19:09:19 +08:00
<Page className="flex-row max-md:flex-col">
2025-11-18 19:16:24 +08:00
<div className="flex-3/4 flex flex-col bg-white rounded-lg gap-16 max-md:gap-0">
{/* banner */}
2025-06-10 19:09:19 +08:00
<section className="flex-none basis-40 relative flex flex-col gap-4 pl-8 pr-4 justify-center">
<Image src={banner} alt="背景图" className="absolute inset-0 w-full h-full object-cover"/>
<h3 className="text-lg font-bold z-10 relative">HTTP邀请您参与</h3>
<p className="text-sm text-gray-600 z-10 relative"></p>
</section>
<div className="flex-auto flex justify-center items-start max-md:m-4">
{/* 个人认证 */}
<section className="w-96 bg-gray-50 p-8 rounded-md flex flex-col gap-4 items-center">
<Image src={personal} alt="个人认证"/>
<div>
<h3 className="text-center text-lg font-bold"></h3>
<p className="text-sm text-gray-600">
</p>
</div>
2025-12-11 14:10:52 +08:00
<Suspense>
<IfNotIdentofy>
2026-03-13 18:26:23 +08:00
<Dialog open={openDialog} onOpenChange={handleDialogOpenChange}>
2025-12-11 14:10:52 +08:00
<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>
2026-03-13 18:26:23 +08:00
<Button onClick={() => handleDialogOpenChange(false)}>
2025-12-11 14:10:52 +08:00
</Button>
</div>
)}
</DialogContent>
</Dialog>
</IfNotIdentofy>
</Suspense>
</section>
</div>
</div>
<Card className="flex-none basis-80">
<CardHeader>
<CardTitle>
<WorkflowIcon size={18}/>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col px-4">
<p className="text-sm text-weak mb-4">
使HTTP代理需完成实名认证
</p>
<p className="flex gap-2 items-center justify-between w-56 self-center">
<span className="flex gap-2">
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">01</span>
<span></span>
</span>
2025-06-10 19:09:19 +08:00
<Image alt="步骤配图" src={step1}/>
</p>
<div className="h-16 w-56 px-4 flex self-center">
<div className={merge(
`w-0 h-full border-x border-primary border-dashed relative`,
`after:absolute after:-left-[3px] after:bottom-0 after:w-1.5 after:h-1.5 after:rounded-full after:bg-primary`,
)}>
</div>
</div>
<p className="flex gap-2 items-center justify-between w-56 self-center">
<span className="flex gap-2">
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">02</span>
<span></span>
</span>
2025-06-10 19:09:19 +08:00
<Image alt="步骤配图" src={step2}/>
</p>
<div className="h-16 w-56 px-4 flex self-center">
<div className={merge(
`w-0 h-full border-x border-primary border-dashed relative`,
`after:absolute after:-left-[3px] after:bottom-0 after:w-1.5 after:h-1.5 after:rounded-full after:bg-primary`,
)}>
</div>
</div>
<p className="flex gap-2 items-center justify-between w-56 self-center">
<span className="flex gap-2">
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">03</span>
2026-03-13 18:26:23 +08:00
<span></span>
</span>
2025-06-10 19:09:19 +08:00
<Image alt="步骤配图" src={step3}/>
</p>
</CardContent>
</Card>
</Page>
)
}
2025-12-11 14:10:52 +08:00
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>
)
}