diff --git a/internal/controller/v1/admin.go b/internal/controller/v1/admin.go index 415bbcf..08b575e 100644 --- a/internal/controller/v1/admin.go +++ b/internal/controller/v1/admin.go @@ -2,12 +2,13 @@ package v1 import ( "context" + "strconv" + "github.com/cloudwego/hertz/pkg/app" "github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/service" "github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/resps" - "strconv" ) 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) { var adminCreateOidcReq dto.AdminOidcConfigDto if err := c.BindAndValidate(&adminCreateOidcReq); err != nil { diff --git a/internal/model/post.go b/internal/model/post.go index c8b9a8a..b93c797 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -6,19 +6,24 @@ import ( "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 { gorm.Model - UserID uint `gorm:"index"` // 发布者的用户ID - User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户 - 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"` // 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"` + UserID uint `gorm:"index"` // 发布者的用户ID + User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户 + // core fields + PostBase + // LikeCount uint64 CommentCount uint64 ViewCount uint64 @@ -71,3 +76,11 @@ func (p *Post) ToDtoWithShortContent(contentLength int) *dto.PostDto { } return dtoPost } + +// Draft 草稿 +type Draft struct { + gorm.Model + PostID uint `gorm:"uniqueIndex"` // 关联的文章ID + Post Post `gorm:"foreignKey:PostID;references:ID"` + PostBase +} diff --git a/internal/router/apiv1/admin.go b/internal/router/apiv1/admin.go index 17b2053..89945a0 100644 --- a/internal/router/apiv1/admin.go +++ b/internal/router/apiv1/admin.go @@ -1,21 +1,22 @@ package apiv1 import ( - "github.com/cloudwego/hertz/pkg/route" - v1 "github.com/snowykami/neo-blog/internal/controller/v1" - "github.com/snowykami/neo-blog/internal/middleware" - "github.com/snowykami/neo-blog/pkg/constant" + "github.com/cloudwego/hertz/pkg/route" + v1 "github.com/snowykami/neo-blog/internal/controller/v1" + "github.com/snowykami/neo-blog/internal/middleware" + "github.com/snowykami/neo-blog/pkg/constant" ) func registerAdminRoutes(group *route.RouterGroup) { - // Need Admin Middleware - adminController := v1.NewAdminController() - consoleGroup := group.Group("/admin").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin)) - { - consoleGroup.POST("/oidc/o", adminController.CreateOidc) - consoleGroup.DELETE("/oidc/o/:id", adminController.DeleteOidc) - consoleGroup.GET("/oidc/o/:id", adminController.GetOidcByID) - consoleGroup.GET("/oidc/list", adminController.ListOidc) - consoleGroup.PUT("/oidc/o/:id", adminController.UpdateOidc) - } + // Need Admin Middleware + adminController := v1.NewAdminController() + consoleGroup := group.Group("/admin").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin)) + { + consoleGroup.POST("/oidc/o", adminController.CreateOidc) + consoleGroup.DELETE("/oidc/o/:id", adminController.DeleteOidc) + consoleGroup.GET("/oidc/o/:id", adminController.GetOidcByID) + consoleGroup.GET("/oidc/list", adminController.ListOidc) + consoleGroup.PUT("/oidc/o/:id", adminController.UpdateOidc) + consoleGroup.GET("/dashboard", adminController.GetDashboard) + } } diff --git a/internal/service/admin.go b/internal/service/admin.go index 7c1a4f6..d7c8cec 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -1,77 +1,110 @@ package service import ( - "github.com/snowykami/neo-blog/internal/dto" - "github.com/snowykami/neo-blog/internal/model" - "github.com/snowykami/neo-blog/internal/repo" - "github.com/snowykami/neo-blog/pkg/errs" - "gorm.io/gorm" + "github.com/snowykami/neo-blog/internal/dto" + "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/errs" + "gorm.io/gorm" ) type AdminService struct{} 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 { - oidcConfig := &model.OidcConfig{ - Name: req.Name, - DisplayName: req.DisplayName, - Icon: req.Icon, - ClientID: req.ClientID, - ClientSecret: req.ClientSecret, - OidcDiscoveryUrl: req.OidcDiscoveryUrl, - Enabled: req.Enabled, - Type: req.Type, - } - return repo.Oidc.CreateOidcConfig(oidcConfig) + oidcConfig := &model.OidcConfig{ + Name: req.Name, + DisplayName: req.DisplayName, + Icon: req.Icon, + ClientID: req.ClientID, + ClientSecret: req.ClientSecret, + OidcDiscoveryUrl: req.OidcDiscoveryUrl, + Enabled: req.Enabled, + Type: req.Type, + } + return repo.Oidc.CreateOidcConfig(oidcConfig) } func (c *AdminService) DeleteOidcConfig(id string) error { - if id == "" { - return errs.ErrBadRequest - } - return repo.Oidc.DeleteOidcConfig(id) + if id == "" { + return errs.ErrBadRequest + } + return repo.Oidc.DeleteOidcConfig(id) } func (c *AdminService) GetOidcConfigByID(id string) (*dto.AdminOidcConfigDto, error) { - if id == "" { - return nil, errs.ErrBadRequest - } - config, err := repo.Oidc.GetOidcConfigByID(id) - if err != nil { - return nil, err - } - return config.ToAdminDto(), nil + if id == "" { + return nil, errs.ErrBadRequest + } + config, err := repo.Oidc.GetOidcConfigByID(id) + if err != nil { + return nil, err + } + return config.ToAdminDto(), nil } func (c *AdminService) ListOidcConfigs(onlyEnabled bool) ([]*dto.AdminOidcConfigDto, error) { - configs, err := repo.Oidc.ListOidcConfigs(onlyEnabled) - if err != nil { - return nil, err - } - var dtos []*dto.AdminOidcConfigDto - for _, config := range configs { - dtos = append(dtos, config.ToAdminDto()) - } - return dtos, nil + configs, err := repo.Oidc.ListOidcConfigs(onlyEnabled) + if err != nil { + return nil, err + } + var dtos []*dto.AdminOidcConfigDto + for _, config := range configs { + dtos = append(dtos, config.ToAdminDto()) + } + return dtos, nil } func (c *AdminService) UpdateOidcConfig(req *dto.AdminOidcConfigDto) error { - if req.ID == 0 { - return errs.ErrBadRequest - } - oidcConfig := &model.OidcConfig{ - Model: gorm.Model{ID: req.ID}, - Name: req.Name, - DisplayName: req.DisplayName, - Icon: req.Icon, - ClientID: req.ClientID, - ClientSecret: req.ClientSecret, - OidcDiscoveryUrl: req.OidcDiscoveryUrl, - Enabled: req.Enabled, - Type: req.Type, - } - return repo.Oidc.UpdateOidcConfig(oidcConfig) + if req.ID == 0 { + return errs.ErrBadRequest + } + oidcConfig := &model.OidcConfig{ + Model: gorm.Model{ID: req.ID}, + Name: req.Name, + DisplayName: req.DisplayName, + Icon: req.Icon, + ClientID: req.ClientID, + ClientSecret: req.ClientSecret, + OidcDiscoveryUrl: req.OidcDiscoveryUrl, + Enabled: req.Enabled, + Type: req.Type, + } + return repo.Oidc.UpdateOidcConfig(oidcConfig) } diff --git a/internal/service/post.go b/internal/service/post.go index beea8ac..ddf0054 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -1,155 +1,157 @@ package service import ( - "context" - "strconv" + "context" + "strconv" - "github.com/snowykami/neo-blog/internal/ctxutils" - "github.com/snowykami/neo-blog/internal/dto" - "github.com/snowykami/neo-blog/internal/model" - "github.com/snowykami/neo-blog/internal/repo" - "github.com/snowykami/neo-blog/pkg/errs" + "github.com/snowykami/neo-blog/internal/ctxutils" + "github.com/snowykami/neo-blog/internal/dto" + "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/errs" ) type PostService struct{} func NewPostService() *PostService { - return &PostService{} + return &PostService{} } func (p *PostService) CreatePost(ctx context.Context, req *dto.CreateOrUpdatePostReq) (uint, error) { - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if !ok { - return 0, errs.ErrUnauthorized - } - post := &model.Post{ - Title: req.Title, - Content: req.Content, - UserID: currentUser.ID, - Labels: func() []model.Label { - labelModels := make([]model.Label, 0) - for _, labelID := range req.Labels { - labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) - if err == nil { - labelModels = append(labelModels, *labelModel) - } - } - return labelModels - }(), - IsPrivate: req.IsPrivate, - } - if err := repo.Post.CreatePost(post); err != nil { - return 0, err - } - return post.ID, nil + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + return 0, errs.ErrUnauthorized + } + post := &model.Post{ + UserID: currentUser.ID, + PostBase: model.PostBase{ + Title: req.Title, + Content: req.Content, + Labels: func() []model.Label { + labelModels := make([]model.Label, 0) + for _, labelID := range req.Labels { + labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) + if err == nil { + labelModels = append(labelModels, *labelModel) + } + } + return labelModels + }(), + IsPrivate: req.IsPrivate, + }, + } + if err := repo.Post.CreatePost(post); err != nil { + return 0, err + } + return post.ID, nil } func (p *PostService) DeletePost(ctx context.Context, id string) error { - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if !ok { - return errs.ErrUnauthorized - } - if id == "" { - return errs.ErrBadRequest - } - post, err := repo.Post.GetPostByID(id) - if err != nil { - return errs.New(errs.ErrNotFound.Code, "post not found", err) - } - if post.UserID != currentUser.ID { - return errs.ErrForbidden - } - if err := repo.Post.DeletePost(id); err != nil { - return errs.ErrInternalServer - } - return nil + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + return errs.ErrUnauthorized + } + if id == "" { + return errs.ErrBadRequest + } + post, err := repo.Post.GetPostByID(id) + if err != nil { + return errs.New(errs.ErrNotFound.Code, "post not found", err) + } + if post.UserID != currentUser.ID { + return errs.ErrForbidden + } + if err := repo.Post.DeletePost(id); err != nil { + return errs.ErrInternalServer + } + return nil } func (p *PostService) GetPost(ctx context.Context, id string) (*dto.PostDto, error) { - if id == "" { - return nil, errs.ErrBadRequest - } - post, err := repo.Post.GetPostByID(id) - if err != nil { - return nil, errs.New(errs.ErrNotFound.Code, "post not found", err) - } - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if post.IsPrivate && (!ok || post.UserID != currentUser.ID) { - return nil, errs.ErrForbidden - } + if id == "" { + return nil, errs.ErrBadRequest + } + post, err := repo.Post.GetPostByID(id) + if err != nil { + return nil, errs.New(errs.ErrNotFound.Code, "post not found", err) + } + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if post.IsPrivate && (!ok || post.UserID != currentUser.ID) { + 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) { - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if !ok { - return 0, errs.ErrUnauthorized - } - if id == "" { - return 0, errs.ErrBadRequest - } - post, err := repo.Post.GetPostByID(id) - if err != nil { - return 0, errs.New(errs.ErrNotFound.Code, "post not found", err) - } - if post.UserID != currentUser.ID { - return 0, errs.ErrForbidden - } - post.Title = req.Title - post.Content = req.Content - post.IsPrivate = req.IsPrivate - post.Labels = func() []model.Label { - labelModels := make([]model.Label, len(req.Labels)) - for _, labelID := range req.Labels { - labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) - if err == nil { - labelModels = append(labelModels, *labelModel) - } - } - return labelModels - }() - if err := repo.Post.UpdatePost(post); err != nil { - return 0, errs.ErrInternalServer - } - return post.ID, nil + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + return 0, errs.ErrUnauthorized + } + if id == "" { + return 0, errs.ErrBadRequest + } + post, err := repo.Post.GetPostByID(id) + if err != nil { + return 0, errs.New(errs.ErrNotFound.Code, "post not found", err) + } + if post.UserID != currentUser.ID { + return 0, errs.ErrForbidden + } + post.Title = req.Title + post.Content = req.Content + post.IsPrivate = req.IsPrivate + post.Labels = func() []model.Label { + labelModels := make([]model.Label, len(req.Labels)) + for _, labelID := range req.Labels { + labelModel, err := repo.Label.GetLabelByID(strconv.Itoa(int(labelID))) + if err == nil { + labelModels = append(labelModels, *labelModel) + } + } + return labelModels + }() + if err := repo.Post.UpdatePost(post); err != nil { + return 0, errs.ErrInternalServer + } + return post.ID, nil } func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]*dto.PostDto, int64, error) { - postDtos := make([]*dto.PostDto, 0) - 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) - if err != nil { - return nil, total, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err) - } - for _, post := range posts { - postDtos = append(postDtos, post.ToDtoWithShortContent(100)) - } - return postDtos, total, nil + postDtos := make([]*dto.PostDto, 0) + 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) + if err != nil { + return nil, total, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err) + } + for _, post := range posts { + postDtos = append(postDtos, post.ToDtoWithShortContent(100)) + } + return postDtos, total, nil } func (p *PostService) ToggleLikePost(ctx context.Context, id string) (bool, error) { - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if !ok { - return false, errs.ErrUnauthorized - } - if id == "" { - return false, errs.ErrBadRequest - } - post, err := repo.Post.GetPostByID(id) - if err != nil { - return false, errs.New(errs.ErrNotFound.Code, "post not found", err) - } - if post.UserID == currentUser.ID { - return false, errs.ErrForbidden - } - idInt, err := strconv.ParseUint(id, 10, 64) - if err != nil { - return false, errs.New(errs.ErrBadRequest.Code, "invalid post ID", err) - } - liked, err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID) - if err != nil { - return false, errs.ErrInternalServer - } - return liked, nil + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + return false, errs.ErrUnauthorized + } + if id == "" { + return false, errs.ErrBadRequest + } + post, err := repo.Post.GetPostByID(id) + if err != nil { + return false, errs.New(errs.ErrNotFound.Code, "post not found", err) + } + if post.UserID == currentUser.ID { + return false, errs.ErrForbidden + } + idInt, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return false, errs.New(errs.ErrBadRequest.Code, "invalid post ID", err) + } + liked, err := repo.Post.ToggleLikePost(uint(idInt), currentUser.ID) + if err != nil { + return false, errs.ErrInternalServer + } + return liked, nil } diff --git a/web/src/api/admin.ts b/web/src/api/admin.ts new file mode 100644 index 0000000..b0b2a53 --- /dev/null +++ b/web/src/api/admin.ts @@ -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> { + const res = await axiosClient.get>('/admin/dashboard') + return res.data +} \ No newline at end of file diff --git a/web/src/app/console/page.tsx b/web/src/app/console/page.tsx index fe67e77..8e42817 100644 --- a/web/src/app/console/page.tsx +++ b/web/src/app/console/page.tsx @@ -1,3 +1,5 @@ +import { Dashboard } from "@/components/console/dashboard"; + export default function Page() { - return
Console
; + return ; } \ No newline at end of file diff --git a/web/src/app/console/post/page.tsx b/web/src/app/console/post/page.tsx index d612c1f..15816a7 100644 --- a/web/src/app/console/post/page.tsx +++ b/web/src/app/console/post/page.tsx @@ -1,3 +1,5 @@ +import { PostManage } from "@/components/console/post-manage"; + export default function Page() { - return
文章管理
; + return ; } \ No newline at end of file diff --git a/web/src/components/auth/common/current-logged.tsx b/web/src/components/auth/common/current-logged.tsx index f94695a..d9a6c97 100644 --- a/web/src/components/auth/common/current-logged.tsx +++ b/web/src/components/auth/common/current-logged.tsx @@ -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() {
{t("currently_logged_in")}
-
-
+
+
{getFallbackAvatarFromUsername(user.nickname || user.username)} diff --git a/web/src/components/console/dashboard/index.tsx b/web/src/components/console/dashboard/index.tsx new file mode 100644 index 0000000..da5577b --- /dev/null +++ b/web/src/components/console/dashboard/index.tsx @@ -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 ( +
+ +
+ ) +} + +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(null); + + useEffect(() => { + getDashboard().then(res => { + setFetchData(res.data); + }).catch(err => { + toast.error(err.message || "Failed to fetch dashboard data"); + }); + }, []) + + if (!fetchData) return
Loading...
+ + return
+ {data.map(item => ( + + + + {item.label} + + + {(fetchData as any)[item.key]} + + + + + ))} +
+} \ No newline at end of file diff --git a/web/src/components/console/data.ts b/web/src/components/console/data.ts index cc44b26..d775ee5 100644 --- a/web/src/components/console/data.ts +++ b/web/src/components/console/data.ts @@ -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 } diff --git a/web/src/components/console/post-manage/index.tsx b/web/src/components/console/post-manage/index.tsx new file mode 100644 index 0000000..b8cfa09 --- /dev/null +++ b/web/src/components/console/post-manage/index.tsx @@ -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([]); + const [orderBy, setOrderBy] = useState(OrderBy.CreatedAt); + const [desc, setDesc] = useState(true); + const [page, setPage] = useState(1); + + useEffect(() => { + listPosts({ page, size: config.postsPerPage, orderBy, desc }).then(res => { + setPosts(res.data.posts); + }); + }, [page, orderBy, desc]); + + return
+ {posts.map(post => )} +
; +} + +function PostItem({ post }: { post: Post }) { + return ( +
+
+ {/* left */} +
+
+ {post.title} +
+
+ ID: {post.id} + | + Created At: {new Date(post.createdAt).toLocaleDateString()} + | + Updated At: {new Date(post.updatedAt).toLocaleDateString()} +
+
+
+ +
+ ) +} \ No newline at end of file diff --git a/web/src/models/common.ts b/web/src/models/common.ts index 443bd8a..208cb36 100644 --- a/web/src/models/common.ts +++ b/web/src/models/common.ts @@ -1,6 +1,6 @@ export interface PaginationParams { orderBy: OrderBy - desc: boolean + desc: boolean // 是否降序 page: number size: number }