Files
juwan-frontend/app/(account)/verify/page.tsx
T
2026-04-25 20:24:18 +08:00

283 lines
9.7 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 { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { StatusBadge } from "@/components/ui/status-badge"
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, ShieldCheck, Upload } from "lucide-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-lg border border-border/60 bg-muted/20 flex flex-col items-center justify-center gap-1.5 text-muted-foreground cursor-pointer hover:bg-accent/50 hover:border-border 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 opacity-70" />
<span className="text-[10px] font-medium">{uploading ? "上传中..." : label}</span>
{value && (
<Badge variant="success" className="text-[10px] px-1.5 py-0 mt-0.5">
</Badge>
)}
</label>
)
}
export default function VerifyPage() {
const [verifyRole, setVerifyRole] = useState<UserRole | "">("")
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 login = useAuthStore((state) => state.login)
useEffect(() => {
let cancelled = false
void listCurrentUserVerifications()
.then((records) => {
if (!cancelled) setVerificationRecords(records)
})
.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 [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 roleMeta = [
{ role: "player", label: "打手认证" },
{ 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-8">
<h1 className="text-2xl font-bold"></h1>
<section className="space-y-4">
<h2 className="text-base font-semibold"></h2>
<div className="rounded-xl border border-border/60 bg-muted/10 p-4 space-y-3 text-sm text-muted-foreground">
<div className="flex items-start gap-2">
<ShieldCheck className="h-4 w-4 shrink-0 mt-0.5 text-primary" />
<span></span>
</div>
<div className="flex items-start gap-2">
<CheckCircle className="h-4 w-4 shrink-0 mt-0.5 text-primary" />
<span></span>
</div>
</div>
</section>
<section className="space-y-4">
<h2 className="text-base font-semibold"></h2>
<div className="rounded-xl border border-border/60 divide-y divide-border/60">
{roleMeta.map((item) => {
const status = statusFor(item.role)
const reason = reasonFor(item.role)
return (
<div key={item.role} className="flex flex-col gap-2 p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{item.label}</span>
{status === "approved" || verifiedRoles.includes(item.role) ? (
<StatusBadge status="success"></StatusBadge>
) : status === "pending" ? (
<StatusBadge status="info"></StatusBadge>
) : status === "rejected" ? (
<StatusBadge status="destructive"></StatusBadge>
) : (
<StatusBadge status="neutral"></StatusBadge>
)}
</div>
{status === "rejected" && (
<div className="flex items-center justify-between mt-2">
<p className="text-xs text-muted-foreground">{reason}</p>
<Button variant="outline" size="sm" onClick={() => setVerifyRole(item.role)}>
</Button>
</div>
)}
</div>
)
})}
</div>
</section>
<Card className="border-border/60 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<Label></Label>
<Select value={verifyRole} onValueChange={(value) => setVerifyRole(value as UserRole)}>
<SelectTrigger>
<SelectValue placeholder="选择认证类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="player"></SelectItem>
<SelectItem value="owner"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<Label></Label>
<div className="flex flex-wrap gap-3">
<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>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Button
className="w-full"
disabled={
!verifyRole ||
submitting ||
statusFor(verifyRole) === "pending" ||
statusFor(verifyRole) === "approved" ||
!idCardFront ||
!idCardBack
}
onClick={() => {
if (!verifyRole) return
void submitVerification(verifyRole)
}}
>
{submitting
? "提交中..."
: verifyRole && statusFor(verifyRole) === "rejected"
? "重新提交认证申请"
: "提交认证申请"}
</Button>
</CardContent>
</Card>
</div>
)
}