update: 2.8.7-beta发布

update: 2.8.7-beta发布
This commit is contained in:
PiexlMax(奇淼
2025-12-01 17:21:02 +08:00
committed by GitHub
62 changed files with 3715 additions and 566 deletions

View File

@@ -0,0 +1,761 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
---
#### **角色与目标**
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**熟练使用Golang、Vue3、Gin、GORM等技术栈。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
---
### **🚀 重要提示GVA Helper MCP 支持**
**在开始任何GVA开发工作之前请务必注意以下重要工作流程**
1. **MCP支持**: GVA框架本身支持MCPModel Context Protocol提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层Model, Service, API, Router都有其唯一职责**严禁跨层调用**。例如API层绝不能直接操作数据库必须通过Service层。Service层绝不能直接处理`gin.Context`
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed``response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 必须为字段添加清晰的 `json``gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**
- 当数据模型中使用指针类型(如 `*string``*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**从指针到非指针需要检查nil值从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体DTOs
- **必须**为字段添加 `json``form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码`gin.Context`**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx``request.XxxSearch`),并返回处理结果和 `error`
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则并将HTTP请求路径映射到具体的API处理函数上同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法返回该插件所有API的根路径例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能避免循环引用
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档也是前端开发的重要参考请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块API、组件、页面、状态都有其唯一职责**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用提供统一的接口服务
- **结构**: 按业务模块创建API文件如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名kebab-case短横线命名
- 组件名PascalCase大驼峰
- 变量名camelCase小驼峰
- 常量名UPPER_SNAKE_CASE大写下划线
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式ISO 8601标准
- **⚠️ 数据类型一致性**
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**后端Go中的指针类型在JSON序列化时会自动处理nil值前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范建议AI在开发gin-vue-admin项目时
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制

View File

@@ -5,6 +5,7 @@ import (
"io"
"mime/multipart"
"strconv"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/model/example"
@@ -135,6 +136,11 @@ func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) {
response.FailWithMessage(err.Error(), c)
return
}
// 路径穿越拦截
if strings.Contains(file.FilePath, "..") || strings.Contains(file.FilePath, "../") || strings.Contains(file.FilePath, "./") || strings.Contains(file.FilePath, ".\\") {
response.FailWithMessage("非法路径,禁止删除", c)
return
}
err = utils.RemoveChunk(file.FileMd5)
if err != nil {
global.GVA_LOG.Error("缓存切片删除失败!", zap.Error(err))

View File

@@ -23,6 +23,7 @@ type ApiGroup struct {
AutoCodeTemplateApi
SysParamsApi
SysVersionApi
SysErrorApi
}
var (
@@ -46,4 +47,5 @@ var (
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
)

View File

@@ -1,15 +1,10 @@
package system
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/goccy/go-json"
"io"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
@@ -108,48 +103,15 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) {
var llm common.JSONMap
err := c.ShouldBindJSON(&llm)
if err != nil {
if err := c.ShouldBindJSON(&llm); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if global.GVA_CONFIG.AutoCode.AiPath == "" {
response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c)
return
}
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"]))
res, err := request.HttpRequest(
path,
"POST",
nil,
nil,
llm,
)
data, err := autoCodeService.LLMAuto(c.Request.Context(), llm)
if err != nil {
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
response.FailWithMessage("大模型生成失败"+err.Error(), c)
return
}
var resStruct response.Response
b, err := io.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
response.FailWithMessage("大模型生成失败"+err.Error(), c)
return
}
err = json.Unmarshal(b, &resStruct)
if err != nil {
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
response.FailWithMessage("大模型生成失败"+err.Error(), c)
return
}
if resStruct.Code == 7 {
global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err))
response.FailWithMessage("大模型生成失败"+resStruct.Msg, c)
return
}
response.OkWithData(resStruct.Data, c)
response.OkWithData(data, c)
}

View File

@@ -135,3 +135,57 @@ func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
}
response.OkWithDetailed(list, "获取成功", c)
}
// ExportSysDictionary
// @Tags SysDictionary
// @Summary 导出字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query system.SysDictionary true "字典ID"
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "导出字典JSON"
// @Router /sysDictionary/exportSysDictionary [get]
func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) {
var dictionary system.SysDictionary
err := c.ShouldBindQuery(&dictionary)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if dictionary.ID == 0 {
response.FailWithMessage("字典ID不能为空", c)
return
}
exportData, err := dictionaryService.ExportSysDictionary(dictionary.ID)
if err != nil {
global.GVA_LOG.Error("导出失败!", zap.Error(err))
response.FailWithMessage("导出失败", c)
return
}
response.OkWithDetailed(exportData, "导出成功", c)
}
// ImportSysDictionary
// @Tags SysDictionary
// @Summary 导入字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.ImportSysDictionaryRequest true "字典JSON数据"
// @Success 200 {object} response.Response{msg=string} "导入字典"
// @Router /sysDictionary/importSysDictionary [post]
func (s *DictionaryApi) ImportSysDictionary(c *gin.Context) {
var req request.ImportSysDictionaryRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = dictionaryService.ImportSysDictionary(req.Json)
if err != nil {
global.GVA_LOG.Error("导入失败!", zap.Error(err))
response.FailWithMessage("导入失败: "+err.Error(), c)
return
}
response.OkWithMessage("导入成功", c)
}

View File

@@ -0,0 +1,199 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type SysErrorApi struct{}
// CreateSysError 创建错误日志
// @Tags SysError
// @Summary 创建错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body system.SysError true "创建错误日志"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /sysError/createSysError [post]
func (sysErrorApi *SysErrorApi) CreateSysError(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
var sysError system.SysError
err := c.ShouldBindJSON(&sysError)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = sysErrorService.CreateSysError(ctx, &sysError)
if err != nil {
global.GVA_LOG.Error("创建失败!", zap.Error(err))
response.FailWithMessage("创建失败:"+err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
// DeleteSysError 删除错误日志
// @Tags SysError
// @Summary 删除错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body system.SysError true "删除错误日志"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /sysError/deleteSysError [delete]
func (sysErrorApi *SysErrorApi) DeleteSysError(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
ID := c.Query("ID")
err := sysErrorService.DeleteSysError(ctx, ID)
if err != nil {
global.GVA_LOG.Error("删除失败!", zap.Error(err))
response.FailWithMessage("删除失败:"+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// DeleteSysErrorByIds 批量删除错误日志
// @Tags SysError
// @Summary 批量删除错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
// @Router /sysError/deleteSysErrorByIds [delete]
func (sysErrorApi *SysErrorApi) DeleteSysErrorByIds(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
IDs := c.QueryArray("IDs[]")
err := sysErrorService.DeleteSysErrorByIds(ctx, IDs)
if err != nil {
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
response.FailWithMessage("批量删除失败:"+err.Error(), c)
return
}
response.OkWithMessage("批量删除成功", c)
}
// UpdateSysError 更新错误日志
// @Tags SysError
// @Summary 更新错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body system.SysError true "更新错误日志"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /sysError/updateSysError [put]
func (sysErrorApi *SysErrorApi) UpdateSysError(c *gin.Context) {
// 从ctx获取标准context进行业务行为
ctx := c.Request.Context()
var sysError system.SysError
err := c.ShouldBindJSON(&sysError)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = sysErrorService.UpdateSysError(ctx, sysError)
if err != nil {
global.GVA_LOG.Error("更新失败!", zap.Error(err))
response.FailWithMessage("更新失败:"+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// FindSysError 用id查询错误日志
// @Tags SysError
// @Summary 用id查询错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param ID query uint true "用id查询错误日志"
// @Success 200 {object} response.Response{data=system.SysError,msg=string} "查询成功"
// @Router /sysError/findSysError [get]
func (sysErrorApi *SysErrorApi) FindSysError(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
ID := c.Query("ID")
resysError, err := sysErrorService.GetSysError(ctx, ID)
if err != nil {
global.GVA_LOG.Error("查询失败!", zap.Error(err))
response.FailWithMessage("查询失败:"+err.Error(), c)
return
}
response.OkWithData(resysError, c)
}
// GetSysErrorList 分页获取错误日志列表
// @Tags SysError
// @Summary 分页获取错误日志列表
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /sysError/getSysErrorList [get]
func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
var pageInfo systemReq.SysErrorSearch
err := c.ShouldBindQuery(&pageInfo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, total, err := sysErrorService.GetSysErrorInfoList(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)
}
// GetSysErrorSolution 触发错误日志的异步处理
// @Tags SysError
// @Summary 根据ID触发处理标记为处理中1分钟后自动改为处理完成
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id query string true "错误日志ID"
// @Success 200 {object} response.Response{msg=string} "处理已提交"
// @Router /sysError/getSysErrorSolution [get]
func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 兼容 id 与 ID 两种参数
ID := c.Query("id")
if ID == "" {
response.FailWithMessage("缺少参数: id", c)
return
}
err := sysErrorService.GetSysErrorSolution(ctx, ID)
if err != nil {
global.GVA_LOG.Error("处理触发失败!", zap.Error(err))
response.FailWithMessage("处理触发失败:"+err.Error(), c)
return
}
response.OkWithMessage("已提交至AI处理", c)
}

View File

@@ -50,6 +50,34 @@ type SysExportTemplateApi struct {
var sysExportTemplateService = service.ServiceGroupApp.SystemServiceGroup.SysExportTemplateService
// PreviewSQL 预览最终生成的SQL
// @Tags SysExportTemplate
// @Summary 预览最终生成的SQL不执行查询仅返回SQL字符串
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param templateID query string true "导出模板ID"
// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件"
// @Success 200 {object} response.Response{data=map[string]string} "获取成功"
// @Router /sysExportTemplate/previewSQL [get]
func (sysExportTemplateApi *SysExportTemplateApi) PreviewSQL(c *gin.Context) {
templateID := c.Query("templateID")
if templateID == "" {
response.FailWithMessage("模板ID不能为空", c)
return
}
// 直接复用导出接口的参数组织方式:使用 URL Query其中 params 为内部编码的查询字符串
queryParams := c.Request.URL.Query()
if sqlPreview, err := sysExportTemplateService.PreviewSQL(templateID, queryParams); err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
} else {
response.OkWithData(gin.H{"sql": sqlPreview}, c)
}
}
// CreateSysExportTemplate 创建导出模板
// @Tags SysExportTemplate
// @Summary 创建导出模板

View File

@@ -1,11 +1,18 @@
package internal
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"time"
"context"
"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"
astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"strings"
"time"
)
type ZapCore struct {
@@ -54,13 +61,75 @@ func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapco
}
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
for i := 0; i < len(fields); i++ {
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
syncer := z.WriteSyncer(fields[i].String)
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
}
}
return z.Core.Write(entry, fields)
for i := 0; i < len(fields); i++ {
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
syncer := z.WriteSyncer(fields[i].String)
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
}
}
// 先写入原日志目标
err := z.Core.Write(entry, fields)
// 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容
if entry.Level >= zapcore.ErrorLevel {
// 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
return err
}
// 避免重复记录 panic 恢复日志panic 由 GinRecovery 单独捕捉入库
if strings.Contains(entry.Message, "[Recovery from panic]") {
return err
}
form := "后端"
level := entry.Level.String()
// 生成基础信息
info := entry.Message
// 提取 zap.Error(err) 内容
var errStr string
for i := 0; i < len(fields); i++ {
f := fields[i]
if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" {
if f.Interface != nil {
errStr = fmt.Sprintf("%v", f.Interface)
} else if f.String != "" {
errStr = f.String
}
break
}
}
if errStr != "" {
info = fmt.Sprintf("%s | 错误: %s", info, errStr)
}
// 附加来源与堆栈信息
if entry.Caller.File != "" {
info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line)
}
stack := entry.Stack
if stack != "" {
info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
// 解析最终业务调用方,并提取其方法源码
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
if exErr == nil {
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
} else {
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
}
}
}
// 使用后台上下文,避免依赖 gin.Context
ctx := context.Background()
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
Form: &form,
Info: &info,
Level: level,
})
}
return err
}
func (z *ZapCore) Sync() error {

View File

@@ -1,13 +1,13 @@
package core
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
)
// Zap 获取 zap.Logger
@@ -24,9 +24,13 @@ func Zap() (logger *zap.Logger) {
core := internal.NewZapCore(levels[i])
cores = append(cores, core)
}
logger = zap.New(zapcore.NewTee(cores...))
// 构建基础 logger错误级别的入库逻辑已在自定义 ZapCore 中处理)
logger = zap.New(zapcore.NewTee(cores...))
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
if global.GVA_CONFIG.Zap.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
opts = append(opts, zap.AddCaller())
}
logger = logger.WithOptions(opts...)
return logger
}

