Files
sundynix-site/admin/src/pages/dashboard.tsx
T
Blizzard 6334dccf4a feat(admin): 侧边栏布局 + 仪表盘 + 全局隐藏滚动条
- 全局隐藏滚动条(web + admin),保留正常滚动
- admin 顶栏改侧边栏导航(仪表盘/文章),响应式移动端抽屉
- 路由重构:首页→仪表盘,文章列表移到 /posts
- 仪表盘页:文章总数/已发布/草稿统计卡片 + 最近更新列表
- 后端 GET /api/admin/stats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:35:05 +08:00

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>
)
}