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

View File

@ -2,12 +2,13 @@ package v1
import ( import (
"context" "context"
"strconv"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
"github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/service" "github.com/snowykami/neo-blog/internal/service"
"github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/errs"
"github.com/snowykami/neo-blog/pkg/resps" "github.com/snowykami/neo-blog/pkg/resps"
"strconv"
) )
type AdminController struct { type AdminController struct {
@ -20,6 +21,15 @@ func NewAdminController() *AdminController {
} }
} }
func (cc *AdminController) GetDashboard(ctx context.Context, c *app.RequestContext) {
dashboardData, err := cc.service.GetDashboard()
if err != nil {
serviceErr := errs.AsServiceError(err)
resps.Custom(c, serviceErr.Code, err.Error(), nil)
}
resps.Ok(c, resps.Success, dashboardData)
}
func (cc *AdminController) CreateOidc(ctx context.Context, c *app.RequestContext) { func (cc *AdminController) CreateOidc(ctx context.Context, c *app.RequestContext) {
var adminCreateOidcReq dto.AdminOidcConfigDto var adminCreateOidcReq dto.AdminOidcConfigDto
if err := c.BindAndValidate(&adminCreateOidcReq); err != nil { if err := c.BindAndValidate(&adminCreateOidcReq); err != nil {

View File

@ -6,19 +6,24 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type PostBase struct {
Title string `gorm:"type:text;not null"`
Cover string `gorm:"type:text"`
Content string `gorm:"type:text;not null"`
Type string `gorm:"type:text;default:markdown"`
CategoryID uint `gorm:"index"`
Category Category `gorm:"foreignKey:CategoryID;references:ID"`
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
IsOriginal bool `gorm:"default:true"`
IsPrivate bool `gorm:"default:false"`
}
type Post struct { type Post struct {
gorm.Model gorm.Model
UserID uint `gorm:"index"` // 发布者的用户ID UserID uint `gorm:"index"` // 发布者的用户ID
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户 User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
Title string `gorm:"type:text;not null"` // 帖子标题 // core fields
Cover string `gorm:"type:text"` // 帖子封面图 PostBase
Content string `gorm:"type:text;not null"` // 帖子内容 //
Type string `gorm:"type:text;default:markdown"` // markdown类型支持markdown或html或txt
CategoryID uint `gorm:"index"` // 帖子分类ID
Category Category `gorm:"foreignKey:CategoryID;references:ID"` // 关联的分类
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
IsOriginal bool `gorm:"default:true"` // 是否为原创帖子
IsPrivate bool `gorm:"default:false"`
LikeCount uint64 LikeCount uint64
CommentCount uint64 CommentCount uint64
ViewCount uint64 ViewCount uint64
@ -71,3 +76,11 @@ func (p *Post) ToDtoWithShortContent(contentLength int) *dto.PostDto {
} }
return dtoPost return dtoPost
} }
// Draft 草稿
type Draft struct {
gorm.Model
PostID uint `gorm:"uniqueIndex"` // 关联的文章ID
Post Post `gorm:"foreignKey:PostID;references:ID"`
PostBase
}

View File

@ -1,21 +1,22 @@
package apiv1 package apiv1
import ( import (
"github.com/cloudwego/hertz/pkg/route" "github.com/cloudwego/hertz/pkg/route"
v1 "github.com/snowykami/neo-blog/internal/controller/v1" v1 "github.com/snowykami/neo-blog/internal/controller/v1"
"github.com/snowykami/neo-blog/internal/middleware" "github.com/snowykami/neo-blog/internal/middleware"
"github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/constant"
) )
func registerAdminRoutes(group *route.RouterGroup) { func registerAdminRoutes(group *route.RouterGroup) {
// Need Admin Middleware // Need Admin Middleware
adminController := v1.NewAdminController() adminController := v1.NewAdminController()
consoleGroup := group.Group("/admin").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin)) consoleGroup := group.Group("/admin").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin))
{ {
consoleGroup.POST("/oidc/o", adminController.CreateOidc) consoleGroup.POST("/oidc/o", adminController.CreateOidc)
consoleGroup.DELETE("/oidc/o/:id", adminController.DeleteOidc) consoleGroup.DELETE("/oidc/o/:id", adminController.DeleteOidc)
consoleGroup.GET("/oidc/o/:id", adminController.GetOidcByID) consoleGroup.GET("/oidc/o/:id", adminController.GetOidcByID)
consoleGroup.GET("/oidc/list", adminController.ListOidc) consoleGroup.GET("/oidc/list", adminController.ListOidc)
consoleGroup.PUT("/oidc/o/:id", adminController.UpdateOidc) consoleGroup.PUT("/oidc/o/:id", adminController.UpdateOidc)
} consoleGroup.GET("/dashboard", adminController.GetDashboard)
}
} }

