240 lines
8.5 KiB
TypeScript
240 lines
8.5 KiB
TypeScript
|
|
'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>
|
||
|
|
);
|
||
|
|
}
|