feat: 添加仪表板功能,整合统计数据并优化后台管理界面

This commit is contained in:
2025-09-24 12:09:16 +08:00
parent 2bcaad716d
commit 636b4d5ea0
13 changed files with 430 additions and 220 deletions

14
web/src/api/admin.ts Normal file
View File

@ -0,0 +1,14 @@
import { BaseResponse } from "@/models/resp"
import axiosClient from "./client"
export interface DashboardResp {
totalUsers: number
totalPosts: number
totalComments: number
totalViews: number
}
export async function getDashboard(): Promise<BaseResponse<DashboardResp>> {
const res = await axiosClient.get<BaseResponse<DashboardResp>>('/admin/dashboard')
return res.data
}

View File

@ -1,3 +1,5 @@
import { Dashboard } from "@/components/console/dashboard";
export default function Page() {
return <div>Console</div>;
return <Dashboard />;
}

View File

@ -1,3 +1,5 @@
import { PostManage } from "@/components/console/post-manage";
export default function Page() {
return <div></div>;
return <PostManage />;
}

View File

@ -18,11 +18,12 @@ export function CurrentLogged() {
const { user, logout } = useAuth();
const handleLoggedContinue = () => {
console.log("continue to", redirectBack);
router.push(redirectBack);
}
const handleLogOut = () => {
logout();
logout();
}
if (!user) return null;
@ -30,8 +31,8 @@ export function CurrentLogged() {
<div className="mb-4">
<SectionDivider className="mb-4">{t("currently_logged_in")}</SectionDivider>
<div className="flex justify-evenly items-center border border-border rounded-md p-2">
<div className="flex gap-4 items-center cursor-pointer">
<div onClick={handleLoggedContinue} className="flex gap-2 justify-center items-center ">
<div onClick={handleLoggedContinue} className="flex gap-4 items-center cursor-pointer">
<div className="flex gap-2 justify-center items-center ">
<Avatar className="h-10 w-10 rounded-full">
<AvatarImage src={getGravatarFromUser({ user })} alt={user.username} />
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback>

View File

@ -0,0 +1,73 @@
"use client"
import { getDashboard, DashboardResp } from "@/api/admin"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Eye, MessageCircle, Newspaper, Users } from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { path } from "../data"
import Link from "next/link"
export function Dashboard() {
return (
<div className="">
<DataOverview />
</div>
)
}
function DataOverview() {
const data = [
{
"key": "totalPosts",
"label": "Total Posts",
"icon": Newspaper,
"url": path.post
},
{
"key": "totalUsers",
"label": "Total Users",
"icon": Users,
"url": path.user
},
{
"key": "totalComments",
"label": "Total Comments",
"icon": MessageCircle,
"url": path.comment
},
{
"key": "totalViews",
"label": "Total Views",
"icon": Eye,
"url": path.file
},
]
const [fetchData, setFetchData] = useState<DashboardResp | null>(null);
useEffect(() => {
getDashboard().then(res => {
setFetchData(res.data);
}).catch(err => {
toast.error(err.message || "Failed to fetch dashboard data");
});
}, [])
if (!fetchData) return <div>Loading...</div>
return <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{data.map(item => (
<Link key={item.key} href={item.url}>
<Card key={item.key} className="p-4">
<CardHeader className="pb-2 text-lg font-medium">
<CardDescription>{item.label}</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl text-primary">
<item.icon className="inline mr-2" />
{(fetchData as any)[item.key]}
</CardTitle>
</CardHeader>
</Card>
</Link>
))}
</div>
}

View File

@ -11,41 +11,53 @@ export interface SidebarItem {
permission: ({ user }: { user: User }) => boolean;
}
export const path = {
dashboard: "/console",
post: "/console/post",
comment: "/console/comment",
file: "/console/file",
user: "/console/user",
global: "/console/global",
userProfile: "/console/user-profile",
userSecurity: "/console/user-security",
userPreference: "/console/user-preference",
}
export const sidebarData: { navMain: SidebarItem[]; navUserCenter: SidebarItem[] } = {
navMain: [
{
title: "dashboard.title",
url: "/console",
url: path.dashboard,
icon: Gauge,
permission: isAdmin
},
{
title: "post.title",
url: "/console/post",
url: path.post,
icon: Newspaper,
permission: isEditor
},
{
title: "comment.title",
url: "/console/comment",
url: path.comment,
icon: MessageCircle,
permission: isEditor
},
{
title: "file.title",
url: "/console/file",
url: path.file,
icon: Folder,
permission: () => true
},
{
title: "user.title",
url: "/console/user",
url: path.user,
icon: Users,
permission: isAdmin
},
{
title: "global.title",
url: "/console/global",
url: path.global,
icon: Settings,
permission: isAdmin
},
@ -53,19 +65,19 @@ export const sidebarData: { navMain: SidebarItem[]; navUserCenter: SidebarItem[]
navUserCenter: [
{
title: "user_profile.title",
url: "/console/user-profile",
url: path.userProfile,
icon: UserPen,
permission: () => true
},
{
title: "user_security.title",
url: "/console/user-security",
url: path.userSecurity,
icon: ShieldCheck,
permission: () => true
},
{
title: "user-preference.title",
url: "/console/user-preference",
url: path.userPreference,
icon: Palette,
permission: () => true
}

View File

@ -0,0 +1,47 @@
"use client";
import { listPosts } from "@/api/post";
import { Separator } from "@/components/ui/separator";
import config from "@/config";
import { OrderBy } from "@/models/common";
import { Post } from "@/models/post"
import { useEffect, useState } from "react";
export function PostManage() {
const [posts, setPosts] = useState<Post[]>([]);
const [orderBy, setOrderBy] = useState<OrderBy>(OrderBy.CreatedAt);
const [desc, setDesc] = useState<boolean>(true);
const [page, setPage] = useState(1);
useEffect(() => {
listPosts({ page, size: config.postsPerPage, orderBy, desc }).then(res => {
setPosts(res.data.posts);
});
}, [page, orderBy, desc]);
return <div>
{posts.map(post => <PostItem key={post.id} post={post} />)}
</div>;
}
function PostItem({ post }: { post: Post }) {
return (
<div>
<div className="flex w-full items-center gap-3 py-2">
{/* left */}
<div>
<div className="text-sm font-medium">
{post.title}
</div>
<div>
<span className="text-xs text-muted-foreground">ID: {post.id}</span>
<span className="mx-2 text-xs text-muted-foreground">|</span>
<span className="text-xs text-muted-foreground">Created At: {new Date(post.createdAt).toLocaleDateString()}</span>
<span className="mx-2 text-xs text-muted-foreground">|</span>
<span className="text-xs text-muted-foreground">Updated At: {new Date(post.updatedAt).toLocaleDateString()}</span>
</div>
</div>
</div>
<Separator className="flex-1" />
</div>
)
}

View File

@ -1,6 +1,6 @@
export interface PaginationParams {
orderBy: OrderBy
desc: boolean
desc: boolean // 是否降序
page: number
size: number
}