View File

@@ -4,7 +4,7 @@ package global
// 目前只有Version正式使用 其余为预留
const (
// Version 当前版本号
Version = "v2.8.6"
Version = "v2.8.7"
// AppName 应用名称
AppName = "Gin-Vue-Admin"
// Description 应用描述

View File

@@ -1,8 +1,8 @@
module github.com/flipped-aurora/gin-vue-admin/server
go 1.23
go 1.24.0
toolchain go1.23.9
toolchain go1.24.2
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
@@ -20,6 +20,7 @@ require (
github.com/gookit/color v1.5.4
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/localrivet/gomcp v1.7.2
github.com/mark3labs/mcp-go v0.41.1
github.com/mholt/archives v0.1.1
github.com/minio/minio-go/v7 v7.0.84
@@ -43,9 +44,9 @@ require (
go.mongodb.org/mongo-driver v1.17.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.32.0
golang.org/x/sync v0.10.0
golang.org/x/text v0.21.0
golang.org/x/crypto v0.37.0
golang.org/x/sync v0.13.0
golang.org/x/text v0.24.0
gorm.io/datatypes v1.2.5
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
@@ -77,6 +78,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
@@ -91,12 +93,16 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -111,10 +117,11 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -128,6 +135,9 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/mozillazg/go-httpheader v0.4.0 // indirect
github.com/nats-io/nats.go v1.42.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
@@ -172,11 +182,13 @@ require (
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/hints v1.1.2 // indirect

View File

@@ -113,6 +113,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g=
github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -139,6 +141,10 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -168,6 +174,12 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -198,6 +210,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -234,6 +248,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -288,8 +304,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -315,6 +331,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/localrivet/gomcp v1.7.2 h1:dJtKCvbI8Gr/L0N7cZlo3XOMyCc7GCahdtbI/Y/K9Ig=
github.com/localrivet/gomcp v1.7.2/go.mod h1:7MBYbqypfmEzDuLWdz2FSkAeX19ZX9cSe6qD6mZgOEc=
github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca h1:q0KYRv+ktfm8KnMROXcRNJEnfXSI3NZ45aMC8T/mg14=
github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca/go.mod h1:8B25VIq6WUPYAdY3aodQnj/hDNmYTcPgzzc7ZZ1++NI=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
@@ -358,6 +378,12 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w=
github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
@@ -511,6 +537,18 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -541,8 +579,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -607,8 +645,8 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -626,8 +664,9 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -661,8 +700,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -696,8 +735,9 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
@@ -761,6 +801,8 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -768,8 +810,10 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -62,6 +62,7 @@ func RegisterTables() {
system.JoinTemplate{},
system.SysParams{},
system.SysVersion{},
system.SysError{},
example.ExaFile{},
example.ExaCustomer{},

View File

@@ -34,11 +34,12 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) {
// 初始化总路由
func Routers() *gin.Engine {
Router := gin.New()
Router.Use(gin.Recovery())
if gin.Mode() == gin.DebugMode {
Router.Use(gin.Logger())
}
Router := gin.New()
// 使用自定义的 Recovery 中间件,记录 panic 并入库
Router.Use(middleware.GinRecovery(true))
if gin.Mode() == gin.DebugMode {
Router.Use(gin.Logger())
}
if !global.GVA_CONFIG.MCP.Separate {
@@ -107,6 +108,7 @@ func Routers() *gin.Engine {
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理
systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类

View File

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

View File

@@ -1,12 +1,12 @@
package middleware
import (
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/gin-gonic/gin"

View File

@@ -2,11 +2,12 @@ package middleware
import (
"errors"
"strconv"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/golang-jwt/jwt/v5"
"strconv"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/gin-gonic/gin"
@@ -59,7 +60,7 @@ func JWTAuth() gin.HandlerFunc {
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
utils.SetToken(c, newToken, int(dr.Seconds()))
utils.SetToken(c, newToken, int(dr.Seconds()/60))
if global.GVA_CONFIG.System.UseMultipoint {
// 记录新的活跃jwt
_ = utils.SetRedisJWT(newToken, newClaims.Username)

View File

@@ -3,3 +3,7 @@ package request
type SysDictionarySearch struct {
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
}
type ImportSysDictionaryRequest struct {
Json string `json:"json" binding:"required"` // JSON字符串
}

View File

@@ -0,0 +1,14 @@
package request
import (
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"time"
)
type SysErrorSearch struct{
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
Form *string `json:"form" form:"form"`
Info *string `json:"info" form:"info"`
request.PageInfo
}

View File

@@ -0,0 +1,21 @@
// 自动生成模板SysError
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// 错误日志 结构体 SysError
type SysError struct {
global.GVA_MODEL
Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源
Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容
Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"`
Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案
Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成
}
// TableName 错误日志 SysError自定义表名 sys_error
func (SysError) TableName() string {
return "sys_error"
}

View File

@@ -1,50 +1,53 @@
package utils
import (
"fmt"
"github.com/pkg/errors"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
)
func RegisterApis(apis ...system.SysApi) {
var count int64
var apiPaths []string
for i := range apis {
apiPaths = append(apiPaths, apis[i].Path)
}
global.GVA_DB.Find(&[]system.SysApi{}, "path in (?)", apiPaths).Count(&count)
if count > 0 {
return
}
err := global.GVA_DB.Create(&apis).Error
func RegisterApis( apis ...system.SysApi) {
err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
for _, api := range apis {
err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error
if err != nil {
zap.L().Error("注册API失败", zap.Error(err), zap.String("api", api.Path), zap.String("method", api.Method), zap.String("apiGroup", api.ApiGroup))
return err
}
}
return nil
})
if err != nil {
fmt.Println(err)
zap.L().Error("注册API失败", zap.Error(err))
}
}
func RegisterMenus(menus ...system.SysBaseMenu) {
var count int64
var menuNames []string
func RegisterMenus( menus ...system.SysBaseMenu) {
parentMenu := menus[0]
otherMenus := menus[1:]
for i := range menus {
menuNames = append(menuNames, menus[i].Name)
}
global.GVA_DB.Find(&[]system.SysBaseMenu{}, "name in (?)", menuNames).Count(&count)
if count > 0 {
return
}
err := global.GVA_DB.Create(&parentMenu).Error
if err != nil {
fmt.Println(err)
}
for i := range otherMenus {
err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
err := tx.Model(system.SysBaseMenu{}).Where("name = ? ", parentMenu.Name).FirstOrCreate(&parentMenu).Error
if err != nil {
zap.L().Error("注册菜单失败", zap.Error(err))
return errors.Wrap(err, "注册菜单失败")
}
pid := parentMenu.ID
otherMenus[i].ParentId = pid
}
err = global.GVA_DB.Create(&otherMenus).Error
for i := range otherMenus {
otherMenus[i].ParentId = pid
err = tx.Model(system.SysBaseMenu{}).Where("name = ? ", otherMenus[i].Name).FirstOrCreate(&otherMenus[i]).Error
if err != nil {
zap.L().Error("注册菜单失败", zap.Error(err))
return errors.Wrap(err, "注册菜单失败")
}
}
return nil
})
if err != nil {
fmt.Println(err)
zap.L().Error("注册菜单失败", zap.Error(err))
}
}

View File

@@ -20,6 +20,7 @@ type RouterGroup struct {
SysExportTemplateRouter
SysParamsRouter
SysVersionRouter
SysErrorRouter
}
var (
@@ -43,4 +44,5 @@ var (
autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi
exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi
sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi
sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi
)

View File

@@ -14,6 +14,8 @@ func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) {
sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary
sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary
sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary
sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary
sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary
}
{
sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary

View File

@@ -0,0 +1,28 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/gin-gonic/gin"
)
type SysErrorRouter struct{}
// InitSysErrorRouter 初始化 错误日志 路由信息
func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord())
sysErrorRouterWithoutRecord := Router.Group("sysError")
sysErrorRouterWithoutAuth := PublicRouter.Group("sysError")
{
sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志
sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志
sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志
sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理
}
{
sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志
sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表
}
{
sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志
}
}

View File

@@ -26,6 +26,7 @@ func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.Router
sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表
sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token
sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板
sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL
}
{
sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格

View File

@@ -9,6 +9,7 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/model/example"
"github.com/flipped-aurora/gin-vue-admin/server/model/example/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils/upload"
"gorm.io/gorm"
)
//@author: [piexlmax](https://github.com/piexlmax)
@@ -107,7 +108,13 @@ func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader,
Key: key,
}
if noSave == "0" {
return f, e.Upload(f)
// 检查是否已存在相同key的记录
var existingFile example.ExaFileUploadAndDownload
err = global.GVA_DB.Where("`key` = ?", key).First(&existingFile).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return f, e.Upload(f)
}
return f, err
}
return f, nil
}

View File

@@ -0,0 +1,51 @@
package system
import (
"context"
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
"github.com/goccy/go-json"
"io"
"strings"
)
// LLMAuto 调用大模型服务,返回生成结果数据
// 入参为通用 JSONMap需包含 mode例如 ai/butler/eye/painter 等)以及业务 prompt/payload
func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) {
if global.GVA_CONFIG.AutoCode.AiPath == "" {
return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中")
}
// 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换
mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode))
res, err := request.HttpRequest(
path,
"POST",
nil,
nil,
llm,
)
if err != nil {
return nil, fmt.Errorf("大模型生成失败: %w", err)
}
defer res.Body.Close()
var resStruct commonResp.Response
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("读取大模型响应失败: %w", err)
}
if err = json.Unmarshal(b, &resStruct); err != nil {
return nil, fmt.Errorf("解析大模型响应失败: %w", err)
}
if resStruct.Code == 7 { // 业务约定7 表示模型生成失败
return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg)
}
return resStruct.Data, nil
}

View File

@@ -22,4 +22,5 @@ type ServiceGroup struct {
AutoCodePackage autoCodePackage
AutoCodeHistory autoCodeHistory
AutoCodeTemplate autoCodeTemplate
SysErrorService
}

View File

@@ -1,6 +1,7 @@
package system
import (
"encoding/json"
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
@@ -152,3 +153,145 @@ func (dictionaryService *DictionaryService) checkCircularReference(currentID uin
return nil
}
//@author: [pixelMax]
//@function: ExportSysDictionary
//@description: 导出字典JSON包含字典详情
//@param: id uint
//@return: exportData map[string]interface{}, err error
func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) {
var dictionary system.SysDictionary
// 查询字典及其所有详情
err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
return db.Order("sort")
}).First(&dictionary).Error
if err != nil {
return nil, err
}
// 清空字典详情中的ID、创建时间、更新时间等字段
var cleanDetails []map[string]interface{}
for _, detail := range dictionary.SysDictionaryDetails {
cleanDetail := map[string]interface{}{
"label": detail.Label,
"value": detail.Value,
"extend": detail.Extend,
"status": detail.Status,
"sort": detail.Sort,
"level": detail.Level,
"path": detail.Path,
}
cleanDetails = append(cleanDetails, cleanDetail)
}
// 构造导出数据
exportData = map[string]interface{}{
"name": dictionary.Name,
"type": dictionary.Type,
"status": dictionary.Status,
"desc": dictionary.Desc,
"sysDictionaryDetails": cleanDetails,
}
return exportData, nil
}
//@author: [pixelMax]
//@function: ImportSysDictionary
//@description: 导入字典JSON包含字典详情
//@param: jsonStr string
//@return: err error
func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error {
// 直接解析到 SysDictionary 结构体
var importData system.SysDictionary
if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil {
return errors.New("JSON 格式错误: " + err.Error())
}
// 验证必填字段
if importData.Name == "" {
return errors.New("字典名称不能为空")
}
if importData.Type == "" {
return errors.New("字典类型不能为空")
}
// 检查字典类型是否已存在
if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type不允许导入")
}
// 创建字典清空导入数据的ID和时间戳
dictionary := system.SysDictionary{
Name: importData.Name,
Type: importData.Type,
Status: importData.Status,
Desc: importData.Desc,
}
// 开启事务
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 创建字典
if err := tx.Create(&dictionary).Error; err != nil {
return err
}
// 处理字典详情
if len(importData.SysDictionaryDetails) > 0 {
// 创建一个映射来跟踪旧ID到新ID的对应关系
idMap := make(map[uint]uint)
// 第一遍:创建所有详情记录
for _, detail := range importData.SysDictionaryDetails {
// 验证必填字段
if detail.Label == "" || detail.Value == "" {
continue
}
// 记录旧ID
oldID := detail.ID
// 创建新的详情记录ID会被GORM自动设置
detailRecord := system.SysDictionaryDetail{
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
Level: detail.Level,
Path: detail.Path,
SysDictionaryID: int(dictionary.ID),
}
// 创建详情记录
if err := tx.Create(&detailRecord).Error; err != nil {
return err
}
// 记录旧ID到新ID的映射
if oldID > 0 {
idMap[oldID] = detailRecord.ID
}
}
// 第二遍更新parent_id关系
for _, detail := range importData.SysDictionaryDetails {
if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 {
if newID, exists := idMap[detail.ID]; exists {
if newParentID, parentExists := idMap[*detail.ParentID]; parentExists {
if err := tx.Model(&system.SysDictionaryDetail{}).
Where("id = ?", newID).
Update("parent_id", newParentID).Error; err != nil {
return err
}
}
}
}
}
}
return nil
})
}

View File

