feat: enhance post management with pagination, search, and order functionality
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 11s

- Added search input for filtering posts by keywords.
- Implemented pagination controls for navigating through posts.
- Introduced order selector for sorting posts based on various criteria.
- Enhanced post item display with additional metrics (view count, like count, comment count).
- Added dropdown menu for post actions (edit, view, toggle privacy, delete).
- Integrated double confirmation for delete action.
- Updated user profile to support background image upload.
- Improved user security settings with better layout and validation.
- Refactored auth context to use useCallback for logout function.
- Added command palette component for improved command execution.
- Introduced popover component for better UI interactions.
- Implemented debounce hooks for optimized state updates.
- Updated localization files with new keys for improved internationalization.
- Added tailwind configuration for styling.
This commit is contained in:
2025-09-25 00:51:29 +08:00
parent 59b68613cd
commit 64b1c54911
44 changed files with 2790 additions and 474 deletions

View File

@ -1,5 +1,7 @@
package main
package v1
func main() {
type ConfigController struct{}
func NewConfigController() *ConfigController {
return &ConfigController{}
}

View File

@ -1,100 +1,102 @@
package dto
type UserDto struct {
ID uint `json:"id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"` // 头像URL
Email string `json:"email"` // 邮箱
Gender string `json:"gender"`
Role string `json:"role"`
Language string `json:"language"` // 语言
ID uint `json:"id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"` // 头像URL
BackgroundUrl string `json:"background_url"`
Email string `json:"email"` // 邮箱
Gender string `json:"gender"`
Role string `json:"role"`
Language string `json:"language"` // 语言
}
type UserOidcConfigDto struct {
Name string `json:"name"` // OIDC配置名称
DisplayName string `json:"display_name"` // OIDC配置显示名称
Icon string `json:"icon"` // OIDC配置图标URL
LoginUrl string `json:"login_url"` // OIDC登录URL
Name string `json:"name"` // OIDC配置名称
DisplayName string `json:"display_name"` // OIDC配置显示名称
Icon string `json:"icon"` // OIDC配置图标URL
LoginUrl string `json:"login_url"` // OIDC登录URL
}
type UserLoginReq struct {
Username string `json:"username"` // username or email
Password string `json:"password"`
Username string `json:"username"` // username or email
Password string `json:"password"`
}
type UserLoginResp struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
}
type UserRegisterReq struct {
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"-" binding:"-"`
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"-" binding:"-"`
}
type UserRegisterResp struct {
Token string `json:"token"` // 访问令牌
RefreshToken string `json:"refresh_token"` // 刷新令牌
User UserDto `json:"user"` // 用户信息
Token string `json:"token"` // 访问令牌
RefreshToken string `json:"refresh_token"` // 刷新令牌
User UserDto `json:"user"` // 用户信息
}
type VerifyEmailReq struct {
Email string `json:"email"` // 邮箱地址
Email string `json:"email"` // 邮箱地址
}
type VerifyEmailResp struct {
Success bool `json:"success"` // 验证码发送成功与否
Success bool `json:"success"` // 验证码发送成功与否
}
type OidcLoginReq struct {
Name string `json:"name"` // OIDC配置名称
Code string `json:"code"` // OIDC授权码
State string `json:"state"`
Name string `json:"name"` // OIDC配置名称
Code string `json:"code"` // OIDC授权码
State string `json:"state"`
}
type OidcLoginResp struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
}
type ListOidcConfigResp struct {
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
}
type GetUserReq struct {
UserID uint `json:"user_id"`
UserID uint `json:"user_id"`
}
type GetUserByUsernameReq struct {
Username string `json:"username"`
Username string `json:"username"`
}
type GetUserResp struct {
User UserDto `json:"user"` // 用户信息
User UserDto `json:"user"` // 用户信息
}
type UpdateUserReq struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
Gender string `json:"gender"`
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
BackgroundUrl string `json:"background_url"`
Gender string `json:"gender"`
}
type UpdateUserResp struct {
User *UserDto `json:"user"` // 更新后的用户信息
User *UserDto `json:"user"` // 更新后的用户信息
}
type UpdatePasswordReq struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
type ResetPasswordReq struct {
Email string `json:"-" binding:"-"`
NewPassword string `json:"new_password"`
Email string `json:"-" binding:"-"`
NewPassword string `json:"new_password"`
}

View File

@ -7,14 +7,15 @@ import (
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一
Nickname string `gorm:"default:''"` // 昵称
AvatarUrl string
Email string `gorm:"uniqueIndex"`
Gender string `gorm:"default:''"`
Role string `gorm:"default:'user'"` // user editor admin
Language string `gorm:"default:'en'"`
Password string // 密码,存储加密后的值
Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一
Nickname string `gorm:"default:''"` // 昵称
AvatarUrl string
BackgroundUrl string
Email string `gorm:"uniqueIndex"`
Gender string `gorm:"default:''"`
Role string `gorm:"default:'user'"` // user editor admin
Language string `gorm:"default:'en'"`
Password string // 密码,存储加密后的值
}
type UserOpenID struct {
@ -27,13 +28,14 @@ type UserOpenID struct {
func (user *User) ToDto() dto.UserDto {
return dto.UserDto{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
AvatarUrl: user.AvatarUrl,
Email: user.Email,
Gender: user.Gender,
Role: user.Role,
Language: user.Language,
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
AvatarUrl: user.AvatarUrl,
BackgroundUrl: user.BackgroundUrl,
Email: user.Email,
Gender: user.Gender,
Role: user.Role,
Language: user.Language,
}
}

View File

@ -1 +1,22 @@
package apiv1
import (
"github.com/cloudwego/hertz/pkg/route"
"github.com/snowykami/neo-blog/internal/controller/v1"
"github.com/snowykami/neo-blog/internal/middleware"
"github.com/snowykami/neo-blog/pkg/constant"
)
func registerConfigRoutes(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)
consoleGroup.GET("/dashboard", adminController.GetDashboard)
}
}

View File

@ -1,23 +1,27 @@
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"
)
// post 文章API路由
func registerPostRoutes(group *route.RouterGroup) {
postController := v1.NewPostController()
postGroup := group.Group("/post").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleEditor))
postGroupWithoutAuth := group.Group("/post").Use(middleware.UseAuth(false))
{
postGroupWithoutAuth.GET("/p/:id", postController.Get)
postGroupWithoutAuth.GET("/list", postController.List)
postGroup.POST("/p", postController.Create)
postGroup.PUT("/p/:id", postController.Update)
postGroup.DELETE("/p/:id", postController.Delete)
}
postController := v1.NewPostController()
postGroup := group.Group("/post").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleEditor))
postGroupWithoutAuth := group.Group("/post").Use(middleware.UseAuth(false))
{
postGroupWithoutAuth.GET("/p/:id", postController.Get)
postGroupWithoutAuth.GET("/list", postController.List)
postGroup.POST("/p", postController.Create)
postGroup.PUT("/p/:id", postController.Update)
postGroup.DELETE("/p/:id", postController.Delete)
// draft
postGroup.POST("/d")
postGroup.GET("/d/:id")
postGroup.DELETE("/d/:id")
}
}

View File

@ -1,110 +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()
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)
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
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)
}