mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 09:20:53 +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 { AdminSidebar } from "@/components/admin/sidebar";
|
||||||
import { Header } from "@/components/header";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
export default function AdminLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}) {
|
||||||
|
|
||||||
export default function AdminLayout({ children }: AdminLayoutProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex h-screen">
|
||||||
<AdminSidebar />
|
{/* 左侧边栏 */}
|
||||||
<div className="flex-1 flex flex-col">
|
<aside className="w-64 border-r">
|
||||||
{/*<Header />*/}
|
<AdminSidebar adminMenuActive={true} />
|
||||||
<main className="flex-1 p-6">
|
</aside>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
<div className="container py-6">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { AdminSidebar } from "@/components/admin/sidebar";
|
// import { AdminSidebar } from "@/components/admin/sidebar";
|
||||||
import { Header } from "@/components/header";
|
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 AdminLayout from "@/app/(app)/admin/layout";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
@ -13,15 +13,13 @@ export default async function AdminPage(): Promise<ReactElement> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="flex min-h-screen">
|
<div className="h-full">
|
||||||
<AdminSidebar />
|
|
||||||
<div className="flex-1 flex flex-col">
|
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 p-6">
|
<main className="container py-6 h-full">
|
||||||
<AdminDashboard userCount={userCount} problemCount={problemCount} />
|
{/* 监控仪表盘替代原有仪表盘 */}
|
||||||
|
<MonitoringDashboard userCount={userCount} problemCount={problemCount} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
</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"
|
"use client"
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
// 仅保留实际使用的组件导入
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { NavMain } from "@/components/nav-main";
|
import { NavMain } from "@/components/nav-main";
|
||||||
import { NavProjects } from "@/components/nav-projects";
|
|
||||||
import { NavUser } from "@/components/nav-user";
|
import { NavUser } from "@/components/nav-user";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@ -15,6 +15,9 @@ import {
|
|||||||
Settings2,
|
Settings2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// 添加缺失的AppSidebar导入
|
||||||
|
import { AppSidebar as BaseAppSidebar } from "@/components/app-sidebar";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
@ -27,13 +30,6 @@ const teams = [
|
|||||||
|
|
||||||
|
|
||||||
const adminData = {
|
const adminData = {
|
||||||
// teams: [
|
|
||||||
// {
|
|
||||||
// name: "Admin Team",
|
|
||||||
// logo: GalleryVerticalEnd,
|
|
||||||
// plan: "Enterprise",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
@ -69,9 +65,10 @@ const adminData = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
|
export function AdminSidebar() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [userAvatar, setUserAvatar] = useState("");
|
const [userAvatar, setUserAvatar] = useState("");
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserAvatar = async () => {
|
const fetchUserAvatar = async () => {
|
||||||
@ -99,20 +96,48 @@ export const AdminSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>)
|
|||||||
email: session?.user?.email || "admin@example.com",
|
email: session?.user?.email || "admin@example.com",
|
||||||
avatar: userAvatar
|
avatar: userAvatar
|
||||||
};
|
};
|
||||||
|
const adminNavItems = adminData.navMain.map((item) => ({
|
||||||
|
...item,
|
||||||
|
items: item.items.map((subItem) => ({
|
||||||
|
...subItem,
|
||||||
|
active: subItem.url === pathname,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<BaseAppSidebar user={user}>
|
||||||
{/*<SidebarHeader>*/}
|
{/*<SidebarHeader>*/}
|
||||||
{/* <TeamSwitcher teams={adminData.teams} />*/}
|
{/* <TeamSwitcher teams={adminData.teams} />*/}
|
||||||
{/*</SidebarHeader>*/}
|
{/*</SidebarHeader>*/}
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={adminData.navMain} />
|
<NavMain items={adminNavItems} />
|
||||||
<NavProjects projects={adminData.projects} />
|
{/* 添加监控菜单项 */}
|
||||||
|
<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>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={user} />
|
<NavUser user={user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<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