├── .gitignore ├── dandelion-admin-server ├── .cursorrules ├── README.md ├── base-server │ ├── Tools │ │ └── encrypt │ │ │ ├── md5.go │ │ │ └── md5_test.go │ ├── boot │ │ ├── boot.go │ │ └── db.go │ ├── cmd │ │ ├── api │ │ │ └── server.go │ │ └── cobra.go │ ├── config │ │ ├── config.go │ │ └── configs_local.yaml │ ├── global │ │ └── global.go │ ├── internal │ │ ├── dao │ │ │ ├── base.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_role.go │ │ │ └── sys_user.go │ │ ├── enum │ │ │ └── response.go │ │ ├── logic │ │ │ ├── authorize.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_role.go │ │ │ └── sys_user.go │ │ ├── model │ │ │ ├── base.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_role.go │ │ │ └── sys_user.go │ │ └── service │ │ │ ├── api.go │ │ │ ├── authorize.go │ │ │ ├── sys_menu.go │ │ │ ├── sys_role.go │ │ │ └── sys_user.go │ ├── main.go │ └── static │ │ └── base-server.txt ├── gateway │ ├── cmd │ │ ├── api │ │ │ └── server.go │ │ └── cobra.go │ ├── config │ │ └── configs_local.yaml │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── gen_swagger.sh │ ├── internal │ │ ├── route │ │ │ ├── base_server.go │ │ │ └── route.go │ │ └── service │ │ │ └── base-server │ │ │ ├── authorize_controller.go │ │ │ ├── service.go │ │ │ ├── sys_menu_controller.go │ │ │ ├── sys_role_controller.go │ │ │ └── sys_user_controller.go │ ├── main.go │ └── static │ │ └── gateway.txt ├── go.mod ├── go.sum ├── proto │ ├── Makefile │ ├── base │ │ ├── authorize.pb.go │ │ ├── authorize.proto │ │ ├── server.pb.go │ │ ├── server.proto │ │ ├── server.rpcx.pb.go │ │ ├── sys_menu.pb.go │ │ ├── sys_menu.proto │ │ ├── sys_role.pb.go │ │ ├── sys_role.proto │ │ ├── sys_user.pb.go │ │ └── sys_user.proto │ └── gen.sh └── tools │ └── encrypt │ ├── md5.go │ └── md5_test.go └── dandelion-admin-ui ├── .cursorrules ├── .env ├── .env.development ├── README.md ├── docs ├── cursor教程.md └── img │ ├── image-1.png │ ├── image-10.png │ ├── image-2.png │ ├── image-3.png │ ├── image-4.png │ ├── image-5.png │ ├── image-6.png │ ├── image-7.png │ ├── image-8.png │ └── image-9.png ├── eslint.config.js ├── index.html ├── mock ├── menu.ts └── user.ts ├── package.json ├── pnpm-lock.yaml ├── public └── vite.svg ├── src ├── App.tsx ├── api │ ├── menu.ts │ ├── role.ts │ ├── types.ts │ └── user.ts ├── assets │ └── react.svg ├── components │ ├── AuthGuard │ │ └── index.tsx │ └── IconSelect │ │ ├── index.css │ │ └── index.tsx ├── hooks │ └── useAuth.ts ├── index.css ├── layouts │ ├── BasicLayout.tsx │ └── components │ │ ├── Breadcrumb.tsx │ │ ├── Header.tsx │ │ ├── Sider.tsx │ │ ├── TabView.tsx │ │ └── menuConfig.tsx ├── main.tsx ├── pages │ ├── dashboard │ │ └── index.tsx │ ├── error │ │ ├── 403.tsx │ │ └── 404.tsx │ ├── login │ │ ├── index.module.css │ │ └── index.tsx │ ├── menu │ │ ├── components │ │ │ ├── MenuForm.tsx │ │ │ └── index.css │ │ ├── index.css │ │ └── index.tsx │ └── system │ │ ├── role │ │ ├── components │ │ │ ├── AssignUsersForm.module.css │ │ │ ├── AssignUsersForm.tsx │ │ │ ├── CreateRoleForm.tsx │ │ │ └── UpdateRoleForm.tsx │ │ ├── index.module.css │ │ └── index.tsx │ │ └── user │ │ ├── components │ │ ├── CreateUserForm.tsx │ │ ├── UpdateUserForm.tsx │ │ └── UserForm.tsx │ │ ├── index.module.css │ │ └── index.tsx ├── router │ └── index.tsx ├── store │ ├── menu.ts │ ├── tab.ts │ └── user.ts ├── types │ └── menu.ts ├── utils │ ├── error.ts │ └── request.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vscode/ 4 | *.suo 5 | *.ntvs* 6 | *.njsproj 7 | *.sln 8 | *.sw? 9 | 10 | # Logs 11 | logs/ 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Dependencies 19 | node_modules/ 20 | dist/ 21 | dist-ssr/ 22 | *.local 23 | 24 | # System 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Go 29 | bin/ 30 | *.exe 31 | *.exe~ 32 | *.dll 33 | *.so 34 | *.dylib 35 | *.test 36 | *.out 37 | go.work -------------------------------------------------------------------------------- /dandelion-admin-server/.cursorrules: -------------------------------------------------------------------------------- 1 | ## 业务编写涉及文件 2 | 强调:业务开发前需要对已有的功能编写流程以及规范进行了解。 3 | 4 | 我来解释下登录业务代码涉及的文件结构,你需要结合一下解释并对代码进行理解: 5 | 1.proto 定义层 (proto/base/): 6 | + authorize.proto: 定义登录接口的请求参数和响应结构 7 | + server.proto: 定义 RPC 服务接口 8 | 2. 网关层 (gateway/): 9 | + internal/route/base_server.go: 注册 HTTP 路由 10 | + internal/service/base-server/authorize_controller.go: HTTP 控制器,处理请求并转发到 RPC 服务 11 | 3. 业务服务层 (base-server/): 12 | + internal/service/authorize.go: RPC 服务实现 13 | + internal/logic/authorize.go: 具体业务逻辑实现 14 | + internal/model/sys_user.go: 数据模型定义 15 | + internal/dao/: 数据访问层(从搜索结果看应该有 sys_user.go) 16 | 编写新业务时的流程: 17 | 1. 在 proto/base/ 中定义服务接口和数据结构 18 | 2. 在 base-server/internal/model/ 中定义数据模型 19 | 3. 在 base-server/internal/dao/ 中实现数据访问方法 20 | 4. 在 base-server/internal/logic/ 中实现具体业务逻辑 21 | 5. 在 base-server/internal/service/ 中实现 RPC 服务接口 22 | 6. 在 gateway/internal/route/ 中注册 HTTP 路由 23 | 7. 在 gateway/internal/service/base-server/ 中实现 HTTP 控制器 24 | 25 | ## 编写业务流程 26 | 1.proto编写 27 | + 需要先进行proto定义,proto的规范需要遵循 authorize.proto 的规 28 | 范,每次生成前都应该先对 authorize.proto 中的定义进行梳理 29 | + 每个message中的字段都需要设置 gogoproto.jsontag 30 | + proto文件按照功能分离 31 | + 所有的service方法定义都应该在server.proto中, 具体参照已有的菜单管理规范 32 | + proto编写完需要再proto目录下执行 ./gen.sh 生成pb.go文件 33 | 34 | 2.业务开发 35 | + 在base-server/internal/model/中定义数据模型,数据模型需要遵循已有的菜单管理规范 36 | + 在base-server/internal/dao/中实现数据访问方法,dao层中业务需要根据实际情况选择继承application.DB或application.Redis 37 | + 在base-server/internal/logic/中实现具体业务逻辑,具体业务逻辑需要遵循已有的菜单管理规范 38 | + 错误响应需要再base-server/internal/enum/response.go中定义并且使用Error初始化,如果有其他的常量需要定义,你可以在consts.go中定义 39 | + 通用的拓展组件需要再base-server/tools/中实现 40 | + 在base-server/internal/service/中实现RPC服务接口,需要严格参照已有的功能规范开发,方法名以及参数需要和server.proto中的定义一致 41 | + 在gateway/internal/route/中注册HTTP路由,HTTP路由需要遵循已有的菜单管理规范 42 | + 在gateway/internal/service/base-server/中实现HTTP控制器,HTTP控制器需要遵循已有的菜单管理规范。 43 | 注意需要根据已有的swagger注释规范进行注释,路由地址默认带有/api前缀 44 | 45 | swagger注释规范: 46 | ``` 47 | // GetSysUserList 48 | // @tags 系统用户管理 49 | // @summary 获取用户列表 50 | // @description 获取系统用户列表 51 | // @router /api/sys_user/search [get] 52 | // @param req body base.GetSysUserListReq true "json入参" 53 | // @success 200 {object} base.GetSysUserListResp "返回值" 54 | ``` 55 | 56 | 3.注意 57 | + 所有开发中需要遵循已有的功能规范 58 | + 如果没有则先进行询问,不要自作主张 59 | + 开发需要先进行功能梳理以及代码评审,评审通过后再进行开发 60 | + 如果某个查询列表存在查询条件,接口最好post而不是get 61 | 62 | -------------------------------------------------------------------------------- /dandelion-admin-server/README.md: -------------------------------------------------------------------------------- 1 | # Dandelion Admin Server 2 | 3 | 基于 [go-dandelion](https://github.com/team-dandelion/go-dandelion) 微服务框架开发的后台管理系统服务端。 4 | 5 | ## 项目构建构成 6 | 7 | 1. 创建项目目录以及初始化mod 8 | ```shell 9 | mkdir dandelion-admin-server && cd dandelion-admin-server && go mod init dandelion-admin-server 10 | ``` 11 | 2. 创建base-server和gateway服务 12 | ```shell 13 | (base) ➜ dandelion-admin-server go-dandelion-cli server 14 | Type of service you want to create, enter a number(1-rpc 2-http):1 15 | Current road strength: /Users/mac/my-project/dandelion-admin/dandelion-admin-server 16 | RPC Server Name:base-server 17 | Whether to use the default initial configuration?(y/n):y 18 | stat /Users/mac/my-project/dandelion-admin/dandelion-admin-server/base-server/static/base-server.txt: no such file or directory 19 | (base) ➜ dandelion-admin-server go-dandelion-cli server 20 | Type of service you want to create, enter a number(1-rpc 2-http):2 21 | Current road strength: /Users/mac/my-project/dandelion-admin/dandelion-admin-server 22 | HTTP Server Name:gateway 23 | Whether to use the default initial configuration?(y/n):y 24 | stat /Users/mac/my-project/dandelion-admin/dandelion-admin-server/gateway/static/gateway.txt: no such file or directory 25 | (base) ➜ dandelion-admin-server 26 | ``` 27 | 28 | 3. 创建proto目录,定义通信协议 29 | ```shell 30 | mkdir proto && mkdir proto/base 31 | ``` 32 | 33 | 4. 生成proto对应的Go代码 34 | ```shell 35 | cd proto 36 | ./gen.sh # 自动遍历所有proto文件并生成对应的Go代码 37 | ``` 38 | 39 | ## 服务说明 40 | 41 | ### base-server 42 | 43 | 基础 RPC 服务,主要功能: 44 | - 用户认证与授权 45 | - 角色权限管理 46 | - 菜单管理 47 | - 系统配置 48 | - 其他基础业务逻辑 49 | 50 | ### gateway 51 | 52 | HTTP 网关服务,主要功能: 53 | - 提供 RESTful API 54 | - 请求转发 55 | - 统一认证 56 | - 接口鉴权 57 | - 限流控制 58 | - 链路追踪 59 | 60 | ### proto 61 | 62 | Protocol Buffers 协议定义目录: 63 | - 定义服务间通信接口 64 | - 统一数据结构 65 | - 版本管理 66 | 67 | ## 技术栈 68 | 69 | - 微服务框架:go-dandelion 70 | - RPC 框架:rpcx 71 | - HTTP 框架:fasthttp 72 | - 数据库:MySQL 73 | - 缓存:Redis 74 | - 链路追踪:Jaeger 75 | - API 文档:Swagger 76 | 77 | ## 开发环境要求 78 | 79 | - Go 1.16+ 80 | - MySQL 5.7+ 81 | - Redis 6.0+ 82 | - Protocol Buffers 3 83 | 84 | ## 业务编写 85 | 86 | 基于登录接口的实现,我来解释下业务代码涉及的文件结构: 87 | 1.proto 定义层 (proto/base/): 88 | + authorize.proto: 定义登录接口的请求参数和响应结构 89 | + server.proto: 定义 RPC 服务接口 90 | 2. 网关层 (gateway/): 91 | + internal/route/base_server.go: 注册 HTTP 路由 92 | + internal/service/base-server/authorize_controller.go: HTTP 控制器,处理请求并转发到 RPC 服务 93 | 3. 业务服务层 (base-server/): 94 | + internal/service/authorize.go: RPC 服务实现 95 | + internal/logic/authorize.go: 具体业务逻辑实现 96 | + internal/model/sys_user.go: 数据模型定义 97 | + internal/dao/: 数据访问层(从搜索结果看应该有 sys_user.go) 98 | 编写新业务时的流程: 99 | 1. 在 proto/base/ 中定义服务接口和数据结构 100 | 2. 在 base-server/internal/model/ 中定义数据模型 101 | 3. 在 base-server/internal/dao/ 中实现数据访问方法 102 | 4. 在 base-server/internal/logic/ 中实现具体业务逻辑 103 | 5. 在 base-server/internal/service/ 中实现 RPC 服务接口 104 | 6. 在 gateway/internal/route/ 中注册 HTTP 路由 105 | 7. 在 gateway/internal/service/base-server/ 中实现 HTTP 控制器 106 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/Tools/encrypt/md5.go: -------------------------------------------------------------------------------- 1 | package encrypt 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | // MD5 计算字符串的MD5值,返回32位小写字符串 10 | func MD5(str string) string { 11 | h := md5.New() 12 | h.Write([]byte(str)) 13 | return hex.EncodeToString(h.Sum(nil)) 14 | } 15 | 16 | // MD5Upper 计算字符串的MD5值,返回32位大写字符串 17 | func MD5Upper(str string) string { 18 | return strings.ToUpper(MD5(str)) 19 | } 20 | 21 | // MD5WithSalt 计算带盐值的MD5,返回32位小写字符串 22 | func MD5WithSalt(str, salt string) string { 23 | return MD5(str + salt) 24 | } 25 | 26 | // MD5WithSaltUpper 计算带盐值的MD5,返回32位大写字符串 27 | func MD5WithSaltUpper(str, salt string) string { 28 | return strings.ToUpper(MD5WithSalt(str, salt)) 29 | } 30 | 31 | // VerifyMD5 验证字符串与MD5是否匹配(不区分大小写) 32 | func VerifyMD5(str, md5str string) bool { 33 | return strings.EqualFold(MD5(str), md5str) 34 | } 35 | 36 | // VerifyMD5WithSalt 验证字符串与加盐的MD5是否匹配(不区分大小写) 37 | func VerifyMD5WithSalt(str, salt, md5str string) bool { 38 | return strings.EqualFold(MD5WithSalt(str, salt), md5str) 39 | } 40 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/Tools/encrypt/md5_test.go: -------------------------------------------------------------------------------- 1 | package encrypt 2 | 3 | import "testing" 4 | 5 | func TestMD5(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expected string 10 | }{ 11 | { 12 | name: "empty string", 13 | input: "", 14 | expected: "d41d8cd98f00b204e9800998ecf8427e", 15 | }, 16 | { 17 | name: "hello world", 18 | input: "hello world", 19 | expected: "5eb63bbbe01eeed093cb22bb8f5acdc3", 20 | }, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := MD5(tt.input); got != tt.expected { 26 | t.Errorf("MD5() = %v, want %v", got, tt.expected) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func TestMD5WithSalt(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | str string 36 | salt string 37 | expected string 38 | }{ 39 | { 40 | name: "with empty salt", 41 | str: "password", 42 | salt: "", 43 | expected: "5f4dcc3b5aa765d61d8327deb882cf99", 44 | }, 45 | { 46 | name: "with salt", 47 | str: "password", 48 | salt: "123", 49 | expected: "123934bb19708f8dac76a2e31f91cef0", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := MD5WithSalt(tt.str, tt.salt); got != tt.expected { 56 | t.Errorf("MD5WithSalt() = %v, want %v", got, tt.expected) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestVerifyMD5(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | str string 66 | md5str string 67 | expected bool 68 | }{ 69 | { 70 | name: "correct md5 lowercase", 71 | str: "hello world", 72 | md5str: "5eb63bbbe01eeed093cb22bb8f5acdc3", 73 | expected: true, 74 | }, 75 | { 76 | name: "correct md5 uppercase", 77 | str: "hello world", 78 | md5str: "5EB63BBBE01EEED093CB22BB8F5ACDC3", 79 | expected: true, 80 | }, 81 | { 82 | name: "incorrect md5", 83 | str: "hello world", 84 | md5str: "wrongmd5hash", 85 | expected: false, 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | if got := VerifyMD5(tt.str, tt.md5str); got != tt.expected { 92 | t.Errorf("VerifyMD5() = %v, want %v", got, tt.expected) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/boot/boot.go: -------------------------------------------------------------------------------- 1 | package boot 2 | 3 | import ( 4 | "github.com/gly-hub/dandelion-plugs/jwt" 5 | "github.com/team-dandelion/go-dandelion/application" 6 | ) 7 | 8 | func Init() { 9 | // 将需要初始化的方法在该处注册 10 | _ = application.Plugs(jwt.Plug()) 11 | DbAutoMigrate() 12 | } 13 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/boot/db.go: -------------------------------------------------------------------------------- 1 | package boot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gly-hub/dandelion-plugs/redislock" 6 | "github.com/team-dandelion/go-dandelion/application" 7 | "github.com/team-dandelion/go-dandelion/logger" 8 | ) 9 | 10 | type DatabaseModel interface { 11 | TableName() string 12 | TableComment() string 13 | } 14 | 15 | var models = make([]DatabaseModel, 0) 16 | 17 | func Register(model ...DatabaseModel) { 18 | if len(model) == 0 { 19 | return 20 | } 21 | models = append(models, model...) 22 | } 23 | 24 | // DbAutoMigrate 异步 25 | func DbAutoMigrate() { 26 | task := func() { 27 | migrate() 28 | } 29 | go task() 30 | return 31 | } 32 | 33 | func migrate() bool { 34 | dbIns := (&application.DB{}).GetDB() 35 | if dbIns == nil { 36 | return false 37 | } 38 | dbIns = dbIns.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci") 39 | redisLock, err := redislock.NewDistributeLockRedis(fmt.Sprintf("go_dandelion_db_migrate"), 600, "1") 40 | if err != nil { 41 | logger.Error(err, "Migrate Model Error") 42 | return false 43 | } 44 | defer func() { 45 | _ = redisLock.Unlock() 46 | }() 47 | 48 | for _, model := range models { 49 | //if dbIns.Migrator().HasTable(model) { 50 | // continue 51 | //} 52 | err = dbIns.Migrator().AutoMigrate(model) 53 | dbIns.Exec(fmt.Sprintf("ALTER TABLE %s COMMENT '%s'", model.TableName(), model.TableComment())) 54 | if err != nil { 55 | logger.Error(err, "Migrate Model Error") 56 | } 57 | } 58 | return true 59 | } 60 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/cmd/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "dandelion-admin-server/base-server/boot" 6 | "dandelion-admin-server/base-server/internal/service" 7 | "github.com/team-dandelion/go-dandelion/application" 8 | "github.com/team-dandelion/go-dandelion/config" 9 | config2 "dandelion-admin-server/base-server/config" 10 | "github.com/team-dandelion/go-dandelion/logger" 11 | "github.com/gly-hub/toolbox/stringx" 12 | "github.com/spf13/cobra" 13 | "io/ioutil" 14 | "os" 15 | "os/signal" 16 | ) 17 | 18 | var ( 19 | env string 20 | StartCmd = &cobra.Command{ 21 | Use: "server", 22 | Short: "Start RPC server", 23 | Example: "base-server server -e local", 24 | SilenceUsage: true, 25 | PreRun: func(cmd *cobra.Command, args []string) { 26 | setup() 27 | }, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | return run() 30 | }, 31 | } 32 | ) 33 | 34 | func init() { 35 | StartCmd.PersistentFlags().StringVarP(&env, "env", "e", "local", "Env") 36 | } 37 | 38 | func setup() { 39 | // 配置初始化 40 | config.InitConfig(env, &config2.CustomConfig) 41 | // 应用初始化 42 | application.Init() 43 | // 初始化服务方法 44 | boot.Init() 45 | } 46 | 47 | func run() error { 48 | // 初始化rpc model 49 | go func() { 50 | application.RpcServer(new(service.RpcApi)) 51 | }() 52 | content, _ := ioutil.ReadFile("./static/base-server.txt") 53 | fmt.Println(logger.Green(string(content))) 54 | quit := make(chan os.Signal) 55 | signal.Notify(quit, os.Interrupt) 56 | <-quit 57 | fmt.Printf("%s Shutdown Server ... \r\n", stringx.GetCurrentTimeStr()) 58 | logger.Info("Server exiting") 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/cmd/cobra.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "dandelion-admin-server/base-server/cmd/api" 6 | "github.com/team-dandelion/go-dandelion/logger" 7 | "github.com/spf13/cobra" 8 | "os" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "dandelion-admin-server/base-server", 13 | Short: "base-server", 14 | SilenceUsage:true, 15 | Long: "authorize", 16 | Args: func(cmd *cobra.Command, args []string) error { 17 | if len(args) < 1 { 18 | return errors.New(logger.Red("requires at least one arg")) 19 | } 20 | return nil 21 | }, 22 | PersistentPostRunE: func(cmd *cobra.Command, args []string) error { 23 | return nil 24 | }, 25 | } 26 | 27 | func init(){ 28 | rootCmd.AddCommand(api.StartCmd) 29 | } 30 | 31 | func Execute(){ 32 | if err := rootCmd.Execute(); err != nil{ 33 | os.Exit(-1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | CustomConfig *CustomConfigModel 5 | ) 6 | 7 | type CustomConfigModel struct { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/config/configs_local.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | #Level 0 紧急的 1警报 2重要的 3错误 4警告 5提示 6信息 7调试 3 | consoleShow: true 4 | consoleLevel: 7 5 | fileWrite: false 6 | fileLevel: 7 7 | multiFileWrite: false 8 | multiFileLevel: 7 9 | 10 | rpcServer: 11 | serverName: "BaseServerService" 12 | registerPlugin: "multiple" 13 | registerServers: 14 | - "127.0.0.1:2379" 15 | basePath: "dandelion-admin-server" 16 | addr: "" 17 | port: 8899 18 | pprof: 18899 19 | 20 | db: 21 | dbType: "mysql" 22 | maxOpenConn: 20 23 | maxIdleConn: 4 24 | maxIdleTime: 100 25 | maxLifeTime: 3600 26 | level: 4 27 | slowThreshold: "100ms" 28 | master: 29 | user: "root" 30 | password: "password" 31 | host: "127.0.0.1" 32 | port: "3306" 33 | database: "dandelion-admin" 34 | slave: 35 | - user: "root" 36 | password: "password" 37 | host: "127.0.0.1" 38 | port: "3306" 39 | database: "dandelion-admin" 40 | 41 | redis: 42 | redisType: "alone" 43 | startAddr: ["127.0.0.1:6379"] 44 | active: 100 45 | idle: 100 46 | auth: "" 47 | connTimeout: "100ms" 48 | readTimeout: "100ms" 49 | writeTimeout: "100ms" 50 | idleTimeout: "100ms" 51 | 52 | tracer: 53 | openTrace: true 54 | traceName: "base-server" 55 | host: "127.0.0.1:6831" 56 | 57 | jwt: 58 | model: "unique" 59 | key: "go-bookstore-fangaoo" 60 | expireTime: 60*60*24 -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/dao/base.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "gorm.io/gorm" 4 | 5 | type BaseDao struct { 6 | } 7 | 8 | func (d *BaseDao) SetOffsetDefault(tx *gorm.DB, page, limit int32) *gorm.DB { 9 | if page <= 0 { 10 | page = 1 11 | } 12 | 13 | if limit <= 0 { 14 | limit = 20 15 | } 16 | 17 | return tx.Offset((int(page) - 1) * int(limit)).Limit(int(limit)) 18 | } 19 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/dao/sys_menu.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/model" 5 | 6 | "github.com/team-dandelion/go-dandelion/application" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type ISysMenu interface { 11 | GetMenuList() (list []model.SysMenu, err error) 12 | CreateMenu(menu *model.SysMenu) error 13 | UpdateMenu(menu *model.SysMenu) error 14 | DeleteMenu(id uint) error 15 | GetMenuById(id uint) (*model.SysMenu, error) 16 | BatchSortMenus(menus []*model.SysMenu) error 17 | } 18 | 19 | type SysMenu struct { 20 | application.DB 21 | } 22 | 23 | func NewSysMenu() ISysMenu { 24 | return &SysMenu{} 25 | } 26 | 27 | func (s *SysMenu) GetMenuList() (list []model.SysMenu, err error) { 28 | err = s.GetDB().Model(&model.SysMenu{}).Order("sort asc").Find(&list).Error 29 | return 30 | } 31 | 32 | func (s *SysMenu) CreateMenu(menu *model.SysMenu) error { 33 | return s.GetDB().Create(menu).Error 34 | } 35 | 36 | func (s *SysMenu) UpdateMenu(menu *model.SysMenu) error { 37 | return s.GetDB().Model(&model.SysMenu{}).Where("id = ?", menu.Id).Save(menu).Error 38 | } 39 | 40 | func (s *SysMenu) DeleteMenu(id uint) error { 41 | return s.GetDB().Delete(&model.SysMenu{}, id).Error 42 | } 43 | 44 | func (s *SysMenu) GetMenuById(id uint) (*model.SysMenu, error) { 45 | var menu model.SysMenu 46 | err := s.GetDB().First(&menu, id).Error 47 | return &menu, err 48 | } 49 | 50 | func (s *SysMenu) BatchSortMenus(menus []*model.SysMenu) error { 51 | return s.GetDB().Transaction(func(tx *gorm.DB) error { 52 | for _, menu := range menus { 53 | if err := tx.Model(&model.SysMenu{}).Where("id = ?", menu.Id).Updates(map[string]interface{}{ 54 | "parent_id": menu.ParentId, 55 | "sort": menu.Sort, 56 | }).Error; err != nil { 57 | return err 58 | } 59 | } 60 | return nil 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/dao/sys_role.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/model" 5 | 6 | "github.com/team-dandelion/go-dandelion/application" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type SysRoleDao struct { 11 | application.DB 12 | BaseDao 13 | } 14 | 15 | func NewSysRole() *SysRoleDao { 16 | return &SysRoleDao{} 17 | } 18 | 19 | // CreateRole 创建角色 20 | func (d *SysRoleDao) CreateRole(role *model.SysRole, menuIds []int32) error { 21 | return d.GetDB().Transaction(func(tx *gorm.DB) error { 22 | // 创建角色 23 | if err := tx.Create(role).Error; err != nil { 24 | return err 25 | } 26 | 27 | // 创建角色-菜单关联 28 | if len(menuIds) > 0 { 29 | roleMenus := make([]*model.SysRoleMenu, 0, len(menuIds)) 30 | for _, menuId := range menuIds { 31 | roleMenus = append(roleMenus, &model.SysRoleMenu{ 32 | RoleId: role.Id, 33 | MenuId: menuId, 34 | }) 35 | } 36 | return tx.Create(&roleMenus).Error 37 | } 38 | return nil 39 | }) 40 | } 41 | 42 | // UpdateRole 更新角色 43 | func (d *SysRoleDao) UpdateRole(role *model.SysRole, menuIds []int32) error { 44 | return d.GetDB().Transaction(func(tx *gorm.DB) error { 45 | // 更新角色信息 46 | if err := tx.Updates(role).Error; err != nil { 47 | return err 48 | } 49 | 50 | // 删除原有的角色-菜单关联 51 | if err := tx.Where("role_id = ?", role.Id).Delete(&model.SysRoleMenu{}).Error; err != nil { 52 | return err 53 | } 54 | 55 | // 创建新的角色-菜单关联 56 | if len(menuIds) > 0 { 57 | roleMenus := make([]*model.SysRoleMenu, 0, len(menuIds)) 58 | for _, menuId := range menuIds { 59 | roleMenus = append(roleMenus, &model.SysRoleMenu{ 60 | RoleId: role.Id, 61 | MenuId: menuId, 62 | }) 63 | } 64 | return tx.Create(&roleMenus).Error 65 | } 66 | return nil 67 | }) 68 | } 69 | 70 | // DeleteRole 删除角色 71 | func (d *SysRoleDao) DeleteRole(id int32) error { 72 | return d.GetDB().Transaction(func(tx *gorm.DB) error { 73 | // 删除角色 74 | if err := tx.Delete(&model.SysRole{}, id).Error; err != nil { 75 | return err 76 | } 77 | 78 | // 删除角色-菜单关联 79 | if err := tx.Where("role_id = ?", id).Delete(&model.SysRoleMenu{}).Error; err != nil { 80 | return err 81 | } 82 | 83 | // 删除用户-角色关联 84 | return tx.Where("role_id = ?", id).Delete(&model.SysUserRole{}).Error 85 | }) 86 | } 87 | 88 | // GetRoleById 根据ID获取角色信息 89 | func (d *SysRoleDao) GetRoleById(id int32) (*model.SysRole, error) { 90 | var role model.SysRole 91 | err := d.GetDB().Where("id = ?", id).First(&role).Error 92 | if err != nil { 93 | return nil, err 94 | } 95 | return &role, nil 96 | } 97 | 98 | // GetRoleList 获取角色列表 99 | func (d *SysRoleDao) GetRoleList(filter model.RoleFilter, page, pageSize int32) ([]*model.SysRole, int64, error) { 100 | var ( 101 | roles []*model.SysRole 102 | total int64 103 | db = d.GetDB().Model(&model.SysRole{}) 104 | ) 105 | 106 | // 构建查询条件 107 | if filter.RoleId > 0 { 108 | db = db.Where("id = ?", filter.RoleId) 109 | } 110 | if filter.Name != "" { 111 | db = db.Where("name LIKE ?", "%"+filter.Name+"%") 112 | } 113 | if filter.Status > 0 { 114 | db = db.Where("status = ?", filter.Status) 115 | } 116 | 117 | // 查询总数 118 | if err := db.Count(&total).Error; err != nil { 119 | return nil, 0, err 120 | } 121 | 122 | // 分页查询 123 | if err := d.SetOffsetDefault(db, page, pageSize).Find(&roles).Error; err != nil { 124 | return nil, 0, err 125 | } 126 | 127 | return roles, total, nil 128 | } 129 | 130 | // AssignUsers 分配用户 131 | func (d *SysRoleDao) AssignUsers(roleId int32, userIds []int32) error { 132 | return d.GetDB().Transaction(func(tx *gorm.DB) error { 133 | // 删除原有的用户-角色关联 134 | if err := tx.Where("role_id = ?", roleId).Delete(&model.SysUserRole{}).Error; err != nil { 135 | return err 136 | } 137 | 138 | // 创建新的用户-角色关联 139 | if len(userIds) > 0 { 140 | userRoles := make([]*model.SysUserRole, 0, len(userIds)) 141 | for _, userId := range userIds { 142 | userRoles = append(userRoles, &model.SysUserRole{ 143 | RoleId: roleId, 144 | UserId: userId, 145 | }) 146 | } 147 | return tx.Create(&userRoles).Error 148 | } 149 | return nil 150 | }) 151 | } 152 | 153 | // GetRoleMenuIds 获取角色关联的菜单ID列表 154 | func (d *SysRoleDao) GetRoleMenuIds(roleId int32) ([]int32, error) { 155 | var menuIds []int32 156 | err := d.GetDB().Model(&model.SysRoleMenu{}).Where("role_id = ?", roleId).Pluck("menu_id", &menuIds).Error 157 | return menuIds, err 158 | } 159 | 160 | // GetUserRoleIds 获取角色ID关联的用户列表 161 | func (d *SysRoleDao) GetUserRoleIds(userId int32) ([]int32, error) { 162 | var roleIds []int32 163 | err := d.GetDB().Model(&model.SysUserRole{}).Where("role_id = ?", userId).Pluck("user_id", &roleIds).Error 164 | return roleIds, err 165 | } 166 | 167 | // CheckNameExists 检查角色名是否存在 168 | func (d *SysRoleDao) CheckNameExists(name string, excludeId int32) (bool, error) { 169 | var count int64 170 | db := d.GetDB().Model(&model.SysRole{}).Where("name = ?", name) 171 | if excludeId > 0 { 172 | db = db.Where("id != ?", excludeId) 173 | } 174 | if err := db.Count(&count).Error; err != nil { 175 | return false, err 176 | } 177 | return count > 0, nil 178 | } 179 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/dao/sys_user.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/model" 5 | 6 | "github.com/team-dandelion/go-dandelion/application" 7 | ) 8 | 9 | type ISysUser interface { 10 | GetUserInfo(filter model.UserFilter) (user *model.SysUser, err error) 11 | CreateUser(user *model.SysUser) error 12 | UpdateUser(user *model.SysUser) error 13 | DeleteUser(id int32) error 14 | GetUserList(filter model.UserFilter, page, pageSize int32) (list []model.SysUser, total int64, err error) 15 | } 16 | 17 | func NewSysUser() ISysUser { 18 | return &SysUserDao{} 19 | } 20 | 21 | type SysUserDao struct { 22 | application.DB 23 | application.Redis 24 | BaseDao 25 | } 26 | 27 | func (s *SysUserDao) GetUserInfo(filter model.UserFilter) (user *model.SysUser, err error) { 28 | tx := s.GetDB().Model(&model.SysUser{}) 29 | 30 | if filter.UserId > 0 { 31 | tx = tx.Where("id = ?", filter.UserId) 32 | } 33 | 34 | if filter.UserName != "" { 35 | tx = tx.Where("user_name = ?", filter.UserName) 36 | } 37 | 38 | if err := tx.First(&user).Error; err != nil { 39 | return nil, err 40 | } 41 | 42 | return 43 | } 44 | 45 | func (s *SysUserDao) CreateUser(user *model.SysUser) error { 46 | return s.GetDB().Create(user).Error 47 | } 48 | 49 | func (s *SysUserDao) UpdateUser(user *model.SysUser) error { 50 | return s.GetDB().Model(&model.SysUser{}).Where("id = ?", user.Id).Updates(map[string]interface{}{ 51 | "nick_name": user.NickName, 52 | "avatar": user.Avatar, 53 | "phone": user.Phone, 54 | "status": user.Status, 55 | }).Error 56 | } 57 | 58 | func (s *SysUserDao) DeleteUser(id int32) error { 59 | return s.GetDB().Delete(&model.SysUser{}, id).Error 60 | } 61 | 62 | func (s *SysUserDao) GetUserList(filter model.UserFilter, page, pageSize int32) (list []model.SysUser, total int64, err error) { 63 | tx := s.GetDB().Model(&model.SysUser{}) 64 | 65 | if filter.UserName != "" { 66 | tx = tx.Where("user_name LIKE ?", "%"+filter.UserName+"%") 67 | } 68 | 69 | if filter.Phone != "" { 70 | tx = tx.Where("phone LIKE ?", "%"+filter.Phone+"%") 71 | } 72 | 73 | if filter.Status > 0 { 74 | tx = tx.Where("status = ?", filter.Status) 75 | } 76 | 77 | if err = tx.Count(&total).Error; err != nil { 78 | return 79 | } 80 | 81 | err = s.SetOffsetDefault(tx, page, pageSize).Find(&list).Error 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/enum/response.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type Error struct { 4 | Code int32 5 | Msg string 6 | } 7 | 8 | func (e *Error) Error() string { 9 | return e.Msg 10 | } 11 | 12 | var ( 13 | Success = &Error{Code: 0, Msg: "success"} 14 | Failed = &Error{Code: -1, Msg: "failed"} 15 | 16 | // 系统错误 (500xx) 17 | SystemError = &Error{Code: 50000, Msg: "系统错误"} 18 | ParamsError = &Error{Code: 50001, Msg: "参数错误"} 19 | DBError = &Error{Code: 50002, Msg: "数据库错误"} 20 | 21 | // 认证错误 (501xx) 22 | NotLogin = &Error{Code: 50100, Msg: "未登录"} 23 | NoPermission = &Error{Code: 50101, Msg: "无权限"} 24 | 25 | // 用户相关错误 (502xx) 26 | SysUserNotFound = &Error{Code: 50200, Msg: "用户不存在"} 27 | PasswordError = &Error{Code: 50201, Msg: "密码错误"} 28 | UserNameExists = &Error{Code: 50202, Msg: "用户名已存在"} 29 | 30 | // 菜单相关错误 (503xx) 31 | MenuNotFound = &Error{Code: 50300, Msg: "菜单不存在"} 32 | ParentMenuNotFound = &Error{Code: 50301, Msg: "父级菜单不存在"} 33 | ParentMenuTypeError = &Error{Code: 50302, Msg: "父级菜单类型错误"} 34 | MenuCircularDependency = &Error{Code: 50303, Msg: "菜单不能形成循环依赖"} 35 | MenuHasChildren = &Error{Code: 50304, Msg: "菜单存在子菜单,不能删除"} 36 | 37 | // 角色管理错误码 38 | RoleNameExists = &Error{Code: 20301, Msg: "角色名称已存在"} 39 | RoleNotFound = &Error{Code: 20302, Msg: "角色不存在"} 40 | RoleHasUsers = &Error{Code: 20303, Msg: "角色下存在用户,无法删除"} 41 | RoleMenuNotFound = &Error{Code: 20304, Msg: "菜单不存在"} 42 | RoleUserNotFound = &Error{Code: 20305, Msg: "用户不存在"} 43 | ) 44 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/logic/authorize.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/dao" 5 | "dandelion-admin-server/base-server/internal/enum" 6 | "dandelion-admin-server/base-server/internal/model" 7 | "dandelion-admin-server/base-server/tools/encrypt" 8 | "dandelion-admin-server/proto/base" 9 | 10 | "sort" 11 | 12 | "github.com/gly-hub/dandelion-plugs/jwt" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type IAuth interface { 17 | Login(params *base.LoginParams) (token string, err error) 18 | GetUserMenu() (list []*base.MenuTreeNode, err error) 19 | GetUserInfo(userId int32) (userInfo *base.UserInfo, err error) 20 | } 21 | 22 | type Auth struct{} 23 | 24 | func NewAuth() IAuth { 25 | return &Auth{} 26 | } 27 | 28 | func (a *Auth) Login(params *base.LoginParams) (token string, err error) { 29 | if params == nil || params.UserName == "" || params.Password == "" { 30 | err = enum.ParamsError 31 | return 32 | } 33 | 34 | user, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 35 | UserName: params.UserName, 36 | }) 37 | 38 | if err != nil && err != gorm.ErrRecordNotFound { 39 | return 40 | } 41 | 42 | if err == gorm.ErrRecordNotFound { 43 | err = enum.SysUserNotFound 44 | return 45 | } 46 | 47 | if user.Password != encrypt.MD5(params.Password) { 48 | err = enum.PasswordError 49 | return 50 | } 51 | 52 | // 生成token 53 | token, err = jwt.Token(&model.UserMeta{ 54 | UserId: user.Id, 55 | Name: user.NickName, 56 | }) 57 | 58 | return 59 | } 60 | 61 | func (a *Auth) GetUserMenu() (list []*base.MenuTreeNode, err error) { 62 | // 获取所有菜单 63 | menuList, err := dao.NewSysMenu().GetMenuList() 64 | if err != nil { 65 | return 66 | } 67 | 68 | // 转换为树状结构 69 | menuMap := make(map[uint]*base.MenuTreeNode) 70 | var rootNodes []*base.MenuTreeNode 71 | 72 | // 第一步:创建所有节点 73 | for _, menu := range menuList { 74 | // 只返回目录和菜单类型,不返回按钮 75 | if menu.Type > 2 { 76 | continue 77 | } 78 | node := &base.MenuTreeNode{ 79 | Id: uint32(menu.Id), 80 | ParentId: uint32(menu.ParentId), 81 | Name: menu.Name, 82 | Path: menu.Path, 83 | Type: int32(menu.Type), 84 | Icon: menu.Icon, 85 | Sort: int32(menu.Sort), 86 | Status: int32(menu.Status), 87 | Children: make([]*base.MenuTreeNode, 0), 88 | } 89 | menuMap[menu.Id] = node 90 | } 91 | 92 | // 第二步:构建树状结构 93 | for _, node := range menuMap { 94 | if node.ParentId == 0 { 95 | rootNodes = append(rootNodes, node) 96 | } else { 97 | if parent, exists := menuMap[uint(node.ParentId)]; exists { 98 | parent.Children = append(parent.Children, node) 99 | } 100 | } 101 | } 102 | 103 | // 第三步:对每一层的节点进行排序 104 | var sortMenuNodes func(nodes []*base.MenuTreeNode) 105 | sortMenuNodes = func(nodes []*base.MenuTreeNode) { 106 | sort.Slice(nodes, func(i, j int) bool { 107 | return nodes[i].Sort < nodes[j].Sort 108 | }) 109 | // 递归排序子节点 110 | for _, node := range nodes { 111 | if len(node.Children) > 0 { 112 | sortMenuNodes(node.Children) 113 | } 114 | } 115 | } 116 | 117 | // 对根节点进行排序 118 | sortMenuNodes(rootNodes) 119 | 120 | list = rootNodes 121 | return 122 | } 123 | 124 | func (a *Auth) GetUserInfo(userId int32) (userInfo *base.UserInfo, err error) { 125 | user, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 126 | UserId: userId, 127 | }) 128 | if err != nil { 129 | return 130 | } 131 | 132 | userInfo = &base.UserInfo{ 133 | Id: user.Id, 134 | UserName: user.UserName, 135 | NickName: user.NickName, 136 | Avatar: user.Avatar, 137 | Phone: user.Phone, 138 | Status: int32(user.Status), 139 | } 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/logic/sys_menu.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/dao" 5 | "dandelion-admin-server/base-server/internal/enum" 6 | "dandelion-admin-server/base-server/internal/model" 7 | "dandelion-admin-server/proto/base" 8 | "sort" 9 | 10 | "github.com/team-dandelion/go-dandelion/logger" 11 | ) 12 | 13 | type IMenu interface { 14 | GetMenuTree() (list []*base.MenuTreeNode, err error) 15 | CreateMenu(req *base.CreateMenuReq) (id uint32, err error) 16 | UpdateMenu(req *base.UpdateMenuReq) error 17 | DeleteMenu(id uint32) error 18 | SortMenu(req *base.SortMenuReq) error 19 | } 20 | 21 | type Menu struct{} 22 | 23 | func NewMenu() IMenu { 24 | return &Menu{} 25 | } 26 | 27 | func (m *Menu) GetMenuTree() (list []*base.MenuTreeNode, err error) { 28 | // 获取所有菜单 29 | menuList, err := dao.NewSysMenu().GetMenuList() 30 | if err != nil { 31 | return 32 | } 33 | 34 | // 转换为树状结构 35 | menuMap := make(map[uint]*base.MenuTreeNode) 36 | var rootNodes []*base.MenuTreeNode 37 | 38 | // 第一步:创建所有节点 39 | for _, menu := range menuList { 40 | node := &base.MenuTreeNode{ 41 | Id: uint32(menu.Id), 42 | ParentId: uint32(menu.ParentId), 43 | Name: menu.Name, 44 | Path: menu.Path, 45 | Type: int32(menu.Type), 46 | Icon: menu.Icon, 47 | Sort: int32(menu.Sort), 48 | Status: int32(menu.Status), 49 | Children: make([]*base.MenuTreeNode, 0), 50 | } 51 | menuMap[menu.Id] = node 52 | } 53 | 54 | // 第二步:构建树状结构 55 | for _, node := range menuMap { 56 | if node.ParentId == 0 { 57 | rootNodes = append(rootNodes, node) 58 | } else { 59 | if parent, exists := menuMap[uint(node.ParentId)]; exists { 60 | parent.Children = append(parent.Children, node) 61 | } 62 | } 63 | } 64 | 65 | // 第三步:对每一层的节点进行排序 66 | var sortMenuNodes func(nodes []*base.MenuTreeNode) 67 | sortMenuNodes = func(nodes []*base.MenuTreeNode) { 68 | sort.Slice(nodes, func(i, j int) bool { 69 | return nodes[i].Sort < nodes[j].Sort 70 | }) 71 | // 递归排序子节点 72 | for _, node := range nodes { 73 | if len(node.Children) > 0 { 74 | sortMenuNodes(node.Children) 75 | } 76 | } 77 | } 78 | 79 | // 对根节点进行排序 80 | sortMenuNodes(rootNodes) 81 | 82 | list = rootNodes 83 | return 84 | } 85 | 86 | func (m *Menu) CreateMenu(req *base.CreateMenuReq) (id uint32, err error) { 87 | // 参数校验 88 | if req.Name == "" { 89 | err = enum.ParamsError 90 | return 91 | } 92 | 93 | // 如果有父级菜单,检查父级菜单是否存在 94 | if req.ParentId != 0 { 95 | parent, err := dao.NewSysMenu().GetMenuById(uint(req.ParentId)) 96 | if err != nil { 97 | return 0, enum.ParentMenuNotFound 98 | } 99 | // 检查父级菜单类型 100 | if int32(parent.Type) >= req.Type || (int32(parent.Type) == 3 && req.Type == 3) { 101 | return 0, enum.ParentMenuTypeError 102 | } 103 | logger.Info("parent.Type: %d, req.Type: %d", parent.Type, req.Type) 104 | logger.Info("校验: %v", int32(parent.Type) <= req.Type && (parent.Type != 3 || 105 | (int32(parent.Type) == 3 && req.Type != 3))) 106 | } 107 | 108 | menu := &model.SysMenu{ 109 | ParentId: uint(req.ParentId), 110 | Name: req.Name, 111 | Path: req.Path, 112 | Type: int(req.Type), 113 | Icon: req.Icon, 114 | Sort: int(req.Sort), 115 | Status: int(req.Status), 116 | } 117 | 118 | err = dao.NewSysMenu().CreateMenu(menu) 119 | if err != nil { 120 | return 121 | } 122 | 123 | id = uint32(menu.Id) 124 | return 125 | } 126 | 127 | func (m *Menu) UpdateMenu(req *base.UpdateMenuReq) error { 128 | // 参数校验 129 | if req.Id == 0 || req.Name == "" { 130 | return enum.ParamsError 131 | } 132 | 133 | // 检查菜单是否存在 134 | menu, err := dao.NewSysMenu().GetMenuById(uint(req.Id)) 135 | if err != nil { 136 | return enum.MenuNotFound 137 | } 138 | 139 | // 如果有父级菜单,检查父级菜单是否存在 140 | if req.ParentId != 0 { 141 | parent, err := dao.NewSysMenu().GetMenuById(uint(req.ParentId)) 142 | if err != nil { 143 | return enum.ParentMenuNotFound 144 | } 145 | // 检查父级菜单类型 146 | if parent.Type > 1 { 147 | return enum.ParentMenuTypeError 148 | } 149 | // 检查是否形成循环 150 | if req.Id == req.ParentId { 151 | return enum.MenuCircularDependency 152 | } 153 | } 154 | 155 | menu.ParentId = uint(req.ParentId) 156 | menu.Name = req.Name 157 | menu.Path = req.Path 158 | menu.Type = int(req.Type) 159 | menu.Icon = req.Icon 160 | menu.Sort = int(req.Sort) 161 | menu.Status = int(req.Status) 162 | 163 | return dao.NewSysMenu().UpdateMenu(menu) 164 | } 165 | 166 | func (m *Menu) DeleteMenu(id uint32) error { 167 | // 检查菜单是否存在 168 | menu, err := dao.NewSysMenu().GetMenuById(uint(id)) 169 | if err != nil { 170 | return enum.MenuNotFound 171 | } 172 | 173 | // 检查是否有子菜单 174 | menuList, err := dao.NewSysMenu().GetMenuList() 175 | if err != nil { 176 | return err 177 | } 178 | 179 | for _, m := range menuList { 180 | if m.ParentId == menu.Id { 181 | return enum.MenuHasChildren 182 | } 183 | } 184 | 185 | return dao.NewSysMenu().DeleteMenu(uint(id)) 186 | } 187 | 188 | func (m *Menu) SortMenu(req *base.SortMenuReq) error { 189 | logger.Info("req: %v", req) 190 | if len(req.List) == 0 { 191 | return enum.ParamsError 192 | } 193 | 194 | // 获取所有需要更新的菜单 195 | menuMap := make(map[uint]*model.SysMenu) 196 | for _, item := range req.List { 197 | menu, err := dao.NewSysMenu().GetMenuById(uint(item.Id)) 198 | if err != nil { 199 | return enum.MenuNotFound 200 | } 201 | menuMap[uint(item.Id)] = menu 202 | } 203 | 204 | // 校验父级菜单类型 205 | for _, item := range req.List { 206 | menu := menuMap[uint(item.Id)] 207 | if item.ParentId != uint32(menu.ParentId) { 208 | // 如果更改了父级菜单,需要校验父级菜单类型 209 | parent, err := dao.NewSysMenu().GetMenuById(uint(item.ParentId)) 210 | if err != nil { 211 | return enum.ParentMenuNotFound 212 | } 213 | 214 | // 检查父级菜单类型 215 | if int32(parent.Type) >= int32(menu.Type) || (int32(parent.Type) == 3 && int32(menu.Type) == 3) { 216 | return enum.ParentMenuTypeError 217 | } 218 | 219 | // 检查是否形成循环 220 | if item.Id == item.ParentId { 221 | return enum.MenuCircularDependency 222 | } 223 | } 224 | } 225 | 226 | // 准备更新数据 227 | var menus []*model.SysMenu 228 | for _, item := range req.List { 229 | menu := menuMap[uint(item.Id)] 230 | menu.ParentId = uint(item.ParentId) 231 | menu.Sort = int(item.Sequence) 232 | menus = append(menus, menu) 233 | } 234 | 235 | // 使用dao层的事务方法批量更新 236 | return dao.NewSysMenu().BatchSortMenus(menus) 237 | } 238 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/logic/sys_role.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/dao" 5 | "dandelion-admin-server/base-server/internal/enum" 6 | "dandelion-admin-server/base-server/internal/model" 7 | "dandelion-admin-server/proto/base" 8 | "time" 9 | 10 | "github.com/team-dandelion/go-dandelion/logger" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type IRole interface { 15 | CreateRole(req *base.CreateRoleReq) (int32, error) 16 | UpdateRole(req *base.UpdateRoleReq) error 17 | DeleteRole(id int32) error 18 | GetRoleList(req *base.GetRoleListReq) (list []*base.RoleInfo, total int64, err error) 19 | AssignUsers(req *base.AssignUsersReq) error 20 | } 21 | 22 | type Role struct{} 23 | 24 | func NewRole() IRole { 25 | return &Role{} 26 | } 27 | 28 | // CreateRole 创建角色 29 | func (r *Role) CreateRole(req *base.CreateRoleReq) (int32, error) { 30 | // 参数校验 31 | if req.Name == "" { 32 | return 0, enum.ParamsError 33 | } 34 | 35 | // 检查角色名是否存在 36 | exists, err := dao.NewSysRole().CheckNameExists(req.Name, 0) 37 | if err != nil { 38 | return 0, err 39 | } 40 | if exists { 41 | return 0, enum.RoleNameExists 42 | } 43 | 44 | // 检查菜单是否存在 45 | if len(req.MenuIds) > 0 { 46 | for _, menuId := range req.MenuIds { 47 | menu, err := dao.NewSysMenu().GetMenuById(uint(menuId)) 48 | if err != nil { 49 | if err == gorm.ErrRecordNotFound { 50 | return 0, enum.RoleMenuNotFound 51 | } 52 | return 0, err 53 | } 54 | if menu == nil { 55 | return 0, enum.RoleMenuNotFound 56 | } 57 | } 58 | } 59 | 60 | // 创建角色 61 | role := &model.SysRole{ 62 | Name: req.Name, 63 | Description: req.Description, 64 | Status: req.Status, 65 | CreatedAt: time.Now().Unix(), 66 | UpdatedAt: time.Now().Unix(), 67 | } 68 | 69 | // 创建角色并关联菜单 70 | err = dao.NewSysRole().CreateRole(role, req.MenuIds) 71 | if err != nil { 72 | return 0, err 73 | } 74 | 75 | return role.Id, nil 76 | } 77 | 78 | // UpdateRole 更新角色 79 | func (r *Role) UpdateRole(req *base.UpdateRoleReq) error { 80 | // 参数校验 81 | if req.Id == 0 || req.Name == "" { 82 | return enum.ParamsError 83 | } 84 | 85 | // 检查角色是否存在 86 | role, err := dao.NewSysRole().GetRoleById(req.Id) 87 | if err != nil { 88 | if err == gorm.ErrRecordNotFound { 89 | return enum.RoleNotFound 90 | } 91 | return err 92 | } 93 | 94 | // 检查角色名是否存在 95 | exists, err := dao.NewSysRole().CheckNameExists(req.Name, req.Id) 96 | if err != nil { 97 | return err 98 | } 99 | if exists { 100 | return enum.RoleNameExists 101 | } 102 | 103 | // 检查菜单是否存在 104 | if len(req.MenuIds) > 0 { 105 | for _, menuId := range req.MenuIds { 106 | menu, err := dao.NewSysMenu().GetMenuById(uint(menuId)) 107 | if err != nil { 108 | if err == gorm.ErrRecordNotFound { 109 | return enum.RoleMenuNotFound 110 | } 111 | return err 112 | } 113 | if menu == nil { 114 | return enum.RoleMenuNotFound 115 | } 116 | } 117 | } 118 | 119 | // 更新角色信息 120 | role.Name = req.Name 121 | role.Description = req.Description 122 | role.Status = req.Status 123 | role.UpdatedAt = time.Now().Unix() 124 | 125 | // 更新角色并关联菜单 126 | return dao.NewSysRole().UpdateRole(role, req.MenuIds) 127 | } 128 | 129 | // DeleteRole 删除角色 130 | func (r *Role) DeleteRole(id int32) error { 131 | // 参数校验 132 | if id == 0 { 133 | return enum.ParamsError 134 | } 135 | 136 | // 检查角色是否存在 137 | _, err := dao.NewSysRole().GetRoleById(id) 138 | if err != nil { 139 | if err == gorm.ErrRecordNotFound { 140 | return enum.RoleNotFound 141 | } 142 | return err 143 | } 144 | 145 | // 检查是否有用户使用该角色 146 | userIds, err := dao.NewSysRole().GetUserRoleIds(id) 147 | if err != nil { 148 | return err 149 | } 150 | if len(userIds) > 0 { 151 | return enum.RoleHasUsers 152 | } 153 | 154 | // 删除角色 155 | return dao.NewSysRole().DeleteRole(id) 156 | } 157 | 158 | // GetRoleList 获取角色列表 159 | func (r *Role) GetRoleList(req *base.GetRoleListReq) (list []*base.RoleInfo, total int64, err error) { 160 | // 构建查询条件 161 | filter := model.RoleFilter{ 162 | Name: req.Name, 163 | Status: req.Status, 164 | } 165 | 166 | // 获取角色列表 167 | roles, total, err := dao.NewSysRole().GetRoleList(filter, req.Page, req.PageSize) 168 | if err != nil { 169 | return nil, 0, err 170 | } 171 | 172 | // 转换为proto结构 173 | list = make([]*base.RoleInfo, 0, len(roles)) 174 | for _, role := range roles { 175 | // 获取角色关联的菜单ID 176 | menuIds, err := dao.NewSysRole().GetRoleMenuIds(role.Id) 177 | if err != nil { 178 | return nil, 0, err 179 | } 180 | 181 | // 获取角色关联的用户ID 182 | userIds, err := dao.NewSysRole().GetUserRoleIds(role.Id) 183 | if err != nil { 184 | return nil, 0, err 185 | } 186 | logger.Debug("userIds", userIds) 187 | list = append(list, &base.RoleInfo{ 188 | Id: role.Id, 189 | Name: role.Name, 190 | Description: role.Description, 191 | Status: role.Status, 192 | CreatedAt: role.CreatedAt, 193 | UpdatedAt: role.UpdatedAt, 194 | MenuIds: menuIds, 195 | UserIds: userIds, 196 | }) 197 | } 198 | 199 | return list, total, nil 200 | } 201 | 202 | // AssignUsers 分配用户 203 | func (r *Role) AssignUsers(req *base.AssignUsersReq) error { 204 | // 参数校验 205 | if req.RoleId == 0 { 206 | return enum.ParamsError 207 | } 208 | 209 | // 检查角色是否存在 210 | _, err := dao.NewSysRole().GetRoleById(req.RoleId) 211 | if err != nil { 212 | if err == gorm.ErrRecordNotFound { 213 | return enum.RoleNotFound 214 | } 215 | return err 216 | } 217 | 218 | // 检查用户是否存在 219 | if len(req.UserIds) > 0 { 220 | for _, userId := range req.UserIds { 221 | user, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 222 | UserId: userId, 223 | }) 224 | if err != nil { 225 | if err == gorm.ErrRecordNotFound { 226 | return enum.RoleUserNotFound 227 | } 228 | return err 229 | } 230 | if user == nil { 231 | return enum.RoleUserNotFound 232 | } 233 | } 234 | } 235 | 236 | // 分配用户 237 | return dao.NewSysRole().AssignUsers(req.RoleId, req.UserIds) 238 | } 239 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/logic/sys_user.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/internal/dao" 5 | "dandelion-admin-server/base-server/internal/enum" 6 | "dandelion-admin-server/base-server/internal/model" 7 | "dandelion-admin-server/base-server/tools/encrypt" 8 | "dandelion-admin-server/proto/base" 9 | "time" 10 | 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type ISysUser interface { 15 | CreateUser(req *base.CreateSysUserReq) error 16 | UpdateUser(req *base.UpdateSysUserReq) error 17 | DeleteUser(id int32) error 18 | GetUserList(req *base.GetSysUserListReq) (list []*base.SysUserInfo, total int64, err error) 19 | } 20 | 21 | type SysUser struct{} 22 | 23 | func NewSysUser() ISysUser { 24 | return &SysUser{} 25 | } 26 | 27 | func (s *SysUser) CreateUser(req *base.CreateSysUserReq) error { 28 | // 参数校验 29 | if req.UserName == "" || req.Password == "" { 30 | return enum.ParamsError 31 | } 32 | 33 | // 检查用户名是否已存在 34 | _, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 35 | UserName: req.UserName, 36 | }) 37 | if err != nil && err != gorm.ErrRecordNotFound { 38 | return err 39 | } 40 | if err == nil { 41 | return enum.UserNameExists 42 | } 43 | 44 | // 创建用户 45 | user := &model.SysUser{ 46 | UserName: req.UserName, 47 | Password: encrypt.MD5(req.Password), 48 | NickName: req.NickName, 49 | Avatar: req.Avatar, 50 | Phone: req.Phone, 51 | Status: req.Status, 52 | CreatedAt: time.Now().Unix(), 53 | UpdatedAt: time.Now().Unix(), 54 | } 55 | 56 | return dao.NewSysUser().CreateUser(user) 57 | } 58 | 59 | func (s *SysUser) UpdateUser(req *base.UpdateSysUserReq) error { 60 | // 参数校验 61 | if req.Id == 0 { 62 | return enum.ParamsError 63 | } 64 | 65 | // 检查用户是否存在 66 | user, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 67 | UserId: req.Id, 68 | }) 69 | if err != nil { 70 | if err == gorm.ErrRecordNotFound { 71 | return enum.SysUserNotFound 72 | } 73 | return err 74 | } 75 | 76 | // 更新用户信息 77 | user.NickName = req.NickName 78 | user.Avatar = req.Avatar 79 | user.Phone = req.Phone 80 | user.Status = req.Status 81 | user.UpdatedAt = time.Now().Unix() 82 | 83 | return dao.NewSysUser().UpdateUser(user) 84 | } 85 | 86 | func (s *SysUser) DeleteUser(id int32) error { 87 | // 参数校验 88 | if id == 0 { 89 | return enum.ParamsError 90 | } 91 | 92 | // 检查用户是否存在 93 | _, err := dao.NewSysUser().GetUserInfo(model.UserFilter{ 94 | UserId: id, 95 | }) 96 | if err != nil { 97 | if err == gorm.ErrRecordNotFound { 98 | return enum.SysUserNotFound 99 | } 100 | return err 101 | } 102 | 103 | return dao.NewSysUser().DeleteUser(id) 104 | } 105 | 106 | func (s *SysUser) GetUserList(req *base.GetSysUserListReq) (list []*base.SysUserInfo, total int64, err error) { 107 | // 构建查询条件 108 | filter := model.UserFilter{ 109 | UserName: req.UserName, 110 | Phone: req.Phone, 111 | Status: req.Status, 112 | } 113 | 114 | // 获取用户列表 115 | userList, total, err := dao.NewSysUser().GetUserList(filter, req.Page, req.PageSize) 116 | if err != nil { 117 | return 118 | } 119 | 120 | // 转换为proto结构 121 | for _, user := range userList { 122 | list = append(list, &base.SysUserInfo{ 123 | Id: user.Id, 124 | UserName: user.UserName, 125 | NickName: user.NickName, 126 | Avatar: user.Avatar, 127 | Phone: user.Phone, 128 | Status: user.Status, 129 | CreatedAt: user.CreatedAt, 130 | UpdatedAt: user.UpdatedAt, 131 | }) 132 | } 133 | 134 | return 135 | } 136 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/model/base.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type CtxOption struct { 4 | UserName string 5 | UserId int32 6 | } 7 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/model/sys_menu.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "dandelion-admin-server/base-server/boot" 4 | 5 | func init() { 6 | boot.Register(&SysMenu{}) 7 | } 8 | 9 | type SysMenu struct { 10 | Id uint `gorm:"type:int;primaryKey;autoIncrement;comment:主键ID"` 11 | ParentId uint `gorm:"type:int;not null;default:0;comment:父菜单ID"` 12 | Name string `gorm:"type:varchar(100);not null;comment:菜单名称"` 13 | Path string `gorm:"type:varchar(200);comment:菜单路径"` 14 | Type int `gorm:"type:int;not null;default:0;comment:菜单类型 0:目录 1:菜单 2:tab页 3:按钮"` 15 | Icon string `gorm:"type:varchar(100);comment:菜单图标"` 16 | Sort int `gorm:"type:int;not null;default:0;comment:排序"` 17 | Status int `gorm:"type:int;not null;default:0;comment:状态 0:正常 1:禁用"` 18 | } 19 | 20 | func (SysMenu) TableName() string { 21 | return "sys_menu" 22 | } 23 | 24 | func (SysMenu) TableComment() string { 25 | return "系统菜单表" 26 | } 27 | 28 | type SysMenuApi struct { 29 | Id uint `gorm:"type:int;primaryKey;autoIncrement;comment:主键ID"` 30 | MenuId uint `gorm:"type:int;not null;comment:菜单ID"` 31 | Api string `gorm:"type:varchar(200);not null;comment:API路径"` 32 | Method string `gorm:"type:varchar(10);not null;comment:请求方法"` 33 | } 34 | 35 | func (SysMenuApi) TableName() string { 36 | return "sys_menu_api" 37 | } 38 | 39 | func (SysMenuApi) TableComment() string { 40 | return "系统菜单API关联表" 41 | } 42 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/model/sys_role.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "dandelion-admin-server/base-server/boot" 4 | 5 | func init() { 6 | boot.Register(&SysRole{}, &SysRoleMenu{}, &SysUserRole{}) 7 | } 8 | 9 | // 角色表 10 | type SysRole struct { 11 | Id int32 `gorm:"primaryKey;autoIncrement"` 12 | Name string `gorm:"size:50;not null;comment:角色名称"` 13 | Description string `gorm:"size:200;comment:角色描述"` 14 | Status int32 `gorm:"default:1;comment:状态 1-启用 2-禁用"` 15 | CreatedAt int64 `gorm:"comment:创建时间"` 16 | UpdatedAt int64 `gorm:"comment:更新时间"` 17 | } 18 | 19 | func (s *SysRole) TableName() string { 20 | return "sys_role" 21 | } 22 | 23 | func (s *SysRole) TableComment() string { 24 | return "角色表" 25 | } 26 | 27 | // 角色-菜单关联表 28 | type SysRoleMenu struct { 29 | Id int32 `gorm:"primaryKey;autoIncrement"` 30 | RoleId int32 `gorm:"not null;comment:角色ID"` 31 | MenuId int32 `gorm:"not null;comment:菜单ID"` 32 | } 33 | 34 | func (s *SysRoleMenu) TableName() string { 35 | return "sys_role_menu" 36 | } 37 | 38 | func (s *SysRoleMenu) TableComment() string { 39 | return "角色-菜单关联表" 40 | } 41 | 42 | // 用户-角色关联表 43 | type SysUserRole struct { 44 | Id int32 `gorm:"primaryKey;autoIncrement"` 45 | UserId int32 `gorm:"not null;comment:用户ID"` 46 | RoleId int32 `gorm:"not null;comment:角色ID"` 47 | } 48 | 49 | func (s *SysUserRole) TableName() string { 50 | return "sys_user_role" 51 | } 52 | 53 | func (s *SysUserRole) TableComment() string { 54 | return "用户-角色关联表" 55 | } 56 | 57 | // 角色查询过滤条件 58 | type RoleFilter struct { 59 | RoleId int32 // 角色ID 60 | Name string // 角色名称 61 | Status int32 // 状态 62 | } 63 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/model/sys_user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "dandelion-admin-server/base-server/boot" 5 | "fmt" 6 | ) 7 | 8 | func init() { 9 | boot.Register(&SysUser{}) 10 | } 11 | 12 | type SysUser struct { 13 | Id int32 `gorm:"type:int;primaryKey;autoIncrement"` 14 | UserName string `gorm:"type:varchar(255);not null;comment:用户名;unique"` 15 | Password string `gorm:"type:varchar(255);not null;comment:密码"` 16 | NickName string `gorm:"type:varchar(255);not null;comment:昵称"` 17 | Avatar string `gorm:"type:varchar(255);not null;comment:头像"` 18 | Phone string `gorm:"type:varchar(255);not null;comment:手机号"` 19 | Status int32 `gorm:"type:int;not null;comment:状态"` 20 | CreatedAt int64 `gorm:"type:bigint;not null;comment:创建时间"` 21 | UpdatedAt int64 `gorm:"type:bigint;not null;comment:更新时间"` 22 | } 23 | 24 | func (s *SysUser) TableName() string { 25 | return "sys_user" 26 | } 27 | 28 | func (s *SysUser) TableComment() string { 29 | return "系统用户表" 30 | } 31 | 32 | type UserMeta struct { 33 | UserId int32 34 | Name string 35 | } 36 | 37 | func (u *UserMeta) Unique() string { 38 | return fmt.Sprintf("DandelionAdmin:SysUserToken:%d", u.UserId) 39 | } 40 | 41 | type UserFilter struct { 42 | UserName string 43 | UserId int32 44 | Phone string 45 | Status int32 46 | } 47 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/service/api.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "dandelion-admin-server/base-server/internal/enum" 6 | "dandelion-admin-server/base-server/internal/model" 7 | "dandelion-admin-server/proto/base" 8 | "errors" 9 | 10 | "github.com/team-dandelion/go-dandelion/server/rpcx" 11 | "github.com/team-dandelion/go-dandelion/tools/rpccall" 12 | ) 13 | 14 | type RpcApi struct { 15 | base.BaseServerServiceImpl 16 | } 17 | 18 | func (*RpcApi) ErrorFormat(err error) *rpccall.CommonResp { 19 | var resp = &rpccall.CommonResp{} 20 | if err == nil { 21 | return resp 22 | } 23 | 24 | var sErr *enum.Error 25 | switch { 26 | case errors.As(err, &sErr): 27 | errors.As(err, &sErr) 28 | resp.Code = sErr.Code 29 | resp.Msg = sErr.Msg 30 | default: 31 | resp.Code = 40000 32 | resp.Msg = err.Error() 33 | } 34 | return resp 35 | } 36 | 37 | func (*RpcApi) Opt(ctx context.Context) model.CtxOption { 38 | return model.CtxOption{ 39 | UserName: rpcx.Header().Value(ctx, "UserName"), 40 | UserId: rpcx.Header().Int32Default(ctx, "UserId", 0), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/service/authorize.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "dandelion-admin-server/base-server/internal/logic" 6 | "dandelion-admin-server/proto/base" 7 | "fmt" 8 | ) 9 | 10 | func (a *RpcApi) Login(ctx context.Context, req *base.LoginParams, resp *base.LoginResp) (err error) { 11 | fmt.Println("Login") 12 | token, err := logic.NewAuth().Login(req) 13 | resp.CommonResp = a.ErrorFormat(err) 14 | resp.Token = token 15 | return 16 | } 17 | 18 | func (a *RpcApi) GetUserMenu(ctx context.Context, req *base.GetUserMenuReq, resp *base.GetUserMenuResp) (err error) { 19 | list, err := logic.NewAuth().GetUserMenu() 20 | resp.CommonResp = a.ErrorFormat(err) 21 | resp.List = list 22 | return 23 | } 24 | 25 | func (a *RpcApi) GetUserInfo(ctx context.Context, req *base.GetUserInfoReq, resp *base.GetUserInfoResp) (err error) { 26 | userInfo, err := logic.NewAuth().GetUserInfo(1) 27 | resp.CommonResp = a.ErrorFormat(err) 28 | resp.UserInfo = userInfo 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/service/sys_menu.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "dandelion-admin-server/base-server/internal/logic" 6 | "dandelion-admin-server/proto/base" 7 | ) 8 | 9 | func (a *RpcApi) GetMenuTree(ctx context.Context, req *base.GetMenuTreeReq, resp *base.GetMenuTreeResp) (err error) { 10 | list, err := logic.NewMenu().GetMenuTree() 11 | resp.CommonResp = a.ErrorFormat(err) 12 | resp.List = list 13 | return 14 | } 15 | 16 | func (a *RpcApi) CreateMenu(ctx context.Context, req *base.CreateMenuReq, resp *base.CreateMenuResp) (err error) { 17 | id, err := logic.NewMenu().CreateMenu(req) 18 | resp.CommonResp = a.ErrorFormat(err) 19 | resp.Id = id 20 | return 21 | } 22 | 23 | func (a *RpcApi) UpdateMenu(ctx context.Context, req *base.UpdateMenuReq, resp *base.UpdateMenuResp) (err error) { 24 | err = logic.NewMenu().UpdateMenu(req) 25 | resp.CommonResp = a.ErrorFormat(err) 26 | return 27 | } 28 | 29 | func (a *RpcApi) DeleteMenu(ctx context.Context, req *base.DeleteMenuReq, resp *base.DeleteMenuResp) (err error) { 30 | err = logic.NewMenu().DeleteMenu(req.Id) 31 | resp.CommonResp = a.ErrorFormat(err) 32 | return 33 | } 34 | 35 | func (a *RpcApi) SortMenu(ctx context.Context, req *base.SortMenuReq, resp *base.SortMenuResp) (err error) { 36 | err = logic.NewMenu().SortMenu(req) 37 | resp.CommonResp = a.ErrorFormat(err) 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/service/sys_role.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "dandelion-admin-server/base-server/internal/logic" 6 | "dandelion-admin-server/proto/base" 7 | ) 8 | 9 | func (a *RpcApi) CreateRole(ctx context.Context, req *base.CreateRoleReq, resp *base.CreateRoleResp) (err error) { 10 | id, err := logic.NewRole().CreateRole(req) 11 | resp.CommonResp = a.ErrorFormat(err) 12 | resp.Id = id 13 | return 14 | } 15 | 16 | func (a *RpcApi) UpdateRole(ctx context.Context, req *base.UpdateRoleReq, resp *base.UpdateRoleResp) (err error) { 17 | err = logic.NewRole().UpdateRole(req) 18 | resp.CommonResp = a.ErrorFormat(err) 19 | return 20 | } 21 | 22 | func (a *RpcApi) DeleteRole(ctx context.Context, req *base.DeleteRoleReq, resp *base.DeleteRoleResp) (err error) { 23 | err = logic.NewRole().DeleteRole(req.Id) 24 | resp.CommonResp = a.ErrorFormat(err) 25 | return 26 | } 27 | 28 | func (a *RpcApi) GetRoleList(ctx context.Context, req *base.GetRoleListReq, resp *base.GetRoleListResp) (err error) { 29 | // 参数校验 30 | if req.Page == 0 { 31 | req.Page = 1 32 | } 33 | if req.PageSize == 0 { 34 | req.PageSize = 10 35 | } 36 | 37 | list, total, err := logic.NewRole().GetRoleList(req) 38 | resp.CommonResp = a.ErrorFormat(err) 39 | resp.List = list 40 | resp.Total = total 41 | return 42 | } 43 | 44 | func (a *RpcApi) AssignUsers(ctx context.Context, req *base.AssignUsersReq, resp *base.AssignUsersResp) (err error) { 45 | err = logic.NewRole().AssignUsers(req) 46 | resp.CommonResp = a.ErrorFormat(err) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/internal/service/sys_user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "dandelion-admin-server/base-server/internal/logic" 6 | "dandelion-admin-server/proto/base" 7 | ) 8 | 9 | func (a *RpcApi) CreateSysUser(ctx context.Context, req *base.CreateSysUserReq, resp *base.CreateSysUserResp) (err error) { 10 | err = logic.NewSysUser().CreateUser(req) 11 | resp.CommonResp = a.ErrorFormat(err) 12 | return 13 | } 14 | 15 | func (a *RpcApi) UpdateSysUser(ctx context.Context, req *base.UpdateSysUserReq, resp *base.UpdateSysUserResp) (err error) { 16 | err = logic.NewSysUser().UpdateUser(req) 17 | resp.CommonResp = a.ErrorFormat(err) 18 | return 19 | } 20 | 21 | func (a *RpcApi) DeleteSysUser(ctx context.Context, req *base.DeleteSysUserReq, resp *base.DeleteSysUserResp) (err error) { 22 | err = logic.NewSysUser().DeleteUser(req.Id) 23 | resp.CommonResp = a.ErrorFormat(err) 24 | return 25 | } 26 | 27 | func (a *RpcApi) GetSysUserList(ctx context.Context, req *base.GetSysUserListReq, resp *base.GetSysUserListResp) (err error) { 28 | list, total, err := logic.NewSysUser().GetUserList(req) 29 | resp.CommonResp = a.ErrorFormat(err) 30 | resp.List = list 31 | resp.Total = total 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "dandelion-admin-server/base-server/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /dandelion-admin-server/base-server/static/base-server.txt: -------------------------------------------------------------------------------- 1 | _ 2 | | |__ __ _ ___ ___ ___ ___ _ __ __ __ ___ _ __ 3 | | '_ \ / _` | / __| / _ \ _____ / __| / _ \ | '__| \ \ / / / _ \ | '__| 4 | | |_) | | (_| | \__ \ | __/ |_____| \__ \ | __/ | | \ V / | __/ | | 5 | |_.__/ \__,_| |___/ \___| |___/ \___| |_| \_/ \___| |_| 6 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/cmd/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "dandelion-admin-server/gateway/internal/route" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/signal" 9 | 10 | routing "github.com/gly-hub/fasthttp-routing" 11 | "github.com/gly-hub/toolbox/ip" 12 | "github.com/gly-hub/toolbox/stringx" 13 | "github.com/spf13/cobra" 14 | "github.com/team-dandelion/go-dandelion/application" 15 | "github.com/team-dandelion/go-dandelion/config" 16 | "github.com/team-dandelion/go-dandelion/logger" 17 | ) 18 | 19 | var ( 20 | env string 21 | StartCmd = &cobra.Command{ 22 | Use: "server", 23 | Short: "Start API server", 24 | Example: "gateway server -e local", 25 | SilenceUsage: true, 26 | PreRun: func(cmd *cobra.Command, args []string) { 27 | setup() 28 | }, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | return run() 31 | }, 32 | } 33 | ) 34 | 35 | func init() { 36 | StartCmd.PersistentFlags().StringVarP(&env, "env", "e", "local", "Env") 37 | } 38 | 39 | func setup() { 40 | // 配置初始化 41 | config.InitConfig(env) 42 | // 应用初始化 43 | application.Init() 44 | // 路由初始化 45 | route.InitRoute() 46 | // 注册头部context链路 47 | application.RegisterHeaderFunc(HeaderFunc) 48 | } 49 | 50 | func HeaderFunc(ctx *routing.Context, data map[string]string) map[string]string { 51 | // 自定义头部链路。该方法能将需要的参数通过rpc进行传递 52 | data["UserId"] = ctx.Header.Value("UserId") 53 | data["UserName"] = ctx.Header.Value("UserName") 54 | return data 55 | } 56 | 57 | func run() error { 58 | // 启动http服务 59 | go func() { 60 | application.HttpServer().Server() 61 | }() 62 | content, _ := ioutil.ReadFile("./static/gateway.txt") 63 | fmt.Println(logger.Green(string(content))) 64 | fmt.Println(logger.Green("Server run at:")) 65 | fmt.Printf("- Local: http://localhost:%d/ \r\n", application.HttpServer().Port()) 66 | fmt.Printf("- Network: http://%s:%d/ \r\n", ip.GetLocalHost(), application.HttpServer().Port()) 67 | fmt.Println() 68 | if config.GetEnv() != "production" { 69 | fmt.Println(logger.Green("Swagger run at:")) 70 | fmt.Printf("- Local: http://localhost:%d/api/swagger/index.html \r\n", application.HttpServer().Port()) 71 | fmt.Printf("- Network: http://%s:%d/api/swagger/index.html \r\n", ip.GetLocalHost(), application.HttpServer().Port()) 72 | } 73 | 74 | quit := make(chan os.Signal) 75 | signal.Notify(quit, os.Interrupt) 76 | <-quit 77 | fmt.Printf("%s Shutdown Server ... \r\n", stringx.GetCurrentTimeStr()) 78 | logger.Info("Server exiting") 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/cmd/cobra.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "dandelion-admin-server/gateway/cmd/api" 6 | "github.com/team-dandelion/go-dandelion/logger" 7 | "github.com/spf13/cobra" 8 | "os" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "dandelion-admin-server/gateway", 13 | Short: "gateway", 14 | SilenceUsage:true, 15 | Long: "authorize", 16 | Args: func(cmd *cobra.Command, args []string) error { 17 | if len(args) < 1 { 18 | return errors.New(logger.Red("requires at least one arg")) 19 | } 20 | return nil 21 | }, 22 | PersistentPostRunE: func(cmd *cobra.Command, args []string) error { 23 | return nil 24 | }, 25 | } 26 | 27 | func init(){ 28 | rootCmd.AddCommand(api.StartCmd) 29 | } 30 | 31 | func Execute(){ 32 | if err := rootCmd.Execute(); err != nil{ 33 | os.Exit(-1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/config/configs_local.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | #Level 0 紧急的 1警报 2重要的 3错误 4警告 5提示 6信息 7调试 3 | consoleShow: true 4 | consoleLevel: 7 5 | fileWrite: false 6 | fileLevel: 7 7 | multiFileWrite: false 8 | multiFileLevel: 7 9 | 10 | httpServer: 11 | port: 8080 12 | pprof: 8088 13 | 14 | rpcClient: 15 | clientName: "gateway" 16 | basePath: "dandelion-admin-server" 17 | registerPlugin: "multiple" 18 | registerServers: 19 | - "127.0.0.1:8899" 20 | failRetryModel: 3 21 | balanceModel: 2 22 | poolSize: 1 23 | 24 | tracer: 25 | openTrace: true 26 | traceName: "gateway" 27 | host: "127.0.0.1:6831" 28 | 29 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/gen_swagger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "开始生成 swagger 文档..." 4 | 5 | # 执行swagger生成命令 6 | swag init --parseDependency --parseDepth=6 --dir . 7 | 8 | if [ $? -eq 0 ]; then 9 | echo "swagger 文档生成完成!" 10 | else 11 | echo "swagger 文档生成失败!" 12 | exit 1 13 | fi -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/route/base_server.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | baseserver "dandelion-admin-server/gateway/internal/service/base-server" 5 | 6 | routing "github.com/gly-hub/fasthttp-routing" 7 | ) 8 | 9 | func initBaseServerRoute(baseRouter *routing.RouteGroup) { 10 | baseServerController := baseserver.NewController() 11 | { 12 | baseRouter.Post("/login", baseServerController.Login) 13 | baseRouter.Get("/user/menu", baseServerController.GetUserMenu) 14 | baseRouter.Get("/user/info", baseServerController.GetUserInfo) 15 | 16 | // 系统菜单管理 17 | sysMenuRouter := baseRouter.Group("/sys_menu") 18 | { 19 | sysMenuRouter.Get("/search", baseServerController.GetMenuTree) 20 | sysMenuRouter.Post("/create", baseServerController.CreateMenu) 21 | sysMenuRouter.Put("/update", baseServerController.UpdateMenu) 22 | sysMenuRouter.Delete("/delete", baseServerController.DeleteMenu) 23 | sysMenuRouter.Put("/sort", baseServerController.SortMenu) 24 | } 25 | 26 | // 系统用户管理 27 | sysUserRouter := baseRouter.Group("/sys_user") 28 | { 29 | sysUserRouter.Post("/search", baseServerController.GetSysUserList) 30 | sysUserRouter.Post("/create", baseServerController.CreateSysUser) 31 | sysUserRouter.Put("/update", baseServerController.UpdateSysUser) 32 | sysUserRouter.Delete("/delete", baseServerController.DeleteSysUser) 33 | } 34 | 35 | // 角色管理 36 | sysRoleRouter := baseRouter.Group("/sys_role") 37 | { 38 | sysRoleRouter.Post("/create", baseServerController.CreateRole) 39 | sysRoleRouter.Put("/update", baseServerController.UpdateRole) 40 | sysRoleRouter.Delete("/delete", baseServerController.DeleteRole) 41 | sysRoleRouter.Post("/search", baseServerController.GetRoleList) 42 | sysRoleRouter.Post("/assign_users", baseServerController.AssignUsers) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/team-dandelion/go-dandelion/application" 5 | "github.com/team-dandelion/go-dandelion/config" 6 | "github.com/team-dandelion/go-dandelion/server/http/middleware" 7 | routingSwagger "github.com/team-dandelion/go-dandelion/swagger" 8 | ) 9 | 10 | func InitRoute() { 11 | baseRouter := application.HttpServer().Router() 12 | if config.GetEnv() != "production" { 13 | // 注册swagger 14 | baseRouter.Get("/swagger/*", routingSwagger.WrapHandler) 15 | middleware.LogIgnoreResult(".*?/swagger/.*?") // 忽略swagger响应值 16 | } 17 | 18 | // 可在该处注册相关子集路由 TODO 19 | initBaseServerRoute(baseRouter) 20 | } 21 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/service/base-server/authorize_controller.go: -------------------------------------------------------------------------------- 1 | package baseserver 2 | 3 | import ( 4 | "dandelion-admin-server/proto/base" 5 | 6 | routing "github.com/gly-hub/fasthttp-routing" 7 | "github.com/team-dandelion/go-dandelion/tools/rpccall" 8 | ) 9 | 10 | // Login 11 | // @tags 系统认证 12 | // @summary 用户登录 13 | // @description 用户登录接口 14 | // @router /api/login [post] 15 | // @param req body base.LoginParams true "json入参" 16 | // @success 200 {object} base.LoginResp "返回值" 17 | func (c *Controller) Login(ctx *routing.Context) error { 18 | return rpccall.SProtoCall(ctx, &base.LoginParams{}, c.BaseServer.Login) 19 | } 20 | 21 | // GetUserMenu 22 | // @tags 系统认证 23 | // @summary 获取用户菜单 24 | // @description 获取当前登录用户的菜单列表 25 | // @router /api/user/menu [get] 26 | // @param req body base.GetUserMenuReq true "json入参" 27 | // @success 200 {object} base.GetUserMenuResp "返回值" 28 | func (c *Controller) GetUserMenu(ctx *routing.Context) error { 29 | return rpccall.SProtoCall(ctx, &base.GetUserMenuReq{}, c.BaseServer.GetUserMenu) 30 | } 31 | 32 | // GetUserInfo 33 | // @tags 系统认证 34 | // @summary 获取用户信息 35 | // @description 获取当前登录用户的信息 36 | // @router /api/user/info [get] 37 | // @param req body base.GetUserInfoReq true "json入参" 38 | // @success 200 {object} base.GetUserInfoResp "返回值" 39 | func (c *Controller) GetUserInfo(ctx *routing.Context) error { 40 | return rpccall.SProtoCall(ctx, &base.GetUserInfoReq{}, c.BaseServer.GetUserInfo) 41 | } 42 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/service/base-server/service.go: -------------------------------------------------------------------------------- 1 | package baseserver 2 | 3 | import ( 4 | "dandelion-admin-server/proto/base" 5 | 6 | "github.com/team-dandelion/go-dandelion/application" 7 | ) 8 | 9 | type Controller struct { 10 | BaseServer *base.BaseServerServiceOneClient 11 | } 12 | 13 | func NewController() *Controller { 14 | return &Controller{ 15 | BaseServer: base.NewBaseServerServiceOneClient(application.GetRpcClient(). 16 | ClientPool.Client()), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/service/base-server/sys_menu_controller.go: -------------------------------------------------------------------------------- 1 | package baseserver 2 | 3 | import ( 4 | "dandelion-admin-server/proto/base" 5 | 6 | routing "github.com/gly-hub/fasthttp-routing" 7 | "github.com/team-dandelion/go-dandelion/tools/rpccall" 8 | ) 9 | 10 | // GetMenuTree 11 | // @tags 系统菜单管理 12 | // @summary 获取菜单树 13 | // @description 获取系统菜单树形结构 14 | // @router /api/sys_menu/search [get] 15 | // @param req body base.GetMenuTreeReq true "json入参" 16 | // @success 200 {object} base.GetMenuTreeResp "返回值" 17 | func (c *Controller) GetMenuTree(ctx *routing.Context) error { 18 | return rpccall.SProtoCall(ctx, &base.GetMenuTreeReq{}, c.BaseServer.GetMenuTree) 19 | } 20 | 21 | // CreateMenu 22 | // @tags 系统菜单管理 23 | // @summary 创建菜单 24 | // @description 创建系统菜单 25 | // @router /api/sys_menu/create [post] 26 | // @param req body base.CreateMenuReq true "json入参" 27 | // @success 200 {object} base.CreateMenuResp "返回值" 28 | func (c *Controller) CreateMenu(ctx *routing.Context) error { 29 | return rpccall.SProtoCall(ctx, &base.CreateMenuReq{}, c.BaseServer.CreateMenu) 30 | } 31 | 32 | // UpdateMenu 33 | // @tags 系统菜单管理 34 | // @summary 更新菜单 35 | // @description 更新系统菜单 36 | // @router /api/sys_menu/update [put] 37 | // @param req body base.UpdateMenuReq true "json入参" 38 | // @success 200 {object} base.UpdateMenuResp "返回值" 39 | func (c *Controller) UpdateMenu(ctx *routing.Context) error { 40 | return rpccall.SProtoCall(ctx, &base.UpdateMenuReq{}, c.BaseServer.UpdateMenu) 41 | } 42 | 43 | // DeleteMenu 44 | // @tags 系统菜单管理 45 | // @summary 删除菜单 46 | // @description 删除系统菜单 47 | // @router /api/sys_menu/delete [delete] 48 | // @param req body base.DeleteMenuReq true "json入参" 49 | // @success 200 {object} base.DeleteMenuResp "返回值" 50 | func (c *Controller) DeleteMenu(ctx *routing.Context) error { 51 | return rpccall.SProtoCall(ctx, &base.DeleteMenuReq{}, c.BaseServer.DeleteMenu) 52 | } 53 | 54 | // SortMenu 55 | // @tags 系统菜单管理 56 | // @summary 菜单排序 57 | // @description 对系统菜单进行排序 58 | // @router /api/sys_menu/sort [put] 59 | // @param req body base.SortMenuReq true "json入参" 60 | // @success 200 {object} base.SortMenuResp "返回值" 61 | func (c *Controller) SortMenu(ctx *routing.Context) error { 62 | return rpccall.SProtoCall(ctx, &base.SortMenuReq{}, c.BaseServer.SortMenu) 63 | } 64 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/service/base-server/sys_role_controller.go: -------------------------------------------------------------------------------- 1 | package baseserver 2 | 3 | import ( 4 | "dandelion-admin-server/proto/base" 5 | 6 | routing "github.com/gly-hub/fasthttp-routing" 7 | "github.com/team-dandelion/go-dandelion/tools/rpccall" 8 | ) 9 | 10 | // CreateRole 11 | // @tags 角色管理 12 | // @summary 创建角色 13 | // @description 创建系统角色 14 | // @router /api/sys_role/create [post] 15 | // @param req body base.CreateRoleReq true "json入参" 16 | // @success 200 {object} base.CreateRoleResp "返回值" 17 | func (c *Controller) CreateRole(ctx *routing.Context) error { 18 | return rpccall.SProtoCall(ctx, &base.CreateRoleReq{}, c.BaseServer.CreateRole) 19 | } 20 | 21 | // UpdateRole 22 | // @tags 角色管理 23 | // @summary 更新角色 24 | // @description 更新系统角色 25 | // @router /api/sys_role/update [put] 26 | // @param req body base.UpdateRoleReq true "json入参" 27 | // @success 200 {object} base.UpdateRoleResp "返回值" 28 | func (c *Controller) UpdateRole(ctx *routing.Context) error { 29 | return rpccall.SProtoCall(ctx, &base.UpdateRoleReq{}, c.BaseServer.UpdateRole) 30 | } 31 | 32 | // DeleteRole 33 | // @tags 角色管理 34 | // @summary 删除角色 35 | // @description 删除系统角色 36 | // @router /api/sys_role/delete [delete] 37 | // @param req body base.DeleteRoleReq true "json入参" 38 | // @success 200 {object} base.DeleteRoleResp "返回值" 39 | func (c *Controller) DeleteRole(ctx *routing.Context) error { 40 | return rpccall.SProtoCall(ctx, &base.DeleteRoleReq{}, c.BaseServer.DeleteRole) 41 | } 42 | 43 | // GetRoleList 44 | // @tags 角色管理 45 | // @summary 获取角色列表 46 | // @description 获取系统角色列表 47 | // @router /api/sys_role/search [post] 48 | // @param req body base.GetRoleListReq true "json入参" 49 | // @success 200 {object} base.GetRoleListResp "返回值" 50 | func (c *Controller) GetRoleList(ctx *routing.Context) error { 51 | return rpccall.SProtoCall(ctx, &base.GetRoleListReq{}, c.BaseServer.GetRoleList) 52 | } 53 | 54 | // AssignUsers 55 | // @tags 角色管理 56 | // @summary 分配用户 57 | // @description 为角色分配用户 58 | // @router /api/sys_role/assign_users [post] 59 | // @param req body base.AssignUsersReq true "json入参" 60 | // @success 200 {object} base.AssignUsersResp "返回值" 61 | func (c *Controller) AssignUsers(ctx *routing.Context) error { 62 | return rpccall.SProtoCall(ctx, &base.AssignUsersReq{}, c.BaseServer.AssignUsers) 63 | } 64 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/internal/service/base-server/sys_user_controller.go: -------------------------------------------------------------------------------- 1 | package baseserver 2 | 3 | import ( 4 | "dandelion-admin-server/proto/base" 5 | 6 | routing "github.com/gly-hub/fasthttp-routing" 7 | "github.com/team-dandelion/go-dandelion/tools/rpccall" 8 | ) 9 | 10 | // GetSysUserList 11 | // @tags 系统用户管理 12 | // @summary 获取用户列表 13 | // @description 获取系统用户列表 14 | // @router /api/sys_user/search [post] 15 | // @param req body base.GetSysUserListReq true "json入参" 16 | // @success 200 {object} base.GetSysUserListResp "返回值" 17 | func (c *Controller) GetSysUserList(ctx *routing.Context) error { 18 | return rpccall.SProtoCall(ctx, &base.GetSysUserListReq{}, c.BaseServer.GetSysUserList) 19 | } 20 | 21 | // CreateSysUser 22 | // @tags 系统用户管理 23 | // @summary 创建用户 24 | // @description 创建系统用户 25 | // @router /api/sys_user/create [post] 26 | // @param req body base.CreateSysUserReq true "json入参" 27 | // @success 200 {object} base.CreateSysUserResp "返回值" 28 | func (c *Controller) CreateSysUser(ctx *routing.Context) error { 29 | return rpccall.SProtoCall(ctx, &base.CreateSysUserReq{}, c.BaseServer.CreateSysUser) 30 | } 31 | 32 | // UpdateSysUser 33 | // @tags 系统用户管理 34 | // @summary 更新用户 35 | // @description 更新系统用户 36 | // @router /api/sys_user/update [put] 37 | // @param req body base.UpdateSysUserReq true "json入参" 38 | // @success 200 {object} base.UpdateSysUserResp "返回值" 39 | func (c *Controller) UpdateSysUser(ctx *routing.Context) error { 40 | return rpccall.SProtoCall(ctx, &base.UpdateSysUserReq{}, c.BaseServer.UpdateSysUser) 41 | } 42 | 43 | // DeleteSysUser 44 | // @tags 系统用户管理 45 | // @summary 删除用户 46 | // @description 删除系统用户 47 | // @router /api/sys_user/delete [delete] 48 | // @param req body base.DeleteSysUserReq true "json入参" 49 | // @success 200 {object} base.DeleteSysUserResp "返回值" 50 | func (c *Controller) DeleteSysUser(ctx *routing.Context) error { 51 | return rpccall.SProtoCall(ctx, &base.DeleteSysUserReq{}, c.BaseServer.DeleteSysUser) 52 | } 53 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "dandelion-admin-server/gateway/cmd" 5 | _ "dandelion-admin-server/gateway/docs" 6 | ) 7 | 8 | // @title Your API Title 9 | // @version 1.0 10 | // @description Your API Description 11 | // @host localhost:8080 12 | // @BasePath /api 13 | func main() { 14 | cmd.Execute() 15 | } 16 | -------------------------------------------------------------------------------- /dandelion-admin-server/gateway/static/gateway.txt: -------------------------------------------------------------------------------- 1 | _ 2 | __ _ __ _ | |_ ___ __ __ __ _ _ _ 3 | / _` | / _` | | __| / _ \ \ \ /\ / / / _` | | | | | 4 | | (_| | | (_| | | |_ | __/ \ V V / | (_| | | |_| | 5 | \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, | 6 | |___/ |___/ 7 | -------------------------------------------------------------------------------- /dandelion-admin-server/go.mod: -------------------------------------------------------------------------------- 1 | module dandelion-admin-server 2 | 3 | go 1.20 4 | 5 | replace github.com/team-dandelion/go-dandelion v1.3.0 => ../../go-dandelion 6 | 7 | require ( 8 | github.com/gly-hub/dandelion-plugs v0.0.0-20240416021220-06a23dda6a2b 9 | github.com/gly-hub/fasthttp-routing v0.0.0-20230103092213-f65d0ebb75bb 10 | github.com/gly-hub/toolbox v0.0.0-20240302072516-c93ff26bbbcc 11 | github.com/go-ozzo/ozzo-routing v2.1.4+incompatible 12 | github.com/gogo/protobuf v1.3.2 13 | github.com/golang/protobuf v1.5.2 14 | github.com/smallnest/rpcx v1.8.7 15 | github.com/spf13/cobra v1.8.1 16 | github.com/team-dandelion/go-dandelion v1.3.0 17 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 18 | ) 19 | 20 | require ( 21 | github.com/FZambia/sentinel v1.1.1 // indirect 22 | github.com/KyleBanks/depth v1.2.1 // indirect 23 | github.com/PuerkitoBio/purell v1.1.1 // indirect 24 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 25 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 26 | github.com/akutz/memconn v0.1.0 // indirect 27 | github.com/alibaba/sentinel-golang v1.0.4 // indirect 28 | github.com/alitto/pond v1.8.3 // indirect 29 | github.com/andybalholm/brotli v1.0.5 // indirect 30 | github.com/apache/thrift v0.18.1 // indirect 31 | github.com/armon/go-metrics v0.4.0 // indirect 32 | github.com/beorn7/perks v1.0.1 // indirect 33 | github.com/cenk/backoff v2.2.1+incompatible // indirect 34 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 35 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 36 | github.com/coreos/go-semver v0.3.0 // indirect 37 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 38 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 39 | github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // indirect 40 | github.com/edwingeng/doublejump v1.0.1 // indirect 41 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 42 | github.com/fatih/color v1.14.1 // indirect 43 | github.com/fsnotify/fsnotify v1.6.0 // indirect 44 | github.com/go-ole/go-ole v1.2.4 // indirect 45 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 46 | github.com/go-openapi/jsonreference v0.19.6 // indirect 47 | github.com/go-openapi/spec v0.20.4 // indirect 48 | github.com/go-openapi/swag v0.19.15 // indirect 49 | github.com/go-ping/ping v1.1.0 // indirect 50 | github.com/go-sql-driver/mysql v1.7.0 // indirect 51 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 52 | github.com/golang/mock v1.6.0 // indirect 53 | github.com/golang/snappy v0.0.4 // indirect 54 | github.com/gomodule/redigo v1.8.9 // indirect 55 | github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect 56 | github.com/google/uuid v1.3.0 // indirect 57 | github.com/grandcat/zeroconf v1.0.0 // indirect 58 | github.com/hashicorp/consul/api v1.18.0 // indirect 59 | github.com/hashicorp/errwrap v1.1.0 // indirect 60 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 61 | github.com/hashicorp/go-hclog v1.2.1 // indirect 62 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 63 | github.com/hashicorp/go-multierror v1.1.1 // indirect 64 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 65 | github.com/hashicorp/golang-lru v0.5.4 // indirect 66 | github.com/hashicorp/hcl v1.0.0 // indirect 67 | github.com/hashicorp/serf v0.10.1 // indirect 68 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 69 | github.com/jackc/pgpassfile v1.0.0 // indirect 70 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 71 | github.com/jackc/pgx/v5 v5.3.0 // indirect 72 | github.com/jinzhu/inflection v1.0.0 // indirect 73 | github.com/jinzhu/now v1.1.5 // indirect 74 | github.com/josharian/intern v1.0.0 // indirect 75 | github.com/json-iterator/go v1.1.12 // indirect 76 | github.com/juju/ratelimit v1.0.2 // indirect 77 | github.com/julienschmidt/httprouter v1.3.0 // indirect 78 | github.com/kavu/go_reuseport v1.5.0 // indirect 79 | github.com/klauspost/compress v1.16.3 // indirect 80 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 81 | github.com/klauspost/reedsolomon v1.11.7 // indirect 82 | github.com/magiconair/properties v1.8.7 // indirect 83 | github.com/mailru/easyjson v0.7.6 // indirect 84 | github.com/mattn/go-colorable v0.1.13 // indirect 85 | github.com/mattn/go-isatty v0.0.17 // indirect 86 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 87 | github.com/miekg/dns v1.1.51 // indirect 88 | github.com/mitchellh/go-homedir v1.1.0 // indirect 89 | github.com/mitchellh/mapstructure v1.5.0 // indirect 90 | github.com/mna/redisc v1.3.2 // indirect 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 92 | github.com/modern-go/reflect2 v1.0.2 // indirect 93 | github.com/onsi/ginkgo/v2 v2.9.0 // indirect 94 | github.com/opentracing/opentracing-go v1.1.0 // indirect 95 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 96 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 97 | github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect 98 | github.com/philhofer/fwd v1.1.2 // indirect 99 | github.com/pkg/errors v0.9.1 // indirect 100 | github.com/prometheus/client_golang v1.11.1 // indirect 101 | github.com/prometheus/client_model v0.2.0 // indirect 102 | github.com/prometheus/common v0.26.0 // indirect 103 | github.com/prometheus/procfs v0.6.0 // indirect 104 | github.com/quic-go/qtls-go1-19 v0.3.2 // indirect 105 | github.com/quic-go/qtls-go1-20 v0.2.2 // indirect 106 | github.com/quic-go/quic-go v0.34.0 // indirect 107 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 108 | github.com/rpcxio/libkv v0.5.1 // indirect 109 | github.com/rpcxio/rpcx-consul v0.0.0-20220730062257-1ff0472e730f // indirect 110 | github.com/rpcxio/rpcx-etcd v0.2.0 // indirect 111 | github.com/rpcxio/rpcx-zookeeper v0.0.0-20220730061732-d20531677676 // indirect 112 | github.com/rs/cors v1.8.3 // indirect 113 | github.com/rs/xid v1.4.0 // indirect 114 | github.com/rubyist/circuitbreaker v2.2.1+incompatible // indirect 115 | github.com/samuel/go-zookeeper v0.0.0-20201211165307-7117e9ea2414 // indirect 116 | github.com/shirou/gopsutil/v3 v3.21.6 // indirect 117 | github.com/smallnest/quick v0.1.0 // indirect 118 | github.com/soheilhy/cmux v0.1.5 // indirect 119 | github.com/spf13/afero v1.9.3 // indirect 120 | github.com/spf13/cast v1.5.0 // indirect 121 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 122 | github.com/spf13/pflag v1.0.5 // indirect 123 | github.com/spf13/viper v1.15.0 // indirect 124 | github.com/subosito/gotenv v1.4.2 // indirect 125 | github.com/swaggo/files v1.0.0 // indirect 126 | github.com/swaggo/swag v1.8.10 // indirect 127 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect 128 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect 129 | github.com/tinylib/msgp v1.1.8 // indirect 130 | github.com/tjfoc/gmsm v1.4.1 // indirect 131 | github.com/tklauser/go-sysconf v0.3.6 // indirect 132 | github.com/tklauser/numcpus v0.2.2 // indirect 133 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect 134 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 135 | github.com/valyala/bytebufferpool v1.0.0 // indirect 136 | github.com/valyala/fasthttp v1.45.0 // indirect 137 | github.com/valyala/fastrand v1.1.0 // indirect 138 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 139 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 140 | github.com/xtaci/kcp-go v5.4.20+incompatible // indirect 141 | go.etcd.io/etcd/api/v3 v3.5.6 // indirect 142 | go.etcd.io/etcd/client/pkg/v3 v3.5.6 // indirect 143 | go.etcd.io/etcd/client/v2 v2.305.6 // indirect 144 | go.etcd.io/etcd/client/v3 v3.5.6 // indirect 145 | go.uber.org/atomic v1.9.0 // indirect 146 | go.uber.org/multierr v1.8.0 // indirect 147 | go.uber.org/zap v1.21.0 // indirect 148 | golang.org/x/crypto v0.7.0 // indirect 149 | golang.org/x/exp v0.0.0-20230307190834-24139beb5833 // indirect 150 | golang.org/x/mod v0.9.0 // indirect 151 | golang.org/x/net v0.8.0 // indirect 152 | golang.org/x/sync v0.1.0 // indirect 153 | golang.org/x/sys v0.6.0 // indirect 154 | golang.org/x/text v0.10.0 // indirect 155 | golang.org/x/tools v0.7.0 // indirect 156 | google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect 157 | google.golang.org/grpc v1.53.0 // indirect 158 | google.golang.org/protobuf v1.29.1 // indirect 159 | gopkg.in/ini.v1 v1.67.0 // indirect 160 | gopkg.in/yaml.v2 v2.4.0 // indirect 161 | gopkg.in/yaml.v3 v3.0.1 // indirect 162 | gorm.io/driver/mysql v1.4.7 // indirect 163 | gorm.io/driver/postgres v1.5.0 // indirect 164 | gorm.io/plugin/dbresolver v1.4.1 // indirect 165 | ) 166 | -------------------------------------------------------------------------------- /dandelion-admin-server/proto/Makefile: -------------------------------------------------------------------------------- 1 | protoMake: 2 | protoc -I. -I${GOPATH}/src -I${GOPATH}/pkg/mod \ 3 | --gofast_out=. --gofast_opt=paths=source_relative \ 4 | --rpcx_out=. --rpcx_opt=paths=source_relative *.proto 5 | -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/authorize.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./;base"; 4 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 5 | import "github.com/team-dandelion/go-dandelion/tools/rpccall/lib.proto"; 6 | import "sys_menu.proto"; 7 | 8 | package authorize_proto; 9 | 10 | message LoginParams { 11 | string user_name = 1 [(gogoproto.jsontag) = 'user_name']; 12 | string password = 2 [(gogoproto.jsontag) = 'password']; 13 | } 14 | 15 | message LoginResp { 16 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 17 | string token = 2 [(gogoproto.jsontag) = 'token']; 18 | } 19 | 20 | message GetUserMenuReq { 21 | } 22 | 23 | message GetUserMenuResp { 24 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 25 | repeated menu_proto.MenuTreeNode list = 2 [(gogoproto.jsontag) = 'list']; 26 | } 27 | 28 | message GetUserInfoReq { 29 | } 30 | 31 | message GetUserInfoResp { 32 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 33 | UserInfo user_info = 2 [(gogoproto.jsontag) = 'user_info']; 34 | } 35 | 36 | message UserInfo { 37 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 38 | string user_name = 2 [(gogoproto.jsontag) = 'user_name']; 39 | string nick_name = 3 [(gogoproto.jsontag) = 'nick_name']; 40 | string avatar = 4 [(gogoproto.jsontag) = 'avatar']; 41 | string phone = 5 [(gogoproto.jsontag) = 'phone']; 42 | int32 status = 6 [(gogoproto.jsontag) = 'status']; 43 | } -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/server.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: server.proto 3 | 4 | package base 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | func init() { proto.RegisterFile("server.proto", fileDescriptor_ad098daeda4239f7) } 24 | 25 | var fileDescriptor_ad098daeda4239f7 = []byte{ 26 | // 436 bytes of a gzipped FileDescriptorProto 27 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x93, 0xdd, 0x4a, 0x23, 0x31, 28 | 0x18, 0x86, 0xb7, 0x07, 0xfb, 0x43, 0x76, 0xb7, 0xcb, 0xe6, 0x64, 0xb7, 0xa3, 0x8c, 0xd5, 0x0b, 29 | 0x18, 0x41, 0x0f, 0xc5, 0x83, 0xb6, 0x62, 0x11, 0x2a, 0x94, 0xfe, 0x20, 0x78, 0x52, 0xa6, 0xfa, 30 | 0x59, 0x07, 0xda, 0x49, 0xcc, 0x37, 0x23, 0xd4, 0x4b, 0xf0, 0x0a, 0xbc, 0x24, 0x0f, 0xbd, 0x04, 31 | 0xa9, 0x37, 0x22, 0x49, 0xa6, 0x9d, 0xa4, 0x99, 0x69, 0x4f, 0x0a, 0x79, 0x9f, 0x37, 0xcf, 0xf0, 32 | 0xa5, 0x09, 0xf9, 0x85, 0x20, 0x1e, 0x41, 0x04, 0x5c, 0xb0, 0x84, 0x79, 0x7f, 0xc2, 0x34, 0xb9, 33 | 0x67, 0x22, 0x7a, 0x82, 0x2c, 0xa8, 0xe2, 0x1c, 0x47, 0x33, 0x88, 0x53, 0x73, 0x9d, 0xe2, 0x6a, 34 | 0x83, 0x5a, 0x0b, 0x36, 0xcd, 0xfa, 0x47, 0xcf, 0x84, 0xfc, 0x6d, 0x86, 0x08, 0x7d, 0x65, 0x95, 35 | 0xbf, 0xd1, 0x0d, 0xd0, 0x06, 0xf9, 0xda, 0x61, 0x93, 0x28, 0xa6, 0xbb, 0xc1, 0xea, 0x03, 0x23, 36 | 0xb5, 0x21, 0x50, 0x79, 0x37, 0x14, 0xe1, 0x0c, 0x3d, 0xaf, 0x98, 0xf6, 0x00, 0x39, 0xed, 0x92, 37 | 0x9f, 0x6d, 0x48, 0x86, 0x08, 0xe2, 0x12, 0xe2, 0x94, 0xee, 0x39, 0x55, 0x83, 0xf6, 0xe0, 0xc1, 38 | 0xab, 0x6f, 0x2e, 0x58, 0xc6, 0x8b, 0xf8, 0x8e, 0x95, 0x1b, 0x25, 0xdd, 0x68, 0xd4, 0x05, 0xe4, 39 | 0xf4, 0x5c, 0x19, 0xe5, 0x07, 0x06, 0x02, 0x80, 0x7a, 0x81, 0x3c, 0xb8, 0xbc, 0xbb, 0x04, 0x52, 40 | 0xb6, 0x53, 0xca, 0x90, 0xd3, 0x16, 0x21, 0x2d, 0x01, 0x61, 0x02, 0x6a, 0xd4, 0x9a, 0x59, 0xcd, 41 | 0x73, 0x69, 0xf1, 0xca, 0x90, 0x96, 0x0c, 0xf9, 0x6d, 0xa1, 0x24, 0xcf, 0x1d, 0x89, 0x89, 0xb4, 42 | 0xe4, 0x0c, 0xa6, 0x50, 0x24, 0xc9, 0x73, 0x47, 0x62, 0x22, 0xe4, 0xf4, 0x94, 0xfc, 0xe8, 0x33, 43 | 0xa1, 0x46, 0xa4, 0xff, 0xcc, 0xde, 0x32, 0x95, 0x82, 0xff, 0xc5, 0x00, 0x39, 0x1d, 0x90, 0xdf, 44 | 0x7a, 0xb4, 0xfe, 0x1c, 0xe5, 0x71, 0xd3, 0x7a, 0xb0, 0xbc, 0x84, 0xd6, 0xe4, 0x19, 0x96, 0xb2, 45 | 0xfd, 0x2d, 0x0d, 0x6d, 0xd5, 0xb3, 0x96, 0x5a, 0x2d, 0x5c, 0x68, 0x5d, 0x6b, 0x68, 0xab, 0x1e, 46 | 0xbe, 0xd4, 0x6a, 0xe1, 0x42, 0xeb, 0x5a, 0x03, 0x39, 0xbd, 0x22, 0xd5, 0x36, 0x24, 0x59, 0xd2, 47 | 0x89, 0x30, 0xa1, 0xce, 0x26, 0x9b, 0x4b, 0xef, 0xc1, 0xb6, 0x8a, 0x79, 0xd1, 0x7a, 0x6c, 0x0a, 48 | 0xb4, 0x16, 0xc8, 0x87, 0x6c, 0x9d, 0x98, 0xcc, 0xf5, 0xdf, 0x5b, 0x82, 0xcc, 0x8b, 0xe6, 0x4a, 49 | 0xf2, 0xdc, 0x91, 0x98, 0xc8, 0xbc, 0x68, 0xae, 0x24, 0xcf, 0x1d, 0x89, 0x89, 0x56, 0xef, 0x4f, 50 | 0x2e, 0xd5, 0x21, 0x59, 0x55, 0x03, 0xe8, 0xf7, 0x57, 0xc6, 0xb4, 0xa7, 0x81, 0x18, 0x4d, 0x62, 51 | 0x79, 0x58, 0x68, 0x7b, 0x0c, 0xe0, 0x78, 0x2c, 0x86, 0xbc, 0x59, 0x7b, 0x5d, 0xf8, 0x95, 0xb7, 52 | 0x85, 0x5f, 0x79, 0x5f, 0xf8, 0x95, 0x97, 0x0f, 0xff, 0xcb, 0xf5, 0xf7, 0xe0, 0xf0, 0x64, 0x1c, 53 | 0x22, 0x8c, 0xbf, 0xa9, 0x1d, 0xc7, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x63, 0x86, 0x58, 0x8a, 54 | 0x7f, 0x05, 0x00, 0x00, 55 | } 56 | -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/server.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "./;base"; 3 | import "authorize.proto"; 4 | import "sys_menu.proto"; 5 | import "sys_user.proto"; 6 | import "sys_role.proto"; 7 | 8 | service BaseServerService { 9 | // 系统接口 10 | rpc Login(authorize_proto.LoginParams) returns (authorize_proto.LoginResp); 11 | rpc GetUserMenu(authorize_proto.GetUserMenuReq) returns (authorize_proto.GetUserMenuResp); 12 | rpc GetUserInfo(authorize_proto.GetUserInfoReq) returns (authorize_proto.GetUserInfoResp); 13 | 14 | // 菜单管理 15 | rpc GetMenuTree(menu_proto.GetMenuTreeReq) returns (menu_proto.GetMenuTreeResp); 16 | rpc CreateMenu(menu_proto.CreateMenuReq) returns (menu_proto.CreateMenuResp); 17 | rpc UpdateMenu(menu_proto.UpdateMenuReq) returns (menu_proto.UpdateMenuResp); 18 | rpc DeleteMenu(menu_proto.DeleteMenuReq) returns (menu_proto.DeleteMenuResp); 19 | rpc SortMenu(menu_proto.SortMenuReq) returns (menu_proto.SortMenuResp); 20 | 21 | // 用户管理 22 | rpc CreateSysUser(sys_user_proto.CreateSysUserReq) returns (sys_user_proto.CreateSysUserResp); 23 | rpc UpdateSysUser(sys_user_proto.UpdateSysUserReq) returns (sys_user_proto.UpdateSysUserResp); 24 | rpc DeleteSysUser(sys_user_proto.DeleteSysUserReq) returns (sys_user_proto.DeleteSysUserResp); 25 | rpc GetSysUserList(sys_user_proto.GetSysUserListReq) returns (sys_user_proto.GetSysUserListResp); 26 | 27 | // 角色管理 28 | rpc CreateRole(role_proto.CreateRoleReq) returns (role_proto.CreateRoleResp); 29 | rpc UpdateRole(role_proto.UpdateRoleReq) returns (role_proto.UpdateRoleResp); 30 | rpc DeleteRole(role_proto.DeleteRoleReq) returns (role_proto.DeleteRoleResp); 31 | rpc GetRoleList(role_proto.GetRoleListReq) returns (role_proto.GetRoleListResp); 32 | rpc AssignUsers(role_proto.AssignUsersReq) returns (role_proto.AssignUsersResp); 33 | } 34 | -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/sys_menu.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "./;base"; 3 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 4 | import "github.com/team-dandelion/go-dandelion/tools/rpccall/lib.proto"; 5 | 6 | package menu_proto; 7 | 8 | message MenuTreeNode { 9 | uint32 id = 1 [(gogoproto.jsontag) = 'id']; 10 | uint32 parent_id = 2 [(gogoproto.jsontag) = 'parent_id']; 11 | string name = 3 [(gogoproto.jsontag) = 'name']; 12 | string path = 4 [(gogoproto.jsontag) = 'path']; 13 | int32 type = 5 [(gogoproto.jsontag) = 'type']; 14 | string icon = 6 [(gogoproto.jsontag) = 'icon']; 15 | int32 sort = 7 [(gogoproto.jsontag) = 'sort']; 16 | int32 status = 8 [(gogoproto.jsontag) = 'status']; 17 | repeated MenuTreeNode children = 9 [(gogoproto.jsontag) = 'children']; 18 | } 19 | 20 | message GetMenuTreeReq { 21 | } 22 | 23 | message GetMenuTreeResp { 24 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 25 | repeated MenuTreeNode list = 2 [(gogoproto.jsontag) = 'list']; 26 | } 27 | 28 | message CreateMenuReq { 29 | uint32 parent_id = 1 [(gogoproto.jsontag) = 'parent_id']; 30 | string name = 2 [(gogoproto.jsontag) = 'name']; 31 | string path = 3 [(gogoproto.jsontag) = 'path']; 32 | int32 type = 4 [(gogoproto.jsontag) = 'type']; 33 | string icon = 5 [(gogoproto.jsontag) = 'icon']; 34 | int32 sort = 6 [(gogoproto.jsontag) = 'sort']; 35 | int32 status = 7 [(gogoproto.jsontag) = 'status']; 36 | } 37 | 38 | message CreateMenuResp { 39 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 40 | uint32 id = 2 [(gogoproto.jsontag) = 'id']; 41 | } 42 | 43 | message UpdateMenuReq { 44 | uint32 id = 1 [(gogoproto.jsontag) = 'id']; 45 | uint32 parent_id = 2 [(gogoproto.jsontag) = 'parent_id']; 46 | string name = 3 [(gogoproto.jsontag) = 'name']; 47 | string path = 4 [(gogoproto.jsontag) = 'path']; 48 | int32 type = 5 [(gogoproto.jsontag) = 'type']; 49 | string icon = 6 [(gogoproto.jsontag) = 'icon']; 50 | int32 sort = 7 [(gogoproto.jsontag) = 'sort']; 51 | int32 status = 8 [(gogoproto.jsontag) = 'status']; 52 | } 53 | 54 | message UpdateMenuResp { 55 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 56 | } 57 | 58 | message DeleteMenuReq { 59 | uint32 id = 1 [(gogoproto.jsontag) = 'id']; 60 | } 61 | 62 | message DeleteMenuResp { 63 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 64 | } 65 | 66 | message MenuSort { 67 | uint32 id = 1 [(gogoproto.jsontag) = 'id']; 68 | int32 sequence = 2 [(gogoproto.jsontag) = 'sequence']; 69 | uint32 parent_id = 3 [(gogoproto.jsontag) = 'parent_id']; 70 | } 71 | 72 | message SortMenuReq { 73 | repeated MenuSort list = 1 [(gogoproto.jsontag) = 'list']; 74 | } 75 | 76 | message SortMenuResp { 77 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 78 | } 79 | -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/sys_role.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "./;base"; 3 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 4 | import "github.com/team-dandelion/go-dandelion/tools/rpccall/lib.proto"; 5 | 6 | package role_proto; 7 | 8 | // 角色信息 9 | message RoleInfo { 10 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 11 | string name = 2 [(gogoproto.jsontag) = 'name']; 12 | string description = 3 [(gogoproto.jsontag) = 'description']; 13 | int32 status = 4 [(gogoproto.jsontag) = 'status']; 14 | int64 created_at = 5 [(gogoproto.jsontag) = 'created_at']; 15 | int64 updated_at = 6 [(gogoproto.jsontag) = 'updated_at']; 16 | repeated int32 menu_ids = 7 [(gogoproto.jsontag) = 'menu_ids']; 17 | repeated int32 user_ids = 8 [(gogoproto.jsontag) = 'user_ids']; 18 | } 19 | 20 | // 创建角色请求 21 | message CreateRoleReq { 22 | string name = 1 [(gogoproto.jsontag) = 'name']; 23 | string description = 2 [(gogoproto.jsontag) = 'description']; 24 | int32 status = 3 [(gogoproto.jsontag) = 'status']; 25 | repeated int32 menu_ids = 4 [(gogoproto.jsontag) = 'menu_ids']; 26 | } 27 | 28 | message CreateRoleResp { 29 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 30 | int32 id = 2 [(gogoproto.jsontag) = 'id']; 31 | } 32 | 33 | // 更新角色请求 34 | message UpdateRoleReq { 35 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 36 | string name = 2 [(gogoproto.jsontag) = 'name']; 37 | string description = 3 [(gogoproto.jsontag) = 'description']; 38 | int32 status = 4 [(gogoproto.jsontag) = 'status']; 39 | repeated int32 menu_ids = 5 [(gogoproto.jsontag) = 'menu_ids']; 40 | } 41 | 42 | message UpdateRoleResp { 43 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 44 | } 45 | 46 | // 删除角色请求 47 | message DeleteRoleReq { 48 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 49 | } 50 | 51 | message DeleteRoleResp { 52 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 53 | } 54 | 55 | // 角色列表请求 56 | message GetRoleListReq { 57 | string name = 1 [(gogoproto.jsontag) = 'name']; 58 | int32 status = 2 [(gogoproto.jsontag) = 'status']; 59 | int32 page = 3 [(gogoproto.jsontag) = 'page']; 60 | int32 page_size = 4 [(gogoproto.jsontag) = 'page_size']; 61 | } 62 | 63 | message GetRoleListResp { 64 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 65 | repeated RoleInfo list = 2 [(gogoproto.jsontag) = 'list']; 66 | int64 total = 3 [(gogoproto.jsontag) = 'total']; 67 | } 68 | 69 | // 分配用户请求 70 | message AssignUsersReq { 71 | int32 role_id = 1 [(gogoproto.jsontag) = 'role_id']; 72 | repeated int32 user_ids = 2 [(gogoproto.jsontag) = 'user_ids']; 73 | } 74 | 75 | message AssignUsersResp { 76 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 77 | } -------------------------------------------------------------------------------- /dandelion-admin-server/proto/base/sys_user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./;base"; 4 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 5 | import "github.com/team-dandelion/go-dandelion/tools/rpccall/lib.proto"; 6 | 7 | package sys_user_proto; 8 | 9 | // 创建用户 10 | message CreateSysUserReq { 11 | string user_name = 1 [(gogoproto.jsontag) = 'user_name']; 12 | string password = 2 [(gogoproto.jsontag) = 'password']; 13 | string nick_name = 3 [(gogoproto.jsontag) = 'nick_name']; 14 | string avatar = 4 [(gogoproto.jsontag) = 'avatar']; 15 | string phone = 5 [(gogoproto.jsontag) = 'phone']; 16 | int32 status = 6 [(gogoproto.jsontag) = 'status']; 17 | } 18 | 19 | message CreateSysUserResp { 20 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 21 | } 22 | 23 | // 更新用户 24 | message UpdateSysUserReq { 25 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 26 | string nick_name = 2 [(gogoproto.jsontag) = 'nick_name']; 27 | string avatar = 3 [(gogoproto.jsontag) = 'avatar']; 28 | string phone = 4 [(gogoproto.jsontag) = 'phone']; 29 | int32 status = 5 [(gogoproto.jsontag) = 'status']; 30 | } 31 | 32 | message UpdateSysUserResp { 33 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 34 | } 35 | 36 | // 删除用户 37 | message DeleteSysUserReq { 38 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 39 | } 40 | 41 | message DeleteSysUserResp { 42 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 43 | } 44 | 45 | // 用户列表 46 | message GetSysUserListReq { 47 | string user_name = 1 [(gogoproto.jsontag) = 'user_name']; 48 | string phone = 2 [(gogoproto.jsontag) = 'phone']; 49 | int32 status = 3 [(gogoproto.jsontag) = 'status']; 50 | int32 page = 4 [(gogoproto.jsontag) = 'page']; 51 | int32 page_size = 5 [(gogoproto.jsontag) = 'page_size']; 52 | } 53 | 54 | message GetSysUserListResp { 55 | rpccall.CommonResp common_resp = 1 [(gogoproto.jsontag) = '-']; 56 | repeated SysUserInfo list = 2 [(gogoproto.jsontag) = 'list']; 57 | int64 total = 3 [(gogoproto.jsontag) = 'total']; 58 | } 59 | 60 | message SysUserInfo { 61 | int32 id = 1 [(gogoproto.jsontag) = 'id']; 62 | string user_name = 2 [(gogoproto.jsontag) = 'user_name']; 63 | string nick_name = 3 [(gogoproto.jsontag) = 'nick_name']; 64 | string avatar = 4 [(gogoproto.jsontag) = 'avatar']; 65 | string phone = 5 [(gogoproto.jsontag) = 'phone']; 66 | int32 status = 6 [(gogoproto.jsontag) = 'status']; 67 | int64 created_at = 7 [(gogoproto.jsontag) = 'created_at']; 68 | int64 updated_at = 8 [(gogoproto.jsontag) = 'updated_at']; 69 | } -------------------------------------------------------------------------------- /dandelion-admin-server/proto/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 确保脚本在出错时退出 4 | set -e 5 | 6 | # 颜色输出函数 7 | GREEN='\033[0;32m' 8 | NC='\033[0m' 9 | 10 | echo -e "${GREEN}开始生成 protobuf 文件...${NC}" 11 | 12 | # 遍历所有子目录 13 | for dir in $(find . -type d); do 14 | # 跳过当前目录和隐藏目录 15 | if [ "$dir" = "." ] || [[ "$dir" == ./.* ]]; then 16 | continue 17 | fi 18 | 19 | # 检查目录中是否有 .proto 文件 20 | if ls $dir/*.proto >/dev/null 2>&1; then 21 | echo -e "${GREEN}处理目录: $dir${NC}" 22 | 23 | # 进入目录 24 | cd $dir 25 | 26 | # 生成 protobuf 文件 27 | protoc -I. -I$GOPATH/src -I$GOPATH/pkg/mod \ 28 | --gofast_out=. --gofast_opt=paths=source_relative \ 29 | --rpcx_out=. --rpcx_opt=paths=source_relative \ 30 | *.proto 31 | 32 | # 返回上级目录 33 | cd - > /dev/null 34 | fi 35 | done 36 | 37 | echo -e "${GREEN}protobuf 文件生成完成!${NC}" -------------------------------------------------------------------------------- /dandelion-admin-server/tools/encrypt/md5.go: -------------------------------------------------------------------------------- 1 | package encrypt 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | // MD5 计算字符串的MD5值,返回32位小写字符串 10 | func MD5(str string) string { 11 | h := md5.New() 12 | h.Write([]byte(str)) 13 | return hex.EncodeToString(h.Sum(nil)) 14 | } 15 | 16 | // MD5Upper 计算字符串的MD5值,返回32位大写字符串 17 | func MD5Upper(str string) string { 18 | return strings.ToUpper(MD5(str)) 19 | } 20 | 21 | // MD5WithSalt 计算带盐值的MD5,返回32位小写字符串 22 | func MD5WithSalt(str, salt string) string { 23 | return MD5(str + salt) 24 | } 25 | 26 | // MD5WithSaltUpper 计算带盐值的MD5,返回32位大写字符串 27 | func MD5WithSaltUpper(str, salt string) string { 28 | return strings.ToUpper(MD5WithSalt(str, salt)) 29 | } 30 | 31 | // VerifyMD5 验证字符串与MD5是否匹配(不区分大小写) 32 | func VerifyMD5(str, md5str string) bool { 33 | return strings.EqualFold(MD5(str), md5str) 34 | } 35 | 36 | // VerifyMD5WithSalt 验证字符串与加盐的MD5是否匹配(不区分大小写) 37 | func VerifyMD5WithSalt(str, salt, md5str string) bool { 38 | return strings.EqualFold(MD5WithSalt(str, salt), md5str) 39 | } 40 | -------------------------------------------------------------------------------- /dandelion-admin-server/tools/encrypt/md5_test.go: -------------------------------------------------------------------------------- 1 | package encrypt 2 | 3 | import "testing" 4 | 5 | func TestMD5(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expected string 10 | }{ 11 | { 12 | name: "empty string", 13 | input: "", 14 | expected: "d41d8cd98f00b204e9800998ecf8427e", 15 | }, 16 | { 17 | name: "hello world", 18 | input: "hello world", 19 | expected: "5eb63bbbe01eeed093cb22bb8f5acdc3", 20 | }, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := MD5(tt.input); got != tt.expected { 26 | t.Errorf("MD5() = %v, want %v", got, tt.expected) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func TestMD5WithSalt(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | str string 36 | salt string 37 | expected string 38 | }{ 39 | { 40 | name: "with empty salt", 41 | str: "password", 42 | salt: "", 43 | expected: "5f4dcc3b5aa765d61d8327deb882cf99", 44 | }, 45 | { 46 | name: "with salt", 47 | str: "password", 48 | salt: "123", 49 | expected: "123934bb19708f8dac76a2e31f91cef0", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := MD5WithSalt(tt.str, tt.salt); got != tt.expected { 56 | t.Errorf("MD5WithSalt() = %v, want %v", got, tt.expected) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestVerifyMD5(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | str string 66 | md5str string 67 | expected bool 68 | }{ 69 | { 70 | name: "correct md5 lowercase", 71 | str: "hello world", 72 | md5str: "5eb63bbbe01eeed093cb22bb8f5acdc3", 73 | expected: true, 74 | }, 75 | { 76 | name: "correct md5 uppercase", 77 | str: "hello world", 78 | md5str: "5EB63BBBE01EEED093CB22BB8F5ACDC3", 79 | expected: true, 80 | }, 81 | { 82 | name: "incorrect md5", 83 | str: "hello world", 84 | md5str: "wrongmd5hash", 85 | expected: false, 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | if got := VerifyMD5(tt.str, tt.md5str); got != tt.expected { 92 | t.Errorf("VerifyMD5() = %v, want %v", got, tt.expected) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /dandelion-admin-ui/.cursorrules: -------------------------------------------------------------------------------- 1 | # 项目开发规范 2 | 3 | ## 目录结构规范 4 | src/ 5 | ├── api/ # API 接口定义 6 | │ ├── types.ts # 类型定义 7 | │ └── [module].ts # 模块接口 8 | ├── pages/ # 页面组件 9 | │ └── [module]/ # 模块页面 10 | ├── components/ # 公共组件 11 | ├── hooks/ # 自定义 hooks 12 | ├── store/ # 状态管理 13 | ├── types/ # 全局类型 14 | └── utils/ # 工具函数 15 | 16 | 17 | 18 | ## 开发流程约定 19 | 20 | 1. 前置准备 21 | - 整理功能点和需求 22 | - 设计数据结构和接口 23 | - 确认是否可复用现有组件 24 | - 评估对现有功能的影响 25 | 26 | 2. 接口开发 27 | - 在 api/types.ts 中定义接口类型 28 | - 在 api/[module].ts 中实现接口 29 | - 必须使用 utils/request.ts 发起请求 30 | - 遵循统一的 ApiResponse 格式 31 | 32 | 3. 页面开发 33 | - 页面组件放在 pages/[module] 目录 34 | - 页面私有组件放在对应页面目录下 35 | - 公共组件放在 components 目录 36 | - CSS Module 文件命名为 index.module.css 37 | 38 | 4. 状态管理 39 | - 模块状态放在 store/[module].ts 40 | - 全局状态放在 store/index.ts 41 | - 必须定义 action 类型 42 | 43 | ## 代码规范 44 | 45 | 1. TypeScript 46 | - 严格定义类型,禁止使用 any 47 | - 接口类型统一在 types.ts 中定义 48 | - 组件 props 必须定义接口 49 | 50 | 2. 请求处理 51 | - 统一使用 utils/request.ts 52 | - 处理所有异常情况 53 | - 遵循 RESTful 规范 54 | 55 | 3. 组件开发 56 | - 函数组件 + Hooks 57 | - 私有函数以 handle 开头 58 | - props 解构赋值 59 | - 必要时添加注释 60 | 61 | 4. 样式规范 62 | - 使用 CSS Module 63 | - BEM 命名规范 64 | - 避免内联样式 65 | 66 | ## 禁止事项 67 | 68 | 1. 不允许直接修改现有功能 69 | 2. 不允许直接使用 fetch/axios 70 | 3. 避免重复造轮子 71 | 4. 禁止在组件中直接操作 DOM 72 | 5. 禁止使用 class 组件 73 | 74 | ## 开发步骤示例 75 | 76 | 1. 新功能开发流程 77 | 78 | ```typescript 79 | // 1. 定义类型 (api/types.ts) 80 | interface NewFeatureParams { 81 | // ... 82 | } 83 | // 2. 实现接口 (api/newFeature.ts) 84 | import request from '@/utils/request'; 85 | export const newFeatureApi = async (params: NewFeatureParams) => { 86 | return request.post('/api/new-feature', params); 87 | }; 88 | // 3. 开发页面 (pages/newFeature/index.tsx) 89 | import styles from './index.module.css'; 90 | const NewFeature: React.FC = () => { 91 | // ... 92 | }; 93 | ``` 94 | 95 | 96 | 2. 功能优化流程 97 | - 记录优化点 98 | - 评估影响范围 99 | - 编写测试用例 100 | - 实现优化 101 | - 验证功能 102 | 103 | ## Git 提交规范 104 | 105 | commit 格式: 106 | - feat: 新功能 107 | - fix: 修复 108 | - docs: 文档 109 | - style: 格式 110 | - refactor: 重构 111 | - test: 测试 112 | - chore: 其他 -------------------------------------------------------------------------------- /dandelion-admin-ui/.env: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=/api 2 | VITE_APP_TITLE=Dandelion Admin -------------------------------------------------------------------------------- /dandelion-admin-ui/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=http://localhost:3000/api 2 | VITE_APP_TITLE=Dandelion Admin (Dev) -------------------------------------------------------------------------------- /dandelion-admin-ui/README.md: -------------------------------------------------------------------------------- 1 | # Dandelion Admin UI 2 | 3 | 基于 React + Vite + TypeScript + Ant Design 的现代化管理系统。 4 | 5 | ## 核心需求 6 | 7 | ### 1. 系统布局 8 | 9 | #### 1.1 左侧布局 10 | - 顶部:Logo + 系统名称 11 | - 底部:可折叠的菜单导航栏 12 | - 支持多级菜单 13 | - 菜单项通过后端接口动态生成 14 | - 支持菜单折叠/展开 15 | 16 | #### 1.2 右侧布局 17 | - 顶部Header 18 | - 面包屑导航 19 | - 用户信息 20 | - 系统设置入口 21 | - 内容区域Content 22 | - 响应式布局 23 | - 路由匹配对应页面组件 24 | - 支持页面缓存 25 | 26 | ### 2. 请求处理 27 | 28 | #### 2.1 统一请求封装 29 | - 基于Axios封装请求库 30 | - 支持请求/响应拦截器 31 | - 统一的错误处理机制 32 | - 支持请求重试 33 | - 请求超时处理 34 | 35 | #### 2.2 响应数据结构 36 | ```typescript 37 | interface ApiResponse { 38 | code: number; // 状态码,20000表示成功 39 | msg: string; // 响应信息 40 | request_id: string;// 请求ID,用于追踪 41 | data: T; // 具体业务数据 42 | } 43 | ``` 44 | 45 | #### 2.3 错误处理 46 | - 统一的错误处理中间件 47 | - code !== 20000 时显示全局错误提示 48 | - 支持特定接口自定义错误处理 49 | - 网络错误统一处理 50 | 51 | #### 2.4 Mock服务 52 | - 基于vite-plugin-mock实现 53 | - 支持开发环境动态mock 54 | - 支持生产环境mock配置 55 | - 模拟真实的接口延迟 56 | 57 | ### 3. 权限控制 58 | 59 | #### 3.1 菜单权限 60 | - 动态菜单树接口 61 | - 菜单配置结构 62 | ```typescript 63 | interface MenuItem { 64 | id: string; 65 | name: string; 66 | path: string; 67 | icon?: string; 68 | children?: MenuItem[]; 69 | permission?: string[]; 70 | } 71 | ``` 72 | 73 | #### 3.2 页面权限 74 | - 路由级别权限控制 75 | - 页面按钮级别权限控制 76 | - 权限code动态获取 77 | - 无权限时的优雅降级处理 78 | 79 | #### 3.3 权限实现方案 80 | - 基于RBAC(Role-Based Access Control)模型 81 | - 路由守卫实现页面权限控制 82 | - 自定义指令实现按钮级权限 83 | - 权限缓存优化 84 | 85 | ## 技术方案 86 | 87 | ### 1. 项目架构 88 | - 基于Vite构建 89 | - React 18 + TypeScript 90 | - Ant Design组件库 91 | - Pnpm包管理器 92 | 93 | ### 2. 状态管理 94 | - Zustand状态管理 95 | - 支持持久化存储 96 | - 模块化状态设计 97 | 98 | ### 3. 路由方案 99 | - React Router 6 100 | - 路由懒加载 101 | - 路由元信息配置 102 | - 路由守卫实现 103 | 104 | ### 4. 代码规范 105 | - ESLint + Prettier代码格式化 106 | - Husky + lint-staged提交校验 107 | - Commitlint提交信息规范 108 | - TypeScript严格模式 109 | 110 | ### 5. 构建优化 111 | - 路由懒加载 112 | - 组件按需加载 113 | - 资源预加载 114 | - 打包分析与优化 115 | 116 | ## 目录结构 117 | ``` 118 | src/ 119 | ├── api/ # API接口定义 120 | ├── assets/ # 静态资源 121 | ├── components/ # 公共组件 122 | ├── hooks/ # 自定义hooks 123 | ├── layouts/ # 布局组件 124 | ├── pages/ # 页面组件 125 | ├── router/ # 路由配置 126 | ├── store/ # 状态管理 127 | ├── styles/ # 全局样式 128 | ├── types/ # TS类型定义 129 | ├── utils/ # 工具函数 130 | └── mock/ # Mock数据 131 | ``` 132 | 133 | ## 开发计划 134 | 135 | ### Phase 1: 基础架构搭建 136 | - [√] 项目初始化 137 | - [√] 布局框架实现 138 | - [√] 路由系统搭建 139 | - [√] 请求库封装 140 | 141 | ### Phase 2: 核心功能实现 142 | - [√] 动态菜单实现 143 | - [√] 权限系统搭建 144 | - [√] Mock服务集成 145 | - [√] 状态管理实现 146 | 147 | ### Phase 3: 功能完善 148 | - [ ] 错误处理机制 149 | - [ ] 页面缓存实现 150 | - [ ] 主题配置 151 | - [ ] 性能优化 152 | 153 | ## 开发规范 154 | 155 | ### Git提交规范 156 | ``` 157 | feat: 新功能 158 | fix: 修复 159 | docs: 文档变更 160 | style: 代码格式 161 | refactor: 重构 162 | test: 测试 163 | chore: 构建过程或辅助工具的变动 164 | ``` 165 | 166 | ### 命名规范 167 | - 文件夹:小写中划线 168 | - 组件:大驼峰 169 | - 函数:小驼峰 170 | - 常量:大写下划线 171 | - 类型:大驼峰+Type后缀 172 | 173 | ## 运行项目 174 | 175 | ```bash 176 | # 安装依赖 177 | pnpm install 178 | 179 | # 开发环境 180 | pnpm dev 181 | 182 | # 构建 183 | pnpm build 184 | 185 | # 预览 186 | pnpm preview 187 | ``` 188 | -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/cursor教程.md: -------------------------------------------------------------------------------- 1 | 2 | 1.创建一个空项目 3 | > 创建一个dandelion-admin-ui的项目,该项目为一个管理后台,采用react + vite + ts + pnpm,样式上采用阿里开源的ant-design,你现在只需要创建一个空项目,不需要样式上的修改,引用需要的组件即可。你可以 创建并运行了 4 | 5 | 注意: 6 | 这里创建完后,没有进行正确的路径运行项目,所以我增加了一个对话 7 | > 注意 你创建完项目后 执行命令应该是在 dandelion-admin/dandelion-admin-ui这个路径下 8 | 9 | 2. 开始准备项目文档 10 | 通常不要一开始就下达开发指令,更应该先进行对话确定需求 11 | 12 | > 现在我需要开发一个最基础的管理系统。1.布局主体分为左右两个部分,左边:包含上下两部分,上方主要显示logo以及站点名称,下方则是菜单栏。右边也是上下两部分组成,上方是header,下方是content区域。切换菜单,content区域将展示对应的page。2.需要实现对应request请求api,然后通过mock代理进行数据模拟。后端的响应结构包含code、msg、request_id、data这个几个字段,其中每个接口的数据从data中获取,中间件中进行统一的错误判断,如果code不等于20000,则采用全局的错误提示。3.需要增加权限控制,侧边栏的菜单应该通过api返回的菜单树进行渲染,并且进入到每个页面都应该获取当前页面所拥有的权限code。这些是我目前能想到的需求,现在需要你整理并完善需求,然后把梳理出的方案写入到readme.md中 13 | 14 | 这时生成了一个对应的开发文档。但我发现其中初始化项目被标记为了未完成,实际已完成。所以我增加了一个对话 15 | 16 | > 当前项目已经初始化了,位于dandelion-admin/dandelion-admin-ui这个层级中 17 | 18 | 这时cursor开始为我下载依赖,并进行基础布局的开发。(pass:这里我预期是只进行目录定位,但cursor进行了开发了) 19 | 20 | 21 | ![alt text](img/image-1.png) 22 | 23 | 可以看到效果很差,这是我们则需要进行代码的优化。 24 | 25 | 3. 开始开发布局以及基础样式 26 | 先对已开发的布局进行调整。 27 | 28 | > 样式上存在问题,并没有很好的完成左右布局的设定,需要优化下代码 29 | 30 | 执行完的效果,可以看到整体的布局已经修复完成了: 31 | ![alt text](img/image-2.png) 32 | 33 | 但我觉得菜单栏的颜色不好看,所以进行了一波调整 34 | 35 | > 我期望左边的背景色为白色,菜单被选中时字体色为主题色,鼠标移入移出也需要有变化 36 | 37 | 执行完的效果: 38 | ![alt text](img/image-3.png) 39 | 40 | 目前整体效果已经出来了,接下来完成增加一些菜单测试数据,看多层级菜单展示是否正常 41 | 42 | > 我期望菜单的测试数据能够多一些层级 43 | 44 | ![alt text](img/image-4.png) 45 | 46 | 这是菜单的开发已经基本完成了,接下来补充上面包屑. 47 | 48 | > 增加面包屑功能 49 | 50 | 出现问题了。![alt text](img/image-5.png) 51 | 52 | 直接把错误丢给cursor 53 | 54 | > delion-admin/dandelion-admin-ui/src/layouts/components/menuConfig.ts:15:24: ERROR: Expected ">" but found "/" 55 | 56 | 修复完成,面包屑的功能已经完成: 57 | ![alt text](img/image-6.png) 58 | 59 | 根据readme里的步骤,截下来需要完成请求库封装。我们直接下命令(pass:因为readme是对话生成的,有上下文,我们直接让他封装即可) 60 | 61 | > 完成请求库封装 62 | 63 | 可以看到,相应结构也是之前我们预定的 64 | ![alt text](img/image-7.png) 65 | 66 | 67 | 3. 核心功能实现 68 | 69 | 刚实现了请求库封装,接下来则完成mock服务的配置,以及实现动态菜单 70 | 71 | > 完成mock服务的配置,以及实现动态菜单 72 | 73 | 这里实现完报错了。 74 | 出现了错误: 75 | > react-router-dom.js?v=0246ea5f:1192 Uncaught Error: useNavigate() may be used only in the context of a component. 76 | at AuthWrapper (AuthWrapper.tsx:11:20) 77 | 78 | 这里算是我的问题,动态路由应该在模拟登录后实现会比较好。先修复下问题。直接把错误丢给对话. 79 | 80 | 没能成功修复,只能进行restorele,直接回到第一个对话进行修改。 81 | 82 | > 先进行mock服务的配置,然后实现登录页面,在进行数据模拟实现动态菜单 83 | 84 | ![alt text](img/image-8.png) 85 | 86 | 代码生成完后,我们去到登录页面。发现mock应该是有问题的,接口404了,所以问题丢给他。 87 | 88 | > request.ts:93 POST http://localhost:3000/api/auth/login 404 (Not Found) 89 | 90 | 这一步是可以正常登录了,等菜单并没有改为通过接口获取,还是写死的。把这个进行优化。 91 | 92 | > 菜单应该通过接口获取,并且不同的账号获取到的模拟数据应该是不一样的。 93 | 94 | ![alt text](img/image-9.png) 95 | 96 | ![alt text](img/image-10.png) 97 | 98 | 到动态菜单也算是完成了。(pass:一直漏掉了一个点**git**,在创建项目的时候就应该先初始化git,每个模块完成都应该进行一下提交才对,我们进行下补救) 99 | 100 | > 初始化git并进行commit 101 | 102 | 接下来完善一下路由守卫(跟着文档的顺序进行) 103 | 104 | > 添加路由守卫 105 | 106 | 出现了错误依旧是直接让cursor进行修复。直到pnpm dev 可以正常运行。 107 | 108 | 109 | 到此基本逻辑就已经实现完了。 -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-1.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-10.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-2.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-3.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-4.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-5.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-6.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-7.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-8.png -------------------------------------------------------------------------------- /dandelion-admin-ui/docs/img/image-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gly-hub/dandelion-admin/645f4360f3eb419651df06ee13e77044159cccf0/dandelion-admin-ui/docs/img/image-9.png -------------------------------------------------------------------------------- /dandelion-admin-ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /dandelion-admin-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dandelion-admin-ui/mock/menu.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | import type { MenuItem } from '../src/api/types'; 3 | 4 | // 导出mockMenus以便其他文件使用 5 | export const mockMenus: MenuItem[] = [ 6 | { 7 | key: 'dashboard', 8 | label: '仪表盘', 9 | icon: 'HomeOutlined', 10 | }, 11 | { 12 | key: 'system', 13 | label: '系统管理', 14 | icon: 'SettingOutlined', 15 | children: [ 16 | { 17 | key: 'user', 18 | label: '用户管理', 19 | icon: 'UserOutlined', 20 | }, 21 | { 22 | key: 'menu', 23 | label: '菜单管理', 24 | icon: 'AppstoreOutlined', 25 | }, 26 | ], 27 | }, 28 | ]; 29 | 30 | // 递归查找并更新菜单项 31 | const updateMenuItem = (items: MenuItem[], key: string, updatedMenu: MenuItem): boolean => { 32 | for (let i = 0; i < items.length; i++) { 33 | if (items[i].key === key) { 34 | // 保留原有的children 35 | const children = items[i].children; 36 | items[i] = { ...items[i], ...updatedMenu }; 37 | if (children) { 38 | items[i].children = children; 39 | } 40 | return true; 41 | } 42 | if (items[i].children) { 43 | if (updateMenuItem(items[i].children, key, updatedMenu)) { 44 | return true; 45 | } 46 | } 47 | } 48 | return false; 49 | }; 50 | 51 | // 递归查找并删除菜单项 52 | const deleteMenuItem = (items: MenuItem[], key: string): boolean => { 53 | for (let i = 0; i < items.length; i++) { 54 | if (items[i].key === key) { 55 | items.splice(i, 1); 56 | return true; 57 | } 58 | if (items[i].children) { 59 | if (deleteMenuItem(items[i].children, key)) { 60 | // 如果子菜单为空,删除children属性 61 | if (items[i].children.length === 0) { 62 | delete items[i].children; 63 | } 64 | return true; 65 | } 66 | } 67 | } 68 | return false; 69 | }; 70 | 71 | // 递归查找父菜单并添加子菜单 72 | const addChildMenuItem = (items: MenuItem[], parentKey: string, newMenu: MenuItem): boolean => { 73 | for (let i = 0; i < items.length; i++) { 74 | if (items[i].key === parentKey) { 75 | if (!items[i].children) { 76 | items[i].children = []; 77 | } 78 | items[i].children.push(newMenu); 79 | return true; 80 | } 81 | if (items[i].children) { 82 | if (addChildMenuItem(items[i].children, parentKey, newMenu)) { 83 | return true; 84 | } 85 | } 86 | } 87 | return false; 88 | }; 89 | 90 | export default [ 91 | // 获取菜单列表(返回完整的树形结构) 92 | { 93 | url: '/menu/list', 94 | method: 'get', 95 | response: () => { 96 | return { 97 | code: 20000, 98 | data: mockMenus, 99 | msg: '获取菜单列表成功', 100 | request_id: Date.now().toString(), 101 | }; 102 | }, 103 | }, 104 | 105 | // 添加菜单 106 | { 107 | url: '/menu/add', 108 | method: 'post', 109 | response: ({ body }) => { 110 | const newMenu = body as MenuItem; 111 | // 如果有parentKey,添加到对应的父菜单下 112 | if (newMenu.parentKey) { 113 | if (addChildMenuItem(mockMenus, newMenu.parentKey, newMenu)) { 114 | return { 115 | code: 20000, 116 | data: newMenu, 117 | msg: '添加菜单成功', 118 | request_id: Date.now().toString(), 119 | }; 120 | } 121 | return { 122 | code: 40004, 123 | msg: '父菜单不存在', 124 | request_id: Date.now().toString(), 125 | }; 126 | } 127 | // 如果没有parentKey,添加到根级别 128 | mockMenus.push(newMenu); 129 | return { 130 | code: 20000, 131 | data: newMenu, 132 | msg: '添加菜单成功', 133 | request_id: Date.now().toString(), 134 | }; 135 | }, 136 | }, 137 | 138 | // 更新菜单 139 | { 140 | url: '/menu/update/:key', 141 | method: 'put', 142 | response: ({ body }) => { 143 | const updatedMenu = body as MenuItem; 144 | if (updateMenuItem(mockMenus, updatedMenu.key, updatedMenu)) { 145 | return { 146 | code: 20000, 147 | data: updatedMenu, 148 | msg: '更新菜单成功', 149 | request_id: Date.now().toString(), 150 | }; 151 | } 152 | return { 153 | code: 40004, 154 | msg: '菜单不存在', 155 | request_id: Date.now().toString(), 156 | }; 157 | }, 158 | }, 159 | 160 | // 删除菜单 161 | { 162 | url: '/menu/delete/:key', 163 | method: 'delete', 164 | response: ({ query }) => { 165 | const { key } = query; 166 | if (deleteMenuItem(mockMenus, key)) { 167 | return { 168 | code: 20000, 169 | data: null, 170 | msg: '删除菜单成功', 171 | request_id: Date.now().toString(), 172 | }; 173 | } 174 | return { 175 | code: 40004, 176 | msg: '菜单不存在', 177 | request_id: Date.now().toString(), 178 | }; 179 | }, 180 | }, 181 | ] as MockMethod[]; -------------------------------------------------------------------------------- /dandelion-admin-ui/mock/user.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | import { mockMenus } from './menu'; 3 | 4 | const tokens = { 5 | admin: 'admin-token', 6 | editor: 'editor-token', 7 | }; 8 | 9 | const users = { 10 | 'admin-token': { 11 | id: '1', 12 | username: 'admin', 13 | nickname: '管理员', 14 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', 15 | roles: ['admin'], 16 | permissions: ['*'], 17 | }, 18 | 'editor-token': { 19 | id: '2', 20 | username: 'editor', 21 | nickname: '编辑者', 22 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', 23 | roles: ['editor'], 24 | permissions: ['view', 'edit'], 25 | }, 26 | }; 27 | 28 | export default [ 29 | // 登录 30 | { 31 | url: '/auth/login', 32 | method: 'post', 33 | response: ({ body }) => { 34 | const { username, password } = body; 35 | const token = tokens[username]; 36 | 37 | if (!token) { 38 | return { 39 | code: 40001, 40 | msg: '用户名或密码错误', 41 | request_id: Date.now().toString(), 42 | }; 43 | } 44 | 45 | return { 46 | code: 20000, 47 | data: { 48 | token, 49 | }, 50 | msg: '登录成功', 51 | request_id: Date.now().toString(), 52 | }; 53 | }, 54 | }, 55 | 56 | // 获取用户信息 57 | { 58 | url: '/user/info', 59 | method: 'get', 60 | response: ({ headers }) => { 61 | const token = headers.authorization?.replace('Bearer ', ''); 62 | const info = users[token]; 63 | 64 | if (!info) { 65 | return { 66 | code: 40001, 67 | msg: '获取用户信息失败', 68 | request_id: Date.now().toString(), 69 | }; 70 | } 71 | 72 | return { 73 | code: 20000, 74 | data: info, 75 | msg: '获取用户信息成功', 76 | request_id: Date.now().toString(), 77 | }; 78 | }, 79 | }, 80 | 81 | // 获取用户菜单 82 | { 83 | url: '/user/menus', 84 | method: 'get', 85 | response: ({ headers }) => { 86 | const token = headers.authorization?.replace('Bearer ', ''); 87 | const info = users[token]; 88 | 89 | if (!info) { 90 | return { 91 | code: 40001, 92 | msg: '获取菜单失败', 93 | request_id: Date.now().toString(), 94 | }; 95 | } 96 | 97 | // admin返回所有菜单,editor返回部分菜单 98 | const menus = info.roles.includes('admin') 99 | ? mockMenus 100 | : mockMenus.filter(item => item.key !== 'system'); 101 | 102 | return { 103 | code: 20000, 104 | data: menus, 105 | msg: '获取菜单成功', 106 | request_id: Date.now().toString(), 107 | }; 108 | }, 109 | }, 110 | 111 | // 退出登录 112 | { 113 | url: '/auth/logout', 114 | method: 'post', 115 | response: () => { 116 | return { 117 | code: 20000, 118 | data: null, 119 | msg: '退出成功', 120 | request_id: Date.now().toString(), 121 | }; 122 | }, 123 | }, 124 | ] as MockMethod[]; -------------------------------------------------------------------------------- /dandelion-admin-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dandelion-admin-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^5.5.2", 14 | "@types/mockjs": "^1.0.10", 15 | "@types/node": "^22.10.6", 16 | "antd": "^5.23.1", 17 | "axios": "^1.7.9", 18 | "mockjs": "^1.1.0", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "react-router-dom": "^7.1.1", 22 | "vite-plugin-mock": "^3.0.2", 23 | "zustand": "^5.0.3" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.17.0", 27 | "@types/react": "^18.3.18", 28 | "@types/react-dom": "^18.3.5", 29 | "@vitejs/plugin-react": "^4.3.4", 30 | "eslint": "^9.17.0", 31 | "eslint-plugin-react-hooks": "^5.0.0", 32 | "eslint-plugin-react-refresh": "^0.4.16", 33 | "globals": "^15.14.0", 34 | "typescript": "~5.6.2", 35 | "typescript-eslint": "^8.18.2", 36 | "vite": "^6.0.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dandelion-admin-ui/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dandelion-admin-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Layout } from 'antd'; 2 | import { GithubOutlined } from '@ant-design/icons'; 3 | 4 | const { Header, Content, Footer } = Layout; 5 | 6 | function App() { 7 | return ( 8 | 9 |
10 |

