240 lines
8.5 KiB
TypeScript
Raw Permalink Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import useSWR from 'swr';
import {
Newspaper,
Package,
MessageSquare,
Users,
Settings,
ArrowUpRight,
ExternalLink,
ChevronRight,
} from 'lucide-react';
import { AdminHeader } from '@/components/admin/AdminHeader';
import { Card, CardContent } from '@/components/ui/card';
import { adminApi } from '@/lib/admin-services';
import type { Message } from '@/lib/types';
export default function DashboardPage() {
const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);
const { data: productCount } = useSWR<number>(
hydrated ? 'dashboard:product-count' : null,
() =>
adminApi.productPaginate({ page: 1, pageSize: 1 }).then((r) => r.total),
);
const { data: newsCount } = useSWR<number>(
hydrated ? 'dashboard:news-count' : null,
() => adminApi.newsPaginate({ page: 1, pageSize: 1 }).then((r) => r.total),
);
const { data: teamCount } = useSWR<number>(
hydrated ? 'dashboard:team-count' : null,
() => adminApi.teamAll().then((r) => r.length),
);
const { data: message } = useSWR<{ total: number; unread: number }>(
hydrated ? 'dashboard:message-summary' : null,
() =>
adminApi
.messagePaginate({ page: 1, pageSize: 100 })
.then((r) => ({
total: r.total,
unread: r.list.filter((m: Message) => m.isRead === 0).length,
})),
);
const stats = [
{
href: '/admin/product',
label: '产品管理',
value: productCount,
icon: <Package className="h-5 w-5" />,
},
{
href: '/admin/news',
label: '新闻资讯',
value: newsCount,
icon: <Newspaper className="h-5 w-5" />,
},
{
href: '/admin/team',
label: '团队成员',
value: teamCount,
icon: <Users className="h-5 w-5" />,
},
{
href: '/admin/message',
label: '客户留言',
value: message?.total,
icon: <MessageSquare className="h-5 w-5" />,
badge:
message && message.unread > 0 ? `${message.unread} 未读` : undefined,
},
];
const quickActions = [
{
href: '/admin/product',
label: '发布产品方案',
hint: '新增 / 编辑产品',
icon: <Package className="h-5 w-5" />,
},
{
href: '/admin/news',
label: '发布新闻动态',
hint: '撰写行业资讯',
icon: <Newspaper className="h-5 w-5" />,
},
{
href: '/admin/team',
label: '维护团队',
hint: '成员信息',
icon: <Users className="h-5 w-5" />,
},
{
href: '/admin/site-config',
label: '站点配置',
hint: '基础信息 / 联系方式',
icon: <Settings className="h-5 w-5" />,
},
];
return (
<div>
<AdminHeader title="仪表盘" description="网站核心数据一览,快速进入各管理模块" />
{/* ===== 数据统计卡 ===== */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{stats.map((s) => (
<Stat key={s.href} {...s} />
))}
</div>
{/* ===== 主体两栏 ===== */}
<div className="mt-6 grid gap-6 lg:grid-cols-3">
{/* 快捷操作 */}
<div className="lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-slate-900"></h2>
<span className="text-xs text-slate-400"></span>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{quickActions.map((a) => (
<Link
key={a.href}
href={a.href}
className="group flex items-center gap-3 rounded-xl border border-slate-200/70 bg-white p-4 shadow-[0_1px_3px_rgba(15,23,42,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:border-brand-200 hover:shadow-[0_12px_28px_-12px_rgba(79,70,229,0.22)]"
>
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-slate-800 to-slate-900 text-white shadow-md shadow-slate-900/20 ring-1 ring-inset ring-white/10">
{a.icon}
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-slate-900">
{a.label}
</div>
<div className="mt-0.5 text-xs text-slate-400">{a.hint}</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-slate-300 transition-all group-hover:translate-x-0.5 group-hover:text-brand-500" />
</Link>
))}
</div>
</div>
{/* 系统信息 / 访问地址 */}
<div>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-slate-900">访</h2>
<span className="text-xs text-slate-400"></span>
</div>
<Card className="border-slate-200/70 shadow-[0_1px_3px_rgba(15,23,42,0.04)]">
<CardContent className="space-y-3 p-5">
<a
href="http://localhost:3000"
target="_blank"
rel="noreferrer"
className="group flex items-center gap-3 rounded-lg border border-slate-200/70 p-3 transition-colors hover:border-brand-200 hover:bg-brand-50/40"
>
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-50 text-brand-600">
<ExternalLink className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-slate-900">
</div>
<div className="truncate text-xs text-slate-400">
http://localhost:3000
</div>
</div>
<ArrowUpRight className="h-4 w-4 text-slate-300 group-hover:text-brand-500" />
</a>
<a
href="http://localhost:3001/api-docs"
target="_blank"
rel="noreferrer"
className="group flex items-center gap-3 rounded-lg border border-slate-200/70 p-3 transition-colors hover:border-brand-200 hover:bg-brand-50/40"
>
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-50 text-brand-600">
<ExternalLink className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-slate-900">
Swagger
</div>
<div className="truncate text-xs text-slate-400">
http://localhost:3001/api-docs
</div>
</div>
<ArrowUpRight className="h-4 w-4 text-slate-300 group-hover:text-brand-500" />
</a>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
function Stat({
href,
label,
value,
icon,
badge,
}: {
href: string;
label: string;
value: number | undefined;
icon: React.ReactNode;
badge?: string;
}) {
return (
<Link href={href}>
<Card className="group relative overflow-hidden border-slate-200/70 shadow-[0_1px_3px_rgba(15,23,42,0.04),0_8px_24px_-12px_rgba(79,70,229,0.1)] transition-all duration-300 hover:-translate-y-0.5 hover:border-brand-200 hover:shadow-[0_4px_12px_rgba(15,23,42,0.06),0_20px_40px_-16px_rgba(79,70,229,0.22)]">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<span className="flex h-11 w-11 items-center justify-center rounded-xl bg-brand-50 text-brand-600">
{icon}
</span>
{badge ? (
<span className="rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-600">
{badge}
</span>
) : (
<ArrowUpRight className="h-4 w-4 text-slate-300 transition-colors group-hover:text-brand-500" />
)}
</div>
<div className="mt-4 flex items-baseline gap-1">
<span className="text-3xl font-bold tabular-nums text-slate-900">
{value ?? '—'}
</span>
</div>
<div className="mt-1 text-sm text-slate-500">{label}</div>
</CardContent>
</Card>
</Link>
);
}