feat(verifications): add identity verification management for admin panel
Some checks failed
Admin CI / build-and-push-web (push) Failing after 6m11s
Some checks failed
Admin CI / build-and-push-web (push) Failing after 6m11s
Add new verifications module including API endpoints for retrieving and reviewing user identity verifications, along with corresponding admin page, routing, and sidebar navigation entry.
This commit is contained in:
@@ -18,3 +18,5 @@ export { subjectsApi, materialsApi } from './materials'
|
|||||||
export type { MaterialSubject, MaterialFile, MaterialFileType, SubjectUpsertRequest, MaterialCreateRequest, MaterialUpdateRequest, MaterialQueryParams } from './materials'
|
export type { MaterialSubject, MaterialFile, MaterialFileType, SubjectUpsertRequest, MaterialCreateRequest, MaterialUpdateRequest, MaterialQueryParams } from './materials'
|
||||||
export { reportsApi } from './reports'
|
export { reportsApi } from './reports'
|
||||||
export type { ReportListItem, ReportDetail, ReportQueryParams, ReportStatus, ReportTargetType, HandleReportRequest, BatchHandleRequest, BatchHandleResponse } 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'
|
||||||
|
|||||||
53
src/api/verifications.ts
Normal file
53
src/api/verifications.ts
Normal file
@@ -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<ApiResponse<PaginatedResponse<VerificationListItem>>>('/admin/verifications', { params }),
|
||||||
|
|
||||||
|
getVerificationById: (id: string) =>
|
||||||
|
apiClient.get<ApiResponse<VerificationDetail>>(`/admin/verifications/${id}`),
|
||||||
|
|
||||||
|
reviewVerification: (id: string, data: ReviewVerificationRequest) =>
|
||||||
|
apiClient.put<ApiResponse<VerificationDetail>>(`/admin/verifications/${id}/review`, data),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default verificationsApi
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Layers,
|
Layers,
|
||||||
Flag,
|
Flag,
|
||||||
|
ShieldCheck,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
@@ -75,6 +76,12 @@ const menuItems: MenuItem[] = [
|
|||||||
icon: Flag,
|
icon: Flag,
|
||||||
roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR],
|
roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '身份认证',
|
||||||
|
path: '/verifications',
|
||||||
|
icon: ShieldCheck,
|
||||||
|
roles: [ROLES.SUPER_ADMIN, ROLES.ADMIN],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '频道管理',
|
title: '频道管理',
|
||||||
path: '/channels',
|
path: '/channels',
|
||||||
|
|||||||
457
src/pages/Verifications.tsx
Normal file
457
src/pages/Verifications.tsx
Normal file
@@ -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<VerificationStatus, string> = {
|
||||||
|
not_submitted: 'bg-gray-500',
|
||||||
|
pending: 'bg-yellow-500',
|
||||||
|
approved: 'bg-green-500',
|
||||||
|
rejected: 'bg-red-500',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText: Record<VerificationStatus, string> = {
|
||||||
|
not_submitted: '未提交',
|
||||||
|
pending: '待审核',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityText: Record<string, string> = {
|
||||||
|
student: '学生',
|
||||||
|
teacher: '教师',
|
||||||
|
staff: '职工',
|
||||||
|
alumni: '校友',
|
||||||
|
external: '外部人员',
|
||||||
|
general: '普通用户',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Verifications() {
|
||||||
|
const [verifications, setVerifications] = useState<VerificationListItem[]>([])
|
||||||
|
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<string>('all')
|
||||||
|
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
|
const [selectedVerification, setSelectedVerification] = useState<VerificationDetail | null>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">身份认证管理</h1>
|
||||||
|
<p className="text-muted-foreground">审核用户身份认证申请</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleRefresh} disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>筛选条件</CardTitle>
|
||||||
|
<CardDescription>根据条件筛选认证申请</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="flex gap-2 flex-1 min-w-[300px]">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索用户名、真实姓名..."
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<Button variant="outline" onClick={handleSearch}>
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder="状态筛选" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
<SelectItem value="pending">待审核</SelectItem>
|
||||||
|
<SelectItem value="approved">已通过</SelectItem>
|
||||||
|
<SelectItem value="rejected">已拒绝</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>认证申请列表</CardTitle>
|
||||||
|
<CardDescription>共 {total} 条申请</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>用户</TableHead>
|
||||||
|
<TableHead>身份类型</TableHead>
|
||||||
|
<TableHead>真实姓名</TableHead>
|
||||||
|
<TableHead>学号/工号</TableHead>
|
||||||
|
<TableHead>部门</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead>提交时间</TableHead>
|
||||||
|
<TableHead className="w-[100px]">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mx-auto text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground mt-2">加载中...</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : verifications.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
verifications.map((verification) => (
|
||||||
|
<TableRow
|
||||||
|
key={verification.id}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => handleViewDetail(verification)}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src={verification.avatar} alt={verification.nickname} />
|
||||||
|
<AvatarFallback>{getInitials(verification.nickname)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{verification.nickname}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">@{verification.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{identityText[verification.identity] || verification.identity}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{verification.real_name}</TableCell>
|
||||||
|
<TableCell>{verification.student_id || verification.employee_id || '-'}</TableCell>
|
||||||
|
<TableCell>{verification.department || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={statusColors[verification.status]}>
|
||||||
|
{statusText[verification.status]}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDate(verification.created_at)}</TableCell>
|
||||||
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewDetail(verification)}
|
||||||
|
title="查看详情"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<PaginationNavigator
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 详情对话框 */}
|
||||||
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||||
|
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>认证详情</DialogTitle>
|
||||||
|
<DialogDescription>查看用户认证申请详情</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{detailLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : selectedVerification ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-12 w-12">
|
||||||
|
<AvatarImage src={selectedVerification.avatar} alt={selectedVerification.nickname} />
|
||||||
|
<AvatarFallback>{getInitials(selectedVerification.nickname)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{selectedVerification.nickname}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">@{selectedVerification.username}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={`ml-auto ${statusColors[selectedVerification.status]}`}>
|
||||||
|
{statusText[selectedVerification.status]}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">身份类型</p>
|
||||||
|
<p className="font-medium">{identityText[selectedVerification.identity] || selectedVerification.identity}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">真实姓名</p>
|
||||||
|
<p className="font-medium">{selectedVerification.real_name}</p>
|
||||||
|
</div>
|
||||||
|
{selectedVerification.student_id && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">学号</p>
|
||||||
|
<p className="font-medium">{selectedVerification.student_id}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedVerification.employee_id && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">工号</p>
|
||||||
|
<p className="font-medium">{selectedVerification.employee_id}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedVerification.department && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-muted-foreground">部门/院系</p>
|
||||||
|
<p className="font-medium">{selectedVerification.department}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-muted-foreground mb-2">证明材料</p>
|
||||||
|
<div className="border rounded-lg p-2">
|
||||||
|
<img
|
||||||
|
src={selectedVerification.proof_materials}
|
||||||
|
alt="证明材料"
|
||||||
|
className="max-w-full h-auto rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedVerification.reviewed_at && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">审核时间</p>
|
||||||
|
<p className="font-medium">{formatDate(selectedVerification.reviewed_at)}</p>
|
||||||
|
</div>
|
||||||
|
{selectedVerification.reviewer_name && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">审核人</p>
|
||||||
|
<p className="font-medium">{selectedVerification.reviewer_name}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedVerification.reject_reason && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-muted-foreground">拒绝原因</p>
|
||||||
|
<p className="font-medium text-red-600">{selectedVerification.reject_reason}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedVerification.status === 'pending' && (
|
||||||
|
<div className="flex gap-2 pt-4">
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleOpenReviewDialog}
|
||||||
|
>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
审核认证
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-8">加载失败</p>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 审核对话框 */}
|
||||||
|
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>审核认证申请</DialogTitle>
|
||||||
|
<DialogDescription>请选择通过或拒绝该认证申请</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">拒绝原因(拒绝时必填)</p>
|
||||||
|
<Textarea
|
||||||
|
placeholder="请输入拒绝原因..."
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleReview(false)}
|
||||||
|
disabled={reviewLoading}
|
||||||
|
className="text-red-600 border-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{reviewLoading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <XCircle className="h-4 w-4 mr-2" />}
|
||||||
|
拒绝
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleReview(true)}
|
||||||
|
disabled={reviewLoading}
|
||||||
|
>
|
||||||
|
{reviewLoading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <CheckCircle className="h-4 w-4 mr-2" />}
|
||||||
|
通过
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -107,6 +107,7 @@ const Channels = lazy(() => import('@/pages/Channels'))
|
|||||||
const Reports = lazy(() => import('@/pages/Reports'))
|
const Reports = lazy(() => import('@/pages/Reports'))
|
||||||
const MaterialSubjects = lazy(() => import('@/pages/MaterialSubjects'))
|
const MaterialSubjects = lazy(() => import('@/pages/MaterialSubjects'))
|
||||||
const Materials = lazy(() => import('@/pages/Materials'))
|
const Materials = lazy(() => import('@/pages/Materials'))
|
||||||
|
const Verifications = lazy(() => import('@/pages/Verifications'))
|
||||||
const Forbidden = lazy(() => import('@/pages/Forbidden'))
|
const Forbidden = lazy(() => import('@/pages/Forbidden'))
|
||||||
const NotFound = lazy(() => import('@/pages/NotFound'))
|
const NotFound = lazy(() => import('@/pages/NotFound'))
|
||||||
|
|
||||||
@@ -235,6 +236,16 @@ export const router = createBrowserRouter([
|
|||||||
</AdminGuard>
|
</AdminGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'verifications',
|
||||||
|
element: (
|
||||||
|
<AdminGuard>
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
|
<Verifications />
|
||||||
|
</Suspense>
|
||||||
|
</AdminGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user