Dandelion Admin

11 |
12 | 13 |
14 | 17 |
18 |
19 |
20 | Dandelion Admin ©{new Date().getFullYear()} Created with Ant Design 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /dandelion-admin-ui/src/api/menu.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import type { ApiResponse } from '@/types/api'; 3 | import type { MenuItem } from '@/types/menu'; 4 | 5 | interface MenuItemResponse { 6 | id: number; 7 | parent_id: number; 8 | name: string; 9 | path: string; 10 | type: number; 11 | icon?: string; 12 | sort: number; 13 | status: number; 14 | children: MenuItemResponse[]; 15 | } 16 | 17 | interface MenuListResponse { 18 | list: MenuItemResponse[]; 19 | } 20 | 21 | interface CreateMenuRequest { 22 | parent_id: number; 23 | name: string; 24 | path: string; 25 | type: number; 26 | icon?: string; 27 | sort: number; 28 | status: number; 29 | } 30 | 31 | interface UpdateMenuRequest extends CreateMenuRequest { 32 | id: number; 33 | } 34 | 35 | interface SortMenuItem { 36 | id: number; 37 | sequence: number; 38 | parent_id: number; 39 | } 40 | 41 | // 将后端菜单数据转换为前端格式 42 | const convertMenuItem = (item: MenuItemResponse): MenuItem => ({ 43 | id: item.id, 44 | parent_id: item.parent_id, 45 | name: item.name, 46 | path: item.path, 47 | type: item.type, 48 | icon: item.icon, 49 | sort: item.sort, 50 | status: item.status, 51 | children: item.children?.map(convertMenuItem) || [], 52 | }); 53 | 54 | // 将前端菜单数据转换为创建请求格式 55 | const convertToCreateRequest = (item: MenuItem): CreateMenuRequest => ({ 56 | parent_id: item.parent_id, 57 | name: item.name, 58 | path: item.path || '', 59 | type: item.type, 60 | icon: item.icon, 61 | sort: item.sort, 62 | status: item.status, 63 | }); 64 | 65 | // 将前端菜单数据转换为更新请求格式 66 | const convertToUpdateRequest = (item: MenuItem): UpdateMenuRequest => ({ 67 | id: item.id, 68 | ...convertToCreateRequest(item), 69 | }); 70 | 71 | // 获取菜单列表 72 | export const getMenuList = async () => { 73 | const response = await request.get>('/api/sys_menu/search'); 74 | return { 75 | data: { 76 | list: response.data?.list?.map(convertMenuItem) || [], 77 | } 78 | }; 79 | }; 80 | 81 | // 添加菜单 82 | export const addMenu = async (menu: MenuItem) => { 83 | const response = await request.post>('/api/sys_menu/create', convertToCreateRequest(menu)); 84 | return { 85 | data: convertMenuItem(response.data) 86 | }; 87 | }; 88 | 89 | // 更新菜单 90 | export const updateMenu = async (menu: MenuItem) => { 91 | const response = await request.put>(`/api/sys_menu/update`, convertToUpdateRequest(menu)); 92 | return { 93 | data: convertMenuItem(response.data) 94 | }; 95 | }; 96 | 97 | // 删除菜单 98 | export const deleteMenu = async (menu: MenuItem) => { 99 | await request.delete>('/api/sys_menu/delete', { id: menu.id }); 100 | }; 101 | 102 | // 更新菜单排序 103 | export const sortMenus = async (items: SortMenuItem[]) => { 104 | await request.put>('/api/sys_menu/sort', { list: items }); 105 | }; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/api/role.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import type { ApiResponse, RoleSearchParams, RoleListResponse, CreateRoleParams, UpdateRoleParams } from './types'; 3 | 4 | // 获取角色列表 5 | export const getRoleList = async (params: RoleSearchParams) => { 6 | const response = await request.post>('/api/sys_role/search', params); 7 | return response.data; 8 | }; 9 | 10 | // 创建角色 11 | export const createRole = async (params: CreateRoleParams) => { 12 | const response = await request.post>('/api/sys_role/create', params); 13 | return response.data; 14 | }; 15 | 16 | // 更新角色 17 | export const updateRole = async (params: UpdateRoleParams) => { 18 | const response = await request.put>('/api/sys_role/update', params); 19 | return response.data; 20 | }; 21 | 22 | // 删除角色 23 | export const deleteRole = async (id: number) => { 24 | const response = await request.delete>(`/api/sys_role/delete`,{id}); 25 | return response.data; 26 | }; 27 | 28 | // 分配用户 29 | export const assignUsers = async (params: { role_id: number; user_ids: number[] }) => { 30 | const response = await request.post>('/api/sys_role/assign_users', params); 31 | return response.data; 32 | }; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/api/types.ts: -------------------------------------------------------------------------------- 1 | // 用户相关接口类型 2 | export interface LoginParams { 3 | username: string; 4 | password: string; 5 | } 6 | 7 | export interface UserInfo { 8 | id: number; 9 | user_name: string; 10 | nick_name: string; 11 | avatar?: string; 12 | phone?: string; 13 | status: number; 14 | } 15 | 16 | export interface UserInfoResponse { 17 | user_info: UserInfo; 18 | } 19 | 20 | // 菜单类型定义 21 | export enum MenuType { 22 | MENU = 0, // 菜单 23 | PAGE = 1, // 页面 24 | TAB = 2, // 标签页 25 | BUTTON = 3, // 按钮 26 | } 27 | 28 | // 后端菜单项接口 29 | export interface MenuItemResponse { 30 | id: number; 31 | parent_id: number; 32 | name: string; 33 | path: string; 34 | type: MenuType; 35 | icon?: string; 36 | sort?: number; 37 | status: number; 38 | children?: MenuItemResponse[]; 39 | } 40 | 41 | // 前端菜单项接口 42 | export interface MenuItem { 43 | key: string; // 菜单标识 44 | label: string; // 菜单名称 45 | icon?: string; // 图标(仅 MENU、PAGE 类型需要) 46 | type: MenuType; // 菜单类型 47 | children?: MenuItem[]; // 子菜单 48 | parentKey?: string; // 父级菜单标识 49 | code?: string; // 权限代码(仅 TAB、BUTTON 类型需要,格式:模块_操作,如:USER_VIEW) 50 | routeCode?: string; // 路由代码(仅 MENU、PAGE 类型需要,如:/system/user) 51 | sort?: number; // 排序 52 | hidden?: boolean; // 是否在菜单中隐藏(仅 MENU、PAGE 类型可用) 53 | status: number; // 状态 54 | } 55 | 56 | // 角色相关接口类型 57 | export interface Role { 58 | id: string; 59 | name: string; 60 | code: string; 61 | description?: string; 62 | permissions: string[]; // 权限代码列表 63 | status: number; 64 | createTime: string; 65 | updateTime: string; 66 | } 67 | 68 | // 通用分页参数 69 | export interface PaginationParams { 70 | current: number; 71 | pageSize: number; 72 | } 73 | 74 | // 通用分页响应 75 | export interface PaginationResult { 76 | list: T[]; 77 | total: number; 78 | current: number; 79 | pageSize: number; 80 | } 81 | 82 | // 通用响应格式 83 | export interface ApiResponse { 84 | code: number; 85 | msg: string; 86 | data: T; 87 | requestId: string; 88 | } 89 | 90 | // 列表响应格式 91 | export interface ListResponse { 92 | list: T[]; 93 | } 94 | 95 | // 用户查询参数 96 | export interface UserSearchParams { 97 | page: number; 98 | page_size: number; 99 | phone?: string; 100 | status?: number; 101 | user_name?: string; 102 | } 103 | 104 | // 用户列表项 105 | export interface UserListItem { 106 | id: number; 107 | user_name: string; 108 | nick_name: string; 109 | avatar: string; 110 | phone: string; 111 | status: number; 112 | created_at: number; 113 | updated_at: number; 114 | } 115 | 116 | // 用户列表响应 117 | export interface UserListResponse { 118 | list: UserListItem[]; 119 | total: number; 120 | } 121 | 122 | // 用户状态枚举 123 | export enum UserStatus { 124 | NORMAL = 1, // 正常 125 | DISABLED = 2 // 禁用 126 | } 127 | 128 | // 创建用户参数 129 | export interface CreateUserParams { 130 | user_name: string; 131 | nick_name: string; 132 | password: string; 133 | phone: string; 134 | avatar?: string; 135 | status: UserStatus; 136 | } 137 | 138 | // 更新用户参数 139 | export interface UpdateUserParams { 140 | id: number; 141 | nick_name: string; 142 | phone: string; 143 | avatar?: string; 144 | status: UserStatus; 145 | } 146 | 147 | // 角色查询参数 148 | export interface RoleSearchParams { 149 | page: number; 150 | page_size: number; 151 | name?: string; 152 | status?: number; 153 | } 154 | 155 | // 角色列表项 156 | export interface RoleListItem { 157 | id: number; 158 | name: string; 159 | description: string; 160 | menu_ids: number[]; 161 | user_ids: number[]; 162 | status: number; 163 | created_at: number; 164 | updated_at: number; 165 | } 166 | 167 | // 角色列表响应 168 | export interface RoleListResponse { 169 | list: RoleListItem[]; 170 | total: number; 171 | } 172 | 173 | // 创建角色参数 174 | export interface CreateRoleParams { 175 | name: string; 176 | description: string; 177 | menu_ids: number[]; 178 | status: number; 179 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import type { ApiResponse, LoginParams, UserInfoResponse, UserSearchParams, UserListResponse, CreateUserParams, UpdateUserParams } from './types'; 3 | import type { MenuItem, MenuResponse } from '@/types/menu'; 4 | 5 | // 登录 6 | export const login = async (params: LoginParams) => { 7 | const response = await request.post>('/api/login', params); 8 | return response.data; 9 | }; 10 | 11 | // 获取用户信息 12 | export const getUserInfo = async () => { 13 | const response = await request.get>('/api/user/info'); 14 | return response.data.user_info; 15 | }; 16 | 17 | // 获取用户菜单 18 | export const getUserMenus = async (): Promise => { 19 | const response = await request.get>('/api/user/menu'); 20 | console.log('response', response); 21 | return response.data?.list || []; 22 | }; 23 | 24 | // 退出登录 25 | export const logout = async () => { 26 | await request.post>('/api/authorize/logout'); 27 | }; 28 | 29 | // 获取用户列表 30 | export const getUserList = async (params: UserSearchParams) => { 31 | const response = await request.post>('/api/sys_user/search', params); 32 | return response.data; 33 | }; 34 | 35 | // 创建用户 36 | export const createUser = async (params: CreateUserParams) => { 37 | const response = await request.post>('/api/sys_user/create', params); 38 | return response.data; 39 | }; 40 | 41 | // 更新用户 42 | export const updateUser = async (params: UpdateUserParams) => { 43 | const response = await request.put>('/api/sys_user/update', params); 44 | return response.data; 45 | }; 46 | 47 | // 删除用户 48 | export const deleteUser = async (id: number) => { 49 | const response = await request.delete>(`/api/sys_user/delete`, { id }); 50 | return response.data; 51 | }; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dandelion-admin-ui/src/components/AuthGuard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import { useAuth } from '@/hooks/useAuth'; 4 | 5 | interface AuthGuardProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const AuthGuard: React.FC = ({ children }) => { 10 | const { loading, isAuthenticated } = useAuth(); 11 | 12 | if (!isAuthenticated && loading) { 13 | return ( 14 |
20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | return <>{children}; 28 | }; 29 | 30 | export default AuthGuard; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/components/IconSelect/index.css: -------------------------------------------------------------------------------- 1 | .icon-select-container { 2 | max-height: 300px; 3 | overflow-y: auto; 4 | padding: 8px; 5 | width: 320px; 6 | } 7 | 8 | .icon-radio-group { 9 | width: 100%; 10 | } 11 | 12 | .icon-radio-button { 13 | width: 32px; 14 | height: 32px; 15 | padding: 4px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | border-radius: 4px !important; 20 | margin: 0 !important; 21 | border: 1px solid #d9d9d9 !important; 22 | } 23 | 24 | .icon-radio-button:hover { 25 | color: #1890ff; 26 | border-color: #1890ff !important; 27 | } 28 | 29 | .icon-radio-button:first-child, 30 | .icon-radio-button:last-child { 31 | border-radius: 4px !important; 32 | } 33 | 34 | .icon-radio-button.ant-radio-button-wrapper-checked { 35 | background-color: #e6f4ff !important; 36 | border-color: #1890ff !important; 37 | color: #1890ff !important; 38 | } 39 | 40 | .icon-select-trigger { 41 | width: 100%; 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | } 46 | 47 | .icon-select-trigger .anticon { 48 | font-size: 16px; 49 | } 50 | 51 | /* 自定义滚动条样式 */ 52 | .icon-select-container::-webkit-scrollbar { 53 | width: 6px; 54 | height: 6px; 55 | } 56 | 57 | .icon-select-container::-webkit-scrollbar-thumb { 58 | background: #ccc; 59 | border-radius: 3px; 60 | } 61 | 62 | .icon-select-container::-webkit-scrollbar-track { 63 | background: #f1f1f1; 64 | border-radius: 3px; 65 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/components/IconSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'antd'; 2 | import * as Icons from '@ant-design/icons'; 3 | import React from 'react'; 4 | import './index.css'; 5 | 6 | interface IconSelectProps { 7 | value?: string; 8 | onChange?: (value: string) => void; 9 | } 10 | 11 | const IconSelect: React.FC = ({ value, onChange }) => { 12 | // 获取所有图标 13 | const iconList = Object.keys(Icons).filter(key => key.endsWith('Outlined')); 14 | 15 | const handleChange = (newValue: string) => { 16 | onChange?.(newValue); 17 | }; 18 | 19 | return ( 20 | 37 | ); 38 | }; 39 | 40 | export default IconSelect; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, useRef } from 'react'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { useUserStore } from '@/store/user'; 4 | 5 | const PUBLIC_PATHS = ['/login']; 6 | 7 | export const useAuth = () => { 8 | const navigate = useNavigate(); 9 | const location = useLocation(); 10 | const { token, userInfo, loading, fetchUserInfo } = useUserStore(); 11 | const isPublicPath = PUBLIC_PATHS.includes(location.pathname); 12 | const authChecked = useRef(false); 13 | 14 | const checkAuth = useCallback(async () => { 15 | // 如果已经检查过认证,且有用户信息,则不再重复检查 16 | if (authChecked.current && userInfo) { 17 | return; 18 | } 19 | 20 | // 如果在公共路径上 21 | if (isPublicPath) { 22 | // 已登录则跳转到首页 23 | if (token) { 24 | navigate('/', { replace: true }); 25 | } 26 | return; 27 | } 28 | 29 | // 非公共路径,未登录则跳转到登录页 30 | if (!token) { 31 | navigate('/login', { replace: true, state: { from: location.pathname } }); 32 | return; 33 | } 34 | 35 | // 已登录但没有用户信息,则获取用户信息 36 | if (token && !userInfo && !authChecked.current) { 37 | try { 38 | await fetchUserInfo(); 39 | authChecked.current = true; 40 | } catch { 41 | // 获取用户信息失败,可能是token过期,跳转到登录页 42 | navigate('/login', { replace: true, state: { from: location.pathname } }); 43 | } 44 | } 45 | }, [token, userInfo, location.pathname, navigate, fetchUserInfo, isPublicPath]); 46 | 47 | useEffect(() => { 48 | checkAuth(); 49 | }, [checkAuth]); 50 | 51 | return { 52 | loading: loading && !isPublicPath && !authChecked.current, 53 | isAuthenticated: !!token && !!userInfo, 54 | isPublicPath, 55 | }; 56 | }; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'antd/dist/reset.css'; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 11 | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 12 | 'Noto Color Emoji'; 13 | } 14 | -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/BasicLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { Layout } from 'antd'; 3 | import { Outlet, useLocation } from 'react-router-dom'; 4 | import Sider from './components/Sider'; 5 | import Header from './components/Header'; 6 | import TabView from './components/TabView'; 7 | import { useTabStore } from '@/store/tab'; 8 | import { useMenuStore } from '@/store/menu'; 9 | import type { MenuItem } from '@/types/menu'; 10 | 11 | const { Content } = Layout; 12 | 13 | const BasicLayout = () => { 14 | const [collapsed, setCollapsed] = useState(false); 15 | const location = useLocation(); 16 | const { addTab } = useTabStore(); 17 | const { menus } = useMenuStore(); 18 | 19 | // 递归查找菜单项 20 | const findMenuItem = useCallback((items: MenuItem[], path: string): MenuItem | null => { 21 | for (const item of items) { 22 | if (item.path === path) { 23 | return item; 24 | } 25 | if (item.children?.length) { 26 | const found = findMenuItem(item.children, path); 27 | if (found) { 28 | return found; 29 | } 30 | } 31 | } 32 | return null; 33 | }, []); 34 | 35 | // 监听路由变化,添加标签页 36 | useEffect(() => { 37 | const path = location.pathname; 38 | 39 | // 如果是根路径,不添加标签页 40 | if (path === '/') { 41 | return; 42 | } 43 | 44 | // 从菜单数据中获取标题 45 | const menuItem = findMenuItem(menus, path); 46 | 47 | // 只有在菜单中存在的路径才添加标签页 48 | if (menuItem) { 49 | addTab({ 50 | key: path, 51 | label: menuItem.name, 52 | path, 53 | closable: true, 54 | }); 55 | } 56 | }, [location.pathname, addTab, findMenuItem, menus]); 57 | 58 | return ( 59 | 60 | 61 | 62 |
setCollapsed(value)} 65 | /> 66 | 67 | 68 |
69 | 70 |
71 |
72 | 73 | 74 | ); 75 | }; 76 | 77 | export default BasicLayout; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/components/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb as AntBreadcrumb } from 'antd'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { menuItems } from './menuConfig.tsx'; 4 | 5 | // 扁平化菜单配置,方便查找 6 | const flattenMenuItems = (items: any[], parent: any[] = []): any[] => { 7 | return items.reduce((acc, item) => { 8 | const path = [...parent, item]; 9 | if (item.children) { 10 | return [...acc, { ...item, parent }, ...flattenMenuItems(item.children, path)]; 11 | } 12 | return [...acc, { ...item, parent }]; 13 | }, []); 14 | }; 15 | 16 | const getBreadcrumbItems = (pathname: string) => { 17 | const paths = pathname.split('/').filter(Boolean); 18 | const currentKey = paths[paths.length - 1] || 'dashboard'; 19 | 20 | const flatMenu = flattenMenuItems(menuItems); 21 | const currentItem = flatMenu.find(item => item.key === currentKey); 22 | 23 | if (!currentItem) return [{ title: '仪表盘' }]; 24 | 25 | const breadcrumbItems = currentItem.parent 26 | .filter((item: any) => item.label) 27 | .map((item: any) => ({ 28 | title: item.label, 29 | })); 30 | 31 | return [...breadcrumbItems, { title: currentItem.label }]; 32 | }; 33 | 34 | const Breadcrumb = () => { 35 | const location = useLocation(); 36 | const items = getBreadcrumbItems(location.pathname); 37 | 38 | return ( 39 | 45 | ); 46 | }; 47 | 48 | export default Breadcrumb; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Button, Space, Dropdown, Avatar } from 'antd'; 2 | import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons'; 3 | import { useUserStore } from '@/store/user'; 4 | import Breadcrumb from './Breadcrumb'; 5 | 6 | interface HeaderProps { 7 | collapsed: boolean; 8 | onCollapse: (collapsed: boolean) => void; 9 | } 10 | 11 | const Header = ({ collapsed, onCollapse }: HeaderProps) => { 12 | const { userInfo, logout } = useUserStore(); 13 | 14 | const userMenuItems = [ 15 | { 16 | key: 'profile', 17 | label: '个人信息', 18 | }, 19 | { 20 | key: 'settings', 21 | label: '设置', 22 | }, 23 | { 24 | type: 'divider', 25 | }, 26 | { 27 | key: 'logout', 28 | label: '退出登录', 29 | onClick: logout, 30 | }, 31 | ]; 32 | 33 | return ( 34 | 48 |
49 |
61 | 62 | 63 | 64 | } 67 | size={32} 68 | style={{ 69 | backgroundColor: userInfo?.avatar ? 'transparent' : '#1890ff' 70 | }} 71 | /> 72 | {userInfo?.nickname || 'Admin'} 73 | 74 | 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default Header; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/components/Sider.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Menu, Spin } from 'antd'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { useEffect, useMemo } from 'react'; 4 | import * as Icons from '@ant-design/icons'; 5 | import { useMenuStore } from '@/store/menu'; 6 | import type { MenuItem } from '@/types/menu'; 7 | 8 | interface SiderProps { 9 | collapsed: boolean; 10 | } 11 | 12 | // 动态创建图标组件 13 | const IconComponent = (iconName: string) => { 14 | const Icon = Icons[`${iconName}Outlined` as keyof typeof Icons]; 15 | return Icon ? : null; 16 | }; 17 | 18 | // 递归处理菜单数据 19 | const processMenuItems = (items: MenuItem[] = []) => { 20 | return items.map(item => ({ 21 | key: item.path, 22 | icon: item.icon ? IconComponent(item.icon) : null, 23 | label: item.name, 24 | children: item.children?.length ? processMenuItems(item.children) : undefined, 25 | })); 26 | }; 27 | 28 | const Sider = ({ collapsed }: SiderProps) => { 29 | const navigate = useNavigate(); 30 | const location = useLocation(); 31 | const { menus, loading, fetchMenus } = useMenuStore(); 32 | const selectedKey = location.pathname; 33 | 34 | useEffect(() => { 35 | // 只在组件挂载时获取一次菜单数据 36 | fetchMenus(); 37 | }, []); // 移除 fetchMenus 依赖 38 | 39 | // 使用 useMemo 缓存处理后的菜单数据 40 | const processedMenus = useMemo(() => processMenuItems(menus), [menus]); 41 | 42 | return ( 43 | 59 |
69 | {collapsed ? 'D' : 'Dandelion'} 70 |
71 | {loading ? ( 72 |
73 | 74 |
75 | ) : ( 76 | navigate(key)} 83 | style={{ 84 | borderRight: 0, 85 | }} 86 | className="custom-menu" 87 | /> 88 | )} 89 | 114 | 115 | ); 116 | }; 117 | 118 | export default Sider; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/components/TabView.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useTabStore, TabItem } from '@/store/tab'; 4 | 5 | const TabView = () => { 6 | const navigate = useNavigate(); 7 | const { tabs, activeKey, removeTab, setActiveKey } = useTabStore(); 8 | 9 | const onChange = (key: string) => { 10 | const tab = tabs.find(item => item.key === key); 11 | if (tab) { 12 | setActiveKey(key); 13 | navigate(tab.path); 14 | } 15 | }; 16 | 17 | const onEdit = (targetKey: any, action: 'add' | 'remove') => { 18 | if (action === 'remove') { 19 | const tab = tabs.find(item => item.key === targetKey); 20 | if (tab) { 21 | removeTab(targetKey); 22 | // 获取新的活动标签 23 | const newActiveTab = tabs.find(item => item.key === activeKey); 24 | if (newActiveTab) { 25 | navigate(newActiveTab.path); 26 | } 27 | } 28 | } 29 | }; 30 | 31 | return ( 32 | ({ 39 | key: tab.key, 40 | label: tab.label, 41 | closable: tab.closable, 42 | }))} 43 | style={{ 44 | background: '#fff', 45 | padding: '6px 8px 0', 46 | marginBottom: 0, 47 | }} 48 | /> 49 | ); 50 | }; 51 | 52 | export default TabView; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/layouts/components/menuConfig.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HomeOutlined, 3 | UserOutlined, 4 | SettingOutlined, 5 | ShoppingCartOutlined, 6 | FileTextOutlined, 7 | TeamOutlined, 8 | AppstoreOutlined, 9 | SafetyCertificateOutlined 10 | } from '@ant-design/icons'; 11 | 12 | export const menuItems = [ 13 | { 14 | key: 'dashboard', 15 | icon: , 16 | label: '仪表盘', 17 | }, 18 | { 19 | key: 'system', 20 | icon: , 21 | label: '系统管理', 22 | children: [ 23 | { 24 | key: 'user', 25 | icon: , 26 | label: '用户管理', 27 | }, 28 | { 29 | key: 'role', 30 | icon: , 31 | label: '角色管理', 32 | }, 33 | { 34 | key: 'menu', 35 | icon: , 36 | label: '菜单管理', 37 | } 38 | ] 39 | }, 40 | { 41 | key: 'business', 42 | icon: , 43 | label: '业务管理', 44 | children: [ 45 | { 46 | key: 'order', 47 | icon: , 48 | label: '订单管理', 49 | children: [ 50 | { 51 | key: 'order-list', 52 | label: '订单列表' 53 | }, 54 | { 55 | key: 'order-review', 56 | label: '订单审核' 57 | } 58 | ] 59 | }, 60 | { 61 | key: 'customer', 62 | icon: , 63 | label: '客户管理', 64 | children: [ 65 | { 66 | key: 'customer-list', 67 | label: '客户列表' 68 | }, 69 | { 70 | key: 'customer-group', 71 | label: '客户分组' 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ]; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { RouterProvider } from 'react-router-dom'; 3 | import router from './router'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Row, Col, Statistic } from 'antd'; 2 | import { UserOutlined, ShoppingCartOutlined } from '@ant-design/icons'; 3 | 4 | const Dashboard = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 | } 14 | /> 15 | 16 | 17 | 18 | 19 | } 23 | /> 24 | 25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default Dashboard; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/error/403.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const Forbidden = () => { 5 | const navigate = useNavigate(); 6 | 7 | return ( 8 |
14 | navigate('/')}> 20 | 返回首页 21 | , 22 | , 25 | ]} 26 | /> 27 |
28 | ); 29 | }; 30 | 31 | export default Forbidden; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/error/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const NotFound = () => { 5 | const navigate = useNavigate(); 6 | 7 | return ( 8 |
14 | navigate('/')}> 20 | 返回首页 21 | , 22 | , 25 | ]} 26 | /> 27 |
28 | ); 29 | }; 30 | 31 | export default NotFound; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/login/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | background: #f0f2f5; 7 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 8 | background-repeat: no-repeat; 9 | background-position: center 110px; 10 | background-size: 100%; 11 | } 12 | 13 | .card { 14 | width: 368px; 15 | margin-bottom: 100px; 16 | } 17 | 18 | .title { 19 | text-align: center; 20 | margin-bottom: 40px; 21 | color: rgba(0, 0, 0, 0.85); 22 | font-weight: bold; 23 | font-size: 33px; 24 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Button, Card } from 'antd'; 2 | import { UserOutlined, LockOutlined } from '@ant-design/icons'; 3 | import { useUserStore } from '@/store/user'; 4 | import type { LoginParams } from '@/api/types'; 5 | import styles from './index.module.css'; 6 | 7 | const Login = () => { 8 | const { login, loading } = useUserStore(); 9 | 10 | const onFinish = async (values: LoginParams) => { 11 | await login(values); 12 | }; 13 | 14 | return ( 15 |
16 | 17 |

