V2.8.4Beta 极致融合AI编辑器 让开发速度更进一步 (#2060)
* fix(style): 修复 border 额外的 reset 导致 tailwind border 属性生效异常的问题 * feat: 添加错误预览组件并优化请求错误处理逻辑 * optimize: select and update necessary fields in `ChangePassword` method - Simplify `ChangePassword` method signature by removing unnecessary return type. - Use `Select()` to fetch only the necessary fields (`id` and `password`) from the database. - Replace `Save()` with `Update()` for more efficient password update operation. Note: use `Save(&user)` to update the whole user record, which will cover other unchanged fields as well, causing data inconsistency when data race conditions. * feat(menu): 版本更新为2.8.4,给菜单增加按钮和参数的预制打包 * feat(menu): 恢复空白的配置文件 * Remove unused `SideMode` field from `ChangeUserInfo` struct Remove unused and deprecated `SideMode` field from user request model. * feat(automation): 增加可以自动生成CURD和续写方法的MCP * fix(mcp): 确保始终返回目录结构信息 * fix(mcp): 当不需要创建模块时提前返回目录结构信息 * feat(automation): 增加可以自动生成CURD和续写方法的MCP * feat(mcp): 添加GAG工具用户确认流程和自动字典创建功能 实现三步工作流程:分析、确认、执行 新增自动字典创建功能,当字段使用字典类型时自动检查并创建字典 添加用户确认机制,确保创建操作前获得用户明确确认 * feat(version): 新增版本管理功能,支持创建、导入、导出和下载版本数据 新增版本管理模块,包含以下功能: 1. 版本数据的增删改查 2. 版本创建功能,可选择关联菜单和API 3. 版本导入导出功能 4. 版本JSON数据下载 5. 相关前端页面和接口实现 * refactor(version): 简化版本管理删除逻辑并移除无用字段 移除版本管理中的状态、创建者、更新者和删除者字段 简化删除和批量删除方法的实现,去除事务和用户ID参数 更新自动生成配置的默认值说明 * feat(版本管理): 新增版本管理功能模块 * fix(menu): 修复递归创建菜单时关联数据未正确处理的问题 * feat(mcp): 添加预设计模块扫描功能以支持代码自动生成 在自动化模块分析器中添加对预设计模块的扫描功能,包括: - 新增PredesignedModuleInfo结构体存储模块信息 - 实现scanPredesignedModules方法扫描plugin和model目录 - 在分析响应中添加predesignedModules字段 - 更新帮助文档说明预设计模块的使用方式 这些修改使系统能够识别并利用现有的预设计模块,提高代码生成效率并减少重复工作。 * feat(mcp): 新增API、菜单和字典生成工具并优化自动生成模块 * docs(mcp): 更新菜单和API创建工具的描述信息 * feat(mcp): 添加字典查询工具用于AI生成逻辑时了解可用字典选项 * feat: 在创建菜单/API/模块结果中添加权限分配提醒 为菜单创建、API创建和模块创建的结果消息添加权限分配提醒,帮助用户了解后续需要进行的权限配置步骤 * refactor(mcp): 统一使用WithBoolean替换WithBool并优化错误处理 * docs(mcp): 更新API创建工具的说明和错误处理日志 * feat(mcp): 添加插件意图检测功能并增强验证逻辑 --------- Co-authored-by: Azir <2075125282@qq.com> Co-authored-by: Feng.YJ <jxfengyijie@gmail.com> Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
This commit is contained in:
parent
39abc7b632
commit
273d7bd50e
|
|
@ -22,6 +22,7 @@ type ApiGroup struct {
|
||||||
AutoCodeHistoryApi
|
AutoCodeHistoryApi
|
||||||
AutoCodeTemplateApi
|
AutoCodeTemplateApi
|
||||||
SysParamsApi
|
SysParamsApi
|
||||||
|
SysVersionApi
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -44,4 +45,5 @@ var (
|
||||||
autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
|
autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
|
||||||
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
|
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
|
||||||
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
|
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
|
||||||
|
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ func (b *BaseApi) ChangePassword(c *gin.Context) {
|
||||||
}
|
}
|
||||||
uid := utils.GetUserID(c)
|
uid := utils.GetUserID(c)
|
||||||
u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password}
|
u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password}
|
||||||
_, err = userService.ChangePassword(u, req.NewPassword)
|
err = userService.ChangePassword(u, req.NewPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("修改失败!", zap.Error(err))
|
global.GVA_LOG.Error("修改失败!", zap.Error(err))
|
||||||
response.FailWithMessage("修改失败,原密码与当前账户不符", c)
|
response.FailWithMessage("修改失败,原密码与当前账户不符", c)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,437 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||||
|
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysVersionApi struct{}
|
||||||
|
|
||||||
|
// buildMenuTree 构建菜单树结构
|
||||||
|
func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu {
|
||||||
|
// 创建菜单映射
|
||||||
|
menuMap := make(map[uint]*system.SysBaseMenu)
|
||||||
|
for i := range menus {
|
||||||
|
menuMap[menus[i].ID] = &menus[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建树结构
|
||||||
|
var rootMenus []system.SysBaseMenu
|
||||||
|
for _, menu := range menus {
|
||||||
|
if menu.ParentId == 0 {
|
||||||
|
// 根菜单
|
||||||
|
menuData := convertMenuToStruct(menu, menuMap)
|
||||||
|
rootMenus = append(rootMenus, menuData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按sort排序根菜单
|
||||||
|
sort.Slice(rootMenus, func(i, j int) bool {
|
||||||
|
return rootMenus[i].Sort < rootMenus[j].Sort
|
||||||
|
})
|
||||||
|
|
||||||
|
return rootMenus
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单
|
||||||
|
func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu {
|
||||||
|
result := system.SysBaseMenu{
|
||||||
|
Path: menu.Path,
|
||||||
|
Name: menu.Name,
|
||||||
|
Hidden: menu.Hidden,
|
||||||
|
Component: menu.Component,
|
||||||
|
Sort: menu.Sort,
|
||||||
|
Meta: menu.Meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理并复制参数数据
|
||||||
|
if len(menu.Parameters) > 0 {
|
||||||
|
cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters))
|
||||||
|
for _, param := range menu.Parameters {
|
||||||
|
cleanParam := system.SysBaseMenuParameter{
|
||||||
|
Type: param.Type,
|
||||||
|
Key: param.Key,
|
||||||
|
Value: param.Value,
|
||||||
|
// 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
|
||||||
|
}
|
||||||
|
cleanParameters = append(cleanParameters, cleanParam)
|
||||||
|
}
|
||||||
|
result.Parameters = cleanParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理并复制菜单按钮数据
|
||||||
|
if len(menu.MenuBtn) > 0 {
|
||||||
|
cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn))
|
||||||
|
for _, btn := range menu.MenuBtn {
|
||||||
|
cleanBtn := system.SysBaseMenuBtn{
|
||||||
|
Name: btn.Name,
|
||||||
|
Desc: btn.Desc,
|
||||||
|
// 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
|
||||||
|
}
|
||||||
|
cleanMenuBtns = append(cleanMenuBtns, cleanBtn)
|
||||||
|
}
|
||||||
|
result.MenuBtn = cleanMenuBtns
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找并处理子菜单
|
||||||
|
var children []system.SysBaseMenu
|
||||||
|
for _, childMenu := range menuMap {
|
||||||
|
if childMenu.ParentId == menu.ID {
|
||||||
|
childData := convertMenuToStruct(*childMenu, menuMap)
|
||||||
|
children = append(children, childData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按sort排序子菜单
|
||||||
|
if len(children) > 0 {
|
||||||
|
sort.Slice(children, func(i, j int) bool {
|
||||||
|
return children[i].Sort < children[j].Sort
|
||||||
|
})
|
||||||
|
result.Children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSysVersion 删除版本管理
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 删除版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body system.SysVersion true "删除版本管理"
|
||||||
|
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||||
|
// @Router /sysVersion/deleteSysVersion [delete]
|
||||||
|
func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) {
|
||||||
|
// 创建业务用Context
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
ID := c.Query("ID")
|
||||||
|
err := sysVersionService.DeleteSysVersion(ctx, ID)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("删除失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OkWithMessage("删除成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSysVersionByIds 批量删除版本管理
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 批量删除版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
|
||||||
|
// @Router /sysVersion/deleteSysVersionByIds [delete]
|
||||||
|
func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) {
|
||||||
|
// 创建业务用Context
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
IDs := c.QueryArray("IDs[]")
|
||||||
|
err := sysVersionService.DeleteSysVersionByIds(ctx, IDs)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("批量删除失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OkWithMessage("批量删除成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSysVersion 用id查询版本管理
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 用id查询版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param ID query uint true "用id查询版本管理"
|
||||||
|
// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功"
|
||||||
|
// @Router /sysVersion/findSysVersion [get]
|
||||||
|
func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) {
|
||||||
|
// 创建业务用Context
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
ID := c.Query("ID")
|
||||||
|
resysVersion, err := sysVersionService.GetSysVersion(ctx, ID)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("查询失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OkWithData(resysVersion, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSysVersionList 分页获取版本管理列表
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 分页获取版本管理列表
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表"
|
||||||
|
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||||
|
// @Router /sysVersion/getSysVersionList [get]
|
||||||
|
func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) {
|
||||||
|
// 创建业务用Context
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var pageInfo systemReq.SysVersionSearch
|
||||||
|
err := c.ShouldBindQuery(&pageInfo)
|
||||||
|
if err != nil {
|
||||||
|
response.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, total, err := sysVersionService.GetSysVersionInfoList(ctx, pageInfo)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OkWithDetailed(response.PageResult{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: pageInfo.Page,
|
||||||
|
PageSize: pageInfo.PageSize,
|
||||||
|
}, "获取成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSysVersionPublic 不需要鉴权的版本管理接口
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 不需要鉴权的版本管理接口
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
|
||||||
|
// @Router /sysVersion/getSysVersionPublic [get]
|
||||||
|
func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) {
|
||||||
|
// 创建业务用Context
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// 此接口不需要鉴权
|
||||||
|
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
|
||||||
|
sysVersionService.GetSysVersionPublic(ctx)
|
||||||
|
response.OkWithDetailed(gin.H{
|
||||||
|
"info": "不需要鉴权的版本管理接口信息",
|
||||||
|
}, "获取成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportVersion 创建发版数据
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 创建发版数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body systemReq.ExportVersionRequest true "创建发版数据"
|
||||||
|
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||||
|
// @Router /sysVersion/exportVersion [post]
|
||||||
|
func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req systemReq.ExportVersionRequest
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的菜单数据
|
||||||
|
var menuData []system.SysBaseMenu
|
||||||
|
if len(req.MenuIds) > 0 {
|
||||||
|
menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("获取菜单数据失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的API数据
|
||||||
|
var apiData []system.SysApi
|
||||||
|
if len(req.ApiIds) > 0 {
|
||||||
|
apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取API数据失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("获取API数据失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理菜单数据,构建递归的children结构
|
||||||
|
processedMenus := buildMenuTree(menuData)
|
||||||
|
|
||||||
|
// 处理API数据,清除ID和时间戳字段
|
||||||
|
processedApis := make([]system.SysApi, 0, len(apiData))
|
||||||
|
for _, api := range apiData {
|
||||||
|
cleanApi := system.SysApi{
|
||||||
|
Path: api.Path,
|
||||||
|
Description: api.Description,
|
||||||
|
ApiGroup: api.ApiGroup,
|
||||||
|
Method: api.Method,
|
||||||
|
}
|
||||||
|
processedApis = append(processedApis, cleanApi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建导出数据
|
||||||
|
exportData := systemRes.ExportVersionResponse{
|
||||||
|
Version: systemReq.VersionInfo{
|
||||||
|
Name: req.VersionName,
|
||||||
|
Code: req.VersionCode,
|
||||||
|
Description: req.Description,
|
||||||
|
ExportTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
},
|
||||||
|
Menus: processedMenus,
|
||||||
|
Apis: processedApis,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为JSON
|
||||||
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("JSON序列化失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存版本记录
|
||||||
|
version := system.SysVersion{
|
||||||
|
VersionName: utils.Pointer(req.VersionName),
|
||||||
|
VersionCode: utils.Pointer(req.VersionCode),
|
||||||
|
Description: utils.Pointer(req.Description),
|
||||||
|
VersionData: utils.Pointer(string(jsonData)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sysVersionService.CreateSysVersion(ctx, &version)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("保存版本记录失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OkWithMessage("创建发版成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadVersionJson 下载版本JSON数据
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 下载版本JSON数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param ID query string true "版本ID"
|
||||||
|
// @Success 200 {object} response.Response{data=object,msg=string} "下载成功"
|
||||||
|
// @Router /sysVersion/downloadVersionJson [get]
|
||||||
|
func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
ID := c.Query("ID")
|
||||||
|
if ID == "" {
|
||||||
|
response.FailWithMessage("版本ID不能为空", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取版本记录
|
||||||
|
version, err := sysVersionService.GetSysVersion(ctx, ID)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("获取版本记录失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建JSON数据
|
||||||
|
var jsonData []byte
|
||||||
|
if version.VersionData != nil && *version.VersionData != "" {
|
||||||
|
jsonData = []byte(*version.VersionData)
|
||||||
|
} else {
|
||||||
|
// 如果没有存储的JSON数据,构建一个基本的结构
|
||||||
|
basicData := systemRes.ExportVersionResponse{
|
||||||
|
Version: systemReq.VersionInfo{
|
||||||
|
Name: *version.VersionName,
|
||||||
|
Code: *version.VersionCode,
|
||||||
|
Description: *version.Description,
|
||||||
|
ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
},
|
||||||
|
Menus: []system.SysBaseMenu{},
|
||||||
|
Apis: []system.SysApi{},
|
||||||
|
}
|
||||||
|
jsonData, _ = json.MarshalIndent(basicData, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置下载响应头
|
||||||
|
filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405"))
|
||||||
|
c.Header("Content-Type", "application/json")
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||||
|
c.Header("Content-Length", strconv.Itoa(len(jsonData)))
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "application/json", jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportVersion 导入版本数据
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 导入版本数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body systemReq.ImportVersionRequest true "版本JSON数据"
|
||||||
|
// @Success 200 {object} response.Response{msg=string} "导入成功"
|
||||||
|
// @Router /sysVersion/importVersion [post]
|
||||||
|
func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// 获取JSON数据
|
||||||
|
var importData systemReq.ImportVersionRequest
|
||||||
|
err := c.ShouldBindJSON(&importData)
|
||||||
|
if err != nil {
|
||||||
|
response.FailWithMessage("解析JSON数据失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数据格式
|
||||||
|
if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" {
|
||||||
|
response.FailWithMessage("版本信息格式错误", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入菜单数据
|
||||||
|
if len(importData.ExportMenu) > 0 {
|
||||||
|
if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil {
|
||||||
|
global.GVA_LOG.Error("导入菜单失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("导入菜单失败: "+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入API数据
|
||||||
|
if len(importData.ExportApi) > 0 {
|
||||||
|
if err := sysVersionService.ImportApis(importData.ExportApi); err != nil {
|
||||||
|
global.GVA_LOG.Error("导入API失败!", zap.Error(err))
|
||||||
|
response.FailWithMessage("导入API失败: "+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导入记录
|
||||||
|
jsonData, _ := json.Marshal(importData)
|
||||||
|
version := system.SysVersion{
|
||||||
|
VersionName: utils.Pointer(importData.VersionInfo.Name),
|
||||||
|
VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))),
|
||||||
|
Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)),
|
||||||
|
VersionData: utils.Pointer(string(jsonData)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sysVersionService.CreateSysVersion(ctx, &version)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err))
|
||||||
|
// 这里不返回错误,因为数据已经导入成功
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OkWithMessage("导入成功", c)
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ func RunServer() {
|
||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
欢迎使用 gin-vue-admin
|
欢迎使用 gin-vue-admin
|
||||||
当前版本:v2.8.3
|
当前版本:v2.8.4
|
||||||
加群方式:微信号:shouzi_1994 QQ群:470239250
|
加群方式:微信号:shouzi_1994 QQ群:470239250
|
||||||
项目地址:https://github.com/flipped-aurora/gin-vue-admin
|
项目地址:https://github.com/flipped-aurora/gin-vue-admin
|
||||||
插件市场:https://plugin.gin-vue-admin.com
|
插件市场:https://plugin.gin-vue-admin.com
|
||||||
|
|
|
||||||
|
|
@ -9296,7 +9296,7 @@ const docTemplate = `{
|
||||||
|
|
||||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
var SwaggerInfo = &swag.Spec{
|
var SwaggerInfo = &swag.Spec{
|
||||||
Version: "v2.8.3",
|
Version: "v2.8.4",
|
||||||
Host: "",
|
Host: "",
|
||||||
BasePath: "",
|
BasePath: "",
|
||||||
Schemes: []string{},
|
Schemes: []string{},
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
|
||||||
sysModel.Condition{},
|
sysModel.Condition{},
|
||||||
sysModel.JoinTemplate{},
|
sysModel.JoinTemplate{},
|
||||||
sysModel.SysParams{},
|
sysModel.SysParams{},
|
||||||
|
sysModel.SysVersion{},
|
||||||
adapter.CasbinRule{},
|
adapter.CasbinRule{},
|
||||||
|
|
||||||
example.ExaFile{},
|
example.ExaFile{},
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ func RegisterTables() {
|
||||||
system.Condition{},
|
system.Condition{},
|
||||||
system.JoinTemplate{},
|
system.JoinTemplate{},
|
||||||
system.SysParams{},
|
system.SysParams{},
|
||||||
|
system.SysVersion{},
|
||||||
|
|
||||||
example.ExaFile{},
|
example.ExaFile{},
|
||||||
example.ExaCustomer{},
|
example.ExaCustomer{},
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ func Routers() *gin.Engine {
|
||||||
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
||||||
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
||||||
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
|
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
|
||||||
|
systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由
|
||||||
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
|
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
|
||||||
systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码
|
systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码
|
||||||
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
|
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import (
|
||||||
// @Tag.Description 用户
|
// @Tag.Description 用户
|
||||||
|
|
||||||
// @title Gin-Vue-Admin Swagger API接口文档
|
// @title Gin-Vue-Admin Swagger API接口文档
|
||||||
// @version v2.8.3
|
// @version v2.8.4
|
||||||
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
||||||
// @securityDefinitions.apikey ApiKeyAuth
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
// @in header
|
// @in header
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package mcpTool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/service"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册工具
|
||||||
|
func init() {
|
||||||
|
RegisterTool(&ApiCreator{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiCreateRequest API创建请求结构
|
||||||
|
type ApiCreateRequest struct {
|
||||||
|
Path string `json:"path"` // API路径
|
||||||
|
Description string `json:"description"` // API中文描述
|
||||||
|
ApiGroup string `json:"apiGroup"` // API组
|
||||||
|
Method string `json:"method"` // HTTP方法
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiCreateResponse API创建响应结构
|
||||||
|
type ApiCreateResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ApiID uint `json:"apiId"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiCreator API创建工具
|
||||||
|
type ApiCreator struct{}
|
||||||
|
|
||||||
|
// New 创建API创建工具
|
||||||
|
func (a *ApiCreator) New() mcp.Tool {
|
||||||
|
return mcp.NewTool("create_api",
|
||||||
|
mcp.WithDescription("创建后端API记录,用于在生成后端接口时自动创建对应的API权限记录,只要创建了API层,router下的文件产生了路径变化等,都需要调用此mcp。"),
|
||||||
|
mcp.WithString("path",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("API路径,如:/user/create"),
|
||||||
|
),
|
||||||
|
mcp.WithString("description",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("API中文描述,如:创建用户"),
|
||||||
|
),
|
||||||
|
mcp.WithString("apiGroup",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("API组名称,用于分类管理,如:用户管理"),
|
||||||
|
),
|
||||||
|
mcp.WithString("method",
|
||||||
|
mcp.Description("HTTP方法"),
|
||||||
|
mcp.DefaultString("POST"),
|
||||||
|
),
|
||||||
|
mcp.WithString("apis",
|
||||||
|
mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理API创建请求
|
||||||
|
func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
args := request.GetArguments()
|
||||||
|
|
||||||
|
var apis []ApiCreateRequest
|
||||||
|
|
||||||
|
// 检查是否是批量创建
|
||||||
|
if apisStr, ok := args["apis"].(string); ok && apisStr != "" {
|
||||||
|
if err := json.Unmarshal([]byte(apisStr), &apis); err != nil {
|
||||||
|
return nil, fmt.Errorf("apis 参数格式错误: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单个API创建
|
||||||
|
path, ok := args["path"].(string)
|
||||||
|
if !ok || path == "" {
|
||||||
|
return nil, errors.New("path 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
description, ok := args["description"].(string)
|
||||||
|
if !ok || description == "" {
|
||||||
|
return nil, errors.New("description 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiGroup, ok := args["apiGroup"].(string)
|
||||||
|
if !ok || apiGroup == "" {
|
||||||
|
return nil, errors.New("apiGroup 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
method := "POST"
|
||||||
|
if val, ok := args["method"].(string); ok && val != "" {
|
||||||
|
method = val
|
||||||
|
}
|
||||||
|
|
||||||
|
apis = append(apis, ApiCreateRequest{
|
||||||
|
Path: path,
|
||||||
|
Description: description,
|
||||||
|
ApiGroup: apiGroup,
|
||||||
|
Method: method,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apis) == 0 {
|
||||||
|
return nil, errors.New("没有要创建的API")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建API记录
|
||||||
|
apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService
|
||||||
|
var responses []ApiCreateResponse
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, apiReq := range apis {
|
||||||
|
api := system.SysApi{
|
||||||
|
Path: apiReq.Path,
|
||||||
|
Description: apiReq.Description,
|
||||||
|
ApiGroup: apiReq.ApiGroup,
|
||||||
|
Method: apiReq.Method,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := apiService.CreateApi(api)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Warn("创建API失败",
|
||||||
|
zap.String("path", apiReq.Path),
|
||||||
|
zap.String("method", apiReq.Method),
|
||||||
|
zap.Error(err))
|
||||||
|
|
||||||
|
responses = append(responses, ApiCreateResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("创建API失败: %v", err),
|
||||||
|
Path: apiReq.Path,
|
||||||
|
Method: apiReq.Method,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 获取创建的API ID
|
||||||
|
var createdApi system.SysApi
|
||||||
|
err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
responses = append(responses, ApiCreateResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
|
||||||
|
ApiID: createdApi.ID,
|
||||||
|
Path: apiReq.Path,
|
||||||
|
Method: apiReq.Method,
|
||||||
|
})
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建总体响应
|
||||||
|
var resultMessage string
|
||||||
|
if len(apis) == 1 {
|
||||||
|
resultMessage = responses[0].Message
|
||||||
|
} else {
|
||||||
|
resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"success": successCount > 0,
|
||||||
|
"message": resultMessage,
|
||||||
|
"totalCount": len(apis),
|
||||||
|
"successCount": successCount,
|
||||||
|
"failedCount": len(apis) - successCount,
|
||||||
|
"details": responses,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultJSON, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
package mcpTool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/service"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterTool(&DictionaryOptionsGenerator{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryOptionsGenerator 字典选项生成器
|
||||||
|
type DictionaryOptionsGenerator struct{}
|
||||||
|
|
||||||
|
// DictionaryGenerateRequest 字典生成请求
|
||||||
|
type DictionaryGenerateRequest struct {
|
||||||
|
DictType string `json:"dictType"` // 字典类型
|
||||||
|
FieldDesc string `json:"fieldDesc"` // 字段描述
|
||||||
|
Options []DictionaryOption `json:"options"` // AI生成的字典选项
|
||||||
|
DictName string `json:"dictName"` // 字典名称(可选)
|
||||||
|
Description string `json:"description"` // 字典描述(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryGenerateResponse 字典生成响应
|
||||||
|
type DictionaryGenerateResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
DictType string `json:"dictType"`
|
||||||
|
OptionsCount int `json:"optionsCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 返回工具注册信息
|
||||||
|
func (d *DictionaryOptionsGenerator) New() mcp.Tool {
|
||||||
|
return mcp.NewTool("generate_dictionary_options",
|
||||||
|
mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"),
|
||||||
|
mcp.WithString("dictType",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("字典类型,用于标识字典的唯一性"),
|
||||||
|
),
|
||||||
|
mcp.WithString("fieldDesc",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("字段描述,用于AI理解字段含义"),
|
||||||
|
),
|
||||||
|
mcp.WithString("options",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"),
|
||||||
|
),
|
||||||
|
mcp.WithString("dictName",
|
||||||
|
mcp.Description("字典名称,如果不提供将自动生成"),
|
||||||
|
),
|
||||||
|
mcp.WithString("description",
|
||||||
|
mcp.Description("字典描述"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name 返回工具名称
|
||||||
|
func (d *DictionaryOptionsGenerator) Name() string {
|
||||||
|
return "generate_dictionary_options"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description 返回工具描述
|
||||||
|
func (d *DictionaryOptionsGenerator) Description() string {
|
||||||
|
return `字典选项生成工具 - 让AI生成并创建字典选项
|
||||||
|
|
||||||
|
此工具允许AI根据字典类型和字段描述生成合适的字典选项,并自动创建字典和字典详情。
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
- dictType: 字典类型(必填)
|
||||||
|
- fieldDesc: 字段描述(必填)
|
||||||
|
- options: AI生成的字典选项数组(必填)
|
||||||
|
- label: 选项标签
|
||||||
|
- value: 选项值
|
||||||
|
- sort: 排序号
|
||||||
|
- dictName: 字典名称(可选,默认根据fieldDesc生成)
|
||||||
|
- description: 字典描述(可选)
|
||||||
|
|
||||||
|
使用场景:
|
||||||
|
1. 在创建模块时,如果字段需要字典类型,AI可以根据字段描述智能生成合适的选项
|
||||||
|
2. 支持各种业务场景的字典选项生成,如状态、类型、等级等
|
||||||
|
3. 自动创建字典和字典详情,无需手动配置
|
||||||
|
|
||||||
|
示例调用:
|
||||||
|
{
|
||||||
|
"dictType": "user_status",
|
||||||
|
"fieldDesc": "用户状态",
|
||||||
|
"options": [
|
||||||
|
{"label": "正常", "value": "1", "sort": 1},
|
||||||
|
{"label": "禁用", "value": "0", "sort": 2}
|
||||||
|
],
|
||||||
|
"dictName": "用户状态字典",
|
||||||
|
"description": "用于管理用户账户状态的字典"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSchema 返回输入参数的JSON Schema
|
||||||
|
func (d *DictionaryOptionsGenerator) InputSchema() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"dictType": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "字典类型,用于标识字典的唯一性",
|
||||||
|
},
|
||||||
|
"fieldDesc": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "字段描述,用于生成字典名称和理解字典用途",
|
||||||
|
},
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"description": "AI生成的字典选项数组",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"label": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "选项标签,显示给用户的文本",
|
||||||
|
},
|
||||||
|
"value": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "选项值,存储在数据库中的值",
|
||||||
|
},
|
||||||
|
"sort": map[string]interface{}{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "排序号,用于控制选项显示顺序",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"label", "value", "sort"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dictName": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "字典名称,可选,默认根据fieldDesc生成",
|
||||||
|
},
|
||||||
|
"description": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "字典描述,可选",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"dictType", "fieldDesc", "options"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理工具调用
|
||||||
|
func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
// 解析请求参数
|
||||||
|
args := request.GetArguments()
|
||||||
|
|
||||||
|
dictType, ok := args["dictType"].(string)
|
||||||
|
if !ok || dictType == "" {
|
||||||
|
return nil, errors.New("dictType 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDesc, ok := args["fieldDesc"].(string)
|
||||||
|
if !ok || fieldDesc == "" {
|
||||||
|
return nil, errors.New("fieldDesc 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsStr, ok := args["options"].(string)
|
||||||
|
if !ok || optionsStr == "" {
|
||||||
|
return nil, errors.New("options 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析options JSON字符串
|
||||||
|
var options []DictionaryOption
|
||||||
|
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
|
||||||
|
return nil, fmt.Errorf("options 参数格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, errors.New("options 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选参数
|
||||||
|
dictName, _ := args["dictName"].(string)
|
||||||
|
description, _ := args["description"].(string)
|
||||||
|
|
||||||
|
// 构建请求对象
|
||||||
|
req := &DictionaryGenerateRequest{
|
||||||
|
DictType: dictType,
|
||||||
|
FieldDesc: fieldDesc,
|
||||||
|
Options: options,
|
||||||
|
DictName: dictName,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建字典
|
||||||
|
response, err := d.createDictionaryWithOptions(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应
|
||||||
|
resultJSON, err := json.MarshalIndent(response, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: []mcp.Content{
|
||||||
|
mcp.TextContent{
|
||||||
|
Type: "text",
|
||||||
|
Text: fmt.Sprintf("字典选项生成结果:\n\n%s", string(resultJSON)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDictionaryWithOptions 创建字典和字典选项
|
||||||
|
func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) {
|
||||||
|
// 检查字典是否已存在
|
||||||
|
exists, err := d.checkDictionaryExists(req.DictType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("检查字典是否存在失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return &DictionaryGenerateResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType),
|
||||||
|
DictType: req.DictType,
|
||||||
|
OptionsCount: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成字典名称
|
||||||
|
dictName := req.DictName
|
||||||
|
if dictName == "" {
|
||||||
|
dictName = d.generateDictionaryName(req.DictType, req.FieldDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建字典
|
||||||
|
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
|
||||||
|
dictionary := system.SysDictionary{
|
||||||
|
Name: dictName,
|
||||||
|
Type: req.DictType,
|
||||||
|
Status: &[]bool{true}[0], // 默认启用
|
||||||
|
Desc: req.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dictionaryService.CreateSysDictionary(dictionary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建字典失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取刚创建的字典ID
|
||||||
|
var createdDict system.SysDictionary
|
||||||
|
err = global.GVA_DB.Where("type = ?", req.DictType).First(&createdDict).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取创建的字典失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建字典详情项
|
||||||
|
dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, option := range req.Options {
|
||||||
|
dictionaryDetail := system.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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DictionaryGenerateResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount),
|
||||||
|
DictType: req.DictType,
|
||||||
|
OptionsCount: successCount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDictionaryExists 检查字典是否存在
|
||||||
|
func (d *DictionaryOptionsGenerator) checkDictionaryExists(dictType string) (bool, error) {
|
||||||
|
var dictionary system.SysDictionary
|
||||||
|
err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, nil // 字典不存在
|
||||||
|
}
|
||||||
|
return false, err // 其他错误
|
||||||
|
}
|
||||||
|
return true, nil // 字典存在
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDictionaryName 生成字典名称
|
||||||
|
func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string {
|
||||||
|
if fieldDesc != "" {
|
||||||
|
return fmt.Sprintf("%s字典", fieldDesc)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s字典", dictType)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
package mcpTool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/service"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册工具
|
||||||
|
func init() {
|
||||||
|
RegisterTool(&DictionaryQuery{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryInfo 字典信息结构
|
||||||
|
type DictionaryInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"` // 字典名(中)
|
||||||
|
Type string `json:"type"` // 字典名(英)
|
||||||
|
Status *bool `json:"status"` // 状态
|
||||||
|
Desc string `json:"desc"` // 描述
|
||||||
|
Details []DictionaryDetailInfo `json:"details"` // 字典详情
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryDetailInfo 字典详情信息结构
|
||||||
|
type DictionaryDetailInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Label string `json:"label"` // 展示值
|
||||||
|
Value string `json:"value"` // 字典值
|
||||||
|
Extend string `json:"extend"` // 扩展值
|
||||||
|
Status *bool `json:"status"` // 启用状态
|
||||||
|
Sort int `json:"sort"` // 排序标记
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryQueryResponse 字典查询响应结构
|
||||||
|
type DictionaryQueryResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Dictionaries []DictionaryInfo `json:"dictionaries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DictionaryQuery 字典查询工具
|
||||||
|
type DictionaryQuery struct{}
|
||||||
|
|
||||||
|
// New 创建字典查询工具
|
||||||
|
func (d *DictionaryQuery) New() mcp.Tool {
|
||||||
|
return mcp.NewTool("query_dictionaries",
|
||||||
|
mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"),
|
||||||
|
mcp.WithString("dictType",
|
||||||
|
mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("includeDisabled",
|
||||||
|
mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("detailsOnly",
|
||||||
|
mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: []mcp.Content{
|
||||||
|
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为响应格式
|
||||||
|
dictInfo := DictionaryInfo{
|
||||||
|
ID: sysDictionary.ID,
|
||||||
|
Name: sysDictionary.Name,
|
||||||
|
Type: sysDictionary.Type,
|
||||||
|
Status: sysDictionary.Status,
|
||||||
|
Desc: sysDictionary.Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字典详情
|
||||||
|
for _, detail := range sysDictionary.SysDictionaryDetails {
|
||||||
|
if includeDisabled || (detail.Status != nil && *detail.Status) {
|
||||||
|
dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
|
||||||
|
ID: detail.ID,
|
||||||
|
Label: detail.Label,
|
||||||
|
Value: detail.Value,
|
||||||
|
Extend: detail.Extend,
|
||||||
|
Status: detail.Status,
|
||||||
|
Sort: detail.Sort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
} else {
|
||||||
|
return db.Where("status = ?", true).Order("sort")
|
||||||
|
}
|
||||||
|
}).Find(&sysDictionaries).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("查询字典列表失败", zap.Error(err))
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: []mcp.Content{
|
||||||
|
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典列表失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为响应格式
|
||||||
|
for _, dict := range sysDictionaries {
|
||||||
|
dictInfo := DictionaryInfo{
|
||||||
|
ID: dict.ID,
|
||||||
|
Name: dict.Name,
|
||||||
|
Type: dict.Type,
|
||||||
|
Status: dict.Status,
|
||||||
|
Desc: dict.Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字典详情
|
||||||
|
for _, detail := range dict.SysDictionaryDetails {
|
||||||
|
if includeDisabled || (detail.Status != nil && *detail.Status) {
|
||||||
|
dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
|
||||||
|
ID: detail.ID,
|
||||||
|
Label: detail.Label,
|
||||||
|
Value: detail.Value,
|
||||||
|
Extend: detail.Extend,
|
||||||
|
Status: detail.Status,
|
||||||
|
Sort: detail.Sort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
mcp.NewTextContent(string(responseJSON)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应
|
||||||
|
response := DictionaryQueryResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "查询字典成功",
|
||||||
|
Total: len(dictionaries),
|
||||||
|
Dictionaries: dictionaries,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJSON, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("序列化响应失败", zap.Error(err))
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: []mcp.Content{
|
||||||
|
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "序列化响应失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: []mcp.Content{
|
||||||
|
mcp.NewTextContent(string(responseJSON)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
# ExecutionPlan 结构体格式说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。
|
||||||
|
|
||||||
|
## 完整结构体定义
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ExecutionPlan struct {
|
||||||
|
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
|
||||||
|
ModuleName string `json:"moduleName"` // 模块名,通常与结构体名相同
|
||||||
|
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"` // 索引类型
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 示例1:创建新包和模块
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageName": "user",
|
||||||
|
"moduleName": "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": null,
|
||||||
|
"checkDataSource": false,
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:仅在现有包中创建模块
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageName": "system",
|
||||||
|
"moduleName": "Role",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 重要注意事项
|
||||||
|
|
||||||
|
1. **PackageType**: 只能是 "plugin" 或 "package"
|
||||||
|
2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供
|
||||||
|
3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供
|
||||||
|
4. **字段类型**: FieldType支持的类型包括:string, int, int64, float64, bool, time.Time, enum, picture, video, file, pictures, array, richtext, json等
|
||||||
|
5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等
|
||||||
|
6. **索引类型**: FieldIndexType支持:index, unique等
|
||||||
|
7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段
|
||||||
|
|
||||||
|
## 常见错误避免
|
||||||
|
|
||||||
|
1. 确保PackageName和ModuleName符合Go语言命名规范
|
||||||
|
2. 字段名使用大写开头的驼峰命名
|
||||||
|
3. JSON标签使用小写开头的驼峰命名
|
||||||
|
4. 数据库列名使用下划线分隔的小写命名
|
||||||
|
5. 必填字段不要遗漏
|
||||||
|
6. 字段类型要与实际需求匹配
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
# GAG工具使用示例 - 带用户确认流程
|
||||||
|
|
||||||
|
## 新的工作流程
|
||||||
|
|
||||||
|
现在GAG工具支持三步工作流程:
|
||||||
|
1. `analyze` - 分析现有模块信息
|
||||||
|
2. `confirm` - 请求用户确认创建计划
|
||||||
|
3. `execute` - 执行创建操作(需要用户确认)
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 第一步:分析
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "analyze",
|
||||||
|
"requirement": "创建一个图书管理功能"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第二步:确认
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "confirm",
|
||||||
|
"executionPlan": {
|
||||||
|
"packageName": "library",
|
||||||
|
"moduleName": "Book",
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三步:执行(需要确认参数)
|
||||||
|
```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. 如果不需要创建包或模块,则不需要提供对应的确认参数
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,283 @@
|
||||||
|
package mcpTool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/service"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册工具
|
||||||
|
func init() {
|
||||||
|
RegisterTool(&MenuCreator{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuCreateRequest 菜单创建请求结构
|
||||||
|
type MenuCreateRequest struct {
|
||||||
|
ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单
|
||||||
|
Path string `json:"path"` // 路由path
|
||||||
|
Name string `json:"name"` // 路由name
|
||||||
|
Hidden bool `json:"hidden"` // 是否在列表隐藏
|
||||||
|
Component string `json:"component"` // 对应前端文件路径
|
||||||
|
Sort int `json:"sort"` // 排序标记
|
||||||
|
Title string `json:"title"` // 菜单名
|
||||||
|
Icon string `json:"icon"` // 菜单图标
|
||||||
|
KeepAlive bool `json:"keepAlive"` // 是否缓存
|
||||||
|
DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由
|
||||||
|
CloseTab bool `json:"closeTab"` // 自动关闭tab
|
||||||
|
ActiveName string `json:"activeName"` // 高亮菜单
|
||||||
|
Parameters []MenuParameterRequest `json:"parameters"` // 路由参数
|
||||||
|
MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuParameterRequest 菜单参数请求结构
|
||||||
|
type MenuParameterRequest struct {
|
||||||
|
Type string `json:"type"` // 参数类型:params或query
|
||||||
|
Key string `json:"key"` // 参数key
|
||||||
|
Value string `json:"value"` // 参数值
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuButtonRequest 菜单按钮请求结构
|
||||||
|
type MenuButtonRequest struct {
|
||||||
|
Name string `json:"name"` // 按钮名称
|
||||||
|
Desc string `json:"desc"` // 按钮描述
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuCreateResponse 菜单创建响应结构
|
||||||
|
type MenuCreateResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
MenuID uint `json:"menuId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuCreator 菜单创建工具
|
||||||
|
type MenuCreator struct{}
|
||||||
|
|
||||||
|
// New 创建菜单创建工具
|
||||||
|
func (m *MenuCreator) New() mcp.Tool {
|
||||||
|
return mcp.NewTool("create_menu",
|
||||||
|
mcp.WithDescription("创建前端菜单记录,用于在生成前端页面时自动创建对应的菜单项,只要前端有页面生成,都需要调用此mcp。"),
|
||||||
|
mcp.WithNumber("parentId",
|
||||||
|
mcp.Description("父菜单ID,0表示根菜单"),
|
||||||
|
mcp.DefaultNumber(0),
|
||||||
|
),
|
||||||
|
mcp.WithString("path",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("路由path,如:userList"),
|
||||||
|
),
|
||||||
|
mcp.WithString("name",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("路由name,用于Vue Router,如:userList"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("hidden",
|
||||||
|
mcp.Description("是否在菜单列表中隐藏"),
|
||||||
|
),
|
||||||
|
mcp.WithString("component",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"),
|
||||||
|
),
|
||||||
|
mcp.WithNumber("sort",
|
||||||
|
mcp.Description("菜单排序号,数字越小越靠前"),
|
||||||
|
mcp.DefaultNumber(1),
|
||||||
|
),
|
||||||
|
mcp.WithString("title",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("菜单显示标题"),
|
||||||
|
),
|
||||||
|
mcp.WithString("icon",
|
||||||
|
mcp.Description("菜单图标名称"),
|
||||||
|
mcp.DefaultString("menu"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("keepAlive",
|
||||||
|
mcp.Description("是否缓存页面"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("defaultMenu",
|
||||||
|
mcp.Description("是否是基础路由"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("closeTab",
|
||||||
|
mcp.Description("是否自动关闭tab"),
|
||||||
|
),
|
||||||
|
mcp.WithString("activeName",
|
||||||
|
mcp.Description("高亮菜单名称"),
|
||||||
|
),
|
||||||
|
mcp.WithString("parameters",
|
||||||
|
mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"),
|
||||||
|
),
|
||||||
|
mcp.WithString("menuBtn",
|
||||||
|
mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理菜单创建请求
|
||||||
|
func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
// 解析请求参数
|
||||||
|
args := request.GetArguments()
|
||||||
|
|
||||||
|
// 必需参数
|
||||||
|
path, ok := args["path"].(string)
|
||||||
|
if !ok || path == "" {
|
||||||
|
return nil, errors.New("path 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := args["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return nil, errors.New("name 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
component, ok := args["component"].(string)
|
||||||
|
if !ok || component == "" {
|
||||||
|
return nil, errors.New("component 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
title, ok := args["title"].(string)
|
||||||
|
if !ok || title == "" {
|
||||||
|
return nil, errors.New("title 参数是必需的")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选参数
|
||||||
|
parentId := uint(0)
|
||||||
|
if val, ok := args["parentId"].(float64); ok {
|
||||||
|
parentId = uint(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden := false
|
||||||
|
if val, ok := args["hidden"].(bool); ok {
|
||||||
|
hidden = val
|
||||||
|
}
|
||||||
|
|
||||||
|
sort := 1
|
||||||
|
if val, ok := args["sort"].(float64); ok {
|
||||||
|
sort = int(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := "menu"
|
||||||
|
if val, ok := args["icon"].(string); ok && val != "" {
|
||||||
|
icon = val
|
||||||
|
}
|
||||||
|
|
||||||
|
keepAlive := false
|
||||||
|
if val, ok := args["keepAlive"].(bool); ok {
|
||||||
|
keepAlive = val
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMenu := false
|
||||||
|
if val, ok := args["defaultMenu"].(bool); ok {
|
||||||
|
defaultMenu = val
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTab := false
|
||||||
|
if val, ok := args["closeTab"].(bool); ok {
|
||||||
|
closeTab = val
|
||||||
|
}
|
||||||
|
|
||||||
|
activeName := ""
|
||||||
|
if val, ok := args["activeName"].(string); ok {
|
||||||
|
activeName = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析参数和按钮
|
||||||
|
var parameters []system.SysBaseMenuParameter
|
||||||
|
if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" {
|
||||||
|
var paramReqs []MenuParameterRequest
|
||||||
|
if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil {
|
||||||
|
return nil, fmt.Errorf("parameters 参数格式错误: %v", err)
|
||||||
|
}
|
||||||
|
for _, param := range paramReqs {
|
||||||
|
parameters = append(parameters, system.SysBaseMenuParameter{
|
||||||
|
Type: param.Type,
|
||||||
|
Key: param.Key,
|
||||||
|
Value: param.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuBtn []system.SysBaseMenuBtn
|
||||||
|
if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" {
|
||||||
|
var btnReqs []MenuButtonRequest
|
||||||
|
if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil {
|
||||||
|
return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err)
|
||||||
|
}
|
||||||
|
for _, btn := range btnReqs {
|
||||||
|
menuBtn = append(menuBtn, system.SysBaseMenuBtn{
|
||||||
|
Name: btn.Name,
|
||||||
|
Desc: btn.Desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建菜单对象
|
||||||
|
menu := system.SysBaseMenu{
|
||||||
|
ParentId: parentId,
|
||||||
|
Path: path,
|
||||||
|
Name: name,
|
||||||
|
Hidden: hidden,
|
||||||
|
Component: component,
|
||||||
|
Sort: sort,
|
||||||
|
Meta: system.Meta{
|
||||||
|
Title: title,
|
||||||
|
Icon: icon,
|
||||||
|
KeepAlive: keepAlive,
|
||||||
|
DefaultMenu: defaultMenu,
|
||||||
|
CloseTab: closeTab,
|
||||||
|
ActiveName: activeName,
|
||||||
|
},
|
||||||
|
Parameters: parameters,
|
||||||
|
MenuBtn: menuBtn,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建菜单
|
||||||
|
menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService
|
||||||
|
err := menuService.AddBaseMenu(menu)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建菜单失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取创建的菜单ID
|
||||||
|
var createdMenu system.SysBaseMenu
|
||||||
|
err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应
|
||||||
|
response := &MenuCreateResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("成功创建菜单 %s", title),
|
||||||
|
MenuID: createdMenu.ID,
|
||||||
|
Name: name,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultJSON, err := json.MarshalIndent(response, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ func JWTAuth() gin.HandlerFunc {
|
||||||
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
|
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
|
||||||
token := utils.GetToken(c)
|
token := utils.GetToken(c)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
response.NoAuth("未登录或非法访问", c)
|
response.NoAuth("未登录或非法访问,请登录", c)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ func JWTAuth() gin.HandlerFunc {
|
||||||
claims, err := j.ParseToken(token)
|
claims, err := j.ParseToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, utils.TokenExpired) {
|
if errors.Is(err, utils.TokenExpired) {
|
||||||
response.NoAuth("授权已过期", c)
|
response.NoAuth("登录已过期,请重新登录", c)
|
||||||
utils.ClearToken(c)
|
utils.ClearToken(c)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ type ChangeUserInfo struct {
|
||||||
AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID
|
AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID
|
||||||
Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱
|
Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱
|
||||||
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像
|
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像
|
||||||
SideMode string `json:"sideMode" gorm:"comment:用户侧边主题"` // 用户侧边主题
|
|
||||||
Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户
|
Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户
|
||||||
Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"`
|
Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysVersionSearch struct {
|
||||||
|
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
|
||||||
|
VersionName *string `json:"versionName" form:"versionName"`
|
||||||
|
VersionCode *string `json:"versionCode" form:"versionCode"`
|
||||||
|
request.PageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportVersionRequest 导出版本请求结构体
|
||||||
|
type ExportVersionRequest struct {
|
||||||
|
VersionName string `json:"versionName" binding:"required"` // 版本名称
|
||||||
|
VersionCode string `json:"versionCode" binding:"required"` // 版本号
|
||||||
|
Description string `json:"description"` // 版本描述
|
||||||
|
MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表
|
||||||
|
ApiIds []uint `json:"apiIds"` // 选中的API ID列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportVersionRequest 导入版本请求结构体
|
||||||
|
type ImportVersionRequest struct {
|
||||||
|
VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
|
||||||
|
ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
|
||||||
|
ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionInfo 版本信息结构体
|
||||||
|
type VersionInfo struct {
|
||||||
|
Name string `json:"name" binding:"required"` // 版本名称
|
||||||
|
Code string `json:"code" binding:"required"` // 版本号
|
||||||
|
Description string `json:"description"` // 版本描述
|
||||||
|
ExportTime string `json:"exportTime"` // 导出时间
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExportVersionResponse 导出版本响应结构体
|
||||||
|
type ExportVersionResponse struct {
|
||||||
|
Version request.VersionInfo `json:"version"` // 版本信息
|
||||||
|
Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
|
||||||
|
Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 自动生成模板SysVersion
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 版本管理 结构体 SysVersion
|
||||||
|
type SysVersion struct {
|
||||||
|
global.GVA_MODEL
|
||||||
|
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
|
||||||
|
VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
|
||||||
|
Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
|
||||||
|
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 版本管理 SysVersion自定义表名 sys_versions
|
||||||
|
func (SysVersion) TableName() string {
|
||||||
|
return "sys_versions"
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ type RouterGroup struct {
|
||||||
AuthorityBtnRouter
|
AuthorityBtnRouter
|
||||||
SysExportTemplateRouter
|
SysExportTemplateRouter
|
||||||
SysParamsRouter
|
SysParamsRouter
|
||||||
|
SysVersionRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -41,4 +42,5 @@ var (
|
||||||
dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi
|
dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi
|
||||||
autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi
|
autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi
|
||||||
exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi
|
exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi
|
||||||
|
sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysVersionRouter struct{}
|
||||||
|
|
||||||
|
// InitSysVersionRouter 初始化 版本管理 路由信息
|
||||||
|
func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) {
|
||||||
|
sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord())
|
||||||
|
sysVersionRouterWithoutRecord := Router.Group("sysVersion")
|
||||||
|
{
|
||||||
|
sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理
|
||||||
|
sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理
|
||||||
|
sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据
|
||||||
|
sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据
|
||||||
|
}
|
||||||
|
{
|
||||||
|
sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理
|
||||||
|
sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表
|
||||||
|
sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -208,7 +208,8 @@ func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = global.GVA_DB.Find(&menus, "id in (?)", menuInfo.Menus).Error
|
// 查询菜单及其关联的参数和按钮
|
||||||
|
err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type ServiceGroup struct {
|
||||||
AuthorityBtnService
|
AuthorityBtnService
|
||||||
SysExportTemplateService
|
SysExportTemplateService
|
||||||
SysParamsService
|
SysParamsService
|
||||||
|
SysVersionService
|
||||||
AutoCodePlugin autoCodePlugin
|
AutoCodePlugin autoCodePlugin
|
||||||
AutoCodePackage autoCodePackage
|
AutoCodePackage autoCodePackage
|
||||||
AutoCodeHistory autoCodeHistory
|
AutoCodeHistory autoCodeHistory
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ package system
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
"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"
|
||||||
|
|
@ -63,20 +64,20 @@ func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysU
|
||||||
//@function: ChangePassword
|
//@function: ChangePassword
|
||||||
//@description: 修改用户密码
|
//@description: 修改用户密码
|
||||||
//@param: u *model.SysUser, newPassword string
|
//@param: u *model.SysUser, newPassword string
|
||||||
//@return: userInter *model.SysUser,err error
|
//@return: err error
|
||||||
|
|
||||||
func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (userInter *system.SysUser, err error) {
|
func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (err error) {
|
||||||
var user system.SysUser
|
var user system.SysUser
|
||||||
if err = global.GVA_DB.Where("id = ?", u.ID).First(&user).Error; err != nil {
|
err = global.GVA_DB.Select("id, password").Where("id = ?", u.ID).First(&user).Error
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if ok := utils.BcryptCheck(u.Password, user.Password); !ok {
|
if ok := utils.BcryptCheck(u.Password, user.Password); !ok {
|
||||||
return nil, errors.New("原密码错误")
|
return errors.New("原密码错误")
|
||||||
}
|
}
|
||||||
user.Password = utils.BcryptHash(newPassword)
|
pwd := utils.BcryptHash(newPassword)
|
||||||
err = global.GVA_DB.Save(&user).Error
|
err = global.GVA_DB.Model(&user).Update("password", pwd).Error
|
||||||
return &user, err
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@author: [piexlmax](https://github.com/piexlmax)
|
//@author: [piexlmax](https://github.com/piexlmax)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
|
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SysVersionService struct{}
|
||||||
|
|
||||||
|
// CreateSysVersion 创建版本管理记录
|
||||||
|
// Author [yourname](https://github.com/yourname)
|
||||||
|
func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) {
|
||||||
|
err = global.GVA_DB.Create(sysVersion).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSysVersion 删除版本管理记录
|
||||||
|
// Author [yourname](https://github.com/yourname)
|
||||||
|
func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) {
|
||||||
|
err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSysVersionByIds 批量删除版本管理记录
|
||||||
|
// Author [yourname](https://github.com/yourname)
|
||||||
|
func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) {
|
||||||
|
err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSysVersion 根据ID获取版本管理记录
|
||||||
|
// Author [yourname](https://github.com/yourname)
|
||||||
|
func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) {
|
||||||
|
err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSysVersionInfoList 分页获取版本管理记录
|
||||||
|
// Author [yourname](https://github.com/yourname)
|
||||||
|
func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) {
|
||||||
|
limit := info.PageSize
|
||||||
|
offset := info.PageSize * (info.Page - 1)
|
||||||
|
// 创建db
|
||||||
|
db := global.GVA_DB.Model(&system.SysVersion{})
|
||||||
|
var sysVersions []system.SysVersion
|
||||||
|
// 如果有条件搜索 下方会自动创建搜索语句
|
||||||
|
if len(info.CreatedAtRange) == 2 {
|
||||||
|
db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.VersionName != nil && *info.VersionName != "" {
|
||||||
|
db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%")
|
||||||
|
}
|
||||||
|
if info.VersionCode != nil && *info.VersionCode != "" {
|
||||||
|
db = db.Where("version_code = ?", *info.VersionCode)
|
||||||
|
}
|
||||||
|
err = db.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit != 0 {
|
||||||
|
db = db.Limit(limit).Offset(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Find(&sysVersions).Error
|
||||||
|
return sysVersions, total, err
|
||||||
|
}
|
||||||
|
func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) {
|
||||||
|
// 此方法为获取数据源定义的数据
|
||||||
|
// 请自行实现
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMenusByIds 根据ID列表获取菜单数据
|
||||||
|
func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) {
|
||||||
|
err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApisByIds 根据ID列表获取API数据
|
||||||
|
func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) {
|
||||||
|
err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportMenus 导入菜单数据
|
||||||
|
func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error {
|
||||||
|
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 递归创建菜单
|
||||||
|
return sysVersionService.createMenusRecursively(tx, menus, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMenusRecursively 递归创建菜单
|
||||||
|
func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error {
|
||||||
|
for _, menu := range menus {
|
||||||
|
// 检查菜单是否已存在
|
||||||
|
var existingMenu system.SysBaseMenu
|
||||||
|
if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil {
|
||||||
|
// 菜单已存在,使用现有菜单ID继续处理子菜单
|
||||||
|
if len(menu.Children) > 0 {
|
||||||
|
if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存参数和按钮数据,稍后处理
|
||||||
|
parameters := menu.Parameters
|
||||||
|
menuBtns := menu.MenuBtn
|
||||||
|
children := menu.Children
|
||||||
|
|
||||||
|
// 创建新菜单(不包含关联数据)
|
||||||
|
newMenu := system.SysBaseMenu{
|
||||||
|
ParentId: parentId,
|
||||||
|
Path: menu.Path,
|
||||||
|
Name: menu.Name,
|
||||||
|
Hidden: menu.Hidden,
|
||||||
|
Component: menu.Component,
|
||||||
|
Sort: menu.Sort,
|
||||||
|
Meta: menu.Meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newMenu).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建参数
|
||||||
|
if len(parameters) > 0 {
|
||||||
|
for _, param := range parameters {
|
||||||
|
newParam := system.SysBaseMenuParameter{
|
||||||
|
SysBaseMenuID: newMenu.ID,
|
||||||
|
Type: param.Type,
|
||||||
|
Key: param.Key,
|
||||||
|
Value: param.Value,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&newParam).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建菜单按钮
|
||||||
|
if len(menuBtns) > 0 {
|
||||||
|
for _, btn := range menuBtns {
|
||||||
|
newBtn := system.SysBaseMenuBtn{
|
||||||
|
SysBaseMenuID: newMenu.ID,
|
||||||
|
Name: btn.Name,
|
||||||
|
Desc: btn.Desc,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&newBtn).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子菜单
|
||||||
|
if len(children) > 0 {
|
||||||
|
if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportApis 导入API数据
|
||||||
|
func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error {
|
||||||
|
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, api := range apis {
|
||||||
|
// 检查API是否已存在
|
||||||
|
var existingApi system.SysApi
|
||||||
|
if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil {
|
||||||
|
// API已存在,跳过
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新API
|
||||||
|
newApi := system.SysApi{
|
||||||
|
Path: api.Path,
|
||||||
|
Description: api.Description,
|
||||||
|
ApiGroup: api.ApiGroup,
|
||||||
|
Method: api.Method,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newApi).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
|
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -188,6 +189,14 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||||
{ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"},
|
{ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"},
|
||||||
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"},
|
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"},
|
||||||
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"},
|
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"},
|
||||||
|
|
||||||
|
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"},
|
||||||
|
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"},
|
||||||
|
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"},
|
||||||
|
{ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"},
|
||||||
|
{ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"},
|
||||||
|
{ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"},
|
||||||
|
{ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"},
|
||||||
}
|
}
|
||||||
if err := db.Create(&entities).Error; err != nil {
|
if err := db.Create(&entities).Error; err != nil {
|
||||||
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,14 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||||
{Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"},
|
||||||
{Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"},
|
||||||
|
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/findSysVersion", V2: "GET"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"},
|
||||||
|
{Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"},
|
||||||
|
|
||||||
{Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"},
|
||||||
{Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"},
|
||||||
{Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"},
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}},
|
||||||
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}},
|
||||||
|
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,81 @@ func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加菜单参数
|
||||||
|
if len(menus[i].Parameters) > 0 {
|
||||||
|
var paramElts []ast.Expr
|
||||||
|
for _, param := range menus[i].Parameters {
|
||||||
|
paramElts = append(paramElts, &ast.CompositeLit{
|
||||||
|
Type: &ast.SelectorExpr{
|
||||||
|
X: &ast.Ident{Name: "model"},
|
||||||
|
Sel: &ast.Ident{Name: "SysBaseMenuParameter"},
|
||||||
|
},
|
||||||
|
Elts: []ast.Expr{
|
||||||
|
&ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Type"},
|
||||||
|
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Type)},
|
||||||
|
},
|
||||||
|
&ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Key"},
|
||||||
|
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Key)},
|
||||||
|
},
|
||||||
|
&ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Value"},
|
||||||
|
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Value)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elts = append(elts, &ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Parameters"},
|
||||||
|
Value: &ast.CompositeLit{
|
||||||
|
Type: &ast.ArrayType{
|
||||||
|
Elt: &ast.SelectorExpr{
|
||||||
|
X: &ast.Ident{Name: "model"},
|
||||||
|
Sel: &ast.Ident{Name: "SysBaseMenuParameter"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Elts: paramElts,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加菜单按钮
|
||||||
|
if len(menus[i].MenuBtn) > 0 {
|
||||||
|
var btnElts []ast.Expr
|
||||||
|
for _, btn := range menus[i].MenuBtn {
|
||||||
|
btnElts = append(btnElts, &ast.CompositeLit{
|
||||||
|
Type: &ast.SelectorExpr{
|
||||||
|
X: &ast.Ident{Name: "model"},
|
||||||
|
Sel: &ast.Ident{Name: "SysBaseMenuBtn"},
|
||||||
|
},
|
||||||
|
Elts: []ast.Expr{
|
||||||
|
&ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Name"},
|
||||||
|
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Name)},
|
||||||
|
},
|
||||||
|
&ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "Desc"},
|
||||||
|
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Desc)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elts = append(elts, &ast.KeyValueExpr{
|
||||||
|
Key: &ast.Ident{Name: "MenuBtn"},
|
||||||
|
Value: &ast.CompositeLit{
|
||||||
|
Type: &ast.ArrayType{
|
||||||
|
Elt: &ast.SelectorExpr{
|
||||||
|
X: &ast.Ident{Name: "model"},
|
||||||
|
Sel: &ast.Ident{Name: "SysBaseMenuBtn"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Elts: btnElts,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
menuElts = append(menuElts, &ast.CompositeLit{
|
menuElts = append(menuElts, &ast.CompositeLit{
|
||||||
Type: nil,
|
Type: nil,
|
||||||
Elts: elts,
|
Elts: elts,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gin-vue-admin",
|
"name": "gin-vue-admin",
|
||||||
"version": "2.8.2",
|
"version": "2.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "node openDocument.js && vite --host --mode development",
|
"serve": "node openDocument.js && vite --host --mode development",
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div id="app" class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800">
|
||||||
id="app"
|
|
||||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800"
|
|
||||||
>
|
|
||||||
<el-config-provider :locale="zhCn">
|
<el-config-provider :locale="zhCn">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<Application />
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
import { useAppStore } from '@/pinia'
|
import Application from '@/components/application/index.vue'
|
||||||
useAppStore()
|
import { useAppStore } from '@/pinia'
|
||||||
defineOptions({
|
|
||||||
|
useAppStore()
|
||||||
|
defineOptions({
|
||||||
name: 'App'
|
name: 'App'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
// 引入初始化样式
|
// 引入初始化样式
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
}
|
}
|
||||||
.el-button {
|
|
||||||
|
.el-button {
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gva-body-h {
|
.gva-body-h {
|
||||||
min-height: calc(100% - 3rem);
|
min-height: calc(100% - 3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gva-container {
|
.gva-container {
|
||||||
height: calc(100% - 2.5rem);
|
height: calc(100% - 2.5rem);
|
||||||
}
|
}
|
||||||
.gva-container2 {
|
|
||||||
|
.gva-container2 {
|
||||||
height: calc(100% - 4.5rem);
|
height: calc(100% - 4.5rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import service from '@/utils/request'
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 删除版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body model.SysVersion true "删除版本管理"
|
||||||
|
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||||
|
// @Router /sysVersion/deleteSysVersion [delete]
|
||||||
|
export const deleteSysVersion = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/deleteSysVersion',
|
||||||
|
method: 'delete',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 批量删除版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body request.IdsReq true "批量删除版本管理"
|
||||||
|
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||||
|
// @Router /sysVersion/deleteSysVersion [delete]
|
||||||
|
export const deleteSysVersionByIds = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/deleteSysVersionByIds',
|
||||||
|
method: 'delete',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 用id查询版本管理
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data query model.SysVersion true "用id查询版本管理"
|
||||||
|
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
||||||
|
// @Router /sysVersion/findSysVersion [get]
|
||||||
|
export const findSysVersion = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/findSysVersion',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 分页获取版本管理列表
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data query request.PageInfo true "分页获取版本管理列表"
|
||||||
|
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
|
||||||
|
// @Router /sysVersion/getSysVersionList [get]
|
||||||
|
export const getSysVersionList = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/getSysVersionList',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 导出版本数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body object true "导出版本数据"
|
||||||
|
// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导出成功\"}"
|
||||||
|
// @Router /sysVersion/exportVersion [post]
|
||||||
|
export const exportVersion = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/exportVersion',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 下载版本JSON数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param ID query string true "版本ID"
|
||||||
|
// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"下载成功\"}"
|
||||||
|
// @Router /sysVersion/downloadVersionJson [get]
|
||||||
|
export const downloadVersionJson = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/downloadVersionJson',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags SysVersion
|
||||||
|
// @Summary 导入版本数据
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param data body object true "版本JSON数据"
|
||||||
|
// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导入成功\"}"
|
||||||
|
// @Router /sysVersion/importVersion [post]
|
||||||
|
export const importVersion = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/sysVersion/importVersion',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><path fill="currentColor" d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504L738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512L828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496L285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512L195.2 285.696a64 64 0 0 1 0-90.496"/></svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.09 14.999a6.9 6.9 0 0 1-.59-2.794C5.5 8.5 8.41 5.499 12 5.499s6.5 3.002 6.5 6.706a6.9 6.9 0 0 1-.59 2.794m-5.91-13v1m10 9h-1m-18 0H2m17.07-7.071l-.707.707m-12.726.001l-.707-.707m9.587 14.377c1.01-.327 1.416-1.252 1.53-2.182c.034-.278-.195-.509-.475-.509H8.477a.483.483 0 0 0-.488.534c.112.928.394 1.606 1.464 2.156m5.064 0H9.453m5.064 0c-.121 1.945-.683 2.716-2.51 2.694c-1.954.036-2.404-.916-2.554-2.693"/></svg>
|
||||||
|
After Width: | Height: | Size: 609 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3"/></svg>
|
||||||
|
After Width: | Height: | Size: 307 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"><path d="M12.5.5h-11a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1M7.5 3H11M1.5 5.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1"/><path d="M3.25 8.25a.25.25 0 0 1 0-.5m0 .5a.25.25 0 0 0 0-.5m0-4.5a.25.25 0 0 1 0-.5m0 .5a.25.25 0 0 0 0-.5M7.5 8H11m-4 2.5v3m-5 0h10"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><path fill="currentColor" d="M928.99 755.83L574.6 203.25c-12.89-20.16-36.76-32.58-62.6-32.58s-49.71 12.43-62.6 32.58L95.01 755.83c-12.91 20.12-12.9 44.91.01 65.03c12.92 20.12 36.78 32.51 62.59 32.49h708.78c25.82.01 49.68-12.37 62.59-32.49s12.92-44.91.01-65.03M554.67 768h-85.33v-85.33h85.33zm0-426.67v298.66h-85.33V341.32z"/></svg>
|
||||||
|
After Width: | Height: | Size: 423 B |
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<error-preview v-if="showError" :error-data="errorInfo" @close="handleClose" @confirm="handleConfirm" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { emitter } from '@/utils/bus'
|
||||||
|
import ErrorPreview from '@/components/errorPreview/index.vue'
|
||||||
|
|
||||||
|
const showError = ref(false)
|
||||||
|
const errorInfo = ref(null)
|
||||||
|
let cb = null
|
||||||
|
|
||||||
|
const showErrorDialog = (data) => {
|
||||||
|
// 这玩意同时只允许存在一个
|
||||||
|
if(showError.value) return
|
||||||
|
|
||||||
|
errorInfo.value = data
|
||||||
|
showError.value = true
|
||||||
|
cb = data?.fn || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
showError.value = false
|
||||||
|
errorInfo.value = null
|
||||||
|
cb = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = (code) => {
|
||||||
|
cb && cb(code)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('show-error', showErrorDialog)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
emitter.off('show-error', showErrorDialog)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/40 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="p-5 border-b border-gray-100 flex justify-between items-center">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-800">{{ displayData.title }}</h3>
|
||||||
|
<button class="text-gray-400 hover:text-gray-600 transition-colors" @click="closeModal">
|
||||||
|
<close />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹窗内容 -->
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- 错误类型 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-xs font-medium text-gray-500 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>
|
||||||
|
</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">
|
||||||
|
{{ displayData.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<div v-if="displayData.tips">
|
||||||
|
<div class="text-xs font-medium text-gray-500 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹窗底部 -->
|
||||||
|
<div class="py-2 px-4 border-t border-gray-100 flex justify-end">
|
||||||
|
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm" @click="handleConfirm">
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, ref, computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
errorData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['close', 'confirm']);
|
||||||
|
|
||||||
|
const presetErrors = {
|
||||||
|
500: {
|
||||||
|
title: '检测到接口错误',
|
||||||
|
type: '服务器发生内部错误',
|
||||||
|
icon: 'server',
|
||||||
|
color: 'text-red-500',
|
||||||
|
tips: '此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存'
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
title: '资源未找到',
|
||||||
|
type: 'Not Found',
|
||||||
|
icon: 'warn',
|
||||||
|
color: 'text-orange-500',
|
||||||
|
tips: '此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格'
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
title: '身份认证失败',
|
||||||
|
type: '身份令牌无效',
|
||||||
|
icon: 'lock',
|
||||||
|
color: 'text-purple-500',
|
||||||
|
tips: '您的身份认证已过期或无效,请重新登录。'
|
||||||
|
},
|
||||||
|
'network': {
|
||||||
|
title: '网络错误',
|
||||||
|
type: 'Network Error',
|
||||||
|
icon: 'fa-wifi-slash',
|
||||||
|
color: 'text-gray-500',
|
||||||
|
tips: '无法连接到服务器,请检查您的网络连接。'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = computed(() => {
|
||||||
|
const preset = presetErrors[props.errorData.code];
|
||||||
|
if (preset) {
|
||||||
|
return {
|
||||||
|
...preset,
|
||||||
|
message: props.errorData.message || '没有提供额外信息。'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '未知错误',
|
||||||
|
type: '检测到请求错误',
|
||||||
|
icon: 'fa-question-circle',
|
||||||
|
color: 'text-gray-400',
|
||||||
|
message: props.errorData.message || '发生了一个未知错误。',
|
||||||
|
tips: '请检查控制台获取更多信息。'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emits('close')
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emits('confirm', props.errorData.code);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -17,7 +17,7 @@ export const viteLogo = (env) => {
|
||||||
`> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin`
|
`> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
console.log(greenText(`> 当前版本:v2.8.3`))
|
console.log(greenText(`> 当前版本:v2.8.4`))
|
||||||
console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`))
|
console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`))
|
||||||
console.log(
|
console.log(
|
||||||
greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`)
|
greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default {
|
||||||
register(app)
|
register(app)
|
||||||
console.log(`
|
console.log(`
|
||||||
欢迎使用 Gin-Vue-Admin
|
欢迎使用 Gin-Vue-Admin
|
||||||
当前版本:v2.8.3
|
当前版本:v2.8.4
|
||||||
加群方式:微信:shouzi_1994 QQ群:622360840
|
加群方式:微信:shouzi_1994 QQ群:622360840
|
||||||
项目地址:https://github.com/flipped-aurora/gin-vue-admin
|
项目地址:https://github.com/flipped-aurora/gin-vue-admin
|
||||||
插件市场:https://plugin.gin-vue-admin.com
|
插件市场:https://plugin.gin-vue-admin.com
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
"/src/view/systemTools/installPlugin/index.vue": "Index",
|
"/src/view/systemTools/installPlugin/index.vue": "Index",
|
||||||
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
||||||
"/src/view/systemTools/system/system.vue": "Config",
|
"/src/view/systemTools/system/system.vue": "Config",
|
||||||
|
"/src/view/systemTools/version/version.vue": "SysVersion",
|
||||||
"/src/plugin/announcement/form/info.vue": "InfoForm",
|
"/src/plugin/announcement/form/info.vue": "InfoForm",
|
||||||
"/src/plugin/announcement/view/info.vue": "Info",
|
"/src/plugin/announcement/view/info.vue": "Info",
|
||||||
"/src/plugin/email/view/index.vue": "Email"
|
"/src/plugin/email/view/index.vue": "Email"
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,7 @@ fieldset,
|
||||||
table,
|
table,
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
border: none;
|
// border: none;
|
||||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import axios from 'axios' // 引入axios
|
import axios from 'axios' // 引入axios
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { useUserStore } from '@/pinia/modules/user'
|
import { useUserStore } from '@/pinia/modules/user'
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus'
|
||||||
|
import { emitter } from '@/utils/bus'
|
||||||
import router from '@/router/index'
|
import router from '@/router/index'
|
||||||
import { ElLoading } from 'element-plus'
|
|
||||||
|
|
||||||
// 添加一个状态变量,用于跟踪是否已有错误弹窗显示
|
|
||||||
let errorBoxVisible = false
|
|
||||||
|
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: import.meta.env.VITE_BASE_API,
|
baseURL: import.meta.env.VITE_BASE_API,
|
||||||
|
|
@ -98,6 +95,7 @@ const resetLoading = () => {
|
||||||
loadingInstance = null
|
loadingInstance = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// http request 拦截器
|
// http request 拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
|
@ -117,15 +115,18 @@ service.interceptors.request.use(
|
||||||
if (!error.config.donNotShowLoading) {
|
if (!error.config.donNotShowLoading) {
|
||||||
closeLoading()
|
closeLoading()
|
||||||
}
|
}
|
||||||
ElMessage({
|
emitter.emit('show-error', {
|
||||||
showClose: true,
|
code: 'request',
|
||||||
message: error,
|
message: error.message || '请求发送失败'
|
||||||
type: 'error'
|
|
||||||
})
|
})
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function getErrorMessage(error) {
|
||||||
|
return error.response?.data?.msg || '请求失败'
|
||||||
|
}
|
||||||
|
|
||||||
// http response 拦截器
|
// http response 拦截器
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
|
|
@ -158,105 +159,38 @@ service.interceptors.response.use(
|
||||||
closeLoading()
|
closeLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经有错误弹窗显示,则不再显示新的弹窗
|
|
||||||
if (errorBoxVisible) {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
// 网络错误时重置loading状态
|
// 网络错误
|
||||||
resetLoading()
|
resetLoading()
|
||||||
errorBoxVisible = true
|
emitter.emit('show-error', {
|
||||||
ElMessageBox.confirm(
|
code: 'network',
|
||||||
`
|
message: getErrorMessage(error)
|
||||||
<p>检测到请求错误</p>
|
|
||||||
<p>${error}</p>
|
|
||||||
`,
|
|
||||||
'请求报错',
|
|
||||||
{
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: '稍后重试',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
}
|
|
||||||
).finally(() => {
|
|
||||||
// 弹窗关闭后重置状态
|
|
||||||
errorBoxVisible = false
|
|
||||||
})
|
})
|
||||||
return
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (error.response.status) {
|
// HTTP 状态码错误
|
||||||
case 500:
|
if (error.response.status === 401) {
|
||||||
errorBoxVisible = true
|
emitter.emit('show-error', {
|
||||||
ElMessageBox.confirm(
|
code: '401',
|
||||||
`
|
message: getErrorMessage(error),
|
||||||
<p>检测到接口错误${error}</p>
|
fn: () => {
|
||||||
<p>错误码<span style="color:red"> 500 </span>:此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存</p>
|
|
||||||
`,
|
|
||||||
'接口报错',
|
|
||||||
{
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: '清理缓存',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
userStore.ClearStorage()
|
userStore.ClearStorage()
|
||||||
router.push({ name: 'Login', replace: true })
|
router.push({ name: 'Login', replace: true })
|
||||||
}).finally(() => {
|
|
||||||
// 弹窗关闭后重置状态
|
|
||||||
errorBoxVisible = false
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 404:
|
|
||||||
errorBoxVisible = true
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`
|
|
||||||
<p>检测到接口错误${error}</p>
|
|
||||||
<p>错误码<span style="color:red"> 404 </span>:此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格</p>
|
|
||||||
`,
|
|
||||||
'接口报错',
|
|
||||||
{
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: '我知道了',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
}
|
}
|
||||||
).finally(() => {
|
|
||||||
// 弹窗关闭后重置状态
|
|
||||||
errorBoxVisible = false
|
|
||||||
})
|
})
|
||||||
break
|
return Promise.reject(error)
|
||||||
case 401:
|
|
||||||
errorBoxVisible = true
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`
|
|
||||||
<p>无效的令牌</p>
|
|
||||||
<p>错误码:<span style="color:red"> 401 </span>错误信息:${error}</p>
|
|
||||||
`,
|
|
||||||
'身份信息',
|
|
||||||
{
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
confirmButtonText: '重新登录',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
const userStore = useUserStore()
|
|
||||||
userStore.ClearStorage()
|
|
||||||
router.push({ name: 'Login', replace: true })
|
|
||||||
}).finally(() => {
|
|
||||||
// 弹窗关闭后重置状态
|
|
||||||
errorBoxVisible = false
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return error
|
emitter.emit('show-error', {
|
||||||
|
code: error.response.status,
|
||||||
|
message: getErrorMessage(error)
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听页面卸载事件,确保loading被正确清理
|
// 监听页面卸载事件,确保loading被正确清理
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('beforeunload', resetLoading)
|
window.addEventListener('beforeunload', resetLoading)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,905 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="gva-search-box">
|
||||||
|
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline"
|
||||||
|
@keyup.enter="onSubmit">
|
||||||
|
<el-form-item label="创建日期" prop="createdAtRange">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
创建日期
|
||||||
|
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
|
||||||
|
<el-icon>
|
||||||
|
<QuestionFilled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-date-picker v-model="searchInfo.createdAtRange" class="w-[380px]" type="datetimerange" range-separator="至"
|
||||||
|
start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="版本名称" prop="versionName">
|
||||||
|
<el-input v-model="searchInfo.versionName" placeholder="搜索条件" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="版本号" prop="versionCode">
|
||||||
|
<el-input v-model="searchInfo.versionCode" placeholder="搜索条件" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<template v-if="showAllQuery">
|
||||||
|
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||||
|
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||||
|
<el-button link type="primary" icon="arrow-down" @click="showAllQuery = true"
|
||||||
|
v-if="!showAllQuery">展开</el-button>
|
||||||
|
<el-button link type="primary" icon="arrow-up" @click="showAllQuery = false" v-else>收起</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div class="gva-table-box">
|
||||||
|
<div class="gva-btn-list">
|
||||||
|
<el-button type="success" icon="download" @click="openExportDialog">创建发版</el-button>
|
||||||
|
<el-button type="warning" icon="upload" @click="openImportDialog">导入版本</el-button>
|
||||||
|
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length"
|
||||||
|
@click="onDelete">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID"
|
||||||
|
@selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
|
||||||
|
<el-table-column sortable align="left" label="日期" prop="CreatedAt" width="180">
|
||||||
|
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column align="left" label="版本名称" prop="versionName" width="120" />
|
||||||
|
|
||||||
|
<el-table-column align="left" label="版本号" prop="versionCode" width="120" />
|
||||||
|
|
||||||
|
<el-table-column align="left" label="操作" fixed="right" min-width="320">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon
|
||||||
|
style="margin-right: 5px">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>查看</el-button>
|
||||||
|
<el-button type="success" link icon="download" class="table-button"
|
||||||
|
@click="downloadJson(scope.row)">下载发版包</el-button>
|
||||||
|
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="gva-pagination">
|
||||||
|
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
|
||||||
|
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
|
||||||
|
@size-change="handleSizeChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true"
|
||||||
|
:before-close="closeDetailShow" title="查看">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="版本名称">
|
||||||
|
{{ detailForm.versionName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="版本号">
|
||||||
|
{{ detailForm.versionCode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="版本描述">
|
||||||
|
{{ detailForm.description }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- 导出版本抽屉 -->
|
||||||
|
<el-drawer v-model="exportDialogVisible" title="创建发版" direction="rtl" size="80%" :before-close="closeExportDialog" :show-close="false">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-lg">创建发版</span>
|
||||||
|
<div>
|
||||||
|
<el-button @click="closeExportDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleExport" :loading="exportLoading">创建发版</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="exportForm" label-width="100px">
|
||||||
|
<el-form-item label="版本名称" required>
|
||||||
|
<el-input v-model="exportForm.versionName" placeholder="请输入版本名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本号" required>
|
||||||
|
<el-input v-model="exportForm.versionCode" placeholder="请输入版本号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本描述">
|
||||||
|
<el-input v-model="exportForm.description" type="textarea" placeholder="请输入版本描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发版信息">
|
||||||
|
<div class="flex gap-5 w-full">
|
||||||
|
<!-- 菜单选择 -->
|
||||||
|
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/2">
|
||||||
|
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||||
|
<span class="m-0 text-gray-800 text-base font-medium">选择菜单</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
|
||||||
|
<el-input v-model="menuFilterText" placeholder="输入关键字进行过滤" clearable size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||||
|
<el-tree ref="menuTreeRef" :data="menuTreeData" :default-checked-keys="selectedMenuIds"
|
||||||
|
:props="menuTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox
|
||||||
|
:filter-node-method="filterMenuNode" @check="onMenuCheck" class="menu-tree">
|
||||||
|
<template #default="{ node }">
|
||||||
|
<span class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||||
|
<span>{{ node.label }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API选择 -->
|
||||||
|
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/2">
|
||||||
|
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||||
|
<span class="m-0 text-gray-800 text-base font-medium">选择API</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
|
||||||
|
<el-input v-model="apiFilterTextName" placeholder="按名称过滤" clearable size="small"
|
||||||
|
style="margin-bottom: 8px" />
|
||||||
|
<el-input v-model="apiFilterTextPath" placeholder="按路径过滤" clearable size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||||
|
<el-tree ref="apiTreeRef" :data="apiTreeData" :default-checked-keys="selectedApiIds"
|
||||||
|
:props="apiTreeProps" default-expand-all highlight-current node-key="onlyId" show-checkbox
|
||||||
|
:filter-node-method="filterApiNode" @check="onApiCheck" class="api-tree">
|
||||||
|
<template #default="{ _, data }">
|
||||||
|
<div class="flex items-center justify-between w-full pr-1">
|
||||||
|
<span>{{ data.description }}</span>
|
||||||
|
<el-tooltip :content="data.path">
|
||||||
|
<span class="max-w-[240px] break-all overflow-ellipsis overflow-hidden">
|
||||||
|
{{ data.path }}
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- 导入版本抽屉 -->
|
||||||
|
<el-drawer v-model="importDialogVisible" title="导入版本" direction="rtl" size="80%" :before-close="closeImportDialog" :show-close="false">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-lg">导入版本</span>
|
||||||
|
<div>
|
||||||
|
<el-button @click="closeImportDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleImport" :loading="importLoading"
|
||||||
|
:disabled="!importJsonContent.trim()">导入</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="上传文件">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="true"
|
||||||
|
:limit="1"
|
||||||
|
accept=".json"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
drag
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将JSON文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
只能上传JSON文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本JSON">
|
||||||
|
<el-input v-model="importJsonContent" type="textarea" :rows="20" placeholder="请粘贴版本JSON"
|
||||||
|
@input="handleJsonContentChange" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="预览内容" v-if="importPreviewData">
|
||||||
|
<div class="flex flex-col flex-1 gap-4 border border-gray-300 rounded p-4 bg-gray-50">
|
||||||
|
<div class="flex gap-5 w-full">
|
||||||
|
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/2">
|
||||||
|
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
|
||||||
|
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||||
|
<h3 class="m-0 text-gray-800 text-base font-medium">菜单 ({{ getTotalMenuCount() }}项)</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||||
|
<el-tree
|
||||||
|
:data="previewMenuTreeData"
|
||||||
|
:props="menuTreeProps"
|
||||||
|
node-key="name"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:check-on-click-node="false"
|
||||||
|
:show-checkbox="false"
|
||||||
|
default-expand-all
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||||
|
<span>{{ data.meta?.title || data.title }}</span>
|
||||||
|
<span class="text-gray-500 text-xs ml-2">{{ data.path }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/2">
|
||||||
|
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
|
||||||
|
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||||
|
<h3 class="m-0 text-gray-800 text-base font-medium">API ({{ importPreviewData.apis?.length || 0 }}项)</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||||
|
<el-tree
|
||||||
|
:data="previewApiTreeData"
|
||||||
|
:props="apiTreeProps"
|
||||||
|
node-key="ID"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:check-on-click-node="false"
|
||||||
|
:show-checkbox="false"
|
||||||
|
default-expand-all
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||||
|
<span>{{ data.description }}</span>
|
||||||
|
<span class="text-gray-500 text-xs ml-2">{{ data.path }} [{{ data.method }}]</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
deleteSysVersion,
|
||||||
|
deleteSysVersionByIds,
|
||||||
|
findSysVersion,
|
||||||
|
getSysVersionList,
|
||||||
|
exportVersion,
|
||||||
|
importVersion,
|
||||||
|
downloadVersionJson
|
||||||
|
} from '@/api/version'
|
||||||
|
|
||||||
|
// 导入菜单和API相关接口
|
||||||
|
import { getMenuList } from '@/api/menu'
|
||||||
|
import { getApiList } from '@/api/api'
|
||||||
|
|
||||||
|
// 全量引入格式化工具 请按需保留
|
||||||
|
import { getDictFunc, formatDate, filterDict } from '@/utils/format'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
// 引入按钮权限标识
|
||||||
|
import { useBtnAuth } from '@/utils/btnAuth'
|
||||||
|
import { useAppStore } from "@/pinia"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SysVersion'
|
||||||
|
})
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// 控制更多查询条件显示/隐藏状态
|
||||||
|
const showAllQuery = ref(false)
|
||||||
|
|
||||||
|
// 导出相关数据
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
|
const exportLoading = ref(false)
|
||||||
|
const exportForm = ref({
|
||||||
|
versionName: '',
|
||||||
|
versionCode: '',
|
||||||
|
description: '',
|
||||||
|
menuIds: [],
|
||||||
|
apiIds: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 树形结构相关数据
|
||||||
|
const menuTreeData = ref([])
|
||||||
|
const apiTreeData = ref([])
|
||||||
|
const selectedMenuIds = ref([])
|
||||||
|
const selectedApiIds = ref([])
|
||||||
|
const menuFilterText = ref('')
|
||||||
|
const apiFilterTextName = ref('')
|
||||||
|
const apiFilterTextPath = ref('')
|
||||||
|
|
||||||
|
// 树形组件引用
|
||||||
|
const menuTreeRef = ref(null)
|
||||||
|
const apiTreeRef = ref(null)
|
||||||
|
|
||||||
|
// 树形属性配置
|
||||||
|
const menuTreeProps = ref({
|
||||||
|
children: 'children',
|
||||||
|
label: function (data) {
|
||||||
|
return data.meta?.title || data.title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiTreeProps = ref({
|
||||||
|
children: 'children',
|
||||||
|
label: 'description'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导入相关数据
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const importLoading = ref(false)
|
||||||
|
const importJsonContent = ref('')
|
||||||
|
const importPreviewData = ref(null)
|
||||||
|
const uploadRef = ref(null)
|
||||||
|
const previewMenuTreeData = ref([])
|
||||||
|
const previewApiTreeData = ref([])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const rule = reactive({
|
||||||
|
versionName: [{
|
||||||
|
required: true,
|
||||||
|
message: '请输入版本名称',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
whitespace: true,
|
||||||
|
message: '不能只输入空格',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
versionCode: [{
|
||||||
|
required: true,
|
||||||
|
message: '请输入版本号',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
whitespace: true,
|
||||||
|
message: '不能只输入空格',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const elFormRef = ref()
|
||||||
|
const elSearchFormRef = ref()
|
||||||
|
|
||||||
|
// =========== 表格控制部分 ===========
|
||||||
|
const page = ref(1)
|
||||||
|
const total = ref(0)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const tableData = ref([])
|
||||||
|
const searchInfo = ref({})
|
||||||
|
// 重置
|
||||||
|
const onReset = () => {
|
||||||
|
searchInfo.value = {}
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const onSubmit = () => {
|
||||||
|
elSearchFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
page.value = 1
|
||||||
|
getTableData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pageSize.value = val
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改页面容量
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
page.value = val
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const getTableData = async () => {
|
||||||
|
const table = await getSysVersionList({ page: page.value, pageSize: pageSize.value, ...searchInfo.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 multipleSelection = ref([])
|
||||||
|
// 多选
|
||||||
|
const handleSelectionChange = (val) => {
|
||||||
|
multipleSelection.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除行
|
||||||
|
const deleteRow = (row) => {
|
||||||
|
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
deleteSysVersionFunc(row)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选删除
|
||||||
|
const onDelete = async () => {
|
||||||
|
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
const IDs = []
|
||||||
|
if (multipleSelection.value.length === 0) {
|
||||||
|
ElMessage({
|
||||||
|
type: 'warning',
|
||||||
|
message: '请选择要删除的数据'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
multipleSelection.value &&
|
||||||
|
multipleSelection.value.map(item => {
|
||||||
|
IDs.push(item.ID)
|
||||||
|
})
|
||||||
|
const res = await deleteSysVersionByIds({ IDs })
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: '删除成功'
|
||||||
|
})
|
||||||
|
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||||
|
page.value--
|
||||||
|
}
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除行
|
||||||
|
const deleteSysVersionFunc = async (row) => {
|
||||||
|
const res = await deleteSysVersion({ ID: row.ID })
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: '删除成功'
|
||||||
|
})
|
||||||
|
if (tableData.value.length === 1 && page.value > 1) {
|
||||||
|
page.value--
|
||||||
|
}
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailForm = ref({})
|
||||||
|
|
||||||
|
// 查看详情控制标记
|
||||||
|
const detailShow = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
// 打开详情弹窗
|
||||||
|
const openDetailShow = () => {
|
||||||
|
detailShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 打开详情
|
||||||
|
const getDetails = async (row) => {
|
||||||
|
// 打开弹窗
|
||||||
|
const res = await findSysVersion({ ID: row.ID })
|
||||||
|
if (res.code === 0) {
|
||||||
|
detailForm.value = res.data
|
||||||
|
openDetailShow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 关闭详情弹窗
|
||||||
|
const closeDetailShow = () => {
|
||||||
|
detailShow.value = false
|
||||||
|
detailForm.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 获取菜单和API列表
|
||||||
|
const getMenuAndApiList = async () => {
|
||||||
|
try {
|
||||||
|
// 获取菜单列表
|
||||||
|
const menuRes = await getMenuList()
|
||||||
|
if (menuRes.code === 0) {
|
||||||
|
menuTreeData.value = menuRes.data || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取API列表
|
||||||
|
const apiRes = await getApiList({ page: 1, pageSize: 9999 })
|
||||||
|
if (apiRes.code === 0) {
|
||||||
|
console.log('原始API数据:', apiRes.data)
|
||||||
|
const apis = apiRes.data.list || []
|
||||||
|
apiTreeData.value = buildApiTree(apis)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据失败:', error)
|
||||||
|
ElMessage.error('获取菜单或API数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建API树形结构
|
||||||
|
const buildApiTree = (apis) => {
|
||||||
|
const apiObj = {}
|
||||||
|
apis.forEach((item) => {
|
||||||
|
item.onlyId = 'p:' + item.path + 'm:' + item.method
|
||||||
|
if (Object.prototype.hasOwnProperty.call(apiObj, item.apiGroup)) {
|
||||||
|
apiObj[item.apiGroup].push(item)
|
||||||
|
} else {
|
||||||
|
Object.assign(apiObj, { [item.apiGroup]: [item] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const apiTree = []
|
||||||
|
for (const key in apiObj) {
|
||||||
|
const treeNode = {
|
||||||
|
ID: key,
|
||||||
|
description: key + '组',
|
||||||
|
children: apiObj[key]
|
||||||
|
}
|
||||||
|
apiTree.push(treeNode)
|
||||||
|
}
|
||||||
|
return apiTree
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树形组件事件处理方法
|
||||||
|
const filterMenuNode = (value, data) => {
|
||||||
|
if (!value) return true
|
||||||
|
const title = data.meta?.title || data.title || ''
|
||||||
|
return title.indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterApiNode = (value, data) => {
|
||||||
|
if (!apiFilterTextName.value && !apiFilterTextPath.value) return true
|
||||||
|
let matchesName, matchesPath
|
||||||
|
if (!apiFilterTextName.value) {
|
||||||
|
matchesName = true
|
||||||
|
} else {
|
||||||
|
matchesName = data.description && data.description.includes(apiFilterTextName.value)
|
||||||
|
}
|
||||||
|
if (!apiFilterTextPath.value) {
|
||||||
|
matchesPath = true
|
||||||
|
} else {
|
||||||
|
matchesPath = data.path && data.path.includes(apiFilterTextPath.value)
|
||||||
|
}
|
||||||
|
return matchesName && matchesPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMenuCheck = (data, checked) => {
|
||||||
|
if (checked.checkedKeys) {
|
||||||
|
selectedMenuIds.value = checked.checkedKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onApiCheck = (data, checked) => {
|
||||||
|
if (checked.checkedKeys) {
|
||||||
|
selectedApiIds.value = checked.checkedKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听过滤文本变化
|
||||||
|
watch(menuFilterText, (val) => {
|
||||||
|
if (menuTreeRef.value) {
|
||||||
|
menuTreeRef.value.filter(val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([apiFilterTextName, apiFilterTextPath], () => {
|
||||||
|
if (apiTreeRef.value) {
|
||||||
|
apiTreeRef.value.filter('')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导出相关方法
|
||||||
|
const openExportDialog = async () => {
|
||||||
|
exportDialogVisible.value = true
|
||||||
|
await getMenuAndApiList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeExportDialog = () => {
|
||||||
|
exportDialogVisible.value = false
|
||||||
|
exportForm.value = {
|
||||||
|
versionName: '',
|
||||||
|
versionCode: '',
|
||||||
|
description: '',
|
||||||
|
menuIds: [],
|
||||||
|
apiIds: []
|
||||||
|
}
|
||||||
|
selectedMenuIds.value = []
|
||||||
|
selectedApiIds.value = []
|
||||||
|
menuFilterText.value = ''
|
||||||
|
apiFilterTextName.value = ''
|
||||||
|
apiFilterTextPath.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
if (!exportForm.value.versionName || !exportForm.value.versionCode) {
|
||||||
|
ElMessage.warning('请填写版本名称和版本号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exportLoading.value = true
|
||||||
|
try {
|
||||||
|
// 获取选中的菜单和API
|
||||||
|
const checkedMenus = menuTreeRef.value ? menuTreeRef.value.getCheckedNodes(false, true) : []
|
||||||
|
const checkedApis = apiTreeRef.value ? apiTreeRef.value.getCheckedNodes(true) : []
|
||||||
|
|
||||||
|
const menuIds = checkedMenus.map(menu => menu.ID)
|
||||||
|
const apiIds = checkedApis.map(api => api.ID)
|
||||||
|
|
||||||
|
exportForm.value.menuIds = menuIds
|
||||||
|
exportForm.value.apiIds = apiIds
|
||||||
|
|
||||||
|
const res = await exportVersion(exportForm.value)
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '创建发版失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('创建发版成功')
|
||||||
|
closeExportDialog()
|
||||||
|
getTableData() // 刷新表格数据
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建发版失败:', error)
|
||||||
|
ElMessage.error('创建发版失败')
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入相关方法
|
||||||
|
const openImportDialog = () => {
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImportDialog = () => {
|
||||||
|
importDialogVisible.value = false
|
||||||
|
importJsonContent.value = ''
|
||||||
|
importPreviewData.value = null
|
||||||
|
previewMenuTreeData.value = []
|
||||||
|
previewApiTreeData.value = []
|
||||||
|
// 清理上传文件
|
||||||
|
if (uploadRef.value) {
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传处理函数
|
||||||
|
const handleFileChange = (file) => {
|
||||||
|
if (!file.raw) return
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!file.name.toLowerCase().endsWith('.json')) {
|
||||||
|
ElMessage.error('只能上传JSON文件')
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const content = e.target.result
|
||||||
|
// 验证JSON格式
|
||||||
|
JSON.parse(content)
|
||||||
|
importJsonContent.value = content
|
||||||
|
handleJsonContentChange()
|
||||||
|
ElMessage.success('文件上传成功')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('JSON文件格式错误')
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileRemove = () => {
|
||||||
|
importJsonContent.value = ''
|
||||||
|
importPreviewData.value = null
|
||||||
|
previewMenuTreeData.value = []
|
||||||
|
previewApiTreeData.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算菜单总数(递归计算所有菜单项)
|
||||||
|
const getTotalMenuCount = () => {
|
||||||
|
if (!importPreviewData.value?.menus) return 0
|
||||||
|
|
||||||
|
const countMenus = (menus) => {
|
||||||
|
let count = 0
|
||||||
|
menus.forEach(menu => {
|
||||||
|
count += 1 // 当前菜单
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
count += countMenus(menu.children) // 递归计算子菜单
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
return countMenus(importPreviewData.value.menus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建树形结构的辅助函数
|
||||||
|
const buildTreeData = (data, parentId = 0) => {
|
||||||
|
const tree = []
|
||||||
|
// 处理parentId可能为字符串"0"或数字0的情况
|
||||||
|
const targetParentId = parentId === 0 ? [0, "0"] : [parentId]
|
||||||
|
const items = data.filter(item => targetParentId.includes(item.parentId))
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const children = buildTreeData(data, item.ID)
|
||||||
|
if (children.length > 0) {
|
||||||
|
item.children = children
|
||||||
|
}
|
||||||
|
tree.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJsonContentChange = () => {
|
||||||
|
if (!importJsonContent.value.trim()) {
|
||||||
|
importPreviewData.value = null
|
||||||
|
previewMenuTreeData.value = []
|
||||||
|
previewApiTreeData.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(importJsonContent.value)
|
||||||
|
|
||||||
|
// 构建预览数据
|
||||||
|
importPreviewData.value = {
|
||||||
|
menus: data.menus || [],
|
||||||
|
apis: data.apis || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用菜单数据,因为它已经是树形结构(包含children字段)
|
||||||
|
if (data.menus && data.menus.length > 0) {
|
||||||
|
previewMenuTreeData.value = data.menus
|
||||||
|
} else {
|
||||||
|
previewMenuTreeData.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建API树形数据(按分组组织)
|
||||||
|
if (data.apis && data.apis.length > 0) {
|
||||||
|
const apiGroups = {}
|
||||||
|
data.apis.forEach(api => {
|
||||||
|
const group = api.apiGroup || '未分组'
|
||||||
|
if (!apiGroups[group]) {
|
||||||
|
apiGroups[group] = {
|
||||||
|
ID: `group_${group}`,
|
||||||
|
description: group,
|
||||||
|
path: '',
|
||||||
|
method: '',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiGroups[group].children.push(api)
|
||||||
|
})
|
||||||
|
previewApiTreeData.value = Object.values(apiGroups)
|
||||||
|
} else {
|
||||||
|
previewApiTreeData.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON解析失败:', error)
|
||||||
|
importPreviewData.value = null
|
||||||
|
previewMenuTreeData.value = []
|
||||||
|
previewApiTreeData.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!importJsonContent.value.trim()) {
|
||||||
|
ElMessage.warning('请输入版本JSON')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(importJsonContent.value)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('JSON格式错误,请检查输入内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
importLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(importJsonContent.value)
|
||||||
|
const res = await importVersion(data)
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('导入成功')
|
||||||
|
closeImportDialog()
|
||||||
|
getTableData() // 刷新表格数据
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '导入失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入失败:', error)
|
||||||
|
ElMessage.error('导入失败')
|
||||||
|
} finally {
|
||||||
|
importLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载版本JSON
|
||||||
|
const downloadJson = async (row) => {
|
||||||
|
try {
|
||||||
|
const res = await downloadVersionJson({ ID: row.ID })
|
||||||
|
// 处理axios响应,获取实际的blob数据
|
||||||
|
// 当responseType为blob时,axios拦截器会返回完整的response对象
|
||||||
|
let blob
|
||||||
|
if (res instanceof Blob) {
|
||||||
|
blob = res
|
||||||
|
} else if (res.data instanceof Blob) {
|
||||||
|
blob = res.data
|
||||||
|
} else {
|
||||||
|
// 如果不是blob,可能是错误响应,尝试从response中获取
|
||||||
|
blob = res
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${row.versionName}_${row.versionCode}.json`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('下载成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error)
|
||||||
|
ElMessage.error('下载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Element Plus 树形组件样式优化 */
|
||||||
|
:deep(.el-tree) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__label) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-scrollbar__view) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue