feat(admin): add system moniter feat

- 新增监控仪表盘组件,用于展示性能指标和统计数据
- 重构布局组件,优化页面结构
- 更新侧边栏组件,添加监控菜单项- 添加使用 web vitals 的钩子函数,用于收集性能数据
This commit is contained in:
fly6516 2025-06-16 13:27:08 +08:00
parent 4917bb4f23
commit 6d528f7757
5 changed files with 262 additions and 40 deletions

View File

@ -1,21 +1,24 @@
import { ReactNode } from "react";
import { AdminSidebar } from "@/components/admin/sidebar";
import { Header } from "@/components/header";
import { ReactNode } from "react";
interface AdminLayoutProps {
export default function AdminLayout({
children
}: {
children: ReactNode;
}
export default function AdminLayout({ children }: AdminLayoutProps) {
}) {
return (
<div className="flex min-h-screen">
<AdminSidebar />
<div className="flex-1 flex flex-col">
{/*<Header />*/}
<main className="flex-1 p-6">
<div className="flex h-screen">
{/* 左侧边栏 */}
<aside className="w-64 border-r">
<AdminSidebar adminMenuActive={true} />
</aside>
{/* 主要内容区域 */}
<main className="flex-1 overflow-auto">
<div className="container py-6">
{children}
</main>
</div>
</main>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { AdminSidebar } from "@/components/admin/sidebar";
// import { AdminSidebar } from "@/components/admin/sidebar";
import { Header } from "@/components/header";
import { AdminDashboard } from "@/components/admin/dashboard";
import { MonitoringDashboard } from "@/components/admin/monitoring-dashboard";
import AdminLayout from "@/app/(app)/admin/layout";
import type { ReactElement } from "react";
import prisma from "@/lib/prisma";
@ -13,15 +13,13 @@ export default async function AdminPage(): Promise<ReactElement> {
return (
<AdminLayout>
<div className="flex min-h-screen">
<AdminSidebar />
<div className="flex-1 flex flex-col">
<div className="h-full">
<Header />
<main className="flex-1 p-6">
<AdminDashboard userCount={userCount} problemCount={problemCount} />
<main className="container py-6 h-full">
{/* 监控仪表盘替代原有仪表盘 */}
<MonitoringDashboard userCount={userCount} problemCount={problemCount} />
</main>
</div>
</div>
</AdminLayout>
);
}

View File

@ -0,0 +1,116 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useWebVitals } from "@/hooks/use-web-vitals";
import { useEffect } from "react";
interface MonitoringDashboardProps {
userCount: number;
problemCount: number;
}
export function MonitoringDashboard({
userCount,
problemCount
}: MonitoringDashboardProps) {
const { vitals, loading } = useWebVitals();
// 添加日志输出性能指标读取结果(已注释)
useEffect(() => {
if (!loading) {
// console.log('[仪表盘] 当前显示指标:', {
// lcp: vitals.lcp ? `${vitals.lcp.value.toFixed(2)}秒(${vitals.lcp.rating})` : '未收集',
// fid: vitals.fid ? `${vitals.fid.value.toFixed(2)}毫秒(${vitals.fid.rating})` : '未收集',
// cls: vitals.cls ? `${vitals.cls.value.toFixed(2)}(${vitals.cls.rating})` : '未收集',
// fcp: vitals.fcp ? `${vitals.fcp.value.toFixed(2)}秒(${vitals.fcp.rating})` : '未收集'
// });
}
}, [loading, vitals]);
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* 用户统计 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{userCount}</div>
<p className="text-xs text-muted-foreground">
+20%
</p>
</CardContent>
</Card>
{/* 题目统计 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
线
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<rect width="20" height="14" x="2" y="7" rx="2"></rect>
<path d="M16 19h6"></path>
<path d="M22 15h-6"></path>
<path d="M22 11h-6"></path>
<path d="M22 7h-6"></path>
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{problemCount}</div>
<p className="text-xs text-muted-foreground">
+15%
</p>
</CardContent>
</Card>
{/* 性能指标卡片 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-2 gap-x-4 gap-y-2">
<dt className="font-medium">LCP:</dt>
<dd>{loading ? '加载中...' : vitals.lcp ? `${vitals.lcp.value.toFixed(2)} 秒(${vitals.lcp.rating})` : '暂无数据'}</dd>
<dt className="font-medium">FID:</dt>
<dd>{loading ? '加载中...' : vitals.fid ? `${vitals.fid.value.toFixed(2)} 毫秒(${vitals.fid.rating})` : '暂无数据'}</dd>
<dt className="font-medium">CLS:</dt>
<dd>{loading ? '加载中...' : vitals.cls ? `${vitals.cls.value.toFixed(2)}(${vitals.cls.rating})` : '暂无数据'}</dd>
<dt className="font-medium">FCP:</dt>
<dd>{loading ? '加载中...' : vitals.fcp ? `${vitals.fcp.value.toFixed(2)} 秒(${vitals.fcp.rating})` : '暂无数据'}</dd>
</dl>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,13 +1,13 @@
"use client"
import { useSession } from "next-auth/react";
import { usePathname } from "next/navigation";
// 仅保留实际使用的组件导入
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarRail,
} from "@/components/ui/sidebar";
import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavUser } from "@/components/nav-user";
import {
Command,
@ -15,6 +15,9 @@ import {
Settings2,
} from "lucide-react";
// 添加缺失的AppSidebar导入
import { AppSidebar as BaseAppSidebar } from "@/components/app-sidebar";
import { useEffect, useState } from "react";
import { PrismaClient } from "@prisma/client";
@ -27,13 +30,6 @@ const teams = [
const adminData = {
// teams: [
// {
// name: "Admin Team",
// logo: GalleryVerticalEnd,
// plan: "Enterprise",
// },
// ],
navMain: [
{
title: "Dashboard",
@ -69,9 +65,10 @@ const adminData = {
]
};
export const AdminSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
export function AdminSidebar() {
const { data: session } = useSession();
const [userAvatar, setUserAvatar] = useState("");
const pathname = usePathname();
useEffect(() => {
const fetchUserAvatar = async () => {
@ -99,20 +96,48 @@ export const AdminSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>)
email: session?.user?.email || "admin@example.com",
avatar: userAvatar
};
const adminNavItems = adminData.navMain.map((item) => ({
...item,
items: item.items.map((subItem) => ({
...subItem,
active: subItem.url === pathname,
})),
}));
return (
<Sidebar collapsible="icon" {...props}>
<BaseAppSidebar user={user}>
{/*<SidebarHeader>*/}
{/* <TeamSwitcher teams={adminData.teams} />*/}
{/*</SidebarHeader>*/}
<SidebarContent>
<NavMain items={adminData.navMain} />
<NavProjects projects={adminData.projects} />
<NavMain items={adminNavItems} />
{/* 添加监控菜单项 */}
<div className="py-2">
<a
href="/admin/monitoring"
className="flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:bg-muted"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4"
>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" x2="12" y1="22.08" y2="12"></line>
</svg>
</a>
</div>
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
</BaseAppSidebar>
);
};
}