Dandelion Admin

18 |
23 | 27 | } 29 | placeholder="用户名: admin" 30 | size="large" 31 | /> 32 | 33 | 37 | } 39 | placeholder="密码: admin" 40 | size="large" 41 | /> 42 | 43 | 44 | 53 | 54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default Login; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/menu/components/MenuForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form, Input, Modal, Select, InputNumber, Row, Col, TreeSelect, Space } from 'antd'; 3 | import type { TreeSelectProps } from 'antd/es/tree-select'; 4 | import * as Icons from '@ant-design/icons'; 5 | import IconSelect from '@/components/IconSelect'; 6 | import type { MenuItem } from '@/types/menu'; 7 | 8 | interface MenuFormProps { 9 | open: boolean; 10 | onCancel: () => void; 11 | onOk: (values: any) => void; 12 | menuList: MenuItem[]; 13 | editingMenu?: MenuItem; 14 | } 15 | 16 | const MenuForm: React.FC = ({ 17 | open, 18 | onCancel, 19 | onOk, 20 | menuList = [], 21 | editingMenu, 22 | }) => { 23 | const [form] = Form.useForm(); 24 | const [menuType, setMenuType] = useState(0); 25 | 26 | const handleOk = async () => { 27 | try { 28 | const values = await form.validateFields(); 29 | onOk(values); 30 | form.resetFields(); 31 | } catch (error) { 32 | console.error('Validate Failed:', error); 33 | } 34 | }; 35 | 36 | const handleCancel = () => { 37 | form.resetFields(); 38 | onCancel(); 39 | }; 40 | 41 | const handleTypeChange = (value: number) => { 42 | setMenuType(value); 43 | // 清空不需要的字段 44 | if (value === 3) { // 按钮类型 45 | form.setFieldsValue({ 46 | icon: undefined, 47 | path: undefined, 48 | }); 49 | } 50 | }; 51 | 52 | const renderIcon = (iconName: string) => { 53 | const IconComponent = Icons[`${iconName}Outlined` as keyof typeof Icons] as React.ComponentType; 54 | return IconComponent ? React.createElement(IconComponent) : null; 55 | }; 56 | 57 | const getMenuTypeTag = (type: number) => { 58 | const typeConfig = { 59 | 0: { text: 'D', className: 'menu-type-dir' }, // Directory 60 | 1: { text: 'M', className: 'menu-type-menu' }, // Menu 61 | 2: { text: 'T', className: 'menu-type-tab' }, // Tab 62 | 3: { text: 'B', className: 'menu-type-button' }, // Button 63 | }[type] || { text: '-', className: '' }; 64 | 65 | return ( 66 | 67 | {typeConfig.text} 68 | 69 | ); 70 | }; 71 | 72 | const convertToTreeData = (menus: MenuItem[]): TreeSelectProps['treeData'] => { 73 | return menus.map(menu => ({ 74 | value: menu.id, 75 | title: ( 76 | 77 | {getMenuTypeTag(menu.type)} 78 | {menu.icon && menu.type !== 3 && renderIcon(menu.icon)} 79 | {menu.name} 80 | 81 | ), 82 | children: menu.children?.length ? convertToTreeData(menu.children) : undefined, 83 | })); 84 | }; 85 | 86 | return ( 87 | 94 |
105 | 106 | 107 | 111 | 118 | 119 | 120 | 121 | 126 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 146 | 147 | 148 | 149 | 150 | 155 | 156 | 157 | 158 | 159 | 160 | {menuType !== 3 && ( 161 | 162 | 163 | 172 | 173 | 174 | 175 | 176 | 181 | 182 | 183 | 184 | 185 | )} 186 | 187 | 188 | 189 | 194 | 198 | 199 | 200 | 201 |
202 |
203 | ); 204 | }; 205 | 206 | export default MenuForm; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/menu/components/index.css: -------------------------------------------------------------------------------- 1 | .ant-form-item { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .ant-modal-body { 6 | padding: 24px 24px 8px; 7 | } 8 | 9 | .ant-form-item-label { 10 | padding-bottom: 4px; 11 | } 12 | 13 | /* 调整图标选择器的样式 */ 14 | .ant-form-item .icon-select-trigger { 15 | height: 32px; 16 | } 17 | 18 | /* 权限配置样式 */ 19 | .permissions-header { 20 | display: flex; 21 | justify-content: flex-end; 22 | margin-bottom: 16px; 23 | } 24 | 25 | .permission-card { 26 | background-color: #fafafa; 27 | padding: 16px; 28 | border-radius: 4px; 29 | margin-bottom: 16px; 30 | border: 1px solid #f0f0f0; 31 | } 32 | 33 | .permission-card:hover { 34 | border-color: #1890ff; 35 | } 36 | 37 | .permission-card .ant-form-item { 38 | margin-bottom: 8px; 39 | } 40 | 41 | .dynamic-delete-button { 42 | color: #ff4d4f; 43 | cursor: pointer; 44 | font-size: 16px; 45 | transition: all 0.3s; 46 | } 47 | 48 | .dynamic-delete-button:hover { 49 | color: #ff7875; 50 | } 51 | 52 | /* 分割线样式 */ 53 | .ant-divider-with-text-left { 54 | margin: 16px 0; 55 | color: rgba(0, 0, 0, 0.85); 56 | font-weight: 500; 57 | font-size: 14px; 58 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/menu/index.css: -------------------------------------------------------------------------------- 1 | .menu-layout { 2 | height: 100%; 3 | background: #fff; 4 | } 5 | 6 | .menu-sider { 7 | border-right: 1px solid #f0f0f0; 8 | background: #fff; 9 | } 10 | 11 | .menu-sider-header { 12 | padding: 16px; 13 | border-bottom: 1px solid #f0f0f0; 14 | } 15 | 16 | .menu-tree { 17 | padding: 16px; 18 | } 19 | 20 | .menu-content { 21 | padding: 24px; 22 | } 23 | 24 | .menu-detail { 25 | display: flex; 26 | flex-direction: column; 27 | gap: 16px; 28 | } 29 | 30 | .menu-detail-item { 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | .menu-detail-item label { 36 | width: 120px; 37 | color: rgba(0, 0, 0, 0.45); 38 | } 39 | 40 | .menu-detail-item span { 41 | color: rgba(0, 0, 0, 0.85); 42 | } 43 | 44 | /* 树节点样式 */ 45 | .ant-tree-node-content-wrapper { 46 | display: flex; 47 | align-items: center; 48 | height: 32px; 49 | } 50 | 51 | .ant-tree-node-content-wrapper .ant-space { 52 | gap: 8px; 53 | } 54 | 55 | /* 卡片样式 */ 56 | .ant-card-head { 57 | border-bottom: 1px solid #f0f0f0; 58 | min-height: 48px; 59 | } 60 | 61 | .ant-card-head-title { 62 | padding: 12px 0; 63 | } 64 | 65 | .ant-card-extra { 66 | padding: 8px 0; 67 | } 68 | 69 | .menu-type-tag { 70 | display: inline-flex; 71 | align-items: center; 72 | justify-content: center; 73 | width: 20px; 74 | height: 20px; 75 | border-radius: 10px; 76 | font-size: 12px; 77 | font-weight: bold; 78 | margin-right: 8px; 79 | } 80 | 81 | .menu-type-dir { 82 | background-color: #e6f7ff; 83 | color: #1890ff; 84 | border: 1px solid #91d5ff; 85 | } 86 | 87 | .menu-type-menu { 88 | background-color: #f6ffed; 89 | color: #52c41a; 90 | border: 1px solid #b7eb8f; 91 | } 92 | 93 | .menu-type-tab { 94 | background-color: #f9f0ff; 95 | color: #722ed1; 96 | border: 1px solid #d3adf7; 97 | } 98 | 99 | .menu-type-button { 100 | background-color: #fff7e6; 101 | color: #fa8c16; 102 | border: 1px solid #ffd591; 103 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/system/role/components/AssignUsersForm.module.css: -------------------------------------------------------------------------------- 1 | .userTransfer { 2 | :global { 3 | /* 调整搜索框样式 */ 4 | .ant-transfer-list-search { 5 | padding: 8px 12px; 6 | border-bottom: 1px solid #f0f0f0; 7 | } 8 | .ant-transfer-list-search-action { 9 | right: 16px; 10 | top: 14px; 11 | color: rgba(0, 0, 0, 0.25); 12 | } 13 | .ant-input-affix-wrapper { 14 | padding: 4px 8px; 15 | } 16 | .ant-input-prefix { 17 | color: rgba(0, 0, 0, 0.25); 18 | margin-right: 4px; 19 | } 20 | 21 | /* 调整标题样式 */ 22 | .ant-transfer-list-header { 23 | padding: 12px; 24 | background: #fafafa; 25 | } 26 | .ant-transfer-list-header-title { 27 | font-size: 14px; 28 | color: rgba(0, 0, 0, 0.88); 29 | font-weight: 500; 30 | } 31 | .ant-transfer-list-header-selected { 32 | color: rgba(0, 0, 0, 0.45); 33 | font-size: 12px; 34 | } 35 | 36 | /* 调整列表样式 */ 37 | .ant-transfer-list { 38 | border-radius: 8px; 39 | border-color: #e5e6eb; 40 | width: 280px !important; 41 | } 42 | .ant-transfer-list-content { 43 | padding: 0; 44 | } 45 | .ant-transfer-list-content-item { 46 | padding: 8px 12px; 47 | min-height: 40px; 48 | display: flex; 49 | align-items: center; 50 | margin: 0; 51 | border-bottom: 1px solid #f0f0f0; 52 | border-radius: 0; 53 | transition: all 0.3s; 54 | } 55 | .ant-transfer-list-content-item:last-child { 56 | border-bottom: none; 57 | } 58 | .ant-transfer-list-content-item:hover { 59 | background-color: #f5f5f5; 60 | } 61 | .ant-transfer-list-content-item-checked { 62 | background-color: #e6f4ff; 63 | } 64 | .ant-transfer-list-content-item-text { 65 | flex: 1; 66 | margin-left: 8px; 67 | color: rgba(0, 0, 0, 0.88); 68 | } 69 | 70 | /* 调整分页样式 */ 71 | .ant-transfer-list-pagination { 72 | margin: 0; 73 | padding: 8px; 74 | border-top: 1px solid #f0f0f0; 75 | } 76 | .ant-pagination-simple .ant-pagination-simple-pager { 77 | margin: 0 4px; 78 | } 79 | 80 | /* 调整操作按钮样式 */ 81 | .ant-transfer-operation { 82 | display: flex; 83 | flex-direction: column; 84 | gap: 8px; 85 | margin: 0; 86 | padding: 8px; 87 | width: 32px; 88 | } 89 | .ant-transfer-operation .ant-btn { 90 | width: 32px; 91 | height: 32px; 92 | min-width: 32px; 93 | padding: 0; 94 | border: 1px solid #d9d9d9; 95 | border-radius: 4px; 96 | display: flex; 97 | align-items: center; 98 | justify-content: center; 99 | background: #fff; 100 | color: rgba(0, 0, 0, 0.45); 101 | transition: all 0.3s; 102 | margin: 0; 103 | } 104 | .ant-transfer-operation .ant-btn:hover { 105 | color: #1677ff; 106 | border-color: #1677ff; 107 | background: #fff; 108 | } 109 | .ant-transfer-operation .ant-btn:active { 110 | color: #0958d9; 111 | border-color: #0958d9; 112 | background: #fff; 113 | } 114 | .ant-transfer-operation .ant-btn[disabled] { 115 | background: #f5f5f5; 116 | border-color: #d9d9d9; 117 | color: rgba(0, 0, 0, 0.25); 118 | cursor: not-allowed; 119 | } 120 | .ant-transfer-operation .anticon { 121 | font-size: 12px; 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/system/role/components/AssignUsersForm.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Form, Transfer } from 'antd'; 3 | import type { FormInstance } from 'antd/es/form'; 4 | import type { RoleListItem } from '@/api/types'; 5 | import type { TransferItem } from 'antd/es/transfer'; 6 | import { SearchOutlined, RightOutlined, LeftOutlined } from '@ant-design/icons'; 7 | import styles from './AssignUsersForm.module.css'; 8 | 9 | interface AssignUsersFormProps { 10 | form: FormInstance; 11 | initialValues: RoleListItem; 12 | userList: TransferItem[]; 13 | loading?: boolean; 14 | } 15 | 16 | const AssignUsersForm: React.FC = ({ 17 | form, 18 | initialValues, 19 | userList, 20 | loading 21 | }) => { 22 | const [targetKeys, setTargetKeys] = useState([]); 23 | 24 | // 处理穿梭框值变化 25 | const handleChange = (newTargetKeys: string[]) => { 26 | setTargetKeys(newTargetKeys); 27 | form.setFieldValue('user_ids', newTargetKeys.map(Number)); 28 | }; 29 | 30 | // 初始化已选择的用户 31 | useEffect(() => { 32 | const selectedKeys = initialValues.user_ids?.map(String) || []; 33 | setTargetKeys(selectedKeys); 34 | form.setFieldValue('user_ids', initialValues.user_ids || []); 35 | }, [initialValues]); 36 | 37 | return ( 38 |
39 | 43 | item.title as string} 48 | listStyle={{ 49 | height: 380, 50 | }} 51 | loading={loading} 52 | showSearch 53 | searchPlaceholder="搜索用户" 54 | notFoundContent="暂无数据" 55 | selectionsIcon={false} 56 | targetKeys={targetKeys} 57 | onChange={handleChange} 58 | filterOption={(inputValue, item) => 59 | (item.title as string).toLowerCase().indexOf(inputValue.toLowerCase()) !== -1 || 60 | (item.description as string).toLowerCase().indexOf(inputValue.toLowerCase()) !== -1 61 | } 62 | oneWay={false} 63 | pagination={{ 64 | pageSize: 10, 65 | simple: true, 66 | showSizeChanger: false, 67 | showLessItems: true 68 | }} 69 | showSelectAll={false} 70 | operations={[ 71 | , 72 | 73 | ]} 74 | /> 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default AssignUsersForm; -------------------------------------------------------------------------------- /dandelion-admin-ui/src/pages/system/role/components/CreateRoleForm.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Form, Input, Select, TreeSelect, Spin } from 'antd'; 3 | import type { FormInstance } from 'antd/es/form'; 4 | import type { DefaultOptionType } from 'antd/es/select'; 5 | import { UserStatus } from '@/api/types'; 6 | import { getMenuList } from '@/api/menu'; 7 | import type { MenuItemResponse } from '@/api/menu'; 8 | 9 | interface CreateRoleFormProps { 10 | form: FormInstance; 11 | } 12 | 13 | const CreateRoleForm: React.FC = ({ form }) => { 14 | const [menuLoading, setMenuLoading] = useState(false); 15 | const [menuTree, setMenuTree] = useState([]); 16 | const [menuMap, setMenuMap] = useState>(new Map()); 17 | 18 | const statusOptions = [ 19 | { label: '正常', value: UserStatus.NORMAL }, 20 | { label: '禁用', value: UserStatus.DISABLED } 21 | ]; 22 | 23 | // 构建菜单ID映射 24 | const buildMenuMap = (menuList: MenuItemResponse[]) => { 25 | const map = new Map(); 26 | const traverse = (list: MenuItemResponse[]) => { 27 | list.forEach(menu => { 28 | map.set(menu.id, menu); 29 | if (menu.children?.length) { 30 | traverse(menu.children); 31 | } 32 | }); 33 | }; 34 | traverse(menuList); 35 | return map; 36 | }; 37 | 38 | // 将菜单列表转换为TreeSelect所需的数据结构 39 | const convertToTreeData = (menuList: MenuItemResponse[]): DefaultOptionType[] => { 40 | return menuList.map(menu => ({ 41 | title: menu.name, 42 | value: menu.id, 43 | key: menu.id, 44 | disabled: menu.status === 2, 45 | children: menu.children ? convertToTreeData(menu.children) : undefined, 46 | label: ( 47 | 48 | {menu.icon && } 49 | {menu.name} 50 | 51 | ({menu.type === 0 ? '菜单' : 52 | menu.type === 1 ? '页面' : 53 | menu.type === 2 ? '标签页' : '按钮'}) 54 | 55 | 56 | ) 57 | })); 58 | }; 59 | 60 | // 获取所有父节点ID 61 | const getParentIds = (menuId: number): number[] => { 62 | const parentIds: number[] = []; 63 | let currentMenu = menuMap.get(menuId); 64 | 65 | while (currentMenu && currentMenu.parent_id !== 0) { 66 | parentIds.push(currentMenu.parent_id); 67 | currentMenu = menuMap.get(currentMenu.parent_id); 68 | } 69 | 70 | return parentIds; 71 | }; 72 | 73 | // 处理菜单选择变化 74 | const handleMenuChange = (value: number[]) => { 75 | // 获取所有选中节点的父节点ID 76 | const parentIds = value.reduce((acc: number[], menuId) => { 77 | const parents = getParentIds(menuId); 78 | return [...acc, ...parents]; 79 | }, []); 80 | 81 | // 合并选中的ID和父节点ID,去重 82 | const allSelectedIds = Array.from(new Set([...value, ...parentIds])); 83 | 84 | form.setFieldValue('menu_ids', allSelectedIds); 85 | }; 86 | 87 | // 加载菜单数据 88 | const loadMenuData = async () => { 89 | setMenuLoading(true); 90 | try { 91 | const { data } = await getMenuList(); 92 | const map = buildMenuMap(data.list); 93 | setMenuMap(map); 94 | setMenuTree(convertToTreeData(data.list)); 95 | } finally { 96 | setMenuLoading(false); 97 | } 98 | }; 99 | 100 | useEffect(() => { 101 | loadMenuData(); 102 | }, []); 103 | 104 | return ( 105 |
109 | 114 | 115 | 116 | 117 | 122 | 126 | 127 | 128 | 133 | 147 | 148 | 149 | 154 | 121 | 122 | 123 | 128 | 132 | 133 | 134 | 139 | 153 | 154 | 155 | 159 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 68 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 53 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | )} 43 | 44 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 75 |