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
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ rm_file/
|
||||
/server/server
|
||||
/server/latest_log
|
||||
/server/__debug_bin*
|
||||
/server/*.local.yaml
|
||||
server/uploads/
|
||||
|
||||
*.iml
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -280,4 +280,6 @@ mcp:
|
||||
version: v1.0.0
|
||||
sse_path: /sse
|
||||
message_path: /message
|
||||
url_prefix: ''
|
||||
url_prefix: ''
|
||||
addr: 8889
|
||||
separate: false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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服务
|
||||
}
|
||||
|
||||
@@ -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,手动迁移
|
||||
}
|
||||
|
||||
@@ -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进行极速开发的全栈开发基础平台"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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(
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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字符串)
|
||||
- json(JSON)
|
||||
- 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. 字段类型要与实际需求匹配
|
||||
@@ -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字符串),json(JSON),array(数组)
|
||||
475
server/mcp/gva_analyze.go
Normal file
475
server/mcp/gva_analyze.go
Normal 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
778
server/mcp/gva_execute.go
Normal 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字符串),json(JSON),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字符串),json(JSON),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_creator:API权限已在模块创建时自动生成\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
170
server/mcp/gva_review.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 拦截器
|
||||
|
||||
5
server/model/system/request/sys_dictionary.go
Normal file
5
server/model/system/request/sys_dictionary.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package request
|
||||
|
||||
type SysDictionarySearch struct {
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
}
|
||||
@@ -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"` // 是否包含子级数据
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) // 获取字典详情的完整路径
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: "更新字典"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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: '请检查控制台获取更多信息。'
|
||||
};
|
||||
|
||||
82
web/src/components/logo/index.vue
Normal file
82
web/src/components/logo/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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: []
|
||||
|
||||
51
web/src/core/error-handel.js
Normal file
51
web/src/core/error-handel.js
Normal 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()
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
43
web/src/directive/clickOutSide.js
Normal file
43
web/src/directive/clickOutSide.js
Normal 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__
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
// 字典文字展示方法
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 拦截器
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user