mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-04 07:40:51 +00:00
feat(admin): add system moniter feat
- 新增监控仪表盘组件,用于展示性能指标和统计数据 - 重构布局组件,优化页面结构 - 更新侧边栏组件,添加监控菜单项- 添加使用 web vitals 的钩子函数,用于收集性能数据
This commit is contained in:
parent
4917bb4f23
commit
6d528f7757
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
116
src/components/admin/monitoring-dashboard.tsx
Normal file
116
src/components/admin/monitoring-dashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
80
src/hooks/use-web-vitals.ts
Normal file
80
src/hooks/use-web-vitals.ts
Normal 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';
|
||||
}
|
Loading…
Reference in New Issue
Block a user