更新个人中心页面

This commit is contained in:
Eamon
2026-09-03 14:37:18 +08:00
parent 3072b97bcf
commit 6d6d300203
20 changed files with 1135 additions and 378 deletions

View File

@@ -0,0 +1,216 @@
import { useRef, useState } from "react"
import { toast } from "sonner"
import { uploadAgreement } from "@/api/agreement"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function AgreementDialog({
open,
onOpenChange,
onSigned,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onSigned: () => void
}) {
const [describe, setDescribe] = useState("")
const [file, setFile] = useState<File | null>(null)
const [agreed, setAgreed] = useState(false)
const [submitting, setSubmitting] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const handleOpenChange = (next: boolean) => {
onOpenChange(next)
if (!next) {
setDescribe("")
setFile(null)
setAgreed(false)
if (fileInputRef.current) fileInputRef.current.value = ""
}
}
const handleSubmit = async () => {
if (!agreed) {
toast.error("请勾选同意协议!")
return
}
if (describe.trim().length < 2) {
toast.error("业务描述必须填写!")
return
}
if (!file) {
toast.error("手持身份证必须上传!")
return
}
setSubmitting(true)
const res = await uploadAgreement({ describe: describe.trim(), file })
setSubmitting(false)
if (!res.success) {
toast.error(res.message)
return
}
toast.success(res.data.msg || "提交成功,请等待客服审核")
handleOpenChange(false)
onSigned()
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
签署网络安全协议
</DialogTitle>
</DialogHeader>
<p className="text-xs leading-5 text-gray-500">
您可通过以下要求勾选同意网络安全协议书后联系客服审核,然后解除微信QQ等屏蔽,新增的账号不会自动解除,记得联系客服哦
</p>
<p className="text-xs leading-5 text-red-500">
注:解除屏蔽后,qq和微信也仅可用于游戏之类的登录授权,不可用于聊天添加好友等聊天软件内部的功能,系统有严密日志,如有此类情况,账号立即封停,且不予退款,若触犯相关法律将上报给公安部门,请您知晓!
</p>
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">
详细描述业务用途<span className="text-red-500">*</span>
</Label>
<Input
value={describe}
onChange={e => setDescribe(e.target.value)}
placeholder="请描述业务用途"
className="h-11 bg-gray-50 border-gray-200"
/>
</div>
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">
上传手持身份证<span className="text-red-500">*</span>
</Label>
<div className="flex items-center gap-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={e => setFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-gray-500 file:mr-3 file:rounded-md file:border-0 file:bg-blue-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-blue-600 hover:file:bg-blue-100"
/>
</div>
<p className="text-xs text-gray-400">手持身份证的半身照片</p>
</div>
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">
网络安全协议书<span className="text-red-500">*</span>
</Label>
<div className="max-h-52 overflow-y-auto rounded-lg border border-gray-200 bg-white p-4 text-xs leading-6 text-gray-600">
<h1 className="text-center text-base font-semibold">
网络信息安全承诺书
</h1>
<p>
本单位(包含个人)郑重承诺遵守本承诺书的有关条款,如有违反本承诺书有关条款的行为,本单位(包含个人)承担由此带来的一切民事、行政和刑事责任。
</p>
<p>
一、本单位(包含个人)承诺遵守《中华人民共和国计算机信息系统安全保护条例》和《计算机信息网络国际联网安全保护管理办法》及有关法律、法规和行政规章制度、文件规定。
</p>
<p>
二、本单位(包含个人)保证不利用网络危害国家安全、泄露国家秘密,不侵犯国家的、社会的、集体的利益和第三方的合法权益,不从事违法犯罪活动。
</p>
<p>
三、本单位(包含个人)承诺严格按照国家相关法律法规做好本单位(包含个人)网站的信息安全管理工作,按政府有关部门要求设立信息安全责任人和信息安全审查员,信息安全责任人和信息安全审查员应在通过公安机关的安全技术培训后,持证上岗。
</p>
<p>
四、本单位(包含个人)承诺健全各项网络安全管理制度和落实各项安全保护技术措施。
</p>
<p>
五、本单位(包含个人)承诺接受公安机关的监督和检查,如实主动提供有关安全保护的信息、资料及数据文件,积极协助查处通过国际联网的计算机信息网络违法犯罪行为。
</p>
<p>
六、本单位(包含个人)承诺不通过互联网制作、复制、查阅和传播下列信息:
</p>
<p className="pl-4">
1、反对宪法所确定的基本原则的。
<br />
2、危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的。
<br />
3、损害国家荣誉和利益的。
<br />
4、煽动民族仇恨、民族歧视,破坏民族团结的。
<br />
5、破坏国家宗教政策,宣扬邪教和封建迷信的。
<br />
6、散布谣言,扰乱社会秩序,破坏社会稳定的。
<br />
7、散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的。
<br />
8、侮辱或者诽谤他人,侵害他人合法权益的。
<br />
9、含有法律法规禁止的其他内容的。
</p>
<p>
七、本单位(包含个人)承诺不从事任何危害计算机信息网络安全的活动,包括但不限于:
</p>
<p className="pl-4">
1、未经允许,进入计算机信息网络或者使用计算机信息网络资源的。
<br />
2、未经允许,对计算机信息网络功能进行删除、修改或者增加的。
<br />
3、未经允许,对计算机信息网络中存储或者传输的数据和应用程序进行删除、修改或者增加的。
<br />
4、故意制作、传播计算机病毒等破坏性程序的。
<br />
5、其他危害计算机信息网络安全的。
</p>
<p>
八、本单位(包含个人)承诺,当计算机信息系统发生重大安全事故时,立即采取应急措施,保留有关原始记录,并在24小时内向政府监管部门报告,并书面知会贵单位(包含个人)。
</p>
<p>
九、本单位(包含个人)承诺,若本单位(包含个人)租用的拨号账号出现违规违法行为导致贵单位(指华连网络科技有限公司)被处罚的,造成的直接损失由本单位(包含个人)赔偿。
</p>
<p>
十、若违反本承诺书有关条款和国家相关法律法规的,本单位(包含个人)直接承担相应法律责任,造成财产损失的,由本单位(包含个人)直接赔偿。同时,贵单位(指华连网络科技有限公司)有权暂停直至解除双方合作合同。
</p>
<p>
十一、解除屏蔽后,qq和微信也仅可用于游戏之类的登录授权,不可用于聊天添加好友等聊天软件内部的功能,系统有严密日志,如有此类情况,账号立即封停,且不预退款,若触犯相关法律将上报给公安部门,请您知晓!
</p>
<p>十二、本承诺书自勾选确认之日起生效并遵行。</p>
</div>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="agreement-agree"
checked={agreed}
onCheckedChange={checked => setAgreed(checked === true)}
/>
<Label
htmlFor="agreement-agree"
className="text-sm text-gray-600 cursor-pointer"
>
阅读、接受并同意签署以上网络信息安全承诺书
</Label>
</div>
<DialogFooter>
<Button
onClick={() => void handleSubmit()}
disabled={submitting}
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
>
{submitting ? "提交中..." : "提交"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -79,7 +79,7 @@ export function EditInfoDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-120 p-6">
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-120 p-6">
<DialogHeader>
<DialogTitle className="text-xl font-semibold text-center">
编辑信息

View File

@@ -0,0 +1,213 @@
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,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onVerified: () => void
}) {
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) => {
onOpenChange(next)
if (!next) {
form.reset({ name: "", idCode: "" })
setStage("form")
setQrUrl("")
}
}
const handleFinish = () => {
onVerified()
onOpenChange(false)
}
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("该身份已经进行认证,无法再次进行认证,请联系在线客服处理")
handleFinish()
return
}
if (code === 10000 && url) {
setQrUrl(url)
setStage("scan")
return
}
toast.error("请正确填写姓名和身份证号码")
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-lg">
{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">
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
>
取消
</Button>
<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">
<Button variant="outline" onClick={() => handleOpenChange(false)}>
取消
</Button>
<Button
onClick={handleFinish}
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
>
实名扫脸结束后请点击
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,48 @@
import QRCode from "qrcode"
import { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
export function QrCodeImage({
text,
size = 70,
className,
}: {
text: string
size?: number
className?: string
}) {
const [dataUrl, setDataUrl] = useState("")
useEffect(() => {
if (!text) {
setDataUrl("")
return
}
let cancelled = false
QRCode.toDataURL(text, {
width: size * 4,
margin: 1,
errorCorrectionLevel: "M",
})
.then(url => {
if (!cancelled) setDataUrl(url)
})
.catch(() => {
if (!cancelled) setDataUrl("")
})
return () => {
cancelled = true
}
}, [text, size])
if (!dataUrl) return null
return (
<img
src={dataUrl}
alt="二维码"
style={{ width: size, height: size }}
className={cn("object-contain", className)}
/>
)
}