Files
juwan-frontend/app/(account)/verify/page.tsx
T

297 lines
9.8 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 {
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, 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 [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-6">
<h1 className="text-2xl font-bold"></h1>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="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>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{roleMeta.map((item) => {
const status = statusFor(item.role)
const reason = reasonFor(item.role)
return (
<Card key={item.role} className="p-3 space-y-2 shadow-none">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{item.label}</span>
{status === "approved" || verifiedRoles.includes(item.role) ? (
<Badge
variant="outline"
className="text-green-700 border-green-200 bg-green-50"
>
</Badge>
) : status === "pending" ? (
<Badge variant="outline">
<Clock className="mr-1 h-3.5 w-3.5" />
</Badge>
) : status === "rejected" ? (
<Badge variant="outline" className="text-red-700 border-red-200 bg-red-50">
</Badge>
) : (
<Badge variant="secondary"></Badge>
)}
</div>
{status === "rejected" && (
<p className="text-xs text-muted-foreground">{reason}</p>
)}
{status === "rejected" && (
<Button variant="outline" size="sm" onClick={() => setVerifyRole(item.role)}>
</Button>
)}
</Card>
)
})}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<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-2">
<Label></Label>
<div className="flex gap-2">
<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>
)
}