View File

@ -0,0 +1,80 @@
'use client';
import { useReportWebVitals } from 'next/web-vitals';
import { useEffect, useState } from 'react';
export interface WebVital {
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
}
interface WebVitals {
lcp?: WebVital;
fid?: WebVital;
cls?: WebVital;
fcp?: WebVital;
}
export function useWebVitals() {
const [vitals, setVitals] = useState<WebVitals>({
lcp: undefined,
fid: undefined,
cls: undefined,
fcp: undefined,
});
const [loading, setLoading] = useState(true);
// 添加详细日志记录指标接收(已注释)
useReportWebVitals((metric) => {
// console.log(`[Web Vitals] 接收指标: ${metric.name}`, {
// value: metric.value,
// rating: getRating(metric)
// });
setVitals((prev) => ({
...prev,
[metric.name.toLowerCase()]: {
value: metric.value,
rating: getRating(metric),
},
}));
});
// 添加日志记录最终指标状态(已注释)
useEffect(() => {
if (!loading) {
// console.log('[Web Vitals] 当前完整指标:', {
// lcp: vitals.lcp?.value,
// fid: vitals.fid?.value,
// cls: vitals.cls?.value,
// fcp: vitals.fcp?.value
// });
}
}, [loading, vitals]);
// 使用useEffect处理加载状态
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 2000);
return () => {
clearTimeout(timer);
};
}, []);
return { vitals, loading };
}
// 添加更具体的类型定义
interface Metric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
}
function getRating(metric: Metric): 'good' | 'needs-improvement' | 'poor' {
if (metric.rating === 'good') return 'good';
if (metric.rating === 'needs-improvement') return 'needs-improvement';
return 'poor';
}