public:发布2.8.6版本 (#2126)
Some checks failed
CI / init (push) Has been cancelled
CI / Frontend node 18.16.0 (push) Has been cancelled
CI / Backend go (1.22) (push) Has been cancelled
CI / release-pr (push) Has been cancelled
CI / devops-test (1.22, 18.16.0) (push) Has been cancelled
CI / release-please (push) Has been cancelled
CI / devops-prod (1.22, 18.x) (push) Has been cancelled
CI / docker (push) Has been cancelled

* feat(mcp): 新增gva_review工具并优化字典和代码生成逻辑

* fix: 调整mcp整体逻辑

* chore: 更新.gitignore,添加对本地配置文件的忽略

* feat(logo): 新增Logo组件并在多个页面中替换原有logo实现

* fix: 修复菜单 Logo 部分删除文本后显示异常的问题

* fix:添加字典列表搜索,支持中英文搜索.添加字典详情搜索

* style: 优化部分视觉样式

* feat: 增强错误预览组件的暗黑模式支持

* feat: 优化请求错误消息获取逻辑,增加状态文本优先级

* feat: 添加前端登录验证码静态验证逻辑

* feat: 添加开发环境启动脚本

* feat: 更新 SvgIcon 组件,支持本地图标和 Iconify 图标、移除未使用的 unocss 依赖

* fix:字典支持 tree 结构

* feat: 优化动态路由注册方式

* feat: 添加配置控制标签页keep-alive功能

* feat: 添加全局错误处理机制,捕获 Vue 和 JS 错误

* refactor: 移除API和菜单创建结果中的权限分配提醒,优化输出信息

* feat: 更新 reset.scss,优化全局样式重置,增强兼容性和可读性

* refactor(字典详情): 优化字典详情查询逻辑,移除预加载改为按需加载

* refactor(路由管理): 优化路由添加逻辑,增强路径处理和顶级路由注册

* refactor(系统配置): 将auto-migrate修改为disable-auto-migrate,保证用户升级的兼容性

* feat(utils): 优化字典数据递归查找功能并替换select为tree-select

* fix(deps): 修复在字段类型为file生成搜索条件无法运行的bug

* fix: 修复header的tools中icon不展示的问题

---------

Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
Co-authored-by: Azir-11 <2075125282@qq.com>
Co-authored-by: bypanghu <bypanghu@163.com>
Co-authored-by: feitianbubu <feitianbubu@qq.com>
Co-authored-by: 青菜白玉汤 <79054161+Azir-11@users.noreply.github.com>
Co-authored-by: krank <emosick@qq.com>
This commit is contained in:
PiexlMax(奇淼
2025-10-19 13:27:48 +08:00
committed by GitHub
parent b01b27f1eb
commit 7d3e7b5b7a
64 changed files with 3940 additions and 3584 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ rm_file/
/server/server
/server/latest_log
/server/__debug_bin*
/server/*.local.yaml
server/uploads/
*.iml

View File

@@ -4,6 +4,7 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
@@ -116,10 +117,17 @@ func (s *DictionaryApi) FindSysDictionary(c *gin.Context) {
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.SysDictionarySearch true "字典 name 或者 type"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量"
// @Router /sysDictionary/getSysDictionaryList [get]
func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
list, err := dictionaryService.GetSysDictionaryInfoList()
var dictionary request.SysDictionarySearch
err := c.ShouldBindQuery(&dictionary)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)

View File

@@ -1,6 +1,8 @@
package system
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
@@ -146,3 +148,120 @@ func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) {
PageSize: pageInfo.PageSize,
}, "获取成功", c)
}
// GetDictionaryTreeList
// @Tags SysDictionaryDetail
// @Summary 获取字典详情树形结构
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param sysDictionaryID query int true "字典ID"
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
// @Router /sysDictionaryDetail/getDictionaryTreeList [get]
func (s *DictionaryDetailApi) GetDictionaryTreeList(c *gin.Context) {
sysDictionaryID := c.Query("sysDictionaryID")
if sysDictionaryID == "" {
response.FailWithMessage("字典ID不能为空", c)
return
}
var id uint
if idUint64, err := strconv.ParseUint(sysDictionaryID, 10, 32); err != nil {
response.FailWithMessage("字典ID格式错误", c)
return
} else {
id = uint(idUint64)
}
list, err := dictionaryDetailService.GetDictionaryTreeList(id)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
}
// GetDictionaryTreeListByType
// @Tags SysDictionaryDetail
// @Summary 根据字典类型获取字典详情树形结构
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param type query string true "字典类型"
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get]
func (s *DictionaryDetailApi) GetDictionaryTreeListByType(c *gin.Context) {
dictType := c.Query("type")
if dictType == "" {
response.FailWithMessage("字典类型不能为空", c)
return
}
list, err := dictionaryDetailService.GetDictionaryTreeListByType(dictType)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
}
// GetDictionaryDetailsByParent
// @Tags SysDictionaryDetail
// @Summary 根据父级ID获取字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.GetDictionaryDetailsByParentRequest true "查询参数"
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情列表"
// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get]
func (s *DictionaryDetailApi) GetDictionaryDetailsByParent(c *gin.Context) {
var req request.GetDictionaryDetailsByParentRequest
err := c.ShouldBindQuery(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, err := dictionaryDetailService.GetDictionaryDetailsByParent(req)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
}
// GetDictionaryPath
// @Tags SysDictionaryDetail
// @Summary 获取字典详情的完整路径
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param id query uint true "字典详情ID"
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情路径"
// @Router /sysDictionaryDetail/getDictionaryPath [get]
func (s *DictionaryDetailApi) GetDictionaryPath(c *gin.Context) {
idStr := c.Query("id")
if idStr == "" {
response.FailWithMessage("字典详情ID不能为空", c)
return
}
var id uint
if idUint64, err := strconv.ParseUint(idStr, 10, 32); err != nil {
response.FailWithMessage("字典详情ID格式错误", c)
return
} else {
id = uint(idUint64)
}
path, err := dictionaryDetailService.GetDictionaryPath(id)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(gin.H{"path": path}, "获取成功", c)
}

View File

@@ -280,4 +280,6 @@ mcp:
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''
url_prefix: ''
addr: 8889
separate: false

View File

@@ -87,6 +87,8 @@ system:
router-prefix: ""
# 严格角色模式 打开后权限将会存在上下级关系
use-strict-auth: false
# 自动迁移数据库表结构生产环境建议设为false手动迁移
disable-auto-migrate: false
# captcha configuration
captcha:
@@ -268,7 +270,6 @@ cors:
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
allow-methods: POST, GET
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
- allow-origin: example2.com
allow-headers: content-type
@@ -280,4 +281,6 @@ mcp:
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''
url_prefix: ''
addr: 8889
separate: false

View File

@@ -6,4 +6,6 @@ type MCP struct {
SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径
MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径
UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 独立MCP服务端口
Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` // 是否独立运行MCP服务
}

View File

@@ -11,4 +11,5 @@ type System struct {
UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis
UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo
UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式
DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构生产环境建议设为false手动迁移
}

View File

@@ -4,9 +4,9 @@ package global
// 目前只有Version正式使用 其余为预留
const (
// Version 当前版本号
Version = "v2.8.5"
Version = "v2.8.6"
// AppName 应用名称
AppName = "Gin-Vue-Admin"
// Description 应用描述
Description = "使用gin+vue进行极速开发的全栈开发基础平台"
)
)

View File

@@ -20,7 +20,7 @@ require (
github.com/gookit/color v1.5.4
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/mark3labs/mcp-go v0.31.0
github.com/mark3labs/mcp-go v0.41.1
github.com/mholt/archives v0.1.1
github.com/minio/minio-go/v7 v7.0.84
github.com/mojocn/base64Captcha v1.3.8
@@ -61,10 +61,12 @@ require (
github.com/STARRY-S/zip v0.2.1 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
@@ -99,6 +101,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // indirect
@@ -153,6 +156,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect

View File

@@ -56,6 +56,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ=
github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -69,6 +71,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -247,6 +251,8 @@ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -315,8 +321,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@@ -473,6 +479,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=

View File

@@ -35,6 +35,11 @@ func Gorm() *gorm.DB {
}
func RegisterTables() {
if global.GVA_CONFIG.System.DisableAutoMigrate {
global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration")
return
}
db := global.GVA_DB
err := db.AutoMigrate(

View File

@@ -40,16 +40,19 @@ func Routers() *gin.Engine {
Router.Use(gin.Logger())
}
sseServer := McpRun()
if !global.GVA_CONFIG.MCP.Separate {
// 注册mcp服务
Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) {
sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request)
})
sseServer := McpRun()
Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) {
sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request)
})
// 注册mcp服务
Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) {
sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request)
})
Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) {
sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request)
})
}
systemRouter := router.RouterGroupApp.System
exampleRouter := router.RouterGroupApp.Example

View File

@@ -21,7 +21,7 @@ import (
// @Tag.Description 用户
// @title Gin-Vue-Admin Swagger API接口文档
// @version v2.8.5
// @version v2.8.6
// @description 使用gin+vue进行极速开发的全栈开发基础平台
// @securityDefinitions.apikey ApiKeyAuth
// @in header

View File

@@ -180,21 +180,11 @@ func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
// 添加权限分配提醒
permissionReminder := "\n\n⚠ 重要提醒:\n" +
"API创建完成后请前往【系统管理】->【角色管理】中为相关角色分配新创建的API权限" +
"以确保用户能够正常访问新接口。\n" +
"具体步骤:\n" +
"1. 进入角色管理页面\n" +
"2. 选择需要授权的角色\n" +
"3. 在API权限中勾选新创建的API接口\n" +
"4. 保存权限配置"
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("API创建结果\n\n%s%s", string(resultJSON), permissionReminder),
Text: fmt.Sprintf("API创建结果\n\n%s", string(resultJSON)),
},
},
}, nil

View File

@@ -21,6 +21,13 @@ func init() {
// DictionaryOptionsGenerator 字典选项生成器
type DictionaryOptionsGenerator struct{}
// DictionaryOption 字典选项结构
type DictionaryOption struct {
Label string `json:"label"`
Value string `json:"value"`
Sort int `json:"sort"`
}
// DictionaryGenerateRequest 字典生成请求
type DictionaryGenerateRequest struct {
DictType string `json:"dictType"` // 字典类型
@@ -139,11 +146,11 @@ func (d *DictionaryOptionsGenerator) InputSchema() map[string]interface{} {
},
"dictName": map[string]interface{}{
"type": "string",
"description": "字典名称,可选默认根据fieldDesc生成",
"description": "字典名称,必填默认根据fieldDesc生成",
},
"description": map[string]interface{}{
"type": "string",
"description": "字典描述,可选",
"description": "字典描述,必填",
},
},
"required": []string{"dictType", "fieldDesc", "options"},
@@ -180,7 +187,6 @@ func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.Cal
return nil, errors.New("options 不能为空")
}
// 可选参数
dictName, _ := args["dictName"].(string)
description, _ := args["description"].(string)

View File

@@ -18,13 +18,18 @@ func init() {
RegisterTool(&DictionaryQuery{})
}
type DictionaryPre struct {
Type string `json:"type"` // 字典名(英)
Desc string `json:"desc"` // 描述
}
// DictionaryInfo 字典信息结构
type DictionaryInfo struct {
ID uint `json:"id"`
Name string `json:"name"` // 字典名(中)
Type string `json:"type"` // 字典名(英)
Status *bool `json:"status"` // 状态
Desc string `json:"desc"` // 描述
ID uint `json:"id"`
Name string `json:"name"` // 字典名(中)
Type string `json:"type"` // 字典名(英)
Status *bool `json:"status"` // 状态
Desc string `json:"desc"` // 描述
Details []DictionaryDetailInfo `json:"details"` // 字典详情
}
@@ -40,9 +45,9 @@ type DictionaryDetailInfo struct {
// DictionaryQueryResponse 字典查询响应结构
type DictionaryQueryResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Total int `json:"total"`
Success bool `json:"success"`
Message string `json:"message"`
Total int `json:"total"`
Dictionaries []DictionaryInfo `json:"dictionaries"`
}
@@ -68,36 +73,36 @@ func (d *DictionaryQuery) New() mcp.Tool {
// Handle 处理字典查询请求
func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
// 获取参数
dictType := ""
if val, ok := args["dictType"].(string); ok {
dictType = val
}
includeDisabled := false
if val, ok := args["includeDisabled"].(bool); ok {
includeDisabled = val
}
detailsOnly := false
if val, ok := args["detailsOnly"].(bool); ok {
detailsOnly = val
}
// 获取字典服务
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
var dictionaries []DictionaryInfo
var err error
if dictType != "" {
// 查询指定类型的字典
var status *bool
if !includeDisabled {
status = &[]bool{true}[0]
}
sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status)
if err != nil {
global.GVA_LOG.Error("查询字典失败", zap.Error(err))
@@ -107,7 +112,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
},
}, nil
}
// 转换为响应格式
dictInfo := DictionaryInfo{
ID: sysDictionary.ID,
@@ -116,7 +121,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
Status: sysDictionary.Status,
Desc: sysDictionary.Desc,
}
// 获取字典详情
for _, detail := range sysDictionary.SysDictionaryDetails {
if includeDisabled || (detail.Status != nil && *detail.Status) {
@@ -130,17 +135,17 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
})
}
}
dictionaries = append(dictionaries, dictInfo)
} else {
// 查询所有字典
var sysDictionaries []system.SysDictionary
db := global.GVA_DB.Model(&system.SysDictionary{})
if !includeDisabled {
db = db.Where("status = ?", true)
}
err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
if includeDisabled {
return db.Order("sort")
@@ -148,7 +153,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
return db.Where("status = ?", true).Order("sort")
}
}).Find(&sysDictionaries).Error
if err != nil {
global.GVA_LOG.Error("查询字典列表失败", zap.Error(err))
return &mcp.CallToolResult{
@@ -157,7 +162,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
},
}, nil
}
// 转换为响应格式
for _, dict := range sysDictionaries {
dictInfo := DictionaryInfo{
@@ -167,7 +172,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
Status: dict.Status,
Desc: dict.Desc,
}
// 获取字典详情
for _, detail := range dict.SysDictionaryDetails {
if includeDisabled || (detail.Status != nil && *detail.Status) {
@@ -181,25 +186,25 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
})
}
}
dictionaries = append(dictionaries, dictInfo)
}
}
// 如果只需要详情信息,则提取所有详情
if detailsOnly {
var allDetails []DictionaryDetailInfo
for _, dict := range dictionaries {
allDetails = append(allDetails, dict.Details...)
}
response := map[string]interface{}{
"success": true,
"message": "查询字典详情成功",
"total": len(allDetails),
"details": allDetails,
}
responseJSON, _ := json.Marshal(response)
return &mcp.CallToolResult{
Content: []mcp.Content{
@@ -207,7 +212,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
},
}, nil
}
// 构建响应
response := DictionaryQueryResponse{
Success: true,
@@ -215,7 +220,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
Total: len(dictionaries),
Dictionaries: dictionaries,
}
responseJSON, err := json.Marshal(response)
if err != nil {
global.GVA_LOG.Error("序列化响应失败", zap.Error(err))
@@ -225,10 +230,10 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques
},
}, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseJSON)),
},
}, nil
}
}

View File

@@ -1,529 +0,0 @@
# ExecutionPlan 结构体格式说明
## 概述
ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。
## 完整结构体定义
```go
type ExecutionPlan struct {
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包
NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息当NeedCreatedPackage=true时必需
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组当NeedCreatedModules=true时必需支持批量创建
Paths map[string]string `json:"paths,omitempty"` // 路径信息
}
```
## 子结构体详细说明
### 1. SysAutoCodePackageCreate 结构体
```go
type SysAutoCodePackageCreate struct {
Desc string `json:"desc"` // 描述,如:"用户管理模块"
Label string `json:"label"` // 展示名,如:"用户管理"
Template string `json:"template"` // 模板类型:"plugin" 或 "package"
PackageName string `json:"packageName"` // 包名,如:"user"
Module string `json:"-"` // 模块名(自动填充,无需设置)
}
```
### 2. AutoCode 结构体(核心字段)
```go
type AutoCode struct {
Package string `json:"package"` // 包名
TableName string `json:"tableName"` // 数据库表名
BusinessDB string `json:"businessDB"` // 业务数据库名
StructName string `json:"structName"` // 结构体名称
PackageName string `json:"packageName"` // 文件名称
Description string `json:"description"` // 结构体中文名称
Abbreviation string `json:"abbreviation"` // 结构体简称
HumpPackageName string `json:"humpPackageName"` // 驼峰命名的包名
GvaModel bool `json:"gvaModel"` // 是否使用GVA默认Model
AutoMigrate bool `json:"autoMigrate"` // 是否自动迁移表结构
AutoCreateResource bool `json:"autoCreateResource"` // 是否自动创建资源标识
AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 是否自动创建API
AutoCreateMenuToSql bool `json:"autoCreateMenuToSql"` // 是否自动创建菜单
AutoCreateBtnAuth bool `json:"autoCreateBtnAuth"` // 是否自动创建按钮权限
OnlyTemplate bool `json:"onlyTemplate"` // 是否只生成模板
IsTree bool `json:"isTree"` // 是否树形结构
TreeJson string `json:"treeJson"` // 树形结构JSON字段
IsAdd bool `json:"isAdd"` // 是否新增
Fields []*AutoCodeField `json:"fields"` // 字段列表
GenerateWeb bool `json:"generateWeb"` // 是否生成前端代码
GenerateServer bool `json:"generateServer"` // 是否生成后端代码
Module string `json:"-"` // 模块(自动填充)
DictTypes []string `json:"-"` // 字典类型(自动填充)
}
```
### 3. AutoCodeField 结构体(字段定义)
```go
type AutoCodeField struct {
FieldName string `json:"fieldName"` // 字段名
FieldDesc string `json:"fieldDesc"` // 字段中文描述
FieldType string `json:"fieldType"` // 字段类型string, int, bool, time.Time等
FieldJson string `json:"fieldJson"` // JSON标签名
DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度
Comment string `json:"comment"` // 数据库字段注释
ColumnName string `json:"columnName"` // 数据库列名
FieldSearchType string `json:"fieldSearchType"` // 搜索类型EQ, LIKE, BETWEEN等
FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件
DictType string `json:"dictType"` // 字典类型
Form bool `json:"form"` // 是否在表单中显示
Table bool `json:"table"` // 是否在表格中显示
Desc bool `json:"desc"` // 是否在详情中显示
Excel bool `json:"excel"` // 是否支持导入导出
Require bool `json:"require"` // 是否必填
DefaultValue string `json:"defaultValue"` // 默认值
ErrorText string `json:"errorText"` // 校验失败提示
Clearable bool `json:"clearable"` // 是否可清空
Sort bool `json:"sort"` // 是否支持排序
PrimaryKey bool `json:"primaryKey"` // 是否主键
DataSource *DataSource `json:"dataSource"` // 数据源配置(用于关联其他表)
CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源
FieldIndexType string `json:"fieldIndexType"` // 索引类型
}
```
### 4. DataSource 结构体(关联表配置)
```go
type DataSource struct {
DBName string `json:"dbName"` // 关联的数据库名称
Table string `json:"table"` // 关联的表名
Label string `json:"label"` // 用于显示的字段名如name、title等
Value string `json:"value"` // 用于存储的值字段名通常是id
Association int `json:"association"` // 关联关系1=一对一2=一对多
HasDeletedAt bool `json:"hasDeletedAt"` // 关联表是否有软删除字段
}
```
## 使用示例
### 示例1创建新包和批量创建多个模块
```json
{
"packageName": "user",
"packageType": "package",
"needCreatedPackage": true,
"needCreatedModules": true,
"packageInfo": {
"desc": "用户管理模块",
"label": "用户管理",
"template": "package",
"packageName": "user"
},
"modulesInfo": [
{
"package": "user",
"tableName": "sys_users",
"businessDB": "",
"structName": "User",
"packageName": "user",
"description": "用户",
"abbreviation": "user",
"humpPackageName": "user",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"treeJson": "",
"isAdd": true,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "Username",
"fieldDesc": "用户名",
"fieldType": "string",
"fieldJson": "username",
"dataTypeLong": "50",
"comment": "用户名",
"columnName": "username",
"fieldSearchType": "LIKE",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请输入用户名",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": {
"dbName": "gva",
"table": "sys_users",
"label": "username",
"value": "id",
"association": 2,
"hasDeletedAt": true
},
"checkDataSource": true,
"fieldIndexType": ""
},
{
"fieldName": "Email",
"fieldDesc": "邮箱",
"fieldType": "string",
"fieldJson": "email",
"dataTypeLong": "100",
"comment": "邮箱地址",
"columnName": "email",
"fieldSearchType": "EQ",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请输入邮箱",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": null,
"checkDataSource": false,
"fieldIndexType": "index"
}
]
},
{
"package": "user",
"tableName": "user_profiles",
"businessDB": "",
"structName": "UserProfile",
"packageName": "user",
"description": "用户档案",
"abbreviation": "userProfile",
"humpPackageName": "user",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"treeJson": "",
"isAdd": true,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "UserID",
"fieldDesc": "用户ID",
"fieldType": "int",
"fieldJson": "userId",
"dataTypeLong": "",
"comment": "关联用户ID",
"columnName": "user_id",
"fieldSearchType": "EQ",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请选择用户",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": null,
"checkDataSource": false,
"fieldIndexType": "index"
},
{
"fieldName": "Avatar",
"fieldDesc": "头像",
"fieldType": "string",
"fieldJson": "avatar",
"dataTypeLong": "255",
"comment": "用户头像URL",
"columnName": "avatar",
"fieldSearchType": "",
"fieldSearchHide": true,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": false,
"require": false,
"defaultValue": "",
"errorText": "",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": null,
"checkDataSource": false,
"fieldIndexType": ""
}
]
}
]
}
```
### 示例2仅在现有包中批量创建多个模块
```json
{
"packageName": "system",
"packageType": "package",
"needCreatedPackage": false,
"needCreatedModules": true,
"packageInfo": null,
"modulesInfo": [
{
"package": "system",
"tableName": "sys_roles",
"businessDB": "",
"structName": "Role",
"packageName": "system",
"description": "角色",
"abbreviation": "role",
"humpPackageName": "system",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "RoleName",
"fieldDesc": "角色名称",
"fieldType": "string",
"fieldJson": "roleName",
"dataTypeLong": "50",
"comment": "角色名称",
"columnName": "role_name",
"fieldSearchType": "LIKE",
"form": true,
"table": true,
"desc": true,
"require": true
}
]
},
{
"package": "system",
"tableName": "sys_permissions",
"businessDB": "",
"structName": "Permission",
"packageName": "system",
"description": "权限",
"abbreviation": "permission",
"humpPackageName": "system",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "PermissionName",
"fieldDesc": "权限名称",
"fieldType": "string",
"fieldJson": "permissionName",
"dataTypeLong": "100",
"comment": "权限名称",
"columnName": "permission_name",
"fieldSearchType": "LIKE",
"form": true,
"table": true,
"desc": true,
"require": true
},
{
"fieldName": "PermissionCode",
"fieldDesc": "权限代码",
"fieldType": "string",
"fieldJson": "permissionCode",
"dataTypeLong": "50",
"comment": "权限代码",
"columnName": "permission_code",
"fieldSearchType": "=",
"form": true,
"table": true,
"desc": true,
"require": true
}
]
}
]
}
```
### 示例3模块关联关系配置详解
以下示例展示了如何配置不同类型的关联关系:
```json
{
"packageName": "order",
"packageType": "package",
"needCreatedPackage": true,
"needCreatedModules": true,
"packageInfo": {
"desc": "订单管理模块",
"label": "订单管理",
"template": "package",
"packageName": "order"
},
"modulesInfo": [
{
"package": "order",
"tableName": "orders",
"structName": "Order",
"packageName": "order",
"description": "订单",
"abbreviation": "order",
"humpPackageName": "order",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "UserID",
"fieldDesc": "下单用户",
"fieldType": "uint",
"fieldJson": "userId",
"columnName": "user_id",
"fieldSearchType": "EQ",
"form": true,
"table": true,
"desc": true,
"require": true,
"dataSource": {
"dbName": "gva",
"table": "sys_users",
"label": "username",
"value": "id",
"association": 2,
"hasDeletedAt": true
},
"checkDataSource": true
},
{
"fieldName": "ProductID",
"fieldDesc": "商品",
"fieldType": "uint",
"fieldJson": "productId",
"columnName": "product_id",
"fieldSearchType": "EQ",
"form": true,
"table": true,
"desc": true,
"require": true,
"dataSource": {
"dbName": "gva",
"table": "products",
"label": "name",
"value": "id",
"association": 2,
"hasDeletedAt": false
},
"checkDataSource": true
},
{
"fieldName": "Status",
"fieldDesc": "订单状态",
"fieldType": "int",
"fieldJson": "status",
"columnName": "status",
"fieldSearchType": "EQ",
"form": true,
"table": true,
"desc": true,
"require": true,
"dictType": "order_status"
}
]
}
]
}
```
## DataSource 配置说明
### 关联关系类型
- **association: 1** - 一对一关联(如用户与用户档案)
- **association: 2** - 一对多关联(如用户与订单)
### 配置要点
1. **dbName**: 通常为 "gva"(默认数据库)
2. **table**: 关联表的实际表名
3. **label**: 用于前端显示的字段(如用户名、商品名称)
4. **value**: 用于存储关联ID的字段通常是 "id"
5. **hasDeletedAt**: 关联表是否支持软删除
6. **checkDataSource**: 建议设为true会验证关联表是否存在
### 常见关联场景
- 用户关联:`{"table": "sys_users", "label": "username", "value": "id"}`
- 角色关联:`{"table": "sys_authorities", "label": "authorityName", "value": "authorityId"}`
- 部门关联:`{"table": "sys_departments", "label": "name", "value": "id"}`
- 分类关联:`{"table": "categories", "label": "name", "value": "id"}`
## 重要注意事项
1. **PackageType**: 只能是 "plugin" 或 "package"
2. **NeedCreatedPackage**: 当为true时PackageInfo必须提供
3. **NeedCreatedModules**: 当为true时ModulesInfo必须提供
4. **字段类型**: FieldType支持的类型包括
- string字符串
- richtext富文本
- int整型
- bool布尔值
- float64浮点型
- time.Time时间
- enum枚举
- picture单图片字符串
- pictures多图片json字符串
- video视频字符串
- file文件json字符串
- jsonJSON
- array数组
5. **搜索类型**: FieldSearchType支持EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等
6. **索引类型**: FieldIndexType支持index, unique等
7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段
8. **关联配置**: 使用dataSource时确保关联表已存在建议开启checkDataSource验证
## 常见错误避免
1. 确保PackageName和ModuleName符合Go语言命名规范
2. 字段名使用大写开头的驼峰命名
3. JSON标签使用小写开头的驼峰命名
4. 数据库列名使用下划线分隔的小写命名
5. 必填字段不要遗漏
6. 字段类型要与实际需求匹配

View File

@@ -1,205 +0,0 @@
# GAG工具使用示例 - 带用户确认流程
## 新的工作流程
现在GAG工具支持三步工作流程
1. `analyze` - 分析现有模块信息
2. `confirm` - 请求用户确认创建计划
3. `execute` - 执行创建操作(需要用户确认)
## 使用示例
### 第一步:分析
```json
{
"action": "analyze",
"requirement": "创建一个图书管理功能"
}
```
### 第二步:确认(支持批量创建多个模块)
```json
{
"action": "confirm",
"executionPlan": {
"packageName": "library",
"packageType": "package",
"needCreatedPackage": true,
"needCreatedModules": true,
"packageInfo": {
"desc": "图书管理包",
"label": "图书管理",
"template": "package",
"packageName": "library"
},
"modulesInfo": [
{
"package": "library",
"tableName": "library_books",
"businessDB": "",
"structName": "Book",
"packageName": "library",
"description": "图书信息",
"abbreviation": "book",
"humpPackageName": "Library",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"treeJson": "",
"isAdd": false,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "title",
"fieldDesc": "书名",
"fieldType": "string",
"fieldJson": "title",
"dataTypeLong": "255",
"comment": "书名",
"columnName": "title",
"fieldSearchType": "LIKE",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请输入书名",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": {},
"checkDataSource": false,
"fieldIndexType": ""
},
{
"fieldName": "AuthorID",
"fieldDesc": "作者",
"fieldType": "uint",
"fieldJson": "authorId",
"dataTypeLong": "",
"comment": "作者ID",
"columnName": "author_id",
"fieldSearchType": "EQ",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请选择作者",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": {
"dbName": "gva",
"table": "library_authors",
"label": "name",
"value": "id",
"association": 2,
"hasDeletedAt": true
},
"checkDataSource": true,
"fieldIndexType": ""
}
]
},
{
"package": "library",
"tableName": "library_authors",
"businessDB": "",
"structName": "Author",
"packageName": "library",
"description": "作者信息",
"abbreviation": "author",
"humpPackageName": "Library",
"gvaModel": true,
"autoMigrate": true,
"autoCreateResource": true,
"autoCreateApiToSql": true,
"autoCreateMenuToSql": true,
"autoCreateBtnAuth": true,
"onlyTemplate": false,
"isTree": false,
"treeJson": "",
"isAdd": false,
"generateWeb": true,
"generateServer": true,
"fields": [
{
"fieldName": "name",
"fieldDesc": "作者姓名",
"fieldType": "string",
"fieldJson": "name",
"dataTypeLong": "100",
"comment": "作者姓名",
"columnName": "name",
"fieldSearchType": "LIKE",
"fieldSearchHide": false,
"dictType": "",
"form": true,
"table": true,
"desc": true,
"excel": true,
"require": true,
"defaultValue": "",
"errorText": "请输入作者姓名",
"clearable": true,
"sort": false,
"primaryKey": false,
"dataSource": {},
"checkDataSource": false,
"fieldIndexType": ""
}
]
}
]
}
}
```
### 第三步:执行(需要确认参数)
```json
{
"action": "execute",
"executionPlan": {
// ... 同上面的executionPlan
},
"packageConfirm": "yes", // 确认创建包
"modulesConfirm": "yes" // 确认创建模块
}
```
## 确认参数说明
- `packageConfirm`: 当`needCreatedPackage`为true时必需
- "yes": 确认创建包
- "no": 取消创建包(停止后续处理)
- `modulesConfirm`: 当`needCreatedModules`为true时必需
- "yes": 确认创建模块
- "no": 取消创建模块(停止后续处理)
## 取消操作的行为
1. 如果用户在`packageConfirm`中选择"no",系统将停止所有后续处理
2. 如果用户在`modulesConfirm`中选择"no",系统将停止模块创建
3. 任何取消操作都会返回相应的取消消息,不会执行任何创建操作
## 注意事项
1. 必须先调用`confirm`来获取确认信息
2.`execute`时必须提供相应的确认参数
3. 确认参数的值必须是"yes"或"no"
4. 如果不需要创建包或模块,则不需要提供对应的确认参数
5. 字段类型支持string字符串,richtext富文本,int整型,bool布尔值,float64浮点型,time.Time时间,enum枚举,picture单图片字符串,pictures多图片json字符串,video视频字符串,file文件json字符串,jsonJSON,array数组

475
server/mcp/gva_analyze.go Normal file
View File

@@ -0,0 +1,475 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"os"
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/mark3labs/mcp-go/mcp"
)
// 注册工具
func init() {
RegisterTool(&GVAAnalyzer{})
}
// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module
type GVAAnalyzer struct{}
// AnalyzeRequest 分析请求结构体
type AnalyzeRequest struct {
Requirement string `json:"requirement" binding:"required"` // 用户需求描述
}
// AnalyzeResponse 分析响应结构体
type AnalyzeResponse struct {
ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
}
// ModuleInfo 模块信息
type ModuleInfo struct {
ModuleName string `json:"moduleName"` // 模块名称
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
StructName string `json:"structName"` // 结构体名称
TableName string `json:"tableName"` // 表名
Description string `json:"description"` // 描述
FilePaths []string `json:"filePaths"` // 相关文件路径
}
// PackageInfo 包信息
type PackageInfo struct {
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
Label string `json:"label"` // 标签
Desc string `json:"desc"` // 描述
Module string `json:"module"` // 模块
IsEmpty bool `json:"isEmpty"` // 是否为空包
}
// PredesignedModuleInfo 预设计模块信息
type PredesignedModuleInfo struct {
ModuleName string `json:"moduleName"` // 模块名称
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
FilePaths []string `json:"filePaths"` // 文件路径列表
Description string `json:"description"` // 描述
}
// CleanupInfo 清理信息
type CleanupInfo struct {
DeletedPackages []string `json:"deletedPackages"` // 已删除的包
DeletedModules []string `json:"deletedModules"` // 已删除的模块
CleanupMessage string `json:"cleanupMessage"` // 清理消息
}
// New 创建GVA分析器工具
func (g *GVAAnalyzer) New() mcp.Tool {
return mcp.NewTool("gva_analyze",
mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"),
mcp.WithString("requirement",
mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"),
mcp.Required(),
),
)
}
// Handle 处理分析请求
func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 解析请求参数
requirementStr, ok := request.GetArguments()["requirement"].(string)
if !ok || requirementStr == "" {
return nil, errors.New("参数错误requirement 必须是非空字符串")
}
// 创建分析请求
analyzeReq := AnalyzeRequest{
Requirement: requirementStr,
}
// 执行分析逻辑
response, err := g.performAnalysis(ctx, analyzeReq)
if err != nil {
return nil, fmt.Errorf("分析失败: %v", err)
}
// 序列化响应
responseJSON, err := json.Marshal(response)
if err != nil {
return nil, fmt.Errorf("序列化响应失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseJSON)),
},
}, nil
}
// performAnalysis 执行分析逻辑
func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
// 1. 获取数据库中的包信息
var packages []model.SysAutoCodePackage
if err := global.GVA_DB.Find(&packages).Error; err != nil {
return nil, fmt.Errorf("获取包信息失败: %v", err)
}
// 2. 获取历史记录
var histories []model.SysAutoCodeHistory
if err := global.GVA_DB.Find(&histories).Error; err != nil {
return nil, fmt.Errorf("获取历史记录失败: %v", err)
}
// 3. 检查空包并进行清理
cleanupInfo := &CleanupInfo{
DeletedPackages: []string{},
DeletedModules: []string{},
}
var validPackages []model.SysAutoCodePackage
var emptyPackageHistoryIDs []uint
for _, pkg := range packages {
isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
continue
}
if isEmpty {
// 删除空包文件夹
if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
} else {
cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName)
}
// 删除数据库记录
if err := global.GVA_DB.Delete(&pkg).Error; err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
}
// 收集相关的历史记录ID
for _, history := range histories {
if history.Package == pkg.PackageName {
emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
}
}
} else {
validPackages = append(validPackages, pkg)
}
}
// 5. 清理空包相关的历史记录和脏历史记录
var dirtyHistoryIDs []uint
for _, history := range histories {
// 检查是否为空包相关的历史记录
for _, emptyID := range emptyPackageHistoryIDs {
if history.ID == emptyID {
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
break
}
}
}
// 删除脏历史记录
if len(dirtyHistoryIDs) > 0 {
if err := global.GVA_DB.Delete(&model.SysAutoCodeHistory{}, "id IN ?", dirtyHistoryIDs).Error; err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
} else {
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", len(dirtyHistoryIDs)))
}
// 清理相关的API和菜单记录
if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
}
}
// 6. 扫描预设计模块
predesignedModules, err := g.scanPredesignedModules()
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
predesignedModules = []PredesignedModuleInfo{} // 设置为空列表,不影响主流程
}
// 7. 过滤掉与已删除包相关的模块
filteredModules := []PredesignedModuleInfo{}
for _, module := range predesignedModules {
isDeleted := false
for _, deletedPkg := range cleanupInfo.DeletedPackages {
if module.PackageName == deletedPkg {
isDeleted = true
break
}
}
if !isDeleted {
filteredModules = append(filteredModules, module)
}
}
// 8. 构建分析结果消息
var analysisMessage strings.Builder
if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
analysisMessage.WriteString("**系统清理完成**\n\n")
if len(cleanupInfo.DeletedPackages) > 0 {
analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
}
if len(cleanupInfo.DeletedModules) > 0 {
analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
}
analysisMessage.WriteString("\n")
cleanupInfo.CleanupMessage = analysisMessage.String()
}
analysisMessage.WriteString(" **分析结果**\n\n")
analysisMessage.WriteString(fmt.Sprintf("- **现有包数量**: %d\n", len(validPackages)))
analysisMessage.WriteString(fmt.Sprintf("- **预设计模块数量**: %d\n\n", len(filteredModules)))
// 9. 转换包信息
existingPackages := make([]PackageInfo, len(validPackages))
for i, pkg := range validPackages {
existingPackages[i] = PackageInfo{
PackageName: pkg.PackageName,
Template: pkg.Template,
Label: pkg.Label,
Desc: pkg.Desc,
Module: pkg.Module,
IsEmpty: false, // 已经过滤掉空包
}
}
dictionaries := []DictionaryPre{} // 这里可以根据需要填充字典信息
err = global.GVA_DB.Table("sys_dictionaries").Find(&dictionaries, "deleted_at is null").Error
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
dictionaries = []DictionaryPre{} // 设置为空列表,不影响主流程
}
// 10. 构建响应
response := &AnalyzeResponse{
ExistingPackages: existingPackages,
PredesignedModules: filteredModules,
Dictionaries: dictionaries,
}
return response, nil
}
// isPackageFolderEmpty 检查包文件夹是否为空
func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) {
// 根据模板类型确定基础路径
var basePath string
if template == "plugin" {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
} else {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName)
}
// 检查文件夹是否存在
if _, err := os.Stat(basePath); os.IsNotExist(err) {
return true, nil // 文件夹不存在,视为空
} else if err != nil {
return false, err // 其他错误
}
// 读取文件夹内容
entries, err := os.ReadDir(basePath)
if err != nil {
return false, err
}
// 检查是否有.go文件
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
return false, nil // 找到.go文件不为空
}
}
return true, nil // 没有找到.go文件为空
}
// removeEmptyPackageFolder 删除空包文件夹
func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
var basePath string
if template == "plugin" {
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
} else {
// 对于package类型需要删除多个目录
paths := []string{
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
}
for _, path := range paths {
if err := g.removeDirectoryIfExists(path); err != nil {
return err
}
}
return nil
}
return g.removeDirectoryIfExists(basePath)
}
// removeDirectoryIfExists 删除目录(如果存在)
func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
return nil // 目录不存在,无需删除
} else if err != nil {
return err // 其他错误
}
return os.RemoveAll(dirPath)
}
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
if len(historyIDs) == 0 {
return nil
}
// 这里可以根据需要实现具体的API和菜单清理逻辑
// 由于涉及到具体的业务逻辑,这里只做日志记录
global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs))
// 可以调用service层的相关方法进行清理
// 例如service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs)
// 例如service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs)
return nil
}
// scanPredesignedModules 扫描预设计模块
func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
// 获取autocode配置路径
autocodeRoot := global.GVA_CONFIG.AutoCode.Root
if autocodeRoot == "" {
return nil, errors.New("autocode根路径未配置")
}
var modules []PredesignedModuleInfo
// 扫描plugin目录
pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin"))
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err))
} else {
modules = append(modules, pluginModules...)
}
// 扫描model目录
modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model"))
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err))
} else {
modules = append(modules, modelModules...)
}
return modules, nil
}
// scanPluginModules 扫描插件模块
func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
return modules, nil // 目录不存在,返回空列表
}
entries, err := os.ReadDir(pluginDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
pluginName := entry.Name()
pluginPath := filepath.Join(pluginDir, pluginName)
// 查找model目录
modelDir := filepath.Join(pluginPath, "model")
if _, err := os.Stat(modelDir); err == nil {
// 扫描model目录下的模块
pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err))
continue
}
modules = append(modules, pluginModules...)
}
}
}
return modules, nil
}
// scanModelModules 扫描模型模块
func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
if _, err := os.Stat(modelDir); os.IsNotExist(err) {
return modules, nil // 目录不存在,返回空列表
}
entries, err := os.ReadDir(modelDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
packageName := entry.Name()
packagePath := filepath.Join(modelDir, packageName)
// 扫描包目录下的模块
packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err))
continue
}
modules = append(modules, packageModules...)
}
}
return modules, nil
}
// scanModulesInDirectory 扫描目录中的模块
func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) {
var modules []PredesignedModuleInfo
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
moduleName := strings.TrimSuffix(entry.Name(), ".go")
filePath := filepath.Join(dir, entry.Name())
module := PredesignedModuleInfo{
ModuleName: moduleName,
PackageName: packageName,
Template: template,
FilePaths: []string{filePath},
Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName),
}
modules = append(modules, module)
}
}
return modules, nil
}

File diff suppressed because it is too large Load Diff

778
server/mcp/gva_execute.go Normal file
View File

@@ -0,0 +1,778 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
RegisterTool(&GVAExecutor{})
}
// GVAExecutor GVA代码生成器
type GVAExecutor struct{}
// ExecuteRequest 执行请求结构
type ExecuteRequest struct {
ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
}
// ExecuteResponse 执行响应结构
type ExecuteResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
PackageID uint `json:"packageId,omitempty"`
HistoryID uint `json:"historyId,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
GeneratedPaths []string `json:"generatedPaths,omitempty"`
NextActions []string `json:"nextActions,omitempty"`
}
// ExecutionPlan 执行计划结构
type ExecutionPlan struct {
PackageName string `json:"packageName"`
PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"`
NeedCreatedModules bool `json:"needCreatedModules"`
NeedCreatedDictionaries bool `json:"needCreatedDictionaries"`
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"`
Paths map[string]string `json:"paths,omitempty"`
DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"`
}
// New 创建GVA代码生成执行器工具
func (g *GVAExecutor) New() mcp.Tool {
return mcp.NewTool("gva_execute",
mcp.WithDescription(`**GVA代码生成执行器直接执行代码生成无需确认步骤**
**核心功能:**
- 根据需求分析和当前的包信息判断是否调用如果需要调用则根据入参描述生成json用于直接生成代码
- 支持批量创建多个模块
- 自动创建包、模块、字典等
- 移除了确认步骤,提高执行效率
**使用场景:**
- 在gva_analyze获取了当前的包信息和字典信息之后如果已经包含了可以使用的包和模块那就不要调用本mcp
- 根据分析结果直接生成代码
- 适用于自动化代码生成流程
**批量创建功能:**
- 支持在单个ExecutionPlan中创建多个模块
- modulesInfo字段为数组可包含多个模块配置
- 一次性处理多个模块的创建和字典生成
**新功能:自动字典创建**
- 当结构体字段使用了字典类型dictType不为空系统会自动检查字典是否存在
- 如果字典不存在,会自动创建对应的字典及默认的字典详情项
- 字典创建包括字典主表记录和默认的选项值选项1、选项2等
**重要限制:**
- 当needCreatedModules=true时模块创建会自动生成API和菜单因此不应再调用api_creator和menu_creator工具
- 只有在单独创建API或菜单不涉及模块创建时才使用api_creator和menu_creator工具
重要ExecutionPlan结构体格式要求支持批量创建
{
"packageName": "包名(string)",
"packageType": "package或plugin(string)如果用户提到了使用插件则创建plugin如果用户没有特定说明则一律选用package。",
"needCreatedPackage": "是否需要创建包(bool)",
"needCreatedModules": "是否需要创建模块(bool)",
"needCreatedDictionaries": "是否需要创建字典(bool)",
"packageInfo": {
"desc": "描述(string)",
"label": "展示名(string)",
"template": "package或plugin(string)如果用户提到了使用插件则创建plugin如果用户没有特定说明则一律选用package。",
"packageName": "包名(string)"
},
"modulesInfo": [{
"package": "包名(string必然是小写开头)",
"tableName": "数据库表名(string使用蛇形命名法)",
"businessDB": "业务数据库(string)",
"structName": "结构体名(string)",
"packageName": "文件名称(string)",
"description": "中文描述(string)",
"abbreviation": "简称(string)",
"humpPackageName": "文件名称 一般是结构体名的小驼峰(string)",
"gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at",
"autoMigrate": "是否自动迁移(bool)",
"autoCreateResource": "是否创建资源(bool默认为false)",
"autoCreateApiToSql": "是否创建API(bool默认为true)",
"autoCreateMenuToSql": "是否创建菜单(bool默认为true)",
"autoCreateBtnAuth": "是否创建按钮权限(bool默认为false)",
"onlyTemplate": "是否仅模板(bool默认为false)",
"isTree": "是否树形结构(bool默认为false)",
"treeJson": "树形JSON字段(string)",
"isAdd": "是否新增(bool) 固定为false",
"generateWeb": "是否生成前端(bool)",
"generateServer": "是否生成后端(bool)",
"fields": [{
"fieldName": "字段名(string)必须大写开头",
"fieldDesc": "字段描述(string)",
"fieldType": "字段类型支持string字符串,richtext富文本,int整型,bool布尔值,float64浮点型,time.Time时间,enum枚举,picture单图片字符串,pictures多图片json字符串,video视频字符串,file文件json字符串,jsonJSON,array数组",
"fieldJson": "JSON标签(string)",
"dataTypeLong": "数据长度(string)",
"comment": "注释(string)",
"columnName": "数据库列名(string)",
"fieldSearchType": "搜索类型:=/>/</>=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)",
"fieldSearchHide": "是否隐藏搜索(bool)",
"dictType": "字典类型(string)",
"form": "表单显示(bool)",
"table": "表格显示(bool)",
"desc": "详情显示(bool)",
"excel": "导入导出(bool)",
"require": "是否必填(bool)",
"defaultValue": "默认值(string)",
"errorText": "错误提示(string)",
"clearable": "是否可清空(bool)",
"sort": "是否排序(bool)",
"primaryKey": "是否主键(bool)",
"dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如SysUser 的表名为 \"sys_users\"ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空",
"checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性",
"fieldIndexType": "索引类型(string)"
}]
}, {
"package": "包名(string)",
"tableName": "第二个模块的表名(string)",
"structName": "第二个模块的结构体名(string)",
"description": "第二个模块的描述(string)",
"...": "更多模块配置..."
}],
"dictionariesInfo":[{
"dictType": "字典类型(string) - 用于标识字典的唯一性",
"dictName": "字典名称(string) - 必须生成,字典的中文名称",
"description": "字典描述(string) - 字典的用途说明",
"status": "字典状态(bool) - true启用false禁用",
"fieldDesc": "字段描述(string) - 用于AI理解字段含义并生成合适的选项",
"options": [{
"label": "显示名称(string) - 用户看到的选项名",
"value": "选项值(string) - 实际存储的值",
"sort": "排序号(int) - 数字越小越靠前"
}]
}]
}
注意:
1. needCreatedPackage=true时packageInfo必需
2. needCreatedModules=true时modulesInfo必需
3. needCreatedDictionaries=true时dictionariesInfo必需
4. dictionariesInfo中的options字段可选如果不提供将根据fieldDesc自动生成默认选项
5. 字典创建会在模块创建之前执行,确保模块字段可以正确引用字典类型
6. packageType只能是"package"或"plugin,如果用户提到了使用插件则创建plugin如果用户没有特定说明则一律选用package。"
7. 字段类型支持string字符串,richtext富文本,int整型,bool布尔值,float64浮点型,time.Time时间,enum枚举,picture单图片字符串,pictures多图片json字符串,video视频字符串,file文件json字符串,jsonJSON,array数组
8. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN
9. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段
10. **重要**当gvaModel=false时必须有一个字段的primaryKey=true否则会导致PrimaryField为nil错误
11. **重要**当gvaModel=true时系统会自动设置ID字段为主键无需手动设置primaryKey=true
12. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会:
- 自动检查字典是否存在,如果不存在则创建字典
- 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景
- 为无法识别的字典类型提供通用默认选项
13. **模块关联配置**当需要配置模块间的关联关系时使用dataSource字段
- **dbName**: 关联的数据库名称
- **table**: 关联的表名
- **label**: 用于显示的字段名如name、title等
- **value**: 用于存储的值字段名通常是id
- **association**: 关联关系类型1=一对一关联2=一对多关联)一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个,则选用一对一,如果他需要关联多个他的关联实体,则选用一对多。
- **hasDeletedAt**: 关联表是否有软删除字段
- **checkDataSource**: 设为true时会验证关联表的存在性
- 示例:{"dbName":"","table":"sys_users","label":"username","value":"id","association":1,"hasDeletedAt":true}
14. **自动字段类型修正**:系统会自动检查和修正字段类型:
- 当字段配置了dataSource且association=2一对多关联系统会自动将fieldType修改为'array'
- 这确保了一对多关联数据的正确存储和处理
- 修正操作会记录在日志中,便于开发者了解变更情况`),
mcp.WithObject("executionPlan",
mcp.Description("执行计划,包含包信息和模块信息"),
mcp.Required(),
),
mcp.WithString("requirement",
mcp.Description("原始需求描述(可选,用于日志记录)"),
),
)
}
// Handle 处理执行请求(移除确认步骤)
func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
executionPlanData, ok := request.GetArguments()["executionPlan"]
if !ok {
return nil, errors.New("参数错误executionPlan 必须提供")
}
// 解析执行计划
planJSON, err := json.Marshal(executionPlanData)
if err != nil {
return nil, fmt.Errorf("解析执行计划失败: %v", err)
}
var plan ExecutionPlan
err = json.Unmarshal(planJSON, &plan)
if err != nil {
return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确参考工具描述中的结构体格式要求", err)
}
// 验证执行计划的完整性
if err := g.validateExecutionPlan(&plan); err != nil {
return nil, fmt.Errorf("执行计划验证失败: %v", err)
}
// 获取原始需求(可选)
var originalRequirement string
if reqData, ok := request.GetArguments()["requirement"]; ok {
if reqStr, ok := reqData.(string); ok {
originalRequirement = reqStr
}
}
// 直接执行创建操作(无确认步骤)
result := g.executeCreation(ctx, &plan)
// 如果执行成功且有原始需求,提供代码复检建议
var reviewMessage string
if result.Success && originalRequirement != "" {
global.GVA_LOG.Info("执行完成返回生成的文件路径供AI进行代码复检...")
// 构建文件路径信息供AI使用
var pathsInfo []string
for _, path := range result.GeneratedPaths {
pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path))
}
reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n"))
} else if originalRequirement == "" {
reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。"
}
// 序列化响应
response := ExecuteResponse{
Success: result.Success,
Message: result.Message,
PackageID: result.PackageID,
HistoryID: result.HistoryID,
Paths: result.Paths,
GeneratedPaths: result.GeneratedPaths,
NextActions: result.NextActions,
}
responseJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)),
},
}, nil
}
// validateExecutionPlan 验证执行计划的完整性
func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
// 验证基本字段
if plan.PackageName == "" {
return errors.New("packageName 不能为空")
}
if plan.PackageType != "package" && plan.PackageType != "plugin" {
return errors.New("packageType 必须是 'package' 或 'plugin'")
}
// 验证packageType和template字段的一致性
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
if plan.PackageType != plan.PackageInfo.Template {
return errors.New("packageType 和 packageInfo.template 必须保持一致")
}
}
// 验证包信息
if plan.NeedCreatedPackage {
if plan.PackageInfo == nil {
return errors.New("当 needCreatedPackage=true 时packageInfo 不能为空")
}
if plan.PackageInfo.PackageName == "" {
return errors.New("packageInfo.packageName 不能为空")
}
if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" {
return errors.New("packageInfo.template 必须是 'package' 或 'plugin'")
}
if plan.PackageInfo.Label == "" {
return errors.New("packageInfo.label 不能为空")
}
if plan.PackageInfo.Desc == "" {
return errors.New("packageInfo.desc 不能为空")
}
}
// 验证模块信息(批量验证)
if plan.NeedCreatedModules {
if len(plan.ModulesInfo) == 0 {
return errors.New("当 needCreatedModules=true 时modulesInfo 不能为空")
}
// 遍历验证每个模块
for moduleIndex, moduleInfo := range plan.ModulesInfo {
if moduleInfo.Package == "" {
return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1)
}
if moduleInfo.StructName == "" {
return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1)
}
if moduleInfo.TableName == "" {
return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1)
}
if moduleInfo.Description == "" {
return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1)
}
if moduleInfo.Abbreviation == "" {
return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1)
}
if moduleInfo.PackageName == "" {
return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1)
}
if moduleInfo.HumpPackageName == "" {
return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
}
// 验证字段信息
if len(moduleInfo.Fields) == 0 {
return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
}
for i, field := range moduleInfo.Fields {
if field.FieldName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
}
// 确保字段名首字母大写
if len(field.FieldName) > 0 {
firstChar := string(field.FieldName[0])
if firstChar >= "a" && firstChar <= "z" {
moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
}
}
if field.FieldDesc == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
}
if field.FieldType == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1)
}
if field.FieldJson == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1)
}
if field.ColumnName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
}
// 验证字段类型
validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
validType := false
for _, validFieldType := range validFieldTypes {
if field.FieldType == validFieldType {
validType = true
break
}
}
if !validType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes)
}
// 验证搜索类型(如果设置了)
if field.FieldSearchType != "" {
validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
validSearchType := false
for _, validType := range validSearchTypes {
if field.FieldSearchType == validType {
validSearchType = true
break
}
}
if !validSearchType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes)
}
}
// 验证 dataSource 字段配置
if field.DataSource != nil {
associationValue := field.DataSource.Association
// 当 association 为 2一对多关联强制修改 fieldType 为 array
if associationValue == 2 {
if field.FieldType != "array" {
global.GVA_LOG.Info(fmt.Sprintf("模块 %d 字段 %d检测到一对多关联(association=2),自动将 fieldType 从 '%s' 修改为 'array'", moduleIndex+1, i+1, field.FieldType))
moduleInfo.Fields[i].FieldType = "array"
}
}
// 验证 association 值的有效性
if associationValue != 1 && associationValue != 2 {
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1一对一或 2一对多", moduleIndex+1, i+1)
}
}
}
// 验证主键设置
if !moduleInfo.GvaModel {
// 当不使用GVA模型时必须有且仅有一个字段设置为主键
primaryKeyCount := 0
for _, field := range moduleInfo.Fields {
if field.PrimaryKey {
primaryKeyCount++
}
}
if primaryKeyCount == 0 {
return fmt.Errorf("模块 %d当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1)
}
if primaryKeyCount > 1 {
return fmt.Errorf("模块 %d当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
}
} else {
// 当使用GVA模型时所有字段的primaryKey都应该为false
for i, field := range moduleInfo.Fields {
if field.PrimaryKey {
return fmt.Errorf("模块 %d当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false系统会自动创建ID主键", moduleIndex+1, i+1)
}
}
}
}
}
return nil
}
// executeCreation 执行创建操作
func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse {
result := &ExecuteResponse{
Success: false,
Paths: make(map[string]string),
GeneratedPaths: []string{}, // 初始化生成文件路径列表
}
// 无论如何都先构建目录结构信息确保paths始终返回
result.Paths = g.buildDirectoryStructure(plan)
// 记录预期生成的文件路径
result.GeneratedPaths = g.collectExpectedFilePaths(plan)
if !plan.NeedCreatedModules {
result.Success = true
result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
return result
}
// 创建包(如果需要)
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
err := packageService.Create(ctx, plan.PackageInfo)
if err != nil {
result.Message = fmt.Sprintf("创建包失败: %v", err)
// 即使创建包失败也要返回paths信息
return result
}
result.Message += "包创建成功; "
}
// 创建指定字典(如果需要)
if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 {
dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo)
result.Message += dictResult
}
// 批量创建字典和模块(如果需要)
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
// 遍历所有模块进行创建
for _, moduleInfo := range plan.ModulesInfo {
// 创建模块
err := moduleInfo.Pretreatment()
if err != nil {
result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
continue // 继续处理下一个模块
}
err = templateService.Create(ctx, *moduleInfo)
if err != nil {
result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
continue // 继续处理下一个模块
}
result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName)
}
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
// 添加重要提醒不要使用其他MCP工具
result.Message += "\n\n⚠ 重要提醒:\n"
result.Message += "模块创建已完成API和菜单已自动生成。请不要再调用以下MCP工具\n"
result.Message += "- api_creatorAPI权限已在模块创建时自动生成\n"
result.Message += "- menu_creator前端菜单已在模块创建时自动生成\n"
result.Message += "如需修改API或菜单请直接在系统管理界面中进行配置。\n"
}
result.Message += "已构建目录结构信息; "
result.Success = true
if result.Message == "" {
result.Message = "执行计划完成"
}
return result
}
// buildDirectoryStructure 构建目录结构信息
func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string {
paths := make(map[string]string)
// 获取配置信息
autoCodeConfig := global.GVA_CONFIG.AutoCode
// 构建基础路径
rootPath := autoCodeConfig.Root
serverPath := autoCodeConfig.Server
webPath := autoCodeConfig.Web
moduleName := autoCodeConfig.Module
// 如果计划中有包名,使用计划中的包名,否则使用默认
packageName := "example"
if plan.PackageName != "" {
packageName = plan.PackageName
}
// 如果计划中有模块信息,获取第一个模块的结构名作为默认值
structName := "ExampleStruct"
if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" {
structName = plan.ModulesInfo[0].StructName
}
// 根据包类型构建不同的路径结构
packageType := plan.PackageType
if packageType == "" {
packageType = "package" // 默认为package模式
}
// 构建服务端路径
if serverPath != "" {
serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
if packageType == "plugin" {
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录下
plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
// API 路径
paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
// Service 路径
paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
// Model 路径
paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
// Router 路径
paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
// Plugin 特有文件
paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath)
paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath)
paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath)
} else {
// Package 模式:传统的目录结构
// API 路径
paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
// Service 路径
paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
// Model 路径
paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
// Router 路径
paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
}
}
// 构建前端路径(两种模式相同)
if webPath != "" {
webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
if packageType == "plugin" {
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录下
pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
} else {
// Package 模式:传统的目录结构
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
}
}
// 添加模块信息
paths["module"] = moduleName
paths["package_name"] = packageName
paths["package_type"] = packageType
paths["struct_name"] = structName
paths["root_path"] = rootPath
paths["server_path"] = serverPath
paths["web_path"] = webPath
return paths
}
// collectExpectedFilePaths 收集预期生成的文件路径
func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
var paths []string
// 获取目录结构
dirPaths := g.buildDirectoryStructure(plan)
// 如果需要创建模块,添加预期的文件路径
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
for _, moduleInfo := range plan.ModulesInfo {
structName := moduleInfo.StructName
// 后端文件
if apiPath, ok := dirPaths["api"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName)))
}
if servicePath, ok := dirPaths["service"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName)))
}
if modelPath, ok := dirPaths["model"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName)))
}
if routerPath, ok := dirPaths["router"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName)))
}
if requestPath, ok := dirPaths["request"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName)))
}
if responsePath, ok := dirPaths["response"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName)))
}
// 前端文件
if vuePage, ok := dirPaths["vue_page"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName)))
}
if vueApi, ok := dirPaths["vue_api"]; ok {
paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName)))
}
}
}
return paths
}
// checkDictionaryExists 检查字典是否存在
func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
_, err := dictionaryService.GetSysDictionary(dictType, 0, nil)
if err != nil {
// 如果是记录不存在的错误返回false
if strings.Contains(err.Error(), "record not found") {
return false, nil
}
// 其他错误返回错误信息
return false, err
}
return true, nil
}
// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
var messages []string
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
for _, dictInfo := range dictionariesInfo {
// 检查字典是否存在
exists, err := g.checkDictionaryExists(dictInfo.DictType)
if err != nil {
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
continue
}
if !exists {
// 字典不存在,创建字典
dictionary := model.SysDictionary{
Name: dictInfo.DictName,
Type: dictInfo.DictType,
Status: utils.Pointer(true),
Desc: dictInfo.Description,
}
err = dictionaryService.CreateSysDictionary(dictionary)
if err != nil {
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
continue
}
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
// 获取刚创建的字典ID
var createdDict model.SysDictionary
err = global.GVA_DB.Where("type = ?", dictInfo.DictType).First(&createdDict).Error
if err != nil {
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err))
continue
}
// 创建字典选项
if len(dictInfo.Options) > 0 {
successCount := 0
for _, option := range dictInfo.Options {
dictionaryDetail := model.SysDictionaryDetail{
Label: option.Label,
Value: option.Value,
Status: &[]bool{true}[0], // 默认启用
Sort: option.Sort,
SysDictionaryID: int(createdDict.ID),
}
err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail)
if err != nil {
global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err))
} else {
successCount++
}
}
messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount))
}
} else {
messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType))
}
}
return strings.Join(messages, "")
}

170
server/mcp/gva_review.go Normal file
View File

@@ -0,0 +1,170 @@
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
// GVAReviewer GVA代码审查工具
type GVAReviewer struct{}
// init 注册工具
func init() {
RegisterTool(&GVAReviewer{})
}
// ReviewRequest 审查请求结构
type ReviewRequest struct {
UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求
GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表
}
// ReviewResponse 审查响应结构
type ReviewResponse struct {
Success bool `json:"success"` // 是否审查成功
Message string `json:"message"` // 审查结果消息
AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示
ReviewDetails string `json:"reviewDetails"` // 详细的审查结果
}
// New 创建GVA代码审查工具
func (g *GVAReviewer) New() mcp.Tool {
return mcp.NewTool("gva_review",
mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用**
**核心功能:**
- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表
- 分析生成的代码是否满足用户的原始需求
- 检查是否涉及到关联、交互等复杂功能
- 如果代码不满足需求提供调整建议和新的prompt
**使用场景:**
- 在gva_execute成功执行后调用
- 用于验证生成的代码是否完整满足用户需求
- 检查模块间的关联关系是否正确实现
- 发现缺失的交互功能或业务逻辑
**工作流程:**
1. 接收用户原始需求和生成的文件列表
2. 分析需求中的关键功能点
3. 检查生成的文件是否覆盖所有功能
4. 识别缺失的关联关系、交互功能等
5. 生成调整建议和新的开发prompt
**输出内容:**
- 审查结果和是否需要调整
- 详细的缺失功能分析
- 针对性的代码调整建议
- 可直接使用的开发prompt
**重要提示:**
- 本工具专门用于代码质量审查,不执行实际的代码修改
- 重点关注模块间关联、用户交互、业务流程完整性
- 提供的调整建议应该具体可执行`),
mcp.WithString("userRequirement",
mcp.Description("经过requirement_analyze处理后的用户需求描述包含详细的功能要求和字段信息"),
mcp.Required(),
),
mcp.WithString("generatedFiles",
mcp.Description("gva_execute创建的文件列表JSON字符串格式包含所有生成的后端和前端文件路径"),
mcp.Required(),
),
)
}
// Handle 处理审查请求
func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取用户需求
userRequirementData, ok := request.GetArguments()["userRequirement"]
if !ok {
return nil, errors.New("参数错误userRequirement 必须提供")
}
userRequirement, ok := userRequirementData.(string)
if !ok {
return nil, errors.New("参数错误userRequirement 必须是字符串类型")
}
// 获取生成的文件列表
generatedFilesData, ok := request.GetArguments()["generatedFiles"]
if !ok {
return nil, errors.New("参数错误generatedFiles 必须提供")
}
generatedFilesStr, ok := generatedFilesData.(string)
if !ok {
return nil, errors.New("参数错误generatedFiles 必须是JSON字符串")
}
// 解析JSON字符串为字符串数组
var generatedFiles []string
err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles)
if err != nil {
return nil, fmt.Errorf("解析generatedFiles失败: %v", err)
}
if len(generatedFiles) == 0 {
return nil, errors.New("参数错误generatedFiles 不能为空")
}
// 直接生成调整提示,不进行复杂分析
adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles)
// 构建简化的审查详情
reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles))
// 构建审查结果
reviewResult := &ReviewResponse{
Success: true,
Message: "代码审查完成",
AdjustmentPrompt: adjustmentPrompt,
ReviewDetails: reviewDetails,
}
// 序列化响应
responseJSON, err := json.MarshalIndent(reviewResult, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化审查结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))),
},
}, nil
}
// generateAdjustmentPrompt 生成调整代码的提示
func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string {
var prompt strings.Builder
prompt.WriteString("🔧 **代码调整指导 Prompt**\n\n")
prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement))
prompt.WriteString("**经过GVA生成后的文件有如下内容**\n")
for _, file := range generatedFiles {
prompt.WriteString(fmt.Sprintf("- %s\n", file))
}
prompt.WriteString("\n")
prompt.WriteString("**请帮我优化和完善代码,确保:**\n")
prompt.WriteString("1. 代码完全满足用户的原始需求\n")
prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n")
prompt.WriteString("3. 实现所有必要的用户交互功能\n")
prompt.WriteString("4. 保持代码的完整性和可维护性\n")
prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n")
prompt.WriteString("6. 确保前后端功能完整对接\n")
prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n")
prompt.WriteString("8. 如果需要vue路由跳转请使用 menu_lister获取完整路由表并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n")
prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求则自行书写vue文件并且调用 menu_creator创建菜单记录\n\n")
prompt.WriteString("10. 如果需要API调用请使用 api_lister获取api表根据需求调用对应接口\n\n")
prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口补全前后端代码并使用 api_creator创建api记录\n\n")
prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n")
prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**")
return prompt.String()
}

View File

@@ -266,21 +266,11 @@ func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
// 添加权限分配提醒
permissionReminder := "\n\n⚠ 重要提醒:\n" +
"菜单创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的菜单权限," +
"以确保用户能够正常访问新菜单。\n" +
"具体步骤:\n" +
"1. 进入角色管理页面\n" +
"2. 选择需要授权的角色\n" +
"3. 在菜单权限中勾选新创建的菜单项\n" +
"4. 保存权限配置"
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("菜单创建结果:\n\n%s%s", string(resultJSON), permissionReminder),
Text: fmt.Sprintf("菜单创建结果:\n\n%s", string(resultJSON)),
},
},
}, nil

View File

@@ -20,8 +20,6 @@ type RequirementAnalysisRequest struct {
UserRequirement string `json:"userRequirement"`
}
// RequirementAnalysisResponse 需求分析响应
type RequirementAnalysisResponse struct {
AIPrompt string `json:"aiPrompt"` // 给AI的提示词
@@ -30,37 +28,36 @@ type RequirementAnalysisResponse struct {
// New 返回工具注册信息
func (t *RequirementAnalyzer) New() mcp.Tool {
return mcp.NewTool("requirement_analyzer",
mcp.WithDescription(`**🚀 需求分析工具 - 首选入口工具(最高优先级)**
mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)**
** 重要提示这是所有MCP工具的首选入口请优先使用**
** 重要提示这是所有MCP工具的首选入口请优先使用**
**🎯 核心职责**
将用户的自然语言需求转换为AI可理解的结构化提示词
** 核心能力**
作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构
**📋 工作流程**
1. 接收用户自然语言需求描述
2. 生成专业的AI提示词要求AI将需求梳理为清晰的逻辑步骤
- **1. 第一步功能描述**
- **2. 第二步功能描述**
- **3. 第三步功能描述**
- **...**
3. 指导后续使用 gva_auto_generate 工具进行代码生成
** 核心功能**
1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系
2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能
3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求
4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议
**✅ 适用场景**
- 用户有新的业务需求需要开发
- 需要创建新的功能模块
- 想要快速搭建业务系统
- 需求描述比较模糊需要AI帮助梳理
** 输出内容**
- 模块数量和架构设计
- 每个模块的详细字段清单
- 数据类型和关联关系设计
- 字典需求和类型定义
- 模块间关系图和扩展建议
**❌ 不负责的事情**
- 不生成具体的包名和模块名(交给 gva_auto_generate
- 不进行代码生成(交给 gva_auto_generate
- 不创建数据库表结构(交给 gva_auto_generate
** 适用场景**
- 用户需求描述不完整,需要智能补全
- 复杂业务系统的模块架构设计
- 需要专业的数据库设计建议
- 想要快速搭建生产级业务系统
**🔄 推荐工作流:**
requirement_analyzer → gva_auto_generate → 其他辅助工具
`),
** 推荐工作流:**
requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具
`),
mcp.WithString("userRequirement",
mcp.Required(),
mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"),
@@ -104,36 +101,99 @@ func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*Requi
}, nil
}
// generateAIPrompt 生成AI提示词 - 要求AI梳理逻辑为1xxx2xxx格式
// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构
func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string {
prompt := fmt.Sprintf(`# 🤖 AI需求逻辑梳理任务
prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务
## 📝 用户原始需求
## 用户原始需求
%s
## 🎯 AI任务要求
请将上述用户需求梳理成清晰的逻辑步骤,格式要求:
## 核心任务
你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。
## 分析步骤
### 第一步:需求解构分析
请仔细分析用户需求,识别出:
1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等)
2. **业务流程**(如:注册、购买、记录、管理等)
3. **数据关系**(实体间的关联关系)
4. **功能模块**(需要哪些独立的管理模块)
### 第二步:模块架构设计
基于需求分析,设计出模块架构,格式如下:
**模块1[模块名称]**
- 功能描述:[该模块的核心功能]
- 主要字段:[列出关键字段,注明数据类型]
- 关联关系:[与其他模块的关系,明确一对一/一对多]
- 字典需求:[需要哪些字典类型]
**模块2[模块名称]**
- 功能描述:[该模块的核心功能]
- 主要字段:[列出关键字段,注明数据类型]
- 关联关系:[与其他模块的关系]
- 字典需求:[需要哪些字典类型]
**1. 第一步功能描述**
**2. 第二步功能描述**
**3. 第三步功能描述**
**...**
## 📋 梳理要求
- 将需求拆解为具体的功能步骤
- 每个步骤用数字编号1、2、3...
- 步骤描述要清晰、具体、可执行
- 按照业务逻辑顺序排列
- 考虑数据流和用户操作流程
### 第三步:字段详细设计
为每个模块详细设计字段:
## 🔄 后续流程
梳理完成后,请使用 gva_auto_generate 工具进行代码生成:
- gva_auto_generate 会根据梳理的逻辑步骤自动生成包名、模块名
- gva_auto_generate 会设计数据表结构和API接口
- gva_auto_generate 会生成完整的前后端代码
#### 模块1字段清单
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- ...
#### 模块2字段清单
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
- ...
现在请开始梳理用户需求:"%s"`, userRequirement, userRequirement)
## 智能分析指导原则
### 模块拆分原则
1. **单一职责**:每个模块只负责一个核心业务实体
2. **数据完整性**:相关数据应该在同一模块中
3. **业务独立性**:模块应该能够独立完成特定业务功能
4. **扩展性考虑**:为未来功能扩展预留空间
### 字段设计原则
1. **必要性**:只包含业务必需的字段
2. **规范性**:遵循数据库设计规范
3. **关联性**:正确识别实体间关系
4. **字典化**:状态、类型等枚举值使用字典
### 关联关系识别
- **一对一**:一个实体只能关联另一个实体的一个记录
- **一对多**:一个实体可以关联另一个实体的多个记录
- **多对多**:通过中间表实现复杂关联
## 特殊场景处理
### 复杂实体识别
当用户提到某个概念时,要判断它是否需要独立模块:
- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等)
- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录)
## 输出要求
### 必须包含的信息
1. **模块数量**:明确需要几个模块
2. **模块关系图**:用文字描述模块间关系
3. **核心字段**每个模块的关键字段至少5-10个
4. **数据类型**string、int、bool、time.Time、float64等
5. **关联设计**:明确哪些字段是关联字段
6. **字典需求**:列出需要创建的字典类型
### 严格遵循用户输入
- 如果用户提供了具体字段,**必须使用**用户提供的字段
- 如果用户提供了SQL文件**严格按照**SQL结构设计
- **不要**随意发散,**不要**添加用户未提及的功能
---
**现在请开始深度分析用户需求:"%s"**
请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement)
return prompt
}

View File

@@ -1,13 +1,12 @@
package middleware
import (
"strconv"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"strconv"
"strings"
)
// CasbinHandler 拦截器

View File

@@ -0,0 +1,5 @@
package request
type SysDictionarySearch struct {
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
}

View File

@@ -8,4 +8,36 @@ import (
type SysDictionaryDetailSearch struct {
system.SysDictionaryDetail
request.PageInfo
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID用于查询指定父级下的子项
Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据
}
// CreateSysDictionaryDetailRequest 创建字典详情请求
type CreateSysDictionaryDetailRequest struct {
Label string `json:"label" form:"label" binding:"required"` // 展示值
Value string `json:"value" form:"value" binding:"required"` // 字典值
Extend string `json:"extend" form:"extend"` // 扩展值
Status *bool `json:"status" form:"status"` // 启用状态
Sort int `json:"sort" form:"sort"` // 排序标记
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
}
// UpdateSysDictionaryDetailRequest 更新字典详情请求
type UpdateSysDictionaryDetailRequest struct {
ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID
Label string `json:"label" form:"label" binding:"required"` // 展示值
Value string `json:"value" form:"value" binding:"required"` // 字典值
Extend string `json:"extend" form:"extend"` // 扩展值
Status *bool `json:"status" form:"status"` // 启用状态
Sort int `json:"sort" form:"sort"` // 排序标记
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
}
// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求
type GetDictionaryDetailsByParentRequest struct {
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID为空时获取顶级
IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据
}

View File

@@ -8,10 +8,12 @@ import (
// 如果含有time.Time 请自行import time包
type SysDictionary struct {
global.GVA_MODEL
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英)
Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态
Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英)
Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态
Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述
ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID
Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典
SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"`
}

View File

@@ -8,12 +8,17 @@ import (
// 如果含有time.Time 请自行import time包
type SysDictionaryDetail struct {
global.GVA_MODEL
Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值
Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值
Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值
Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态
Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记
Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值
Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值
Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值
Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态
Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记
ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID
Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情
Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度从0开始
Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3"
Disabled bool `json:"disabled" gorm:"-"` // 禁用状态根据status字段动态计算
}
func (SysDictionaryDetail) TableName() string {

View File

@@ -16,7 +16,11 @@ func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.Route
dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail
}
{
dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail
dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表
dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail
dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表
dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构
dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构
dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情
dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径
}
}

View File

@@ -225,6 +225,40 @@ func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCode
entities = append(entities, createEntity...)
}
// 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据
existingPackageNames := make(map[string]bool)
// 收集所有存在的包名
for i := 0; i < len(server); i++ {
existingPackageNames[server[i].PackageName] = true
}
for i := 0; i < len(plugin); i++ {
existingPackageNames[plugin[i].PackageName] = true
}
// 找出需要删除的数据库记录
deleteEntityIDs := []uint{}
for i := 0; i < len(entities); i++ {
if !existingPackageNames[entities[i].PackageName] {
deleteEntityIDs = append(deleteEntityIDs, entities[i].ID)
}
}
// 删除数据库中不存在文件的记录
if len(deleteEntityIDs) > 0 {
err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error
if err != nil {
return nil, errors.Wrap(err, "删除不存在的包记录失败!")
}
// 从返回结果中移除已删除的记录
filteredEntities := []model.SysAutoCodePackage{}
for i := 0; i < len(entities); i++ {
if existingPackageNames[entities[i].PackageName] {
filteredEntities = append(filteredEntities, entities[i])
}
}
entities = filteredEntities
}
return entities, nil
}

View File

@@ -3,6 +3,9 @@ package system
import (
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/gin-gonic/gin"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"gorm.io/gorm"
@@ -60,10 +63,11 @@ func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary sy
func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) {
var dict system.SysDictionary
sysDictionaryMap := map[string]interface{}{
"Name": sysDictionary.Name,
"Type": sysDictionary.Type,
"Status": sysDictionary.Status,
"Desc": sysDictionary.Desc,
"Name": sysDictionary.Name,
"Type": sysDictionary.Type,
"Status": sysDictionary.Status,
"Desc": sysDictionary.Desc,
"ParentID": sysDictionary.ParentID,
}
err = global.GVA_DB.Where("id = ?", sysDictionary.ID).First(&dict).Error
if err != nil {
@@ -75,6 +79,14 @@ func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *s
return errors.New("存在相同的type不允许创建")
}
}
// 检查是否会形成循环引用
if sysDictionary.ParentID != nil && *sysDictionary.ParentID != 0 {
if err := dictionaryService.checkCircularReference(sysDictionary.ID, *sysDictionary.ParentID); err != nil {
return err
}
}
err = global.GVA_DB.Model(&dict).Updates(sysDictionaryMap).Error
return err
}
@@ -93,7 +105,7 @@ func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uin
flag = *status
}
err = global.GVA_DB.Where("(type = ? OR id = ?) and status = ?", Type, Id, flag).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", true).Order("sort")
return db.Where("status = ? and deleted_at is null", true).Order("sort")
}).First(&sysDictionary).Error
return
}
@@ -105,8 +117,38 @@ func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uin
//@param: info request.SysDictionarySearch
//@return: err error, list interface{}, total int64
func (dictionaryService *DictionaryService) GetSysDictionaryInfoList() (list interface{}, err error) {
func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) {
var sysDictionarys []system.SysDictionary
err = global.GVA_DB.Find(&sysDictionarys).Error
query := global.GVA_DB.WithContext(c)
if req.Name != "" {
query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%")
}
// 预加载子字典
query = query.Preload("Children")
err = query.Find(&sysDictionarys).Error
return sysDictionarys, err
}
// checkCircularReference 检查是否会形成循环引用
func (dictionaryService *DictionaryService) checkCircularReference(currentID uint, parentID uint) error {
if currentID == parentID {
return errors.New("不能将字典设置为自己的父级")
}
// 递归检查父级链条
var parent system.SysDictionary
err := global.GVA_DB.Where("id = ?", parentID).First(&parent).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil // 父级不存在,允许设置
}
return err
}
// 如果父级还有父级,继续检查
if parent.ParentID != nil && *parent.ParentID != 0 {
return dictionaryService.checkCircularReference(currentID, *parent.ParentID)
}
return nil
}

View File

@@ -1,6 +1,9 @@
package system
import (
"fmt"
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
@@ -17,6 +20,24 @@ type DictionaryDetailService struct{}
var DictionaryDetailServiceApp = new(DictionaryDetailService)
func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) {
// 计算层级和路径
if sysDictionaryDetail.ParentID != nil {
var parent system.SysDictionaryDetail
err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error
if err != nil {
return err
}
sysDictionaryDetail.Level = parent.Level + 1
if parent.Path == "" {
sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID))
} else {
sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
}
} else {
sysDictionaryDetail.Level = 0
sysDictionaryDetail.Path = ""
}
err = global.GVA_DB.Create(&sysDictionaryDetail).Error
return err
}
@@ -28,6 +49,16 @@ func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetai
//@return: err error
func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) {
// 检查是否有子项
var count int64
err = global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("parent_id = ?", sysDictionaryDetail.ID).Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return fmt.Errorf("该字典详情下还有子项,无法删除")
}
err = global.GVA_DB.Delete(&sysDictionaryDetail).Error
return err
}
@@ -39,8 +70,93 @@ func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetai
//@return: err error
func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetail(sysDictionaryDetail *system.SysDictionaryDetail) (err error) {
// 如果更新了父级ID需要重新计算层级和路径
if sysDictionaryDetail.ParentID != nil {
var parent system.SysDictionaryDetail
err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error
if err != nil {
return err
}
// 检查循环引用
if dictionaryDetailService.checkCircularReference(sysDictionaryDetail.ID, *sysDictionaryDetail.ParentID) {
return fmt.Errorf("不能将字典详情设置为自己或其子项的父级")
}
sysDictionaryDetail.Level = parent.Level + 1
if parent.Path == "" {
sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID))
} else {
sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
}
} else {
sysDictionaryDetail.Level = 0
sysDictionaryDetail.Path = ""
}
err = global.GVA_DB.Save(sysDictionaryDetail).Error
return err
if err != nil {
return err
}
// 更新所有子项的层级和路径
return dictionaryDetailService.updateChildrenLevelAndPath(sysDictionaryDetail.ID)
}
// checkCircularReference 检查循环引用
func (dictionaryDetailService *DictionaryDetailService) checkCircularReference(id, parentID uint) bool {
if id == parentID {
return true
}
var parent system.SysDictionaryDetail
err := global.GVA_DB.First(&parent, parentID).Error
if err != nil {
return false
}
if parent.ParentID == nil {
return false
}
return dictionaryDetailService.checkCircularReference(id, *parent.ParentID)
}
// updateChildrenLevelAndPath 更新子项的层级和路径
func (dictionaryDetailService *DictionaryDetailService) updateChildrenLevelAndPath(parentID uint) error {
var children []system.SysDictionaryDetail
err := global.GVA_DB.Where("parent_id = ?", parentID).Find(&children).Error
if err != nil {
return err
}
var parent system.SysDictionaryDetail
err = global.GVA_DB.First(&parent, parentID).Error
if err != nil {
return err
}
for _, child := range children {
child.Level = parent.Level + 1
if parent.Path == "" {
child.Path = strconv.Itoa(int(parent.ID))
} else {
child.Path = parent.Path + "," + strconv.Itoa(int(parent.ID))
}
err = global.GVA_DB.Save(&child).Error
if err != nil {
return err
}
// 递归更新子项的子项
err = dictionaryDetailService.updateChildrenLevelAndPath(child.ID)
if err != nil {
return err
}
}
return nil
}
//@author: [piexlmax](https://github.com/piexlmax)
@@ -79,6 +195,12 @@ func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetailIn
if info.SysDictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", info.SysDictionaryID)
}
if info.ParentID != nil {
db = db.Where("parent_id = ?", *info.ParentID)
}
if info.Level != nil {
db = db.Where("level = ?", *info.Level)
}
err = db.Count(&total).Error
if err != nil {
return
@@ -94,14 +216,135 @@ func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictio
return sysDictionaryDetails, err
}
// GetDictionaryTreeList 获取字典树形结构列表
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) {
var sysDictionaryDetails []system.SysDictionaryDetail
// 只获取顶级项目parent_id为空
err = global.GVA_DB.Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&sysDictionaryDetails).Error
if err != nil {
return nil, err
}
// 递归加载子项并设置disabled属性
for i := range sysDictionaryDetails {
// 设置disabled属性当status为false时disabled为true
if sysDictionaryDetails[i].Status != nil {
sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status
} else {
sysDictionaryDetails[i].Disabled = false // 默认不禁用
}
err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i])
if err != nil {
return nil, err
}
}
return sysDictionaryDetails, nil
}
// loadChildren 递归加载子项
func (dictionaryDetailService *DictionaryDetailService) loadChildren(detail *system.SysDictionaryDetail) error {
var children []system.SysDictionaryDetail
err := global.GVA_DB.Where("parent_id = ?", detail.ID).Order("sort").Find(&children).Error
if err != nil {
return err
}
for i := range children {
// 设置disabled属性当status为false时disabled为true
if children[i].Status != nil {
children[i].Disabled = !*children[i].Status
} else {
children[i].Disabled = false // 默认不禁用
}
err = dictionaryDetailService.loadChildren(&children[i])
if err != nil {
return err
}
}
detail.Children = children
return nil
}
// GetDictionaryDetailsByParent 根据父级ID获取字典详情
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryDetailsByParent(req request.GetDictionaryDetailsByParentRequest) (list []system.SysDictionaryDetail, err error) {
db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("sys_dictionary_id = ?", req.SysDictionaryID)
if req.ParentID != nil {
db = db.Where("parent_id = ?", *req.ParentID)
} else {
db = db.Where("parent_id IS NULL")
}
err = db.Order("sort").Find(&list).Error
if err != nil {
return list, err
}
// 设置disabled属性
for i := range list {
if list[i].Status != nil {
list[i].Disabled = !*list[i].Status
} else {
list[i].Disabled = false // 默认不禁用
}
}
// 如果需要包含子级数据,使用递归方式加载所有层级的子项
if req.IncludeChildren {
for i := range list {
err = dictionaryDetailService.loadChildren(&list[i])
if err != nil {
return list, err
}
}
}
return list, err
}
// 按照字典type获取字典全部内容的方法
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryListByType(t string) (list []system.SysDictionaryDetail, err error) {
var sysDictionaryDetails []system.SysDictionaryDetail
db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id")
err = db.Debug().Find(&sysDictionaryDetails, "type = ?", t).Error
err = db.Find(&sysDictionaryDetails, "type = ?", t).Error
return sysDictionaryDetails, err
}
// GetDictionaryTreeListByType 根据字典类型获取树形结构
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeListByType(t string) (list []system.SysDictionaryDetail, err error) {
var sysDictionaryDetails []system.SysDictionaryDetail
db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).
Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id").
Where("sys_dictionaries.type = ? AND sys_dictionary_details.parent_id IS NULL", t).
Order("sys_dictionary_details.sort")
err = db.Find(&sysDictionaryDetails).Error
if err != nil {
return nil, err
}
// 递归加载子项并设置disabled属性
for i := range sysDictionaryDetails {
// 设置disabled属性当status为false时disabled为true
if sysDictionaryDetails[i].Status != nil {
sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status
} else {
sysDictionaryDetails[i].Disabled = false // 默认不禁用
}
err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i])
if err != nil {
return nil, err
}
}
return sysDictionaryDetails, nil
}
// 按照字典id+字典内容value获取单条字典内容
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue(dictionaryID uint, value string) (detail system.SysDictionaryDetail, err error) {
var sysDictionaryDetail system.SysDictionaryDetail
@@ -116,3 +359,34 @@ func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeV
err = db.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error
return sysDictionaryDetails, err
}
// GetDictionaryPath 获取字典详情的完整路径
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPath(id uint) (path []system.SysDictionaryDetail, err error) {
var detail system.SysDictionaryDetail
err = global.GVA_DB.First(&detail, id).Error
if err != nil {
return nil, err
}
path = append(path, detail)
if detail.ParentID != nil {
parentPath, err := dictionaryDetailService.GetDictionaryPath(*detail.ParentID)
if err != nil {
return nil, err
}
path = append(parentPath, path...)
}
return path, nil
}
// GetDictionaryPathByValue 根据值获取字典详情的完整路径
func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPathByValue(dictionaryID uint, value string) (path []system.SysDictionaryDetail, err error) {
detail, err := dictionaryDetailService.GetDictionaryInfoByValue(dictionaryID, value)
if err != nil {
return nil, err
}
return dictionaryDetailService.GetDictionaryPath(detail.ID)
}

View File

@@ -139,6 +139,11 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/findSysDictionaryDetail", Description: "根据ID获取字典内容"},
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getSysDictionaryDetailList", Description: "获取字典内容列表"},
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeList", Description: "获取字典数列表"},
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeListByType", Description: "根据分类获取字典数列表"},
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryDetailsByParent", Description: "根据父级ID获取字典详情"},
{ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryPath", Description: "获取字典详情的完整路径"},
{ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/createSysDictionary", Description: "新增字典"},
{ApiGroup: "系统字典", Method: "DELETE", Path: "/sysDictionary/deleteSysDictionary", Description: "删除字典"},
{ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"},

View File

@@ -139,6 +139,10 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/createSysDictionaryDetail", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getSysDictionaryDetailList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/deleteSysDictionaryDetail", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeListByType", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryDetailsByParent", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryPath", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/findSysDictionary", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/updateSysDictionary", V2: "PUT"},

View File

@@ -219,14 +219,9 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string {
if field.FieldType == "array" {
multipleAttr = "multiple "
}
result += fmt.Sprintf(` <el-select %sv-model="searchInfo.%s" clearable filterable placeholder="请选择" @clear="()=>{searchInfo.%s=undefined}">
result += fmt.Sprintf(` <el-tree-select v-model="formData.%s" placeholder="请选择%s" :data="%sOptions" style="width:100%%" filterable :clearable="%v" check-strictly %s></el-tree-select>
`,
multipleAttr, field.FieldJson, field.FieldJson)
result += fmt.Sprintf(` <el-option v-for="(item,key) in %sOptions" :key="key" :label="item.label" :value="item.value" />
`,
field.DictType)
result += ` </el-select>
`
field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr)
} else if field.CheckDataSource {
multipleAttr := ""
if field.DataSource.Association == 2 {
@@ -488,14 +483,9 @@ func GenerateFormItem(field systemReq.AutoCodeField) string {
case "string":
if field.DictType != "" {
result += fmt.Sprintf(` <el-select v-model="formData.%s" placeholder="请选择%s" style="width:100%%" filterable :clearable="%v">
result += fmt.Sprintf(` <el-tree-select v-model="formData.%s" placeholder="请选择%s" :data="%sOptions" style="width:100%%" filterable :clearable="%v" check-strictly></el-tree-select>
`,
field.FieldJson, field.FieldDesc, field.Clearable)
result += fmt.Sprintf(` <el-option v-for="(item,key) in %sOptions" :key="key" :label="item.label" :value="item.value" />
`,
field.DictType)
result += ` </el-select>
`
field.FieldJson, field.FieldDesc, field.DictType, field.Clearable)
} else {
result += fmt.Sprintf(` <el-input v-model="formData.%s" :clearable="%v" placeholder="请输入%s" />
`,
@@ -710,7 +700,7 @@ func GenerateSearchField(field systemReq.AutoCodeField) string {
// 生成普通搜索字段
if field.FieldType == "enum" || field.FieldType == "picture" ||
field.FieldType == "pictures" || field.FieldType == "video" ||
field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" {
field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" || field.FieldType == "file" {
result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ",
field.FieldName, field.FieldJson, field.FieldJson)
} else {

View File

@@ -5,6 +5,7 @@ import (
"context"
"errors"
"io"
"mime"
"mime/multipart"
"path/filepath"
"strings"
@@ -76,12 +77,18 @@ func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string,
filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename
}
// 根据文件扩展名检测 MIME 类型
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
// 设置超时10分钟
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
defer cancel()
// Upload the file with PutObject 大文件自动切换为分片上传
info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: contentType})
if err != nil {
global.GVA_LOG.Error("上传文件到minio失败", zap.Any("err", err.Error()))
return "", "", errors.New("上传文件到minio失败, err:" + err.Error())

View File

@@ -4,7 +4,7 @@ VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
VITE_FILE_API = /api
VITE_BASE_PATH = http://127.0.0.1
VITE_POSITION = close
VITE_POSITION = open
VITE_EDITOR = code
// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm
// 如果使用docker-compose开发模式设置为下面的地址或本机主机IP

View File

@@ -1,8 +1,9 @@
{
"name": "gin-vue-admin",
"version": "2.8.5",
"version": "2.8.6",
"private": true,
"scripts": {
"dev": "node openDocument.js && vite --host --mode development",
"serve": "node openDocument.js && vite --host --mode development",
"build": "vite build --mode production",
"limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build",
@@ -14,6 +15,7 @@
"@element-plus/icons-vue": "^2.3.1",
"@form-create/designer": "^3.2.6",
"@form-create/element-ui": "^3.2.10",
"@iconify/vue": "^5.0.0",
"@unocss/transformer-directives": "^66.4.2",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
@@ -76,7 +78,6 @@
"globals": "^16.3.0",
"sass": "^1.78.0",
"terser": "^5.31.6",
"unocss": "^66.4.2",
"vite": "^6.2.3",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-importer": "^0.2.5",

View File

@@ -1,5 +1,8 @@
<template>
<div id="app" class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800">
<div
id="app"
class="bg-gray-50 text-slate-700 !dark:text-slate-500 dark:bg-slate-800"
>
<el-config-provider :locale="zhCn">
<router-view />
<Application />
@@ -8,36 +11,36 @@
</template>
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import Application from '@/components/application/index.vue'
import { useAppStore } from '@/pinia'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import Application from '@/components/application/index.vue'
import { useAppStore } from '@/pinia'
useAppStore()
defineOptions({
name: 'App'
})
useAppStore()
defineOptions({
name: 'App'
})
</script>
<style lang="scss">
// 引入初始化样式
#app {
height: 100vh;
overflow: hidden;
font-weight: 400 !important;
}
// 引入初始化样式
#app {
height: 100vh;
overflow: hidden;
font-weight: 400 !important;
}
.el-button {
font-weight: 400 !important;
}
.el-button {
font-weight: 400 !important;
}
.gva-body-h {
min-height: calc(100% - 3rem);
}
.gva-body-h {
min-height: calc(100% - 3rem);
}
.gva-container {
height: calc(100% - 2.5rem);
}
.gva-container {
height: calc(100% - 2.5rem);
}
.gva-container2 {
height: calc(100% - 4.5rem);
}
.gva-container2 {
height: calc(100% - 4.5rem);
}
</style>

View File

@@ -78,3 +78,68 @@ export const getSysDictionaryDetailList = (params) => {
params
})
}
// @Tags SysDictionaryDetail
// @Summary 获取层级字典详情树形结构根据字典ID
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param sysDictionaryID query string true "字典ID"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysDictionaryDetail/getDictionaryTreeList [get]
export const getDictionaryTreeList = (params) => {
return service({
url: '/sysDictionaryDetail/getDictionaryTreeList',
method: 'get',
params
})
}
// @Tags SysDictionaryDetail
// @Summary 获取层级字典详情树形结构(根据字典类型)
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param dictType query string true "字典类型"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get]
export const getDictionaryTreeListByType = (params) => {
return service({
url: '/sysDictionaryDetail/getDictionaryTreeListByType',
method: 'get',
params
})
}
// @Tags SysDictionaryDetail
// @Summary 根据父级ID获取字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param parentID query string true "父级ID"
// @Param includeChildren query boolean false "是否包含子级"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get]
export const getDictionaryDetailsByParent = (params) => {
return service({
url: '/sysDictionaryDetail/getDictionaryDetailsByParent',
method: 'get',
params
})
}
// @Tags SysDictionaryDetail
// @Summary 获取字典详情的完整路径
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param ID query string true "字典详情ID"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysDictionaryDetail/getDictionaryPath [get]
export const getDictionaryPath = (params) => {
return service({
url: '/sysDictionaryDetail/getDictionaryPath',
method: 'get',
params
})
}

View File

@@ -1,13 +1,13 @@
<template>
<div
class="fixed inset-0 bg-black/40 flex items-center justify-center z-[999]"
class="fixed inset-0 bg-black/40 dark:bg-black/60 flex items-center justify-center z-[999]"
@click.self="closeModal"
>
<div class="bg-white rounded-xl shadow-dialog w-full max-w-md mx-4 transform transition-all duration-300 ease-in-out">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-dialog dark:shadow-lg w-full max-w-md mx-4 transform transition-all duration-300 ease-in-out border border-transparent dark:border-gray-700">
<!-- 弹窗头部 -->
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-800">{{ displayData.title }}</h3>
<div class="text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" @click="closeModal">
<div class="p-5 border-b border-gray-100 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">{{ displayData.title }}</h3>
<div class="text-gray-400 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-200 transition-colors cursor-pointer" @click="closeModal">
<close class="h-6 w-6" />
</div>
</div>
@@ -16,36 +16,36 @@
<div class="p-6 pt-0">
<!-- 错误类型 -->
<div class="mb-4">
<div class="text-xs font-medium text-gray-500 uppercase mb-2">错误类型</div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">错误类型</div>
<div class="flex items-center gap-2">
<lock v-if="displayData.icon === 'lock'" class="text-red-500 w-5 h-5" />
<warn v-if="displayData.icon === 'warn'" class="text-red-500 w-5 h-5" />
<server v-if="displayData.icon === 'server'" class="text-red-500 w-5 h-5" />
<span class="font-medium text-gray-800">{{ displayData.type }}</span>
<lock v-if="displayData.icon === 'lock'" :class="['w-5 h-5', displayData.color]" />
<warn v-if="displayData.icon === 'warn'" :class="['w-5 h-5', displayData.color]" />
<server v-if="displayData.icon === 'server'" :class="['w-5 h-5', displayData.color]" />
<span class="font-medium text-gray-800 dark:text-gray-100">{{ displayData.type }}</span>
</div>
</div>
<!-- 具体错误 -->
<div class="mb-6">
<div class="text-xs font-medium text-gray-500 uppercase mb-2">具体错误</div>
<div class="bg-gray-100 rounded-lg p-3 text-sm text-gray-700 leading-relaxed">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">具体错误</div>
<div class="bg-gray-100 dark:bg-gray-900/40 rounded-lg p-3 text-sm text-gray-700 dark:text-gray-200 leading-relaxed">
{{ displayData.message }}
</div>
</div>
<!-- 提示信息 -->
<div v-if="displayData.tips">
<div class="text-xs font-medium text-gray-500 uppercase mb-2">提示</div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">提示</div>
<div class="flex items-center gap-2">
<idea class="text-blue-500 w-5 h-5" />
<p class="text-sm text-gray-600">{{ displayData.tips }}</p>
<idea class="text-blue-500 dark:text-blue-400 w-5 h-5" />
<p class="text-sm text-gray-600 dark:text-gray-300">{{ displayData.tips }}</p>
</div>
</div>
</div>
<!-- 弹窗底部 -->
<div class="py-2 px-4 border-t border-gray-100 flex justify-end">
<div class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm cursor-pointer" @click="handleConfirm">
<div class="py-2 px-4 border-t border-gray-100 dark:border-gray-700 flex justify-end">
<div class="px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white dark:text-gray-100 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors font-medium text-sm shadow-sm cursor-pointer" @click="handleConfirm">
确定
</div>
</div>
@@ -70,28 +70,28 @@ const presetErrors = {
title: '检测到接口错误',
type: '服务器发生内部错误',
icon: 'server',
color: 'text-red-500',
color: 'text-red-500 dark:text-red-400',
tips: '此类错误内容常见于后台panic请先查看后台日志如果影响您正常使用可强制登出清理缓存'
},
404: {
title: '资源未找到',
type: 'Not Found',
icon: 'warn',
color: 'text-orange-500',
color: 'text-orange-500 dark:text-orange-400',
tips: '此类错误多为接口未注册或未重启或者请求路径方法与api路径方法不符--如果为自动化代码请检查是否存在空格'
},
401: {
title: '身份认证失败',
type: '身份令牌无效',
icon: 'lock',
color: 'text-purple-500',
color: 'text-purple-500 dark:text-purple-400',
tips: '您的身份认证已过期或无效,请重新登录。'
},
'network': {
title: '网络错误',
type: 'Network Error',
icon: 'fa-wifi-slash',
color: 'text-gray-500',
color: 'text-gray-500 dark:text-gray-400',
tips: '无法连接到服务器,请检查您的网络连接。'
}
};
@@ -109,7 +109,7 @@ const displayData = computed(() => {
title: '未知错误',
type: '检测到请求错误',
icon: 'fa-question-circle',
color: 'text-gray-400',
color: 'text-gray-400 dark:text-gray-300',
message: props.errorData.message || '发生了一个未知错误。',
tips: '请检查控制台获取更多信息。'
};

View File

@@ -0,0 +1,82 @@
<script setup>
import { ref, watchEffect } from 'vue';
import { useAppStore } from '@/pinia/modules/app.js';
import { storeToRefs } from 'pinia';
const props = defineProps({
// logo 尺寸,单位为:rem
size: {
type: Number,
default: 2
}
})
const darkLogoPath = '/logo-dark.png';
const lightLogoPath = '/logo.png';
const appStore = useAppStore();
const { isDark } = storeToRefs(appStore);
const logoSrc = ref('');
const showTextPlaceholder = ref(false);
// 检查图片是否存在
function checkImageExists(url) {
return new Promise((resolve) => {
const tryToLoad = new Image();
tryToLoad.onload = () => resolve(true);
tryToLoad.onerror = () => resolve(false);
tryToLoad.src = url;
});
}
watchEffect(async () => {
showTextPlaceholder.value = false; // 重置占位符状态
// 暗色模式直接 load可以省一次亮色的 load
if (isDark.value && await checkImageExists(darkLogoPath)) {
logoSrc.value = darkLogoPath;
return;
}
if (await checkImageExists(lightLogoPath)) {
logoSrc.value = lightLogoPath;
return
}
// 到这里就包没有提供两种 logo 了
showTextPlaceholder.value = true;
console.error(
'错误: 在公共目录中找不到logo.png或logo-dark.png。'
);
console.warn(
'解决方案: 请在您的公共目录(/public)中放置logo.png和/或logo-dark.png文件或确保路径正确。'
);
});
// 直接用 16px 作为默认的基准大小
const SPACING = 16
function getSize() {
return {
width: `${props.size * SPACING}px`,
height: `${props.size * SPACING}px`,
}
}
</script>
<template>
<img v-if="!showTextPlaceholder && logoSrc" :src="logoSrc" :alt="$GIN_VUE_ADMIN.appName" class="object-contain"
:style="{
...getSize()
}" :class="{
'filter invert-[90%] hue-rotate-180 brightness-110':
isDark && logoSrc === '/logo.png',
}" />
<div v-else-if="showTextPlaceholder"
class="rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-200 font-bold text-xs"
:style="{
...getSize()
}">
GVA
</div>
</template>

View File

@@ -1,32 +1,44 @@
<template>
<svg :class="svgClass" v-bind="$attrs" :color="color">
<use :xlink:href="'#' + name" rel="external nofollow" />
</svg>
<template v-if="localIcon">
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
<use :xlink:href="'#' + localIcon" rel="external nofollow" />
</svg>
</template>
<template v-else-if="icon">
<Icon :icon="icon" v-bind="bindAttrs" />
</template>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String,
default: 'currentColor'
}
})
const svgClass = computed(() => {
if (props.name) {
return `svg-icon ${props.name}`
}
return 'svg-icon'
})
</script>
<style scoped>
.svg-icon {
@apply w-4 h-4;
fill: currentColor;
vertical-align: middle;
<script setup>
import { computed, useAttrs } from 'vue';
import { Icon } from '@iconify/vue'
/**
* 使用示例:
* 本地图标(所有可用的本地图标见控制台输出):
* <SvgIcon localIcon="lock" class="text-red-500 text-3xl" />
*
* 在线图标相关查询网站https://icones.js.org/ 或是https://icon-sets.iconify.design/
* <SvgIcon icon="mingcute:love-fill" class="text-red-500 text-3xl" />
*/
defineProps({
// 通过 symbol id 使用本地注册的 svg 图标
localIcon: {
type: String,
required: false,
default: ''
},
// Iconify 图标名称, 例如: 'mdi:home'
icon: {
type: String,
required: false,
default: ''
}
</style>
})
const attrs = useAttrs();
const bindAttrs = computed(() => ({
class: (attrs.class) || '',
style: (attrs.style) || ''
}))
</script>

View File

@@ -7,7 +7,6 @@ const greenText = (text) => `\x1b[32m${text}\x1b[0m`
export const config = {
appName: 'Gin-Vue-Admin',
appLogo: 'logo.png',
showViteLogo: true,
KeepAliveTabs: true,
logs: []

View File

@@ -0,0 +1,51 @@
function sendErrorTip(errorInfo) {
console.groupCollapsed(`捕获到错误: ${errorInfo.type}`);
console.log('错误类型:', errorInfo.type);
console.log('错误信息:', errorInfo.message);
console.log('调用栈:', errorInfo.stack);
if (errorInfo.component) {
console.log('组件名:', errorInfo.component.name)
console.log('组件地址:', errorInfo.component.__file)
}
if (errorInfo.vueInfo) console.log('Vue 信息:', errorInfo.vueInfo);
if (errorInfo.source) console.log('来源文件:', errorInfo.source);
if (errorInfo.lineno) console.log('行号:', errorInfo.lineno);
if (errorInfo.colno) console.log('列号:', errorInfo.colno);
console.groupEnd();
}
function initVueErrorHandler(app) {
app.config.errorHandler = (err, vm, info) => {
let errorType = 'Vue Error';
sendErrorTip({
type: errorType,
message: err.message,
stack: err.stack,
component: vm.$options || 'Unknown Vue Component',
vueInfo: info
});
};
}
function initJsErrorHandler() {
window.onerror = (message, source, lineno, colno, error) => {
let errorType = 'JS Error';
sendErrorTip({
type: errorType,
message: message,
stack: error ? error.stack : 'No stack available',
source: source,
lineno: lineno,
colno: colno
});
return false;
};
}
export function initErrorHandler(app) {
initVueErrorHandler(app)
initJsErrorHandler()
}

View File

@@ -10,7 +10,7 @@ const createIconComponent = (name) => ({
name: 'SvgIcon',
render() {
return h(svgIcon, {
name: name
localIcon: name
})
}
})
@@ -21,6 +21,7 @@ const registerIcons = async (app) => {
'@/plugin/**/assets/icons/**/*.svg'
) // 插件目录 svg 图标
const mergedIconModules = Object.assign({}, iconModules, pluginIconModules) // 合并所有 svg 图标
let allKeys = []
for (const path in mergedIconModules) {
let pluginName = ''
if (path.startsWith('/src/plugin/')) {
@@ -36,16 +37,19 @@ const registerIcons = async (app) => {
continue
}
const key = `${pluginName}${iconName}`
// 开发模式下列出所有 svg 图标,方便开发者直接查找复制使用
import.meta.env.MODE == 'development' &&
console.log(`svg-icon-component: <${key} />`)
const iconComponent = createIconComponent(key)
config.logs.push({
key: key,
label: key
})
app.component(key, iconComponent)
// 开发模式下列出所有 svg 图标,方便开发者直接查找复制使用
allKeys.push(key)
}
import.meta.env.MODE == 'development' &&
console.log(`所有可用的本地图标: ${allKeys.join(', ')}`)
}
export const register = (app) => {

View File

@@ -0,0 +1,43 @@
export default {
install: (app) => {
app.directive('click-outside', {
mounted(el, binding) {
const handler = (e) => {
// 如果绑定的元素包含事件目标,或元素已经被移除,则不触发
if (!el || el.contains(e.target) || e.target === el) return
// 支持函数或对象 { handler: fn, exclude: [el1, el2], capture: true }
const value = binding.value
if (value && typeof value === 'object') {
if (
value.exclude &&
value.exclude.some(
(ex) => ex && ex.contains && ex.contains(e.target)
)
)
return
if (typeof value.handler === 'function') value.handler(e)
} else if (typeof value === 'function') {
value(e)
}
}
// 存到 el 上,便于解绑
el.__clickOutsideHandler__ = handler
// 延迟注册,避免 mounted 时触发(比如当点击就是触发绑定动作时)
setTimeout(() => {
document.addEventListener('mousedown', handler)
document.addEventListener('touchstart', handler)
}, 0)
},
unmounted(el) {
const h = el.__clickOutsideHandler__
if (h) {
document.removeEventListener('mousedown', h)
document.removeEventListener('touchstart', h)
delete el.__clickOutsideHandler__
}
}
})
}
}

View File

@@ -1,6 +1,6 @@
import './style/element_visiable.scss'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'uno.css';
import 'uno.css'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
@@ -12,11 +12,24 @@ import router from '@/router/index'
import '@/permission'
import run from '@/core/gin-vue-admin.js'
import auth from '@/directive/auth'
import clickOutSide from '@/directive/clickOutSide'
import { store } from '@/pinia'
import App from './App.vue'
// import { initErrorHandler } from '@/core/error-handel'
const app = createApp(App)
// 注入错误处理捕获
// initErrorHandler(app)
app.config.productionTip = false
app.use(run).use(ElementPlus).use(store).use(auth).use(router).mount('#app')
app
.use(run)
.use(ElementPlus)
.use(store)
.use(auth)
.use(clickOutSide)
.use(router)
.mount('#app')
export default app

View File

@@ -19,8 +19,27 @@ function isExternalUrl(val) {
return typeof val === 'string' && /^(https?:)?\/\//.test(val)
}
// 将 n 级菜单扁平化为:一级 layout + 二级页面组件
function addRouteByChildren(route, segments = []) {
// 工具函数:统一路径归一化
function normalizeAbsolutePath(p) {
const s = '/' + String(p || '')
return s.replace(/\/+/g, '/')
}
function normalizeRelativePath(p) {
return String(p || '').replace(/^\/+/, '')
}
// 安全注册:仅在路由名未存在时注册顶级路由
function addTopLevelIfAbsent(r) {
if (!router.hasRoute(r.name)) {
router.addRoute(r)
}
}
// 将 n 级菜单扁平化为:
// - 常规:一级 layout + 二级页面组件
// - 若某节点 meta.defaultMenu === true该节点为顶级不包裹在 layout 下),其子节点作为该顶级的二级页面组件
function addRouteByChildren(route, segments = [], parentName = null) {
// 跳过外链根节点
if (isExternalUrl(route?.path) || isExternalUrl(route?.name) || isExternalUrl(route?.component)) {
return
@@ -28,26 +47,54 @@ function addRouteByChildren(route, segments = []) {
// 顶层 layout 仅用于承载,不参与路径拼接
if (route?.name === 'layout') {
route.children?.forEach((child) => addRouteByChildren(child, []))
route.children?.forEach((child) => addRouteByChildren(child, [], null))
return
}
// 如果标记为 defaultMenu则该路由应作为顶级路由不包裹在 layout 下)
if (route?.meta?.defaultMenu === true && parentName === null) {
const fullPath = [...segments, route.path].filter(Boolean).join('/')
const children = route.children ? [...route.children] : []
const newRoute = { ...route, path: fullPath }
delete newRoute.children
delete newRoute.parent
// 顶级路由使用绝对路径
newRoute.path = normalizeAbsolutePath(newRoute.path)
// 若已存在同名路由则整体跳过(之前应已处理过其子节点)
if (router.hasRoute(newRoute.name)) return
addTopLevelIfAbsent(newRoute)
// 若该 defaultMenu 节点仍有子节点,继续递归处理其子节点(挂载到该顶级路由下)
if (children.length) {
// 重置片段,使其成为顶级下的二级相对路径
children.forEach((child) => addRouteByChildren(child, [], newRoute.name))
}
return
}
// 还有子节点,继续向下收集路径片段(忽略外链片段)
if (route?.children && route.children.length) {
const nextSegments = isExternalUrl(route.path) ? segments : [...segments, route.path]
route.children.forEach((child) => addRouteByChildren(child, nextSegments))
route.children.forEach((child) => addRouteByChildren(child, nextSegments, parentName))
return
}
// 叶子节点:注册为 layout 的二级子路由
// 叶子节点:注册为其父defaultMenu 顶级或 layout的二级子路由
const fullPath = [...segments, route.path].filter(Boolean).join('/')
const newRoute = { ...route, path: fullPath }
delete newRoute.children
delete newRoute.parent
// 子路由使用相对路径,避免 /layout/layout/... 的问题
newRoute.path = newRoute.path.replace(/^\/+/, '')
newRoute.path = normalizeRelativePath(newRoute.path)
router.addRoute('layout', newRoute)
if (parentName) {
// 挂载到 defaultMenu 顶级路由下
router.addRoute(parentName, newRoute)
} else {
// 常规:挂载到 layout 下
router.addRoute('layout', newRoute)
}
}
// 处理路由加载
@@ -60,7 +107,8 @@ const setupRouter = async (userStore) => {
const baseRouters = routerStore.asyncRouters || []
const layoutRoute = baseRouters[0]
if (layoutRoute?.name === 'layout' && !router.hasRoute('layout')) {
router.addRoute(layoutRoute)
const bareLayout = { ...layoutRoute, children: [] }
router.addRoute(bareLayout)
}
// 扁平化:将 layout.children 与其余顶层异步路由一并作为二级子路由注册到 layout 下
@@ -73,7 +121,7 @@ const setupRouter = async (userStore) => {
if (r?.name !== 'layout') toRegister.push(r)
})
}
toRegister.forEach((r) => addRouteByChildren(r, []))
toRegister.forEach((r) => addRouteByChildren(r, [], null))
return true
} catch (error) {
console.error('Setup router failed:', error)

View File

@@ -1,4 +1,5 @@
import { findSysDictionary } from '@/api/sysDictionary'
import { getDictionaryTreeListByType } from '@/api/sysDictionaryDetail'
import { defineStore } from 'pinia'
import { ref } from 'vue'
@@ -10,25 +11,235 @@ export const useDictionaryStore = defineStore('dictionary', () => {
dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes }
}
const getDictionary = async (type) => {
if (dictionaryMap.value[type] && dictionaryMap.value[type].length) {
return dictionaryMap.value[type]
// 过滤树形数据的深度
const filterTreeByDepth = (items, currentDepth, targetDepth) => {
if (targetDepth === 0) {
// depth=0 返回全部数据
return items
}
if (currentDepth >= targetDepth) {
// 达到目标深度移除children
return items.map((item) => ({
label: item.label,
value: item.value,
extend: item.extend
}))
}
// 递归处理子项
return items.map((item) => ({
label: item.label,
value: item.value,
extend: item.extend,
children: item.children
? filterTreeByDepth(item.children, currentDepth + 1, targetDepth)
: undefined
}))
}
// 将树形结构扁平化为数组(用于兼容原有的平铺格式)
const flattenTree = (items) => {
const result = []
const traverse = (nodes) => {
nodes.forEach((item) => {
result.push({
label: item.label,
value: item.value,
extend: item.extend
})
if (item.children && item.children.length > 0) {
traverse(item.children)
}
})
}
traverse(items)
return result
}
// 标准化树形数据,确保每个节点都包含标准的字段格式
const normalizeTreeData = (items) => {
return items.map((item) => ({
label: item.label,
value: item.value,
extend: item.extend,
children:
item.children && item.children.length > 0
? normalizeTreeData(item.children)
: undefined
}))
}
// 根据value和depth查找指定节点并返回其children
const findNodeByValue = (
items,
targetValue,
currentDepth = 1,
maxDepth = 0
) => {
for (const item of items) {
// 如果找到目标value的节点
if (item.value === targetValue) {
// 如果maxDepth为0返回所有children
if (maxDepth === 0) {
return item.children ? normalizeTreeData(item.children) : []
}
// 否则根据depth限制返回children
if (item.children && item.children.length > 0) {
return filterTreeByDepth(item.children, 1, maxDepth)
}
return []
}
// 如果当前深度小于最大深度继续在children中查找
if (
item.children &&
item.children.length > 0 &&
(maxDepth === 0 || currentDepth < maxDepth)
) {
const result = findNodeByValue(
item.children,
targetValue,
currentDepth + 1,
maxDepth
)
if (result !== null) {
return result
}
}
}
return null
}
const getDictionary = async (type, depth = 0, value = null) => {
// 如果传入了value参数则查找指定节点的children
if (value !== null) {
// 构建缓存key包含value和depth信息
const cacheKey = `${type}_value_${value}_depth_${depth}`
if (
dictionaryMap.value[cacheKey] &&
dictionaryMap.value[cacheKey].length
) {
return dictionaryMap.value[cacheKey]
}
try {
// 获取完整的树形结构数据
const treeRes = await getDictionaryTreeListByType({ type })
if (
treeRes.code === 0 &&
treeRes.data &&
treeRes.data.list &&
treeRes.data.list.length > 0
) {
// 查找指定value的节点并返回其children
const targetNodeChildren = findNodeByValue(
treeRes.data.list,
value,
1,
depth
)
if (targetNodeChildren !== null) {
let resultData
if (depth === 0) {
// depth=0 时返回完整的children树形结构
resultData = targetNodeChildren
} else {
// 其他depth值扁平化children数据
resultData = flattenTree(targetNodeChildren)
}
const dictionaryRes = {}
dictionaryRes[cacheKey] = resultData
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[cacheKey]
} else {
// 如果没找到指定value的节点返回空数组
return []
}
}
} catch (error) {
console.error('根据value获取字典数据失败:', error)
return []
}
}
// 原有的逻辑不传value参数时的处理
// 构建缓存key包含depth信息
const cacheKey = depth === 0 ? `${type}_tree` : `${type}_depth_${depth}`
if (dictionaryMap.value[cacheKey] && dictionaryMap.value[cacheKey].length) {
return dictionaryMap.value[cacheKey]
} else {
const res = await findSysDictionary({ type })
if (res.code === 0) {
const dictionaryRes = {}
const dict = []
res.data.resysDictionary.sysDictionaryDetails &&
res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
dict.push({
label: item.label,
value: item.value,
extend: item.extend
try {
// 首先尝试获取树形结构数据
const treeRes = await getDictionaryTreeListByType({ type })
if (
treeRes.code === 0 &&
treeRes.data &&
treeRes.data.list &&
treeRes.data.list.length > 0
) {
// 使用树形结构数据
const treeData = treeRes.data.list
let resultData
if (depth === 0) {
// depth=0 时返回完整的树形结构,但要确保字段格式标准化
resultData = normalizeTreeData(treeData)
} else {
// 其他depth值根据depth参数过滤数据然后扁平化
const filteredData = filterTreeByDepth(treeData, 1, depth)
resultData = flattenTree(filteredData)
}
const dictionaryRes = {}
dictionaryRes[cacheKey] = resultData
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[cacheKey]
} else {
// 如果没有树形数据,回退到原有的平铺方式
const res = await findSysDictionary({ type })
if (res.code === 0) {
const dictionaryRes = {}
const dict = []
res.data.resysDictionary.sysDictionaryDetails &&
res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
dict.push({
label: item.label,
value: item.value,
extend: item.extend
})
})
dictionaryRes[cacheKey] = dict
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[cacheKey]
}
}
} catch (error) {
console.error('获取字典数据失败:', error)
// 发生错误时回退到原有方式
const res = await findSysDictionary({ type })
if (res.code === 0) {
const dictionaryRes = {}
const dict = []
res.data.resysDictionary.sysDictionaryDetails &&
res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
dict.push({
label: item.label,
value: item.value,
extend: item.extend
})
})
})
dictionaryRes[res.data.resysDictionary.type] = dict
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[type]
dictionaryRes[cacheKey] = dict
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[cacheKey]
}
}
}
}

View File

@@ -1,136 +1,137 @@
/* Document
========================================================================== */
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`
*/
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
'Noto Sans',
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji'; /* 4 */
// TODO: 在下一个大版本更新的时候需要改回正确的16px
font-size: 14px;
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
margin: 0; /* 1 */
line-height: inherit; /* 2 */
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
abbr:where([title]) {
text-decoration: underline dotted;
}
/* Text-level semantics
========================================================================== */
/*
Remove the default font size and weight for headings.
*/
/**
* Remove the gray background on active links in IE 10.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
background-color: transparent;
color: inherit;
text-decoration: inherit;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
samp,
pre {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
@@ -148,322 +149,233 @@ sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
font-family: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
-webkit-appearance: button; /* 1 */
/* background-color: transparent; */
background-image: none; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
/*
Use the modern Firefox focus style for all focusable elements.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
:-moz-focusring {
outline: auto;
}
/**
* Restore the focus styles unset by the previous rule.
*/
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
:-moz-ui-invalid {
box-shadow: none;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
* Add the correct display in all browsers.
*/
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/*
Removes the default spacing and border for appropriate elements.
*/
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
HTML,
body,
div,
ul,
ol,
dl,
li,
dt,
dd,
p,
blockquote,
pre,
form,
fieldset,
table,
th,
td {
// border: none;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-size: 14px;
margin: 0px;
padding: 0px;
}
html,
body {
height: 100%;
width: 100%;
}
address,
caption,
cite,
code,
th,
var {
font-style: normal;
font-weight: normal;
}
a {
text-decoration: none;
}
input::-ms-clear {
display: none;
}
input::-ms-reveal {
display: none;
}
input {
-webkit-appearance: none;
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
outline: none;
padding: 0;
}
input::-webkit-input-placeholder {
color: #ccc;
legend {
padding: 0;
}
input::-ms-input-placeholder {
color: #ccc;
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
input::-moz-placeholder {
color: #ccc;
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
input[type='submit'],
input[type='button'] {
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: #9ca3af; /* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role='button'] {
cursor: pointer;
}
button[disabled],
input[disabled] {
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
img {
border: none;
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
ul,
ol,
li {
list-style-type: none;
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}

View File

@@ -1,9 +1,76 @@
import { useDictionaryStore } from '@/pinia/modules/dictionary'
// 获取字典方法 使用示例 getDict('sex').then(res) 或者 async函数下 const res = await getDict('sex')
export const getDict = async (type) => {
const dictionaryStore = useDictionaryStore()
await dictionaryStore.getDictionary(type)
return dictionaryStore.dictionaryMap[type]
/**
* 生成字典缓存key
* @param {string} type - 字典类型
* @param {number} depth - 深度参数
* @param {string|number|null} value - 指定节点的value
* @returns {string} 缓存key
*/
const generateCacheKey = (type, depth, value) => {
if (value !== null && value !== undefined) {
return `${type}_value_${value}_depth_${depth}`
}
return depth === 0 ? `${type}_tree` : `${type}_depth_${depth}`
}
/**
* 获取字典数据
* @param {string} type - 字典类型,必填
* @param {Object} options - 可选参数
* @param {number} options.depth - 指定获取字典的深度默认为0完整树形结构
* @param {string|number|null} options.value - 指定节点的value获取该节点的children默认为null
* @returns {Promise<Array>} 字典数据数组
* @example
* // 获取完整的字典树形结构
* const dictTree = await getDict('user_status')
*
* // 获取指定深度的扁平化字典数据
* const dictFlat = await getDict('user_status', {
* depth: 2
* })
*
* // 获取指定节点的children
* const children = await getDict('user_status', {
* value: 'active'
* })
*/
export const getDict = async (
type,
options = {
depth: 0,
value: null
}
) => {
// 参数验证
if (!type || typeof type !== 'string') {
console.warn('getDict: type参数必须是非空字符串')
return []
}
if (typeof options.depth !== 'number' || options.depth < 0) {
console.warn('getDict: depth参数必须是非负数')
options.depth = 0
}
try {
const dictionaryStore = useDictionaryStore()
// 调用store方法获取字典数据
await dictionaryStore.getDictionary(type, options.depth, options.value)
// 生成缓存key
const cacheKey = generateCacheKey(type, options.depth, options.value)
// 从缓存中获取数据
const result = dictionaryStore.dictionaryMap[cacheKey]
// 返回数据,确保返回数组
return Array.isArray(result) ? result : []
} catch (error) {
console.error('getDict: 获取字典数据失败', { type, options, error })
return []
}
}
// 字典文字展示方法

View File

@@ -19,18 +19,57 @@ export const formatDate = (time) => {
}
export const filterDict = (value, options) => {
const rowLabel = options && options.filter((item) => item.value === value)
return rowLabel && rowLabel[0] && rowLabel[0].label
// 递归查找函数
const findInOptions = (opts, targetValue) => {
if (!opts || !Array.isArray(opts)) return null
for (const item of opts) {
if (item.value === targetValue) {
return item
}
if (item.children && Array.isArray(item.children)) {
const found = findInOptions(item.children, targetValue)
if (found) return found
}
}
return null
}
const rowLabel = findInOptions(options, value)
return rowLabel && rowLabel.label
}
export const filterDataSource = (dataSource, value) => {
// 递归查找函数
const findInDataSource = (data, targetValue) => {
if (!data || !Array.isArray(data)) return null
for (const item of data) {
// 检查当前项是否匹配
if (item.value === targetValue) {
return item
}
// 如果有children属性递归查找
if (item.children && Array.isArray(item.children)) {
const found = findInDataSource(item.children, targetValue)
if (found) return found
}
}
return null
}
if (Array.isArray(value)) {
return value.map((item) => {
const rowLabel = dataSource && dataSource.find((i) => i.value === item)
const rowLabel = findInDataSource(dataSource, item)
return rowLabel?.label
})
}
const rowLabel = dataSource && dataSource.find((item) => item.value === value)
const rowLabel = findInDataSource(dataSource, value)
return rowLabel?.label
}

View File

@@ -124,7 +124,8 @@ service.interceptors.request.use(
)
function getErrorMessage(error) {
return error.response?.data?.msg || '请求失败'
// 优先级: 响应体中的 msg > statusText > 默认消息
return error.response?.data?.msg || error.response?.statusText || '请求失败'
}
// http response 拦截器

View File

@@ -1,26 +1,46 @@
<template>
<div v-loading.fullscreen.lock="fullscreenLoading">
<div class="flex gap-4 p-2">
<div class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4">
<div class="flex gap-4 pt-2">
<div
class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
>
<el-scrollbar style="height: calc(100vh - 300px)">
<el-tree
:data="categories"
node-key="id"
:props="defaultProps"
@node-click="handleNodeClick"
default-expand-all
:data="categories"
node-key="id"
:props="defaultProps"
@node-click="handleNodeClick"
default-expand-all
>
<template #default="{ node, data }">
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
<div
class="w-36"
:class="
search.classId === data.ID ? 'text-blue-500 font-bold' : ''
"
>
{{ data.name }}
</div>
<el-dropdown>
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
<el-icon class="ml-3 text-right" v-if="data.ID > 0"
><MoreFilled
/></el-icon>
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
<el-dropdown-item @click="addCategoryFun(data)"
>添加分类</el-dropdown-item
>
<el-dropdown-item
@click="editCategory(data)"
v-if="data.ID > 0"
>编辑分类</el-dropdown-item
>
<el-dropdown-item
@click="deleteCategoryFun(data.ID)"
v-if="data.ID > 0"
>删除分类</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -28,98 +48,118 @@
</el-tree>
</el-scrollbar>
</div>
<div class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900">
<div
class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900"
>
<div class="gva-table-box mt-0 mb-0">
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
<warning-bar
title="点击“文件名”可以编辑;选择的类别即是上传的类别。"
/>
<div class="gva-btn-list gap-3">
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
<upload-common
:image-common="imageCommon"
:classId="search.classId"
@on-success="onSuccess"
/>
<cropper-image :classId="search.classId" @on-success="onSuccess" />
<QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
<upload-image
:image-url="imageUrl"
:file-size="512"
:max-w-h="1080"
:classId="search.classId"
@on-success="onSuccess"
:image-url="imageUrl"
:file-size="512"
:max-w-h="1080"
:classId="search.classId"
@on-success="onSuccess"
/>
<el-button type="primary" icon="upload" @click="importUrlFunc">
导入URL
</el-button>
<el-input
v-model="search.keyword"
class="w-72"
placeholder="请输入文件名或备注"
v-model="search.keyword"
class="w-72"
placeholder="请输入文件名或备注"
/>
<el-button type="primary" icon="search" @click="onSubmit"
>查询
</el-button
>
>查询
</el-button>
</div>
<el-table :data="tableData">
<el-table-column align="left" label="预览" width="100">
<template #default="scope">
<CustomPic pic-type="file" :pic-src="scope.row.url" preview/>
<CustomPic pic-type="file" :pic-src="scope.row.url" preview />
</template>
</el-table-column>
<el-table-column align="left" label="日期" prop="UpdatedAt" width="180">
<el-table-column
align="left"
label="日期"
prop="UpdatedAt"
width="180"
>
<template #default="scope">
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
</template>
</el-table-column>
<el-table-column
align="left"
label="文件名/备注"
prop="name"
width="180"
align="left"
label="文件名/备注"
prop="name"
width="180"
>
<template #default="scope">
<div class="cursor-pointer" @click="editFileNameFunc(scope.row)">
<div
class="cursor-pointer"
@click="editFileNameFunc(scope.row)"
>
{{ scope.row.name }}
</div>
</template>
</el-table-column>
<el-table-column align="left" label="链接" prop="url" min-width="300"/>
<el-table-column
align="left"
label="链接"
prop="url"
min-width="300"
/>
<el-table-column align="left" label="标签" prop="tag" width="100">
<template #default="scope">
<el-tag
:type="scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'"
disable-transitions
>{{ scope.row.tag }}
:type="
scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'
"
disable-transitions
>{{ scope.row.tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="left" label="操作" width="160">
<template #default="scope">
<el-button
icon="download"
type="primary"
link
@click="downloadFile(scope.row)"
>下载
</el-button
>
icon="download"
type="primary"
link
@click="downloadFile(scope.row)"
>下载
</el-button>
<el-button
icon="delete"
type="primary"
link
@click="deleteFileFunc(scope.row)"
>删除
</el-button
>
icon="delete"
type="primary"
link
@click="deleteFileFunc(scope.row)"
>删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:style="{ float: 'right', padding: '20px' }"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:style="{ float: 'right', padding: '20px' }"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
@@ -127,23 +167,34 @@
</div>
<!-- 添加分类弹窗 -->
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
draggable
<el-dialog
v-model="categoryDialogVisible"
@close="closeAddCategoryDialog"
width="520"
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
draggable
>
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
<el-form
ref="categoryForm"
:rules="rules"
:model="categoryFormData"
label-width="80px"
>
<el-form-item label="上级分类">
<el-tree-select
v-model="categoryFormData.pid"
:data="categories"
check-strictly
:props="defaultProps"
:render-after-expand="false"
style="width: 240px"
v-model="categoryFormData.pid"
:data="categories"
check-strictly
:props="defaultProps"
:render-after-expand="false"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="分类名称" prop="name">
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
<el-input
v-model.trim="categoryFormData.name"
placeholder="分类名称"
></el-input>
</el-form-item>
</el-form>
<template #footer>
@@ -151,88 +202,91 @@
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {
getFileList,
deleteFile,
editFileName,
importURL
} from '@/api/fileUploadAndDownload'
import {downloadImage} from '@/utils/downloadImg'
import CustomPic from '@/components/customPic/index.vue'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import {CreateUUID, formatDate} from '@/utils/format'
import WarningBar from '@/components/warningBar/warningBar.vue'
import {
getFileList,
deleteFile,
editFileName,
importURL
} from '@/api/fileUploadAndDownload'
import { downloadImage } from '@/utils/downloadImg'
import CustomPic from '@/components/customPic/index.vue'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import { CreateUUID, formatDate } from '@/utils/format'
import WarningBar from '@/components/warningBar/warningBar.vue'
import {ref} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {addCategory, deleteCategory, getCategoryList} from "@/api/attachmentCategory";
import CropperImage from "@/components/upload/cropper.vue";
import QRCodeUpload from "@/components/upload/QR-code.vue";
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
addCategory,
deleteCategory,
getCategoryList
} from '@/api/attachmentCategory'
import CropperImage from '@/components/upload/cropper.vue'
import QRCodeUpload from '@/components/upload/QR-code.vue'
defineOptions({
name: 'Upload'
})
const fullscreenLoading = ref(false)
const path = ref(import.meta.env.VITE_BASE_API)
const imageUrl = ref('')
const imageCommon = ref('')
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const search = ref({
keyword: null,
classId: 0
})
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
const onSubmit = () => {
search.value.classId = 0
page.value = 1
getTableData()
}
// 查询
const getTableData = async () => {
const table = await getFileList({
page: page.value,
pageSize: pageSize.value,
...search.value
defineOptions({
name: 'Upload'
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
const fullscreenLoading = ref(false)
const path = ref(import.meta.env.VITE_BASE_API)
const imageUrl = ref('')
const imageCommon = ref('')
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const search = ref({
keyword: null,
classId: 0
})
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
}
getTableData()
const deleteFileFunc = async (row) => {
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
const onSubmit = () => {
search.value.classId = 0
page.value = 1
getTableData()
}
// 查询
const getTableData = async () => {
const table = await getFileList({
page: page.value,
pageSize: pageSize.value,
...search.value
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const deleteFileFunc = async (row) => {
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await deleteFile(row)
if (res.code === 0) {
@@ -252,30 +306,30 @@ const deleteFileFunc = async (row) => {
message: '已取消删除'
})
})
}
const downloadFile = (row) => {
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
downloadImage(row.url, row.name)
} else {
downloadImage(path.value + '/' + row.url, row.name)
}
}
/**
* 编辑文件名或者备注
* @param row
* @returns {Promise<void>}
*/
const editFileNameFunc = async (row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
})
.then(async ({value}) => {
const downloadFile = (row) => {
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
downloadImage(row.url, row.name)
} else {
downloadImage(path.value + '/' + row.url, row.name)
}
}
/**
* 编辑文件名或者备注
* @param row
* @returns {Promise<void>}
*/
const editFileNameFunc = async (row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
})
.then(async ({ value }) => {
row.name = value
// console.log(row)
const res = await editFileName(row)
@@ -293,22 +347,22 @@ const editFileNameFunc = async (row) => {
message: '取消修改'
})
})
}
}
/**
* 导入URL
*/
const importUrlFunc = () => {
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputPlaceholder:
/**
* 导入URL
*/
const importUrlFunc = () => {
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputPlaceholder:
'我的图片|https://my-oss.com/my.png\nhttps://my-oss.com/my_1.png',
inputPattern: /\S/,
inputErrorMessage: '不能为空'
})
.then(async ({value}) => {
inputPattern: /\S/,
inputErrorMessage: '不能为空'
})
.then(async ({ value }) => {
let data = value.split('\n')
let importData = []
data.forEach((item) => {
@@ -348,101 +402,101 @@ const importUrlFunc = () => {
message: '取消导入'
})
})
}
const onSuccess = () => {
search.value.keyword = null
page.value = 1
getTableData()
}
const defaultProps = {
children: 'children',
label: 'name',
value: 'ID'
}
const categories = ref([])
const fetchCategories = async () => {
const res = await getCategoryList()
let data = {
name: '全部分类',
ID: 0,
pid: 0,
children:[]
}
if (res.code === 0) {
categories.value = res.data || []
categories.value.unshift(data)
const onSuccess = () => {
search.value.keyword = null
page.value = 1
getTableData()
}
}
const handleNodeClick = (node) => {
search.value.keyword = null
search.value.classId = node.ID
page.value = 1
getTableData()
}
const categoryDialogVisible = ref(false)
const categoryFormData = ref({
ID: 0,
pid: 0,
name: ''
})
const categoryForm = ref(null)
const rules = ref({
name: [
{required: true, message: '请输入分类名称', trigger: 'blur'},
{max: 20, message: '最多20位字符', trigger: 'blur'}
]
})
const addCategoryFun = (category) => {
categoryDialogVisible.value = true
categoryFormData.value.ID = 0
categoryFormData.value.pid = category.ID
}
const editCategory = (category) => {
categoryFormData.value = {
ID: category.ID,
pid: category.pid,
name: category.name
const defaultProps = {
children: 'children',
label: 'name',
value: 'ID'
}
categoryDialogVisible.value = true
}
const deleteCategoryFun = async (id) => {
const res = await deleteCategory({id: id})
if (res.code === 0) {
ElMessage.success({type: 'success', message: '删除成功'})
await fetchCategories()
}
}
const confirmAddCategory = async () => {
categoryForm.value.validate(async valid => {
if (valid) {
const res = await addCategory(categoryFormData.value)
if (res.code === 0) {
ElMessage({type: 'success', message: '操作成功'})
await fetchCategories()
closeAddCategoryDialog()
}
const categories = ref([])
const fetchCategories = async () => {
const res = await getCategoryList()
let data = {
name: '全部分类',
ID: 0,
pid: 0,
children: []
}
})
}
if (res.code === 0) {
categories.value = res.data || []
categories.value.unshift(data)
}
}
const closeAddCategoryDialog = () => {
categoryDialogVisible.value = false
categoryFormData.value = {
const handleNodeClick = (node) => {
search.value.keyword = null
search.value.classId = node.ID
page.value = 1
getTableData()
}
const categoryDialogVisible = ref(false)
const categoryFormData = ref({
ID: 0,
pid: 0,
name: ''
}
}
})
fetchCategories()
const categoryForm = ref(null)
const rules = ref({
name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ max: 20, message: '最多20位字符', trigger: 'blur' }
]
})
const addCategoryFun = (category) => {
categoryDialogVisible.value = true
categoryFormData.value.ID = 0
categoryFormData.value.pid = category.ID
}
const editCategory = (category) => {
categoryFormData.value = {
ID: category.ID,
pid: category.pid,
name: category.name
}
categoryDialogVisible.value = true
}
const deleteCategoryFun = async (id) => {
const res = await deleteCategory({ id: id })
if (res.code === 0) {
ElMessage.success({ type: 'success', message: '删除成功' })
await fetchCategories()
}
}
const confirmAddCategory = async () => {
categoryForm.value.validate(async (valid) => {
if (valid) {
const res = await addCategory(categoryFormData.value)
if (res.code === 0) {
ElMessage({ type: 'success', message: '操作成功' })
await fetchCategories()
closeAddCategoryDialog()
}
}
})
}
const closeAddCategoryDialog = () => {
categoryDialogVisible.value = false
categoryFormData.value = {
ID: 0,
pid: 0,
name: ''
}
}
fetchCategories()
</script>

View File

@@ -9,15 +9,11 @@
>
<div class="flex items-center cursor-pointer flex-1">
<div
class="flex items-center cursor-pointer"
class="flex items-center justify-center cursor-pointer"
:class="isMobile ? '' : 'min-w-48'"
@click="router.push({ path: '/' })"
>
<img
alt
class="h-12 bg-white rounded-full"
:src="$GIN_VUE_ADMIN.appLogo"
/>
<Logo />
<div
v-if="!isMobile"
class="inline-flex font-bold text-2xl ml-2"
@@ -112,6 +108,8 @@
import { setUserAuthority } from '@/api/user'
import { fmtTitle } from '@/utils/fmtRouterTitle'
import gvaAside from '@/view/layout/aside/index.vue'
import Logo from '@/components/logo/index.vue'
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()

View File

@@ -7,12 +7,11 @@
<div class="flex items-center mx-4 gap-4">
<el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
<el-dropdown @command="toDoc">
<el-icon
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
>
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
<el-icon>
<Film />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@@ -27,31 +26,37 @@
</el-tooltip>
<el-tooltip class="" effect="dark" content="搜索" placement="bottom">
<el-icon
@click="handleCommand"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
>
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
<el-icon
@click="handleCommand"
>
<Search />
</el-icon>
</span>
</el-tooltip>
<el-tooltip class="" effect="dark" content="系统设置" placement="bottom">
<el-icon
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
@click="toggleSetting"
>
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
<el-icon
@click="toggleSetting"
>
<Setting />
</el-icon>
</span>
</el-tooltip>
<el-tooltip class="" effect="dark" content="刷新" placement="bottom">
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
<el-icon
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
:class="showRefreshAnmite ? 'animate-spin' : ''"
@click="toggleRefresh"
:class="showRefreshAnmite ? 'animate-spin' : ''"
@click="toggleRefresh"
>
<Refresh />
</el-icon>
</span>
</el-tooltip>
<el-tooltip
class=""
@@ -59,20 +64,21 @@
content="切换主题"
placement="bottom"
>
<el-icon
v-if="appStore.isDark"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-600 cursor-pointer border-solid"
@click="appStore.toggleTheme(false)"
>
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
<el-icon
v-if="appStore.isDark"
@click="appStore.toggleTheme(false)"
>
<Sunny />
</el-icon>
<el-icon
v-else
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 cursor-pointer border-solid"
@click="appStore.toggleTheme(true)"
v-else
@click="appStore.toggleTheme(true)"
>
<Moon />
</el-icon>
</span>
</el-tooltip>
<gva-setting v-model:drawer="showSettingDrawer"></gva-setting>

View File

@@ -14,7 +14,8 @@
<div class="flex flex-row w-full gva-container pt-16 box-border !h-full">
<gva-aside
v-if="
config.side_mode === 'normal' || config.side_mode === 'sidebar' ||
config.side_mode === 'normal' ||
config.side_mode === 'sidebar' ||
(device === 'mobile' && config.side_mode == 'head') ||
(device === 'mobile' && config.side_mode == 'combination')
"
@@ -23,10 +24,10 @@
v-if="config.side_mode === 'combination' && device !== 'mobile'"
mode="normal"
/>
<div class="flex-1 px-2 w-0 h-full">
<div class="flex-1 w-0 h-full">
<gva-tabs v-if="config.showTabs" />
<div
class="overflow-auto"
class="overflow-auto px-2"
:class="config.showTabs ? 'gva-container2' : 'gva-container pt-1'"
>
<router-view v-if="reloadFlag" v-slot="{ Component, route }">
@@ -34,7 +35,10 @@
id="gva-base-load-dom"
class="gva-body-h bg-gray-50 dark:bg-slate-800"
>
<transition mode="out-in" :name="route.meta.transitionType || config.transition_type">
<transition
mode="out-in"
:name="route.meta.transitionType || config.transition_type"
>
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" :key="route.fullPath" />
</keep-alive>

View File

@@ -49,9 +49,11 @@
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="space-y-5">
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center justify-center text-red-600 dark:text-red-400 text-xl">
<div
class="w-12 h-12 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center justify-center text-red-600 dark:text-red-400 text-xl">
🔄
</div>
<div>
@@ -59,19 +61,18 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">将所有设置恢复为默认值</p>
</div>
</div>
<el-button
type="danger"
size="small"
<el-button type="danger" size="small"
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
@click="handleResetConfig"
>
@click="handleResetConfig">
重置配置
</el-button>
</div>
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl">
<div
class="w-12 h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl">
📤
</div>
<div>
@@ -79,20 +80,19 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">导出当前配置为 JSON 文件</p>
</div>
</div>
<el-button
type="primary"
size="small"
<el-button type="primary" size="small"
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
:style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }"
@click="handleExportConfig"
>
@click="handleExportConfig">
导出配置
</el-button>
</div>
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center justify-center text-green-600 dark:text-green-400 text-xl">
<div
class="w-12 h-12 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center justify-center text-green-600 dark:text-green-400 text-xl">
📥
</div>
<div>
@@ -100,18 +100,10 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1"> JSON 文件导入配置</p>
</div>
</div>
<el-upload
ref="uploadRef"
:auto-upload="false"
:show-file-list="false"
accept=".json"
@change="handleImportConfig"
>
<el-button
type="success"
size="small"
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5"
>
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".json"
@change="handleImportConfig">
<el-button type="success" size="small"
class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5">
导入配置
</el-button>
</el-upload>
@@ -131,13 +123,9 @@
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="flex items-start gap-5">
<div class="w-16 h-16 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
<img
src="/logo.png"
alt="Gin-Vue-Admin Logo"
class="w-10 h-10 object-contain"
@error="handleLogoError"
/>
<div
class="w-16 h-16 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
<Logo />
</div>
<div class="flex-1">
<h4 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Gin-Vue-Admin</h4>
@@ -145,21 +133,15 @@
基于 Vue3 + Gin 的全栈开发基础平台提供完整的后台管理解决方案
</p>
<div class="flex items-center gap-3 text-sm">
<a
href="https://github.com/flipped-aurora/gin-vue-admin"
target="_blank"
<a href="https://github.com/flipped-aurora/gin-vue-admin" target="_blank"
class="font-medium transition-colors duration-150 hover:underline"
:style="{ color: config.primaryColor }"
>
:style="{ color: config.primaryColor }">
GitHub 仓库
</a>
<span class="text-gray-400 dark:text-gray-500">·</span>
<a
href="https://www.gin-vue-admin.com/"
target="_blank"
<a href="https://www.gin-vue-admin.com/" target="_blank"
class="font-medium transition-colors duration-150 hover:underline"
:style="{ color: config.primaryColor }"
>
:style="{ color: config.primaryColor }">
官方文档
</a>
</div>
@@ -172,10 +154,11 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { storeToRefs } from 'pinia'
import { useAppStore } from '@/pinia'
import Logo from '@/components/logo/index.vue'
defineOptions({
name: 'GeneralSettings'
@@ -187,7 +170,6 @@ const uploadRef = ref()
const browserInfo = ref('')
const screenResolution = ref('')
const logoUrl = ref('')
onMounted(() => {
const userAgent = navigator.userAgent
@@ -206,10 +188,6 @@ onMounted(() => {
screenResolution.value = `${screen.width}×${screen.height}`
})
const handleLogoError = () => {
logoUrl.value = ''
}
const handleResetConfig = async () => {
try {
await ElMessageBox.confirm(
@@ -221,7 +199,7 @@ const handleResetConfig = async () => {
type: 'warning'
}
)
appStore.resetConfig()
ElMessage.success('配置已重置')
} catch {
@@ -233,7 +211,7 @@ const handleExportConfig = () => {
const configData = JSON.stringify(config.value, null, 2)
const blob = new Blob([configData], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `gin-vue-admin-config-${new Date().toISOString().split('T')[0]}.json`
@@ -241,7 +219,7 @@ const handleExportConfig = () => {
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('配置已导出')
}
@@ -250,13 +228,13 @@ const handleImportConfig = (file) => {
reader.onload = (e) => {
try {
const importedConfig = JSON.parse(e.target.result)
Object.keys(importedConfig).forEach(key => {
if (key in config.value) {
config.value[key] = importedConfig[key]
}
})
ElMessage.success('配置已导入')
} catch (error) {
ElMessage.error('配置文件格式错误')
@@ -280,6 +258,7 @@ const handleImportConfig = (file) => {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);

View File

@@ -13,7 +13,7 @@
>
<div>
<div class="flex items-center justify-center">
<img class="w-24" :src="$GIN_VUE_ADMIN.appLogo" alt />
<Logo :size="6" />
</div>
<div class="mb-9">
<p class="text-center text-4xl font-bold">
@@ -131,12 +131,14 @@
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/pinia/modules/user'
import Logo from '@/components/logo/index.vue'
defineOptions({
name: 'Login'
})
const router = useRouter()
const captchaRequiredLength = ref(6)
// 验证函数
const checkUsername = (rule, value, callback) => {
if (value.length < 5) {
@@ -152,19 +154,36 @@
callback()
}
}
const checkCaptcha = (rule, value, callback) => {
if (!loginFormData.openCaptcha) {
return callback()
}
const sanitizedValue = (value || '').replace(/\s+/g, '')
if (!sanitizedValue) {
return callback(new Error('请输入验证码'))
}
if (!/^\d+$/.test(sanitizedValue)) {
return callback(new Error('验证码须为数字'))
}
if (sanitizedValue.length < captchaRequiredLength.value) {
return callback(
new Error(`请输入至少${captchaRequiredLength.value}位数字验证码`)
)
}
if (sanitizedValue !== value) {
loginFormData.captcha = sanitizedValue
}
callback()
}
// 获取验证码
const loginVerify = async () => {
const ele = await captcha()
rules.captcha.push({
max: ele.data.captchaLength,
min: ele.data.captchaLength,
message: `请输入${ele.data.captchaLength}位验证码`,
trigger: 'blur'
})
picPath.value = ele.data.picPath
loginFormData.captchaId = ele.data.captchaId
loginFormData.openCaptcha = ele.data.openCaptcha
const lengthFromServer = Number(ele.data?.captchaLength) || 0
captchaRequiredLength.value = Math.max(6, lengthFromServer)
picPath.value = ele.data?.picPath
loginFormData.captchaId = ele.data?.captchaId
loginFormData.openCaptcha = ele.data?.openCaptcha
}
loginVerify()
@@ -181,12 +200,7 @@
const rules = reactive({
username: [{ validator: checkUsername, trigger: 'blur' }],
password: [{ validator: checkPassword, trigger: 'blur' }],
captcha: [
{
message: '验证码格式不正确',
trigger: 'blur'
}
]
captcha: [{ validator: checkCaptcha, trigger: 'blur' }]
})
const userStore = useUserStore()
@@ -202,7 +216,6 @@
message: '请正确填写登录信息',
showClose: true
})
await loginVerify()
return false
}

View File

@@ -3,50 +3,91 @@
<warning-bar
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
/>
<div class="flex gap-4 p-2">
<div
class="flex-none w-52 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
>
<div class="flex justify-between items-center">
<span class="text font-bold">字典列表</span>
<el-button type="primary" @click="openDrawer"> 新增 </el-button>
</div>
<el-scrollbar class="mt-4" style="height: calc(100vh - 300px)">
<div
v-for="dictionary in dictionaryData"
:key="dictionary.ID"
class="rounded flex justify-between items-center px-2 py-4 cursor-pointer mt-2 hover:bg-blue-50 dark:hover:bg-blue-900 bg-gray-50 dark:bg-gray-800 gap-4"
:class="
selectID === dictionary.ID
? 'text-active'
: 'text-slate-700 dark:text-slate-50'
"
@click="toDetail(dictionary)"
>
<span class="max-w-[160px] truncate">{{ dictionary.name }}</span>
<div class="min-w-[40px]">
<el-icon
class="text-blue-500"
@click.stop="updateSysDictionaryFunc(dictionary)"
>
<Edit />
</el-icon>
<el-icon
class="ml-2 text-red-500"
@click="deleteSysDictionaryFunc(dictionary)"
>
<Delete />
</el-icon>
</div>
<el-splitter class="h-full">
<el-splitter-panel size="400px" min="200px" max="800px" collapsible>
<div
class="flex-none bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
>
<div class="flex justify-between items-center relative">
<span class="text font-bold">字典列表</span>
<el-input
class="!absolute top-0 left-0 z-2 ease-in-out animate-slide-left"
placeholder="搜索"
v-if="showSearchInput"
v-model="searchName"
clearable
:autofocus="showSearchInput"
@clear="clearSearchInput"
:prefix-icon="Search"
v-click-outside="handleCloseSearchInput"
@keydown="handleInputKeyDown"
>
<template #append>
<el-button
:type="searchName ? 'primary' : 'info'"
@click="getTableData"
>搜索</el-button
>
</template>
</el-input>
<el-button
class="ml-auto"
:icon="Search"
@click="showSearchInputHandler"
></el-button>
<el-button type="primary" @click="openDrawer" :icon="Plus">
</el-button>
</div>
</el-scrollbar>
</div>
<div
class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900"
>
<sysDictionaryDetail :sys-dictionary-i-d="selectID" />
</div>
</div>
<el-scrollbar class="mt-4" style="height: calc(100vh - 300px)">
<div
v-for="dictionary in dictionaryData"
:key="dictionary.ID"
class="rounded flex justify-between items-center px-2 py-4 cursor-pointer mt-2 hover:bg-blue-50 dark:hover:bg-blue-900 bg-gray-50 dark:bg-gray-800 gap-4"
:class="[
selectID === dictionary.ID
? 'text-active'
: 'text-slate-700 dark:text-slate-50',
dictionary.parentID ? 'ml-4 border-l-2 border-blue-200' : ''
]"
@click="toDetail(dictionary)"
>
<div class="max-w-[160px] truncate">
<span
v-if="dictionary.parentID"
class="text-xs text-gray-400 mr-1"
></span
>
{{ dictionary.name }}
<span class="mr-auto text-sm">{{ dictionary.type }}</span>
</div>
<div class="min-w-[40px]">
<el-icon
class="text-blue-500"
@click.stop="updateSysDictionaryFunc(dictionary)"
>
<Edit />
</el-icon>
<el-icon
class="ml-2 text-red-500"
@click="deleteSysDictionaryFunc(dictionary)"
>
<Delete />
</el-icon>
</div>
</div>
</el-scrollbar>
</div>
</el-splitter-panel>
<el-splitter-panel :min="200">
<div
class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900"
>
<sysDictionaryDetail :sys-dictionary-i-d="selectID" />
</div>
</el-splitter-panel>
</el-splitter>
<el-drawer
v-model="drawerFormVisible"
:size="appStore.drawerSize"
@@ -70,6 +111,22 @@
:rules="rules"
label-width="110px"
>
<el-form-item label="父级字典" prop="parentID">
<el-select
v-model="formData.parentID"
placeholder="请选择父级字典(可选)"
clearable
filterable
:style="{ width: '100%' }"
>
<el-option
v-for="dict in availableParentDictionaries"
:key="dict.ID"
:label="`${dict.name}${dict.type}`"
:value="dict.ID"
/>
</el-select>
</el-form-item>
<el-form-item label="字典名(中)" prop="name">
<el-input
v-model="formData.name"
@@ -119,8 +176,8 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import sysDictionaryDetail from './sysDictionaryDetail.vue'
import { Edit } from '@element-plus/icons-vue'
import { useAppStore } from "@/pinia";
import { Edit, Plus, Search } from '@element-plus/icons-vue'
import { useAppStore } from '@/pinia'
defineOptions({
name: 'SysDictionary'
@@ -134,8 +191,11 @@
name: null,
type: null,
status: true,
desc: null
desc: null,
parentID: null
})
const searchName = ref('')
const showSearchInput = ref(false)
const rules = ref({
name: [
{
@@ -161,16 +221,47 @@
})
const dictionaryData = ref([])
const availableParentDictionaries = ref([])
// 查询
const getTableData = async () => {
const res = await getSysDictionaryList()
const res = await getSysDictionaryList({
name: searchName.value.trim()
})
if (res.code === 0) {
dictionaryData.value = res.data
selectID.value = res.data[0].ID
// 更新可选父级字典列表
updateAvailableParentDictionaries()
}
}
// 更新可选父级字典列表
const updateAvailableParentDictionaries = () => {
// 如果是编辑模式,排除当前字典及其子字典
if (type.value === 'update' && formData.value.ID) {
availableParentDictionaries.value = dictionaryData.value.filter(
(dict) => {
return (
dict.ID !== formData.value.ID &&
!isChildDictionary(dict.ID, formData.value.ID)
)
}
)
} else {
// 创建模式,显示所有字典
availableParentDictionaries.value = [...dictionaryData.value]
}
}
// 检查是否为子字典(防止循环引用)
const isChildDictionary = (dictId, parentId) => {
const dict = dictionaryData.value.find((d) => d.ID === dictId)
if (!dict || !dict.parentID) return false
if (dict.parentID === parentId) return true
return isChildDictionary(dict.parentID, parentId)
}
getTableData()
const toDetail = (row) => {
@@ -185,6 +276,8 @@
if (res.code === 0) {
formData.value = res.data.resysDictionary
drawerFormVisible.value = true
// 更新可选父级字典列表
updateAvailableParentDictionaries()
}
}
const closeDrawer = () => {
@@ -193,7 +286,8 @@
name: null,
type: null,
status: true,
desc: null
desc: null,
parentID: null
}
}
const deleteSysDictionaryFunc = async (row) => {
@@ -240,6 +334,29 @@
type.value = 'create'
drawerForm.value && drawerForm.value.clearValidate()
drawerFormVisible.value = true
// 更新可选父级字典列表
updateAvailableParentDictionaries()
}
const clearSearchInput = () => {
if (!showSearchInput.value) return
searchName.value = ''
showSearchInput.value = false
getTableData()
}
const handleCloseSearchInput = () => {
if (!showSearchInput.value || searchName.value.trim() != '') return
showSearchInput.value = false
}
const showSearchInputHandler = () => {
showSearchInput.value = true
}
const handleInputKeyDown = (e) => {
if (e.key === 'Enter' && searchName.value.trim() !== '') {
getTableData()
}
}
</script>

View File

@@ -1,31 +1,56 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list justify-between">
<div class="gva-btn-list justify-between flex items-center">
<span class="text font-bold">字典详细内容</span>
<el-button type="primary" icon="plus" @click="openDrawer">
新增字典项
</el-button>
<div class="flex items-center gap-2">
<el-input
placeholder="搜索展示值"
v-model="searchName"
clearable
class="!w-64"
@clear="clearSearchInput"
:prefix-icon="Search"
v-click-outside="handleCloseSearchInput"
@keydown="handleInputKeyDown"
>
<template #append>
<el-button
:type="searchName ? 'primary' : 'info'"
@click="getTreeData"
>搜索</el-button
>
</template>
</el-input>
<el-button type="primary" icon="plus" @click="openDrawer">
新增字典项
</el-button>
</div>
</div>
<!-- 表格视图 -->
<el-table
ref="multipleTable"
:data="tableData"
:data="treeData"
style="width: 100%"
tooltip-effect="dark"
:tree-props="{ children: 'children'}"
row-key="ID"
default-expand-all
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="展示值" prop="label" min-width="240"/>
<el-table-column align="left" label="字典值" prop="value" />
<el-table-column align="left" label="扩展值" prop="extend" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">
{{ formatDate(scope.row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column align="left" label="展示值" prop="label" />
<el-table-column align="left" label="字典值" prop="value" />
<el-table-column align="left" label="扩展值" prop="extend" />
<el-table-column align="left" label="层级" prop="level" width="80" />
<el-table-column
align="left"
@@ -45,8 +70,20 @@
width="120"
/>
<el-table-column align="left" label="操作" :min-width="appStore.operateMinWith">
<el-table-column
align="left"
label="操作"
:min-width="appStore.operateMinWith"
>
<template #default="scope">
<el-button
type="success"
link
icon="plus"
@click="addChildNode(scope.row)"
>
添加子项
</el-button>
<el-button
type="primary"
link
@@ -66,18 +103,6 @@
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-drawer
@@ -103,6 +128,18 @@
:rules="rules"
label-width="110px"
>
<el-form-item label="父级字典项" prop="parentID">
<el-cascader
v-model="formData.parentID"
:options="[rootOption,...treeData]"
:props="cascadeProps"
placeholder="请选择父级字典项(可选)"
clearable
filterable
:style="{ width: '100%' }"
@change="handleParentChange"
/>
</el-form-item>
<el-form-item label="展示值" prop="label">
<el-input
v-model="formData.label"
@@ -151,18 +188,20 @@
deleteSysDictionaryDetail,
updateSysDictionaryDetail,
findSysDictionaryDetail,
getSysDictionaryDetailList
getDictionaryTreeList
} from '@/api/sysDictionaryDetail' // 此处请自行替换地址
import { ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatBoolean, formatDate } from '@/utils/format'
import { useAppStore } from "@/pinia";
import { useAppStore } from '@/pinia'
import { Search } from '@element-plus/icons-vue'
defineOptions({
name: 'SysDictionaryDetail'
})
const appStore = useAppStore()
const searchName = ref('')
const props = defineProps({
sysDictionaryID: {
@@ -175,8 +214,10 @@
label: null,
value: null,
status: true,
sort: null
sort: null,
parentID: null
})
const rules = ref({
label: [
{
@@ -201,42 +242,46 @@
]
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const treeData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
// 级联选择器配置
const cascadeProps = {
value: 'ID',
label: 'label',
children: 'children',
checkStrictly: true, // 允许选择任意级别
emitPath: false // 只返回选中节点的值
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async () => {
// 获取树形数据
const getTreeData = async () => {
if (!props.sysDictionaryID) return
const table = await getSysDictionaryDetailList({
page: page.value,
pageSize: pageSize.value,
sysDictionaryID: props.sysDictionaryID
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
try {
const res = await getDictionaryTreeList({
sysDictionaryID: props.sysDictionaryID
})
if (res.code === 0) {
treeData.value = res.data.list || []
}
} catch (error) {
console.error('获取树形数据失败:', error)
ElMessage.error('获取层级数据失败')
}
}
getTableData()
const rootOption = {
ID: null,
label: '无父级(根级)'
}
// 初始加载
getTreeData()
const type = ref('')
const drawerFormVisible = ref(false)
const updateSysDictionaryDetailFunc = async (row) => {
drawerForm.value && drawerForm.value.clearValidate()
const res = await findSysDictionaryDetail({ ID: row.ID })
@@ -247,6 +292,27 @@
}
}
// 添加子节点
const addChildNode = (parentNode) => {
console.log(parentNode)
type.value = 'create'
formData.value = {
label: null,
value: null,
status: true,
sort: null,
parentID: parentNode.ID,
sysDictionaryID: props.sysDictionaryID
}
drawerForm.value && drawerForm.value.clearValidate()
drawerFormVisible.value = true
}
// 处理父级选择变化
const handleParentChange = (value) => {
formData.value.parentID = value
}
const closeDrawer = () => {
drawerFormVisible.value = false
formData.value = {
@@ -254,9 +320,11 @@
value: null,
status: true,
sort: null,
parentID: null,
sysDictionaryID: props.sysDictionaryID
}
}
const deleteSysDictionaryDetailFunc = async (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
@@ -272,7 +340,7 @@
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
await getTreeData() // 重新加载数据
}
})
}
@@ -300,22 +368,41 @@
message: '创建/更改成功'
})
closeDrawer()
getTableData()
await getTreeData() // 重新加载数据
}
})
}
const openDrawer = () => {
type.value = 'create'
formData.value.parentID = null
drawerForm.value && drawerForm.value.clearValidate()
drawerFormVisible.value = true
}
const clearSearchInput = () => {
searchName.value = ''
getTreeData()
}
const handleCloseSearchInput = () => {
// 处理搜索输入框关闭
}
const handleInputKeyDown = (e) => {
if (e.key === 'Enter' && searchName.value.trim() !== '') {
getTreeData()
}
}
watch(
() => props.sysDictionaryID,
() => {
getTableData()
getTreeData()
}
)
</script>
<style></style>
<style scoped>
</style>

View File

@@ -47,6 +47,9 @@
<el-form-item label="限流时间">
<el-input-number v-model.number="config.system['iplimit-time']" />
</el-form-item>
<el-form-item label="禁用自动迁移数据库表结构">
<el-switch v-model="config.system['disable-auto-migrate']" />
</el-form-item>
<el-tooltip
content="请修改完成后注意一并修改前端env环境下的VITE_BASE_PATH"
placement="top-start"