2026-09-03 14:37:18 +08:00
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
|
|
|
|
import { useState } from "react"
|
|
|
|
|
|
import { Controller, useForm } from "react-hook-form"
|
|
|
|
|
|
import { toast } from "sonner"
|
|
|
|
|
|
import { z } from "zod"
|
|
|
|
|
|
import { submitRealName } from "@/api/agreement"
|
|
|
|
|
|
import { QrCodeImage } from "@/components/qrCodeImage"
|
|
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
DialogContent,
|
|
|
|
|
|
DialogFooter,
|
|
|
|
|
|
DialogHeader,
|
|
|
|
|
|
DialogTitle,
|
|
|
|
|
|
} from "@/components/ui/dialog"
|
|
|
|
|
|
import { FieldError } from "@/components/ui/field"
|
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
|
|
|
|
|
|
|
|
const realNameSchema = z.object({
|
|
|
|
|
|
name: z.string().trim().min(1, "请输入真实姓名"),
|
|
|
|
|
|
idCode: z
|
|
|
|
|
|
.string()
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.regex(/^\d{15}$|^\d{18}$|^\d{17}(\d|X|x)$/, "身份证号码不合法"),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
type RealNameValues = z.infer<typeof realNameSchema>
|
|
|
|
|
|
|
|
|
|
|
|
export default function RealNameDialog({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onOpenChange,
|
|
|
|
|
|
onVerified,
|
2026-09-11 17:06:29 +08:00
|
|
|
|
mandatory = false,
|
2026-09-03 14:37:18 +08:00
|
|
|
|
}: {
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
|
onVerified: () => void
|
2026-09-11 17:06:29 +08:00
|
|
|
|
/** 强制模式:不可关闭,直到完成实名认证(由外层 Gate 在认证成功后卸载) */
|
|
|
|
|
|
mandatory?: boolean
|
2026-09-03 14:37:18 +08:00
|
|
|
|
}) {
|
|
|
|
|
|
const [stage, setStage] = useState<"form" | "scan">("form")
|
|
|
|
|
|
const [qrUrl, setQrUrl] = useState("")
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
const form = useForm<RealNameValues>({
|
|
|
|
|
|
resolver: zodResolver(realNameSchema),
|
|
|
|
|
|
defaultValues: { name: "", idCode: "" },
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenChange = (next: boolean) => {
|
2026-09-11 17:06:29 +08:00
|
|
|
|
if (!next && mandatory) return
|
2026-09-03 14:37:18 +08:00
|
|
|
|
onOpenChange(next)
|
|
|
|
|
|
if (!next) {
|
|
|
|
|
|
form.reset({ name: "", idCode: "" })
|
|
|
|
|
|
setStage("form")
|
|
|
|
|
|
setQrUrl("")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFinish = () => {
|
|
|
|
|
|
onVerified()
|
2026-09-11 17:06:29 +08:00
|
|
|
|
if (!mandatory) onOpenChange(false)
|
2026-09-03 14:37:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: RealNameValues) => {
|
|
|
|
|
|
setSubmitting(true)
|
|
|
|
|
|
const res = await submitRealName({
|
|
|
|
|
|
idCode: data.idCode,
|
|
|
|
|
|
name: data.name,
|
|
|
|
|
|
})
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
|
|
|
|
|
|
if (!res.success) {
|
|
|
|
|
|
toast.error(res.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { code, url } = res.data
|
|
|
|
|
|
if (code === 20000) {
|
|
|
|
|
|
toast.success("已经认证成功")
|
|
|
|
|
|
handleFinish()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (code === 30000) {
|
|
|
|
|
|
toast.error("该身份已经进行认证,无法再次进行认证,请联系在线客服处理")
|
2026-09-11 17:06:29 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (code === -10000) {
|
|
|
|
|
|
toast.error("接口问题,请联系客服")
|
2026-09-03 14:37:18 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (code === 10000 && url) {
|
|
|
|
|
|
setQrUrl(url)
|
|
|
|
|
|
setStage("scan")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
toast.error("请正确填写姓名和身份证号码")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
2026-09-11 17:06:29 +08:00
|
|
|
|
<DialogContent
|
|
|
|
|
|
className="max-h-[85dvh] overflow-y-auto sm:max-w-lg"
|
|
|
|
|
|
showCloseButton={!mandatory}
|
|
|
|
|
|
onEscapeKeyDown={mandatory ? e => e.preventDefault() : undefined}
|
|
|
|
|
|
onPointerDownOutside={mandatory ? e => e.preventDefault() : undefined}
|
|
|
|
|
|
>
|
2026-09-03 14:37:18 +08:00
|
|
|
|
{stage === "form" ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
|
<DialogTitle className="text-xl font-semibold text-blue-600">
|
|
|
|
|
|
尊敬的用户,您还未进行实名认证!
|
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="max-h-56 overflow-y-auto space-y-2 rounded-lg bg-gray-50 p-4 text-xs leading-5 text-gray-600">
|
|
|
|
|
|
<p>
|
|
|
|
|
|
《中华人民共和国网络安全法》第二十四条规定:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
网络运营者为用户办理网络接入、域名注册服务,办理固定电话、移动电话等入网手续,或者为用户提供信息发布、即时通讯等服务,在与用户签订协议或者确认提供服务时,应当要求用户提供真实身份信息。用户不提供真实身份信息的,网络运营者不得为其提供相关服务。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-red-500">
|
|
|
|
|
|
1、为了你的帐号安全请勿使用他人手机号或身份信息
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
2、聚IP承诺:用户的个人隐私数据仅与国家公安一所信息库比对,本公司不调用及储存用户的实人认证数据,仅接收公安一所方面的实人认证结果!
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>3、若您扫脸认证不成功,请联系客服</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
|
className="space-y-4"
|
|
|
|
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
|
|
|
|
noValidate
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<Label className="text-sm text-gray-600">真实姓名</Label>
|
|
|
|
|
|
<Controller
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
render={({ field, fieldState }) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
{...field}
|
|
|
|
|
|
placeholder="请输入真实姓名"
|
|
|
|
|
|
className="h-11 bg-gray-50 border-gray-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<Label className="text-sm text-gray-600">身份证号</Label>
|
|
|
|
|
|
<Controller
|
|
|
|
|
|
name="idCode"
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
render={({ field, fieldState }) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
{...field}
|
|
|
|
|
|
placeholder="请输入身份证号码"
|
|
|
|
|
|
className="h-11 bg-gray-50 border-gray-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="gap-3">
|
2026-09-11 17:06:29 +08:00
|
|
|
|
{!mandatory && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleOpenChange(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2026-09-03 14:37:18 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
|
|
|
|
|
>
|
|
|
|
|
|
{submitting ? "认证中..." : "确认"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
|
<DialogTitle className="text-xl font-semibold text-blue-600">
|
|
|
|
|
|
请打开微信扫码进行认证
|
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<p className="text-center text-sm text-gray-600">
|
|
|
|
|
|
若扫脸后仍未认证成功,请联系客服
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-center py-2">
|
|
|
|
|
|
<div className="rounded-lg border border-gray-200 p-2">
|
|
|
|
|
|
<QrCodeImage text={qrUrl} size={240} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="gap-3">
|
2026-09-11 17:06:29 +08:00
|
|
|
|
{!mandatory && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => handleOpenChange(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2026-09-03 14:37:18 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
onClick={handleFinish}
|
|
|
|
|
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
|
|
|
|
|
>
|
|
|
|
|
|
实名扫脸结束后请点击
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|