6334dccf4a
- 全局隐藏滚动条(web + admin),保留正常滚动 - admin 顶栏改侧边栏导航(仪表盘/文章),响应式移动端抽屉 - 路由重构:首页→仪表盘,文章列表移到 /posts - 仪表盘页:文章总数/已发布/草稿统计卡片 + 最近更新列表 - 后端 GET /api/admin/stats Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
3.9 KiB
TypeScript
115 lines
3.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { CheckCircle2, FileEdit, FileText, Plus } from 'lucide-react'
|
|
import { fetchStats, type Stats } from '@/api/posts'
|
|
|
|
export default function DashboardPage() {
|
|
const [stats, setStats] = useState<Stats | null>(null)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
fetchStats()
|
|
.then((s) => {
|
|
if (!cancelled) setStats(s)
|
|
})
|
|
.catch((e: Error) => {
|
|
if (!cancelled) setError(e.message)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const cards = [
|
|
{ label: '文章总数', value: stats?.total, icon: FileText },
|
|
{ label: '已发布', value: stats?.published, icon: CheckCircle2 },
|
|
{ label: '草稿', value: stats?.draft, icon: FileEdit },
|
|
]
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-6 flex items-center justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-[22px] font-[650] tracking-[-0.01em]">仪表盘</h1>
|
|
<p className="mt-1 text-[13.5px] text-ink-2">内容概览</p>
|
|
</div>
|
|
<Link
|
|
to="/posts/new"
|
|
className="flex shrink-0 items-center gap-1.5 rounded-lg bg-accent px-4 py-2 text-[13.5px] font-medium text-ground transition-opacity hover:opacity-90"
|
|
>
|
|
<Plus className="size-4" /> 新建文章
|
|
</Link>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="mb-4 rounded-lg border border-hairline bg-surface px-4 py-3 text-[13.5px] text-ink-2">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
{cards.map((c) => (
|
|
<div
|
|
key={c.label}
|
|
className="rounded-xl border border-hairline bg-surface p-5"
|
|
>
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<span className="text-[13px] text-ink-2">{c.label}</span>
|
|
<c.icon className="size-[18px] text-ink-3" />
|
|
</div>
|
|
<div className="text-[28px] font-[650] tabular-nums">
|
|
{c.value ?? '—'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-8">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h2 className="text-[15px] font-semibold">最近更新</h2>
|
|
<Link
|
|
to="/posts"
|
|
className="font-mono text-[12.5px] text-accent-ink hover:underline"
|
|
>
|
|
全部文章 →
|
|
</Link>
|
|
</div>
|
|
<div className="overflow-hidden rounded-xl border border-hairline bg-surface">
|
|
{stats && stats.recent.length > 0 ? (
|
|
stats.recent.map((post) => (
|
|
<Link
|
|
key={post.id}
|
|
to={`/posts/${post.id}`}
|
|
className="flex items-center justify-between gap-4 border-b border-hairline px-5 py-3.5 transition-colors last:border-b-0 hover:bg-ground"
|
|
>
|
|
<span className="truncate text-[14px] font-medium">
|
|
{post.title}
|
|
</span>
|
|
<span className="flex shrink-0 items-center gap-3">
|
|
{post.published_at ? (
|
|
<span className="rounded-full bg-accent-soft px-2.5 py-0.5 text-[12px] text-accent-ink">
|
|
已发布
|
|
</span>
|
|
) : (
|
|
<span className="rounded-full bg-code px-2.5 py-0.5 text-[12px] text-ink-3">
|
|
草稿
|
|
</span>
|
|
)}
|
|
<span className="font-mono text-[12px] tabular-nums text-ink-3">
|
|
{post.updated_at.slice(0, 10)}
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
))
|
|
) : (
|
|
<p className="px-5 py-10 text-center text-[13.5px] text-ink-3">
|
|
{stats ? '还没有文章,点右上角新建' : '加载中…'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|