From 0f86e5b9eefe01b8cce8afaebf2cd75b6aeb1828 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 5 Apr 2026 20:26:57 +0800 Subject: [PATCH] feat(verifications): add identity verification management for admin panel Add new verifications module including API endpoints for retrieving and reviewing user identity verifications, along with corresponding admin page, routing, and sidebar navigation entry. --- src/api/index.ts | 2 + src/api/verifications.ts | 53 ++++ src/components/Layout/Sidebar.tsx | 7 + src/pages/Verifications.tsx | 457 ++++++++++++++++++++++++++++++ src/router/index.tsx | 11 + 5 files changed, 530 insertions(+) create mode 100644 src/api/verifications.ts create mode 100644 src/pages/Verifications.tsx diff --git a/src/api/index.ts b/src/api/index.ts index 7494f3a..806d1ac 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -18,3 +18,5 @@ export { subjectsApi, materialsApi } from './materials' export type { MaterialSubject, MaterialFile, MaterialFileType, SubjectUpsertRequest, MaterialCreateRequest, MaterialUpdateRequest, MaterialQueryParams } from './materials' export { reportsApi } from './reports' export type { ReportListItem, ReportDetail, ReportQueryParams, ReportStatus, ReportTargetType, HandleReportRequest, BatchHandleRequest, BatchHandleResponse } from './reports' +export { verificationsApi } from './verifications' +export type { VerificationListItem, VerificationDetail, VerificationQueryParams, VerificationStatus, UserIdentity, ReviewVerificationRequest } from './verifications' diff --git a/src/api/verifications.ts b/src/api/verifications.ts new file mode 100644 index 0000000..2d2fec7 --- /dev/null +++ b/src/api/verifications.ts @@ -0,0 +1,53 @@ +import apiClient from './client' +import type { ApiResponse, PaginatedResponse, User } from '@/types' + +export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external' +export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected' + +export interface VerificationListItem { + id: string + user_id: string + username: string + nickname: string + avatar: string + identity: UserIdentity + status: VerificationStatus + real_name: string + student_id?: string + employee_id?: string + department?: string + created_at: string + reviewed_at?: string + reviewer_name?: string +} + +export interface VerificationDetail extends VerificationListItem { + proof_materials: string + reviewed_by?: string + reject_reason?: string +} + +export interface VerificationQueryParams { + page?: number + page_size?: number + status?: VerificationStatus + keyword?: string +} + +export interface ReviewVerificationRequest { + approve: boolean + reject_reason?: string +} + +export const verificationsApi = { + getVerifications: (params: VerificationQueryParams) => + apiClient.get>>('/admin/verifications', { params }), + + getVerificationById: (id: string) => + apiClient.get>(`/admin/verifications/${id}`), + + reviewVerification: (id: string, data: ReviewVerificationRequest) => + apiClient.put>(`/admin/verifications/${id}/review`, data), +} + +export default verificationsApi diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 068fe68..9f94751 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -15,6 +15,7 @@ import { ChevronDown, Layers, Flag, + ShieldCheck, } from 'lucide-react' import { Button } from '@/components/ui/button' import { useState } from 'react' @@ -75,6 +76,12 @@ const menuItems: MenuItem[] = [ icon: Flag, roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR], }, + { + title: '身份认证', + path: '/verifications', + icon: ShieldCheck, + roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN], + }, { title: '频道管理', path: '/channels', diff --git a/src/pages/Verifications.tsx b/src/pages/Verifications.tsx new file mode 100644 index 0000000..ab08892 --- /dev/null +++ b/src/pages/Verifications.tsx @@ -0,0 +1,457 @@ +import { useState, useEffect, useCallback } from 'react' +import { verificationsApi, type VerificationListItem, type VerificationDetail, type VerificationStatus } from '@/api' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Textarea } from '@/components/ui/textarea' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { PaginationNavigator } from '@/components/ui/pagination' +import { Search, RefreshCw, Eye, CheckCircle, XCircle, Loader2 } from 'lucide-react' + +const statusColors: Record = { + not_submitted: 'bg-gray-500', + pending: 'bg-yellow-500', + approved: 'bg-green-500', + rejected: 'bg-red-500', +} + +const statusText: Record = { + not_submitted: '未提交', + pending: '待审核', + approved: '已通过', + rejected: '已拒绝', +} + +const identityText: Record = { + student: '学生', + teacher: '教师', + staff: '职工', + alumni: '校友', + external: '外部人员', + general: '普通用户', +} + +export default function Verifications() { + const [verifications, setVerifications] = useState([]) + const [loading, setLoading] = useState(true) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const pageSize = 10 + const [keyword, setKeyword] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + + const [detailOpen, setDetailOpen] = useState(false) + const [selectedVerification, setSelectedVerification] = useState(null) + const [detailLoading, setDetailLoading] = useState(false) + + const [reviewDialogOpen, setReviewDialogOpen] = useState(false) + const [reviewLoading, setReviewLoading] = useState(false) + const [rejectReason, setRejectReason] = useState('') + + const loadVerifications = useCallback(async () => { + setLoading(true) + try { + const params = { + page, + page_size: pageSize, + keyword: keyword || undefined, + status: statusFilter !== 'all' ? statusFilter as VerificationStatus : undefined, + } + const response = await verificationsApi.getVerifications(params) + setVerifications(response.data.data.list || []) + setTotal(response.data.data.total || 0) + } catch (error) { + console.error('Failed to load verifications:', error) + setVerifications([]) + setTotal(0) + } finally { + setLoading(false) + } + }, [page, keyword, statusFilter]) + + useEffect(() => { + loadVerifications() + }, [loadVerifications]) + + const handleViewDetail = async (verification: VerificationListItem) => { + setDetailLoading(true) + setDetailOpen(true) + try { + const response = await verificationsApi.getVerificationById(verification.id) + setSelectedVerification(response.data.data) + } catch (error) { + console.error('Failed to load verification detail:', error) + setSelectedVerification(null) + } finally { + setDetailLoading(false) + } + } + + const handleOpenReviewDialog = () => { + setRejectReason('') + setReviewDialogOpen(true) + } + + const handleReview = async (approve: boolean) => { + if (!selectedVerification) return + + if (!approve && !rejectReason.trim()) { + alert('请填写拒绝原因') + return + } + + setReviewLoading(true) + try { + await verificationsApi.reviewVerification(selectedVerification.id, { + approve, + reject_reason: approve ? undefined : rejectReason, + }) + setReviewDialogOpen(false) + setDetailOpen(false) + loadVerifications() + } catch (error) { + console.error('Failed to review verification:', error) + alert('审核失败') + } finally { + setReviewLoading(false) + } + } + + const handleSearch = () => { + setPage(1) + loadVerifications() + } + + const handleRefresh = () => { + loadVerifications() + } + + const handlePageChange = (newPage: number) => { + setPage(newPage) + } + + const totalPages = Math.ceil(total / pageSize) + + const getInitials = (name: string) => { + return name.slice(0, 2).toUpperCase() + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + + return ( +
+
+
+

身份认证管理

+

审核用户身份认证申请

+
+ +
+ + + + 筛选条件 + 根据条件筛选认证申请 + + +
+
+ setKeyword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="max-w-sm" + /> + +
+ +
+
+
+ + + + 认证申请列表 + 共 {total} 条申请 + + + + + + 用户 + 身份类型 + 真实姓名 + 学号/工号 + 部门 + 状态 + 提交时间 + 操作 + + + + {loading ? ( + + + +

加载中...

+
+
+ ) : verifications.length === 0 ? ( + + + 暂无数据 + + + ) : ( + verifications.map((verification) => ( + handleViewDetail(verification)} + > + +
+ + + {getInitials(verification.nickname)} + +
+

{verification.nickname}

+

@{verification.username}

+
+
+
+ + {identityText[verification.identity] || verification.identity} + + {verification.real_name} + {verification.student_id || verification.employee_id || '-'} + {verification.department || '-'} + + + {statusText[verification.status]} + + + {formatDate(verification.created_at)} + e.stopPropagation()}> + + +
+ )) + )} +
+
+ + {totalPages > 1 && ( +
+ +
+ )} +
+
+ + {/* 详情对话框 */} + + + + 认证详情 + 查看用户认证申请详情 + + {detailLoading ? ( +
+ +
+ ) : selectedVerification ? ( +
+
+ + + {getInitials(selectedVerification.nickname)} + +
+

{selectedVerification.nickname}

+

@{selectedVerification.username}

+
+ + {statusText[selectedVerification.status]} + +
+ +
+
+

身份类型

+

{identityText[selectedVerification.identity] || selectedVerification.identity}

+
+
+

真实姓名

+

{selectedVerification.real_name}

+
+ {selectedVerification.student_id && ( +
+

学号

+

{selectedVerification.student_id}

+
+ )} + {selectedVerification.employee_id && ( +
+

工号

+

{selectedVerification.employee_id}

+
+ )} + {selectedVerification.department && ( +
+

部门/院系

+

{selectedVerification.department}

+
+ )} +
+

证明材料

+
+ 证明材料 +
+
+ {selectedVerification.reviewed_at && ( + <> +
+

审核时间

+

{formatDate(selectedVerification.reviewed_at)}

+
+ {selectedVerification.reviewer_name && ( +
+

审核人

+

{selectedVerification.reviewer_name}

+
+ )} + + )} + {selectedVerification.reject_reason && ( +
+

拒绝原因

+

{selectedVerification.reject_reason}

+
+ )} +
+ + {selectedVerification.status === 'pending' && ( +
+ +
+ )} +
+ ) : ( +

加载失败

+ )} +
+
+ + {/* 审核对话框 */} + + + + 审核认证申请 + 请选择通过或拒绝该认证申请 + +
+
+

拒绝原因(拒绝时必填)

+