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
+156 -97
View File
@@ -3,7 +3,6 @@
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { import {
Select, Select,
@@ -12,59 +11,132 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator" import {
import { Textarea } from "@/components/ui/textarea" 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 type { UserRole } from "@/lib/types"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { CheckCircle, Clock, ShieldCheck, Upload } from "lucide-react" 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() { export default function VerifyPage() {
const [verifyRole, setVerifyRole] = useState<UserRole | "">("") const [verifyRole, setVerifyRole] = useState<UserRole | "">("")
const [realName, setRealName] = useState("") const [idCardFront, setIdCardFront] = useState("")
const [idNumber, setIdNumber] = useState("") const [idCardBack, setIdCardBack] = useState("")
const [gameProfile, setGameProfile] = 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 verificationStatus = useAuthStore((state) => state.verificationStatus)
const verificationReasons = useAuthStore((state) => state.verificationReasons) const verificationReasons = useAuthStore((state) => state.verificationReasons)
const verifiedRoles = useAuthStore((state) => state.verifiedRoles) const verifiedRoles = useAuthStore((state) => state.verifiedRoles)
const submitVerification = useAuthStore((state) => state.submitVerification) const login = useAuthStore((state) => state.login)
const approveVerification = useAuthStore((state) => state.approveVerification)
const timersRef = useRef<Map<UserRole, ReturnType<typeof setTimeout>>>(new Map())
useEffect( useEffect(() => {
() => () => { let cancelled = false
timersRef.current.forEach((timer) => {
clearTimeout(timer) void listCurrentUserVerifications()
.then((records) => {
if (!cancelled) setVerificationRecords(records)
})
.catch((error) => {
if (!cancelled) notifyInfo(toApiError(error).msg)
}) })
timersRef.current.clear()
},
[],
)
const buildMaterials = () => { return () => {
const materials = { cancelled = true
realName, }
idNumber, }, [])
gameProfile,
idCardFront: "mock://idCardFront",
idCardBack: "mock://idCardBack",
gameScreenshot: "mock://gameScreenshot",
} satisfies Record<string, string>
return materials 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 submitWithMockApproval = (role: UserRole) => { const submitVerification = async (role: UserRole) => {
submitVerification(role, buildMaterials()) if (!idCardFront || !idCardBack) {
const oldTimer = timersRef.current.get(role) notifyInfo("请先上传身份证正反面")
if (oldTimer) { return
clearTimeout(oldTimer) }
setSubmitting(true)
try {
await applyCurrentUserVerification({
role,
materials: {
idCardFront,
idCardBack,
gameScreenshots: gameScreenshot ? [gameScreenshot] : undefined,
},
})
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 timer = setTimeout(() => {
approveVerification(role)
timersRef.current.delete(role)
}, 3000)
timersRef.current.set(role, timer)
} }
const roleMeta = [ const roleMeta = [
@@ -72,6 +144,16 @@ export default function VerifyPage() {
{ role: "owner", label: "店主认证" }, { role: "owner", label: "店主认证" },
] as const ] 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 ( return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6"> <div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
@@ -98,8 +180,8 @@ export default function VerifyPage() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{roleMeta.map((item) => { {roleMeta.map((item) => {
const status = verificationStatus[item.role] const status = statusFor(item.role)
const reason = verificationReasons[item.role] const reason = reasonFor(item.role)
return ( return (
<Card key={item.role} className="p-3 space-y-2 shadow-none"> <Card key={item.role} className="p-3 space-y-2 shadow-none">
@@ -131,12 +213,8 @@ export default function VerifyPage() {
)} )}
{status === "rejected" && ( {status === "rejected" && (
<Button <Button variant="outline" size="sm" onClick={() => setVerifyRole(item.role)}>
variant="outline"
size="sm"
onClick={() => submitWithMockApproval(item.role)}
>
</Button> </Button>
)} )}
</Card> </Card>
@@ -163,72 +241,53 @@ export default function VerifyPage() {
</Select> </Select>
</div> </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"> <div className="space-y-2">
<Label></Label> <Label></Label>
<div className="flex gap-2"> <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"> <MaterialUpload
<Upload className="h-5 w-5" /> label="身份证正面"
<span className="text-[10px]"></span> value={idCardFront}
</div> uploading={uploading === "idCardFront"}
<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"> onSelect={(file) => uploadMaterial("idCardFront", file)}
<Upload className="h-5 w-5" /> />
<span className="text-[10px]"></span> <MaterialUpload
</div> label="身份证反面"
<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"> value={idCardBack}
<Upload className="h-5 w-5" /> uploading={uploading === "idCardBack"}
<span className="text-[10px]"></span> onSelect={(file) => uploadMaterial("idCardBack", file)}
</div> />
<MaterialUpload
label="游戏截图"
value={gameScreenshot}
uploading={uploading === "gameScreenshot"}
onSelect={(file) => uploadMaterial("gameScreenshot", file)}
/>
</div> </div>
<p className="text-xs text-muted-foreground"> JPGPNG 5MB</p> <p className="text-xs text-muted-foreground">
</p>
</div> </div>
<Button <Button
className="w-full" className="w-full"
disabled={ disabled={
!verifyRole || !verifyRole ||
verificationStatus[verifyRole] === "pending" || submitting ||
verificationStatus[verifyRole] === "approved" statusFor(verifyRole) === "pending" ||
statusFor(verifyRole) === "approved" ||
!idCardFront ||
!idCardBack
} }
onClick={() => { onClick={() => {
if (!verifyRole) return if (!verifyRole) return
submitWithMockApproval(verifyRole) void submitVerification(verifyRole)
}} }}
> >
{verifyRole && verificationStatus[verifyRole] === "rejected" {submitting
? "重新提交认证申请" ? "提交中..."
: "提交认证申请"} : verifyRole && statusFor(verifyRole) === "rejected"
? "重新提交认证申请"
: "提交认证申请"}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
+8 -1
View File
@@ -16,4 +16,11 @@ export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./revi
export { getServiceById, listServices, listServicesByPlayer } from "./services" export { getServiceById, listServices, listServicesByPlayer } from "./services"
export { getShopById, getShopByOwnerId, listShops } from "./shops" export { getShopById, getShopByOwnerId, listShops } from "./shops"
export { getWalletBalance, listWalletTransactions } from "./transactions" 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 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> { export async function getUserById(userId: string): Promise<User | undefined> {
try { try {
return await httpJson<User>(`/api/v1/users/${encodeURIComponent(userId)}`) return await httpJson<User>(`/api/v1/users/${encodeURIComponent(userId)}`)
@@ -34,3 +53,20 @@ export async function switchCurrentRole(role: UserRole): Promise<void> {
json: { role }, 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
}