Files
jiang13-forum/frontend/src/pages/admin/AdminDashboardPage.tsx
freefire 6b0a1d4281 增加用户认证、等级、徽章与积分体系,并优化管理后台体验。
覆盖站长调账与积分解锁内容;后台按审核优先分组导航,仪表盘展示待办,用户管理改为成员目录式布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 02:42:18 +08:00

169 lines
5.4 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.
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FileText, Flag, MessageSquare } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { Badge } from '@/components/ui/badge';
import { api } from '../../api/client';
import { useAdminGuard } from '../../layouts/AdminLayout';
import type { AdminDashboard } from '../../api/types';
import { cn } from '@/lib/utils';
export default function AdminDashboardPage() {
const nav = useNavigate();
const { ready } = useAdminGuard();
const [data, setData] = useState<AdminDashboard | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!ready) return;
api.adminDashboard()
.then(setData)
.finally(() => setLoading(false));
}, [ready]);
if (!ready || loading) {
return <div className="flex justify-center py-16"><Spinner size="lg" /></div>;
}
if (!data) return null;
const pendingPosts = data.pending_posts ?? 0;
const pendingComments = data.pending_comments ?? 0;
const pendingReports = data.pending_reports ?? 0;
const pendingTotal = pendingPosts + pendingComments + pendingReports;
const stats = [
{ label: '注册用户', value: data.users },
{ label: '帖子总数', value: data.posts },
{ label: '板块数量', value: data.boards },
{ label: '评论总数', value: data.comments },
];
const queues = [
{
key: 'posts',
label: '待审帖子',
count: pendingPosts,
hint: '新帖与修改待审核',
to: '/admin/posts',
icon: FileText,
},
{
key: 'comments',
label: '待审评论',
count: pendingComments,
hint: '评论与回复待审核',
to: '/admin/comments',
icon: MessageSquare,
},
{
key: 'reports',
label: '待处理举报',
count: pendingReports,
hint: '用户举报需人工处理',
to: '/admin/reports',
icon: Flag,
},
];
return (
<div className="admin-page">
<div className="admin-page-head">
<h1></h1>
<p></p>
</div>
<section className="admin-queue-section" aria-label="待处理事项">
<div className="admin-section-label">
<span></span>
{pendingTotal > 0 ? (
<Badge variant="orange">{pendingTotal} </Badge>
) : (
<span className="admin-section-muted"></span>
)}
</div>
<div className="admin-queue-grid">
{queues.map(q => {
const Icon = q.icon;
const hasWork = q.count > 0;
return (
<button
key={q.key}
type="button"
className={cn('admin-queue-card', hasWork && 'has-work')}
onClick={() => nav(q.to)}
>
<div className="admin-queue-card-top">
<Icon size={18} aria-hidden />
<span className="admin-queue-count">{q.count}</span>
</div>
<div className="admin-queue-label">{q.label}</div>
<div className="admin-queue-hint">{q.hint}</div>
</button>
);
})}
</div>
</section>
<section className="admin-stat-section" aria-label="运行概览">
<div className="admin-section-label">
<span></span>
</div>
<div className="admin-stat-grid">
{stats.map(s => (
<div key={s.label} className="admin-stat-card">
<div className="admin-stat-value">{s.value}</div>
<div className="admin-stat-label">{s.label}</div>
</div>
))}
</div>
</section>
<div className="admin-card">
<div className="admin-card-head">
<span></span>
<button type="button" className="admin-text-link" onClick={() => nav('/admin/posts')}>
</button>
</div>
<table className="admin-table">
<thead>
<tr>
<th>ID</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{data.recent_posts.map(p => (
<tr key={p.id}>
<td>{p.id}</td>
<td>
<button type="button" className="admin-text-link" onClick={() => nav(`/post/${p.id}`)}>
{p.title}
</button>
</td>
<td>
{p.user?.id ? (
<button type="button" className="admin-text-link" onClick={() => nav(`/user/${p.user!.id}`)}>
{p.user.nickname}
</button>
) : '—'}
</td>
<td className="space-x-1">
{p.featured ? <Badge variant="orange"></Badge> : null}
{p.pinned ? <Badge variant="green"></Badge> : null}
{!p.featured && !p.pinned ? '—' : null}
</td>
<td>{new Date(p.created_at).toLocaleString('zh-CN')}</td>
</tr>
))}
</tbody>
</table>
{data.recent_posts.length === 0 && <div className="admin-empty"></div>}
</div>
</div>
);
}