mirror of
https://gitcode.com/flipped-aurora/gin-vue-admin.git
synced 2025-12-30 03:42:26 +00:00
update: 2.8.7-beta发布
update: 2.8.7-beta发布
This commit is contained in:
761
.aone_copilot/rules/project_rules.md
Normal file
761
.aone_copilot/rules/project_rules.md
Normal 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框架本身支持MCP(Model 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. **重视安全性**:实现完善的权限控制和数据验证机制
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
199
server/api/v1/system/sys_error.go
Normal file
199
server/api/v1/system/sys_error.go
Normal 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)
|
||||
}
|
||||
@@ -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 创建导出模板
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package global
|
||||
// 目前只有Version正式使用 其余为预留
|
||||
const (
|
||||
// Version 当前版本号
|
||||
Version = "v2.8.6"
|
||||
Version = "v2.8.7"
|
||||
// AppName 应用名称
|
||||
AppName = "Gin-Vue-Admin"
|
||||
// Description 应用描述
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -62,6 +62,7 @@ func RegisterTables() {
|
||||
system.JoinTemplate{},
|
||||
system.SysParams{},
|
||||
system.SysVersion{},
|
||||
system.SysError{},
|
||||
|
||||
example.ExaFile{},
|
||||
example.ExaCustomer{},
|
||||
|
||||
@@ -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) // 文件上传下载分类
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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字符串
|
||||
}
|
||||
|
||||
14
server/model/system/request/sys_error.go
Normal file
14
server/model/system/request/sys_error.go
Normal 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
|
||||
}
|
||||
21
server/model/system/sys_error.go
Normal file
21
server/model/system/sys_error.go
Normal 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"
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
28
server/router/system/sys_error.go
Normal file
28
server/router/system/sys_error.go
Normal 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) // 新建错误日志
|
||||
}
|
||||
}
|
||||
@@ -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导出表格
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
51
server/service/system/auto_code_llm.go
Normal file
51
server/service/system/auto_code_llm.go
Normal 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
|
||||
}
|
||||
@@ -22,4 +22,5 @@ type ServiceGroup struct {
|
||||
AutoCodePackage autoCodePackage
|
||||
AutoCodeHistory autoCodeHistory
|
||||
AutoCodeTemplate autoCodeTemplate
|
||||
SysErrorService
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
124
server/service/system/sys_error.go
Normal file
124
server/service/system/sys_error.go
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: "批量删除公告"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"}},
|
||||
|
||||
62
server/utils/ast/extract_func.go
Normal file
62
server/utils/ast/extract_func.go
Normal 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
|
||||
}
|
||||
79
server/utils/stacktrace/stacktrace.go
Normal file
79
server/utils/stacktrace/stacktrace.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -162,7 +162,6 @@ export const butler = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const eye = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/llmAuto',
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
126
web/src/api/system/sysError.js
Normal file
126
web/src/api/system/sysError.js
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,7 +33,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
console.log(appStore.config)
|
||||
}
|
||||
|
||||
const setToken = (val) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
457
web/src/view/systemTools/sysError/sysError.vue
Normal file
457
web/src/view/systemTools/sysError/sysError.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user