import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FileText, Flag, MessageSquare, Link2, Activity } 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, AdminDashboardTraffic } from '../../api/types'; import { cn } from '@/lib/utils'; import CommunitySupportStrip from '../../components/admin/CommunitySupportStrip'; function formatDashNum(n: number) { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 10_000) return `${(n / 1000).toFixed(1)}k`; return String(n ?? 0); } function pvTrendLabel(today: number, yesterday: number) { if (yesterday <= 0) { return today > 0 ? '昨日无数据' : '较昨日 —'; } const delta = ((today - yesterday) / yesterday) * 100; if (Math.abs(delta) < 0.5) return '与昨日持平'; const abs = Math.abs(delta).toFixed(0); return delta > 0 ? `较昨日 ↑ ${abs}%` : `较昨日 ↓ ${abs}%`; } function trendTone(today: number, yesterday: number): 'up' | 'down' | 'flat' { if (yesterday <= 0) return 'flat'; const delta = today - yesterday; if (delta > 0) return 'up'; if (delta < 0) return 'down'; return 'flat'; } export default function AdminDashboardPage() { const nav = useNavigate(); const { ready } = useAdminGuard(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { if (!ready) return; api.adminDashboard() .then(setData) .finally(() => setLoading(false)); }, [ready]); if (!ready || loading) { return
; } if (!data) return null; const pendingPosts = data.pending_posts ?? 0; const pendingComments = data.pending_comments ?? 0; const pendingReports = data.pending_reports ?? 0; const pendingFriendLinks = data.pending_friend_links ?? 0; const pendingTotal = pendingPosts + pendingComments + pendingReports + pendingFriendLinks; const traffic: AdminDashboardTraffic = data.traffic ?? { enabled: false, today_pv: 0, today_uv: 0, yesterday_pv: 0, total_pv: 0, }; 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, }, { key: 'links', label: '待审友链', count: pendingFriendLinks, hint: '友链申请', to: '/admin/links', icon: Link2, }, ]; const scale = [ { label: '注册用户', value: data.users }, { label: '帖子总数', value: data.posts }, { label: '板块数量', value: data.boards }, { label: '评论总数', value: data.comments }, ]; const tone = trendTone(traffic.today_pv, traffic.yesterday_pv); return (

仪表盘

{pendingTotal > 0 && ( {pendingTotal} 项待处理 )}

处理待办,并一眼看到今日访问

今日访问

{!traffic.enabled ? (

访问采集尚未开启,流量统计暂不可用。

) : ( <>
{formatDashNum(traffic.today_pv)}
今日访问 (PV) 今日访客 (UV) {formatDashNum(traffic.today_uv)}
{pvTrendLabel(traffic.today_pv, traffic.yesterday_pv)}

累计访问 {formatDashNum(traffic.total_pv)} (受日志保留天数影响)

)}

待处理

{pendingTotal === 0 && ( 暂无积压 )}
{queues.map((q) => { const Icon = q.icon; const hasWork = q.count > 0; return ( ); })}

内容规模

{scale.map((s) => (
{formatDashNum(s.value)}
{s.label}
))}
最新帖子
{data.recent_posts.map((p) => ( ))}
ID 标题 作者 标记 时间
{p.id} {p.user?.id ? ( ) : '—'} {p.featured ? 推荐 : null} {p.pinned ? 全局置顶 : null} {p.board_pinned ? 板块置顶 : null} {!p.featured && !p.pinned && !p.board_pinned ? '—' : null} {new Date(p.created_at).toLocaleString('zh-CN')}
{data.recent_posts.length === 0 &&
暂无帖子
}
); }