From 6a2e1355e6b3c1f7ee30359498a6d353bfc3c232 Mon Sep 17 00:00:00 2001 From: bypanghu Date: Tue, 30 Sep 2025 13:34:03 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E5=AD=97=E5=85=B8=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20tree=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/v1/system/sys_dictionary_detail.go | 119 +++++ server/middleware/casbin_rbac.go | 5 +- .../system/request/sys_dictionary_detail.go | 32 ++ server/model/system/sys_dictionary.go | 10 +- server/model/system/sys_dictionary_detail.go | 16 +- server/router/system/sys_dictionary_detail.go | 8 +- server/service/system/sys_dictionary.go | 44 +- .../service/system/sys_dictionary_detail.go | 248 ++++++++++- server/source/system/api.go | 5 + server/source/system/casbin.go | 4 + web/src/api/sysDictionaryDetail.js | 65 +++ web/src/pinia/modules/dictionary.js | 245 ++++++++++- web/src/utils/dictionary.js | 77 +++- .../superAdmin/dictionary/sysDictionary.vue | 210 +++++---- .../dictionary/sysDictionaryDetail.vue | 408 ++++++++++++++++-- 15 files changed, 1342 insertions(+), 154 deletions(-) diff --git a/server/api/v1/system/sys_dictionary_detail.go b/server/api/v1/system/sys_dictionary_detail.go index 754af1be6a..6be069f818 100644 --- a/server/api/v1/system/sys_dictionary_detail.go +++ b/server/api/v1/system/sys_dictionary_detail.go @@ -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) +} diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go index 6744c758b5..f71aecd059 100644 --- a/server/middleware/casbin_rbac.go +++ b/server/middleware/casbin_rbac.go @@ -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 拦截器 diff --git a/server/model/system/request/sys_dictionary_detail.go b/server/model/system/request/sys_dictionary_detail.go index 2f97da2803..2419cc3037 100644 --- a/server/model/system/request/sys_dictionary_detail.go +++ b/server/model/system/request/sys_dictionary_detail.go @@ -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"` // 是否包含子级数据 } diff --git a/server/model/system/sys_dictionary.go b/server/model/system/sys_dictionary.go index c0b9bf7fc3..0bc60848fa 100644 --- a/server/model/system/sys_dictionary.go +++ b/server/model/system/sys_dictionary.go @@ -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"` } diff --git a/server/model/system/sys_dictionary_detail.go b/server/model/system/sys_dictionary_detail.go index 4084136c2e..30b4e3f220 100644 --- a/server/model/system/sys_dictionary_detail.go +++ b/server/model/system/sys_dictionary_detail.go @@ -8,12 +8,16 @@ 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" } func (SysDictionaryDetail) TableName() string { diff --git a/server/router/system/sys_dictionary_detail.go b/server/router/system/sys_dictionary_detail.go index cde6bdcb6e..3f4aa5d997 100644 --- a/server/router/system/sys_dictionary_detail.go +++ b/server/router/system/sys_dictionary_detail.go @@ -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) // 获取字典详情的完整路径 } } diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go index 193565bcc7..d49c90b7df 100644 --- a/server/service/system/sys_dictionary.go +++ b/server/service/system/sys_dictionary.go @@ -2,6 +2,7 @@ package system import ( "errors" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" @@ -62,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 { @@ -77,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 } @@ -113,6 +123,32 @@ func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Cont 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 +} diff --git a/server/service/system/sys_dictionary_detail.go b/server/service/system/sys_dictionary_detail.go index 18042c788f..f6a73b1edb 100644 --- a/server/service/system/sys_dictionary_detail.go +++ b/server/service/system/sys_dictionary_detail.go @@ -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) @@ -50,7 +166,7 @@ func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetai //@return: sysDictionaryDetail system.SysDictionaryDetail, err error func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetail(id uint) (sysDictionaryDetail system.SysDictionaryDetail, err error) { - err = global.GVA_DB.Where("id = ?", id).First(&sysDictionaryDetail).Error + err = global.GVA_DB.Preload("Children").Where("id = ?", id).First(&sysDictionaryDetail).Error return } @@ -79,33 +195,120 @@ 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 } - err = db.Limit(limit).Offset(offset).Order("sort").Find(&sysDictionaryDetails).Error + err = db.Preload("Children").Limit(limit).Offset(offset).Order("sort").Find(&sysDictionaryDetails).Error return sysDictionaryDetails, total, err } // 按照字典id获取字典全部内容的方法 func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { var sysDictionaryDetails []system.SysDictionaryDetail - err = global.GVA_DB.Find(&sysDictionaryDetails, "sys_dictionary_id = ?", dictionaryID).Error + err = global.GVA_DB.Preload("Children").Find(&sysDictionaryDetails, "sys_dictionary_id = ?", dictionaryID).Error 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 + } + + // 递归加载子项 + for i := range sysDictionaryDetails { + 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 { + 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") + } + + if req.IncludeChildren { + db = db.Preload("Children") + } + + err = db.Order("sort").Find(&list).Error + 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.Debug().Preload("Children").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 + } + + // 递归加载子项 + for i := range sysDictionaryDetails { + 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 - err = global.GVA_DB.First(&sysDictionaryDetail, "sys_dictionary_id = ? and value = ?", dictionaryID, value).Error + err = global.GVA_DB.Preload("Children").First(&sysDictionaryDetail, "sys_dictionary_id = ? and value = ?", dictionaryID, value).Error return sysDictionaryDetail, err } @@ -113,6 +316,37 @@ func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeValue(t string, value string) (detail 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.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error + err = db.Preload("Children").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) +} diff --git a/server/source/system/api.go b/server/source/system/api.go index 710dc13550..87a47695c8 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -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: "更新字典"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 94e1a24c00..6bd3c04993 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -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"}, diff --git a/web/src/api/sysDictionaryDetail.js b/web/src/api/sysDictionaryDetail.js index d4f877224c..1f4ab7384c 100644 --- a/web/src/api/sysDictionaryDetail.js +++ b/web/src/api/sysDictionaryDetail.js @@ -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 + }) +} diff --git a/web/src/pinia/modules/dictionary.js b/web/src/pinia/modules/dictionary.js index 57a2844970..b0c089f962 100644 --- a/web/src/pinia/modules/dictionary.js +++ b/web/src/pinia/modules/dictionary.js @@ -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] + } } } } diff --git a/web/src/utils/dictionary.js b/web/src/utils/dictionary.js index 89ec656e43..c67bb82811 100644 --- a/web/src/utils/dictionary.js +++ b/web/src/utils/dictionary.js @@ -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} 字典数据数组 + * @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 [] + } } // 字典文字展示方法 diff --git a/web/src/view/superAdmin/dictionary/sysDictionary.vue b/web/src/view/superAdmin/dictionary/sysDictionary.vue index 627fa0eead..894e8a3c13 100644 --- a/web/src/view/superAdmin/dictionary/sysDictionary.vue +++ b/web/src/view/superAdmin/dictionary/sysDictionary.vue @@ -3,80 +3,91 @@ -
-
-
- 字典列表 - - - - - - -
- -
-
- {{ dictionary.name }} - ({{ dictionary.type }}) -
+ + +
+
+ 字典列表 + + + + + + +
+ +
+
+ └─ + {{ dictionary.name }} + ({{ dictionary.type }}) +
-
- - - - - - +
+ + + + + + +
-
-
-
-
- -
-
+
+
+ + +
+ +
+
+ + + + + + + { @@ -202,9 +231,37 @@ 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) => { @@ -219,6 +276,8 @@ if (res.code === 0) { formData.value = res.data.resysDictionary drawerFormVisible.value = true + // 更新可选父级字典列表 + updateAvailableParentDictionaries() } } const closeDrawer = () => { @@ -227,7 +286,8 @@ name: null, type: null, status: true, - desc: null + desc: null, + parentID: null } } const deleteSysDictionaryFunc = async (row) => { @@ -274,6 +334,8 @@ type.value = 'create' drawerForm.value && drawerForm.value.clearValidate() drawerFormVisible.value = true + // 更新可选父级字典列表 + updateAvailableParentDictionaries() } const clearSearchInput = () => { diff --git a/web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue b/web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue index cf1f38a213..12131159a0 100644 --- a/web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue +++ b/web/src/view/superAdmin/dictionary/sysDictionaryDetail.vue @@ -3,29 +3,51 @@
字典详细内容 - - - - - 新增字典项 - + 表格视图 + + + 层级视图 + + + + + + + + 新增字典项 + +
+ + + + + + + + - - - + - + + +