@@ -0,0 +1,124 @@
package system
import (
"context"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
type SysErrorService struct{}
// CreateSysError 创建错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) {
err = global.GVA_DB.Create(sysError).Error
return err
}
// DeleteSysError 删除错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) DeleteSysError(ctx context.Context, ID string) (err error) {
err = global.GVA_DB.Delete(&system.SysError{}, "id = ?", ID).Error
return err
}
// DeleteSysErrorByIds 批量删除错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) DeleteSysErrorByIds(ctx context.Context, IDs []string) (err error) {
err = global.GVA_DB.Delete(&[]system.SysError{}, "id in ?", IDs).Error
return err
}
// UpdateSysError 更新错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) UpdateSysError(ctx context.Context, sysError system.SysError) (err error) {
err = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", sysError.ID).Updates(&sysError).Error
return err
}
// GetSysError 根据ID获取错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) GetSysError(ctx context.Context, ID string) (sysError system.SysError, err error) {
err = global.GVA_DB.Where("id = ?", ID).First(&sysError).Error
return
}
// GetSysErrorInfoList 分页获取错误日志记录
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, info systemReq.SysErrorSearch) (list []system.SysError, total int64, err error) {
limit := info.PageSize
offset := info.PageSize * (info.Page - 1)
// 创建db
db := global.GVA_DB.Model(&system.SysError{}).Order("created_at desc")
var sysErrors []system.SysError
// 如果有条件搜索 下方会自动创建搜索语句
if len(info.CreatedAtRange) == 2 {
db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
}
if info.Form != nil && *info.Form != "" {
db = db.Where("form = ?", *info.Form)
}
if info.Info != nil && *info.Info != "" {
db = db.Where("info LIKE ?", "%"+*info.Info+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
if limit != 0 {
db = db.Limit(limit).Offset(offset)
}
err = db.Find(&sysErrors).Error
return sysErrors, total, err
}
// GetSysErrorSolution 异步处理错误
// Author [yourname](https://github.com/yourname)
func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) {
// 立即更新为处理中
err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error
if err != nil {
return err
}
// 异步协程在一分钟后更新为处理完成
go func(id string) {
// 查询当前错误信息用于生成方案
var se system.SysError
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error
// 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案
var form, info string
if se.Form != nil {
form = *se.Form
}
if se.Info != nil {
info = *se.Info
}
llmReq := common.JSONMap{
"mode": "solution",
"command": "solution",
"info": info,
"form": form,
}
// 调用服务层 LLMAuto忽略错误但尽量写入方案
var solution string
if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil {
solution = fmt.Sprintf("%v", data)
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error
} else {
// 即使生成失败也标记为完成,避免任务卡住
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error
}
}(ID)
return nil
}

View File

@@ -213,6 +213,16 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator)
}
if condition.Operator == "BETWEEN" {
sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column)
startValue := paramsValues.Get("start" + condition.From)
endValue := paramsValues.Get("end" + condition.From)
if startValue != "" && endValue != "" {
db = db.Where(sql, startValue, endValue)
}
continue
}
if value != "" {
if condition.Operator == "LIKE" {
value = "%" + value + "%"
@@ -317,14 +327,14 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
for j, colCell := range row {
cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1)
var sErr error
if v, err := strconv.ParseFloat(colCell, 64); err == nil {
sErr = f.SetCellValue("Sheet1", cell, v)
} else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil {
sErr = f.SetCellValue("Sheet1", cell, v)
} else {
sErr = f.SetCellValue("Sheet1", cell, colCell)
}
var sErr error
if v, err := strconv.ParseFloat(colCell, 64); err == nil {
sErr = f.SetCellValue("Sheet1", cell, v)
} else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil {
sErr = f.SetCellValue("Sheet1", cell, v)
} else {
sErr = f.SetCellValue("Sheet1", cell, colCell)
}
if sErr != nil {
return nil, "", sErr
@@ -340,6 +350,185 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
return file, template.Name, nil
}
// PreviewSQL 预览最终生成的 SQL不执行查询仅返回 SQL 字符串)
// Author [piexlmax](https://github.com/piexlmax) & [trae-ai]
func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) {
// 解析 params与导出逻辑保持一致
var params = values.Get("params")
paramsValues, _ := url.ParseQuery(params)
// 加载模板
var template system.SysExportTemplate
err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error
if err != nil {
return "", err
}
// 解析模板列
var templateInfoMap = make(map[string]string)
columns, err := utils.GetJSONKeys(template.TemplateInfo)
if err != nil {
return "", err
}
err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
if err != nil {
return "", err
}
var selectKeyFmt []string
for _, key := range columns {
selectKeyFmt = append(selectKeyFmt, key)
}
selects := strings.Join(selectKeyFmt, ", ")
// 生成 FROM 与 JOIN 片段
var sb strings.Builder
sb.WriteString("SELECT ")
sb.WriteString(selects)
sb.WriteString(" FROM ")
sb.WriteString(template.TableName)
if len(template.JoinTemplate) > 0 {
for _, join := range template.JoinTemplate {
sb.WriteString(" ")
sb.WriteString(join.JOINS)
sb.WriteString(" ")
sb.WriteString(join.Table)
sb.WriteString(" ON ")
sb.WriteString(join.ON)
}
}
// WHERE 条件
var wheres []string
// 软删除过滤
filterDeleted := false
if paramsValues != nil {
filterParam := paramsValues.Get("filterDeleted")
if filterParam == "true" {
filterDeleted = true
}
}
if filterDeleted {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
if len(template.JoinTemplate) > 0 {
for _, join := range template.JoinTemplate {
if sysExportTemplateService.hasDeletedAtColumn(join.Table) {
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
}
}
}
}
// 模板条件(保留与 ExportExcel 同步的解析规则)
if len(template.Conditions) > 0 {
for _, condition := range template.Conditions {
op := strings.ToUpper(strings.TrimSpace(condition.Operator))
col := strings.TrimSpace(condition.Column)
// 预览优先展示传入值,没有则展示占位符
val := ""
if paramsValues != nil {
val = paramsValues.Get(condition.From)
}
switch op {
case "BETWEEN":
startValue := ""
endValue := ""
if paramsValues != nil {
startValue = paramsValues.Get("start" + condition.From)
endValue = paramsValues.Get("end" + condition.From)
}
if startValue != "" && endValue != "" {
wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue))
} else {
wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From))
}
case "IN", "NOT IN":
if val != "" {
// 逗号分隔值做简单展示
parts := strings.Split(val, ",")
for i := range parts { parts[i] = strings.TrimSpace(parts[i]) }
wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','")))
} else {
wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From))
}
case "LIKE":
if val != "" {
wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val))
} else {
wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From))
}
default:
if val != "" {
wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val))
} else {
wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From))
}
}
}
}
if len(wheres) > 0 {
sb.WriteString(" WHERE ")
sb.WriteString(strings.Join(wheres, " AND "))
}
// 排序
order := ""
if paramsValues != nil {
order = paramsValues.Get("order")
}
if order == "" && template.Order != "" {
order = template.Order
}
if order != "" {
sb.WriteString(" ORDER BY ")
sb.WriteString(order)
}
// limit/offset如果传入或默认值为0则不生成
limitStr := ""
offsetStr := ""
if paramsValues != nil {
limitStr = paramsValues.Get("limit")
offsetStr = paramsValues.Get("offset")
}
// 处理模板默认limit仅当非0时
if limitStr == "" && template.Limit != nil && *template.Limit != 0 {
limitStr = strconv.Itoa(*template.Limit)
}
// 解析为数值,用于判断是否生成
limitInt := 0
offsetInt := 0
if limitStr != "" {
if v, e := strconv.Atoi(limitStr); e == nil { limitInt = v }
}
if offsetStr != "" {
if v, e := strconv.Atoi(offsetStr); e == nil { offsetInt = v }
}
if limitInt > 0 {
sb.WriteString(" LIMIT ")
sb.WriteString(strconv.Itoa(limitInt))
if offsetInt > 0 {
sb.WriteString(" OFFSET ")
sb.WriteString(strconv.Itoa(offsetInt))
}
} else {
// 当limit未设置或为0时仅当offset>0才生成OFFSET
if offsetInt > 0 {
sb.WriteString(" OFFSET ")
sb.WriteString(strconv.Itoa(offsetInt))
}
}
return sb.String(), nil
}
// ExportTemplate 导出Excel模板
// Author [piexlmax](https://github.com/piexlmax)
func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) {

View File

@@ -149,6 +149,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
{ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典建议选择"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"},
{ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"},
{ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"},
{ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"},
@@ -175,8 +177,17 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"},
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"},
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"},
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"},
{ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"},
{ApiGroup: "错误日志", Method: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"},
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"},
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"},
{ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"},
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"},
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"},
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"},
{ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"},
{ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"},
{ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfoByIds", Description: "批量删除公告"},

View File

@@ -149,6 +149,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
{Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/importSysDictionary", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"},
@@ -176,8 +178,17 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/previewSQL", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysError/createSysError", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"},
{Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/info/deleteInfoByIds", V2: "DELETE"},

View File

@@ -103,6 +103,7 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
{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: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}},
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 9, Meta: Meta{Title: "错误日志", Icon: "warn"}},
{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"}},

View File

@@ -0,0 +1,62 @@
package ast
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
)
// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码
// 返回:方法名、完整源码、起止行号
func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) {
// 读取源文件
src, readErr := os.ReadFile(filePath)
if readErr != nil {
err = fmt.Errorf("read file failed: %w", readErr)
return
}
// 解析 AST
fset := token.NewFileSet()
file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments)
if parseErr != nil {
err = fmt.Errorf("parse file failed: %w", parseErr)
return
}
// 在 AST 中定位包含指定行号的函数声明
var target *ast.FuncDecl
ast.Inspect(file, func(n ast.Node) bool {
fd, ok := n.(*ast.FuncDecl)
if !ok {
return true
}
s := fset.Position(fd.Pos()).Line
e := fset.Position(fd.End()).Line
if line >= s && line <= e {
target = fd
startLine = s
endLine = e
return false
}
return true
})
if target == nil {
err = fmt.Errorf("no function encloses line %d in %s", line, filePath)
return
}
// 使用字节偏移精确提取源码片段(包含注释与原始格式)
start := fset.Position(target.Pos()).Offset
end := fset.Position(target.End()).Offset
if start < 0 || end > len(src) || start >= end {
err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src))
return
}
source = string(src[start:end])
name = target.Name.Name
return
}

View File

@@ -0,0 +1,79 @@
package stacktrace
import (
"regexp"
"strconv"
"strings"
)
// Frame 表示一次栈帧解析结果
type Frame struct {
File string
Line int
Func string
}
var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号
// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件
func FindFinalCaller(stack string) (Frame, bool) {
if stack == "" {
return Frame{}, false
}
lines := strings.Split(stack, "\n")
var currFunc string
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
if m := fileLineRe.FindStringSubmatch(line); m != nil {
file := m[1]
ln, _ := strconv.Atoi(m[2])
if shouldSkip(file) {
// 跳过此帧,同时重置函数名以避免错误配对
currFunc = ""
continue
}
return Frame{File: file, Line: ln, Func: currFunc}, true
}
// 记录函数名行,下一行通常是文件:行
currFunc = line
}
return Frame{}, false
}
func shouldSkip(file string) bool {
// 第三方库与 Go 模块缓存
if strings.Contains(file, "/go/pkg/mod/") {
return true
}
if strings.Contains(file, "/go.uber.org/") {
return true
}
if strings.Contains(file, "/gorm.io/") {
return true
}
// 标准库
if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go
return true
}
// 框架内不需要作为最终调用方的路径
if strings.Contains(file, "/server/core/zap.go") {
return true
}
if strings.Contains(file, "/server/core/") {
return true
}
if strings.Contains(file, "/server/utils/errorhook/") {
return true
}
if strings.Contains(file, "/server/middleware/") {
return true
}
if strings.Contains(file, "/server/router/") {
return true
}
return false
}

View File

@@ -1,6 +1,6 @@
{
"name": "gin-vue-admin",
"version": "2.8.6",
"version": "2.8.7",
"private": true,
"scripts": {
"dev": "node openDocument.js && vite --host --mode development",
@@ -45,7 +45,7 @@
"spark-md5": "^3.0.2",
"universal-cookie": "^7",
"vform3-builds": "^3.0.10",
"vite-auto-import-svg": "^1.9.0",
"vite-auto-import-svg": "^2.1.0",
"vue": "^3.5.7",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3",

View File

@@ -3,7 +3,7 @@
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" :size="appStore.config.global_size">
<router-view />
<Application />
</el-config-provider>
@@ -15,7 +15,7 @@
import Application from '@/components/application/index.vue'
import { useAppStore } from '@/pinia'
useAppStore()
const appStore = useAppStore()
defineOptions({
name: 'App'
})

View File

@@ -162,7 +162,6 @@ export const butler = (data) => {
})
}
export const eye = (data) => {
return service({
url: '/autoCode/llmAuto',

View File

@@ -126,3 +126,20 @@ export const exportTemplate = (params) => {
params
})
}
// PreviewSQL 预览最终生成的SQL
// @Tags SysExportTemplate
// @Summary 预览最终生成的SQL
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Router /sysExportTemplate/previewSQL [get]
// @Param templateID query string true "导出模板ID"
// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件"
export const previewSQL = (params) => {
return service({
url: '/sysExportTemplate/previewSQL',
method: 'get',
params
})
}

View File

@@ -78,3 +78,35 @@ export const getSysDictionaryList = (params) => {
params
})
}
// @Tags SysDictionary
// @Summary 导出字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.SysDictionary true "字典ID"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"导出成功"}"
// @Router /sysDictionary/exportSysDictionary [get]
export const exportSysDictionary = (params) => {
return service({
url: '/sysDictionary/exportSysDictionary',
method: 'get',
params
})
}
// @Tags SysDictionary
// @Summary 导入字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body object true "字典JSON数据"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"导入成功"}"
// @Router /sysDictionary/importSysDictionary [post]
export const importSysDictionary = (data) => {
return service({
url: '/sysDictionary/importSysDictionary',
method: 'post',
data
})
}

View File

@@ -0,0 +1,126 @@
import service from '@/utils/request'
// @Tags SysError
// @Summary 创建错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body model.SysError true "创建错误日志"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /sysError/createSysError [post]
export const createSysError = (data) => {
return service({
url: '/sysError/createSysError',
method: 'post',
data
})
}
// @Tags SysError
// @Summary 删除错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body model.SysError true "删除错误日志"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /sysError/deleteSysError [delete]
export const deleteSysError = (params) => {
return service({
url: '/sysError/deleteSysError',
method: 'delete',
params
})
}
// @Tags SysError
// @Summary 批量删除错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body request.IdsReq true "批量删除错误日志"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
// @Router /sysError/deleteSysError [delete]
export const deleteSysErrorByIds = (params) => {
return service({
url: '/sysError/deleteSysErrorByIds',
method: 'delete',
params
})
}
// @Tags SysError
// @Summary 更新错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body model.SysError true "更新错误日志"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /sysError/updateSysError [put]
export const updateSysError = (data) => {
return service({
url: '/sysError/updateSysError',
method: 'put',
data
})
}
// @Tags SysError
// @Summary 用id查询错误日志
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data query model.SysError true "用id查询错误日志"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
// @Router /sysError/findSysError [get]
export const findSysError = (params) => {
return service({
url: '/sysError/findSysError',
method: 'get',
params
})
}
// @Tags SysError
// @Summary 分页获取错误日志列表
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data query request.PageInfo true "分页获取错误日志列表"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /sysError/getSysErrorList [get]
export const getSysErrorList = (params) => {
return service({
url: '/sysError/getSysErrorList',
method: 'get',
params
})
}
// @Tags SysError
// @Summary 不需要鉴权的错误日志接口
// @Accept application/json
// @Produce application/json
// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /sysError/getSysErrorPublic [get]
export const getSysErrorPublic = () => {
return service({
url: '/sysError/getSysErrorPublic',
method: 'get',
})
}
// @Tags SysError
// @Summary 触发错误处理(异步)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id query string true "错误日志ID"
// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"处理已提交\"}"
// @Router /sysError/getSysErrorSolution [get]
export const getSysErrorSolution = (params) => {
return service({
url: '/sysError/getSysErrorSolution',
method: 'get',
params
})
}

View File

@@ -1,51 +1,24 @@
import { createSysError } from '@/api/system/sysError'
function sendErrorTip(errorInfo) {
console.groupCollapsed(`捕获到错误: ${errorInfo.type}`);
console.log('错误类型:', errorInfo.type);
console.log('错误信息:', errorInfo.message);
console.log('调用栈:', errorInfo.stack);
if (errorInfo.component) {
console.log('组件名:', errorInfo.component.name)
console.log('组件地址:', errorInfo.component.__file)
}
if (errorInfo.vueInfo) console.log('Vue 信息:', errorInfo.vueInfo);
if (errorInfo.source) console.log('来源文件:', errorInfo.source);
if (errorInfo.lineno) console.log('行号:', errorInfo.lineno);
if (errorInfo.colno) console.log('列号:', errorInfo.colno);
console.groupEnd();
setTimeout(() => {
const errorData = {
form: errorInfo.type,
info: `${errorInfo.message}\nStack: ${errorInfo.stack}${errorInfo.component ? `\nComponent: ${errorInfo.component.name || 'Unknown'}` : ''}${errorInfo.vueInfo ? `\nVue Info: ${errorInfo.vueInfo}` : ''}${errorInfo.source ? `\nSource: ${errorInfo.source}:${errorInfo.lineno}:${errorInfo.colno}` : ''}`,
level: 'error',
solution: null
}
createSysError(errorData).catch(apiErr => {
console.error('Failed to create error record:', apiErr)
})
}, 0)
}
function initVueErrorHandler(app) {
app.config.errorHandler = (err, vm, info) => {
let errorType = 'Vue Error';
window.addEventListener('unhandledrejection', (event) => {
sendErrorTip({
type: errorType,
message: err.message,
stack: err.stack,
component: vm.$options || 'Unknown Vue Component',
vueInfo: info
type: '前端',
message: `错误信息: ${event.reason}`,
stack: `调用栈: ${event.reason?.stack || '没有调用栈信息'}`,
});
};
}
function initJsErrorHandler() {
window.onerror = (message, source, lineno, colno, error) => {
let errorType = 'JS Error';
sendErrorTip({
type: errorType,
message: message,
stack: error ? error.stack : 'No stack available',
source: source,
lineno: lineno,
colno: colno
});
return false;
};
}
export function initErrorHandler(app) {
initVueErrorHandler(app)
initJsErrorHandler()
}
});

View File

@@ -15,13 +15,10 @@ import auth from '@/directive/auth'
import clickOutSide from '@/directive/clickOutSide'
import { store } from '@/pinia'
import App from './App.vue'
// import { initErrorHandler } from '@/core/error-handel'
import '@/core/error-handel'
const app = createApp(App)
// 注入错误处理捕获
// initErrorHandler(app)
app.config.productionTip = false
app

View File

@@ -73,6 +73,7 @@
"/src/view/systemTools/index.vue": "System",
"/src/view/systemTools/installPlugin/index.vue": "Index",
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
"/src/view/systemTools/sysError/sysError.vue": "SysError",
"/src/view/systemTools/system/system.vue": "Config",
"/src/view/systemTools/version/version.vue": "SysVersion",
"/src/plugin/announcement/form/info.vue": "InfoForm",

View File

@@ -19,7 +19,8 @@ export const useAppStore = defineStore('app', () => {
show_watermark: true,
side_mode: 'normal',
// 页面过渡动画配置
transition_type: 'slide'
transition_type: 'slide',
global_size: 'default'
})
const isDark = useDark({
@@ -99,6 +100,10 @@ export const useAppStore = defineStore('app', () => {
config.transition_type = e
}
const toggleGlobalSize = (e) => {
config.global_size = e
}
const baseCoinfg = {
weakness: false,
grey: false,
@@ -111,7 +116,8 @@ export const useAppStore = defineStore('app', () => {
show_watermark: true,
side_mode: 'normal',
// 页面过渡动画配置
transition_type: 'slide'
transition_type: 'slide',
global_size: 'default'
}
const resetConfig = () => {
@@ -150,6 +156,7 @@ export const useAppStore = defineStore('app', () => {
toggleConfigWatermark,
toggleSideMode,
toggleTransition,
resetConfig
resetConfig,
toggleGlobalSize
}
})

View File

@@ -33,7 +33,6 @@ export const useUserStore = defineStore('user', () => {
}
})
}
console.log(appStore.config)
}
const setToken = (val) => {

View File

@@ -40,7 +40,7 @@
}
.gva-search-box {
@apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2;
@apply p-4 pb-0 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2;
}
.gva-form-box {

View File

@@ -1,9 +1,9 @@
<template>
<div class="grid grid-cols-2 gap-6 font-inter px-6">
<div class="grid grid-cols-2 gap-6 gva-theme-font px-6">
<div
v-for="layout in layoutModes"
:key="layout.value"
class="bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl p-6 cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-xl"
class="gva-theme-layout-card"
:class="{
'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900 transform -translate-y-1 shadow-xl': modelValue === layout.value
}"
@@ -43,7 +43,7 @@
</div>
<div class="text-center">
<span class="block text-base font-semibold text-gray-900 dark:text-white mb-2" :class="{ 'text-current': modelValue === layout.value }" :style="modelValue === layout.value ? { color: primaryColor } : {}">{{ layout.label }}</span>
<span class="block text-base font-semibold gva-theme-text-main mb-2" :class="{ 'text-current': modelValue === layout.value }" :style="modelValue === layout.value ? { color: primaryColor } : {}">{{ layout.label }}</span>
<span class="block text-sm text-gray-500 dark:text-gray-400">{{ layout.description }}</span>
</div>
</div>
@@ -163,30 +163,16 @@ const getHeaderStyle = (layout) => {
}
}
const getContentStyle = (layout) => {
const getContentStyle = () => {
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.5' }
}
const getRightSidebarStyle = (layout) => {
if (layout.primaryElement === 'rightSidebar') {
return { backgroundColor: primaryColor.value, opacity: '0.95' }
} else if (layout.secondaryElement === 'rightSidebar') {
return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' }
} else {
return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' }
}
}
const handleLayoutChange = (layout) => {
emit('update:modelValue', layout)
}
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.flex-col {
flex-direction: column;
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex items-center justify-between py-4 font-inter border-b border-gray-100 dark:border-gray-700 last:border-b-0">
<div class="gva-theme-setting-item">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ label }}</span>
<span class="gva-theme-setting-label">{{ label }}</span>
<slot name="suffix"></slot>
</div>
<div class="flex items-center setting-controls">
@@ -34,9 +34,7 @@ const primaryColorWithOpacity = computed(() => config.value.primaryColor + '40')
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.setting-controls {
::v-deep(.el-switch) {

View File

@@ -1,13 +1,13 @@
<template>
<div class="font-inter">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-8 shadow-sm">
<div class="mb-8">
<p class="text-base font-semibold text-gray-700 dark:text-gray-300 mb-5">精选色彩</p>
<div class="gva-theme-font">
<div class="gva-theme-card-bg p-4">
<div class="mb-4">
<p class="text-base font-semibold text-gray-700 dark:text-gray-300 mb-4">精选色彩</p>
<div class="grid grid-cols-3 gap-4">
<div
v-for="colorItem in presetColors"
:key="colorItem.color"
class="flex items-center gap-4 p-4 bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-lg"
class="flex items-center gap-4 p-2 bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-lg"
:class="{
'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-800 transform -translate-y-1 shadow-lg': modelValue === colorItem.color
}"
@@ -32,15 +32,15 @@
</div>
</div>
<div class="min-w-0 flex-1">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">{{ colorItem.name }}</span>
<span class="block text-sm font-semibold gva-theme-text-main">{{ colorItem.name }}</span>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between p-5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl mb-6 shadow-sm">
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl mb-6 shadow-sm">
<div class="flex-1">
<h4 class="text-base font-semibold text-gray-900 dark:text-white">自定义颜色</h4>
<h4 class="text-base font-semibold gva-theme-text-main">自定义颜色</h4>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">选择任意颜色作为主题色</p>
</div>
<el-color-picker
@@ -52,7 +52,7 @@
/>
</div>
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl p-5 shadow-sm">
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl p-4 shadow-sm">
<div class="flex items-center justify-between">
<span class="text-base font-semibold text-gray-700 dark:text-gray-300">当前主题色</span>
<div class="flex items-center gap-3">
@@ -122,9 +122,7 @@ watch(() => props.modelValue, (newValue) => {
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.custom-color-picker {
::v-deep(.el-color-picker__trigger) {

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex justify-center">
<div class="inline-flex bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-1 gap-1">
<div class="gva-theme-mode-selector">
<div
v-for="mode in themeModes"
:key="mode.value"
class="flex flex-col items-center justify-center px-4 py-3 rounded-md cursor-pointer transition-all duration-150 ease-in-out min-w-[64px]"
class="gva-theme-mode-item"
:class="[
modelValue === mode.value
? 'text-white shadow-sm transform -translate-y-0.5'
@@ -32,7 +32,7 @@ defineOptions({
name: 'ThemeModeSelector'
})
const props = defineProps({
defineProps({
modelValue: {
type: String,
default: 'auto'

View File

@@ -5,11 +5,11 @@
direction="rtl"
:size="width"
:show-close="false"
class="theme-config-drawer"
class="gva-theme-drawer"
>
<template #header>
<div class="flex items-center justify-between w-full px-6 py-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white font-inter">系统配置</h2>
<h2 class="text-xl font-semibold gva-theme-text-main gva-theme-font">系统配置</h2>
<el-button
type="primary"
size="small"
@@ -22,14 +22,14 @@
</div>
</template>
<div class="bg-white dark:bg-gray-900">
<div class="px-8 pt-4 pb-6 border-b border-gray-200 dark:border-gray-700">
<div class="bg-white dark:bg-gray-900 px-6">
<div class="px-8 pt-4 pb-6">
<div class="flex justify-center">
<div class="inline-flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1.5 border border-gray-200 dark:border-gray-700 shadow-sm">
<div
v-for="tab in tabs"
:key="tab.key"
class="px-6 py-3 text-base text-center cursor-pointer font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]"
class="px-4 py-2 text-base text-center cursor-pointer font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]"
:class="[
activeTab === tab.key
? 'text-white shadow-md transform -translate-y-0.5'
@@ -106,57 +106,100 @@
}, { deep: true });
</script>
<style lang="scss" scoped>
.theme-config-drawer {
<style lang="scss">
.gva-theme-drawer {
.el-drawer {
@apply bg-white dark:bg-gray-900;
}
.el-drawer__header {
@apply p-0 border-0;
}
.el-drawer__body {
@apply p-0;
}
}
.gva-theme-font {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
::v-deep(.el-drawer) {
background: white;
}
::v-deep(.el-drawer__header) {
padding: 0;
border: 0;
}
::v-deep(.el-drawer__body) {
padding: 0;
}
}
.dark .theme-config-drawer {
::v-deep(.el-drawer) {
background: #111827;
.gva-theme-card-bg {
@apply bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm;
}
.gva-theme-card-white {
@apply bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5;
}
.gva-theme-section-header {
@apply flex items-center justify-center mb-6;
}
.gva-theme-section-title {
@apply px-6 text-lg font-semibold text-gray-700 dark:text-gray-300;
}
.gva-theme-divider {
@apply h-px bg-gray-200 dark:bg-gray-700 flex-1;
}
.gva-theme-text-main {
@apply text-gray-900 dark:text-white;
}
.gva-theme-text-sub {
@apply text-gray-600 dark:text-gray-400;
}
.gva-theme-section-content {
animation: fadeInUp 0.3s ease;
}
.gva-theme-setting-item {
@apply flex items-center justify-between py-4 gva-theme-font border-b border-gray-100 dark:border-gray-700 last:border-b-0;
}
.gva-theme-setting-label {
@apply text-sm font-medium gva-theme-text-main;
}
.gva-theme-mode-selector {
@apply inline-flex bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-1 gap-1;
}
.gva-theme-mode-item {
@apply flex flex-col items-center justify-center px-3 py-2 rounded-md cursor-pointer transition-all duration-150 ease-in-out min-w-[64px];
}
.gva-theme-layout-card {
@apply bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl p-3 cursor-pointer transition-all duration-150 ease-in-out hover:-translate-y-1 hover:shadow-xl;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
}
.font-inter {
font-family: 'Inter', sans-serif;
}
.reset-btn {
border-radius: 0.5rem;
font-weight: 500;
transition: all 150ms ease-in-out;
&:hover {
transform: translateY(-2px);
filter: brightness(0.9);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
to {
opacity: 1;
transform: translateY(0);
}
}
/* Custom scrollbar for webkit browsers */
::-webkit-scrollbar {
.gva-theme-drawer ::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
.gva-theme-drawer ::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
.gva-theme-drawer ::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
@@ -165,11 +208,11 @@
}
}
.dark ::-webkit-scrollbar-track {
.dark .gva-theme-drawer ::-webkit-scrollbar-track {
background: #1f2937;
}
.dark ::-webkit-scrollbar-thumb {
.dark .gva-theme-drawer ::-webkit-scrollbar-thumb {
background: #4b5563;
&:hover {
@@ -177,3 +220,9 @@
}
}
</style>
<style lang="scss" scoped>
.reset-btn {
@apply rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5 hover:brightness-90 hover:shadow-lg;
}
</style>

View File

@@ -1,75 +1,86 @@
<template>
<div class="font-inter">
<div class="gva-theme-font">
<!-- Theme Mode Section -->
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题模式</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">主题模式</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<ThemeModeSelector
v-model="config.darkMode"
@update:modelValue="appStore.toggleDarkMode"
/>
<div class="gva-theme-section-content">
<ThemeModeSelector v-model="config.darkMode" @update:modelValue="appStore.toggleDarkMode" />
</div>
</div>
<!-- Theme Color Section -->
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题颜色</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">主题颜色</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<ThemeColorPicker
v-model="config.primaryColor"
@update:modelValue="appStore.togglePrimaryColor"
/>
<div class="gva-theme-section-content">
<ThemeColorPicker v-model="config.primaryColor" @update:modelValue="appStore.togglePrimaryColor" />
</div>
</div>
<!-- Global Size Section -->
<div class="mb-10">
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">全局尺寸</span>
<div class="gva-theme-divider"></div>
</div>
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<SettingItem label="全局尺寸">
<template #suffix>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">设置全局组件尺寸</span>
</template>
<div class="w-39">
<el-select v-model="config.global_size" placeholder="请选择" @change="appStore.toggleGlobalSize">
<el-option label="默认就好了" value="default" />
<el-option label="大点好" value="large" />
<el-option label="小的也不错" value="small" />
</el-select>
</div>
</SettingItem>
</div>
</div>
</div>
<!-- Visual Accessibility Section -->
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">视觉辅助</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">视觉辅助</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<SettingItem label="灰色模式">
<template #suffix>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">降低色彩饱和度</span>
</template>
<el-switch
v-model="config.grey"
@change="appStore.toggleGrey"
/>
<el-switch v-model="config.grey" @change="appStore.toggleGrey" />
</SettingItem>
<SettingItem label="色弱模式">
<template #suffix>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">优化色彩对比度</span>
</template>
<el-switch
v-model="config.weakness"
@change="appStore.toggleWeakness"
/>
<el-switch v-model="config.weakness" @change="appStore.toggleWeakness" />
</SettingItem>
<SettingItem label="显示水印">
<template #suffix>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">在页面显示水印标识</span>
</template>
<el-switch
v-model="config.show_watermark"
@change="appStore.toggleConfigWatermark"
/>
<el-switch v-model="config.show_watermark" @change="appStore.toggleConfigWatermark" />
</SettingItem>
</div>
</div>
@@ -92,23 +103,4 @@ const appStore = useAppStore()
const { config } = storeToRefs(appStore)
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.section-content {
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,38 +1,38 @@
<template>
<div class="font-inter">
<div class="gva-theme-font">
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">系统信息</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">系统信息</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
<span class="text-gray-600 dark:text-gray-400 font-medium">版本</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">v2.7.4</span>
<span class="gva-theme-text-sub font-medium">版本</span>
<span class="font-mono gva-theme-text-main font-semibold">v2.7.4</span>
</div>
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
<span class="text-gray-600 dark:text-gray-400 font-medium">前端框架</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">Vue 3</span>
<span class="gva-theme-text-sub font-medium">前端框架</span>
<span class="font-mono gva-theme-text-main font-semibold">Vue 3</span>
</div>
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
<span class="text-gray-600 dark:text-gray-400 font-medium">UI 组件库</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">Element Plus</span>
<span class="gva-theme-text-sub font-medium">UI 组件库</span>
<span class="font-mono gva-theme-text-main font-semibold">Element Plus</span>
</div>
<div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600">
<span class="text-gray-600 dark:text-gray-400 font-medium">构建工具</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">Vite</span>
<span class="gva-theme-text-sub font-medium">构建工具</span>
<span class="font-mono gva-theme-text-main font-semibold">Vite</span>
</div>
<div class="flex justify-between items-center py-3">
<span class="text-gray-600 dark:text-gray-400 font-medium">浏览器</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">{{ browserInfo }}</span>
<span class="gva-theme-text-sub font-medium">浏览器</span>
<span class="font-mono gva-theme-text-main font-semibold">{{ browserInfo }}</span>
</div>
<div class="flex justify-between items-center py-3">
<span class="text-gray-600 dark:text-gray-400 font-medium">屏幕分辨率</span>
<span class="font-mono text-gray-900 dark:text-white font-semibold">{{ screenResolution }}</span>
<span class="gva-theme-text-sub font-medium">屏幕分辨率</span>
<span class="font-mono gva-theme-text-main font-semibold">{{ screenResolution }}</span>
</div>
</div>
</div>
@@ -40,24 +40,24 @@
</div>
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">配置管理</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">配置管理</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<div class="space-y-5">
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
class="gva-theme-card-white flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center justify-center text-red-600 dark:text-red-400 text-xl">
🔄
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">重置配置</h4>
<h4 class="text-sm font-semibold gva-theme-text-main">重置配置</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">将所有设置恢复为默认值</p>
</div>
</div>
@@ -69,14 +69,14 @@
</div>
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
class="gva-theme-card-white flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl">
📤
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">导出配置</h4>
<h4 class="text-sm font-semibold gva-theme-text-main">导出配置</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">导出当前配置为 JSON 文件</p>
</div>
</div>
@@ -89,14 +89,14 @@
</div>
<div
class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
class="gva-theme-card-white flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center justify-center text-green-600 dark:text-green-400 text-xl">
📥
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">导入配置</h4>
<h4 class="text-sm font-semibold gva-theme-text-main">导入配置</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1"> JSON 文件导入配置</p>
</div>
</div>
@@ -114,22 +114,22 @@
</div>
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">关于项目</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">关于项目</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<div class="flex items-start gap-5">
<div
class="w-16 h-16 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
<Logo />
</div>
<div class="flex-1">
<h4 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Gin-Vue-Admin</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-5 leading-relaxed">
<h4 class="text-xl font-semibold gva-theme-text-main mb-3">Gin-Vue-Admin</h4>
<p class="text-sm gva-theme-text-sub mb-5 leading-relaxed">
基于 Vue3 + Gin 的全栈开发基础平台提供完整的后台管理解决方案
</p>
<div class="flex items-center gap-3 text-sm">
@@ -244,24 +244,4 @@ const handleImportConfig = (file) => {
}
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.section-content {
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="font-inter">
<div class="gva-theme-font">
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">布局模式</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">布局模式</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="gva-theme-section-content">
<LayoutModeCard
v-model="config.side_mode"
@update:modelValue="appStore.toggleSideMode"
@@ -16,14 +16,14 @@
</div>
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">界面配置</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">界面配置</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<SettingItem label="显示标签页">
<template #suffix>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">页面标签导航</span>
@@ -55,19 +55,19 @@
</div>
<div class="mb-10">
<div class="flex items-center justify-center mb-6">
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">尺寸配置</span>
<div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div>
<div class="gva-theme-section-header">
<div class="gva-theme-divider"></div>
<span class="gva-theme-section-title">尺寸配置</span>
<div class="gva-theme-divider"></div>
</div>
<div class="section-content">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm">
<div class="space-y-6">
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="gva-theme-section-content">
<div class="gva-theme-card-bg">
<div class="space-y-4">
<div class="gva-theme-card-white">
<div class="flex items-center justify-between">
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏展开宽度</h4>
<h4 class="text-sm font-medium gva-theme-text-main">侧边栏展开宽度</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏完全展开时的宽度</p>
</div>
<div class="flex items-center gap-2">
@@ -84,10 +84,10 @@
</div>
</div>
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="gva-theme-card-white">
<div class="flex items-center justify-between">
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏收缩宽度</h4>
<h4 class="text-sm font-medium gva-theme-text-main">侧边栏收缩宽度</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏收缩时的最小宽度</p>
</div>
<div class="flex items-center gap-2">
@@ -103,10 +103,10 @@
</div>
</div>
<div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5">
<div class="gva-theme-card-white">
<div class="flex items-center justify-between">
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white">菜单项高度</h4>
<h4 class="text-sm font-medium gva-theme-text-main">菜单项高度</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏菜单项的行高</p>
</div>
<div class="flex items-center gap-2">
@@ -142,23 +142,4 @@ const appStore = useAppStore()
const { config } = storeToRefs(appStore)
</script>
<style scoped>
.font-inter {
font-family: 'Inter', sans-serif;
}
.section-content {
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -4,7 +4,7 @@
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
/>
<el-splitter class="h-full">
<el-splitter-panel size="400px" min="200px" max="800px" collapsible>
<el-splitter-panel size="300px" min="200px" max="800px" collapsible>
<div
class="flex-none bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
>
@@ -30,13 +30,36 @@
>
</template>
</el-input>
<el-button
class="ml-auto"
:icon="Search"
@click="showSearchInputHandler"
></el-button>
<el-button type="primary" @click="openDrawer" :icon="Plus">
</el-button>
<el-button-group class="ml-auto">
<el-tooltip content="搜索" placement="top">
<el-button
:icon="Search"
@click="showSearchInputHandler"
/>
</el-tooltip>
<el-tooltip content="导入字典" placement="top">
<el-button
type="success"
:icon="Upload"
@click="openImportDialog"
/>
</el-tooltip>
<el-tooltip content="AI 生成字典" placement="top">
<el-button
type="warning"
@click="openAiDialog"
>
AI
</el-button>
</el-tooltip>
<el-tooltip content="新建字典" placement="top">
<el-button
type="primary"
:icon="Plus"
@click="openDrawer"
/>
</el-tooltip>
</el-button-group>
</div>
<el-scrollbar class="mt-4" style="height: calc(100vh - 300px)">
<div
@@ -61,15 +84,22 @@
<span class="mr-auto text-sm">{{ dictionary.type }}</span>
</div>
<div class="min-w-[40px]">
<div class="min-w-[60px] flex items-center gap-2">
<el-icon
class="text-blue-500"
class="!text-green-500"
@click.stop="exportDictionary(dictionary)"
title="导出字典"
>
<Download />
</el-icon>
<el-icon
class="!text-blue-500"
@click.stop="updateSysDictionaryFunc(dictionary)"
>
<Edit />
</el-icon>
<el-icon
class="ml-2 text-red-500"
class="!text-red-500"
@click="deleteSysDictionaryFunc(dictionary)"
>
<Delete />
@@ -160,6 +190,154 @@
</el-form-item>
</el-form>
</el-drawer>
<!-- 导入字典抽屉 -->
<el-drawer
v-model="importDrawerVisible"
:size="appStore.drawerSize"
:show-close="false"
:before-close="closeImportDrawer"
>
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">导入字典JSON</span>
<div>
<el-button @click="closeImportDrawer"> </el-button>
<el-button type="primary" @click="handleImport" :loading="importing">
确认导入
</el-button>
</div>
</div>
</template>
<div class="import-drawer-content">
<div class="mb-4">
<el-alert
title="请粘贴、编辑或拖拽JSON文件到下方区域"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 拖拽上传区域 -->
<div
class="drag-upload-area"
:class="{ 'is-dragging': isDragging }"
@drop.prevent="handleDrop"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@click="triggerFileInput"
>
<el-icon class="upload-icon"><Upload /></el-icon>
<div class="upload-text">
<p> JSON 文件拖到此处或点击选择文件</p>
<p class="upload-hint">也可以在下方文本框直接编辑</p>
</div>
<input
ref="fileInputRef"
type="file"
accept=".json,application/json"
style="display: none"
@change="handleFileSelect"
/>
</div>
<div class="json-editor-container mt-4">
<el-input
v-model="importJsonText"
type="textarea"
:rows="15"
placeholder='请输入JSON数据例如
{
"name": "性别",
"type": "gender",
"status": true,
"desc": "性别字典",
"sysDictionaryDetails": [
{
"label": "男",
"value": "1",
"status": true,
"sort": 1
},
{
"label": "女",
"value": "2",
"status": true,
"sort": 2
}
]
}'
class="json-textarea"
/>
</div>
<div class="mt-4" v-if="jsonPreviewError">
<el-alert
:title="jsonPreviewError"
type="error"
:closable="false"
show-icon
/>
</div>
</div>
</el-drawer>
<!-- AI 对话框 -->
<el-dialog
v-model="aiDialogVisible"
title="AI 生成字典"
width="520px"
:before-close="closeAiDialog"
>
<div class="relative">
<el-input
v-model="aiPrompt"
type="textarea"
:rows="6"
:maxlength="2000"
placeholder="请输入生成字典的描述,例如:生成一个用户状态字典(启用/禁用)\n支持粘贴或上传图片后识图生成。"
resize="none"
@keydown.ctrl.enter="handleAiGenerate"
@paste="handlePaste"
@focus="handleFocus"
@blur="handleBlur"
/>
<input
ref="imageFileInputRef"
type="file"
accept="image/*"
style="display:none"
@change="handleImageSelect"
/>
<div class="flex absolute right-2 bottom-2">
<el-tooltip effect="light">
<template #content>
<div>粘贴或上传图片后识别图片内容生成字典</div>
</template>
<el-button type="primary" @click="eyeFunc">
<el-icon size="18">
<ai-gva />
</el-icon>
识图
</el-button>
</el-tooltip>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeAiDialog"> </el-button>
<el-button type="primary" @click="handleAiGenerate" :loading="aiGenerating">
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -169,14 +347,17 @@
deleteSysDictionary,
updateSysDictionary,
findSysDictionary,
getSysDictionaryList
getSysDictionaryList,
exportSysDictionary,
importSysDictionary
} from '@/api/sysDictionary' // 此处请自行替换地址
import { butler, eye } from '@/api/autoCode'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import sysDictionaryDetail from './sysDictionaryDetail.vue'
import { Edit, Plus, Search } from '@element-plus/icons-vue'
import { Edit, Plus, Search, Download, Upload } from '@element-plus/icons-vue'
import { useAppStore } from '@/pinia'
defineOptions({
@@ -223,6 +404,103 @@
const dictionaryData = ref([])
const availableParentDictionaries = ref([])
// 导入相关
const importDrawerVisible = ref(false)
const importJsonText = ref('')
const importing = ref(false)
const jsonPreviewError = ref('')
const jsonPreview = ref(null)
const isDragging = ref(false)
const fileInputRef = ref(null)
// AI 相关
const aiDialogVisible = ref(false)
const aiPrompt = ref('')
const aiGenerating = ref(false)
// 图片上传/识别相关
const imageFileInputRef = ref(null)
const focused = ref(false)
const handleFocus = () => {
focused.value = true
}
const handleBlur = () => {
focused.value = false
}
// 触发图片选择
const triggerImageSelect = () => {
imageFileInputRef.value?.click()
}
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
const reader = new FileReader();
reader.onload =async (e) => {
const base64String = e.target.result;
const res = await eye({ picture: base64String,command: 'dictEye' })
if (res.code === 0) {
aiPrompt.value = res.data
}
};
reader.readAsDataURL(file);
}
}
};
const eyeFunc = async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const base64String = e.target.result;
const res = await eye({ picture: base64String,command: 'dictEye' })
if (res.code === 0) {
aiPrompt.value = res.data
}
};
reader.readAsDataURL(file);
}
};
input.click();
}
// 监听JSON文本变化实时预览
watch(importJsonText, (newVal) => {
if (!newVal.trim()) {
jsonPreview.value = null
jsonPreviewError.value = ''
return
}
try {
jsonPreview.value = JSON.parse(newVal)
jsonPreviewError.value = ''
} catch (e) {
jsonPreviewError.value = 'JSON格式错误: ' + e.message
jsonPreview.value = null
}
})
// 格式化JSON预览
const jsonPreviewFormatted = computed(() => {
if (!jsonPreview.value) return ''
return JSON.stringify(jsonPreview.value, null, 2)
})
// 查询
const getTableData = async () => {
const res = await getSysDictionaryList({
@@ -358,9 +636,190 @@
getTableData()
}
}
// 导出字典
const exportDictionary = async (row) => {
try {
const res = await exportSysDictionary({ ID: row.ID })
if (res.code === 0) {
// 将JSON数据转换为字符串并下载
const jsonStr = JSON.stringify(res.data, null, 2)
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${row.type}_${row.name}_dictionary.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
}
} catch (error) {
ElMessage.error('导出失败: ' + error.message)
}
}
// 打开导入抽屉
const openImportDialog = () => {
importDrawerVisible.value = true
importJsonText.value = ''
jsonPreview.value = null
jsonPreviewError.value = ''
isDragging.value = false
}
// 关闭导入抽屉
const closeImportDrawer = () => {
importDrawerVisible.value = false
importJsonText.value = ''
jsonPreview.value = null
jsonPreviewError.value = ''
isDragging.value = false
}
// 处理拖拽进入
const handleDragOver = (e) => {
isDragging.value = true
}
// 处理拖拽离开
const handleDragLeave = (e) => {
isDragging.value = false
}
// 处理文件拖拽
const handleDrop = (e) => {
isDragging.value = false
const files = e.dataTransfer.files
if (files.length === 0) return
const file = files[0]
readJsonFile(file)
}
// 触发文件选择
const triggerFileInput = () => {
fileInputRef.value?.click()
}
// 处理文件选择
const handleFileSelect = (e) => {
const files = e.target.files
if (files.length === 0) return
const file = files[0]
readJsonFile(file)
// 清空input以便可以重复选择同一文件
e.target.value = ''
}
// 读取JSON文件
const readJsonFile = (file) => {
// 检查文件类型
if (!file.name.endsWith('.json')) {
ElMessage.warning('请上传 JSON 文件')
return
}
// 读取文件内容
const reader = new FileReader()
reader.onload = (event) => {
try {
const content = event.target.result
// 验证是否为有效的 JSON
JSON.parse(content)
importJsonText.value = content
ElMessage.success('文件读取成功')
} catch (error) {
ElMessage.error('文件内容不是有效的 JSON 格式')
}
}
reader.onerror = () => {
ElMessage.error('文件读取失败')
}
reader.readAsText(file)
}
// 处理导入
const handleImport = async () => {
if (!importJsonText.value.trim()) {
ElMessage.warning('请输入JSON数据')
return
}
if (jsonPreviewError.value) {
ElMessage.error('JSON格式错误请检查后重试')
return
}
try {
importing.value = true
const res = await importSysDictionary({ json: importJsonText.value })
if (res.code === 0) {
ElMessage.success('导入成功')
closeImportDrawer()
getTableData()
}
} catch (error) {
ElMessage.error('导入失败: ' + error.message)
} finally {
importing.value = false
}
}
// 打开 AI 对话框
const openAiDialog = () => {
aiDialogVisible.value = true
aiPrompt.value = ''
}
// 关闭 AI 对话框
const closeAiDialog = () => {
aiDialogVisible.value = false
aiPrompt.value = ''
}
// 处理 AI 生成
const handleAiGenerate = async () => {
if (!aiPrompt.value.trim()) {
ElMessage.warning('请输入描述内容')
return
}
try {
aiGenerating.value = true
const aiRes = await butler({
prompt: aiPrompt.value,
command: 'dict'
})
if (aiRes && aiRes.code === 0) {
ElMessage.success('AI 生成成功')
try {
// 将 AI 返回的数据填充到导入文本框(支持字符串或对象)
if (typeof aiRes.data === 'string') {
importJsonText.value = aiRes.data
} else {
importJsonText.value = JSON.stringify(aiRes.data, null, 2)
}
// 清除可能的解析错误并打开导入抽屉
jsonPreviewError.value = ''
importDrawerVisible.value = true
closeAiDialog()
} catch (e) {
ElMessage.error('处理 AI 返回结果失败: ' + (e.message || e))
}
} else {
ElMessage.error(aiRes.msg || 'AI 生成失败')
}
} catch (err) {
ElMessage.error('AI 调用失败: ' + (err.message || err))
} finally {
aiGenerating.value = false
}
}
</script>
<style>
<style scoped>
.dict-box {
height: calc(100vh - 240px);
}
@@ -369,4 +828,99 @@
background-color: var(--el-color-primary) !important;
color: #fff;
}
.import-drawer-content {
padding: 0 4px;
}
/* 拖拽上传区域 */
.drag-upload-area {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
background-color: #fafafa;
transition: all 0.3s ease;
cursor: pointer;
}
.drag-upload-area:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.drag-upload-area.is-dragging {
border-color: #409eff;
background-color: #ecf5ff;
transform: scale(1.02);
}
.upload-icon {
font-size: 48px;
color: #8c939d;
margin-bottom: 16px;
}
.drag-upload-area.is-dragging .upload-icon {
color: #409eff;
}
.upload-text {
color: #606266;
}
.upload-text p {
margin: 4px 0;
}
.upload-hint {
font-size: 12px;
color: #909399;
}
.json-editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.json-textarea :deep(.el-textarea__inner) {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
line-height: 1.5;
}
.json-preview {
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 16px;
max-height: 400px;
overflow: auto;
}
.json-preview pre {
margin: 0;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.dark .drag-upload-area {
background-color: #1d1e1f;
border-color: #414243;
}
.dark .drag-upload-area:hover,
.dark .drag-upload-area.is-dragging {
background-color: #1a3a52;
border-color: #409eff;
}
.dark .json-preview {
background-color: #1d1e1f;
border-color: #414243;
}
</style>

View File

@@ -38,25 +38,19 @@
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="展示值" prop="label" min-width="240"/>
<el-table-column align="left" label="展示值" prop="label" min-width="100"/>
<el-table-column align="left" label="字典值" prop="value" />
<el-table-column align="left" label="扩展值" prop="extend" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">
{{ formatDate(scope.row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column align="left" label="层级" prop="level" width="80" />
<el-table-column
align="left"
label="启用状态"
prop="status"
width="120"
width="100"
>
<template #default="scope">
{{ formatBoolean(scope.row.status) }}
@@ -67,17 +61,18 @@
align="left"
label="排序标记"
prop="sort"
width="120"
width="100"
/>
<el-table-column
align="left"
label="操作"
:min-width="appStore.operateMinWith"
fixed="right"
>
<template #default="scope">
<el-button
type="success"
type="primary"
link
icon="plus"
@click="addChildNode(scope.row)"

View File

@@ -139,7 +139,7 @@
icon="edit-pen"
class="table-button"
@click="showCode(scope.row)"
>代码</el-button
>代码和SQL预览</el-button
>
<el-button
type="primary"
@@ -401,28 +401,79 @@
</el-form>
</el-drawer>
<!-- 合并:代码模板 + SQL预览 抽屉 -->
<el-drawer
v-model="codeVisible"
size="60%"
:before-close="closeDialog"
:title="type === 'create' ? '添加' : '修改'"
:show-close="false"
v-model="drawerVisible"
size="70%"
:title="'模板与预览'"
:show-close="true"
destroy-on-close
>
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">模板</span>
<span class="text-lg">模板与预览</span>
<div>
<el-button type="primary" @click="closeDialog">确 定</el-button>
<el-button @click="drawerVisible = false">关 闭</el-button>
<el-button v-if="activeTab === 'sql'" type="primary" @click="runPreview">生 成</el-button>
</div>
</div>
</template>
<v-ace-editor
v-model:value="webCode"
lang="vue"
theme="github_dark"
class="h-full"
/>
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="代码模板" name="code">
<v-ace-editor
v-model:value="webCode"
lang="vue"
theme="github_dark"
class="w-full h-96"
:options="{ showPrintMargin: false, fontSize: 14 }"
/>
</el-tab-pane>
<el-tab-pane label="SQL预览" name="sql">
<div class="flex flex-col gap-4">
<div class="w-full">
<el-form :model="previewForm" label-width="120px">
<el-form-item label="过滤已删除">
<el-switch v-model="previewForm.filterDeleted" />
</el-form-item>
<el-form-item label="默认排序">
<el-input v-model="previewForm.order" placeholder="例如: id desc" />
</el-form-item>
<el-form-item label="限制条数">
<el-input-number v-model="previewForm.limit" :min="0" />
</el-form-item>
<el-form-item label="偏移量">
<el-input-number v-model="previewForm.offset" :min="0" />
</el-form-item>
<el-divider content-position="left">查询条件</el-divider>
<div v-if="previewConditions.length === 0" class="text-gray">该模板暂无条件</div>
<template v-for="(cond, idx) in previewConditions" :key="idx">
<el-form-item :label="cond.column + ' ' + cond.operator">
<template v-if="cond.operator === 'BETWEEN'">
<div class="flex gap-2 w-full">
<el-input v-model="previewForm['start' + cond.from]" placeholder="开始值: start{{cond.from}}" />
<el-input v-model="previewForm['end' + cond.from]" placeholder="结束值: end{{cond.from}}" />
</div>
</template>
<template v-else>
<el-input v-model="previewForm[cond.from]" :placeholder="'变量: ' + cond.from" />
</template>
</el-form-item>
</template>
</el-form>
</div>
<div class="w-full">
<v-ace-editor
v-model:value="previewSQLCode"
lang="sql"
theme="github_dark"
class="w-full h-96"
:options="aceOptions"
/>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-drawer>
</div>
</template>
@@ -436,6 +487,7 @@
findSysExportTemplate,
getSysExportTemplateList
} from '@/api/exportTemplate.js'
import { previewSQL } from '@/api/exportTemplate.js'
// 全量引入格式化工具 请按需保留
import { formatDate } from '@/utils/format'
@@ -448,6 +500,7 @@
import 'ace-builds/src-noconflict/mode-vue'
import 'ace-builds/src-noconflict/theme-github_dark'
import 'ace-builds/src-noconflict/mode-sql'
defineOptions({
name: 'ExportTemplate'
@@ -903,7 +956,8 @@ JOINS模式下不支持导入
getTableData()
}
}
const codeVisible = ref(false)
const drawerVisible = ref(false)
const activeTab = ref('code')
// 弹窗控制标记
const dialogFormVisible = ref(false)
@@ -911,7 +965,56 @@ JOINS模式下不支持导入
const showCode = (row) => {
webCode.value = getCode(row.templateID)
codeVisible.value = true
activeTab.value = 'code'
drawerVisible.value = true
}
// 预览 SQL
const previewForm = ref({ filterDeleted: true, order: '', limit: 0, offset: 0 })
const previewSQLCode = ref('')
const previewTemplate = ref(null)
const previewConditions = ref([])
const aceOptions = { wrap: true, showPrintMargin: false, fontSize: 14 }
const openPreview = async (row) => {
// 获取模板完整信息以展示条件输入项
const res = await findSysExportTemplate({ ID: row.ID })
if (res.code === 0) {
previewTemplate.value = res.data.resysExportTemplate
previewConditions.value = (previewTemplate.value.conditions || []).map((c) => ({
from: c.from,
column: c.column,
operator: c.operator
}))
// 预填默认的排序与限制
previewForm.value.order = previewTemplate.value.order || ''
previewForm.value.limit = previewTemplate.value.limit || 0
previewForm.value.offset = 0
previewSQLCode.value = ''
activeTab.value = 'sql'
drawerVisible.value = true
}
}
const runPreview = async () => {
if (!previewTemplate.value) return
// 组装 params与导出组件保持一致
const paramsCopy = JSON.parse(JSON.stringify(previewForm.value))
// 将布尔与数值等按照导出组件规则编码
if (paramsCopy.filterDeleted) paramsCopy.filterDeleted = 'true'
const entries = Object.entries(paramsCopy).filter(([key, v]) => {
if (v === '' || v === null || v === undefined) return false
if ((key === 'limit' || key === 'offset') && Number(v) === 0) return false
return true
})
const params = entries
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const res = await previewSQL({ templateID: previewTemplate.value.templateID, params })
if (res.code === 0) {
previewSQLCode.value = res.data.sql || ''
}
}
// 打开弹窗
@@ -922,7 +1025,6 @@ JOINS模式下不支持导入
// 关闭弹窗
const closeDialog = () => {
codeVisible.value = false
dialogFormVisible.value = false
formData.value = {
name: '',

View File

@@ -0,0 +1,457 @@
<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="form">
<el-input v-model="searchInfo.form" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="错误内容" prop="info">
<el-input v-model="searchInfo.info" 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
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="form"
width="120"
/>
<el-table-column
align="left"
label="错误等级"
prop="level"
width="120"
>
<template #default="scope">
<el-tag
effect="dark"
:type="levelTagMap[scope.row.level] || 'info'"
>
{{ levelLabelMap[scope.row.level] || defaultLevelLabel }}
</el-tag>
</template>
</el-table-column>
<el-table-column
align="left"
label="处理状态"
prop="status"
width="140"
>
<template #default="scope">
<el-tag
effect="light"
:type="statusTagMap[scope.row.status] || 'info'"
>
{{ statusLabelMap[scope.row.status] || defaultStatusLabel }}
</el-tag>
</template>
</el-table-column>
<el-table-column
align="left"
label="错误内容"
prop="info"
show-overflow-tooltip
width="240"
/>
<el-table-column
align="left"
label="解决方案"
show-overflow-tooltip
prop="solution"
width="120"
/>
<el-table-column
align="left"
label="操作"
fixed="right"
:min-width="appStore.operateMinWith"
>
<template #default="scope">
<el-button
v-if="scope.row.status !== '处理中'"
type="primary"
link
class="table-button"
@click="getSolution(scope.row.ID)"
>
<el-icon><ai-gva /></el-icon>方案
</el-button>
<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="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="2" border direction="vertical">
<el-descriptions-item label="错误来源">
{{ detailForm.form }}
</el-descriptions-item>
<el-descriptions-item label="错误等级">
<el-tag
effect="dark"
:type="levelTagMap[detailForm.level] || 'info'"
>
{{ levelLabelMap[detailForm.level] || defaultLevelLabel }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理状态">
<el-tag
effect="light"
:type="statusTagMap[detailForm.status] || 'info'"
>
{{ statusLabelMap[detailForm.status] || defaultStatusLabel }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="错误内容" :span="2">
<pre class="whitespace-pre-wrap break-words">{{ detailForm.info }}</pre>
</el-descriptions-item>
<el-descriptions-item label="解决方案" :span="2">
<pre class="whitespace-pre-wrap break-words">{{ detailForm.solution }}</pre>
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
</template>
<script setup>
import {
deleteSysError,
deleteSysErrorByIds,
findSysError,
getSysErrorList,
getSysErrorSolution
} from '@/api/system/sysError'
import { formatDate } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref } from 'vue'
import { useAppStore } from '@/pinia'
defineOptions({
name: 'SysError'
})
const appStore = useAppStore()
// 控制更多查询条件显示/隐藏状态
const showAllQuery = ref(false)
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 getSolution = async (id) => {
const confirmed = await ElMessageBox.confirm(
'日志将通过 AI-PATH 传输至 GVA AI 用于错误分析,并在 GVA 官方平台短暂存储作为 AI 上下文。是否确认进行 AI 处理?(此功能仅向授权用户开放)',
'提示(Beta)',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).catch(() => false)
if (!confirmed) return
const res = await getSysErrorSolution({ id })
if (res.code === 0) {
ElMessage({ type: 'success', message: res.msg || '处理已提交1分钟后完成' })
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 getSysErrorList({
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 setOptions = async () => {}
// 获取需要的字典 可能为空 按需保留
setOptions()
// 多选数据
const multipleSelection = ref([])
// 多选
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
// 删除行
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteSysErrorFunc(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 deleteSysErrorByIds({ IDs })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === IDs.length && page.value > 1) {
page.value--
}
getTableData()
}
})
}
// 删除行
const deleteSysErrorFunc = async (row) => {
const res = await deleteSysError({ 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 findSysError({ ID: row.ID })
if (res.code === 0) {
detailForm.value = res.data
openDetailShow()
}
}
// 关闭详情弹窗
const closeDetailShow = () => {
detailShow.value = false
detailForm.value = {}
}
const statusLabelMap = {
未处理: '未处理',
处理中: '处理中',
处理完成: '处理完成',
处理失败: '处理失败'
}
const statusTagMap = {
未处理: 'info',
处理中: 'warning',
处理完成: 'success',
处理失败: 'danger'
}
const defaultStatusLabel = '未处理'
const levelLabelMap = {
fatal: '致命错误',
error: '一般错误'
}
const levelTagMap = {
fatal: 'danger',
error: 'warning'
}
const defaultLevelLabel = '一般错误'
</script>

View File

@@ -96,7 +96,8 @@
</el-drawer>
<!-- 导出版本抽屉 -->
<el-drawer v-model="exportDialogVisible" title="创建发版" direction="rtl" size="80%" :before-close="closeExportDialog" :show-close="false">
<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>
@@ -119,14 +120,14 @@
<el-form-item label="发版信息">
<div class="flex gap-3 w-full">
<!-- 菜单选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<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 class="card-col card-vertical">
<div class="card-header">
<span class="card-title">选择菜单</span>
</div>
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
<div class="card-filter">
<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">
<div class="card-body">
<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">
@@ -140,16 +141,16 @@
</div>
<!-- API选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<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 class="card-col card-vertical">
<div class="card-header">
<span class="card-title">选择API</span>
</div>
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
<div class="card-filter">
<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">
<div class="card-body">
<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">
@@ -157,7 +158,8 @@
<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">
<span
class="max-w-[240px] break-all overflow-ellipsis overflow-hidden text-gray-500 dark:text-gray-400">
{{ data.path }}
</span>
</el-tooltip>
@@ -168,14 +170,14 @@
</div>
<!-- 字典选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<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 class="card-col card-vertical">
<div class="card-header">
<span class="card-title">选择字典</span>
</div>
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
<div class="card-filter">
<el-input v-model="dictFilterText" placeholder="输入关键字进行过滤" clearable size="small" />
</div>
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
<div class="card-body">
<el-tree ref="dictTreeRef" :data="dictTreeData" :default-checked-keys="selectedDictIds"
:props="dictTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox
:filter-node-method="filterDictNode" @check="onDictCheck" class="dict-tree">
@@ -183,7 +185,7 @@
<div class="flex items-center justify-between w-full pr-1">
<span>{{ data.name || data.label }}</span>
<el-tooltip :content="data.desc || (data.value ? `值: ${data.value}` : '')">
<span class="text-gray-500 text-xs ml-2">
<span class="text-gray-500 dark:text-gray-400 text-xs ml-2">
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
</span>
</el-tooltip>
@@ -198,7 +200,8 @@
</el-drawer>
<!-- 导入版本抽屉 -->
<el-drawer v-model="importDialogVisible" title="导入版本" direction="rtl" size="80%" :before-close="closeImportDialog" :show-close="false">
<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>
@@ -211,16 +214,8 @@
</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-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>
@@ -237,77 +232,60 @@
@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="preview-wrap">
<div class="flex gap-3 w-full">
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
<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 class="card-col">
<div class="card-vertical">
<div class="card-header">
<h3 class="card-title">菜单 ({{ 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
>
<div class="card-body">
<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>
<span class="text-gray-500 dark:text-gray-400 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/3">
<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 class="card-col">
<div class="card-vertical">
<div class="card-header">
<h3 class="card-title">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
>
<div class="card-body">
<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>
<span class="text-gray-500 dark:text-gray-400 text-xs ml-2">{{ data.path }} [{{ data.method
}}]</span>
</div>
</template>
</el-tree>
</div>
</div>
</div>
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
<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">字典 ({{ importPreviewData.dictionaries?.length || 0 }})</h3>
<div class="card-col">
<div class="card-vertical">
<div class="card-header">
<h3 class="card-title">字典 ({{ importPreviewData.dictionaries?.length || 0 }})</h3>
</div>
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
<el-tree
:data="previewDictTreeData"
:props="dictTreeProps"
node-key="ID"
:expand-on-click-node="false"
:check-on-click-node="false"
:show-checkbox="false"
default-expand-all
>
<div class="card-body">
<el-tree :data="previewDictTreeData" :props="dictTreeProps" 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.name || data.label }}</span>
<span class="text-gray-500 text-xs ml-2">
<span class="text-gray-500 dark:text-gray-400 text-xs ml-2">
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
</span>
</div>
@@ -661,11 +639,11 @@ const filterDictNode = (value, data) => {
const desc = data.desc || ''
const label = data.label || ''
const dataValue = data.value || ''
return name.indexOf(value) !== -1 ||
type.indexOf(value) !== -1 ||
desc.indexOf(value) !== -1 ||
label.indexOf(value) !== -1 ||
dataValue.indexOf(value) !== -1
return name.indexOf(value) !== -1 ||
type.indexOf(value) !== -1 ||
desc.indexOf(value) !== -1 ||
label.indexOf(value) !== -1 ||
dataValue.indexOf(value) !== -1
}
const onMenuCheck = (data, checked) => {
@@ -789,14 +767,14 @@ const closeImportDialog = () => {
// 文件上传处理函数
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) => {
@@ -825,7 +803,7 @@ const handleFileRemove = () => {
// 计算菜单总数(递归计算所有菜单项)
const getTotalMenuCount = () => {
if (!importPreviewData.value?.menus) return 0
const countMenus = (menus) => {
let count = 0
menus.forEach(menu => {
@@ -836,7 +814,7 @@ const getTotalMenuCount = () => {
})
return count
}
return countMenus(importPreviewData.value.menus)
}
@@ -971,6 +949,35 @@ const downloadJson = async (row) => {
</script>
<style scoped>
/* 复用卡片样式(支持暗色) */
.card-col {
@apply border border-gray-300 dark:border-gray-600 rounded overflow-hidden flex-1 bg-white dark:bg-gray-900;
}
.card-vertical {
@apply flex flex-col h-full;
}
.card-header {
@apply flex justify-between items-center px-4 py-3 bg-gray-50 dark:bg-gray-800 border-b border-gray-300 dark:border-gray-600;
}
.card-title {
@apply m-0 text-gray-800 dark:text-gray-200 text-base font-medium;
}
.card-filter {
@apply px-4 py-3 border-b border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800;
}
.card-body {
@apply flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto;
}
.preview-wrap {
@apply flex flex-col flex-1 gap-4 border border-gray-300 dark:border-gray-600 rounded p-4 bg-gray-50 dark:bg-gray-900;
}
/* Element Plus 树形组件样式优化 */
:deep(.el-tree) {
background-color: transparent;

View File

@@ -99,7 +99,7 @@ export default ({ mode }) => {
optimizeDeps,
plugins: [
process.env.VITE_POSITION === 'open' &&
vueDevTools({ launchEditor: process.env.VITE_EDITOR }),
vueDevTools({ launchEditor: process.env.VITE_EDITOR }),
legacyPlugin({
targets: [
'Android > 39',