mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 19:16:24 +00:00
feat: 添加仪表板功能,整合统计数据并优化后台管理界面
This commit is contained in:
14
web/src/api/admin.ts
Normal file
14
web/src/api/admin.ts
Normal 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
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { Dashboard } from "@/components/console/dashboard";
|
||||
|
||||
export default function Page() {
|
||||
return <div>Console</div>;
|
||||
return <Dashboard />;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { PostManage } from "@/components/console/post-manage";
|
||||
|
||||
export default function Page() {
|
||||
return <div>文章管理</div>;
|
||||
return <PostManage />;
|
||||
}
|
@ -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>
|
||||
|
73
web/src/components/console/dashboard/index.tsx
Normal file
73
web/src/components/console/dashboard/index.tsx
Normal 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>
|
||||
}
|
@ -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
|
||||
}
|
||||
|
47
web/src/components/console/post-manage/index.tsx
Normal file
47
web/src/components/console/post-manage/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export interface PaginationParams {
|
||||
orderBy: OrderBy
|
||||
desc: boolean
|
||||
desc: boolean // 是否降序
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
Reference in New Issue
Block a user