From 0812e334df51891f057f923aaf284d3e067dbdad Mon Sep 17 00:00:00 2001 From: Snowykami Date: Fri, 26 Sep 2025 00:25:26 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=99=A8=E5=92=8C=E8=B7=AF=E7=94=B1=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/controller/v1/config.go | 7 ------- internal/controller/v1/misc.go | 1 + internal/model/json.go | 1 + internal/model/kv.go | 5 +++++ internal/repo/kv.go | 5 +++++ internal/repo/kv_test.go | 1 + internal/router/apiv1/config.go | 22 ---------------------- internal/router/apiv1/misc.go | 1 + 8 files changed, 14 insertions(+), 29 deletions(-) delete mode 100644 internal/controller/v1/config.go create mode 100644 internal/controller/v1/misc.go create mode 100644 internal/model/json.go create mode 100644 internal/model/kv.go create mode 100644 internal/repo/kv.go create mode 100644 internal/repo/kv_test.go delete mode 100644 internal/router/apiv1/config.go create mode 100644 internal/router/apiv1/misc.go diff --git a/internal/controller/v1/config.go b/internal/controller/v1/config.go deleted file mode 100644 index 2bb4268..0000000 --- a/internal/controller/v1/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package v1 - -type ConfigController struct{} - -func NewConfigController() *ConfigController { - return &ConfigController{} -} diff --git a/internal/controller/v1/misc.go b/internal/controller/v1/misc.go new file mode 100644 index 0000000..b7b1f99 --- /dev/null +++ b/internal/controller/v1/misc.go @@ -0,0 +1 @@ +package v1 diff --git a/internal/model/json.go b/internal/model/json.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/internal/model/json.go @@ -0,0 +1 @@ +package model diff --git a/internal/model/kv.go b/internal/model/kv.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/internal/model/kv.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/internal/repo/kv.go b/internal/repo/kv.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/internal/repo/kv.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/internal/repo/kv_test.go b/internal/repo/kv_test.go new file mode 100644 index 0000000..e0281bf --- /dev/null +++ b/internal/repo/kv_test.go @@ -0,0 +1 @@ +package repo diff --git a/internal/router/apiv1/config.go b/internal/router/apiv1/config.go deleted file mode 100644 index e52da65..0000000 --- a/internal/router/apiv1/config.go +++ /dev/null @@ -1,22 +0,0 @@ -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) - } -} diff --git a/internal/router/apiv1/misc.go b/internal/router/apiv1/misc.go new file mode 100644 index 0000000..c0e14f5 --- /dev/null +++ b/internal/router/apiv1/misc.go @@ -0,0 +1 @@ +package apiv1 From f501948f91856836f358c1b04b6d178e41f31476 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Fri, 26 Sep 2025 00:25:34 +0800 Subject: [PATCH 2/2] Refactor site configuration and color scheme management - Replaced static config with dynamic site info context. - Updated color scheme handling in various components to use site info. - Removed deprecated config file and integrated site info fetching. - Enhanced user preference page to allow color scheme selection. - Adjusted blog and console components to reflect new site info structure. - Improved error handling and fallback mechanisms for site info retrieval. --- internal/controller/v1/misc.go | 63 +++++++ internal/dto/user.go | 98 +++++------ internal/model/json.go | 83 +++++++++ internal/model/kv.go | 7 +- internal/model/user.go | 58 ++++--- internal/repo/init.go | 1 + internal/repo/kv.go | 49 +++++- internal/repo/kv_test.go | 17 ++ internal/router/apiv1/misc.go | 17 ++ internal/router/apiv1/v1.go | 23 +-- internal/service/user.go | 10 +- web/src/api/misc.ts | 9 + web/src/api/user.ts | 1 - web/src/app/(main)/layout.tsx | 7 +- web/src/app/console/user-preference/page.tsx | 4 +- web/src/app/layout.tsx | 55 +++++- web/src/app/styles/blue.css | 4 +- web/src/app/styles/green.css | 4 +- web/src/app/styles/orange.css | 4 +- web/src/app/styles/red.css | 4 +- web/src/app/styles/rose.css | 4 +- web/src/app/styles/violet.css | 4 +- web/src/app/styles/yellow.css | 4 +- .../components/auth/common/auth-header.tsx | 9 +- web/src/components/auth/login/login-form.tsx | 6 +- .../components/blog-home/blog-home-card.tsx | 12 +- web/src/components/blog-home/blog-home.tsx | 13 +- web/src/components/blog-post/blog-post.tsx | 8 +- web/src/components/blog/blog-sidebar-card.tsx | 16 +- web/src/components/comment/index.tsx | 11 +- web/src/components/console/app-sidebar.tsx | 5 +- .../console/common/color-scheme-selector.tsx | 84 +++++++++ .../console/user-preference/index.tsx | 45 +++++ .../components/console/user-profile/index.tsx | 3 +- web/src/components/layout/footer.tsx | 6 +- .../layout/nav/avatar-with-dropdown-menu.tsx | 10 +- .../components/layout/nav/navbar-or-side.tsx | 9 +- web/src/config.ts | 27 --- web/src/contexts/device-context.tsx | 7 +- web/src/contexts/site-info-context.tsx | 160 ++++++++++++++++++ web/src/locales/zh-CN.json | 7 +- web/src/models/user.ts | 1 + 42 files changed, 770 insertions(+), 199 deletions(-) create mode 100644 web/src/api/misc.ts create mode 100644 web/src/components/console/common/color-scheme-selector.tsx create mode 100644 web/src/components/console/user-preference/index.tsx delete mode 100644 web/src/config.ts create mode 100644 web/src/contexts/site-info-context.tsx diff --git a/internal/controller/v1/misc.go b/internal/controller/v1/misc.go index b7b1f99..79e3dcd 100644 --- a/internal/controller/v1/misc.go +++ b/internal/controller/v1/misc.go @@ -1 +1,64 @@ package v1 + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/resps" +) + +const ( + KeySiteInfo = "site_info" +) + +type MiscController struct{} + +func NewMiscController() *MiscController { + return &MiscController{} +} + +func (mc *MiscController) GetSiteInfo(ctx context.Context, c *app.RequestContext) { + value, err := repo.KV.GetKV(KeySiteInfo, utils.H{ + "metadata": utils.H{ + "name": "Neo Blog S", + "icon": "https://cdn.liteyuki.org/snowykami/avatar.jpg", + "description": "A neo blog system.", + }, + "color_schemes": []string{"blue", "green", "orange", "red", "rose", "violet", "yellow"}, + "default_cover": "https://cdn.liteyuki.org/blog/background.png", + "owner": utils.H{ + "name": "SnowyKami", + "description": "A full-stack developer.", + "avatar": "https://cdn.liteyuki.org/snowykami/avatar.jpg", + }, + "posts_per_page": 9, + "comments_per_page": 8, + "verify_code_cool_down": 60, + "animation_duration_second": 0.618, + "footer": utils.H{ + "text": "Liteyuki ICP 114514", + "links": []string{"https://www.liteyuki.com/"}, + }, + }) + if err != nil { + resps.InternalServerError(c, err.Error()) + return + } + resps.Ok(c, "", value) +} + +func (mc *MiscController) SetSiteInfo(ctx context.Context, c *app.RequestContext) { + data := make(map[string]interface{}) + err := c.BindAndValidate(&data) + if err != nil { + resps.BadRequest(c, err.Error()) + return + } + err = repo.KV.SetKV(KeySiteInfo, data) + if err != nil { + resps.InternalServerError(c, err.Error()) + } + resps.Ok(c, "", nil) +} diff --git a/internal/dto/user.go b/internal/dto/user.go index dc1dbc3..df3759a 100644 --- a/internal/dto/user.go +++ b/internal/dto/user.go @@ -1,102 +1,104 @@ package dto type UserDto struct { - 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"` // 语言 + ID uint `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Nickname string `json:"nickname"` + AvatarUrl string `json:"avatar_url"` // 头像URL + BackgroundUrl string `json:"background_url"` + PreferredColor string `json:"preferred_color"` // 主题色 + 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"` - BackgroundUrl string `json:"background_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"` + PreferredColor string `json:"preferred_color"` + 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"` } diff --git a/internal/model/json.go b/internal/model/json.go index 8b53790..535bd3b 100644 --- a/internal/model/json.go +++ b/internal/model/json.go @@ -1 +1,84 @@ package model + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +// JSONMap 是一个通用的 JSON 类型(map[string]any) +type JSONMap map[string]any + +func (JSONMap) GormDataType() string { + return "json" +} + +func (JSONMap) GormDBDataType(db *gorm.DB, _ *schema.Field) string { + switch db.Dialector.Name() { + case "mysql": + return "JSON" + case "postgres": + return "JSONB" + default: // sqlite 等 + return "TEXT" + } +} + +// Value 实现 driver.Valuer,用于写入数据库 +func (j JSONMap) Value() (driver.Value, error) { + if j == nil { + return nil, nil + } + b, err := json.Marshal(map[string]any(j)) + if err != nil { + return nil, err + } + return string(b), nil +} + +// Scan 实现 sql.Scanner,用于从数据库读取并反序列化 +func (j *JSONMap) Scan(src any) error { + if src == nil { + *j = nil + return nil + } + var data []byte + switch v := src.(type) { + case string: + data = []byte(v) + case []byte: + data = v + default: + return fmt.Errorf("cannot scan JSONMap from %T", src) + } + if len(data) == 0 { + *j = nil + return nil + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return err + } + *j = JSONMap(m) + return nil +} + +func (j JSONMap) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any(j)) +} + +func (j *JSONMap) UnmarshalJSON(b []byte) error { + if len(b) == 0 || string(b) == "null" { + *j = nil + return nil + } + var m map[string]any + if err := json.Unmarshal(b, &m); err != nil { + return err + } + *j = JSONMap(m) + return nil +} diff --git a/internal/model/kv.go b/internal/model/kv.go index 7905807..6649f65 100644 --- a/internal/model/kv.go +++ b/internal/model/kv.go @@ -1,5 +1,6 @@ -package main - -func main() { +package model +type KV struct { + Key string `gorm:"primaryKey;type:varchar(64);not null;comment:键"` + Value JSONMap } diff --git a/internal/model/user.go b/internal/model/user.go index 24855b1..006f9f7 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -1,41 +1,43 @@ package model import ( - "github.com/snowykami/neo-blog/internal/dto" - "gorm.io/gorm" + "github.com/snowykami/neo-blog/internal/dto" + "gorm.io/gorm" ) type User struct { - gorm.Model - 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 // 密码,存储加密后的值 + gorm.Model + Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一 + Nickname string `gorm:"default:''"` // 昵称 + AvatarUrl string + BackgroundUrl string + PreferredColor string `gorm:"default:'global'"` // 主题色 + 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 { - gorm.Model - UserID uint `gorm:"index"` - User User `gorm:"foreignKey:UserID;references:ID"` - Issuer string `gorm:"index"` // OIDC Issuer - Sub string `gorm:"index"` // OIDC Sub openid + gorm.Model + UserID uint `gorm:"index"` + User User `gorm:"foreignKey:UserID;references:ID"` + Issuer string `gorm:"index"` // OIDC Issuer + Sub string `gorm:"index"` // OIDC Sub openid } func (user *User) ToDto() dto.UserDto { - return dto.UserDto{ - 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, - } + return dto.UserDto{ + ID: user.ID, + Username: user.Username, + Nickname: user.Nickname, + AvatarUrl: user.AvatarUrl, + BackgroundUrl: user.BackgroundUrl, + PreferredColor: user.PreferredColor, + Email: user.Email, + Gender: user.Gender, + Role: user.Role, + Language: user.Language, + } } diff --git a/internal/repo/init.go b/internal/repo/init.go index 3aaf9df..f4d625a 100644 --- a/internal/repo/init.go +++ b/internal/repo/init.go @@ -129,6 +129,7 @@ func migrate() error { &model.Label{}, &model.Like{}, &model.File{}, + &model.KV{}, &model.OidcConfig{}, &model.Post{}, &model.Session{}, diff --git a/internal/repo/kv.go b/internal/repo/kv.go index 7905807..90f260a 100644 --- a/internal/repo/kv.go +++ b/internal/repo/kv.go @@ -1,5 +1,50 @@ -package main +package repo -func main() { +import ( + "errors" + "github.com/snowykami/neo-blog/internal/model" + "gorm.io/gorm" +) + +type kvRepo struct{} + +var KV = &kvRepo{} + +func (k *kvRepo) GetKV(key string, defaultValue ...any) (any, error) { + var kv = &model.KV{} + err := GetDB().First(kv, "key = ?", key).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if len(defaultValue) > 0 { + return defaultValue[0], nil + } + return nil, nil + } + return nil, err + } + + if kv.Value == nil { + if len(defaultValue) > 0 { + return defaultValue[0], nil + } + return nil, nil + } + + if v, ok := kv.Value["value"]; ok { + return v, nil + } + + if len(defaultValue) > 0 { + return defaultValue[0], nil + } + return nil, nil +} + +func (k *kvRepo) SetKV(key string, value any) error { + kv := &model.KV{ + Key: key, + Value: map[string]any{"value": value}, + } + return GetDB().Save(kv).Error } diff --git a/internal/repo/kv_test.go b/internal/repo/kv_test.go index e0281bf..b9f110f 100644 --- a/internal/repo/kv_test.go +++ b/internal/repo/kv_test.go @@ -1 +1,18 @@ package repo + +import "testing" + +func TestKvRepo_GetKV(t *testing.T) { + err := KV.SetKV("AAA", map[string]interface{}{"b": 1, "c": "2"}) + if err != nil { + t.Fatal(err) + } + v, _ := KV.GetKV("AAA") + t.Log(v) + if v.(map[string]interface{})["b"] != float64(1) { + t.Fatal("b not equal") + } + if v.(map[string]interface{})["c"] != "2" { + t.Fatal("c not equal") + } +} diff --git a/internal/router/apiv1/misc.go b/internal/router/apiv1/misc.go index c0e14f5..5a05f8a 100644 --- a/internal/router/apiv1/misc.go +++ b/internal/router/apiv1/misc.go @@ -1 +1,18 @@ 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" +) + +func registerMiscRoutes(group *route.RouterGroup) { + miscController := v1.NewMiscController() + miscGroupAdmin := group.Group("/misc").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin)) + miscGroupWithoutAuth := group.Group("/misc").Use(middleware.UseAuth(false)) + { + miscGroupWithoutAuth.GET("/site-info", miscController.GetSiteInfo) + miscGroupAdmin.PUT("/site-info", miscController.SetSiteInfo) + } +} diff --git a/internal/router/apiv1/v1.go b/internal/router/apiv1/v1.go index 2101e5e..b3dc582 100644 --- a/internal/router/apiv1/v1.go +++ b/internal/router/apiv1/v1.go @@ -3,15 +3,16 @@ package apiv1 import "github.com/cloudwego/hertz/pkg/app/server" func RegisterRoutes(h *server.Hertz) { - apiV1Group := h.Group("/api/v1") - { - registerCommentRoutes(apiV1Group) - registerAdminRoutes(apiV1Group) - registerFileRoutes(apiV1Group) - registerLabelRoutes(apiV1Group) - registerLikeRoutes(apiV1Group) - registerPageRoutes(apiV1Group) - registerPostRoutes(apiV1Group) - registerUserRoutes(apiV1Group) - } + apiV1Group := h.Group("/api/v1") + { + registerCommentRoutes(apiV1Group) + registerAdminRoutes(apiV1Group) + registerFileRoutes(apiV1Group) + registerLabelRoutes(apiV1Group) + registerLikeRoutes(apiV1Group) + registerMiscRoutes(apiV1Group) + registerPageRoutes(apiV1Group) + registerPostRoutes(apiV1Group) + registerUserRoutes(apiV1Group) + } } diff --git a/internal/service/user.go b/internal/service/user.go index 69d99e3..747ade1 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -355,10 +355,12 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e Model: gorm.Model{ ID: req.ID, }, - Username: req.Username, - Nickname: req.Nickname, - Gender: req.Gender, - AvatarUrl: req.AvatarUrl, + Username: req.Username, + Nickname: req.Nickname, + Gender: req.Gender, + AvatarUrl: req.AvatarUrl, + BackgroundUrl: req.BackgroundUrl, + PreferredColor: req.PreferredColor, } err := repo.User.UpdateUser(user) if err != nil { diff --git a/web/src/api/misc.ts b/web/src/api/misc.ts new file mode 100644 index 0000000..0a9b5ac --- /dev/null +++ b/web/src/api/misc.ts @@ -0,0 +1,9 @@ +import { BaseResponse } from "@/models/resp"; +import axiosClient from "./client"; +import type { SiteInfo } from "@/contexts/site-info-context"; + + +export async function getSiteInfo(): Promise>{ + const res = await axiosClient.get>('/misc/site-info'); + return res.data; +} \ No newline at end of file diff --git a/web/src/api/user.ts b/web/src/api/user.ts index 3f2f6a8..046160e 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -16,7 +16,6 @@ export async function userLogin( rememberMe?: boolean, captcha?: string, }): Promise> { - console.log("Logging in with captcha:", captcha) const res = await axiosClient.post>( '/user/login', { username, password, rememberMe }, diff --git a/web/src/app/(main)/layout.tsx b/web/src/app/(main)/layout.tsx index 4f3e464..a161b85 100644 --- a/web/src/app/(main)/layout.tsx +++ b/web/src/app/(main)/layout.tsx @@ -4,19 +4,22 @@ import { motion } from 'motion/react' import { Navbar } from '@/components/layout/nav/navbar-or-side' import { BackgroundProvider } from '@/contexts/background-context' import Footer from '@/components/layout/footer' -import config from '@/config' +import { useSiteInfo } from '@/contexts/site-info-context' export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + + const { siteInfo } = useSiteInfo(); + if (!siteInfo) return null; return ( <> + transition={{ duration: siteInfo.animationDurationSecond, ease: "easeOut" }}>
diff --git a/web/src/app/console/user-preference/page.tsx b/web/src/app/console/user-preference/page.tsx index e07a5d3..05212f1 100644 --- a/web/src/app/console/user-preference/page.tsx +++ b/web/src/app/console/user-preference/page.tsx @@ -1,3 +1,5 @@ +import { UserPreferencePage } from "@/components/console/user-preference"; + export default function Page() { - return
个性化设置
+ return } \ No newline at end of file diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 60b6e00..ed5a6e5 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -4,13 +4,13 @@ import { Geist, Geist_Mono } from "next/font/google"; import { DeviceProvider } from "@/contexts/device-context"; import { NextIntlClientProvider } from 'next-intl'; import { AuthProvider } from "@/contexts/auth-context"; -import config from "@/config"; import { getFirstLocale } from '@/i18n/request'; import { Toaster } from "@/components/ui/sonner" import { getLoginUser } from "@/api/user"; import { NuqsAdapter } from 'nuqs/adapters/next/app' import "./globals.css"; - +import { fallbackSiteInfo, SiteInfoProvider } from "@/contexts/site-info-context"; +import { getSiteInfo } from "@/api/misc"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -22,23 +22,58 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: config.metadata.name, - description: config.metadata.description, -}; +export async function generateMetadata(): Promise { + const siteInfo = await getSiteInfo().then(res => res.data).catch(() => fallbackSiteInfo); + const siteName = siteInfo?.metadata?.name ?? "Snowykami's Blog"; + const description = siteInfo?.metadata?.description ?? "分享一些好玩的东西"; + const icon = siteInfo?.metadata?.icon ?? "/favicon.ico"; + const defaultImage = siteInfo?.defaultCover ?? icon; + + return { + title: { + default: siteName, + template: `%s - ${siteName}`, + }, + description, + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), + icons: { + icon, + apple: icon, + }, + openGraph: { + title: siteName, + description, + type: 'website', + images: [ + { + url: defaultImage, + alt: siteName, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: siteName, + description, + images: defaultImage ? [defaultImage] : undefined, + }, + }; +} export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - + const token = (await cookies()).get("token")?.value || ""; const refreshToken = (await cookies()).get("refresh_token")?.value || ""; const user = await getLoginUser({ token, refreshToken }).then(res => res.data).catch(() => null); + const siteInfo = await getSiteInfo().then(res => res.data).catch(() => fallbackSiteInfo); + const colorSchemes = siteInfo?.colorSchemes ? siteInfo.colorSchemes : fallbackSiteInfo.colorSchemes; return ( - + @@ -47,7 +82,9 @@ export default async function RootLayout({ - {children} + + {children} + diff --git a/web/src/app/styles/blue.css b/web/src/app/styles/blue.css index a1a9954..f429298 100644 --- a/web/src/app/styles/blue.css +++ b/web/src/app/styles/blue.css @@ -1,4 +1,4 @@ -:root[user-color="blue"] { +[data-user-color="blue"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.623 0.214 259.815); } -.dark[user-color="blue"] { +.dark[data-user-color="blue"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/green.css b/web/src/app/styles/green.css index 25f4f2f..2c3ddfe 100644 --- a/web/src/app/styles/green.css +++ b/web/src/app/styles/green.css @@ -1,4 +1,4 @@ -:root[user-color="green"] { +[data-user-color="green"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.723 0.219 149.579); } -.dark[user-color="green"] { +.dark[data-user-color="green"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/orange.css b/web/src/app/styles/orange.css index d98efce..80b51a9 100644 --- a/web/src/app/styles/orange.css +++ b/web/src/app/styles/orange.css @@ -1,4 +1,4 @@ -:root[user-color="orange"] { +[data-user-color="orange"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.705 0.213 47.604); } -.dark[user-color="orange"] { +.dark[data-user-color="orange"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/red.css b/web/src/app/styles/red.css index a87d519..87f6ebb 100644 --- a/web/src/app/styles/red.css +++ b/web/src/app/styles/red.css @@ -1,4 +1,4 @@ -:root[user-color="red"] { +[data-user-color="red"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.637 0.237 25.331); } -.dark[user-color="red"] { +.dark[data-user-color="red"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/rose.css b/web/src/app/styles/rose.css index 3d16b53..4b8a37c 100644 --- a/web/src/app/styles/rose.css +++ b/web/src/app/styles/rose.css @@ -1,4 +1,4 @@ -:root[user-color="rose"] { +[data-user-color="rose"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.645 0.246 16.439); } -.dark[user-color="rose"] { +.dark[data-user-color="rose"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/violet.css b/web/src/app/styles/violet.css index 6e25c35..905c54e 100644 --- a/web/src/app/styles/violet.css +++ b/web/src/app/styles/violet.css @@ -1,4 +1,4 @@ -:root[user-color="violet"] { +[data-user-color="violet"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.606 0.25 292.717); } -.dark[user-color="violet"] { +.dark[data-user-color="violet"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/app/styles/yellow.css b/web/src/app/styles/yellow.css index 6c89599..8581614 100644 --- a/web/src/app/styles/yellow.css +++ b/web/src/app/styles/yellow.css @@ -1,4 +1,4 @@ -:root[user-color="violet"] { +[data-user-color="yellow"] { --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); @@ -33,7 +33,7 @@ --sidebar-ring: oklch(0.795 0.184 86.047); } -.dark[user-color="violet"] { +.dark[data-user-color="yellow"] { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); diff --git a/web/src/components/auth/common/auth-header.tsx b/web/src/components/auth/common/auth-header.tsx index fd6fcbe..874cf6b 100644 --- a/web/src/components/auth/common/auth-header.tsx +++ b/web/src/components/auth/common/auth-header.tsx @@ -1,21 +1,24 @@ -import config from "@/config"; +import { useSiteInfo } from "@/contexts/site-info-context"; import Image from "next/image"; import Link from "next/link"; export function AuthHeader() { + const { siteInfo } = useSiteInfo(); + + if (!siteInfo) return null; return (
Logo
- {config.metadata.name} + {siteInfo.metadata?.name || ''}
) diff --git a/web/src/components/auth/login/login-form.tsx b/web/src/components/auth/login/login-form.tsx index 71b81f0..e7ed62a 100644 --- a/web/src/components/auth/login/login-form.tsx +++ b/web/src/components/auth/login/login-form.tsx @@ -103,7 +103,7 @@ export function LoginForm({ {user && } {t("with_oidc")} -
+
{/* OIDC 登录选项 */} {oidcConfigs.length > 0 && ( @@ -168,9 +168,8 @@ export function LoginForm({
}