View File

@ -1,77 +1,110 @@
package service package service
import ( import (
"github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo" "github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/errs"
"gorm.io/gorm" "gorm.io/gorm"
) )
type AdminService struct{} type AdminService struct{}
func NewAdminService() *AdminService { func NewAdminService() *AdminService {
return &AdminService{} return &AdminService{}
}
func (c *AdminService) GetDashboard() (map[string]any, error) {
var (
postCount, commentCount, userCount, viewCount int64
err error
mustCount = func(q *gorm.DB, dest *int64) {
if err == nil {
err = q.Count(dest).Error
}
}
mustScan = func(q *gorm.DB, dest *int64) {
if err == nil {
err = q.Scan(dest).Error
}
}
)
db := repo.GetDB()
mustCount(db.Model(&model.Comment{}), &commentCount)
mustCount(db.Model(&model.Post{}), &postCount)
mustCount(db.Model(&model.User{}), &userCount)
mustScan(db.Model(&model.Post{}).Select("SUM(view_count)"), &viewCount)
if err != nil {
return nil, err
}
return map[string]any{
"total_comments": commentCount,
"total_posts": postCount,
"total_users": userCount,
"total_views": viewCount,
}, nil
} }
func (c *AdminService) CreateOidcConfig(req *dto.AdminOidcConfigDto) error { func (c *AdminService) CreateOidcConfig(req *dto.AdminOidcConfigDto) error {
oidcConfig := &model.OidcConfig{ oidcConfig := &model.OidcConfig{
Name: req.Name, Name: req.Name,
DisplayName: req.DisplayName, DisplayName: req.DisplayName,
Icon: req.Icon, Icon: req.Icon,
ClientID: req.ClientID, ClientID: req.ClientID,
ClientSecret: req.ClientSecret, ClientSecret: req.ClientSecret,
OidcDiscoveryUrl: req.OidcDiscoveryUrl, OidcDiscoveryUrl: req.OidcDiscoveryUrl,
Enabled: req.Enabled, Enabled: req.Enabled,
Type: req.Type, Type: req.Type,
} }
return repo.Oidc.CreateOidcConfig(oidcConfig) return repo.Oidc.CreateOidcConfig(oidcConfig)
} }
func (c *AdminService) DeleteOidcConfig(id string) error { func (c *AdminService) DeleteOidcConfig(id string) error {
if id == "" { if id == "" {
return errs.ErrBadRequest return errs.ErrBadRequest
} }
return repo.Oidc.DeleteOidcConfig(id) return repo.Oidc.DeleteOidcConfig(id)
} }
func (c *AdminService) GetOidcConfigByID(id string) (*dto.AdminOidcConfigDto, error) { func (c *AdminService) GetOidcConfigByID(id string) (*dto.AdminOidcConfigDto, error) {
if id == "" { if id == "" {
return nil, errs.ErrBadRequest return nil, errs.ErrBadRequest
} }
config, err := repo.Oidc.GetOidcConfigByID(id) config, err := repo.Oidc.GetOidcConfigByID(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return config.ToAdminDto(), nil return config.ToAdminDto(), nil
} }
func (c *AdminService) ListOidcConfigs(onlyEnabled bool) ([]*dto.AdminOidcConfigDto, error) { func (c *AdminService) ListOidcConfigs(onlyEnabled bool) ([]*dto.AdminOidcConfigDto, error) {
configs, err := repo.Oidc.ListOidcConfigs(onlyEnabled) configs, err := repo.Oidc.ListOidcConfigs(onlyEnabled)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var dtos []*dto.AdminOidcConfigDto var dtos []*dto.AdminOidcConfigDto
for _, config := range configs { for _, config := range configs {
dtos = append(dtos, config.ToAdminDto()) dtos = append(dtos, config.ToAdminDto())
} }
return dtos, nil return dtos, nil
} }
func (c *AdminService) UpdateOidcConfig(req *dto.AdminOidcConfigDto) error { func (c *AdminService) UpdateOidcConfig(req *dto.AdminOidcConfigDto) error {
if req.ID == 0 { if req.ID == 0 {
return errs.ErrBadRequest return errs.ErrBadRequest
} }
oidcConfig := &model.OidcConfig{ oidcConfig := &model.OidcConfig{
Model: gorm.Model{ID: req.ID}, Model: gorm.Model{ID: req.ID},
Name: req.Name, Name: req.Name,
DisplayName: req.DisplayName, DisplayName: req.DisplayName,
Icon: req.Icon, Icon: req.Icon,
ClientID: req.ClientID, ClientID: req.ClientID,
ClientSecret: req.ClientSecret, ClientSecret: req.ClientSecret,
OidcDiscoveryUrl: req.OidcDiscoveryUrl, OidcDiscoveryUrl: req.OidcDiscoveryUrl,
Enabled: req.Enabled, Enabled: req.Enabled,
Type: req.Type, Type: req.Type,
} }
return repo.Oidc.UpdateOidcConfig(oidcConfig) return repo.Oidc.UpdateOidcConfig(oidcConfig)
} }

View File

@ -1,155 +1,157 @@
package service package service
import ( import (
"context" "context"
"strconv" "strconv"
"github.com/snowykami/neo-blog/internal/ctxutils" "github.com/snowykami/neo-blog/internal/ctxutils"
"github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo" "github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/errs"
) )
type PostService struct{} type PostService struct{}
func NewPostService() *PostService { func NewPostService() *PostService {
return &PostService{} return &PostService{}
} }
func (p *PostService) CreatePost(ctx context.Context, req *dto.CreateOrUpdatePostReq) (uint, error) { func (p *PostService) CreatePost(ctx context.Context, req *dto.CreateOrUpdatePostReq) (uint, error) {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return 0, errs.ErrUnauthorized return 0, errs.ErrUnauthorized
} }
post := &model.Post{ post := &model.Post{
Title: req.Title, UserID: currentUser.ID,
Content: req.Content, PostBase: model.PostBase{
UserID: currentUser.ID, Title: req.Title,
Labels: func() []model.Label { Content: req.Content,
labelModels := make([]model.Label, 0) Labels: func() []model.Label {
for _, labelID := range req.Labels { labelModels := make([]model.Label, 0)
labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) for _, labelID := range req.Labels {
if err == nil { labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID)))
labelModels = append(labelModels, *labelModel) if err == nil {
} labelModels = append(labelModels, *labelModel)
} }
return labelModels }
}(), return labelModels
IsPrivate: req.IsPrivate, }(),
} IsPrivate: req.IsPrivate,
if err := repo.Post.CreatePost(post); err != nil { },
return 0, err }
} if err := repo.Post.CreatePost(post); err != nil {
return post.ID, nil return 0, err
}
return post.ID, nil
} }
func (p *PostService) DeletePost(ctx context.Context, id string) error { func (p *PostService) DeletePost(ctx context.Context, id string) error {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return errs.ErrUnauthorized return errs.ErrUnauthorized
} }
if id == "" { if id == "" {
return errs.ErrBadRequest return errs.ErrBadRequest
} }
post, err := repo.Post.GetPostByID(id) post, err := repo.Post.GetPostByID(id)
if err != nil { if err != nil {
return errs.New(errs.ErrNotFound.Code, "post not found", err) return errs.New(errs.ErrNotFound.Code, "post not found", err)
} }
if post.UserID != currentUser.ID { if post.UserID != currentUser.ID {
return errs.ErrForbidden return errs.ErrForbidden
} }
if err := repo.Post.DeletePost(id); err != nil { if err := repo.Post.DeletePost(id); err != nil {
return errs.ErrInternalServer return errs.ErrInternalServer
} }
return nil return nil
} }
func (p *PostService) GetPost(ctx context.Context, id string) (*dto.PostDto, error) { func (p *PostService) GetPost(ctx context.Context, id string) (*dto.PostDto, error) {
if id == "" { if id == "" {
return nil, errs.ErrBadRequest return nil, errs.ErrBadRequest
} }
post, err := repo.Post.GetPostByID(id) post, err := repo.Post.GetPostByID(id)
if err != nil { if err != nil {
return nil, errs.New(errs.ErrNotFound.Code, "post not found", err) return nil, errs.New(errs.ErrNotFound.Code, "post not found", err)
} }
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if post.IsPrivate && (!ok || post.UserID != currentUser.ID) { if post.IsPrivate && (!ok || post.UserID != currentUser.ID) {
return nil, errs.ErrForbidden return nil, errs.ErrForbidden
} }
return post.ToDto(), nil return post.ToDto(), nil
} }
func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.CreateOrUpdatePostReq) (uint, error) { func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.CreateOrUpdatePostReq) (uint, error) {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return 0, errs.ErrUnauthorized return 0, errs.ErrUnauthorized
} }
if id == "" { if id == "" {
return 0, errs.ErrBadRequest return 0, errs.ErrBadRequest
} }
post, err := repo.Post.GetPostByID(id) post, err := repo.Post.GetPostByID(id)
if err != nil { if err != nil {
return 0, errs.New(errs.ErrNotFound.Code, "post not found", err) return 0, errs.New(errs.ErrNotFound.Code, "post not found", err)
} }
if post.UserID != currentUser.ID { if post.UserID != currentUser.ID {
return 0, errs.ErrForbidden return 0, errs.ErrForbidden
} }
post.Title = req.Title post.Title = req.Title
post.Content = req.Content post.Content = req.Content
post.IsPrivate = req.IsPrivate post.IsPrivate = req.IsPrivate
post.Labels = func() []model.Label { post.Labels = func() []model.Label {
labelModels := make([]model.Label, len(req.Labels)) labelModels := make([]model.Label, len(req.Labels))
for _, labelID := range req.Labels { for _, labelID := range req.Labels {
labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID)))
if err == nil { if err == nil {
labelModels = append(labelModels, *labelModel) labelModels = append(labelModels, *labelModel)
} }
} }
return labelModels return labelModels
}() }()
if err := repo.Post.UpdatePost(post); err != nil { if err := repo.Post.UpdatePost(post); err != nil {
return 0, errs.ErrInternalServer return 0, errs.ErrInternalServer
} }
return post.ID, nil return post.ID, nil
} }
func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]*dto.PostDto, int64, error) { func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]*dto.PostDto, int64, error) {
postDtos := make([]*dto.PostDto, 0) postDtos := make([]*dto.PostDto, 0)
currentUserID, _ := ctxutils.GetCurrentUserID(ctx) currentUserID, _ := ctxutils.GetCurrentUserID(ctx)
posts, total, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Labels, req.LabelRule, req.Page, req.Size, req.OrderBy, req.Desc) posts, total, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Labels, req.LabelRule, req.Page, req.Size, req.OrderBy, req.Desc)
if err != nil { if err != nil {
return nil, total, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err) return nil, total, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err)
} }
for _, post := range posts { for _, post := range posts {
postDtos = append(postDtos, post.ToDtoWithShortContent(100)) postDtos = append(postDtos, post.ToDtoWithShortContent(100))
} }
return postDtos, total, nil return postDtos, total, nil
} }
func (p *PostService) ToggleLikePost(ctx context.Context, id string) (bool, error) { func (p *PostService) ToggleLikePost(ctx context.Context, id string) (bool, error) {
currentUser, ok := ctxutils.GetCurrentUser(ctx) currentUser, ok := ctxutils.GetCurrentUser(ctx)
if !ok { if !ok {
return false, errs.ErrUnauthorized return false, errs.ErrUnauthorized
} }
if id == "" { if id == "" {
return false, errs.ErrBadRequest return false, errs.ErrBadRequest
} }
post, err := repo.Post.GetPostByID(id) post, err := repo.Post.GetPostByID(id)
if err != nil { if err != nil {
return false, errs.New(errs.ErrNotFound.Code, "post not found", err) return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
} }
if post.UserID == currentUser.ID { if post.UserID == currentUser.ID {
return false, errs.ErrForbidden return false, errs.ErrForbidden
} }
idInt, err := strconv.ParseUint(id, 10, 64) idInt, err := strconv.ParseUint(id, 10, 64)
if err != nil { if err != nil {
return false, errs.New(errs.ErrBadRequest.Code, "invalid post ID", err) return false, errs.New(errs.ErrBadRequest.Code, "invalid post ID", err)
} }
liked, err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID) liked, err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID)
if err != nil { if err != nil {
return false, errs.ErrInternalServer return false, errs.ErrInternalServer
} }
return liked, nil return liked, nil
} }

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() { 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() { export default function Page() {
return <div></div>; return <PostManage />;
} }

View File

@ -18,11 +18,12 @@ export function CurrentLogged() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const handleLoggedContinue = () => { const handleLoggedContinue = () => {
console.log("continue to", redirectBack);
router.push(redirectBack); router.push(redirectBack);
} }
const handleLogOut = () => { const handleLogOut = () => {
logout(); logout();
} }
if (!user) return null; if (!user) return null;
@ -30,8 +31,8 @@ export function CurrentLogged() {
<div className="mb-4"> <div className="mb-4">
<SectionDivider className="mb-4">{t("currently_logged_in")}</SectionDivider> <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 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-4 items-center cursor-pointer">
<div onClick={handleLoggedContinue} className="flex gap-2 justify-center items-center "> <div className="flex gap-2 justify-center items-center ">
<Avatar className="h-10 w-10 rounded-full"> <Avatar className="h-10 w-10 rounded-full">
<AvatarImage src={getGravatarFromUser({ user })} alt={user.username} /> <AvatarImage src={getGravatarFromUser({ user })} alt={user.username} />
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback> <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; 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[] } = { export const sidebarData: { navMain: SidebarItem[]; navUserCenter: SidebarItem[] } = {
navMain: [ navMain: [
{ {
title: "dashboard.title", title: "dashboard.title",
url: "/console", url: path.dashboard,
icon: Gauge, icon: Gauge,
permission: isAdmin permission: isAdmin
}, },
{ {
title: "post.title", title: "post.title",
url: "/console/post", url: path.post,
icon: Newspaper, icon: Newspaper,
permission: isEditor permission: isEditor
}, },
{ {
title: "comment.title", title: "comment.title",
url: "/console/comment", url: path.comment,
icon: MessageCircle, icon: MessageCircle,
permission: isEditor permission: isEditor
}, },
{ {
title: "file.title", title: "file.title",
url: "/console/file", url: path.file,
icon: Folder, icon: Folder,
permission: () => true permission: () => true
}, },
{ {
title: "user.title", title: "user.title",
url: "/console/user", url: path.user,
icon: Users, icon: Users,
permission: isAdmin permission: isAdmin
}, },
{ {
title: "global.title", title: "global.title",
url: "/console/global", url: path.global,
icon: Settings, icon: Settings,
permission: isAdmin permission: isAdmin
}, },
@ -53,19 +65,19 @@ export const sidebarData: { navMain: SidebarItem[]; navUserCenter: SidebarItem[]
navUserCenter: [ navUserCenter: [
{ {
title: "user_profile.title", title: "user_profile.title",
url: "/console/user-profile", url: path.userProfile,
icon: UserPen, icon: UserPen,
permission: () => true permission: () => true
}, },
{ {
title: "user_security.title", title: "user_security.title",
url: "/console/user-security", url: path.userSecurity,
icon: ShieldCheck, icon: ShieldCheck,
permission: () => true permission: () => true
}, },
{ {
title: "user-preference.title", title: "user-preference.title",
url: "/console/user-preference", url: path.userPreference,
icon: Palette, icon: Palette,
permission: () => true 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 { export interface PaginationParams {
orderBy: OrderBy orderBy: OrderBy
desc: boolean desc: boolean // 是否降序
page: number page: number
size: number size: number
} }