fix(account): submit verification through backend

This commit is contained in:
zetaloop
2026-04-25 14:17:51 +08:00
parent e3572bf86b
commit 2661cfcd8a
3 changed files with 200 additions and 98 deletions
+157 -98
View File
@@ -3,7 +3,6 @@
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
@@ -12,59 +11,132 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
import {
applyCurrentUserVerification,
getCurrentUserForLogin,
listCurrentUserVerifications,
uploadFile,
} from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { UserRole } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { CheckCircle, Clock, ShieldCheck, Upload } from "lucide-react"
import { useEffect, useRef, useState } from "react"
import { useEffect, useState } from "react"
type MaterialKey = "idCardFront" | "idCardBack" | "gameScreenshot"
function MaterialUpload({
label,
value,
uploading,
onSelect,
}: {
label: string
value: string
uploading: boolean
onSelect: (file: File) => Promise<void>
}) {
return (
<label className="h-24 w-24 rounded-md border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground cursor-pointer hover:border-muted-foreground/50 transition-colors">
<input
type="file"
accept="image/*"
className="hidden"
disabled={uploading}
onChange={(event) => {
const target = event.currentTarget
const file = target.files?.[0]
if (!file) return
void onSelect(file)
target.value = ""
}}
/>
<Upload className="h-5 w-5" />
<span className="text-[10px]">{uploading ? "上传中..." : label}</span>
{value && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
</Badge>
)}
</label>
)
}
export default function VerifyPage() {
const [verifyRole, setVerifyRole] = useState<UserRole | "">("")
const [realName, setRealName] = useState("")
const [idNumber, setIdNumber] = useState("")
const [gameProfile, setGameProfile] = useState("")
const [idCardFront, setIdCardFront] = useState("")
const [idCardBack, setIdCardBack] = useState("")
const [gameScreenshot, setGameScreenshot] = useState("")
const [uploading, setUploading] = useState<MaterialKey | null>(null)
const [submitting, setSubmitting] = useState(false)
const [verificationRecords, setVerificationRecords] = useState<
Awaited<ReturnType<typeof listCurrentUserVerifications>>
>([])
const verificationStatus = useAuthStore((state) => state.verificationStatus)
const verificationReasons = useAuthStore((state) => state.verificationReasons)
const verifiedRoles = useAuthStore((state) => state.verifiedRoles)
const submitVerification = useAuthStore((state) => state.submitVerification)
const approveVerification = useAuthStore((state) => state.approveVerification)
const timersRef = useRef<Map<UserRole, ReturnType<typeof setTimeout>>>(new Map())
const login = useAuthStore((state) => state.login)
useEffect(
() => () => {
timersRef.current.forEach((timer) => {
clearTimeout(timer)
useEffect(() => {
let cancelled = false
void listCurrentUserVerifications()
.then((records) => {
if (!cancelled) setVerificationRecords(records)
})
timersRef.current.clear()
.catch((error) => {
if (!cancelled) notifyInfo(toApiError(error).msg)
})
return () => {
cancelled = true
}
}, [])
const uploadMaterial = async (key: MaterialKey, file: File) => {
setUploading(key)
try {
const url = await uploadFile(file, "verification")
if (key === "idCardFront") setIdCardFront(url)
if (key === "idCardBack") setIdCardBack(url)
if (key === "gameScreenshot") setGameScreenshot(url)
notifySuccess("材料已上传")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setUploading(null)
}
}
const submitVerification = async (role: UserRole) => {
if (!idCardFront || !idCardBack) {
notifyInfo("请先上传身份证正反面")
return
}
setSubmitting(true)
try {
await applyCurrentUserVerification({
role,
materials: {
idCardFront,
idCardBack,
gameScreenshots: gameScreenshot ? [gameScreenshot] : undefined,
},
[],
)
const buildMaterials = () => {
const materials = {
realName,
idNumber,
gameProfile,
idCardFront: "mock://idCardFront",
idCardBack: "mock://idCardBack",
gameScreenshot: "mock://gameScreenshot",
} satisfies Record<string, string>
return materials
})
const [records, updated] = await Promise.all([
listCurrentUserVerifications(),
getCurrentUserForLogin(),
])
setVerificationRecords(records)
login(updated, updated.verifiedRoles ?? [updated.role])
notifySuccess("认证申请已提交")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSubmitting(false)
}
const submitWithMockApproval = (role: UserRole) => {
submitVerification(role, buildMaterials())
const oldTimer = timersRef.current.get(role)
if (oldTimer) {
clearTimeout(oldTimer)
}
const timer = setTimeout(() => {
approveVerification(role)
timersRef.current.delete(role)
}, 3000)
timersRef.current.set(role, timer)
}
const roleMeta = [
@@ -72,6 +144,16 @@ export default function VerifyPage() {
{ role: "owner", label: "店主认证" },
] as const
const statusFor = (role: UserRole) => {
const record = verificationRecords.find((item) => item.role === role)
return record?.status ?? verificationStatus[role]
}
const reasonFor = (role: UserRole) => {
const record = verificationRecords.find((item) => item.role === role)
return record?.rejectReason ?? verificationReasons[role]
}
return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<h1 className="text-2xl font-bold"></h1>
@@ -98,8 +180,8 @@ export default function VerifyPage() {
</CardHeader>
<CardContent className="space-y-3">
{roleMeta.map((item) => {
const status = verificationStatus[item.role]
const reason = verificationReasons[item.role]
const status = statusFor(item.role)
const reason = reasonFor(item.role)
return (
<Card key={item.role} className="p-3 space-y-2 shadow-none">
@@ -131,12 +213,8 @@ export default function VerifyPage() {
)}
{status === "rejected" && (
<Button
variant="outline"
size="sm"
onClick={() => submitWithMockApproval(item.role)}
>
<Button variant="outline" size="sm" onClick={() => setVerifyRole(item.role)}>
</Button>
)}
</Card>
@@ -163,70 +241,51 @@ export default function VerifyPage() {
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="real-name"></Label>
<Input
id="real-name"
placeholder="请输入真实姓名"
value={realName}
onChange={(event) => setRealName(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="id-number"></Label>
<Input
id="id-number"
placeholder="请输入身份证号"
value={idNumber}
onChange={(event) => setIdNumber(event.target.value)}
/>
</div>
<Separator />
<div className="space-y-2">
<Label></Label>
<Textarea
placeholder="请描述你的游戏经历、段位、擅长游戏等"
rows={3}
value={gameProfile}
onChange={(event) => setGameProfile(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex gap-2">
<div className="h-24 w-24 rounded-md border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground cursor-pointer hover:border-muted-foreground/50 transition-colors">
<Upload className="h-5 w-5" />
<span className="text-[10px]"></span>
<MaterialUpload
label="身份证正面"
value={idCardFront}
uploading={uploading === "idCardFront"}
onSelect={(file) => uploadMaterial("idCardFront", file)}
/>
<MaterialUpload
label="身份证反面"
value={idCardBack}
uploading={uploading === "idCardBack"}
onSelect={(file) => uploadMaterial("idCardBack", file)}
/>
<MaterialUpload
label="游戏截图"
value={gameScreenshot}
uploading={uploading === "gameScreenshot"}
onSelect={(file) => uploadMaterial("gameScreenshot", file)}
/>
</div>
<div className="h-24 w-24 rounded-md border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground cursor-pointer hover:border-muted-foreground/50 transition-colors">
<Upload className="h-5 w-5" />
<span className="text-[10px]"></span>
</div>
<div className="h-24 w-24 rounded-md border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground cursor-pointer hover:border-muted-foreground/50 transition-colors">
<Upload className="h-5 w-5" />
<span className="text-[10px]"></span>
</div>
</div>
<p className="text-xs text-muted-foreground"> JPGPNG 5MB</p>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Button
className="w-full"
disabled={
!verifyRole ||
verificationStatus[verifyRole] === "pending" ||
verificationStatus[verifyRole] === "approved"
submitting ||
statusFor(verifyRole) === "pending" ||
statusFor(verifyRole) === "approved" ||
!idCardFront ||
!idCardBack
}
onClick={() => {
if (!verifyRole) return
submitWithMockApproval(verifyRole)
void submitVerification(verifyRole)
}}
>
{verifyRole && verificationStatus[verifyRole] === "rejected"
{submitting
? "提交中..."
: verifyRole && statusFor(verifyRole) === "rejected"
? "重新提交认证申请"
: "提交认证申请"}
</Button>
+8 -1
View File
@@ -16,4 +16,11 @@ export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./revi
export { getServiceById, listServices, listServicesByPlayer } from "./services"
export { getShopById, getShopByOwnerId, listShops } from "./shops"
export { getWalletBalance, listWalletTransactions } from "./transactions"
export { getCurrentUserForLogin, getUserById, switchCurrentRole, updateCurrentUser } from "./users"
export {
applyCurrentUserVerification,
getCurrentUserForLogin,
getUserById,
listCurrentUserVerifications,
switchCurrentRole,
updateCurrentUser,
} from "./users"
+36
View File
@@ -8,6 +8,25 @@ export type UpdateCurrentUserInput = {
bio?: string
}
export type VerificationMaterials = {
idCardFront: string
idCardBack: string
gameScreenshots?: string[]
voiceDemo?: string
}
export type VerificationRecord = {
id: number
userId: number
userNickname: string
role: UserRole
status: string
materials: Record<string, string>
rejectReason?: string
createdAt: string
reviewedAt?: string
}
export async function getUserById(userId: string): Promise<User | undefined> {
try {
return await httpJson<User>(`/api/v1/users/${encodeURIComponent(userId)}`)
@@ -34,3 +53,20 @@ export async function switchCurrentRole(role: UserRole): Promise<void> {
json: { role },
})
}
export async function applyCurrentUserVerification(input: {
role: UserRole
materials: VerificationMaterials
}): Promise<void> {
await httpJson<unknown>("/api/v1/users/me/verification", {
method: "POST",
json: input,
})
}
export async function listCurrentUserVerifications(): Promise<VerificationRecord[]> {
const res = await httpJson<{ list: VerificationRecord[] }>("/api/v1/users/me/verification", {
cache: "no-store",
})
return res.list
}