Files
web/src/app/admin/identify/page.tsx
2026-03-14 18:00:27 +08:00

254 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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'
import {ReactNode, Suspense, use, useEffect, useRef, useState} from 'react'
import * as qrcode from 'qrcode'
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'
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)
// 重置认证流程
const handleDialogOpenChange = async (open: boolean) => {
setOpenDialog(open)
if (!open) {
setStep('form')
setTarget('')
await refreshProfile()
}
}
// ======================
// render
// ======================
return (
<Page className="flex-row max-md:flex-col">
<div className="flex-3/4 flex flex-col bg-white rounded-lg gap-16 max-md:gap-0">
{/* banner */}
<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>
<Suspense>
<IfNotIdentofy>
<Dialog open={openDialog} onOpenChange={handleDialogOpenChange}>
<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={() => handleDialogOpenChange(false)}>
</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>
<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>
<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>
<span></span>
</span>
<Image alt="步骤配图" src={step3}/>
</p>
</CardContent>
</Card>
</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>
)
}