🎨 精简完善系统
This commit is contained in:
215
AGENTS.md
Normal file
215
AGENTS.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 1. 文件作用
|
||||||
|
|
||||||
|
本文件定义本仓库的协作约束,供人类开发者和代码代理统一遵循。
|
||||||
|
|
||||||
|
目标只有三个:
|
||||||
|
|
||||||
|
- 先分清模块边界,再动代码。
|
||||||
|
- 新功能默认落在 `server` 和 `web-admin`。
|
||||||
|
- `web` 只作为旧版参考实现,不参与当前交付。
|
||||||
|
|
||||||
|
## 2. 项目总览
|
||||||
|
|
||||||
|
本仓库是一个前后端分离项目,当前有效开发面如下:
|
||||||
|
|
||||||
|
| 目录 | 角色 | 技术栈 | 是否允许修改 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `server` | 后端服务 | Go 1.24、Gin、Gorm、Viper、Zap | 允许 |
|
||||||
|
| `web-admin` | 新管理后台前端 | React 19、TypeScript、Vite、Ant Design、Zustand、React Query | 允许 |
|
||||||
|
| `web` | 原版 Vue 管理后台 | Vue | 不允许 |
|
||||||
|
|
||||||
|
当前默认管理后台目录已经在根目录 `Makefile` 中指定为 `WEB_DIR ?= web-admin`,后续构建与打包应以 `web-admin` 为准。
|
||||||
|
|
||||||
|
## 3. 工作边界
|
||||||
|
|
||||||
|
### 3.1 允许修改的目录
|
||||||
|
|
||||||
|
- `server/**`
|
||||||
|
- `web-admin/**`
|
||||||
|
- 根目录与部署目录中和当前任务直接相关的文档、脚本、配置文件
|
||||||
|
|
||||||
|
### 3.2 禁止修改的目录
|
||||||
|
|
||||||
|
- `web/**`
|
||||||
|
|
||||||
|
`web` 是旧版 Vue 后台,仅用于界面、接口命名、交互流程和功能覆盖范围参考。不得在该目录内新增、删除或修改任何文件。
|
||||||
|
|
||||||
|
### 3.3 修改原则
|
||||||
|
|
||||||
|
- 如果 `web-admin` 缺少某个页面或交互,可以参考 `web` 的实现思路,但要用 React 方式重写。
|
||||||
|
- 如果 `server` 的接口能力不足,应优先补齐后端,再让 `web-admin` 接入。
|
||||||
|
- 不为了兼容旧实现而把新代码继续写回 `web`。
|
||||||
|
|
||||||
|
## 4. 目录说明
|
||||||
|
|
||||||
|
### 4.1 `server`
|
||||||
|
|
||||||
|
后端采用典型分层结构,开发时按职责落位:
|
||||||
|
|
||||||
|
| 目录 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `server/api` | HTTP 接口入口。负责参数接收、调用 service、返回响应。 |
|
||||||
|
| `server/service` | 业务逻辑层。负责业务编排、事务边界、数据写入策略。 |
|
||||||
|
| `server/router` | 路由注册。负责把接口暴露给 Gin。 |
|
||||||
|
| `server/model` | 数据模型、请求结构、响应结构。 |
|
||||||
|
| `server/middleware` | 中间件。负责鉴权、日志、限流、错误处理等横切逻辑。 |
|
||||||
|
| `server/initialize` | 初始化流程。负责数据库、Redis、路由、校验器、定时器等装配。 |
|
||||||
|
| `server/config` | 配置结构定义。与 `config.yaml` / `config.docker.yaml` 对应。 |
|
||||||
|
| `server/utils` | 通用工具,不承载具体业务语义。 |
|
||||||
|
| `server/mcp` | MCP 相关能力。新增此类能力时优先放在这里。 |
|
||||||
|
| `server/docs` | Swagger 产物目录。只有接口变更需要更新文档时才修改。 |
|
||||||
|
|
||||||
|
后端变更约束:
|
||||||
|
|
||||||
|
- 不要把业务逻辑堆进 `api` 层。
|
||||||
|
- 不要把页面专属拼装逻辑散落到 `utils`。
|
||||||
|
- 新接口需要同时检查路由注册、权限、请求结构和返回结构是否完整。
|
||||||
|
- 涉及配置项变更时,必须同步检查 `server/config.yaml`、`server/config.docker.yaml` 和对应结构体。
|
||||||
|
|
||||||
|
### 4.2 `web-admin`
|
||||||
|
|
||||||
|
新后台是当前主开发前端,目录组织以功能模块为中心:
|
||||||
|
|
||||||
|
| 目录 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `web-admin/src/features` | 业务页面与模块实现。页面优先落在这里。 |
|
||||||
|
| `web-admin/src/router` | 路由定义与页面装配。 |
|
||||||
|
| `web-admin/src/lib` | 请求封装、日期、树结构、存储等通用前端基础设施。 |
|
||||||
|
| `web-admin/src/store` | 全局状态。当前主要用于鉴权与会话信息。 |
|
||||||
|
| `web-admin/src/types` | 共享类型定义。 |
|
||||||
|
| `web-admin/public` | 静态资源。 |
|
||||||
|
|
||||||
|
前端变更约束:
|
||||||
|
|
||||||
|
- 新页面优先沿用现有 `features/*Page.tsx` 的组织方式。
|
||||||
|
- 接口请求优先收敛到 `web-admin/src/lib` 或对应 feature 内部的请求层,不要在组件里散写 HTTP 细节。
|
||||||
|
- 类型优先显式声明,不要用大量 `any`。
|
||||||
|
- 页面以管理后台场景为前提,优先保证信息密度、操作链路和可维护性。
|
||||||
|
|
||||||
|
### 4.3 `web`
|
||||||
|
|
||||||
|
该目录只读。允许查看,不允许编辑。
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
|
||||||
|
- 查旧版菜单结构
|
||||||
|
- 查既有接口命名
|
||||||
|
- 查页面字段和交互流程
|
||||||
|
- 查某个能力在旧后台中的展示方式
|
||||||
|
|
||||||
|
不适用场景:
|
||||||
|
|
||||||
|
- 直接在这里修 bug
|
||||||
|
- 在这里补新功能
|
||||||
|
- 为了图省事复制构建配置并改回旧目录
|
||||||
|
|
||||||
|
## 5. 本地开发
|
||||||
|
|
||||||
|
### 5.1 环境基线
|
||||||
|
|
||||||
|
| 项目 | 要求 |
|
||||||
|
| --- | --- |
|
||||||
|
| Go | 1.24.x |
|
||||||
|
| Node.js | >= 18.16,建议 20+ |
|
||||||
|
| 包管理器 | `web-admin` 使用 `npm` |
|
||||||
|
|
||||||
|
### 5.2 常用命令
|
||||||
|
|
||||||
|
#### 后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/lee/GolandProjects/Go-Web-Template/server
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 新后台前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/lee/GolandProjects/Go-Web-Template/web-admin
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
默认约定:
|
||||||
|
|
||||||
|
- `web-admin` 开发地址:`http://localhost:8081`
|
||||||
|
- 后端默认代理目标:`http://127.0.0.1:8888`
|
||||||
|
|
||||||
|
#### Swagger 文档
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/lee/GolandProjects/Go-Web-Template/server
|
||||||
|
swag init
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 根目录构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/lee/GolandProjects/Go-Web-Template
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 变更落地规则
|
||||||
|
|
||||||
|
### 6.1 后端任务
|
||||||
|
|
||||||
|
遇到后端需求时,优先按下面顺序检查:
|
||||||
|
|
||||||
|
1. 是否已有对应 `model`、`service`、`api`、`router`。
|
||||||
|
2. 是否需要补权限、菜单、字典、参数或日志记录。
|
||||||
|
3. 是否影响 Swagger 文档、配置文件或初始化逻辑。
|
||||||
|
4. 是否需要补测试,至少覆盖纯工具函数和稳定业务逻辑。
|
||||||
|
|
||||||
|
### 6.2 前端任务
|
||||||
|
|
||||||
|
遇到管理后台需求时,优先按下面顺序处理:
|
||||||
|
|
||||||
|
1. 先确认目标页面属于 `web-admin` 哪个 feature。
|
||||||
|
2. 需要参考旧实现时,只读取 `web`,不在 `web` 动手。
|
||||||
|
3. 先整理接口契约,再组织页面状态和交互。
|
||||||
|
4. 提交前至少完成构建或 lint 校验。
|
||||||
|
|
||||||
|
### 6.3 跨端联动任务
|
||||||
|
|
||||||
|
如果需求同时涉及前后端,推荐顺序如下:
|
||||||
|
|
||||||
|
1. 先定义或确认接口契约。
|
||||||
|
2. 先补 `server`,保证接口可运行。
|
||||||
|
3. 再接 `web-admin` 页面和交互。
|
||||||
|
4. 最后联调并确认代理、鉴权、菜单和权限表现一致。
|
||||||
|
|
||||||
|
## 7. 验证要求
|
||||||
|
|
||||||
|
只要改动了对应模块,至少执行一项最低验证:
|
||||||
|
|
||||||
|
| 改动范围 | 最低验证 |
|
||||||
|
| --- | --- |
|
||||||
|
| `server` Go 代码 | `go test ./...` 或最小相关包测试 |
|
||||||
|
| `web-admin` 前端代码 | `npm run build` 或 `npm run lint` |
|
||||||
|
| 接口定义变更 | 补充或更新 Swagger 文档,并做一次接口联调 |
|
||||||
|
| 构建脚本或部署配置 | 执行对应构建命令,确认没有明显路径错误 |
|
||||||
|
|
||||||
|
如果因为外部依赖无法完成验证,需要明确说明阻塞点,不得省略。
|
||||||
|
|
||||||
|
## 8. 代码风格补充
|
||||||
|
|
||||||
|
- 文档、注释、提交说明优先使用清晰中文。
|
||||||
|
- 注释只说明职责、边界、约束和必要语义,不复述代码表面行为。
|
||||||
|
- 新增后端代码遵循现有分层,不主动引入跨层调用。
|
||||||
|
- 新增前端代码遵循现有 React + TypeScript + Ant Design 方案,不回退到旧 Vue 写法。
|
||||||
|
- 非必要不新增大型依赖。新增依赖前先确认当前栈无法直接解决问题。
|
||||||
|
|
||||||
|
## 9. 决策优先级
|
||||||
|
|
||||||
|
出现冲突时,按下面顺序决策:
|
||||||
|
|
||||||
|
1. 不修改 `web`
|
||||||
|
2. 以 `web-admin` 作为当前管理后台主实现
|
||||||
|
3. 以 `server` 的真实接口和数据结构为准
|
||||||
|
4. 参考旧版 `web` 的业务行为,而不是复制其实现细节
|
||||||
|
|
||||||
|
## 10. 一句话约束
|
||||||
|
|
||||||
|
这个仓库的当前主线是:维护 `server`,建设 `web-admin`,只参考 `web`,绝不修改 `web`。
|
||||||
65
README.md
65
README.md
@@ -6,10 +6,10 @@
|
|||||||
<div align=center>
|
<div align=center>
|
||||||
<img src="https://img.shields.io/badge/golang-1.20-blue"/>
|
<img src="https://img.shields.io/badge/golang-1.20-blue"/>
|
||||||
<img src="https://img.shields.io/badge/gin-1.9.1-lightBlue"/>
|
<img src="https://img.shields.io/badge/gin-1.9.1-lightBlue"/>
|
||||||
<img src="https://img.shields.io/badge/vue-3.3.4-brightgreen"/>
|
<img src="https://img.shields.io/badge/react-19.2.4-brightgreen"/>
|
||||||
<img src="https://img.shields.io/badge/element--plus-2.3.8-green"/>
|
<img src="https://img.shields.io/badge/antd-6.3.5-green"/>
|
||||||
<img src="https://img.shields.io/badge/gorm-1.25.2-red"/>
|
<img src="https://img.shields.io/badge/gorm-1.25.2-red"/>
|
||||||
<img src="https://gitcode.com/flipped-aurora/gin-vue-admin/star/badge.svg"/>
|
<img src="https://gitcode.com/flipped-aurora/gin-react-admin/star/badge.svg"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align=center>
|
<div align=center>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
## 支持claw生态
|
## 支持claw生态
|
||||||
|
|
||||||
[🦞GvaClaw](https://plugin.gin-vue-admin.com/details/159)
|
[🦞GvaClaw](https://plugin.gin-react-admin.com/details/159)
|
||||||
|
|
||||||
## ✨一分钟生成前后端基础代码
|
## ✨一分钟生成前后端基础代码
|
||||||
|
|
||||||
@@ -43,29 +43,29 @@
|
|||||||
|
|
||||||
|
|
||||||
# 项目文档
|
# 项目文档
|
||||||
[在线文档](https://www.gin-vue-admin.com) : https://www.gin-vue-admin.com
|
[在线文档](https://www.gin-react-admin.com) : https://www.gin-react-admin.com
|
||||||
|
|
||||||
[初始化](https://www.gin-vue-admin.com/guide/start-quickly/initialization.html)
|
[初始化](https://www.gin-react-admin.com/guide/start-quickly/initialization.html)
|
||||||
|
|
||||||
[从环境到部署教学视频](https://www.bilibili.com/video/BV1Rg411u7xH)
|
[从环境到部署教学视频](https://www.bilibili.com/video/BV1Rg411u7xH)
|
||||||
|
|
||||||
[开发教学](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (贡献者: <a href="https://github.com/LLemonGreen">LLemonGreen</a> And <a href="https://github.com/fkk0509">Fann</a>)
|
[开发教学](https://www.gin-react-admin.com/guide/start-quickly/env.html) (贡献者: <a href="https://github.com/LLemonGreen">LLemonGreen</a> And <a href="https://github.com/fkk0509">Fann</a>)
|
||||||
|
|
||||||
[交流社区](https://support.qq.com/products/371961)
|
[交流社区](https://support.qq.com/products/371961)
|
||||||
|
|
||||||
[插件市场](https://plugin.gin-vue-admin.com/)
|
[插件市场](https://plugin.gin-react-admin.com/)
|
||||||
|
|
||||||
[软件著作权证书](https://www.gin-vue-admin.com/copyright.pdf)
|
[软件著作权证书](https://www.gin-react-admin.com/copyright.pdf)
|
||||||
|
|
||||||
# 重要提示
|
# 重要提示
|
||||||
|
|
||||||
1.本项目从起步到开发到部署均有文档和详细视频教程
|
1.本项目从起步到开发到部署均有文档和详细视频教程
|
||||||
|
|
||||||
2.本项目需要您有一定的golang和vue基础
|
2.本项目需要您有一定的golang和react基础
|
||||||
|
|
||||||
3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html)
|
3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-react-admin.com/coffee/payment.html)
|
||||||
|
|
||||||
4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html)
|
4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-react-admin.com/licenseindex.html)
|
||||||
\
|
\
|
||||||
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="1000">
|
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="1000">
|
||||||
|
|
||||||
@@ -75,20 +75,20 @@
|
|||||||
|
|
||||||
### 1.1 项目介绍
|
### 1.1 项目介绍
|
||||||
|
|
||||||
> Gin-vue-admin是一个基于 [vue](https://vuejs.org) 和 [gin](https://gin-gonic.com) 开发的全栈前后端分离的开发基础平台,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。
|
> Gin-React-Admin 是一个基于 [react](https://react.dev) 和 [gin](https://gin-gonic.com) 开发的全栈前后端分离开发基础平台,集成 JWT 鉴权、动态路由、动态菜单、Casbin 鉴权与多种后台治理能力,让您把更多时间专注在业务开发上。
|
||||||
|
|
||||||
[在线预览](http://demo.gin-vue-admin.com): http://demo.gin-vue-admin.com
|
[在线预览](http://demo.gin-react-admin.com): http://demo.gin-react-admin.com
|
||||||
|
|
||||||
测试用户名:admin
|
测试用户名:admin
|
||||||
|
|
||||||
测试密码:123456
|
测试密码:123456
|
||||||
|
|
||||||
### 1.2 贡献指南
|
### 1.2 贡献指南
|
||||||
Hi! 首先感谢你使用 gin-vue-admin。
|
Hi! 首先感谢你使用 Gin-React-Admin。
|
||||||
|
|
||||||
Gin-vue-admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。
|
Gin-React-Admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。
|
||||||
|
|
||||||
Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-admin 贡献代码或提供建议,请阅读以下内容。
|
Gin-React-Admin 的成长离不开大家的支持,如果你愿意为 Gin-React-Admin 贡献代码或提供建议,请阅读以下内容。
|
||||||
|
|
||||||
#### 1.2.1 Issue 规范
|
#### 1.2.1 Issue 规范
|
||||||
- issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。
|
- issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。
|
||||||
@@ -114,12 +114,12 @@ Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-adm
|
|||||||
|
|
||||||
### 2.1 server项目
|
### 2.1 server项目
|
||||||
|
|
||||||
使用 `Goland` 等编辑工具,打开server目录,不可以打开 gin-vue-admin 根目录
|
使用 `Goland` 等编辑工具,打开server目录,不可以打开 gin-react-admin 根目录
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://github.com/flipped-aurora/gin-vue-admin.git
|
git clone https://github.com/flipped-aurora/gin-react-admin.git
|
||||||
# 进入server文件夹
|
# 进入server文件夹
|
||||||
cd server
|
cd server
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ swag init
|
|||||||
|
|
||||||
#### 2.4.1 开发
|
#### 2.4.1 开发
|
||||||
|
|
||||||
使用`VSCode`打开根目录下的工作区文件`gin-vue-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。
|
使用`VSCode`打开根目录下的工作区文件`gin-react-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。
|
||||||
|
|
||||||
#### 2.4.2 运行/调试
|
#### 2.4.2 运行/调试
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ swag init
|
|||||||
|
|
||||||
### 4.1 系统架构图
|
### 4.1 系统架构图
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 4.2 前端详细设计图 (提供者:<a href="https://github.com/baobeisuper">baobeisuper</a>)
|
### 4.2 前端详细设计图 (提供者:<a href="https://github.com/baobeisuper">baobeisuper</a>)
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ swag init
|
|||||||
├── packfile (静态文件打包)
|
├── packfile (静态文件打包)
|
||||||
├── resource (静态资源文件夹)
|
├── resource (静态资源文件夹)
|
||||||
│ ├── excel (excel导入导出默认路径)
|
│ ├── excel (excel导入导出默认路径)
|
||||||
│ ├── page (表单生成器)
|
│ ├── page (页面静态资源目录)
|
||||||
│ └── template (模板)
|
│ └── template (模板)
|
||||||
├── router (路由层)
|
├── router (路由层)
|
||||||
├── service (service层)
|
├── service (service层)
|
||||||
@@ -242,7 +242,7 @@ swag init
|
|||||||
│ ├── components -- 全局组件
|
│ ├── components -- 全局组件
|
||||||
│ ├── core -- gva 组件包
|
│ ├── core -- gva 组件包
|
||||||
│ │ ├── config.js -- gva网站配置文件
|
│ │ ├── config.js -- gva网站配置文件
|
||||||
│ │ ├── gin-vue-admin.js -- 注册欢迎文件
|
│ │ ├── gin-react-admin.js -- 注册欢迎文件
|
||||||
│ │ └── global.js -- 统一导入文件
|
│ │ └── global.js -- 统一导入文件
|
||||||
│ ├── directive -- v-auth 注册文件
|
│ ├── directive -- v-auth 注册文件
|
||||||
│ ├── main.js -- 主文件
|
│ ├── main.js -- 主文件
|
||||||
@@ -313,11 +313,10 @@ swag init
|
|||||||
- 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。
|
- 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。
|
||||||
- 条件搜索:增加条件搜索示例。
|
- 条件搜索:增加条件搜索示例。
|
||||||
- restful示例:可以参考用户管理模块中的示例API。
|
- restful示例:可以参考用户管理模块中的示例API。
|
||||||
- 前端文件参考: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-vue-admin/blob/master/web/src/view/superAdmin/api/api.vue)
|
- 前端文件参考: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-react-admin/blob/master/web/src/view/superAdmin/api/api.vue)
|
||||||
- 后台文件参考: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/router/sys_api.go)
|
- 后台文件参考: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-react-admin/blob/master/server/router/sys_api.go)
|
||||||
- 多点登录限制:需要在`config.yaml`中把`system`中的`use-multipoint`修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。
|
- 多点登录限制:需要在`config.yaml`中把`system`中的`use-multipoint`修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。
|
||||||
- 分片上传:提供文件分片上传和大文件分片上传功能示例。
|
- 分片上传:提供文件分片上传和大文件分片上传功能示例。
|
||||||
- 表单生成器:表单生成器借助 [@Variant Form](https://github.com/vform666/variant-form) 。
|
|
||||||
- 代码生成器:后台基础逻辑以及简单curd的代码生成器。
|
- 代码生成器:后台基础逻辑以及简单curd的代码生成器。
|
||||||
|
|
||||||
## 6. 知识库
|
## 6. 知识库
|
||||||
@@ -346,7 +345,7 @@ swag init
|
|||||||
|
|
||||||
> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0
|
> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0
|
||||||
|
|
||||||
(5)gin-vue-admin 版本更新介绍视频
|
(5)gin-react-admin 版本更新介绍视频
|
||||||
|
|
||||||
> bilibili:https://www.bilibili.com/video/BV1kv4y1g7nT
|
> bilibili:https://www.bilibili.com/video/BV1kv4y1g7nT
|
||||||
|
|
||||||
@@ -369,21 +368,21 @@ decodeBytes, err := base64.StdEncoding.DecodeString(str)
|
|||||||
fmt.Println(decodeBytes, err)
|
fmt.Println(decodeBytes, err)
|
||||||
```
|
```
|
||||||
|
|
||||||
### [关于我们](https://www.gin-vue-admin.com/about/join.html)
|
### [关于我们](https://www.gin-react-admin.com/about/join.html)
|
||||||
|
|
||||||
## 8. 贡献者
|
## 8. 贡献者
|
||||||
|
|
||||||
感谢您对gin-vue-admin的贡献!
|
感谢您对gin-react-admin的贡献!
|
||||||
|
|
||||||
<a href="https://openomy.app/github/flipped-aurora/gin-vue-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
<a href="https://openomy.app/github/flipped-aurora/gin-react-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||||
<img src="https://openomy.app/svg?repo=flipped-aurora/gin-vue-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
<img src="https://openomy.app/svg?repo=flipped-aurora/gin-react-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 9. 捐赠
|
## 9. 捐赠
|
||||||
|
|
||||||
如果你觉得这个项目对你有帮助,你可以请作者喝饮料 :tropical_drink: [点我](https://www.gin-vue-admin.com/coffee/index.html)
|
如果你觉得这个项目对你有帮助,你可以请作者喝饮料 :tropical_drink: [点我](https://www.gin-react-admin.com/coffee/index.html)
|
||||||
|
|
||||||
## 10. 注意事项
|
## 10. 注意事项
|
||||||
|
|
||||||
请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license)
|
请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-react-admin.com/license)
|
||||||
未授权去除版权信息将依法追究法律责任
|
未授权去除版权信息将依法追究法律责任
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ kind: ConfigMap
|
|||||||
metadata:
|
metadata:
|
||||||
name: config.yaml
|
name: config.yaml
|
||||||
annotations:
|
annotations:
|
||||||
flipped-aurora/gin-vue-admin: backend
|
flipped-aurora/gin-react-admin: backend
|
||||||
github: "https://github.com/flipped-aurora/gin-vue-admin.git"
|
github: "https://github.com/flipped-aurora/gin-react-admin.git"
|
||||||
app.kubernetes.io/version: 0.0.1
|
app.kubernetes.io/version: 0.0.1
|
||||||
labels:
|
labels:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
data:
|
data:
|
||||||
config.yaml: |
|
config.yaml: |
|
||||||
# git.echol.cn/loser/Go-Web-Template/server Global Configuration
|
# git.echol.cn/loser/Go-Web-Template/server Global Configuration
|
||||||
@@ -125,7 +125,7 @@ data:
|
|||||||
region: 'ap-shanghai'
|
region: 'ap-shanghai'
|
||||||
secret-id: 'xxxxxxxx'
|
secret-id: 'xxxxxxxx'
|
||||||
secret-key: 'xxxxxxxx'
|
secret-key: 'xxxxxxxx'
|
||||||
base-url: 'https://gin.vue.admin'
|
base-url: 'https://gin-react-admin.com'
|
||||||
path-prefix: 'git.echol.cn/loser/Go-Web-Template/server'
|
path-prefix: 'git.echol.cn/loser/Go-Web-Template/server'
|
||||||
|
|
||||||
# excel configuration
|
# excel configuration
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ kind: Deployment
|
|||||||
metadata:
|
metadata:
|
||||||
name: gva-server
|
name: gva-server
|
||||||
annotations:
|
annotations:
|
||||||
flipped-aurora/gin-vue-admin: backend
|
flipped-aurora/gin-react-admin: backend
|
||||||
github: "https://github.com/flipped-aurora/gin-vue-admin.git"
|
github: "https://github.com/flipped-aurora/gin-react-admin.git"
|
||||||
app.kubernetes.io/version: 0.0.1
|
app.kubernetes.io/version: 0.0.1
|
||||||
labels:
|
labels:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: gin-vue-admin-container
|
- name: gin-react-admin-container
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest
|
image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ kind: Service
|
|||||||
metadata:
|
metadata:
|
||||||
name: gva-server
|
name: gva-server
|
||||||
annotations:
|
annotations:
|
||||||
flipped-aurora/gin-vue-admin: backend
|
flipped-aurora/gin-react-admin: backend
|
||||||
github: "https://github.com/flipped-aurora/gin-vue-admin.git"
|
github: "https://github.com/flipped-aurora/gin-react-admin.git"
|
||||||
app.kubernetes.io/version: 0.0.1
|
app.kubernetes.io/version: 0.0.1
|
||||||
labels:
|
labels:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
app: gva-server
|
app: gva-server
|
||||||
version: gva-vue3
|
version: gva-react
|
||||||
ports:
|
ports:
|
||||||
- port: 8888
|
- port: 8888
|
||||||
name: http
|
name: http
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
| `packfile` | 静态文件打包 | 静态文件打包 |
|
| `packfile` | 静态文件打包 | 静态文件打包 |
|
||||||
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
|
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
|
||||||
| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
|
| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
|
||||||
| `--page` | 表单生成器 | 表单生成器 打包后的dist |
|
| `--page` | 页面静态资源目录 | 历史页面资源输出目录 |
|
||||||
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
|
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
|
||||||
| `router` | 路由层 | 路由层 |
|
| `router` | 路由层 | 路由层 |
|
||||||
| `service` | service层 | 存放业务逻辑问题 |
|
| `service` | service层 | 存放业务逻辑问题 |
|
||||||
@@ -51,4 +51,3 @@
|
|||||||
| `utils` | 工具包 | 工具函数封装 |
|
| `utils` | 工具包 | 工具函数封装 |
|
||||||
| `--timer` | timer | 定时器接口封装 |
|
| `--timer` | timer | 定时器接口封装 |
|
||||||
| `--upload` | oss | oss接口封装 |
|
| `--upload` | oss | oss接口封装 |
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ type ApiGroup struct {
|
|||||||
OperationRecordApi
|
OperationRecordApi
|
||||||
DictionaryDetailApi
|
DictionaryDetailApi
|
||||||
AuthorityBtnApi
|
AuthorityBtnApi
|
||||||
SysExportTemplateApi
|
|
||||||
McpApi
|
McpApi
|
||||||
SysParamsApi
|
SysParamsApi
|
||||||
SysVersionApi
|
|
||||||
SysErrorApi
|
SysErrorApi
|
||||||
LoginLogApi
|
LoginLogApi
|
||||||
ApiTokenApi
|
ApiTokenApi
|
||||||
@@ -39,7 +37,6 @@ var (
|
|||||||
sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService
|
sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService
|
||||||
operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
|
operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
|
||||||
dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
|
dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
|
||||||
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
|
|
||||||
mcpService = service.ServiceGroupApp.SystemServiceGroup.McpService
|
mcpService = service.ServiceGroupApp.SystemServiceGroup.McpService
|
||||||
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
|
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
|
||||||
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
|
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
|
||||||
|
|||||||
@@ -1,456 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/service"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 用于token一次性存储
|
|
||||||
var (
|
|
||||||
exportTokenCache = make(map[string]interface{})
|
|
||||||
exportTokenExpiration = make(map[string]time.Time)
|
|
||||||
tokenMutex sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// 五分钟检测窗口过期
|
|
||||||
func cleanupExpiredTokens() {
|
|
||||||
for {
|
|
||||||
time.Sleep(5 * time.Minute)
|
|
||||||
tokenMutex.Lock()
|
|
||||||
now := time.Now()
|
|
||||||
for token, expiry := range exportTokenExpiration {
|
|
||||||
if now.After(expiry) {
|
|
||||||
delete(exportTokenCache, token)
|
|
||||||
delete(exportTokenExpiration, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokenMutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
go cleanupExpiredTokens()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 创建导出模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body system.SysExportTemplate true "创建导出模板"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
|
|
||||||
// @Router /sysExportTemplate/createSysExportTemplate [post]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) CreateSysExportTemplate(c *gin.Context) {
|
|
||||||
var sysExportTemplate system.SysExportTemplate
|
|
||||||
err := c.ShouldBindJSON(&sysExportTemplate)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
verify := utils.Rules{
|
|
||||||
"Name": {utils.NotEmpty()},
|
|
||||||
}
|
|
||||||
if err := utils.Verify(sysExportTemplate, verify); err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sysExportTemplateService.CreateSysExportTemplate(&sysExportTemplate); err != nil {
|
|
||||||
global.GVA_LOG.Error("创建失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("创建失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithMessage("创建成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysExportTemplate 删除导出模板
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 删除导出模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body system.SysExportTemplate true "删除导出模板"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
|
||||||
// @Router /sysExportTemplate/deleteSysExportTemplate [delete]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplate(c *gin.Context) {
|
|
||||||
var sysExportTemplate system.SysExportTemplate
|
|
||||||
err := c.ShouldBindJSON(&sysExportTemplate)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sysExportTemplateService.DeleteSysExportTemplate(sysExportTemplate); err != nil {
|
|
||||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("删除失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithMessage("删除成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysExportTemplateByIds 批量删除导出模板
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 批量删除导出模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body request.IdsReq true "批量删除导出模板"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"批量删除成功"}"
|
|
||||||
// @Router /sysExportTemplate/deleteSysExportTemplateByIds [delete]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplateByIds(c *gin.Context) {
|
|
||||||
var IDS request.IdsReq
|
|
||||||
err := c.ShouldBindJSON(&IDS)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sysExportTemplateService.DeleteSysExportTemplateByIds(IDS); err != nil {
|
|
||||||
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("批量删除失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithMessage("批量删除成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSysExportTemplate 更新导出模板
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 更新导出模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body system.SysExportTemplate true "更新导出模板"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
|
|
||||||
// @Router /sysExportTemplate/updateSysExportTemplate [put]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) UpdateSysExportTemplate(c *gin.Context) {
|
|
||||||
var sysExportTemplate system.SysExportTemplate
|
|
||||||
err := c.ShouldBindJSON(&sysExportTemplate)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
verify := utils.Rules{
|
|
||||||
"Name": {utils.NotEmpty()},
|
|
||||||
}
|
|
||||||
if err := utils.Verify(sysExportTemplate, verify); err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sysExportTemplateService.UpdateSysExportTemplate(sysExportTemplate); err != nil {
|
|
||||||
global.GVA_LOG.Error("更新失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("更新失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithMessage("更新成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindSysExportTemplate 用id查询导出模板
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 用id查询导出模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data query system.SysExportTemplate true "用id查询导出模板"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
|
|
||||||
// @Router /sysExportTemplate/findSysExportTemplate [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) FindSysExportTemplate(c *gin.Context) {
|
|
||||||
var sysExportTemplate system.SysExportTemplate
|
|
||||||
err := c.ShouldBindQuery(&sysExportTemplate)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resysExportTemplate, err := sysExportTemplateService.GetSysExportTemplate(sysExportTemplate.ID); err != nil {
|
|
||||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("查询失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithData(gin.H{"resysExportTemplate": resysExportTemplate}, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysExportTemplateList 分页获取导出模板列表
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 分页获取导出模板列表
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data query systemReq.SysExportTemplateSearch true "分页获取导出模板列表"
|
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
|
|
||||||
// @Router /sysExportTemplate/getSysExportTemplateList [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) GetSysExportTemplateList(c *gin.Context) {
|
|
||||||
var pageInfo systemReq.SysExportTemplateSearch
|
|
||||||
err := c.ShouldBindQuery(&pageInfo)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if list, total, err := sysExportTemplateService.GetSysExportTemplateInfoList(pageInfo); err != nil {
|
|
||||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取失败", c)
|
|
||||||
} else {
|
|
||||||
response.OkWithDetailed(response.PageResult{
|
|
||||||
List: list,
|
|
||||||
Total: total,
|
|
||||||
Page: pageInfo.Page,
|
|
||||||
PageSize: pageInfo.PageSize,
|
|
||||||
}, "获取成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportExcel 导出表格token
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 导出表格
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Router /sysExportTemplate/exportExcel [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) ExportExcel(c *gin.Context) {
|
|
||||||
templateID := c.Query("templateID")
|
|
||||||
if templateID == "" {
|
|
||||||
response.FailWithMessage("模板ID不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams := c.Request.URL.Query()
|
|
||||||
|
|
||||||
//创造一次性token
|
|
||||||
token := utils.RandomString(32) // 随机32位
|
|
||||||
|
|
||||||
// 记录本次请求参数
|
|
||||||
exportParams := map[string]interface{}{
|
|
||||||
"templateID": templateID,
|
|
||||||
"queryParams": queryParams,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参数保留记录完成鉴权
|
|
||||||
tokenMutex.Lock()
|
|
||||||
exportTokenCache[token] = exportParams
|
|
||||||
exportTokenExpiration[token] = time.Now().Add(30 * time.Minute)
|
|
||||||
tokenMutex.Unlock()
|
|
||||||
|
|
||||||
// 生成一次性链接
|
|
||||||
exportUrl := fmt.Sprintf("/sysExportTemplate/exportExcelByToken?token=%s", token)
|
|
||||||
response.OkWithData(exportUrl, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportExcelByToken 导出表格
|
|
||||||
// @Tags ExportExcelByToken
|
|
||||||
// @Summary 导出表格
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Router /sysExportTemplate/exportExcelByToken [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) ExportExcelByToken(c *gin.Context) {
|
|
||||||
token := c.Query("token")
|
|
||||||
if token == "" {
|
|
||||||
response.FailWithMessage("导出token不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取token并且从缓存中剔除
|
|
||||||
tokenMutex.RLock()
|
|
||||||
exportParamsRaw, exists := exportTokenCache[token]
|
|
||||||
expiry, _ := exportTokenExpiration[token]
|
|
||||||
tokenMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists || time.Now().After(expiry) {
|
|
||||||
global.GVA_LOG.Error("导出token无效或已过期!")
|
|
||||||
response.FailWithMessage("导出token无效或已过期", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从token获取参数
|
|
||||||
exportParams, ok := exportParamsRaw.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
global.GVA_LOG.Error("解析导出参数失败!")
|
|
||||||
response.FailWithMessage("解析导出参数失败", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取导出参数
|
|
||||||
templateID := exportParams["templateID"].(string)
|
|
||||||
queryParams := exportParams["queryParams"].(url.Values)
|
|
||||||
|
|
||||||
// 清理一次性token
|
|
||||||
tokenMutex.Lock()
|
|
||||||
delete(exportTokenCache, token)
|
|
||||||
delete(exportTokenExpiration, token)
|
|
||||||
tokenMutex.Unlock()
|
|
||||||
|
|
||||||
// 导出
|
|
||||||
if file, name, err := sysExportTemplateService.ExportExcel(templateID, queryParams); err != nil {
|
|
||||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取失败", c)
|
|
||||||
} else {
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+utils.RandomString(6)+".xlsx"))
|
|
||||||
c.Header("success", "true")
|
|
||||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportTemplate 导出表格模板
|
|
||||||
// @Tags SysExportTemplate
|
|
||||||
// @Summary 导出表格模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Router /sysExportTemplate/exportTemplate [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplate(c *gin.Context) {
|
|
||||||
templateID := c.Query("templateID")
|
|
||||||
if templateID == "" {
|
|
||||||
response.FailWithMessage("模板ID不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创造一次性token
|
|
||||||
token := utils.RandomString(32) // 随机32位
|
|
||||||
|
|
||||||
// 记录本次请求参数
|
|
||||||
exportParams := map[string]interface{}{
|
|
||||||
"templateID": templateID,
|
|
||||||
"isTemplate": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参数保留记录完成鉴权
|
|
||||||
tokenMutex.Lock()
|
|
||||||
exportTokenCache[token] = exportParams
|
|
||||||
exportTokenExpiration[token] = time.Now().Add(30 * time.Minute)
|
|
||||||
tokenMutex.Unlock()
|
|
||||||
|
|
||||||
// 生成一次性链接
|
|
||||||
exportUrl := fmt.Sprintf("/sysExportTemplate/exportTemplateByToken?token=%s", token)
|
|
||||||
response.OkWithData(exportUrl, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportTemplateByToken 通过token导出表格模板
|
|
||||||
// @Tags ExportTemplateByToken
|
|
||||||
// @Summary 通过token导出表格模板
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Router /sysExportTemplate/exportTemplateByToken [get]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplateByToken(c *gin.Context) {
|
|
||||||
token := c.Query("token")
|
|
||||||
if token == "" {
|
|
||||||
response.FailWithMessage("导出token不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取token并且从缓存中剔除
|
|
||||||
tokenMutex.RLock()
|
|
||||||
exportParamsRaw, exists := exportTokenCache[token]
|
|
||||||
expiry, _ := exportTokenExpiration[token]
|
|
||||||
tokenMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists || time.Now().After(expiry) {
|
|
||||||
global.GVA_LOG.Error("导出token无效或已过期!")
|
|
||||||
response.FailWithMessage("导出token无效或已过期", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从token获取参数
|
|
||||||
exportParams, ok := exportParamsRaw.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
global.GVA_LOG.Error("解析导出参数失败!")
|
|
||||||
response.FailWithMessage("解析导出参数失败", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为模板导出
|
|
||||||
isTemplate, _ := exportParams["isTemplate"].(bool)
|
|
||||||
if !isTemplate {
|
|
||||||
global.GVA_LOG.Error("token类型错误!")
|
|
||||||
response.FailWithMessage("token类型错误", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取导出参数
|
|
||||||
templateID := exportParams["templateID"].(string)
|
|
||||||
|
|
||||||
// 清理一次性token
|
|
||||||
tokenMutex.Lock()
|
|
||||||
delete(exportTokenCache, token)
|
|
||||||
delete(exportTokenExpiration, token)
|
|
||||||
tokenMutex.Unlock()
|
|
||||||
|
|
||||||
// 导出模板
|
|
||||||
if file, name, err := sysExportTemplateService.ExportTemplate(templateID); err != nil {
|
|
||||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取失败", c)
|
|
||||||
} else {
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+"模板.xlsx"))
|
|
||||||
c.Header("success", "true")
|
|
||||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportExcel 导入表格
|
|
||||||
// @Tags SysImportTemplate
|
|
||||||
// @Summary 导入表格
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Router /sysExportTemplate/importExcel [post]
|
|
||||||
func (sysExportTemplateApi *SysExportTemplateApi) ImportExcel(c *gin.Context) {
|
|
||||||
templateID := c.Query("templateID")
|
|
||||||
if templateID == "" {
|
|
||||||
response.FailWithMessage("模板ID不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, err := c.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("文件获取失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("文件获取失败", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sysExportTemplateService.ImportExcel(templateID, file); err != nil {
|
|
||||||
global.GVA_LOG.Error(err.Error(), zap.Error(err))
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
} else {
|
|
||||||
response.OkWithMessage("导入成功", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
|
||||||
systemRes "git.echol.cn/loser/Go-Web-Template/server/model/system/response"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysVersionApi struct{}
|
|
||||||
|
|
||||||
// buildMenuTree 构建菜单树结构
|
|
||||||
func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu {
|
|
||||||
// 创建菜单映射
|
|
||||||
menuMap := make(map[uint]*system.SysBaseMenu)
|
|
||||||
for i := range menus {
|
|
||||||
menuMap[menus[i].ID] = &menus[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建树结构
|
|
||||||
var rootMenus []system.SysBaseMenu
|
|
||||||
for _, menu := range menus {
|
|
||||||
if menu.ParentId == 0 {
|
|
||||||
// 根菜单
|
|
||||||
menuData := convertMenuToStruct(menu, menuMap)
|
|
||||||
rootMenus = append(rootMenus, menuData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按sort排序根菜单
|
|
||||||
sort.Slice(rootMenus, func(i, j int) bool {
|
|
||||||
return rootMenus[i].Sort < rootMenus[j].Sort
|
|
||||||
})
|
|
||||||
|
|
||||||
return rootMenus
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单
|
|
||||||
func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu {
|
|
||||||
result := system.SysBaseMenu{
|
|
||||||
Path: menu.Path,
|
|
||||||
Name: menu.Name,
|
|
||||||
Hidden: menu.Hidden,
|
|
||||||
Component: menu.Component,
|
|
||||||
Sort: menu.Sort,
|
|
||||||
Meta: menu.Meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理并复制参数数据
|
|
||||||
if len(menu.Parameters) > 0 {
|
|
||||||
cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters))
|
|
||||||
for _, param := range menu.Parameters {
|
|
||||||
cleanParam := system.SysBaseMenuParameter{
|
|
||||||
Type: param.Type,
|
|
||||||
Key: param.Key,
|
|
||||||
Value: param.Value,
|
|
||||||
// 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
|
|
||||||
}
|
|
||||||
cleanParameters = append(cleanParameters, cleanParam)
|
|
||||||
}
|
|
||||||
result.Parameters = cleanParameters
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理并复制菜单按钮数据
|
|
||||||
if len(menu.MenuBtn) > 0 {
|
|
||||||
cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn))
|
|
||||||
for _, btn := range menu.MenuBtn {
|
|
||||||
cleanBtn := system.SysBaseMenuBtn{
|
|
||||||
Name: btn.Name,
|
|
||||||
Desc: btn.Desc,
|
|
||||||
// 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID
|
|
||||||
}
|
|
||||||
cleanMenuBtns = append(cleanMenuBtns, cleanBtn)
|
|
||||||
}
|
|
||||||
result.MenuBtn = cleanMenuBtns
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找并处理子菜单
|
|
||||||
var children []system.SysBaseMenu
|
|
||||||
for _, childMenu := range menuMap {
|
|
||||||
if childMenu.ParentId == menu.ID {
|
|
||||||
childData := convertMenuToStruct(*childMenu, menuMap)
|
|
||||||
children = append(children, childData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按sort排序子菜单
|
|
||||||
if len(children) > 0 {
|
|
||||||
sort.Slice(children, func(i, j int) bool {
|
|
||||||
return children[i].Sort < children[j].Sort
|
|
||||||
})
|
|
||||||
result.Children = children
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysVersion 删除版本管理
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 删除版本管理
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body system.SysVersion true "删除版本管理"
|
|
||||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
|
||||||
// @Router /sysVersion/deleteSysVersion [delete]
|
|
||||||
func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) {
|
|
||||||
// 创建业务用Context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
ID := c.Query("ID")
|
|
||||||
err := sysVersionService.DeleteSysVersion(ctx, ID)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("删除失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.OkWithMessage("删除成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysVersionByIds 批量删除版本管理
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 批量删除版本管理
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
|
|
||||||
// @Router /sysVersion/deleteSysVersionByIds [delete]
|
|
||||||
func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) {
|
|
||||||
// 创建业务用Context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
IDs := c.QueryArray("IDs[]")
|
|
||||||
err := sysVersionService.DeleteSysVersionByIds(ctx, IDs)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("批量删除失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.OkWithMessage("批量删除成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindSysVersion 用id查询版本管理
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 用id查询版本管理
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param ID query uint true "用id查询版本管理"
|
|
||||||
// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功"
|
|
||||||
// @Router /sysVersion/findSysVersion [get]
|
|
||||||
func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) {
|
|
||||||
// 创建业务用Context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
ID := c.Query("ID")
|
|
||||||
resysVersion, err := sysVersionService.GetSysVersion(ctx, ID)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("查询失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.OkWithData(resysVersion, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysVersionList 分页获取版本管理列表
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 分页获取版本管理列表
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表"
|
|
||||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
|
||||||
// @Router /sysVersion/getSysVersionList [get]
|
|
||||||
func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) {
|
|
||||||
// 创建业务用Context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
var pageInfo systemReq.SysVersionSearch
|
|
||||||
err := c.ShouldBindQuery(&pageInfo)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
list, total, err := sysVersionService.GetSysVersionInfoList(ctx, pageInfo)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.OkWithDetailed(response.PageResult{
|
|
||||||
List: list,
|
|
||||||
Total: total,
|
|
||||||
Page: pageInfo.Page,
|
|
||||||
PageSize: pageInfo.PageSize,
|
|
||||||
}, "获取成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysVersionPublic 不需要鉴权的版本管理接口
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 不需要鉴权的版本管理接口
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
|
|
||||||
// @Router /sysVersion/getSysVersionPublic [get]
|
|
||||||
func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) {
|
|
||||||
// 创建业务用Context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
// 此接口不需要鉴权
|
|
||||||
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
|
|
||||||
sysVersionService.GetSysVersionPublic(ctx)
|
|
||||||
response.OkWithDetailed(gin.H{
|
|
||||||
"info": "不需要鉴权的版本管理接口信息",
|
|
||||||
}, "获取成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportVersion 创建发版数据
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 创建发版数据
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body systemReq.ExportVersionRequest true "创建发版数据"
|
|
||||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
|
||||||
// @Router /sysVersion/exportVersion [post]
|
|
||||||
func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
var req systemReq.ExportVersionRequest
|
|
||||||
err := c.ShouldBindJSON(&req)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage(err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选中的菜单数据
|
|
||||||
var menuData []system.SysBaseMenu
|
|
||||||
if len(req.MenuIds) > 0 {
|
|
||||||
menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取菜单数据失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选中的API数据
|
|
||||||
var apiData []system.SysApi
|
|
||||||
if len(req.ApiIds) > 0 {
|
|
||||||
apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("获取API数据失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取API数据失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选中的字典数据
|
|
||||||
var dictData []system.SysDictionary
|
|
||||||
if len(req.DictIds) > 0 {
|
|
||||||
dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取字典数据失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理菜单数据,构建递归的children结构
|
|
||||||
processedMenus := buildMenuTree(menuData)
|
|
||||||
|
|
||||||
// 处理API数据,清除ID和时间戳字段
|
|
||||||
processedApis := make([]system.SysApi, 0, len(apiData))
|
|
||||||
for _, api := range apiData {
|
|
||||||
cleanApi := system.SysApi{
|
|
||||||
Path: api.Path,
|
|
||||||
Description: api.Description,
|
|
||||||
ApiGroup: api.ApiGroup,
|
|
||||||
Method: api.Method,
|
|
||||||
}
|
|
||||||
processedApis = append(processedApis, cleanApi)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理字典数据,清除ID和时间戳字段,包含字典详情
|
|
||||||
processedDicts := make([]system.SysDictionary, 0, len(dictData))
|
|
||||||
for _, dict := range dictData {
|
|
||||||
cleanDict := system.SysDictionary{
|
|
||||||
Name: dict.Name,
|
|
||||||
Type: dict.Type,
|
|
||||||
Status: dict.Status,
|
|
||||||
Desc: dict.Desc,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理字典详情数据,清除ID和时间戳字段
|
|
||||||
cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails))
|
|
||||||
for _, detail := range dict.SysDictionaryDetails {
|
|
||||||
cleanDetail := system.SysDictionaryDetail{
|
|
||||||
Label: detail.Label,
|
|
||||||
Value: detail.Value,
|
|
||||||
Extend: detail.Extend,
|
|
||||||
Status: detail.Status,
|
|
||||||
Sort: detail.Sort,
|
|
||||||
// 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID
|
|
||||||
}
|
|
||||||
cleanDetails = append(cleanDetails, cleanDetail)
|
|
||||||
}
|
|
||||||
cleanDict.SysDictionaryDetails = cleanDetails
|
|
||||||
|
|
||||||
processedDicts = append(processedDicts, cleanDict)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建导出数据
|
|
||||||
exportData := systemRes.ExportVersionResponse{
|
|
||||||
Version: systemReq.VersionInfo{
|
|
||||||
Name: req.VersionName,
|
|
||||||
Code: req.VersionCode,
|
|
||||||
Description: req.Description,
|
|
||||||
ExportTime: time.Now().Format("2006-01-02 15:04:05"),
|
|
||||||
},
|
|
||||||
Menus: processedMenus,
|
|
||||||
Apis: processedApis,
|
|
||||||
Dictionaries: processedDicts,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为JSON
|
|
||||||
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("JSON序列化失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存版本记录
|
|
||||||
version := system.SysVersion{
|
|
||||||
VersionName: utils.Pointer(req.VersionName),
|
|
||||||
VersionCode: utils.Pointer(req.VersionCode),
|
|
||||||
Description: utils.Pointer(req.Description),
|
|
||||||
VersionData: utils.Pointer(string(jsonData)),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sysVersionService.CreateSysVersion(ctx, &version)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("保存版本记录失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.OkWithMessage("创建发版成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadVersionJson 下载版本JSON数据
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 下载版本JSON数据
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param ID query string true "版本ID"
|
|
||||||
// @Success 200 {object} response.Response{data=object,msg=string} "下载成功"
|
|
||||||
// @Router /sysVersion/downloadVersionJson [get]
|
|
||||||
func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
ID := c.Query("ID")
|
|
||||||
if ID == "" {
|
|
||||||
response.FailWithMessage("版本ID不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取版本记录
|
|
||||||
version, err := sysVersionService.GetSysVersion(ctx, ID)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("获取版本记录失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建JSON数据
|
|
||||||
var jsonData []byte
|
|
||||||
if version.VersionData != nil && *version.VersionData != "" {
|
|
||||||
jsonData = []byte(*version.VersionData)
|
|
||||||
} else {
|
|
||||||
// 如果没有存储的JSON数据,构建一个基本的结构
|
|
||||||
basicData := systemRes.ExportVersionResponse{
|
|
||||||
Version: systemReq.VersionInfo{
|
|
||||||
Name: *version.VersionName,
|
|
||||||
Code: *version.VersionCode,
|
|
||||||
Description: *version.Description,
|
|
||||||
ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
},
|
|
||||||
Menus: []system.SysBaseMenu{},
|
|
||||||
Apis: []system.SysApi{},
|
|
||||||
}
|
|
||||||
jsonData, _ = json.MarshalIndent(basicData, "", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置下载响应头
|
|
||||||
filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405"))
|
|
||||||
c.Header("Content-Type", "application/json")
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
|
||||||
c.Header("Content-Length", strconv.Itoa(len(jsonData)))
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, "application/json", jsonData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportVersion 导入版本数据
|
|
||||||
// @Tags SysVersion
|
|
||||||
// @Summary 导入版本数据
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Accept application/json
|
|
||||||
// @Produce application/json
|
|
||||||
// @Param data body systemReq.ImportVersionRequest true "版本JSON数据"
|
|
||||||
// @Success 200 {object} response.Response{msg=string} "导入成功"
|
|
||||||
// @Router /sysVersion/importVersion [post]
|
|
||||||
func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
|
|
||||||
// 获取JSON数据
|
|
||||||
var importData systemReq.ImportVersionRequest
|
|
||||||
err := c.ShouldBindJSON(&importData)
|
|
||||||
if err != nil {
|
|
||||||
response.FailWithMessage("解析JSON数据失败:"+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证数据格式
|
|
||||||
if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" {
|
|
||||||
response.FailWithMessage("版本信息格式错误", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入菜单数据
|
|
||||||
if len(importData.ExportMenu) > 0 {
|
|
||||||
if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil {
|
|
||||||
global.GVA_LOG.Error("导入菜单失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("导入菜单失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入API数据
|
|
||||||
if len(importData.ExportApi) > 0 {
|
|
||||||
if err := sysVersionService.ImportApis(importData.ExportApi); err != nil {
|
|
||||||
global.GVA_LOG.Error("导入API失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("导入API失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入字典数据
|
|
||||||
if len(importData.ExportDictionary) > 0 {
|
|
||||||
if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil {
|
|
||||||
global.GVA_LOG.Error("导入字典失败!", zap.Error(err))
|
|
||||||
response.FailWithMessage("导入字典失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建导入记录
|
|
||||||
jsonData, _ := json.Marshal(importData)
|
|
||||||
version := system.SysVersion{
|
|
||||||
VersionName: utils.Pointer(importData.VersionInfo.Name),
|
|
||||||
VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))),
|
|
||||||
Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)),
|
|
||||||
VersionData: utils.Pointer(string(jsonData)),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sysVersionService.CreateSysVersion(ctx, &version)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err))
|
|
||||||
// 这里不返回错误,因为数据已经导入成功
|
|
||||||
}
|
|
||||||
|
|
||||||
response.OkWithMessage("导入成功", c)
|
|
||||||
}
|
|
||||||
@@ -97,7 +97,7 @@ captcha:
|
|||||||
open-captcha-timeout: 3600 # open-captcha大于0时才生效
|
open-captcha-timeout: 3600 # open-captcha大于0时才生效
|
||||||
|
|
||||||
# mysql connect configuration
|
# mysql connect configuration
|
||||||
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
|
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-react-admin.com/docs/first_master)
|
||||||
mysql:
|
mysql:
|
||||||
path: ""
|
path: ""
|
||||||
port: ""
|
port: ""
|
||||||
@@ -111,7 +111,7 @@ mysql:
|
|||||||
log-zap: false
|
log-zap: false
|
||||||
|
|
||||||
# pgsql connect configuration
|
# pgsql connect configuration
|
||||||
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
|
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-react-admin.com/docs/first_master)
|
||||||
pgsql:
|
pgsql:
|
||||||
path: ""
|
path: ""
|
||||||
port: ""
|
port: ""
|
||||||
@@ -211,7 +211,7 @@ tencent-cos:
|
|||||||
region: ap-shanghai
|
region: ap-shanghai
|
||||||
secret-id: your-secret-id
|
secret-id: your-secret-id
|
||||||
secret-key: your-secret-key
|
secret-key: your-secret-key
|
||||||
base-url: https://gin.vue.admin
|
base-url: https://gin-react-admin.com
|
||||||
path-prefix: git.echol.cn/loser/Go-Web-Template/server
|
path-prefix: git.echol.cn/loser/Go-Web-Template/server
|
||||||
|
|
||||||
# aws s3 configuration (minio compatible)
|
# aws s3 configuration (minio compatible)
|
||||||
@@ -223,13 +223,13 @@ aws-s3:
|
|||||||
disable-ssl: false
|
disable-ssl: false
|
||||||
secret-id: your-secret-id
|
secret-id: your-secret-id
|
||||||
secret-key: your-secret-key
|
secret-key: your-secret-key
|
||||||
base-url: https://gin.vue.admin
|
base-url: https://gin-react-admin.com
|
||||||
path-prefix: git.echol.cn/loser/Go-Web-Template/server
|
path-prefix: git.echol.cn/loser/Go-Web-Template/server
|
||||||
|
|
||||||
# cloudflare r2 configuration
|
# cloudflare r2 configuration
|
||||||
cloudflare-r2:
|
cloudflare-r2:
|
||||||
bucket: xxxx0bucket
|
bucket: xxxx0bucket
|
||||||
base-url: https://gin.vue.admin.com
|
base-url: https://gin-react-admin.com
|
||||||
path: uploads
|
path: uploads
|
||||||
account-id: xxx_account_id
|
account-id: xxx_account_id
|
||||||
access-key-id: xxx_key_id
|
access-key-id: xxx_key_id
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func RunServer() {
|
|||||||
mcpBaseURL := mcpTool.ResolveMCPServiceURL()
|
mcpBaseURL := mcpTool.ResolveMCPServiceURL()
|
||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
欢迎使用 gin-vue-admin
|
欢迎使用 Gin-React-Admin
|
||||||
当前版本:%s
|
当前版本:%s
|
||||||
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
|
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
|
||||||
MCP 独立服务请手动启动: go run ./cmd/mcp -config ./cmd/mcp/config.yaml
|
MCP 独立服务请手动启动: go run ./cmd/mcp -config ./cmd/mcp/config.yaml
|
||||||
|
|||||||
3810
server/docs/docs.go
3810
server/docs/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ const (
|
|||||||
// Version 当前版本号
|
// Version 当前版本号
|
||||||
Version = "v2.9.1"
|
Version = "v2.9.1"
|
||||||
// AppName 应用名称
|
// AppName 应用名称
|
||||||
AppName = "Gin-Vue-Admin"
|
AppName = "Gin-React-Admin"
|
||||||
// Description 应用描述
|
// Description 应用描述
|
||||||
Description = "使用gin+vue进行极速开发的全栈开发基础平台"
|
Description = "使用gin+react进行极速开发的全栈开发基础平台"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package initialize
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
commonModel "git.echol.cn/loser/Go-Web-Template/server/model/common"
|
commonModel "git.echol.cn/loser/Go-Web-Template/server/model/common"
|
||||||
sysModel "git.echol.cn/loser/Go-Web-Template/server/model/system"
|
sysModel "git.echol.cn/loser/Go-Web-Template/server/model/system"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/service/system"
|
"git.echol.cn/loser/Go-Web-Template/server/service/system"
|
||||||
@@ -46,11 +47,7 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
|
|||||||
sysModel.SysBaseMenuParameter{},
|
sysModel.SysBaseMenuParameter{},
|
||||||
sysModel.SysBaseMenuBtn{},
|
sysModel.SysBaseMenuBtn{},
|
||||||
sysModel.SysAuthorityBtn{},
|
sysModel.SysAuthorityBtn{},
|
||||||
sysModel.SysExportTemplate{},
|
|
||||||
sysModel.Condition{},
|
|
||||||
sysModel.JoinTemplate{},
|
|
||||||
sysModel.SysParams{},
|
sysModel.SysParams{},
|
||||||
sysModel.SysVersion{},
|
|
||||||
sysModel.SysError{},
|
sysModel.SysError{},
|
||||||
sysModel.SysLoginLog{},
|
sysModel.SysLoginLog{},
|
||||||
sysModel.SysApiToken{},
|
sysModel.SysApiToken{},
|
||||||
@@ -86,9 +83,6 @@ func (e *ensureTables) TableCreated(ctx context.Context) bool {
|
|||||||
sysModel.SysBaseMenuParameter{},
|
sysModel.SysBaseMenuParameter{},
|
||||||
sysModel.SysBaseMenuBtn{},
|
sysModel.SysBaseMenuBtn{},
|
||||||
sysModel.SysAuthorityBtn{},
|
sysModel.SysAuthorityBtn{},
|
||||||
sysModel.SysExportTemplate{},
|
|
||||||
sysModel.Condition{},
|
|
||||||
sysModel.JoinTemplate{},
|
|
||||||
|
|
||||||
adapter.CasbinRule{},
|
adapter.CasbinRule{},
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,7 @@ func RegisterTables() {
|
|||||||
system.SysBaseMenuParameter{},
|
system.SysBaseMenuParameter{},
|
||||||
system.SysBaseMenuBtn{},
|
system.SysBaseMenuBtn{},
|
||||||
system.SysAuthorityBtn{},
|
system.SysAuthorityBtn{},
|
||||||
system.SysExportTemplate{},
|
|
||||||
system.Condition{},
|
|
||||||
system.JoinTemplate{},
|
|
||||||
system.SysParams{},
|
system.SysParams{},
|
||||||
system.SysVersion{},
|
|
||||||
system.SysError{},
|
system.SysError{},
|
||||||
system.SysApiToken{},
|
system.SysApiToken{},
|
||||||
system.SysLoginLog{},
|
system.SysLoginLog{},
|
||||||
|
|||||||
@@ -84,14 +84,12 @@ func Routers() *gin.Engine {
|
|||||||
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
||||||
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
||||||
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
|
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
|
||||||
systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由
|
|
||||||
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
|
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
|
||||||
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
|
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
|
||||||
systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理
|
systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理
|
||||||
systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
|
systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
|
||||||
systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
|
systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
|
||||||
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理
|
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理
|
||||||
systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板
|
|
||||||
systemRouter.InitMcpRouter(PrivateGroup) // MCP 管理
|
systemRouter.InitMcpRouter(PrivateGroup) // MCP 管理
|
||||||
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
|
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
|
||||||
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
|
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import (
|
|||||||
// @Tag.Name SysUser
|
// @Tag.Name SysUser
|
||||||
// @Tag.Description 用户
|
// @Tag.Description 用户
|
||||||
|
|
||||||
// @title Gin-Vue-Admin Swagger API接口文档
|
// @title Gin-React-Admin Swagger API接口文档
|
||||||
// @version v2.9.1
|
// @version v2.9.1
|
||||||
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
// @description 使用gin+react进行极速开发的全栈开发基础平台
|
||||||
// @securityDefinitions.apikey ApiKeyAuth
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name x-token
|
// @name x-token
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func requireMCPIntegration(t *testing.T) {
|
|||||||
// 测试 MCP 客户端连接
|
// 测试 MCP 客户端连接
|
||||||
func TestMcpClientConnection(t *testing.T) {
|
func TestMcpClientConnection(t *testing.T) {
|
||||||
requireMCPIntegration(t)
|
requireMCPIntegration(t)
|
||||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-react-admin MCP服务")
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create client failed: %v", err)
|
t.Fatalf("create client failed: %v", err)
|
||||||
@@ -28,7 +28,7 @@ func TestMcpClientConnection(t *testing.T) {
|
|||||||
func TestTools(t *testing.T) {
|
func TestTools(t *testing.T) {
|
||||||
requireMCPIntegration(t)
|
requireMCPIntegration(t)
|
||||||
t.Run("currentTime", func(t *testing.T) {
|
t.Run("currentTime", func(t *testing.T) {
|
||||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-react-admin MCP服务")
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create client: %v", err)
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
@@ -58,7 +58,7 @@ func TestTools(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("getNickname", func(t *testing.T) {
|
t.Run("getNickname", func(t *testing.T) {
|
||||||
|
|
||||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-react-admin MCP服务")
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create client: %v", err)
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
@@ -102,7 +102,7 @@ func TestTools(t *testing.T) {
|
|||||||
|
|
||||||
func TestGetTools(t *testing.T) {
|
func TestGetTools(t *testing.T) {
|
||||||
requireMCPIntegration(t)
|
requireMCPIntegration(t)
|
||||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-react-admin MCP服务")
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create client: %v", err)
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
|||||||
@@ -69,14 +69,14 @@ func (m *MenuCreator) New() mcp.Tool {
|
|||||||
),
|
),
|
||||||
mcp.WithString("name",
|
mcp.WithString("name",
|
||||||
mcp.Required(),
|
mcp.Required(),
|
||||||
mcp.Description("路由name,用于Vue Router,如:userList"),
|
mcp.Description("路由name,用于前端路由标识,如:userList"),
|
||||||
),
|
),
|
||||||
mcp.WithBoolean("hidden",
|
mcp.WithBoolean("hidden",
|
||||||
mcp.Description("是否在菜单列表中隐藏"),
|
mcp.Description("是否在菜单列表中隐藏"),
|
||||||
),
|
),
|
||||||
mcp.WithString("component",
|
mcp.WithString("component",
|
||||||
mcp.Required(),
|
mcp.Required(),
|
||||||
mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"),
|
mcp.Description("对应的前端React组件路径,如:features/users/UserManagementPage"),
|
||||||
),
|
),
|
||||||
mcp.WithNumber("sort",
|
mcp.WithNumber("sort",
|
||||||
mcp.Description("菜单排序号,数字越小越靠前"),
|
mcp.Description("菜单排序号,数字越小越靠前"),
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package request
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysExportTemplateSearch struct {
|
|
||||||
system.SysExportTemplate
|
|
||||||
StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"`
|
|
||||||
EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"`
|
|
||||||
request.PageInfo
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ func DefaultMenu() []system.SysBaseMenu {
|
|||||||
ParentId: 0,
|
ParentId: 0,
|
||||||
Path: "dashboard",
|
Path: "dashboard",
|
||||||
Name: "dashboard",
|
Name: "dashboard",
|
||||||
Component: "view/dashboard/index.vue",
|
Component: "features/dashboard/DashboardPage",
|
||||||
Sort: 1,
|
Sort: 1,
|
||||||
Meta: system.Meta{
|
Meta: system.Meta{
|
||||||
Title: "仪表盘",
|
Title: "仪表盘",
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package request
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysVersionSearch struct {
|
|
||||||
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
|
|
||||||
VersionName *string `json:"versionName" form:"versionName"`
|
|
||||||
VersionCode *string `json:"versionCode" form:"versionCode"`
|
|
||||||
request.PageInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportVersionRequest 导出版本请求结构体
|
|
||||||
type ExportVersionRequest struct {
|
|
||||||
VersionName string `json:"versionName" binding:"required"` // 版本名称
|
|
||||||
VersionCode string `json:"versionCode" binding:"required"` // 版本号
|
|
||||||
Description string `json:"description"` // 版本描述
|
|
||||||
MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表
|
|
||||||
ApiIds []uint `json:"apiIds"` // 选中的API ID列表
|
|
||||||
DictIds []uint `json:"dictIds"` // 选中的字典ID列表
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportVersionRequest 导入版本请求结构体
|
|
||||||
type ImportVersionRequest struct {
|
|
||||||
VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
|
|
||||||
ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
|
|
||||||
ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi
|
|
||||||
ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
|
|
||||||
}
|
|
||||||
|
|
||||||
// VersionInfo 版本信息结构体
|
|
||||||
type VersionInfo struct {
|
|
||||||
Name string `json:"name" binding:"required"` // 版本名称
|
|
||||||
Code string `json:"code" binding:"required"` // 版本号
|
|
||||||
Description string `json:"description"` // 版本描述
|
|
||||||
ExportTime string `json:"exportTime"` // 导出时间
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExportVersionResponse 导出版本响应结构体
|
|
||||||
type ExportVersionResponse struct {
|
|
||||||
Version request.VersionInfo `json:"version"` // 版本信息
|
|
||||||
Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
|
|
||||||
Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi
|
|
||||||
Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 自动生成模板SysExportTemplate
|
|
||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 导出模板 结构体 SysExportTemplate
|
|
||||||
type SysExportTemplate struct {
|
|
||||||
global.GVA_MODEL
|
|
||||||
DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称
|
|
||||||
Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称
|
|
||||||
TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称
|
|
||||||
TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识
|
|
||||||
TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息
|
|
||||||
SQL string `json:"sql" form:"sql" gorm:"column:sql;type:text;comment:自定义导出SQL;"` //自定义导出SQL
|
|
||||||
ImportSQL string `json:"importSql" form:"importSql" gorm:"column:import_sql;type:text;comment:自定义导入SQL;"` //自定义导入SQL
|
|
||||||
Limit *int `json:"limit" form:"limit" gorm:"column:limit;comment:导出限制"`
|
|
||||||
Order string `json:"order" form:"order" gorm:"column:order;comment:排序"`
|
|
||||||
Conditions []Condition `json:"conditions" form:"conditions" gorm:"foreignKey:TemplateID;references:TemplateID;comment:条件"`
|
|
||||||
JoinTemplate []JoinTemplate `json:"joinTemplate" form:"joinTemplate" gorm:"foreignKey:TemplateID;references:TemplateID;comment:关联"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JoinTemplate struct {
|
|
||||||
global.GVA_MODEL
|
|
||||||
TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"`
|
|
||||||
JOINS string `json:"joins" form:"joins" gorm:"column:joins;comment:关联"`
|
|
||||||
Table string `json:"table" form:"table" gorm:"column:table;comment:关联表"`
|
|
||||||
ON string `json:"on" form:"on" gorm:"column:on;comment:关联条件"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (JoinTemplate) TableName() string {
|
|
||||||
return "sys_export_template_join"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Condition struct {
|
|
||||||
global.GVA_MODEL
|
|
||||||
TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"`
|
|
||||||
From string `json:"from" form:"from" gorm:"column:from;comment:条件取的key"`
|
|
||||||
Column string `json:"column" form:"column" gorm:"column:column;comment:作为查询条件的字段"`
|
|
||||||
Operator string `json:"operator" form:"operator" gorm:"column:operator;comment:操作符"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Condition) TableName() string {
|
|
||||||
return "sys_export_template_condition"
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// 自动生成模板SysVersion
|
|
||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 版本管理 结构体 SysVersion
|
|
||||||
type SysVersion struct {
|
|
||||||
global.GVA_MODEL
|
|
||||||
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
|
|
||||||
VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
|
|
||||||
Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
|
|
||||||
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 版本管理 SysVersion自定义表名 sys_versions
|
|
||||||
func (SysVersion) TableName() string {
|
|
||||||
return "sys_versions"
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,8 @@ type RouterGroup struct {
|
|||||||
OperationRecordRouter
|
OperationRecordRouter
|
||||||
DictionaryDetailRouter
|
DictionaryDetailRouter
|
||||||
AuthorityBtnRouter
|
AuthorityBtnRouter
|
||||||
SysExportTemplateRouter
|
|
||||||
McpRouter
|
McpRouter
|
||||||
SysParamsRouter
|
SysParamsRouter
|
||||||
SysVersionRouter
|
|
||||||
SysErrorRouter
|
SysErrorRouter
|
||||||
LoginLogRouter
|
LoginLogRouter
|
||||||
ApiTokenRouter
|
ApiTokenRouter
|
||||||
@@ -39,8 +37,6 @@ var (
|
|||||||
authorityMenuApi = api.ApiGroupApp.SystemApiGroup.AuthorityMenuApi
|
authorityMenuApi = api.ApiGroupApp.SystemApiGroup.AuthorityMenuApi
|
||||||
operationRecordApi = api.ApiGroupApp.SystemApiGroup.OperationRecordApi
|
operationRecordApi = api.ApiGroupApp.SystemApiGroup.OperationRecordApi
|
||||||
dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi
|
dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi
|
||||||
exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi
|
|
||||||
mcpApi = api.ApiGroupApp.SystemApiGroup.McpApi
|
mcpApi = api.ApiGroupApp.SystemApiGroup.McpApi
|
||||||
sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi
|
|
||||||
sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi
|
sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/middleware"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysExportTemplateRouter struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitSysExportTemplateRouter 初始化 导出模板 路由信息
|
|
||||||
func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) {
|
|
||||||
sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord())
|
|
||||||
sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate")
|
|
||||||
sysExportTemplateRouterWithoutAuth := pubRouter.Group("sysExportTemplate")
|
|
||||||
|
|
||||||
{
|
|
||||||
sysExportTemplateRouter.POST("createSysExportTemplate", exportTemplateApi.CreateSysExportTemplate) // 新建导出模板
|
|
||||||
sysExportTemplateRouter.DELETE("deleteSysExportTemplate", exportTemplateApi.DeleteSysExportTemplate) // 删除导出模板
|
|
||||||
sysExportTemplateRouter.DELETE("deleteSysExportTemplateByIds", exportTemplateApi.DeleteSysExportTemplateByIds) // 批量删除导出模板
|
|
||||||
sysExportTemplateRouter.PUT("updateSysExportTemplate", exportTemplateApi.UpdateSysExportTemplate) // 更新导出模板
|
|
||||||
sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 导入excel模板数据
|
|
||||||
}
|
|
||||||
{
|
|
||||||
sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板
|
|
||||||
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导出表格
|
|
||||||
sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/middleware"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysVersionRouter struct{}
|
|
||||||
|
|
||||||
// InitSysVersionRouter 初始化 版本管理 路由信息
|
|
||||||
func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) {
|
|
||||||
sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord())
|
|
||||||
sysVersionRouterWithoutRecord := Router.Group("sysVersion")
|
|
||||||
{
|
|
||||||
sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理
|
|
||||||
sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理
|
|
||||||
sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据
|
|
||||||
sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据
|
|
||||||
}
|
|
||||||
{
|
|
||||||
sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理
|
|
||||||
sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表
|
|
||||||
sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,9 +14,7 @@ type ServiceGroup struct {
|
|||||||
OperationRecordService
|
OperationRecordService
|
||||||
DictionaryDetailService
|
DictionaryDetailService
|
||||||
AuthorityBtnService
|
AuthorityBtnService
|
||||||
SysExportTemplateService
|
|
||||||
SysParamsService
|
SysParamsService
|
||||||
SysVersionService
|
|
||||||
McpService
|
McpService
|
||||||
SysErrorService
|
SysErrorService
|
||||||
LoginLogService
|
LoginLogService
|
||||||
|
|||||||
@@ -1,724 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
|
||||||
"github.com/xuri/excelize/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysExportTemplateService struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
var SysExportTemplateServiceApp = new(SysExportTemplateService)
|
|
||||||
|
|
||||||
// CreateSysExportTemplate 创建导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) CreateSysExportTemplate(sysExportTemplate *system.SysExportTemplate) (err error) {
|
|
||||||
err = global.GVA_DB.Create(sysExportTemplate).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysExportTemplate 删除导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) {
|
|
||||||
err = global.GVA_DB.Delete(&sysExportTemplate).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysExportTemplateByIds 批量删除导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplateByIds(ids request.IdsReq) (err error) {
|
|
||||||
err = global.GVA_DB.Delete(&[]system.SysExportTemplate{}, "id in ?", ids.Ids).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSysExportTemplate 更新导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) UpdateSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) {
|
|
||||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
conditions := sysExportTemplate.Conditions
|
|
||||||
e := tx.Delete(&[]system.Condition{}, "template_id = ?", sysExportTemplate.TemplateID).Error
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
sysExportTemplate.Conditions = nil
|
|
||||||
|
|
||||||
joins := sysExportTemplate.JoinTemplate
|
|
||||||
e = tx.Delete(&[]system.JoinTemplate{}, "template_id = ?", sysExportTemplate.TemplateID).Error
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
sysExportTemplate.JoinTemplate = nil
|
|
||||||
|
|
||||||
e = tx.Updates(&sysExportTemplate).Error
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
if len(conditions) > 0 {
|
|
||||||
for i := range conditions {
|
|
||||||
conditions[i].ID = 0
|
|
||||||
}
|
|
||||||
e = tx.Create(&conditions).Error
|
|
||||||
}
|
|
||||||
if len(joins) > 0 {
|
|
||||||
for i := range joins {
|
|
||||||
joins[i].ID = 0
|
|
||||||
}
|
|
||||||
e = tx.Create(&joins).Error
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysExportTemplate 根据id获取导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplate(id uint) (sysExportTemplate system.SysExportTemplate, err error) {
|
|
||||||
err = global.GVA_DB.Where("id = ?", id).Preload("JoinTemplate").Preload("Conditions").First(&sysExportTemplate).Error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysExportTemplateInfoList 分页获取导出模板记录
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateInfoList(info systemReq.SysExportTemplateSearch) (list []system.SysExportTemplate, total int64, err error) {
|
|
||||||
limit := info.PageSize
|
|
||||||
offset := info.PageSize * (info.Page - 1)
|
|
||||||
// 创建db
|
|
||||||
db := global.GVA_DB.Model(&system.SysExportTemplate{})
|
|
||||||
var sysExportTemplates []system.SysExportTemplate
|
|
||||||
// 如果有条件搜索 下方会自动创建搜索语句
|
|
||||||
if info.StartCreatedAt != nil && info.EndCreatedAt != nil {
|
|
||||||
db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt)
|
|
||||||
}
|
|
||||||
if info.Name != "" {
|
|
||||||
db = db.Where("name LIKE ?", "%"+info.Name+"%")
|
|
||||||
}
|
|
||||||
if info.TableName != "" {
|
|
||||||
db = db.Where("table_name = ?", info.TableName)
|
|
||||||
}
|
|
||||||
if info.TemplateID != "" {
|
|
||||||
db = db.Where("template_id = ?", info.TemplateID)
|
|
||||||
}
|
|
||||||
err = db.Count(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit != 0 {
|
|
||||||
db = db.Limit(limit).Offset(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Find(&sysExportTemplates).Error
|
|
||||||
return sysExportTemplates, total, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportExcel 导出Excel
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) {
|
|
||||||
var params = values.Get("params")
|
|
||||||
paramsValues, err := url.ParseQuery(params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("解析 params 参数失败: %v", err)
|
|
||||||
}
|
|
||||||
var template system.SysExportTemplate
|
|
||||||
err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
f := excelize.NewFile()
|
|
||||||
defer func() {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Create a new sheet.
|
|
||||||
index, err := f.NewSheet("Sheet1")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var templateInfoMap = make(map[string]string)
|
|
||||||
columns, err := utils.GetJSONKeys(template.TemplateInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
var tableTitle []string
|
|
||||||
var selectKeyFmt []string
|
|
||||||
for _, key := range columns {
|
|
||||||
selectKeyFmt = append(selectKeyFmt, key)
|
|
||||||
tableTitle = append(tableTitle, templateInfoMap[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
selects := strings.Join(selectKeyFmt, ", ")
|
|
||||||
var tableMap []map[string]interface{}
|
|
||||||
db := global.GVA_DB
|
|
||||||
if template.DBName != "" {
|
|
||||||
db = global.MustGetGlobalDBByDBName(template.DBName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有自定义SQL,则优先使用自定义SQL
|
|
||||||
if template.SQL != "" {
|
|
||||||
// 将 url.Values 转换为 map[string]interface{} 以支持 GORM 的命名参数
|
|
||||||
sqlParams := make(map[string]interface{})
|
|
||||||
for k, v := range paramsValues {
|
|
||||||
if len(v) > 0 {
|
|
||||||
sqlParams[k] = v[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行原生 SQL,支持 @key 命名参数
|
|
||||||
err = db.Raw(template.SQL, sqlParams).Scan(&tableMap).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(template.JoinTemplate) > 0 {
|
|
||||||
for _, join := range template.JoinTemplate {
|
|
||||||
db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db = db.Select(selects).Table(template.TableName)
|
|
||||||
|
|
||||||
filterDeleted := false
|
|
||||||
|
|
||||||
filterParam := paramsValues.Get("filterDeleted")
|
|
||||||
if filterParam == "true" {
|
|
||||||
filterDeleted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if filterDeleted {
|
|
||||||
// 自动过滤主表的软删除
|
|
||||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
|
|
||||||
|
|
||||||
// 过滤关联表的软删除(如果有)
|
|
||||||
if len(template.JoinTemplate) > 0 {
|
|
||||||
for _, join := range template.JoinTemplate {
|
|
||||||
// 检查关联表是否有deleted_at字段
|
|
||||||
hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table)
|
|
||||||
if hasDeletedAt {
|
|
||||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(template.Conditions) > 0 {
|
|
||||||
for _, condition := range template.Conditions {
|
|
||||||
sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator)
|
|
||||||
value := paramsValues.Get(condition.From)
|
|
||||||
|
|
||||||
if condition.Operator == "IN" || condition.Operator == "NOT IN" {
|
|
||||||
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 + "%"
|
|
||||||
}
|
|
||||||
db = db.Where(sql, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 通过参数传入limit
|
|
||||||
limit := paramsValues.Get("limit")
|
|
||||||
if limit != "" {
|
|
||||||
l, e := strconv.Atoi(limit)
|
|
||||||
if e == nil {
|
|
||||||
db = db.Limit(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 模板的默认limit
|
|
||||||
if limit == "" && template.Limit != nil && *template.Limit != 0 {
|
|
||||||
db = db.Limit(*template.Limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过参数传入offset
|
|
||||||
offset := paramsValues.Get("offset")
|
|
||||||
if offset != "" {
|
|
||||||
o, e := strconv.Atoi(offset)
|
|
||||||
if e == nil {
|
|
||||||
db = db.Offset(o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前表的所有字段
|
|
||||||
table := template.TableName
|
|
||||||
orderColumns, err := db.Migrator().ColumnTypes(table)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建一个 map 来存储字段名
|
|
||||||
fields := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, column := range orderColumns {
|
|
||||||
fields[column.Name()] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过参数传入order
|
|
||||||
order := paramsValues.Get("order")
|
|
||||||
|
|
||||||
if order == "" && template.Order != "" {
|
|
||||||
// 如果没有order入参,这里会使用模板的默认排序
|
|
||||||
order = template.Order
|
|
||||||
}
|
|
||||||
|
|
||||||
if order != "" {
|
|
||||||
checkOrderArr := strings.Split(order, " ")
|
|
||||||
orderStr := ""
|
|
||||||
// 检查请求的排序字段是否在字段列表中
|
|
||||||
if _, ok := fields[checkOrderArr[0]]; !ok {
|
|
||||||
return nil, "", fmt.Errorf("order by %s is not in the fields", order)
|
|
||||||
}
|
|
||||||
orderStr = checkOrderArr[0]
|
|
||||||
if len(checkOrderArr) > 1 {
|
|
||||||
if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" {
|
|
||||||
return nil, "", fmt.Errorf("order by %s is not secure", order)
|
|
||||||
}
|
|
||||||
orderStr = orderStr + " " + checkOrderArr[1]
|
|
||||||
}
|
|
||||||
db = db.Order(orderStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Debug().Find(&tableMap).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows [][]string
|
|
||||||
rows = append(rows, tableTitle)
|
|
||||||
for _, exTable := range tableMap {
|
|
||||||
var row []string
|
|
||||||
for _, column := range columns {
|
|
||||||
column = strings.ReplaceAll(column, "\"", "")
|
|
||||||
column = strings.ReplaceAll(column, "`", "")
|
|
||||||
if len(template.JoinTemplate) > 0 {
|
|
||||||
columnAs := strings.Split(column, " as ")
|
|
||||||
if len(columnAs) > 1 {
|
|
||||||
column = strings.TrimSpace(strings.Split(column, " as ")[1])
|
|
||||||
} else {
|
|
||||||
columnArr := strings.Split(column, ".")
|
|
||||||
if len(columnArr) > 1 {
|
|
||||||
column = strings.Split(column, ".")[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 需要对时间类型特殊处理
|
|
||||||
if t, ok := exTable[column].(time.Time); ok {
|
|
||||||
row = append(row, t.Format("2006-01-02 15:04:05"))
|
|
||||||
} else {
|
|
||||||
row = append(row, fmt.Sprintf("%v", exTable[column]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows = append(rows, row)
|
|
||||||
}
|
|
||||||
for i, row := range rows {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sErr != nil {
|
|
||||||
return nil, "", sErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.SetActiveSheet(index)
|
|
||||||
file, err = f.WriteToBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
var template system.SysExportTemplate
|
|
||||||
err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
f := excelize.NewFile()
|
|
||||||
defer func() {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Create a new sheet.
|
|
||||||
index, err := f.NewSheet("Sheet1")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var templateInfoMap = make(map[string]string)
|
|
||||||
|
|
||||||
columns, err := utils.GetJSONKeys(template.TemplateInfo)
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
var tableTitle []string
|
|
||||||
for _, key := range columns {
|
|
||||||
tableTitle = append(tableTitle, templateInfoMap[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range tableTitle {
|
|
||||||
fErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(i+1), 1), tableTitle[i])
|
|
||||||
if fErr != nil {
|
|
||||||
return nil, "", fErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.SetActiveSheet(index)
|
|
||||||
file, err = f.WriteToBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, template.Name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:检查表是否有deleted_at列
|
|
||||||
func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool {
|
|
||||||
var count int64
|
|
||||||
global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count)
|
|
||||||
return count > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportExcel 导入Excel
|
|
||||||
// Author [piexlmax](https://github.com/piexlmax)
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) {
|
|
||||||
var template system.SysExportTemplate
|
|
||||||
err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
f, err := excelize.OpenReader(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := f.GetRows("Sheet1")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(rows) < 2 {
|
|
||||||
return errors.New("Excel data is not enough.\nIt should contain title row and data")
|
|
||||||
}
|
|
||||||
|
|
||||||
var templateInfoMap = make(map[string]string)
|
|
||||||
err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db := global.GVA_DB
|
|
||||||
if template.DBName != "" {
|
|
||||||
db = global.MustGetGlobalDBByDBName(template.DBName)
|
|
||||||
}
|
|
||||||
|
|
||||||
items, err := sysExportTemplateService.parseExcelToMap(rows, templateInfoMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
if template.ImportSQL != "" {
|
|
||||||
return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items)
|
|
||||||
}
|
|
||||||
return sysExportTemplateService.importByGORM(tx, template.TableName, items)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) parseExcelToMap(rows [][]string, templateInfoMap map[string]string) ([]map[string]interface{}, error) {
|
|
||||||
var titleKeyMap = make(map[string]string)
|
|
||||||
for key, title := range templateInfoMap {
|
|
||||||
titleKeyMap[title] = key
|
|
||||||
}
|
|
||||||
|
|
||||||
excelTitle := rows[0]
|
|
||||||
for i, str := range excelTitle {
|
|
||||||
excelTitle[i] = strings.TrimSpace(str)
|
|
||||||
}
|
|
||||||
values := rows[1:]
|
|
||||||
items := make([]map[string]interface{}, 0, len(values))
|
|
||||||
for _, row := range values {
|
|
||||||
var item = make(map[string]interface{})
|
|
||||||
for ii, value := range row {
|
|
||||||
if ii >= len(excelTitle) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := titleKeyMap[excelTitle[ii]]; !ok {
|
|
||||||
continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过
|
|
||||||
}
|
|
||||||
key := titleKeyMap[excelTitle[ii]]
|
|
||||||
item[key] = value
|
|
||||||
}
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) importBySQL(tx *gorm.DB, sql string, items []map[string]interface{}) error {
|
|
||||||
for _, item := range items {
|
|
||||||
if err := tx.Exec(sql, item).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sysExportTemplateService *SysExportTemplateService) importByGORM(tx *gorm.DB, tableName string, items []map[string]interface{}) error {
|
|
||||||
needCreated := tx.Migrator().HasColumn(tableName, "created_at")
|
|
||||||
needUpdated := tx.Migrator().HasColumn(tableName, "updated_at")
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
if item["created_at"] == nil && needCreated {
|
|
||||||
item["created_at"] = time.Now()
|
|
||||||
}
|
|
||||||
if item["updated_at"] == nil && needUpdated {
|
|
||||||
item["updated_at"] = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Table(tableName).CreateInBatches(&items, 1000).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func getColumnName(n int) string {
|
|
||||||
columnName := ""
|
|
||||||
for n > 0 {
|
|
||||||
n--
|
|
||||||
columnName = string(rune('A'+n%26)) + columnName
|
|
||||||
n /= 26
|
|
||||||
}
|
|
||||||
return columnName
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/config"
|
"git.echol.cn/loser/Go-Web-Template/server/config"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
@@ -19,6 +20,10 @@ import (
|
|||||||
|
|
||||||
type PgsqlInitHandler struct{}
|
type PgsqlInitHandler struct{}
|
||||||
|
|
||||||
|
func quotePgIdentifier(name string) string {
|
||||||
|
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewPgsqlInitHandler() *PgsqlInitHandler {
|
func NewPgsqlInitHandler() *PgsqlInitHandler {
|
||||||
return &PgsqlInitHandler{}
|
return &PgsqlInitHandler{}
|
||||||
}
|
}
|
||||||
@@ -55,9 +60,13 @@ func (h PgsqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (n
|
|||||||
dsn := conf.PgsqlEmptyDsn()
|
dsn := conf.PgsqlEmptyDsn()
|
||||||
var createSql string
|
var createSql string
|
||||||
if conf.Template != "" {
|
if conf.Template != "" {
|
||||||
createSql = fmt.Sprintf("CREATE DATABASE %s WITH TEMPLATE %s;", c.Dbname, conf.Template)
|
createSql = fmt.Sprintf(
|
||||||
|
"CREATE DATABASE %s WITH TEMPLATE %s;",
|
||||||
|
quotePgIdentifier(c.Dbname),
|
||||||
|
quotePgIdentifier(conf.Template),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
createSql = fmt.Sprintf("CREATE DATABASE %s;", c.Dbname)
|
createSql = fmt.Sprintf("CREATE DATABASE %s;", quotePgIdentifier(c.Dbname))
|
||||||
}
|
}
|
||||||
if err = createDatabase(dsn, "pgx", createSql); err != nil {
|
if err = createDatabase(dsn, "pgx", createSql); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
systemReq "git.echol.cn/loser/Go-Web-Template/server/model/system/request"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SysVersionService struct{}
|
|
||||||
|
|
||||||
// CreateSysVersion 创建版本管理记录
|
|
||||||
// Author [yourname](https://github.com/yourname)
|
|
||||||
func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) {
|
|
||||||
err = global.GVA_DB.Create(sysVersion).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysVersion 删除版本管理记录
|
|
||||||
// Author [yourname](https://github.com/yourname)
|
|
||||||
func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) {
|
|
||||||
err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSysVersionByIds 批量删除版本管理记录
|
|
||||||
// Author [yourname](https://github.com/yourname)
|
|
||||||
func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) {
|
|
||||||
err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysVersion 根据ID获取版本管理记录
|
|
||||||
// Author [yourname](https://github.com/yourname)
|
|
||||||
func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) {
|
|
||||||
err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSysVersionInfoList 分页获取版本管理记录
|
|
||||||
// Author [yourname](https://github.com/yourname)
|
|
||||||
func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) {
|
|
||||||
limit := info.PageSize
|
|
||||||
offset := info.PageSize * (info.Page - 1)
|
|
||||||
// 创建db
|
|
||||||
db := global.GVA_DB.Model(&system.SysVersion{})
|
|
||||||
var sysVersions []system.SysVersion
|
|
||||||
// 如果有条件搜索 下方会自动创建搜索语句
|
|
||||||
if len(info.CreatedAtRange) == 2 {
|
|
||||||
db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.VersionName != nil && *info.VersionName != "" {
|
|
||||||
db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%")
|
|
||||||
}
|
|
||||||
if info.VersionCode != nil && *info.VersionCode != "" {
|
|
||||||
db = db.Where("version_code = ?", *info.VersionCode)
|
|
||||||
}
|
|
||||||
err = db.Count(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit != 0 {
|
|
||||||
db = db.Limit(limit).Offset(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Find(&sysVersions).Error
|
|
||||||
return sysVersions, total, err
|
|
||||||
}
|
|
||||||
func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) {
|
|
||||||
// 此方法为获取数据源定义的数据
|
|
||||||
// 请自行实现
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMenusByIds 根据ID列表获取菜单数据
|
|
||||||
func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) {
|
|
||||||
err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApisByIds 根据ID列表获取API数据
|
|
||||||
func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) {
|
|
||||||
err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDictionariesByIds 根据ID列表获取字典数据
|
|
||||||
func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) {
|
|
||||||
err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportMenus 导入菜单数据
|
|
||||||
func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error {
|
|
||||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
// 递归创建菜单
|
|
||||||
return sysVersionService.createMenusRecursively(tx, menus, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// createMenusRecursively 递归创建菜单
|
|
||||||
func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error {
|
|
||||||
for _, menu := range menus {
|
|
||||||
// 检查菜单是否已存在
|
|
||||||
var existingMenu system.SysBaseMenu
|
|
||||||
if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil {
|
|
||||||
// 菜单已存在,使用现有菜单ID继续处理子菜单
|
|
||||||
if len(menu.Children) > 0 {
|
|
||||||
if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存参数和按钮数据,稍后处理
|
|
||||||
parameters := menu.Parameters
|
|
||||||
menuBtns := menu.MenuBtn
|
|
||||||
children := menu.Children
|
|
||||||
|
|
||||||
// 创建新菜单(不包含关联数据)
|
|
||||||
newMenu := system.SysBaseMenu{
|
|
||||||
ParentId: parentId,
|
|
||||||
Path: menu.Path,
|
|
||||||
Name: menu.Name,
|
|
||||||
Hidden: menu.Hidden,
|
|
||||||
Component: menu.Component,
|
|
||||||
Sort: menu.Sort,
|
|
||||||
Meta: menu.Meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Create(&newMenu).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建参数
|
|
||||||
if len(parameters) > 0 {
|
|
||||||
for _, param := range parameters {
|
|
||||||
newParam := system.SysBaseMenuParameter{
|
|
||||||
SysBaseMenuID: newMenu.ID,
|
|
||||||
Type: param.Type,
|
|
||||||
Key: param.Key,
|
|
||||||
Value: param.Value,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&newParam).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建菜单按钮
|
|
||||||
if len(menuBtns) > 0 {
|
|
||||||
for _, btn := range menuBtns {
|
|
||||||
newBtn := system.SysBaseMenuBtn{
|
|
||||||
SysBaseMenuID: newMenu.ID,
|
|
||||||
Name: btn.Name,
|
|
||||||
Desc: btn.Desc,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&newBtn).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子菜单
|
|
||||||
if len(children) > 0 {
|
|
||||||
if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportApis 导入API数据
|
|
||||||
func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error {
|
|
||||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
for _, api := range apis {
|
|
||||||
// 检查API是否已存在
|
|
||||||
var existingApi system.SysApi
|
|
||||||
if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil {
|
|
||||||
// API已存在,跳过
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新API
|
|
||||||
newApi := system.SysApi{
|
|
||||||
Path: api.Path,
|
|
||||||
Description: api.Description,
|
|
||||||
ApiGroup: api.ApiGroup,
|
|
||||||
Method: api.Method,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Create(&newApi).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportDictionaries 导入字典数据
|
|
||||||
func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error {
|
|
||||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
for _, dict := range dictionaries {
|
|
||||||
// 检查字典是否已存在
|
|
||||||
var existingDict system.SysDictionary
|
|
||||||
if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil {
|
|
||||||
// 字典已存在,跳过
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新字典
|
|
||||||
newDict := system.SysDictionary{
|
|
||||||
Name: dict.Name,
|
|
||||||
Type: dict.Type,
|
|
||||||
Status: dict.Status,
|
|
||||||
Desc: dict.Desc,
|
|
||||||
SysDictionaryDetails: dict.SysDictionaryDetails,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Create(&newDict).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -152,24 +152,13 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
|||||||
{ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/checkFileMd5", Description: "文件完整度验证"},
|
{ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/checkFileMd5", Description: "文件完整度验证"},
|
||||||
{ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/mergeFileMd5", Description: "上传完成合并文件"},
|
{ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/mergeFileMd5", Description: "上传完成合并文件"},
|
||||||
|
|
||||||
{ApiGroup: "email", Method: "POST", Path: "/email/emailTest", Description: "发送测试邮件"},
|
//{ApiGroup: "email", Method: "POST", Path: "/email/emailTest", Description: "发送测试邮件"},
|
||||||
{ApiGroup: "email", Method: "POST", Path: "/email/sendEmail", Description: "发送邮件"},
|
//{ApiGroup: "email", Method: "POST", Path: "/email/sendEmail", Description: "发送邮件"},
|
||||||
|
|
||||||
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/setAuthorityBtn", Description: "设置按钮权限"},
|
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/setAuthorityBtn", Description: "设置按钮权限"},
|
||||||
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/getAuthorityBtn", Description: "获取已有按钮权限"},
|
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/getAuthorityBtn", Description: "获取已有按钮权限"},
|
||||||
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/canRemoveAuthorityBtn", Description: "删除按钮"},
|
{ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/canRemoveAuthorityBtn", Description: "删除按钮"},
|
||||||
|
|
||||||
{ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/createSysExportTemplate", Description: "新增导出模板"},
|
|
||||||
{ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplate", Description: "删除导出模板"},
|
|
||||||
{ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplateByIds", Description: "批量删除导出模板"},
|
|
||||||
{ApiGroup: "导出模板", Method: "PUT", Path: "/sysExportTemplate/updateSysExportTemplate", Description: "更新导出模板"},
|
|
||||||
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/findSysExportTemplate", Description: "根据ID获取导出模板"},
|
|
||||||
{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: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"},
|
||||||
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"},
|
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"},
|
||||||
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"},
|
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"},
|
||||||
@@ -188,14 +177,6 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
|||||||
{ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"},
|
{ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"},
|
||||||
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"},
|
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"},
|
||||||
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"},
|
{ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"},
|
||||||
|
|
||||||
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"},
|
|
||||||
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"},
|
|
||||||
{ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"},
|
|
||||||
{ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"},
|
|
||||||
{ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"},
|
|
||||||
{ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"},
|
|
||||||
{ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"},
|
|
||||||
}
|
}
|
||||||
if err := db.Create(&entities).Error; err != nil {
|
if err := db.Create(&entities).Error; err != nil {
|
||||||
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
||||||
|
|||||||
@@ -161,17 +161,6 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
|||||||
{Ptype: "p", V0: "888", V1: "/authorityBtn/getAuthorityBtn", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/authorityBtn/getAuthorityBtn", V2: "POST"},
|
||||||
{Ptype: "p", V0: "888", V1: "/authorityBtn/canRemoveAuthorityBtn", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/authorityBtn/canRemoveAuthorityBtn", V2: "POST"},
|
||||||
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/createSysExportTemplate", V2: "POST"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplate", V2: "DELETE"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplateByIds", V2: "DELETE"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/updateSysExportTemplate", V2: "PUT"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/findSysExportTemplate", V2: "GET"},
|
|
||||||
{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/createSysError", V2: "POST"},
|
||||||
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"},
|
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"},
|
||||||
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"},
|
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"},
|
||||||
@@ -191,14 +180,6 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
|||||||
{Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"},
|
||||||
{Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"},
|
{Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"},
|
||||||
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/findSysVersion", V2: "GET"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"},
|
|
||||||
{Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"},
|
|
||||||
|
|
||||||
{Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"},
|
||||||
{Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"},
|
||||||
{Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"},
|
{Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"},
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
sysModel "git.echol.cn/loser/Go-Web-Template/server/model/system"
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/service/system"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type initExcelTemplate struct{}
|
|
||||||
|
|
||||||
const initOrderExcelTemplate = initOrderDictDetail + 1
|
|
||||||
|
|
||||||
// auto run
|
|
||||||
func init() {
|
|
||||||
system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *initExcelTemplate) InitializerName() string {
|
|
||||||
return "sys_export_templates"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) {
|
|
||||||
db, ok := ctx.Value("db").(*gorm.DB)
|
|
||||||
if !ok {
|
|
||||||
return ctx, system.ErrMissingDBContext
|
|
||||||
}
|
|
||||||
return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *initExcelTemplate) TableCreated(ctx context.Context) bool {
|
|
||||||
db, ok := ctx.Value("db").(*gorm.DB)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return db.Migrator().HasTable(&sysModel.SysExportTemplate{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) {
|
|
||||||
db, ok := ctx.Value("db").(*gorm.DB)
|
|
||||||
if !ok {
|
|
||||||
return ctx, system.ErrMissingDBContext
|
|
||||||
}
|
|
||||||
|
|
||||||
entities := []sysModel.SysExportTemplate{
|
|
||||||
{
|
|
||||||
Name: "api",
|
|
||||||
TableName: "sys_apis",
|
|
||||||
TemplateID: "api",
|
|
||||||
TemplateInfo: `{
|
|
||||||
"path":"路径",
|
|
||||||
"method":"方法(大写)",
|
|
||||||
"description":"方法介绍",
|
|
||||||
"api_group":"方法分组"
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := db.Create(&entities).Error; err != nil {
|
|
||||||
return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!")
|
|
||||||
}
|
|
||||||
next := context.WithValue(ctx, i.InitializerName(), entities)
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *initExcelTemplate) DataInserted(ctx context.Context) bool {
|
|
||||||
db, ok := ctx.Value("db").(*gorm.DB)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -53,14 +53,14 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
|
|||||||
|
|
||||||
// 定义所有菜单
|
// 定义所有菜单
|
||||||
allMenus := []SysBaseMenu{
|
allMenus := []SysBaseMenu{
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "features/dashboard/DashboardPage", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "DashboardOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "features/discovery/ModuleLandingPage:about", Sort: 9, Meta: Meta{Title: "关于系统", Icon: "InfoCircleOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "features/discovery/ModuleLandingPage:superAdmin", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "LockOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}},
|
{MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "features/person/ProfilePage", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "UserOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "common", Name: "common", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "公共能力", Icon: "folder-opened"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "common", Name: "common", Component: "features/discovery/ModuleLandingPage:common", Sort: 6, Meta: Meta{Title: "公共能力", Icon: "AppstoreOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "编程辅助", Icon: "tools"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "features/discovery/ModuleLandingPage:systemTools", Sort: 5, Meta: Meta{Title: "编程辅助", Icon: "ToolOutlined"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://echol.cn", Name: "https://echol.cn", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}},
|
||||||
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}},
|
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "features/server/ServerStatePage", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "CloudServerOutlined"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先创建父级菜单(ParentId = 0 的菜单)
|
// 先创建父级菜单(ParentId = 0 的菜单)
|
||||||
@@ -77,28 +77,25 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
|
|||||||
// 定义子菜单,并设置正确的ParentId
|
// 定义子菜单,并设置正确的ParentId
|
||||||
childMenus := []SysBaseMenu{
|
childMenus := []SysBaseMenu{
|
||||||
// superAdmin子菜单
|
// superAdmin子菜单
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "features/roles/RoleManagementPage", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "TeamOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "features/menus/MenuManagementPage", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "AppstoreOutlined", KeepAlive: true}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "features/apis/ApiManagementPage", Sort: 3, Meta: Meta{Title: "API管理", Icon: "ApiOutlined", KeepAlive: true}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "features/users/UserManagementPage", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "UserOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "features/dictionaries/DictionaryManagementPage", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "BookOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "features/logs/OperationLogPage", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "ProfileOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "features/params/ParamsManagementPage", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "SettingOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 8, Meta: Meta{Title: "系统配置", Icon: "operation"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "system", Name: "system", Component: "features/system/SystemConfigPage", Sort: 8, Meta: Meta{Title: "系统配置", Icon: "SettingOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 9, Meta: Meta{Title: "API Token", Icon: "key"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "apiToken", Name: "apiToken", Component: "features/tokens/ApiTokenPage", Sort: 9, Meta: Meta{Title: "API Token", Icon: "LockOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 10, Meta: Meta{Title: "登录日志", Icon: "monitor"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "loginLog", Name: "loginLog", Component: "features/logs/LoginLogPage", Sort: 10, Meta: Meta{Title: "登录日志", Icon: "FileTextOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 11, Meta: Meta{Title: "版本管理", Icon: "server"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysError", Name: "sysError", Component: "features/errors/ErrorLogPage", Sort: 11, Meta: Meta{Title: "错误日志", Icon: "BugOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 12, Meta: Meta{Title: "错误日志", Icon: "warn"}},
|
|
||||||
|
|
||||||
// common子菜单
|
// common子菜单
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["common"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 1, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["common"], Path: "upload", Name: "upload", Component: "features/media/MediaLibraryPage", Sort: 1, Meta: Meta{Title: "媒体库(上传下载)", Icon: "UploadOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["common"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 2, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["common"], Path: "breakpoint", Name: "breakpoint", Component: "features/breakpoint/BreakpointPage", Sort: 2, Meta: Meta{Title: "断点续传", Icon: "DeploymentUnitOutlined"}},
|
||||||
|
|
||||||
// systemTools子菜单
|
// systemTools子菜单
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 1, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "features/mcp/McpTestPage", Sort: 1, Meta: Meta{Title: "MCP Tools管理", Icon: "ToolOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 2, Meta: Meta{Title: "导出模板", Icon: "reading"}},
|
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "features/mcp/McpToolPage", Sort: 2, Meta: Meta{Title: "MCP Tools模板", Icon: "ToolOutlined"}},
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/mcpTest/index.vue", Sort: 3, Meta: Meta{Title: "MCP Tools管理", Icon: "partly-cloudy"}},
|
|
||||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/mcpTool/index.vue", Sort: 4, Meta: Meta{Title: "MCP Tools模板", Icon: "magnet"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建子菜单
|
// 创建子菜单
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
- `system` 系统配置
|
- `system` 系统配置
|
||||||
- `apiToken` API Token
|
- `apiToken` API Token
|
||||||
- `loginLog` 登录日志
|
- `loginLog` 登录日志
|
||||||
- `sysVersion` 版本管理
|
|
||||||
- `sysError` 错误日志
|
- `sysError` 错误日志
|
||||||
|
|
||||||
### 公共模块
|
### 公共模块
|
||||||
@@ -33,8 +32,6 @@
|
|||||||
|
|
||||||
### 编程辅助
|
### 编程辅助
|
||||||
|
|
||||||
- `formCreate` 表单生成器
|
|
||||||
- `exportTemplate` 导出模板
|
|
||||||
- `mcpTest` MCP Tools 管理
|
- `mcpTest` MCP Tools 管理
|
||||||
- `mcpTool` MCP Tools 模板
|
- `mcpTool` MCP Tools 模板
|
||||||
|
|
||||||
@@ -154,15 +151,12 @@
|
|||||||
### 已做成模块入口页
|
### 已做成模块入口页
|
||||||
|
|
||||||
- 关于系统
|
- 关于系统
|
||||||
- 版本管理
|
|
||||||
- 媒体库
|
- 媒体库
|
||||||
- 断点续传
|
- 断点续传
|
||||||
- 表单生成器
|
|
||||||
- 导出模板
|
|
||||||
- MCP Tools 管理
|
- MCP Tools 管理
|
||||||
- MCP Tools 模板
|
- MCP Tools 模板
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|
||||||
- 新后台没有修改后端协议,仍然复用原有 token、菜单、权限和接口格式。
|
- 新后台没有修改后端协议,仍然复用原有 token、菜单、权限和接口格式。
|
||||||
- 当前仍有部分研发辅助模块保留为入口页,原因不是无法实现,而是这些模块交互面较大,适合单独拆阶段继续重构。
|
- 当前仍有部分模块保留为入口页,原因不是无法实现,而是这些模块交互面较大,适合单独拆阶段继续重构。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Alert, Result, Spin } from 'antd'
|
|||||||
import { authApi, menuApi } from '@/lib/api'
|
import { authApi, menuApi } from '@/lib/api'
|
||||||
import { buildFullMenus, findDefaultRoute, flattenMenus, isExternalMenu } from '@/lib/menu'
|
import { buildFullMenus, findDefaultRoute, flattenMenus, isExternalMenu } from '@/lib/menu'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import { InitPage } from '@/features/auth/InitPage'
|
||||||
import { LoginPage } from '@/features/auth/LoginPage'
|
import { LoginPage } from '@/features/auth/LoginPage'
|
||||||
import { AdminShell } from '@/features/layout/AdminShell'
|
import { AdminShell } from '@/features/layout/AdminShell'
|
||||||
import { appRoutes } from '@/router/fsRoutes'
|
import { appRoutes } from '@/router/fsRoutes'
|
||||||
@@ -133,6 +134,7 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/init" element={<InitPage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<BootstrapGate />}>
|
<Route element={<BootstrapGate />}>
|
||||||
<Route path="/*" element={<LayoutFrame />} />
|
<Route path="/*" element={<LayoutFrame />} />
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
@@ -16,84 +15,121 @@ import {
|
|||||||
message,
|
message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { apiRegistryApi, authorityApi } from '@/lib/api'
|
import { apiRegistryApi } from '@/lib/api'
|
||||||
import { flattenAuthorities } from '@/lib/tree'
|
import type { ApiRecord, SyncApiPayload } from '@/types/system'
|
||||||
import type { ApiRecord, Authority } from '@/types/system'
|
|
||||||
|
|
||||||
const methodOptions = ['GET', 'POST', 'PUT', 'DELETE']
|
const methodOptions = [
|
||||||
|
{ value: 'POST', label: '创建', color: 'green' },
|
||||||
|
{ value: 'GET', label: '查看', color: 'blue' },
|
||||||
|
{ value: 'PUT', label: '更新', color: 'orange' },
|
||||||
|
{ value: 'DELETE', label: '删除', color: 'red' },
|
||||||
|
]
|
||||||
|
|
||||||
|
type SearchValues = {
|
||||||
|
path?: string
|
||||||
|
description?: string
|
||||||
|
apiGroup?: string
|
||||||
|
method?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiFormValues = {
|
||||||
|
path: string
|
||||||
|
description: string
|
||||||
|
apiGroup: string
|
||||||
|
method: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function methodMeta(method: string) {
|
||||||
|
return methodOptions.find((item) => item.value === method) || { label: method, color: 'default' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSyncPayload(payload?: SyncApiPayload): SyncApiPayload {
|
||||||
|
return {
|
||||||
|
newApis: payload?.newApis || [],
|
||||||
|
deleteApis: payload?.deleteApis || [],
|
||||||
|
ignoreApis: payload?.ignoreApis || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ApiManagementPage() {
|
export function ApiManagementPage() {
|
||||||
const [searchForm] = Form.useForm()
|
const [searchForm] = Form.useForm<SearchValues>()
|
||||||
const [editForm] = Form.useForm()
|
const [editForm] = Form.useForm<ApiFormValues>()
|
||||||
const [apis, setApis] = useState<ApiRecord[]>([])
|
const [apis, setApis] = useState<ApiRecord[]>([])
|
||||||
const [roles, setRoles] = useState<Authority[]>([])
|
const [selectedRows, setSelectedRows] = useState<ApiRecord[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [modalOpen, setModalOpen] = useState(false)
|
const [orderKey, setOrderKey] = useState<string>()
|
||||||
|
const [desc, setDesc] = useState<boolean>(true)
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [editingApi, setEditingApi] = useState<ApiRecord | null>(null)
|
const [editingApi, setEditingApi] = useState<ApiRecord | null>(null)
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [syncDrawerOpen, setSyncDrawerOpen] = useState(false)
|
||||||
const [activeApi, setActiveApi] = useState<ApiRecord | null>(null)
|
const [syncPayload, setSyncPayload] = useState<SyncApiPayload>(normalizeSyncPayload())
|
||||||
const [selectedRoles, setSelectedRoles] = useState<number[]>([])
|
const [syncing, setSyncing] = useState(false)
|
||||||
const [savingRoles, setSavingRoles] = useState(false)
|
const [groupOptions, setGroupOptions] = useState<Array<{ label: string; value: string }>>([])
|
||||||
|
const [groupMap, setGroupMap] = useState<Record<string, string>>({})
|
||||||
|
const [editGroupSearch, setEditGroupSearch] = useState('')
|
||||||
|
const [syncGroupSearch, setSyncGroupSearch] = useState('')
|
||||||
|
|
||||||
const roleOptions = useMemo(
|
const ensureGroupOption = useCallback((rawValue: string) => {
|
||||||
() =>
|
const value = rawValue.trim()
|
||||||
flattenAuthorities(roles).map((item) => ({
|
if (!value) {
|
||||||
label: `${' '.repeat(item.depth)}${item.authorityName} (${item.authorityId})`,
|
return ''
|
||||||
value: item.authorityId,
|
}
|
||||||
})),
|
setGroupOptions((current) => {
|
||||||
[roles],
|
if (current.some((item) => item.value === value)) {
|
||||||
)
|
return current
|
||||||
|
}
|
||||||
|
return [...current, { label: value, value }]
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}, [])
|
||||||
|
|
||||||
const apiGroupOptions = useMemo(
|
const loadGroupOptions = useCallback(async () => {
|
||||||
() =>
|
const response = await apiRegistryApi.getApiGroups()
|
||||||
Array.from(new Set(apis.map((item) => item.apiGroup)))
|
setGroupOptions(response.data.groups.map((item) => ({ label: item, value: item })))
|
||||||
.filter(Boolean)
|
setGroupMap(response.data.apiGroupMap)
|
||||||
.map((group) => ({
|
}, [])
|
||||||
label: group,
|
|
||||||
value: group,
|
|
||||||
})),
|
|
||||||
[apis],
|
|
||||||
)
|
|
||||||
|
|
||||||
const reloadApis = useCallback(async () => {
|
const reloadApis = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const [apiRes, roleRes] = await Promise.all([
|
const apiRes = await apiRegistryApi.getApiList({
|
||||||
apiRegistryApi.getApiList({
|
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
orderKey,
|
||||||
|
desc,
|
||||||
...searchForm.getFieldsValue(),
|
...searchForm.getFieldsValue(),
|
||||||
}),
|
})
|
||||||
authorityApi.getAuthorityList(),
|
|
||||||
])
|
|
||||||
setApis(apiRes.data.list)
|
setApis(apiRes.data.list)
|
||||||
setTotal(apiRes.data.total)
|
setTotal(apiRes.data.total)
|
||||||
setRoles(roleRes.data)
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [page, pageSize, searchForm])
|
}, [desc, orderKey, page, pageSize, searchForm])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadApis()
|
void reloadApis()
|
||||||
}, [reloadApis])
|
}, [reloadApis])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadGroupOptions()
|
||||||
|
}, [loadGroupOptions])
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingApi(null)
|
setEditingApi(null)
|
||||||
editForm.resetFields()
|
editForm.resetFields()
|
||||||
editForm.setFieldsValue({ method: 'POST' })
|
editForm.setFieldsValue({ method: 'POST', path: '', apiGroup: '', description: '' })
|
||||||
setModalOpen(true)
|
setDrawerOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (record: ApiRecord) => {
|
const openEdit = async (record: ApiRecord) => {
|
||||||
const response = await apiRegistryApi.getApiById(record.ID)
|
const response = await apiRegistryApi.getApiById(record.ID)
|
||||||
setEditingApi(response.data.api)
|
setEditingApi(response.data.api)
|
||||||
editForm.setFieldsValue(response.data.api)
|
editForm.setFieldsValue(response.data.api)
|
||||||
setModalOpen(true)
|
setDrawerOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveApi = async () => {
|
const saveApi = async () => {
|
||||||
@@ -101,17 +137,15 @@ export function ApiManagementPage() {
|
|||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
if (editingApi) {
|
if (editingApi) {
|
||||||
await apiRegistryApi.updateApi({
|
await apiRegistryApi.updateApi({ ID: editingApi.ID, ...values })
|
||||||
ID: editingApi.ID,
|
|
||||||
...values,
|
|
||||||
})
|
|
||||||
message.success('接口已更新')
|
message.success('接口已更新')
|
||||||
} else {
|
} else {
|
||||||
await apiRegistryApi.createApi(values)
|
await apiRegistryApi.createApi(values)
|
||||||
message.success('接口已创建')
|
message.success('接口已创建,请到角色管理页分配权限')
|
||||||
}
|
}
|
||||||
setModalOpen(false)
|
setDrawerOpen(false)
|
||||||
reloadApis()
|
await reloadApis()
|
||||||
|
await loadGroupOptions()
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -120,64 +154,143 @@ export function ApiManagementPage() {
|
|||||||
const deleteApi = (record: ApiRecord) => {
|
const deleteApi = (record: ApiRecord) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `删除接口 ${record.path}`,
|
title: `删除接口 ${record.path}`,
|
||||||
|
content: '此操作会删除当前 API 在所有角色下的权限关系。',
|
||||||
okButtonProps: { danger: true },
|
okButtonProps: { danger: true },
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await apiRegistryApi.deleteApi({ ID: record.ID })
|
await apiRegistryApi.deleteApi({ ID: record.ID })
|
||||||
message.success('接口已删除')
|
message.success('接口已删除')
|
||||||
reloadApis()
|
if (apis.length === 1 && page > 1) {
|
||||||
|
setPage((current) => current - 1)
|
||||||
|
} else {
|
||||||
|
await reloadApis()
|
||||||
|
}
|
||||||
|
await loadGroupOptions()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRoleDrawer = async (record: ApiRecord) => {
|
const batchDeleteApis = () => {
|
||||||
const response = await apiRegistryApi.getApiRoles(record.path, record.method)
|
Modal.confirm({
|
||||||
setActiveApi(record)
|
title: '批量删除接口',
|
||||||
setSelectedRoles(response.data)
|
content: `当前将删除 ${selectedRows.length} 条接口记录,是否继续?`,
|
||||||
setDrawerOpen(true)
|
okButtonProps: { danger: true },
|
||||||
|
onOk: async () => {
|
||||||
|
await apiRegistryApi.deleteApisByIds(selectedRows.map((item) => item.ID))
|
||||||
|
message.success('批量删除成功')
|
||||||
|
setSelectedRows([])
|
||||||
|
if (apis.length === selectedRows.length && page > 1) {
|
||||||
|
setPage((current) => current - 1)
|
||||||
|
} else {
|
||||||
|
await reloadApis()
|
||||||
|
}
|
||||||
|
await loadGroupOptions()
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveRoles = async () => {
|
const refreshCasbin = () => {
|
||||||
if (!activeApi) {
|
Modal.confirm({
|
||||||
|
title: '刷新 Casbin 缓存',
|
||||||
|
content: '确定立即刷新权限缓存吗?',
|
||||||
|
onOk: async () => {
|
||||||
|
await apiRegistryApi.freshCasbin()
|
||||||
|
message.success('Casbin 缓存已刷新')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSyncDrawer = async () => {
|
||||||
|
const response = await apiRegistryApi.syncApi()
|
||||||
|
const nextPayload = normalizeSyncPayload(response.data)
|
||||||
|
nextPayload.newApis = nextPayload.newApis.map((item) => ({
|
||||||
|
...item,
|
||||||
|
apiGroup: item.apiGroup || groupMap[item.path.split('/')[1]] || '',
|
||||||
|
}))
|
||||||
|
setSyncPayload(nextPayload)
|
||||||
|
setSyncDrawerOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoreSyncApi = async (record: ApiRecord, flag: boolean) => {
|
||||||
|
await apiRegistryApi.ignoreApi({ path: record.path, method: record.method, flag })
|
||||||
|
message.success(flag ? '已加入忽略列表' : '已取消忽略')
|
||||||
|
setSyncPayload((current) => {
|
||||||
|
if (flag) {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
newApis: current.newApis.filter((item) => !(item.path === record.path && item.method === record.method)),
|
||||||
|
ignoreApis: [...current.ignoreApis, record],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
ignoreApis: current.ignoreApis.filter((item) => !(item.path === record.path && item.method === record.method)),
|
||||||
|
newApis: [...current.newApis, record],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSingleSyncApi = async (record: ApiRecord) => {
|
||||||
|
if (!record.apiGroup) {
|
||||||
|
message.error('请先填写 API 分组')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSavingRoles(true)
|
if (!record.description) {
|
||||||
|
message.error('请先填写 API 描述')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await apiRegistryApi.createApi(record)
|
||||||
|
message.success('接口已新增')
|
||||||
|
setSyncPayload((current) => ({
|
||||||
|
...current,
|
||||||
|
newApis: current.newApis.filter((item) => !(item.path === record.path && item.method === record.method)),
|
||||||
|
}))
|
||||||
|
await reloadApis()
|
||||||
|
await loadGroupOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSyncPayload = async () => {
|
||||||
|
if (syncPayload.newApis.some((item) => !item.apiGroup || !item.description)) {
|
||||||
|
message.error('存在未填写分组或描述的 API,无法确认同步')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSyncing(true)
|
||||||
try {
|
try {
|
||||||
await apiRegistryApi.setApiRoles({
|
await apiRegistryApi.enterSyncApi(syncPayload)
|
||||||
path: activeApi.path,
|
message.success('API 同步完成')
|
||||||
method: activeApi.method,
|
setSyncDrawerOpen(false)
|
||||||
authorityIds: selectedRoles,
|
await reloadApis()
|
||||||
})
|
await loadGroupOptions()
|
||||||
message.success('接口角色关系已更新')
|
|
||||||
setDrawerOpen(false)
|
|
||||||
} finally {
|
} finally {
|
||||||
setSavingRoles(false)
|
setSyncing(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<ApiRecord> = [
|
const columns: ColumnsType<ApiRecord> = [
|
||||||
{ title: 'ID', dataIndex: 'ID', width: 80 },
|
{ title: 'ID', dataIndex: 'ID', width: 80, sorter: true },
|
||||||
{ title: '路径', dataIndex: 'path', width: 260 },
|
{ title: 'API 路径', dataIndex: 'path', width: 260, sorter: true },
|
||||||
{ title: '分组', dataIndex: 'apiGroup', width: 160 },
|
{ title: 'API 分组', dataIndex: 'apiGroup', width: 180, sorter: true },
|
||||||
{ title: '描述', dataIndex: 'description', width: 240 },
|
{ title: 'API 简介', dataIndex: 'description', width: 220, sorter: true },
|
||||||
{
|
{
|
||||||
title: '方法',
|
title: '请求',
|
||||||
dataIndex: 'method',
|
dataIndex: 'method',
|
||||||
width: 100,
|
width: 120,
|
||||||
render: (value: string) => <Tag color={value === 'GET' ? 'blue' : value === 'POST' ? 'green' : value === 'PUT' ? 'orange' : 'red'}>{value}</Tag>,
|
sorter: true,
|
||||||
|
render: (value: string) => {
|
||||||
|
const meta = methodMeta(value)
|
||||||
|
return <Tag color={meta.color}>{`${value} / ${meta.label}`}</Tag>
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 220,
|
width: 160,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" onClick={() => openEdit(record)}>
|
<Button type="link" onClick={() => openEdit(record)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" onClick={() => openRoleDrawer(record)}>
|
|
||||||
分配角色
|
|
||||||
</Button>
|
|
||||||
<Button danger type="link" onClick={() => deleteApi(record)}>
|
<Button danger type="link" onClick={() => deleteApi(record)}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
@@ -189,52 +302,60 @@ export function ApiManagementPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Card className="glass-panel page-panel">
|
<Card className="glass-panel page-panel">
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||||||
API 管理
|
API 管理
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
||||||
当前页覆盖接口目录、接口元数据和接口角色关系。
|
这里仅维护 API 目录和分组。菜单权限与 API 权限统一在角色管理中分配。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space wrap>
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await apiRegistryApi.freshCasbin()
|
|
||||||
message.success('Casbin 缓存已刷新')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
刷新 Casbin
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" onClick={openCreate}>
|
<Button type="primary" onClick={openCreate}>
|
||||||
新建接口
|
新增
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button disabled={!selectedRows.length} onClick={batchDeleteApis}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
<Button onClick={refreshCasbin}>刷新缓存</Button>
|
||||||
|
<Button onClick={() => void openSyncDrawer()}>同步 API</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Form form={searchForm} layout="vertical" onFinish={() => { setPage(1); reloadApis() }}>
|
<Form
|
||||||
<Row gutter={[16, 0]}>
|
form={searchForm}
|
||||||
<Col xs={24} md={8}>
|
layout="inline"
|
||||||
|
onFinish={() => {
|
||||||
|
setPage(1)
|
||||||
|
void reloadApis()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Form.Item name="path" label="路径">
|
<Form.Item name="path" label="路径">
|
||||||
<Input />
|
<Input placeholder="路径" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={8}>
|
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
<Input />
|
<Input placeholder="描述" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
<Form.Item name="apiGroup" label="API 分组">
|
||||||
<Col xs={24} md={4}>
|
<Select
|
||||||
<Form.Item name="apiGroup" label="分组">
|
allowClear
|
||||||
<Select allowClear options={apiGroupOptions} />
|
showSearch
|
||||||
|
style={{ width: 180 }}
|
||||||
|
options={groupOptions}
|
||||||
|
placeholder="请选择"
|
||||||
|
filterOption={(input, option) => String(option?.value || '').toLowerCase().includes(input.toLowerCase())}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
<Form.Item name="method" label="请求">
|
||||||
<Col xs={24} md={4}>
|
<Select
|
||||||
<Form.Item name="method" label="方法">
|
allowClear
|
||||||
<Select allowClear options={methodOptions.map((item) => ({ label: item, value: item }))} />
|
style={{ width: 160 }}
|
||||||
|
options={methodOptions.map((item) => ({ label: `${item.label}(${item.value})`, value: item.value }))}
|
||||||
|
placeholder="请选择"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
<Form.Item>
|
||||||
</Row>
|
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
查询
|
查询
|
||||||
@@ -243,21 +364,27 @@ export function ApiManagementPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
searchForm.resetFields()
|
searchForm.resetFields()
|
||||||
setPage(1)
|
setPage(1)
|
||||||
reloadApis()
|
void reloadApis()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="glass-panel page-panel">
|
<Card className="glass-panel page-panel">
|
||||||
<Table
|
<Table
|
||||||
rowKey="ID"
|
rowKey="ID"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={apis}
|
dataSource={apis}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1400 }}
|
||||||
|
rowSelection={{
|
||||||
|
onChange: (_, rows) => setSelectedRows(rows),
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -268,50 +395,231 @@ export function ApiManagementPage() {
|
|||||||
setPageSize(nextPageSize)
|
setPageSize(nextPageSize)
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onChange={(_, __, sorter) => {
|
||||||
|
if (Array.isArray(sorter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!sorter.order || !sorter.field) {
|
||||||
|
setOrderKey(undefined)
|
||||||
|
setDesc(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOrderKey(sorter.field === 'ID' ? 'id' : String(sorter.field))
|
||||||
|
setDesc(sorter.order === 'descend')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
open={modalOpen}
|
open={drawerOpen}
|
||||||
title={editingApi ? '编辑接口' : '新建接口'}
|
title={editingApi ? `编辑 API · ${editingApi.path}` : '新增 API'}
|
||||||
onCancel={() => setModalOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
onOk={saveApi}
|
width={520}
|
||||||
confirmLoading={saving}
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={saving} onClick={saveApi}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message="新增 API 后不会自动授权角色。正确流程是:新建 API -> 新建菜单 -> 到角色管理页统一分配菜单和 API 权限。"
|
||||||
|
/>
|
||||||
<Form form={editForm} layout="vertical">
|
<Form form={editForm} layout="vertical">
|
||||||
<Form.Item name="path" label="路径" rules={[{ required: true, message: '请输入路径' }]}>
|
<Form.Item name="path" label="路径" rules={[{ required: true, message: '请输入路径' }]}>
|
||||||
<Input placeholder="/api/example" />
|
<Input placeholder="/api/example" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="描述" rules={[{ required: true, message: '请输入描述' }]}>
|
<Form.Item name="method" label="请求" rules={[{ required: true, message: '请选择请求方式' }]}>
|
||||||
<Input />
|
<Select options={methodOptions.map((item) => ({ label: `${item.label}(${item.value})`, value: item.value }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="apiGroup" label="分组" rules={[{ required: true, message: '请输入分组' }]}>
|
<Form.Item name="apiGroup" label="API 分组" rules={[{ required: true, message: '请选择或输入 API 分组' }]}>
|
||||||
<Input />
|
<Select
|
||||||
|
showSearch
|
||||||
|
placeholder="输入后会先模糊搜索;若不存在,按回车创建新分组"
|
||||||
|
options={groupOptions}
|
||||||
|
filterOption={(input, option) => String(option?.value || '').toLowerCase().includes(input.toLowerCase())}
|
||||||
|
onSearch={setEditGroupSearch}
|
||||||
|
onInputKeyDown={(event) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const createdGroup = ensureGroupOption(editGroupSearch)
|
||||||
|
if (createdGroup) {
|
||||||
|
editForm.setFieldValue('apiGroup', createdGroup)
|
||||||
|
setEditGroupSearch('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="method" label="方法" rules={[{ required: true, message: '请选择方法' }]}>
|
<Form.Item name="description" label="API 简介" rules={[{ required: true, message: '请输入 API 简介' }]}>
|
||||||
<Select options={methodOptions.map((item) => ({ label: item, value: item }))} />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Drawer>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={syncDrawerOpen}
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setSyncDrawerOpen(false)}
|
||||||
title={activeApi ? `接口角色分配 · ${activeApi.path}` : '接口角色分配'}
|
width={1080}
|
||||||
width={520}
|
title="同步路由"
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={savingRoles} onClick={saveRoles}>
|
<Space>
|
||||||
保存
|
<Button onClick={() => setSyncDrawerOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={syncing} onClick={() => void submitSyncPayload()}>
|
||||||
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||||
mode="multiple"
|
<Alert
|
||||||
style={{ width: '100%' }}
|
type="warning"
|
||||||
value={selectedRoles}
|
showIcon
|
||||||
options={roleOptions}
|
message="同步 API 会把路由变更写入接口表。新增路由需要先补齐分组和描述;忽略路由不会参与同步。"
|
||||||
onChange={setSelectedRoles}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Typography.Title level={5}>新增路由</Typography.Title>
|
||||||
|
<Table
|
||||||
|
rowKey={(record) => `${record.path}::${record.method}`}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={syncPayload.newApis}
|
||||||
|
columns={[
|
||||||
|
{ title: 'API 路径', dataIndex: 'path', width: 220 },
|
||||||
|
{
|
||||||
|
title: 'API 分组',
|
||||||
|
width: 220,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Select
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
value={record.apiGroup}
|
||||||
|
options={groupOptions}
|
||||||
|
filterOption={(input, option) => String(option?.value || '').toLowerCase().includes(input.toLowerCase())}
|
||||||
|
onSearch={setSyncGroupSearch}
|
||||||
|
onInputKeyDown={(event) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const createdGroup = ensureGroupOption(syncGroupSearch)
|
||||||
|
if (!createdGroup) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSyncGroupSearch('')
|
||||||
|
setSyncPayload((current) => ({
|
||||||
|
...current,
|
||||||
|
newApis: current.newApis.map((item) =>
|
||||||
|
item.path === record.path && item.method === record.method ? { ...item, apiGroup: createdGroup } : item,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSyncPayload((current) => ({
|
||||||
|
...current,
|
||||||
|
newApis: current.newApis.map((item) =>
|
||||||
|
item.path === record.path && item.method === record.method ? { ...item, apiGroup: value } : item,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'API 简介',
|
||||||
|
width: 240,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Input
|
||||||
|
value={record.description}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value
|
||||||
|
setSyncPayload((current) => ({
|
||||||
|
...current,
|
||||||
|
newApis: current.newApis.map((item) =>
|
||||||
|
item.path === record.path && item.method === record.method ? { ...item, description: value } : item,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '请求',
|
||||||
|
width: 140,
|
||||||
|
render: (_, record) => {
|
||||||
|
const meta = methodMeta(record.method)
|
||||||
|
return <Tag color={meta.color}>{`${record.method} / ${meta.label}`}</Tag>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button type="link" onClick={() => void createSingleSyncApi(record)}>
|
||||||
|
单条新增
|
||||||
|
</Button>
|
||||||
|
<Button type="link" onClick={() => void ignoreSyncApi(record, true)}>
|
||||||
|
忽略
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Title level={5}>已删除路由</Typography.Title>
|
||||||
|
<Table
|
||||||
|
rowKey={(record) => `${record.path}::${record.method}`}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={syncPayload.deleteApis}
|
||||||
|
columns={[
|
||||||
|
{ title: 'API 路径', dataIndex: 'path' },
|
||||||
|
{ title: 'API 分组', dataIndex: 'apiGroup' },
|
||||||
|
{ title: 'API 简介', dataIndex: 'description' },
|
||||||
|
{
|
||||||
|
title: '请求',
|
||||||
|
render: (_, record) => {
|
||||||
|
const meta = methodMeta(record.method)
|
||||||
|
return <Tag color={meta.color}>{`${record.method} / ${meta.label}`}</Tag>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Title level={5}>忽略路由</Typography.Title>
|
||||||
|
<Table
|
||||||
|
rowKey={(record) => `${record.path}::${record.method}`}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={syncPayload.ignoreApis}
|
||||||
|
columns={[
|
||||||
|
{ title: 'API 路径', dataIndex: 'path' },
|
||||||
|
{ title: 'API 分组', dataIndex: 'apiGroup' },
|
||||||
|
{ title: 'API 简介', dataIndex: 'description' },
|
||||||
|
{
|
||||||
|
title: '请求',
|
||||||
|
render: (_, record) => {
|
||||||
|
const meta = methodMeta(record.method)
|
||||||
|
return <Tag color={meta.color}>{`${record.method} / ${meta.label}`}</Tag>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button type="link" onClick={() => void ignoreSyncApi(record, false)}>
|
||||||
|
取消忽略
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
305
web-admin/src/features/auth/InitPage.tsx
Normal file
305
web-admin/src/features/auth/InitPage.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Alert, Button, Card, Form, Input, Result, Select, Space, Spin, Typography, message } from 'antd'
|
||||||
|
import { initApi } from '@/lib/api'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import type { InitCheckResult, InitDBPayload } from '@/types/system'
|
||||||
|
|
||||||
|
const dbTypeOptions: Array<{ label: string; value: InitDBPayload['dbType'] }> = [
|
||||||
|
{ label: 'MySQL', value: 'mysql' },
|
||||||
|
{ label: 'PostgreSQL', value: 'pgsql' },
|
||||||
|
{ label: 'SQLite', value: 'sqlite' },
|
||||||
|
{ label: 'MSSQL', value: 'mssql' },
|
||||||
|
]
|
||||||
|
|
||||||
|
type InitState = 'checking' | 'required' | 'ready'
|
||||||
|
|
||||||
|
const dbDefaults: Record<InitDBPayload['dbType'], Partial<InitDBPayload>> = {
|
||||||
|
mysql: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '3306',
|
||||||
|
},
|
||||||
|
pgsql: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '5432',
|
||||||
|
template: 'template1',
|
||||||
|
},
|
||||||
|
sqlite: {
|
||||||
|
dbPath: 'db',
|
||||||
|
},
|
||||||
|
mssql: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '1433',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDbDefaults(currentValues: InitDBPayload, nextDbType: InitDBPayload['dbType']): InitDBPayload {
|
||||||
|
return {
|
||||||
|
...currentValues,
|
||||||
|
...dbDefaults[nextDbType],
|
||||||
|
dbType: nextDbType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const token = useAuthStore((state) => state.token)
|
||||||
|
const [form] = Form.useForm<InitDBPayload>()
|
||||||
|
const [state, setState] = useState<InitState>('checking')
|
||||||
|
const [checkingError, setCheckingError] = useState<string | null>(null)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const dbType = Form.useWatch('dbType', form) ?? 'mysql'
|
||||||
|
|
||||||
|
const checkInitialization = async () => {
|
||||||
|
try {
|
||||||
|
const response = await initApi.checkDB()
|
||||||
|
const data: InitCheckResult = response.data
|
||||||
|
|
||||||
|
if (data.needInit) {
|
||||||
|
setState('required')
|
||||||
|
} else {
|
||||||
|
setState('ready')
|
||||||
|
}
|
||||||
|
setCheckingError(null)
|
||||||
|
} catch (error) {
|
||||||
|
const messageText = error instanceof Error ? error.message : '初始化状态检测失败'
|
||||||
|
setCheckingError(messageText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void checkInitialization()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const helperText = useMemo(() => {
|
||||||
|
switch (dbType) {
|
||||||
|
case 'mysql':
|
||||||
|
return '将创建数据库并写回 MySQL 连接配置。'
|
||||||
|
case 'pgsql':
|
||||||
|
return '默认使用 PostgreSQL 公共库建库,可按需指定 template。'
|
||||||
|
case 'sqlite':
|
||||||
|
return '将创建本地 SQLite 数据库文件,并写回 sqlite 配置。'
|
||||||
|
case 'mssql':
|
||||||
|
return '将按当前连接信息接入 MSSQL,并写回 mssql 配置。'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}, [dbType])
|
||||||
|
|
||||||
|
const onDbTypeChange = (nextDbType: InitDBPayload['dbType']) => {
|
||||||
|
const currentValues = form.getFieldsValue()
|
||||||
|
form.setFieldsValue(applyDbDefaults(currentValues, nextDbType))
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
const values = await form.validateFields()
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
await initApi.initDB(values)
|
||||||
|
message.success('初始化完成,请使用 admin 账户登录')
|
||||||
|
navigate('/login', { replace: true })
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'checking') {
|
||||||
|
return (
|
||||||
|
<div className="login-shell">
|
||||||
|
<section className="login-hero">
|
||||||
|
<span className="capsule">Project Bootstrap</span>
|
||||||
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
|
正在检测
|
||||||
|
<br />
|
||||||
|
项目初始化状态
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
|
||||||
|
初始化页只在数据库尚未建立、配置尚未回写时使用。检测完成后会自动切换到对应状态。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="login-form-wrap">
|
||||||
|
<Card className="glass-panel login-card" bordered={false}>
|
||||||
|
<div className="fullscreen-status" style={{ minHeight: 360 }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<span>正在检测初始化状态...</span>
|
||||||
|
{checkingError ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="检测失败"
|
||||||
|
description={checkingError}
|
||||||
|
action={
|
||||||
|
<Button size="small" onClick={() => void checkInitialization()}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'ready') {
|
||||||
|
return (
|
||||||
|
<div className="login-shell">
|
||||||
|
<section className="login-hero">
|
||||||
|
<span className="capsule">Project Bootstrap</span>
|
||||||
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
|
项目已经完成
|
||||||
|
<br />
|
||||||
|
首次初始化
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
|
||||||
|
当前服务已经具备数据库连接和基础管理员账号,不需要再次执行初始化。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="login-form-wrap">
|
||||||
|
<Card className="glass-panel login-card" bordered={false}>
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title="无需再次初始化"
|
||||||
|
subTitle="如果需要调整系统配置,请登录后台后在系统配置页修改。"
|
||||||
|
extra={[
|
||||||
|
<Button key="login" type="primary" onClick={() => navigate(token ? '/' : '/login', { replace: true })}>
|
||||||
|
{token ? '进入后台' : '前往登录'}
|
||||||
|
</Button>,
|
||||||
|
<Button key="check" onClick={() => void checkInitialization()}>
|
||||||
|
重新检测
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-shell">
|
||||||
|
<section className="login-hero">
|
||||||
|
<span className="capsule">Project Bootstrap</span>
|
||||||
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
|
首次启动先完成
|
||||||
|
<br />
|
||||||
|
项目初始化
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
|
||||||
|
这里负责创建主业务数据库、写回当前服务配置,并生成默认管理员账号。初始化完成后,再进入后台登录。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="login-form-wrap">
|
||||||
|
<Card className="glass-panel login-card" bordered={false}>
|
||||||
|
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||||||
|
初始化项目
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph className="text-muted" style={{ marginBottom: 20 }}>
|
||||||
|
默认管理员账号固定为 `admin`,密码使用下面填写的管理员密码。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
{checkingError ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="状态检测存在异常"
|
||||||
|
description={checkingError}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
adminPassword: '',
|
||||||
|
dbType: 'mysql',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '3306',
|
||||||
|
dbName: 'go_web_template',
|
||||||
|
userName: 'root',
|
||||||
|
dbPath: 'db',
|
||||||
|
template: 'template1',
|
||||||
|
}}
|
||||||
|
onFinish={submit}
|
||||||
|
>
|
||||||
|
<Form.Item name="dbType" label="数据库类型" rules={[{ required: true, message: '请选择数据库类型' }]}>
|
||||||
|
<Select options={dbTypeOptions} onChange={onDbTypeChange} />
|
||||||
|
</Form.Item>
|
||||||
|
<Typography.Paragraph className="text-muted" style={{ marginTop: -8, marginBottom: 16 }}>
|
||||||
|
{helperText}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<div className="init-form-grid">
|
||||||
|
{dbType === 'sqlite' ? (
|
||||||
|
<Form.Item
|
||||||
|
name="dbPath"
|
||||||
|
label="数据库目录"
|
||||||
|
rules={[{ required: true, message: '请输入 SQLite 数据库目录' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如 db" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form.Item name="host" label="数据库地址" rules={[{ required: true, message: '请输入数据库地址' }]}>
|
||||||
|
<Input placeholder="例如 127.0.0.1" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="port" label="数据库端口" rules={[{ required: true, message: '请输入数据库端口' }]}>
|
||||||
|
<Input placeholder="例如 3306" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="userName"
|
||||||
|
label="数据库用户名"
|
||||||
|
rules={[{ required: true, message: '请输入数据库用户名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如 root" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="password" label="数据库密码">
|
||||||
|
<Input.Password placeholder="请输入数据库密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Form.Item name="dbName" label="数据库名" rules={[{ required: true, message: '请输入数据库名' }]}>
|
||||||
|
<Input placeholder={dbType === 'sqlite' ? '例如 go_web_template' : '例如 go_web_template'} />
|
||||||
|
</Form.Item>
|
||||||
|
{dbType === 'pgsql' ? (
|
||||||
|
<Form.Item name="template" label="建库模板">
|
||||||
|
<Input placeholder="默认 template1" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name="adminPassword"
|
||||||
|
label="管理员密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入管理员密码' },
|
||||||
|
{ min: 6, message: '管理员密码至少 6 位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="初始化后 admin 账户使用此密码登录" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 20 }}
|
||||||
|
message="初始化完成后,后端会立即持有数据库连接,并将连接配置写回当前配置文件。"
|
||||||
|
/>
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||||
|
开始初始化
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => void checkInitialization()}>重新检测</Button>
|
||||||
|
<Button onClick={() => navigate('/login', { replace: true })}>返回登录页</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { Alert, Button, Card, Form, Input, Typography, message } from 'antd'
|
import { Alert, Button, Card, Form, Input, Result, Spin, Typography, message } from 'antd'
|
||||||
import { authApi, menuApi } from '@/lib/api'
|
import { authApi, initApi, menuApi } from '@/lib/api'
|
||||||
import { buildFullMenus, findDefaultRoute } from '@/lib/menu'
|
import { buildFullMenus, findDefaultRoute } from '@/lib/menu'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import type { CaptchaInfo } from '@/types/system'
|
import type { CaptchaInfo, InitCheckResult } from '@/types/system'
|
||||||
|
|
||||||
type LoginForm = {
|
type LoginForm = {
|
||||||
username: string
|
username: string
|
||||||
@@ -34,6 +34,9 @@ export function LoginPage() {
|
|||||||
const [captcha, setCaptcha] = useState<CaptchaInfo | null>(null)
|
const [captcha, setCaptcha] = useState<CaptchaInfo | null>(null)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [loadCaptchaError, setLoadCaptchaError] = useState<string | null>(null)
|
const [loadCaptchaError, setLoadCaptchaError] = useState<string | null>(null)
|
||||||
|
const [checkingInit, setCheckingInit] = useState(true)
|
||||||
|
const [needInit, setNeedInit] = useState(false)
|
||||||
|
const [loadInitError, setLoadInitError] = useState<string | null>(null)
|
||||||
|
|
||||||
const redirectTarget = useMemo(() => {
|
const redirectTarget = useMemo(() => {
|
||||||
const state = location.state as { redirectTo?: string } | null
|
const state = location.state as { redirectTo?: string } | null
|
||||||
@@ -51,8 +54,38 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkInitialization = async () => {
|
||||||
|
try {
|
||||||
|
setCheckingInit(true)
|
||||||
|
const response = await initApi.checkDB()
|
||||||
|
const data: InitCheckResult = response.data
|
||||||
|
setNeedInit(data.needInit)
|
||||||
|
setLoadInitError(null)
|
||||||
|
return data.needInit
|
||||||
|
} catch (error) {
|
||||||
|
const messageText = error instanceof Error ? error.message : '初始化状态检测失败'
|
||||||
|
setLoadInitError(messageText)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setCheckingInit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCaptcha()
|
let ignore = false
|
||||||
|
|
||||||
|
const prepare = async () => {
|
||||||
|
const required = await checkInitialization()
|
||||||
|
if (!required && !ignore) {
|
||||||
|
await fetchCaptcha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepare()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ignore = true
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -87,10 +120,84 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkingInit) {
|
||||||
return (
|
return (
|
||||||
<div className="login-shell">
|
<div className="login-shell">
|
||||||
<section className="login-hero">
|
<section className="login-hero">
|
||||||
<span className="capsule">Gin-Vue-Admin · React 重设计版</span>
|
<span className="capsule">Gin-React-Admin · React 重设计版</span>
|
||||||
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
|
登录前先检查
|
||||||
|
<br />
|
||||||
|
项目初始化状态
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
|
||||||
|
如果当前服务还没有建立数据库连接,登录页会直接给出初始化入口,避免出现无法登录但原因不清楚的状态。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="login-form-wrap">
|
||||||
|
<Card className="glass-panel login-card" bordered={false}>
|
||||||
|
<div className="fullscreen-status" style={{ minHeight: 360 }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<span>正在检测初始化状态...</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needInit) {
|
||||||
|
return (
|
||||||
|
<div className="login-shell">
|
||||||
|
<section className="login-hero">
|
||||||
|
<span className="capsule">Gin-React-Admin · React 重设计版</span>
|
||||||
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
|
当前项目还没有
|
||||||
|
<br />
|
||||||
|
完成首次初始化
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
|
||||||
|
后端已经暴露公开初始化接口。先创建数据库并写回配置,再使用管理员账号登录后台。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="login-form-wrap">
|
||||||
|
<Card className="glass-panel login-card" bordered={false}>
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title="检测到项目尚未初始化"
|
||||||
|
subTitle="当前登录流程不可用。请先进入初始化页,完成数据库和管理员账号初始化。"
|
||||||
|
extra={[
|
||||||
|
<Button key="init" type="primary" onClick={() => navigate('/init', { replace: true })}>
|
||||||
|
前往初始化
|
||||||
|
</Button>,
|
||||||
|
<Button key="retry" onClick={() => void checkInitialization()}>
|
||||||
|
重新检测
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{loadInitError ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="初始化状态检测异常"
|
||||||
|
description={loadInitError}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-shell">
|
||||||
|
<section className="login-hero">
|
||||||
|
<span className="capsule">Gin-React-Admin · React 重设计版</span>
|
||||||
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
<div style={{ maxWidth: 620, marginTop: 72 }}>
|
||||||
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
|
||||||
把现有后端协议
|
把现有后端协议
|
||||||
@@ -110,6 +217,20 @@ export function LoginPage() {
|
|||||||
<Typography.Paragraph className="text-muted">
|
<Typography.Paragraph className="text-muted">
|
||||||
首版默认沿用原系统认证协议,避免后端改造。
|
首版默认沿用原系统认证协议,避免后端改造。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
{loadInitError ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="初始化状态检测失败"
|
||||||
|
description={loadInitError}
|
||||||
|
action={
|
||||||
|
<Button size="small" onClick={() => void checkInitialization()}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{loadCaptchaError ? (
|
{loadCaptchaError ? (
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|||||||
@@ -42,18 +42,18 @@ export const moduleCatalog: Record<string, ModuleDescriptor> = {
|
|||||||
title: '菜单管理',
|
title: '菜单管理',
|
||||||
group: '平台治理',
|
group: '平台治理',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
summary: '维护后台菜单树、路由属性、按钮定义和角色关联。',
|
summary: '维护后台菜单树、路由属性、参数和按钮定义。',
|
||||||
features: ['菜单树编辑', '路由路径维护', '角色分配', '按钮权限入口'],
|
features: ['菜单树编辑', '路由路径维护', '参数配置', '按钮定义'],
|
||||||
endpoints: ['/menu/getBaseMenuTree', '/menu/addBaseMenu', '/menu/updateBaseMenu', '/menu/setMenuRoles'],
|
endpoints: ['/menu/getMenuList', '/menu/addBaseMenu', '/menu/updateBaseMenu'],
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
name: 'api',
|
name: 'api',
|
||||||
title: 'API 管理',
|
title: 'API 管理',
|
||||||
group: '平台治理',
|
group: '平台治理',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
summary: '管理接口目录、接口角色授权和 Casbin 刷新。',
|
summary: '管理接口目录、同步结果、分组和 Casbin 刷新。',
|
||||||
features: ['接口列表', '接口增删改', '角色授权', '接口分组'],
|
features: ['接口列表', '接口增删改', '接口同步', '接口分组'],
|
||||||
endpoints: ['/api/getApiList', '/api/createApi', '/api/updateApi', '/api/setApiRoles'],
|
endpoints: ['/api/getApiList', '/api/createApi', '/api/updateApi', '/api/syncApi'],
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
@@ -118,15 +118,6 @@ export const moduleCatalog: Record<string, ModuleDescriptor> = {
|
|||||||
features: ['登录结果', '失败原因', '设备信息'],
|
features: ['登录结果', '失败原因', '设备信息'],
|
||||||
endpoints: ['/sysLoginLog/getLoginLogList', '/sysLoginLog/deleteLoginLogByIds'],
|
endpoints: ['/sysLoginLog/getLoginLogList', '/sysLoginLog/deleteLoginLogByIds'],
|
||||||
},
|
},
|
||||||
sysVersion: {
|
|
||||||
name: 'sysVersion',
|
|
||||||
title: '版本管理',
|
|
||||||
group: '运维',
|
|
||||||
status: 'partial',
|
|
||||||
summary: '围绕版本导出、导入和回滚的发布管理模块。',
|
|
||||||
features: ['版本列表', '版本导出', '版本同步'],
|
|
||||||
endpoints: ['/sysVersion/getSysVersionList', '/sysVersion/exportVersion', '/sysVersion/importVersion'],
|
|
||||||
},
|
|
||||||
sysError: {
|
sysError: {
|
||||||
name: 'sysError',
|
name: 'sysError',
|
||||||
title: '错误日志',
|
title: '错误日志',
|
||||||
@@ -168,27 +159,9 @@ export const moduleCatalog: Record<string, ModuleDescriptor> = {
|
|||||||
title: '编程辅助',
|
title: '编程辅助',
|
||||||
group: '研发辅助',
|
group: '研发辅助',
|
||||||
status: 'partial',
|
status: 'partial',
|
||||||
summary: '聚合表单设计、导出模板和 MCP 管理等研发辅助能力。',
|
summary: '聚合 MCP 管理等研发辅助能力。',
|
||||||
features: ['表单设计', '导出模板', 'MCP 管理'],
|
features: ['MCP 管理', 'MCP 模板'],
|
||||||
endpoints: ['/sysExportTemplate/getSysExportTemplateList', '/mcp/status'],
|
endpoints: ['/mcp/status', '/mcp/tools', '/mcp/createTool'],
|
||||||
},
|
|
||||||
formCreate: {
|
|
||||||
name: 'formCreate',
|
|
||||||
title: '表单生成器',
|
|
||||||
group: '研发辅助',
|
|
||||||
status: 'partial',
|
|
||||||
summary: '拖拽式表单构建入口。',
|
|
||||||
features: ['表单设计'],
|
|
||||||
endpoints: [],
|
|
||||||
},
|
|
||||||
exportTemplate: {
|
|
||||||
name: 'exportTemplate',
|
|
||||||
title: '导出模板',
|
|
||||||
group: '研发辅助',
|
|
||||||
status: 'partial',
|
|
||||||
summary: '维护 Excel 导入导出模板。',
|
|
||||||
features: ['模板管理', 'SQL 预览'],
|
|
||||||
endpoints: ['/sysExportTemplate/getSysExportTemplateList', '/sysExportTemplate/previewSQL'],
|
|
||||||
},
|
},
|
||||||
mcpTest: {
|
mcpTest: {
|
||||||
name: 'mcpTest',
|
name: 'mcpTest',
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
Cascader,
|
||||||
Card,
|
Card,
|
||||||
|
Divider,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
|
Segmented,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
@@ -16,33 +19,136 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
AppstoreOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
BugOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
DeploymentUnitOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { authorityApi, menuApi } from '@/lib/api'
|
import { authorityBtnApi, menuApi } from '@/lib/api'
|
||||||
import { flattenAuthorities, flattenMenusForOptions } from '@/lib/tree'
|
import {
|
||||||
import type { Authority, MenuNode } from '@/types/system'
|
buildMenuComponentOptions,
|
||||||
|
getComponentLabelByRouteName,
|
||||||
|
getComponentSuggestedRouteName,
|
||||||
|
getComponentValueByRouteName,
|
||||||
|
} from '@/features/menus/menuComponentCatalog'
|
||||||
|
import { flattenMenusForOptions } from '@/lib/tree'
|
||||||
|
import { filterRemovedMenus } from '@/lib/menu'
|
||||||
|
import type { MenuButton, MenuNode, MenuParameter } from '@/types/system'
|
||||||
|
|
||||||
|
type MenuFormValues = {
|
||||||
|
parentId: number
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
hidden: boolean
|
||||||
|
component: string
|
||||||
|
sort: number
|
||||||
|
meta: {
|
||||||
|
title: string
|
||||||
|
icon?: string
|
||||||
|
activeName?: string
|
||||||
|
defaultMenu?: boolean
|
||||||
|
closeTab?: boolean
|
||||||
|
keepAlive?: boolean
|
||||||
|
transitionType?: string
|
||||||
|
}
|
||||||
|
parameters: MenuParameter[]
|
||||||
|
menuBtn: MenuButton[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentInputMode = 'selector' | 'manual'
|
||||||
|
|
||||||
|
const componentOptions = buildMenuComponentOptions()
|
||||||
|
|
||||||
|
const iconRegistry = {
|
||||||
|
AppstoreOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
BugOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
DeploymentUnitOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconOptions = Object.entries(iconRegistry).map(([name, IconComponent]) => ({
|
||||||
|
value: name,
|
||||||
|
searchText: name.toLowerCase(),
|
||||||
|
label: (
|
||||||
|
<Space>
|
||||||
|
<IconComponent />
|
||||||
|
<span>{name}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createEmptyMenuForm(parentId = 0): MenuFormValues {
|
||||||
|
return {
|
||||||
|
parentId,
|
||||||
|
path: '',
|
||||||
|
name: '',
|
||||||
|
hidden: false,
|
||||||
|
component: '',
|
||||||
|
sort: 1,
|
||||||
|
meta: {
|
||||||
|
title: '',
|
||||||
|
icon: '',
|
||||||
|
activeName: '',
|
||||||
|
defaultMenu: false,
|
||||||
|
closeTab: false,
|
||||||
|
keepAlive: false,
|
||||||
|
transitionType: '',
|
||||||
|
},
|
||||||
|
parameters: [],
|
||||||
|
menuBtn: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMenuComponentValue(menu: Pick<MenuNode, 'name' | 'component'>) {
|
||||||
|
return getComponentValueByRouteName(menu.name) || menu.component
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMenuComponentDisplay(menu: Pick<MenuNode, 'name' | 'component'>) {
|
||||||
|
return getComponentLabelByRouteName(menu.name) || resolveMenuComponentValue(menu)
|
||||||
|
}
|
||||||
|
|
||||||
export function MenuManagementPage() {
|
export function MenuManagementPage() {
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm<MenuFormValues>()
|
||||||
const [menus, setMenus] = useState<MenuNode[]>([])
|
const [menus, setMenus] = useState<MenuNode[]>([])
|
||||||
const [roles, setRoles] = useState<Authority[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [modalOpen, setModalOpen] = useState(false)
|
const [editorOpen, setEditorOpen] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [editingMenu, setEditingMenu] = useState<MenuNode | null>(null)
|
const [editingMenu, setEditingMenu] = useState<MenuNode | null>(null)
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [componentInputMode, setComponentInputMode] = useState<ComponentInputMode>('selector')
|
||||||
const [activeMenu, setActiveMenu] = useState<MenuNode | null>(null)
|
const selectedComponent = Form.useWatch('component', form)
|
||||||
const [selectedRoles, setSelectedRoles] = useState<number[]>([])
|
const selectedIconName = Form.useWatch(['meta', 'icon'], form)
|
||||||
const [defaultRouterRoles, setDefaultRouterRoles] = useState<number[]>([])
|
|
||||||
const [savingRoles, setSavingRoles] = useState(false)
|
|
||||||
|
|
||||||
const roleOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
flattenAuthorities(roles).map((item) => ({
|
|
||||||
label: `${' '.repeat(item.depth)}${item.authorityName} (${item.authorityId})`,
|
|
||||||
value: item.authorityId,
|
|
||||||
})),
|
|
||||||
[roles],
|
|
||||||
)
|
|
||||||
|
|
||||||
const menuOptions = useMemo(
|
const menuOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -56,48 +162,51 @@ export function MenuManagementPage() {
|
|||||||
const reloadMenus = async () => {
|
const reloadMenus = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const [menuRes, roleRes] = await Promise.all([menuApi.getBaseMenuTree(), authorityApi.getAuthorityList()])
|
const menuRes = await menuApi.getMenuList()
|
||||||
setMenus(menuRes.data.menus)
|
setMenus(filterRemovedMenus(menuRes.data))
|
||||||
setRoles(roleRes.data)
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadMenus()
|
void reloadMenus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const openCreate = (parentId = 0) => {
|
const openCreate = (parentId = 0) => {
|
||||||
setEditingMenu(null)
|
setEditingMenu(null)
|
||||||
|
setComponentInputMode('selector')
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
form.setFieldsValue({
|
form.setFieldsValue(createEmptyMenuForm(parentId))
|
||||||
parentId,
|
setEditorOpen(true)
|
||||||
sort: 1,
|
|
||||||
hidden: false,
|
|
||||||
keepAlive: false,
|
|
||||||
closeTab: false,
|
|
||||||
})
|
|
||||||
setModalOpen(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (record: MenuNode) => {
|
const openEdit = async (record: MenuNode) => {
|
||||||
const response = await menuApi.getBaseMenuById(record.ID)
|
const response = await menuApi.getBaseMenuById(record.ID)
|
||||||
const menu = response.data.menu
|
const menu = response.data.menu
|
||||||
|
const resolvedComponent = resolveMenuComponentValue(menu)
|
||||||
setEditingMenu(menu)
|
setEditingMenu(menu)
|
||||||
|
setComponentInputMode(getComponentValueByRouteName(menu.name) || getComponentSuggestedRouteName(menu.component) ? 'selector' : 'manual')
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
parentId: menu.parentId,
|
parentId: menu.parentId,
|
||||||
title: menu.meta.title,
|
|
||||||
name: menu.name,
|
|
||||||
path: menu.path,
|
path: menu.path,
|
||||||
component: menu.component,
|
name: menu.name,
|
||||||
icon: menu.meta.icon,
|
hidden: Boolean(menu.hidden),
|
||||||
|
component: resolvedComponent,
|
||||||
sort: menu.sort,
|
sort: menu.sort,
|
||||||
hidden: menu.hidden,
|
meta: {
|
||||||
keepAlive: menu.meta.keepAlive,
|
title: menu.meta.title,
|
||||||
closeTab: menu.meta.closeTab,
|
icon: menu.meta.icon,
|
||||||
|
activeName: menu.meta.activeName,
|
||||||
|
defaultMenu: Boolean(menu.meta.defaultMenu),
|
||||||
|
closeTab: Boolean(menu.meta.closeTab),
|
||||||
|
keepAlive: Boolean(menu.meta.keepAlive),
|
||||||
|
transitionType: menu.meta.transitionType,
|
||||||
|
},
|
||||||
|
parameters: menu.parameters || [],
|
||||||
|
menuBtn: menu.menuBtn || [],
|
||||||
})
|
})
|
||||||
setModalOpen(true)
|
setEditorOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveMenu = async () => {
|
const saveMenu = async () => {
|
||||||
@@ -105,17 +214,31 @@ export function MenuManagementPage() {
|
|||||||
const payload = {
|
const payload = {
|
||||||
ID: editingMenu?.ID,
|
ID: editingMenu?.ID,
|
||||||
parentId: values.parentId || 0,
|
parentId: values.parentId || 0,
|
||||||
name: values.name,
|
|
||||||
path: values.path,
|
path: values.path,
|
||||||
|
name: values.name,
|
||||||
|
hidden: Boolean(values.hidden),
|
||||||
component: values.component,
|
component: values.component,
|
||||||
sort: values.sort,
|
sort: values.sort,
|
||||||
hidden: Boolean(values.hidden),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: values.title,
|
title: values.meta.title,
|
||||||
icon: values.icon,
|
icon: values.meta.icon,
|
||||||
keepAlive: Boolean(values.keepAlive),
|
activeName: values.meta.activeName,
|
||||||
closeTab: Boolean(values.closeTab),
|
defaultMenu: Boolean(values.meta.defaultMenu),
|
||||||
|
closeTab: Boolean(values.meta.closeTab),
|
||||||
|
keepAlive: Boolean(values.meta.keepAlive),
|
||||||
|
transitionType: values.meta.transitionType,
|
||||||
},
|
},
|
||||||
|
parameters: (values.parameters || []).map((item) => ({
|
||||||
|
ID: item.ID,
|
||||||
|
type: item.type,
|
||||||
|
key: item.key,
|
||||||
|
value: item.value,
|
||||||
|
})),
|
||||||
|
menuBtn: (values.menuBtn || []).map((item) => ({
|
||||||
|
ID: item.ID,
|
||||||
|
name: item.name,
|
||||||
|
desc: item.desc,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -125,10 +248,10 @@ export function MenuManagementPage() {
|
|||||||
message.success('菜单已更新')
|
message.success('菜单已更新')
|
||||||
} else {
|
} else {
|
||||||
await menuApi.addBaseMenu(payload)
|
await menuApi.addBaseMenu(payload)
|
||||||
message.success('菜单已创建')
|
message.success('菜单已创建,请到角色管理分配菜单权限')
|
||||||
}
|
}
|
||||||
setModalOpen(false)
|
setEditorOpen(false)
|
||||||
reloadMenus()
|
await reloadMenus()
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -137,39 +260,29 @@ export function MenuManagementPage() {
|
|||||||
const deleteMenu = (record: MenuNode) => {
|
const deleteMenu = (record: MenuNode) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `删除菜单 ${record.meta.title}`,
|
title: `删除菜单 ${record.meta.title}`,
|
||||||
content: '如果该菜单已被角色使用,删除前请先调整菜单授权。',
|
content: '此操作会删除所有角色下的当前菜单关系,请确认后继续。',
|
||||||
okButtonProps: { danger: true },
|
okButtonProps: { danger: true },
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await menuApi.deleteBaseMenu(record.ID)
|
await menuApi.deleteBaseMenu(record.ID)
|
||||||
message.success('菜单已删除')
|
message.success('菜单已删除')
|
||||||
reloadMenus()
|
await reloadMenus()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRoleDrawer = async (record: MenuNode) => {
|
const removeMenuButton = async (index: number) => {
|
||||||
const response = await menuApi.getMenuRoles(record.ID)
|
const currentButtons = form.getFieldValue('menuBtn') || []
|
||||||
setActiveMenu(record)
|
const target = currentButtons[index]
|
||||||
setSelectedRoles(response.data.authorityIds)
|
if (!target) {
|
||||||
setDefaultRouterRoles(response.data.defaultRouterAuthorityIds)
|
|
||||||
setDrawerOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRoles = async () => {
|
|
||||||
if (!activeMenu) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSavingRoles(true)
|
if (target.ID) {
|
||||||
try {
|
await authorityBtnApi.canRemoveAuthorityBtn(target.ID)
|
||||||
await menuApi.setMenuRoles({
|
|
||||||
menuId: activeMenu.ID,
|
|
||||||
authorityIds: selectedRoles,
|
|
||||||
})
|
|
||||||
message.success('菜单角色关系已更新')
|
|
||||||
setDrawerOpen(false)
|
|
||||||
} finally {
|
|
||||||
setSavingRoles(false)
|
|
||||||
}
|
}
|
||||||
|
form.setFieldValue(
|
||||||
|
'menuBtn',
|
||||||
|
currentButtons.filter((_: MenuButton, currentIndex: number) => currentIndex !== index),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<MenuNode> = [
|
const columns: ColumnsType<MenuNode> = [
|
||||||
@@ -179,23 +292,36 @@ export function MenuManagementPage() {
|
|||||||
width: 180,
|
width: 180,
|
||||||
render: (_, record) => record.meta.title,
|
render: (_, record) => record.meta.title,
|
||||||
},
|
},
|
||||||
{ title: '路由 Name', dataIndex: 'name', width: 140 },
|
|
||||||
{ title: '路由 Path', dataIndex: 'path', width: 180 },
|
|
||||||
{ title: '组件路径', dataIndex: 'component', width: 280 },
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '图标',
|
||||||
width: 180,
|
width: 140,
|
||||||
|
render: (_, record) => record.meta.icon || '-',
|
||||||
|
},
|
||||||
|
{ title: '路由 Name', dataIndex: 'name', width: 160 },
|
||||||
|
{ title: '路由 Path', dataIndex: 'path', width: 180 },
|
||||||
|
{ title: '父节点', dataIndex: 'parentId', width: 100 },
|
||||||
|
{ title: '排序', dataIndex: 'sort', width: 90 },
|
||||||
|
{
|
||||||
|
title: '组件路径',
|
||||||
|
width: 320,
|
||||||
|
render: (_, record) => resolveMenuComponentDisplay(record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '显示配置',
|
||||||
|
width: 280,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Tag color={record.hidden ? 'default' : 'green'}>{record.hidden ? '隐藏' : '显示'}</Tag>
|
<Tag color={record.hidden ? 'default' : 'green'}>{record.hidden ? '隐藏' : '显示'}</Tag>
|
||||||
{record.meta.keepAlive ? <Tag color="blue">KeepAlive</Tag> : null}
|
{record.meta.keepAlive ? <Tag color="blue">KeepAlive</Tag> : null}
|
||||||
|
{record.meta.closeTab ? <Tag color="orange">CloseTab</Tag> : null}
|
||||||
|
{record.meta.defaultMenu ? <Tag color="purple">基础页</Tag> : null}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 260,
|
width: 200,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
@@ -205,9 +331,6 @@ export function MenuManagementPage() {
|
|||||||
<Button type="link" onClick={() => openEdit(record)}>
|
<Button type="link" onClick={() => openEdit(record)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" onClick={() => openRoleDrawer(record)}>
|
|
||||||
分配角色
|
|
||||||
</Button>
|
|
||||||
<Button danger type="link" onClick={() => deleteMenu(record)}>
|
<Button danger type="link" onClick={() => deleteMenu(record)}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
@@ -219,13 +342,19 @@ export function MenuManagementPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Card className="glass-panel page-panel">
|
<Card className="glass-panel page-panel">
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="菜单页只负责维护菜单结构。角色授权统一放到角色管理中处理:先新建 API,再新建菜单,最后到角色管理分配菜单和 API 权限。"
|
||||||
|
/>
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||||||
菜单管理
|
菜单管理
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
||||||
当前页直接维护后端基础菜单树,保证新 React 后台与原权限模型保持一致。
|
对齐原版工作流,直接维护基础菜单树、菜单参数和可控按钮定义。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" onClick={() => openCreate()}>
|
<Button type="primary" onClick={() => openCreate()}>
|
||||||
@@ -239,98 +368,251 @@ export function MenuManagementPage() {
|
|||||||
dataSource={menus}
|
dataSource={menus}
|
||||||
expandable={{ defaultExpandAllRows: true }}
|
expandable={{ defaultExpandAllRows: true }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 1300 }}
|
scroll={{ x: 1700 }}
|
||||||
/>
|
/>
|
||||||
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={modalOpen}
|
|
||||||
title={editingMenu ? '编辑菜单' : '新建菜单'}
|
|
||||||
onCancel={() => setModalOpen(false)}
|
|
||||||
onOk={saveMenu}
|
|
||||||
confirmLoading={saving}
|
|
||||||
width={760}
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical">
|
|
||||||
<Form.Item name="parentId" label="父菜单">
|
|
||||||
<Select options={[{ label: '根菜单', value: 0 }, ...menuOptions]} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="title" label="展示名称" rules={[{ required: true, message: '请输入展示名称' }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item
|
|
||||||
name="name"
|
|
||||||
label="路由 Name"
|
|
||||||
rules={[{ required: true, message: '请输入路由 Name' }]}
|
|
||||||
style={{ width: '50%' }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="path"
|
|
||||||
label="路由 Path"
|
|
||||||
rules={[{ required: true, message: '请输入路由 Path' }]}
|
|
||||||
style={{ width: '50%' }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Space.Compact>
|
|
||||||
<Form.Item
|
|
||||||
name="component"
|
|
||||||
label="组件路径"
|
|
||||||
rules={[{ required: true, message: '请输入组件路径' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:view/superAdmin/user/user.vue" />
|
|
||||||
</Form.Item>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item name="icon" label="图标名" style={{ width: '60%' }}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="sort" label="排序" style={{ width: '40%' }}>
|
|
||||||
<InputNumber style={{ width: '100%' }} min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
</Space.Compact>
|
|
||||||
<Space size="large">
|
|
||||||
<Form.Item name="hidden" label="隐藏" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="keepAlive" label="KeepAlive" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="closeTab" label="自动关闭 Tab" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={editorOpen}
|
||||||
onClose={() => setDrawerOpen(false)}
|
title={editingMenu ? `编辑菜单 · ${editingMenu.meta.title}` : '新增菜单'}
|
||||||
title={activeMenu ? `菜单角色分配 · ${activeMenu.meta.title}` : '菜单角色分配'}
|
onClose={() => setEditorOpen(false)}
|
||||||
width={520}
|
width={920}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={savingRoles} onClick={saveRoles}>
|
<Space>
|
||||||
|
<Button onClick={() => setEditorOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={saving} onClick={saveMenu}>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{defaultRouterRoles.length ? (
|
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
message={`有 ${defaultRouterRoles.length} 个角色将当前菜单设为默认首页,调整前请确认首页策略。`}
|
message="文件路径支持组件选择器和手动输入两种模式。新增菜单后,请到角色管理页继续分配菜单权限。"
|
||||||
/>
|
/>
|
||||||
) : null}
|
<Form form={form} layout="vertical">
|
||||||
<Select
|
<Typography.Title level={5}>基础信息</Typography.Title>
|
||||||
mode="multiple"
|
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 16 }}>
|
||||||
|
<Typography.Text type="secondary">文件路径模式</Typography.Text>
|
||||||
|
<Segmented<ComponentInputMode>
|
||||||
|
value={componentInputMode}
|
||||||
|
options={[
|
||||||
|
{ label: '组件选择器', value: 'selector' },
|
||||||
|
{ label: '手动输入', value: 'manual' },
|
||||||
|
]}
|
||||||
|
onChange={(value) => setComponentInputMode(value)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
{componentInputMode === 'selector' ? (
|
||||||
|
<Form.Item label="文件路径" required>
|
||||||
|
<Cascader
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
value={selectedRoles}
|
options={componentOptions}
|
||||||
options={roleOptions}
|
value={(selectedComponent || '').split('/').filter(Boolean)}
|
||||||
onChange={setSelectedRoles}
|
onChange={(value) => {
|
||||||
|
const componentValue = value.join('/')
|
||||||
|
form.setFieldValue('component', componentValue)
|
||||||
|
if (!editingMenu) {
|
||||||
|
const suggestedRouteName = getComponentSuggestedRouteName(componentValue)
|
||||||
|
if (suggestedRouteName && !form.getFieldValue('name')) {
|
||||||
|
form.setFieldValue('name', suggestedRouteName)
|
||||||
|
form.setFieldValue('path', suggestedRouteName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="请选择页面组件"
|
||||||
/>
|
/>
|
||||||
|
<Form.Item name="component" hidden rules={[{ required: true, message: '请输入文件路径' }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item name="component" label="文件路径" rules={[{ required: true, message: '请输入文件路径' }]}>
|
||||||
|
<Input placeholder="例如 features/users/UserManagementPage" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item
|
||||||
|
className="config-grid-span-full"
|
||||||
|
name={['meta', 'title']}
|
||||||
|
label="展示名称"
|
||||||
|
rules={[{ required: true, message: '请输入菜单展示名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入菜单展示名称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="name" label="路由 Name" rules={[{ required: true, message: '请输入路由 Name' }]}>
|
||||||
|
<Input
|
||||||
|
placeholder="唯一英文字符串"
|
||||||
|
onChange={(event) => {
|
||||||
|
const currentPath = form.getFieldValue('path')
|
||||||
|
if (!editingMenu || currentPath === form.getFieldValue('name')) {
|
||||||
|
form.setFieldValue('path', event.target.value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="path" label="路由 Path" rules={[{ required: true, message: '请输入路由 Path' }]}>
|
||||||
|
<Input placeholder="建议与路由 Name 保持一致" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title level={5}>路由配置</Typography.Title>
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item name="parentId" label="父节点">
|
||||||
|
<Select options={[{ label: '根目录', value: 0 }, ...menuOptions]} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="sort" label="排序标记">
|
||||||
|
<InputNumber style={{ width: '100%' }} min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title level={5}>显示设置</Typography.Title>
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item name={['meta', 'icon']} label="图标">
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
placeholder="请选择图标"
|
||||||
|
options={iconOptions}
|
||||||
|
filterOption={(input, option) => String((option as { searchText?: string })?.searchText || '').includes(input.toLowerCase())}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="hidden" label="是否隐藏" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
{selectedIconName && selectedIconName in iconRegistry ? (
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Tag icon={(() => {
|
||||||
|
const PreviewIcon = iconRegistry[selectedIconName as keyof typeof iconRegistry]
|
||||||
|
return <PreviewIcon />
|
||||||
|
})()}>
|
||||||
|
{selectedIconName}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title level={5}>高级配置</Typography.Title>
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item name={['meta', 'activeName']} label="高亮菜单">
|
||||||
|
<Input placeholder="为空时默认使用当前路由 Name" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['meta', 'transitionType']} label="路由切换动画">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
options={[
|
||||||
|
{ label: '淡入淡出', value: 'fade' },
|
||||||
|
{ label: '滑动', value: 'slide' },
|
||||||
|
{ label: '缩放', value: 'zoom' },
|
||||||
|
{ label: '无动画', value: 'none' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Space size="large" wrap>
|
||||||
|
<Form.Item name={['meta', 'keepAlive']} label="KeepAlive" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['meta', 'closeTab']} label="CloseTab" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['meta', 'defaultMenu']} label="基础页面" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div className="section-heading" style={{ marginBottom: 12 }}>
|
||||||
|
<Typography.Title level={5} style={{ marginBottom: 0 }}>
|
||||||
|
菜单参数配置
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
<Form.List name="parameters">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<Card key={field.key} size="small" className="config-array-item">
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item
|
||||||
|
name={[field.name, 'type']}
|
||||||
|
label="参数类型"
|
||||||
|
rules={[{ required: true, message: '请选择参数类型' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: 'query', value: 'query' },
|
||||||
|
{ label: 'params', value: 'params' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.name, 'key']}
|
||||||
|
label="参数 Key"
|
||||||
|
rules={[{ required: true, message: '请输入参数 Key' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item className="config-grid-span-full" name={[field.name, 'value']} label="参数值">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Button danger type="link" icon={<DeleteOutlined />} onClick={() => remove(field.name)}>
|
||||||
|
删除参数
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => add({ type: 'query', key: '', value: '' })}
|
||||||
|
>
|
||||||
|
新增菜单参数
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div className="section-heading" style={{ marginBottom: 12 }}>
|
||||||
|
<Typography.Title level={5} style={{ marginBottom: 0 }}>
|
||||||
|
可控按钮配置
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
<Form.List name="menuBtn">
|
||||||
|
{(fields, { add }) => (
|
||||||
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Card key={field.key} size="small" className="config-array-item">
|
||||||
|
<div className="config-form-grid">
|
||||||
|
<Form.Item
|
||||||
|
name={[field.name, 'name']}
|
||||||
|
label="按钮名称"
|
||||||
|
rules={[{ required: true, message: '请输入按钮名称' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={[field.name, 'desc']} label="按钮备注">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Button danger type="link" icon={<DeleteOutlined />} onClick={() => void removeMenuButton(index)}>
|
||||||
|
删除按钮
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={() => add({ name: '', desc: '' })}>
|
||||||
|
新增可控按钮
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ComponentTreeNode = {
|
|||||||
|
|
||||||
const componentOptions: ComponentOption[] = [
|
const componentOptions: ComponentOption[] = [
|
||||||
{ value: 'features/dashboard/DashboardPage', label: 'DashboardPage · 仪表盘', routeName: 'dashboard' },
|
{ value: 'features/dashboard/DashboardPage', label: 'DashboardPage · 仪表盘', routeName: 'dashboard' },
|
||||||
|
{ value: 'features/discovery/ModuleLandingPage:about', label: 'ModuleLandingPage · 关于系统', routeName: 'about' },
|
||||||
{ value: 'features/roles/RoleManagementPage', label: 'RoleManagementPage · 角色管理', routeName: 'authority' },
|
{ value: 'features/roles/RoleManagementPage', label: 'RoleManagementPage · 角色管理', routeName: 'authority' },
|
||||||
{ value: 'features/menus/MenuManagementPage', label: 'MenuManagementPage · 菜单管理', routeName: 'menu' },
|
{ value: 'features/menus/MenuManagementPage', label: 'MenuManagementPage · 菜单管理', routeName: 'menu' },
|
||||||
{ value: 'features/apis/ApiManagementPage', label: 'ApiManagementPage · API 管理', routeName: 'api' },
|
{ value: 'features/apis/ApiManagementPage', label: 'ApiManagementPage · API 管理', routeName: 'api' },
|
||||||
@@ -54,17 +55,14 @@ const componentOptions: ComponentOption[] = [
|
|||||||
label: 'ModuleLandingPage · 编程辅助',
|
label: 'ModuleLandingPage · 编程辅助',
|
||||||
routeName: 'systemTools',
|
routeName: 'systemTools',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: 'features/discovery/ModuleLandingPage:exportTemplate',
|
|
||||||
label: 'ModuleLandingPage · 导出模板',
|
|
||||||
routeName: 'exportTemplate',
|
|
||||||
},
|
|
||||||
{ value: 'features/mcp/McpTestPage', label: 'McpTestPage · MCP Tools 管理', routeName: 'mcpTest' },
|
{ value: 'features/mcp/McpTestPage', label: 'McpTestPage · MCP Tools 管理', routeName: 'mcpTest' },
|
||||||
{ value: 'features/mcp/McpToolPage', label: 'McpToolPage · MCP Tools 模板', routeName: 'mcpTool' },
|
{ value: 'features/mcp/McpToolPage', label: 'McpToolPage · MCP Tools 模板', routeName: 'mcpTool' },
|
||||||
{ value: 'features/media/MediaLibraryPage', label: 'MediaLibraryPage · 媒体库', routeName: 'upload' },
|
{ value: 'features/media/MediaLibraryPage', label: 'MediaLibraryPage · 媒体库', routeName: 'upload' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const componentRouteNameMap = new Map(componentOptions.map((item) => [item.value, item.routeName]))
|
const componentRouteNameMap = new Map(componentOptions.map((item) => [item.value, item.routeName]))
|
||||||
|
const routeNameComponentMap = new Map(componentOptions.map((item) => [item.routeName, item.value]))
|
||||||
|
const routeNameLabelMap = new Map(componentOptions.map((item) => [item.routeName, item.label]))
|
||||||
|
|
||||||
export function buildMenuComponentOptions(): ComponentTreeNode[] {
|
export function buildMenuComponentOptions(): ComponentTreeNode[] {
|
||||||
const result: ComponentTreeNode[] = []
|
const result: ComponentTreeNode[] = []
|
||||||
@@ -105,3 +103,11 @@ export function isKnownMenuComponent(componentValue: string) {
|
|||||||
export function getComponentSuggestedRouteName(componentValue: string) {
|
export function getComponentSuggestedRouteName(componentValue: string) {
|
||||||
return componentRouteNameMap.get(componentValue)
|
return componentRouteNameMap.get(componentValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getComponentValueByRouteName(routeName: string) {
|
||||||
|
return routeNameComponentMap.get(routeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getComponentLabelByRouteName(routeName: string) {
|
||||||
|
return routeNameLabelMap.get(routeName)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -17,58 +18,150 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import type { DataNode } from 'antd/es/tree'
|
import type { DataNode } from 'antd/es/tree'
|
||||||
import { apiRegistryApi, authorityApi, casbinApi, menuApi, userApi } from '@/lib/api'
|
import { authorityBtnApi, apiRegistryApi, authorityApi, casbinApi, menuApi, userApi } from '@/lib/api'
|
||||||
import { collectCheckedLeafMenus, flattenAuthorities } from '@/lib/tree'
|
import { filterRemovedMenus } from '@/lib/menu'
|
||||||
import type { ApiRecord, Authority, MenuNode, UserInfo } from '@/types/system'
|
import { collectMenusByIds, flattenAuthorities } from '@/lib/tree'
|
||||||
|
import type { ApiRecord, Authority, MenuButton, MenuNode, UserInfo } from '@/types/system'
|
||||||
|
|
||||||
type PermissionTab = 'menus' | 'apis' | 'users'
|
type PermissionTab = 'menus' | 'apis'
|
||||||
|
|
||||||
function mapMenusToTree(menus: MenuNode[]): DataNode[] {
|
type RoleFormValues = {
|
||||||
return menus.map((menu) => ({
|
authorityId: number
|
||||||
key: menu.ID,
|
authorityName: string
|
||||||
title: menu.meta.title,
|
parentId: number
|
||||||
children: mapMenusToTree(menu.children || []),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapApisToTree(apis: ApiRecord[]): DataNode[] {
|
type UserSearchValues = {
|
||||||
const grouped = apis.reduce<Record<string, ApiRecord[]>>((accumulator, api) => {
|
username: string
|
||||||
if (!accumulator[api.apiGroup]) {
|
nickName: string
|
||||||
accumulator[api.apiGroup] = []
|
}
|
||||||
|
|
||||||
|
function filterMenuTree(nodes: MenuNode[], keyword: string): MenuNode[] {
|
||||||
|
const normalized = keyword.trim()
|
||||||
|
if (!normalized) {
|
||||||
|
return nodes
|
||||||
}
|
}
|
||||||
accumulator[api.apiGroup].push(api)
|
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
const children = filterMenuTree(node.children || [], normalized)
|
||||||
|
if (node.meta.title.includes(normalized) || node.name.includes(normalized) || children.length > 0) {
|
||||||
|
return [{ ...node, children }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiTree(apis: ApiRecord[], nameKeyword: string, pathKeyword: string): DataNode[] {
|
||||||
|
const filteredApis = apis.filter((api) => {
|
||||||
|
const matchedName = !nameKeyword || api.description.includes(nameKeyword)
|
||||||
|
const matchedPath = !pathKeyword || api.path.includes(pathKeyword)
|
||||||
|
return matchedName && matchedPath
|
||||||
|
})
|
||||||
|
|
||||||
|
const grouped = filteredApis.reduce<Record<string, ApiRecord[]>>((accumulator, api) => {
|
||||||
|
const group = api.apiGroup || '未分组'
|
||||||
|
if (!accumulator[group]) {
|
||||||
|
accumulator[group] = []
|
||||||
|
}
|
||||||
|
accumulator[group].push(api)
|
||||||
return accumulator
|
return accumulator
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
return Object.entries(grouped).map(([group, items]) => ({
|
return Object.entries(grouped).map(([group, items]) => ({
|
||||||
key: `group:${group}`,
|
key: `group:${group}`,
|
||||||
title: `${group} 组`,
|
title: `${group}组`,
|
||||||
children: items.map((item) => ({
|
children: items.map((item) => ({
|
||||||
key: `${item.path}::${item.method}`,
|
key: `${item.path}::${item.method}`,
|
||||||
title: `${item.method} ${item.path} · ${item.description}`,
|
title: (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
|
||||||
|
<span>{item.description || `${item.method} ${item.path}`}</span>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{item.path}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMenuTree(
|
||||||
|
menus: MenuNode[],
|
||||||
|
defaultRouter: string | undefined,
|
||||||
|
onOpenButtons: (menu: MenuNode) => void,
|
||||||
|
onSetHomepage: (menu: MenuNode) => void,
|
||||||
|
): DataNode[] {
|
||||||
|
return menus.map((menu) => ({
|
||||||
|
key: menu.ID,
|
||||||
|
title: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
{defaultRouter === menu.name ? <Tag color="gold">默认首页</Tag> : null}
|
||||||
|
{menu.name && !(menu.children || []).length ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
onSetHomepage(menu)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
设为首页
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{menu.menuBtn?.length ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
onOpenButtons(menu)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
分配按钮
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: buildMenuTree(menu.children || [], defaultRouter, onOpenButtons, onSetHomepage),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
export function RoleManagementPage() {
|
export function RoleManagementPage() {
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm<RoleFormValues>()
|
||||||
|
const [userSearchForm] = Form.useForm<UserSearchValues>()
|
||||||
const [roles, setRoles] = useState<Authority[]>([])
|
const [roles, setRoles] = useState<Authority[]>([])
|
||||||
const [menuTree, setMenuTree] = useState<MenuNode[]>([])
|
const [menuTree, setMenuTree] = useState<MenuNode[]>([])
|
||||||
const [apis, setApis] = useState<ApiRecord[]>([])
|
const [apis, setApis] = useState<ApiRecord[]>([])
|
||||||
const [users, setUsers] = useState<UserInfo[]>([])
|
|
||||||
const [roleModalOpen, setRoleModalOpen] = useState(false)
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
||||||
const [savingRole, setSavingRole] = useState(false)
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [roleDrawerOpen, setRoleDrawerOpen] = useState(false)
|
||||||
|
const [permissionDrawerOpen, setPermissionDrawerOpen] = useState(false)
|
||||||
|
const [userDrawerOpen, setUserDrawerOpen] = useState(false)
|
||||||
|
const [savingRole, setSavingRole] = useState(false)
|
||||||
|
const [loadingUsers, setLoadingUsers] = useState(false)
|
||||||
|
const [savingPermission, setSavingPermission] = useState(false)
|
||||||
|
const [savingUsers, setSavingUsers] = useState(false)
|
||||||
const [activeTab, setActiveTab] = useState<PermissionTab>('menus')
|
const [activeTab, setActiveTab] = useState<PermissionTab>('menus')
|
||||||
const [editingRole, setEditingRole] = useState<Authority | null>(null)
|
const [editingRole, setEditingRole] = useState<Authority | null>(null)
|
||||||
const [copySource, setCopySource] = useState<Authority | null>(null)
|
const [copySource, setCopySource] = useState<Authority | null>(null)
|
||||||
const [activeRole, setActiveRole] = useState<Authority | null>(null)
|
const [activeRole, setActiveRole] = useState<Authority | null>(null)
|
||||||
const [menuChecked, setMenuChecked] = useState<number[]>([])
|
const [menuChecked, setMenuChecked] = useState<number[]>([])
|
||||||
|
const [menuHalfChecked, setMenuHalfChecked] = useState<number[]>([])
|
||||||
const [defaultRouter, setDefaultRouter] = useState<string>()
|
const [defaultRouter, setDefaultRouter] = useState<string>()
|
||||||
const [apiChecked, setApiChecked] = useState<string[]>([])
|
const [apiChecked, setApiChecked] = useState<string[]>([])
|
||||||
const [userIds, setUserIds] = useState<number[]>([])
|
const [menuFilter, setMenuFilter] = useState('')
|
||||||
const [savingPermission, setSavingPermission] = useState(false)
|
const [apiNameFilter, setApiNameFilter] = useState('')
|
||||||
|
const [apiPathFilter, setApiPathFilter] = useState('')
|
||||||
|
const [users, setUsers] = useState<UserInfo[]>([])
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = useState<Set<number>>(new Set())
|
||||||
|
const [userPage, setUserPage] = useState(1)
|
||||||
|
const [userPageSize, setUserPageSize] = useState(10)
|
||||||
|
const [userTotal, setUserTotal] = useState(0)
|
||||||
|
const [buttonModalOpen, setButtonModalOpen] = useState(false)
|
||||||
|
const [buttonMenu, setButtonMenu] = useState<MenuNode | null>(null)
|
||||||
|
const [selectedButtonIds, setSelectedButtonIds] = useState<number[]>([])
|
||||||
|
const [savingButtons, setSavingButtons] = useState(false)
|
||||||
|
|
||||||
const roleOptions = useMemo(
|
const roleOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -79,6 +172,29 @@ export function RoleManagementPage() {
|
|||||||
[roles],
|
[roles],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultRouterOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
collectMenusByIds(menuTree, new Set([...menuChecked, ...menuHalfChecked]))
|
||||||
|
.filter((menu) => menu.name && !menu.name.startsWith('http://') && !menu.name.startsWith('https://'))
|
||||||
|
.filter((menu) => !(menu.children || []).length)
|
||||||
|
.map((menu) => ({
|
||||||
|
label: menu.meta.title,
|
||||||
|
value: menu.name,
|
||||||
|
})),
|
||||||
|
[menuChecked, menuHalfChecked, menuTree],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!permissionDrawerOpen || !defaultRouter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (defaultRouterOptions.some((item) => item.value === defaultRouter)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDefaultRouter(undefined)
|
||||||
|
message.warning('当前默认首页已不在授权菜单内,已自动清空,请重新选择')
|
||||||
|
}, [defaultRouter, defaultRouterOptions, permissionDrawerOpen])
|
||||||
|
|
||||||
const reloadRoles = async () => {
|
const reloadRoles = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -90,15 +206,77 @@ export function RoleManagementPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadRoles()
|
void reloadRoles()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const openCreate = (parentId?: number) => {
|
const loadUserList = useCallback(async (roleId: number) => {
|
||||||
|
setLoadingUsers(true)
|
||||||
|
try {
|
||||||
|
const [userRes, roleUserRes] = await Promise.all([
|
||||||
|
userApi.getUserList({
|
||||||
|
page: userPage,
|
||||||
|
pageSize: userPageSize,
|
||||||
|
username: userSearchForm.getFieldValue('username') || '',
|
||||||
|
nickName: userSearchForm.getFieldValue('nickName') || '',
|
||||||
|
orderKey: 'id',
|
||||||
|
desc: true,
|
||||||
|
}),
|
||||||
|
authorityApi.getUsersByAuthorityId(roleId),
|
||||||
|
])
|
||||||
|
|
||||||
|
setUsers(userRes.data.list)
|
||||||
|
setUserTotal(userRes.data.total)
|
||||||
|
setSelectedUserIds(new Set(roleUserRes.data))
|
||||||
|
} finally {
|
||||||
|
setLoadingUsers(false)
|
||||||
|
}
|
||||||
|
}, [userPage, userPageSize, userSearchForm])
|
||||||
|
|
||||||
|
const openButtonModal = useCallback(async (menu: MenuNode) => {
|
||||||
|
if (!activeRole) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const response = await authorityBtnApi.getAuthorityBtn({
|
||||||
|
menuID: menu.ID,
|
||||||
|
authorityId: activeRole.authorityId,
|
||||||
|
})
|
||||||
|
setButtonMenu(menu)
|
||||||
|
setSelectedButtonIds(response.data.selected)
|
||||||
|
setButtonModalOpen(true)
|
||||||
|
}, [activeRole])
|
||||||
|
|
||||||
|
const filteredMenus = useMemo(() => filterMenuTree(menuTree, menuFilter), [menuFilter, menuTree])
|
||||||
|
const menuTreeData = useMemo(
|
||||||
|
() =>
|
||||||
|
buildMenuTree(
|
||||||
|
filteredMenus,
|
||||||
|
defaultRouter,
|
||||||
|
(menu) => void openButtonModal(menu),
|
||||||
|
(menu) => {
|
||||||
|
const checkedSet = new Set([...menuChecked, ...menuHalfChecked])
|
||||||
|
if (!checkedSet.has(menu.ID)) {
|
||||||
|
message.warning('请先勾选菜单,再将其设为首页')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDefaultRouter(menu.name)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[defaultRouter, filteredMenus, menuChecked, menuHalfChecked, openButtonModal],
|
||||||
|
)
|
||||||
|
const apiTreeData = useMemo(() => buildApiTree(apis, apiNameFilter, apiPathFilter), [apiNameFilter, apiPathFilter, apis])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userDrawerOpen && activeRole) {
|
||||||
|
void loadUserList(activeRole.authorityId)
|
||||||
|
}
|
||||||
|
}, [activeRole, loadUserList, userDrawerOpen])
|
||||||
|
|
||||||
|
const openCreate = (parentId = 0) => {
|
||||||
setEditingRole(null)
|
setEditingRole(null)
|
||||||
setCopySource(null)
|
setCopySource(null)
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
form.setFieldsValue({ parentId: parentId ?? 0 })
|
form.setFieldsValue({ parentId, authorityId: undefined as never, authorityName: '' })
|
||||||
setRoleModalOpen(true)
|
setRoleDrawerOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = (record: Authority) => {
|
const openEdit = (record: Authority) => {
|
||||||
@@ -109,18 +287,18 @@ export function RoleManagementPage() {
|
|||||||
authorityName: record.authorityName,
|
authorityName: record.authorityName,
|
||||||
parentId: record.parentId ?? 0,
|
parentId: record.parentId ?? 0,
|
||||||
})
|
})
|
||||||
setRoleModalOpen(true)
|
setRoleDrawerOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCopy = (record: Authority) => {
|
const openCopy = (record: Authority) => {
|
||||||
setEditingRole(null)
|
setEditingRole(null)
|
||||||
setCopySource(record)
|
setCopySource(record)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
authorityId: undefined,
|
authorityId: undefined as never,
|
||||||
authorityName: `${record.authorityName}-副本`,
|
authorityName: `${record.authorityName}-副本`,
|
||||||
parentId: record.parentId ?? 0,
|
parentId: record.parentId ?? 0,
|
||||||
})
|
})
|
||||||
setRoleModalOpen(true)
|
setRoleDrawerOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveRole = async () => {
|
const saveRole = async () => {
|
||||||
@@ -131,7 +309,7 @@ export function RoleManagementPage() {
|
|||||||
await authorityApi.copyAuthority({
|
await authorityApi.copyAuthority({
|
||||||
oldAuthorityId: copySource.authorityId,
|
oldAuthorityId: copySource.authorityId,
|
||||||
authority: {
|
authority: {
|
||||||
authorityId: values.authorityId,
|
authorityId: Number(values.authorityId),
|
||||||
authorityName: values.authorityName,
|
authorityName: values.authorityName,
|
||||||
parentId: values.parentId,
|
parentId: values.parentId,
|
||||||
},
|
},
|
||||||
@@ -147,15 +325,15 @@ export function RoleManagementPage() {
|
|||||||
message.success('角色已更新')
|
message.success('角色已更新')
|
||||||
} else {
|
} else {
|
||||||
await authorityApi.createAuthority({
|
await authorityApi.createAuthority({
|
||||||
authorityId: values.authorityId,
|
authorityId: Number(values.authorityId),
|
||||||
authorityName: values.authorityName,
|
authorityName: values.authorityName,
|
||||||
parentId: values.parentId,
|
parentId: values.parentId,
|
||||||
})
|
})
|
||||||
message.success('角色已创建')
|
message.success('角色已创建')
|
||||||
}
|
}
|
||||||
|
|
||||||
setRoleModalOpen(false)
|
setRoleDrawerOpen(false)
|
||||||
reloadRoles()
|
await reloadRoles()
|
||||||
} finally {
|
} finally {
|
||||||
setSavingRole(false)
|
setSavingRole(false)
|
||||||
}
|
}
|
||||||
@@ -164,37 +342,49 @@ export function RoleManagementPage() {
|
|||||||
const deleteRole = (record: Authority) => {
|
const deleteRole = (record: Authority) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `删除角色 ${record.authorityName}`,
|
title: `删除角色 ${record.authorityName}`,
|
||||||
content: '删除前请确认没有用户正在依赖该角色。',
|
content: '删除前请确认没有用户仍依赖当前角色。',
|
||||||
okButtonProps: { danger: true },
|
okButtonProps: { danger: true },
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await authorityApi.deleteAuthority(record.authorityId)
|
await authorityApi.deleteAuthority(record.authorityId)
|
||||||
message.success('角色已删除')
|
message.success('角色已删除')
|
||||||
reloadRoles()
|
await reloadRoles()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openPermissionDrawer = async (record: Authority) => {
|
const openPermissionDrawer = async (record: Authority) => {
|
||||||
setActiveRole(record)
|
setActiveRole(record)
|
||||||
setDrawerOpen(true)
|
setPermissionDrawerOpen(true)
|
||||||
const [menuRes, checkedMenusRes, apiRes, policyRes, userRes, roleUserRes] = await Promise.all([
|
setActiveTab('menus')
|
||||||
|
setMenuFilter('')
|
||||||
|
setApiNameFilter('')
|
||||||
|
setApiPathFilter('')
|
||||||
|
const [menuRes, checkedMenusRes, apiRes, policyRes] = await Promise.all([
|
||||||
menuApi.getBaseMenuTree(),
|
menuApi.getBaseMenuTree(),
|
||||||
menuApi.getMenuAuthority(record.authorityId),
|
menuApi.getMenuAuthority(record.authorityId),
|
||||||
apiRegistryApi.getAllApis(),
|
apiRegistryApi.getAllApis(),
|
||||||
casbinApi.getPolicyPathByAuthorityId(record.authorityId),
|
casbinApi.getPolicyPathByAuthorityId(record.authorityId),
|
||||||
userApi.getUserList({ page: 1, pageSize: 999 }),
|
|
||||||
authorityApi.getUsersByAuthorityId(record.authorityId),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
setMenuTree(menuRes.data.menus)
|
setMenuTree(filterRemovedMenus(menuRes.data.menus))
|
||||||
setMenuChecked(
|
setMenuChecked(
|
||||||
checkedMenusRes.data.menus.map((item) => Number((item as unknown as { menuId?: number; ID: number }).menuId ?? item.ID)),
|
checkedMenusRes.data.menus.map((item) =>
|
||||||
|
Number((item as unknown as { menuId?: number; ID: number }).menuId ?? item.ID),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
setMenuHalfChecked([])
|
||||||
setDefaultRouter(record.defaultRouter)
|
setDefaultRouter(record.defaultRouter)
|
||||||
setApis(apiRes.data.apis)
|
setApis(apiRes.data.apis)
|
||||||
setApiChecked(policyRes.data.paths.map((item) => `${item.path}::${item.method}`))
|
setApiChecked(policyRes.data.paths.map((item) => `${item.path}::${item.method}`))
|
||||||
setUsers(userRes.data.list)
|
}
|
||||||
setUserIds(roleUserRes.data)
|
|
||||||
|
const openUserDrawer = async (record: Authority) => {
|
||||||
|
setActiveRole(record)
|
||||||
|
setUserDrawerOpen(true)
|
||||||
|
userSearchForm.setFieldsValue({ username: '', nickName: '' })
|
||||||
|
setUserPage(1)
|
||||||
|
setUserPageSize(10)
|
||||||
|
await loadUserList(record.authorityId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveCurrentPermission = async () => {
|
const saveCurrentPermission = async () => {
|
||||||
@@ -204,7 +394,7 @@ export function RoleManagementPage() {
|
|||||||
setSavingPermission(true)
|
setSavingPermission(true)
|
||||||
try {
|
try {
|
||||||
if (activeTab === 'menus') {
|
if (activeTab === 'menus') {
|
||||||
const checkedMenus = collectCheckedLeafMenus(menuTree, new Set(menuChecked))
|
const checkedMenus = collectMenusByIds(menuTree, new Set([...menuChecked, ...menuHalfChecked]))
|
||||||
await menuApi.addMenuAuthority({
|
await menuApi.addMenuAuthority({
|
||||||
authorityId: activeRole.authorityId,
|
authorityId: activeRole.authorityId,
|
||||||
menus: checkedMenus,
|
menus: checkedMenus,
|
||||||
@@ -216,7 +406,7 @@ export function RoleManagementPage() {
|
|||||||
defaultRouter,
|
defaultRouter,
|
||||||
})
|
})
|
||||||
message.success('菜单权限已保存')
|
message.success('菜单权限已保存')
|
||||||
} else if (activeTab === 'apis') {
|
} else {
|
||||||
const selectedApis = apis
|
const selectedApis = apis
|
||||||
.filter((item) => apiChecked.includes(`${item.path}::${item.method}`))
|
.filter((item) => apiChecked.includes(`${item.path}::${item.method}`))
|
||||||
.map((item) => ({ path: item.path, method: item.method }))
|
.map((item) => ({ path: item.path, method: item.method }))
|
||||||
@@ -225,59 +415,82 @@ export function RoleManagementPage() {
|
|||||||
casbinInfos: selectedApis,
|
casbinInfos: selectedApis,
|
||||||
})
|
})
|
||||||
message.success('API 权限已保存')
|
message.success('API 权限已保存')
|
||||||
} else {
|
|
||||||
await authorityApi.setRoleUsers({
|
|
||||||
authorityId: activeRole.authorityId,
|
|
||||||
userIds,
|
|
||||||
})
|
|
||||||
message.success('角色用户关系已保存')
|
|
||||||
}
|
}
|
||||||
reloadRoles()
|
await reloadRoles()
|
||||||
} finally {
|
} finally {
|
||||||
setSavingPermission(false)
|
setSavingPermission(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuOptions = useMemo(
|
const saveRoleUsers = async () => {
|
||||||
() =>
|
if (!activeRole) {
|
||||||
collectCheckedLeafMenus(menuTree, new Set(menuChecked)).map((menu) => ({
|
return
|
||||||
label: menu.meta.title,
|
}
|
||||||
value: menu.name,
|
setSavingUsers(true)
|
||||||
})),
|
try {
|
||||||
[menuChecked, menuTree],
|
await authorityApi.setRoleUsers({
|
||||||
)
|
authorityId: activeRole.authorityId,
|
||||||
|
userIds: Array.from(selectedUserIds),
|
||||||
|
})
|
||||||
|
message.success('角色用户关系已保存')
|
||||||
|
setUserDrawerOpen(false)
|
||||||
|
} finally {
|
||||||
|
setSavingUsers(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButtons = async () => {
|
||||||
|
if (!activeRole || !buttonMenu) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSavingButtons(true)
|
||||||
|
try {
|
||||||
|
await authorityBtnApi.setAuthorityBtn({
|
||||||
|
menuID: buttonMenu.ID,
|
||||||
|
authorityId: activeRole.authorityId,
|
||||||
|
selected: selectedButtonIds,
|
||||||
|
})
|
||||||
|
message.success('按钮权限已保存')
|
||||||
|
setButtonModalOpen(false)
|
||||||
|
} finally {
|
||||||
|
setSavingButtons(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<Authority> = [
|
const columns: ColumnsType<Authority> = [
|
||||||
{
|
{
|
||||||
title: '角色 ID',
|
title: '角色 ID',
|
||||||
dataIndex: 'authorityId',
|
dataIndex: 'authorityId',
|
||||||
width: 120,
|
width: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '角色名称',
|
title: '角色名称',
|
||||||
dataIndex: 'authorityName',
|
dataIndex: 'authorityName',
|
||||||
width: 200,
|
width: 220,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '默认首页',
|
title: '默认首页',
|
||||||
dataIndex: 'defaultRouter',
|
dataIndex: 'defaultRouter',
|
||||||
width: 140,
|
width: 180,
|
||||||
render: (value: string | undefined) => value || '-',
|
render: (value: string | undefined) => value || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 320,
|
width: 420,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="link" onClick={() => openPermissionDrawer(record)}>
|
<Button type="link" onClick={() => openPermissionDrawer(record)}>
|
||||||
设置权限
|
设置权限
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="link" onClick={() => openUserDrawer(record)}>
|
||||||
|
分配给用户
|
||||||
|
</Button>
|
||||||
<Button type="link" onClick={() => openCreate(record.authorityId)}>
|
<Button type="link" onClick={() => openCreate(record.authorityId)}>
|
||||||
新增子角色
|
新增子角色
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" onClick={() => openCopy(record)}>
|
<Button type="link" onClick={() => openCopy(record)}>
|
||||||
复制
|
拷贝
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" onClick={() => openEdit(record)}>
|
<Button type="link" onClick={() => openEdit(record)}>
|
||||||
编辑
|
编辑
|
||||||
@@ -293,13 +506,15 @@ export function RoleManagementPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Card className="glass-panel page-panel">
|
<Card className="glass-panel page-panel">
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<Alert type="info" showIcon message="右上角头像支持切换角色。角色页同时负责菜单授权、API 授权和角色用户绑定。" />
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||||||
角色管理
|
角色管理
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
||||||
当前页接通角色树、菜单授权、API 授权和角色用户绑定。
|
角色树管理、权限抽屉和用户分配抽屉拆分处理,保持权限配置流程清晰。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" onClick={() => openCreate()}>
|
<Button type="primary" onClick={() => openCreate()}>
|
||||||
@@ -314,33 +529,51 @@ export function RoleManagementPage() {
|
|||||||
expandable={{ defaultExpandAllRows: true }}
|
expandable={{ defaultExpandAllRows: true }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
open={roleModalOpen}
|
open={roleDrawerOpen}
|
||||||
title={copySource ? `复制角色 · ${copySource.authorityName}` : editingRole ? '编辑角色' : '新建角色'}
|
title={copySource ? `拷贝角色 · ${copySource.authorityName}` : editingRole ? '编辑角色' : '新增角色'}
|
||||||
onCancel={() => setRoleModalOpen(false)}
|
onClose={() => setRoleDrawerOpen(false)}
|
||||||
onOk={saveRole}
|
width={520}
|
||||||
confirmLoading={savingRole}
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setRoleDrawerOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={savingRole} onClick={saveRole}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item name="parentId" label="父角色">
|
<Form.Item name="parentId" label="父级角色" rules={[{ required: true, message: '请选择父级角色' }]}>
|
||||||
<Select options={[{ label: '根角色', value: 0 }, ...roleOptions]} />
|
<Select options={[{ label: '根角色', value: 0 }, ...roleOptions]} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="authorityId" label="角色 ID" rules={[{ required: true, message: '请输入角色 ID' }]}>
|
<Form.Item
|
||||||
<Input />
|
name="authorityId"
|
||||||
|
label="角色 ID"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入角色 ID' },
|
||||||
|
{
|
||||||
|
validator: (_, value: number) =>
|
||||||
|
String(value || '').match(/^[1-9]\d*$/) ? Promise.resolve() : Promise.reject(new Error('请输入正整数')),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={Boolean(editingRole)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="authorityName" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
|
<Form.Item name="authorityName" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Drawer>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={permissionDrawerOpen}
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setPermissionDrawerOpen(false)}
|
||||||
width={900}
|
width={960}
|
||||||
title={activeRole ? `角色权限 · ${activeRole.authorityName}` : '角色权限'}
|
title={activeRole ? `角色配置 · ${activeRole.authorityName}` : '角色配置'}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={savingPermission} onClick={saveCurrentPermission}>
|
<Button type="primary" loading={savingPermission} onClick={saveCurrentPermission}>
|
||||||
保存当前标签
|
保存当前标签
|
||||||
@@ -348,50 +581,199 @@ export function RoleManagementPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as PermissionTab)}>
|
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as PermissionTab)}>
|
||||||
<Tabs.TabPane tab="菜单权限" key="menus">
|
<Tabs.TabPane tab="角色菜单" key="menus">
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
|
<Input placeholder="筛选菜单" value={menuFilter} onChange={(event) => setMenuFilter(event.target.value)} />
|
||||||
<Select
|
<Select
|
||||||
value={defaultRouter}
|
value={defaultRouter}
|
||||||
options={menuOptions}
|
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="请选择默认首页"
|
placeholder="请选择默认首页"
|
||||||
|
style={{ minWidth: 260 }}
|
||||||
|
options={defaultRouterOptions}
|
||||||
onChange={(value) => setDefaultRouter(value)}
|
onChange={(value) => setDefaultRouter(value)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="默认首页必须来自已勾选菜单。菜单节点上的“分配按钮”用于维护当前角色的按钮权限。"
|
||||||
|
/>
|
||||||
<Tree
|
<Tree
|
||||||
checkable
|
checkable
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
checkedKeys={menuChecked}
|
checkedKeys={menuChecked}
|
||||||
treeData={mapMenusToTree(menuTree)}
|
treeData={menuTreeData}
|
||||||
onCheck={(checkedKeys) => setMenuChecked((checkedKeys as number[]).map((item) => Number(item)))}
|
onCheck={(checkedKeys, info) => {
|
||||||
|
if (Array.isArray(checkedKeys)) {
|
||||||
|
setMenuChecked(checkedKeys.map((item) => Number(item)))
|
||||||
|
setMenuHalfChecked(((info as { halfCheckedKeys?: Array<string | number> }).halfCheckedKeys || []).map((item) => Number(item)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setMenuChecked(checkedKeys.checked.map((item) => Number(item)))
|
||||||
|
setMenuHalfChecked(checkedKeys.halfChecked.map((item) => Number(item)))
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab="API 权限" key="apis">
|
<Tabs.TabPane tab="角色 API" key="apis">
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
|
<Input placeholder="筛选描述" value={apiNameFilter} onChange={(event) => setApiNameFilter(event.target.value)} />
|
||||||
|
<Input placeholder="筛选路径" value={apiPathFilter} onChange={(event) => setApiPathFilter(event.target.value)} />
|
||||||
|
</div>
|
||||||
<Tree
|
<Tree
|
||||||
checkable
|
checkable
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
checkedKeys={apiChecked}
|
checkedKeys={apiChecked}
|
||||||
treeData={mapApisToTree(apis)}
|
treeData={apiTreeData}
|
||||||
onCheck={(checkedKeys) => setApiChecked((checkedKeys as string[]).filter((item) => !item.startsWith('group:')))}
|
onCheck={(checkedKeys) => {
|
||||||
|
if (Array.isArray(checkedKeys)) {
|
||||||
|
setApiChecked(checkedKeys.filter((item) => !String(item).startsWith('group:')).map(String))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setApiChecked(
|
||||||
|
checkedKeys.checked.filter((item) => !String(item).startsWith('group:')).map((item) => String(item)),
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs.TabPane>
|
</Space>
|
||||||
<Tabs.TabPane tab="角色用户" key="users">
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
value={userIds}
|
|
||||||
options={users.map((user) => ({
|
|
||||||
label: `${user.nickName} (${user.userName})`,
|
|
||||||
value: user.ID,
|
|
||||||
}))}
|
|
||||||
onChange={(value) => setUserIds(value)}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: 12 }}>
|
|
||||||
<Tag>{userIds.length} 个用户已绑定到当前角色</Tag>
|
|
||||||
</div>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
open={userDrawerOpen}
|
||||||
|
onClose={() => setUserDrawerOpen(false)}
|
||||||
|
width={900}
|
||||||
|
title={activeRole ? `分配给用户 · ${activeRole.authorityName}` : '分配给用户'}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setUserDrawerOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={savingUsers} onClick={saveRoleUsers}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="保存时会全量覆盖当前角色的用户关联关系。若用户仅剩这一个角色,被移除后其主角色不会自动调整。"
|
||||||
|
/>
|
||||||
|
<Form
|
||||||
|
form={userSearchForm}
|
||||||
|
layout="inline"
|
||||||
|
onFinish={() => {
|
||||||
|
setUserPage(1)
|
||||||
|
if (activeRole) {
|
||||||
|
void loadUserList(activeRole.authorityId)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item name="username" label="用户名">
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="nickName" label="昵称">
|
||||||
|
<Input placeholder="请输入昵称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
userSearchForm.resetFields()
|
||||||
|
setUserPage(1)
|
||||||
|
if (activeRole) {
|
||||||
|
void loadUserList(activeRole.authorityId)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Table
|
||||||
|
rowKey="ID"
|
||||||
|
loading={loadingUsers}
|
||||||
|
dataSource={users}
|
||||||
|
pagination={{
|
||||||
|
current: userPage,
|
||||||
|
pageSize: userPageSize,
|
||||||
|
total: userTotal,
|
||||||
|
showSizeChanger: true,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
setUserPage(page)
|
||||||
|
setUserPageSize(pageSize)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
rowSelection={{
|
||||||
|
preserveSelectedRowKeys: true,
|
||||||
|
selectedRowKeys: Array.from(selectedUserIds),
|
||||||
|
onSelect: (record, selected) => {
|
||||||
|
setSelectedUserIds((previous) => {
|
||||||
|
const next = new Set(previous)
|
||||||
|
if (selected) {
|
||||||
|
next.add(record.ID)
|
||||||
|
} else {
|
||||||
|
next.delete(record.ID)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSelectAll: (selected, _, changeRows) => {
|
||||||
|
setSelectedUserIds((previous) => {
|
||||||
|
const next = new Set(previous)
|
||||||
|
changeRows.forEach((user) => {
|
||||||
|
if (selected) {
|
||||||
|
next.add(user.ID)
|
||||||
|
} else {
|
||||||
|
next.delete(user.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{ title: 'ID', dataIndex: 'ID', width: 80 },
|
||||||
|
{ title: '用户名', dataIndex: 'userName', width: 160 },
|
||||||
|
{ title: '昵称', dataIndex: 'nickName', width: 160 },
|
||||||
|
{
|
||||||
|
title: '主角色',
|
||||||
|
width: 160,
|
||||||
|
render: (_, user) => user.authority?.authorityName || '-',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={buttonModalOpen}
|
||||||
|
title={buttonMenu ? `分配按钮 · ${buttonMenu.meta.title}` : '分配按钮'}
|
||||||
|
onCancel={() => setButtonModalOpen(false)}
|
||||||
|
onOk={() => void saveButtons()}
|
||||||
|
confirmLoading={savingButtons}
|
||||||
|
>
|
||||||
|
<Table<MenuButton>
|
||||||
|
rowKey="ID"
|
||||||
|
pagination={false}
|
||||||
|
dataSource={buttonMenu?.menuBtn || []}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: selectedButtonIds,
|
||||||
|
onChange: (keys) => setSelectedButtonIds(keys.map((item) => Number(item))),
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{ title: '按钮名称', dataIndex: 'name' },
|
||||||
|
{ title: '按钮备注', dataIndex: 'desc' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ img {
|
|||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(16, 37, 66, 0.96) 0%, rgba(19, 51, 84, 0.94) 100%);
|
linear-gradient(180deg, rgba(16, 37, 66, 0.96) 0%, rgba(19, 51, 84, 0.94) 100%);
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-brand {
|
.admin-brand {
|
||||||
@@ -117,6 +118,8 @@ img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-menu .ant-menu-sub.ant-menu-inline {
|
.admin-nav-menu .ant-menu-sub.ant-menu-inline {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
margin: 8px 0 12px;
|
margin: 8px 0 12px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -124,6 +127,11 @@ img {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-nav-menu .ant-menu-submenu,
|
||||||
|
.admin-nav-menu .ant-menu-submenu-inline {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-item,
|
.admin-nav-menu.ant-menu-dark .ant-menu-item,
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-title,
|
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-title,
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-item a,
|
.admin-nav-menu.ant-menu-dark .ant-menu-item a,
|
||||||
@@ -303,6 +311,12 @@ img {
|
|||||||
padding: 28px;
|
padding: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.init-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.capsule {
|
.capsule {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -319,6 +333,41 @@ img {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.admin-shell,
|
||||||
|
.login-shell,
|
||||||
|
.hero-grid,
|
||||||
|
.metric-grid,
|
||||||
|
.config-form-grid,
|
||||||
|
.server-meter-grid,
|
||||||
|
.server-runtime-grid,
|
||||||
|
.server-disk-grid,
|
||||||
|
.catalog-grid,
|
||||||
|
.init-form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero {
|
||||||
|
min-height: 320px;
|
||||||
|
padding: 40px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-card {
|
.catalog-card {
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
import type {
|
import type {
|
||||||
ApiRecord,
|
ApiRecord,
|
||||||
|
ApiGroupsPayload,
|
||||||
ApiTokenRecord,
|
ApiTokenRecord,
|
||||||
AttachmentCategory,
|
AttachmentCategory,
|
||||||
|
AuthorityButtonSelection,
|
||||||
Authority,
|
Authority,
|
||||||
CaptchaInfo,
|
CaptchaInfo,
|
||||||
Dictionary,
|
Dictionary,
|
||||||
DictionaryDetail,
|
DictionaryDetail,
|
||||||
|
InitCheckResult,
|
||||||
|
InitDBPayload,
|
||||||
LoginLog,
|
LoginLog,
|
||||||
LoginResult,
|
LoginResult,
|
||||||
McpContent,
|
McpContent,
|
||||||
@@ -19,6 +23,7 @@ import type {
|
|||||||
OperationRecord,
|
OperationRecord,
|
||||||
PagePayload,
|
PagePayload,
|
||||||
ServerState,
|
ServerState,
|
||||||
|
SyncApiPayload,
|
||||||
SysErrorRecord,
|
SysErrorRecord,
|
||||||
SysParam,
|
SysParam,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
@@ -48,6 +53,15 @@ export const authApi = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initApi = {
|
||||||
|
checkDB() {
|
||||||
|
return http.post<InitCheckResult>('/init/checkdb')
|
||||||
|
},
|
||||||
|
initDB(payload: InitDBPayload) {
|
||||||
|
return http.post<Record<string, never>>('/init/initdb', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const menuApi = {
|
export const menuApi = {
|
||||||
getMenu() {
|
getMenu() {
|
||||||
return http.post<{ menus: MenuNode[] }>('/menu/getMenu')
|
return http.post<{ menus: MenuNode[] }>('/menu/getMenu')
|
||||||
@@ -167,12 +181,27 @@ export const apiRegistryApi = {
|
|||||||
deleteApi(payload: { ID: number }) {
|
deleteApi(payload: { ID: number }) {
|
||||||
return http.post<Record<string, never>>('/api/deleteApi', payload)
|
return http.post<Record<string, never>>('/api/deleteApi', payload)
|
||||||
},
|
},
|
||||||
|
deleteApisByIds(ids: number[]) {
|
||||||
|
return http.delete<Record<string, never>>('/api/deleteApisByIds', { data: { ids } })
|
||||||
|
},
|
||||||
getApiById(id: number) {
|
getApiById(id: number) {
|
||||||
return http.post<{ api: ApiRecord }>('/api/getApiById', { ID: id })
|
return http.post<{ api: ApiRecord }>('/api/getApiById', { ID: id })
|
||||||
},
|
},
|
||||||
freshCasbin() {
|
freshCasbin() {
|
||||||
return http.get<Record<string, never>>('/api/freshCasbin')
|
return http.get<Record<string, never>>('/api/freshCasbin')
|
||||||
},
|
},
|
||||||
|
syncApi() {
|
||||||
|
return http.get<SyncApiPayload>('/api/syncApi')
|
||||||
|
},
|
||||||
|
getApiGroups() {
|
||||||
|
return http.get<ApiGroupsPayload>('/api/getApiGroups')
|
||||||
|
},
|
||||||
|
ignoreApi(payload: { path: string; method: string; flag: boolean }) {
|
||||||
|
return http.post<Record<string, never>>('/api/ignoreApi', payload)
|
||||||
|
},
|
||||||
|
enterSyncApi(payload: SyncApiPayload) {
|
||||||
|
return http.post<Record<string, never>>('/api/enterSyncApi', payload)
|
||||||
|
},
|
||||||
getApiRoles(path: string, method: string) {
|
getApiRoles(path: string, method: string) {
|
||||||
return http.get<number[]>('/api/getApiRoles', { params: { path, method } })
|
return http.get<number[]>('/api/getApiRoles', { params: { path, method } })
|
||||||
},
|
},
|
||||||
@@ -181,6 +210,20 @@ export const apiRegistryApi = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const authorityBtnApi = {
|
||||||
|
getAuthorityBtn(payload: { menuID: number; authorityId: number }) {
|
||||||
|
return http.post<AuthorityButtonSelection>('/authorityBtn/getAuthorityBtn', payload)
|
||||||
|
},
|
||||||
|
setAuthorityBtn(payload: { menuID: number; authorityId: number; selected: number[] }) {
|
||||||
|
return http.post<Record<string, never>>('/authorityBtn/setAuthorityBtn', payload)
|
||||||
|
},
|
||||||
|
canRemoveAuthorityBtn(id: number) {
|
||||||
|
return http.post<Record<string, never>>('/authorityBtn/canRemoveAuthorityBtn', undefined, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const dictionaryApi = {
|
export const dictionaryApi = {
|
||||||
getDictionaryList(params?: Record<string, unknown>) {
|
getDictionaryList(params?: Record<string, unknown>) {
|
||||||
return http.get<Dictionary[]>('/sysDictionary/getSysDictionaryList', { params })
|
return http.get<Dictionary[]>('/sysDictionary/getSysDictionaryList', { params })
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AppMenu, MenuNode } from '@/types/system'
|
import type { AppMenu, MenuNode } from '@/types/system'
|
||||||
|
|
||||||
|
const removedFrontendMenuNames = new Set(['sysVersion', 'exportTemplate', 'formCreate'])
|
||||||
|
|
||||||
export function isExternalMenu(menu: Pick<MenuNode, 'path' | 'component'>) {
|
export function isExternalMenu(menu: Pick<MenuNode, 'path' | 'component'>) {
|
||||||
return (
|
return (
|
||||||
menu.path.startsWith('http://') ||
|
menu.path.startsWith('http://') ||
|
||||||
@@ -9,6 +11,17 @@ export function isExternalMenu(menu: Pick<MenuNode, 'path' | 'component'>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterRemovedMenus<T extends MenuNode>(menus: T[]): T[] {
|
||||||
|
return menus.flatMap((menu) => {
|
||||||
|
if (removedFrontendMenuNames.has(menu.name)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = filterRemovedMenus((menu.children || []) as T[])
|
||||||
|
return [{ ...menu, children } as T]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePathSegment(path: string) {
|
function normalizePathSegment(path: string) {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return ''
|
return ''
|
||||||
@@ -22,7 +35,7 @@ export function buildFullMenus(
|
|||||||
parentPath = '',
|
parentPath = '',
|
||||||
parentName?: string,
|
parentName?: string,
|
||||||
): AppMenu[] {
|
): AppMenu[] {
|
||||||
return [...menus]
|
return [...filterRemovedMenus(menus)]
|
||||||
.sort((left, right) => left.sort - right.sort)
|
.sort((left, right) => left.sort - right.sort)
|
||||||
.map((menu) => {
|
.map((menu) => {
|
||||||
const normalized = normalizePathSegment(menu.path)
|
const normalized = normalizePathSegment(menu.path)
|
||||||
|
|||||||
@@ -24,3 +24,13 @@ export function collectCheckedLeafMenus(menus: MenuNode[], checkedKeys: Set<numb
|
|||||||
return collectCheckedLeafMenus(children, checkedKeys)
|
return collectCheckedLeafMenus(children, checkedKeys)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function collectMenusByIds(menus: MenuNode[], checkedKeys: Set<number>): MenuNode[] {
|
||||||
|
return menus.flatMap((menu) => {
|
||||||
|
const matchedChildren = collectMenusByIds(menu.children || [], checkedKeys)
|
||||||
|
if (checkedKeys.has(menu.ID)) {
|
||||||
|
return [{ ...menu, children: undefined }, ...matchedChildren]
|
||||||
|
}
|
||||||
|
return matchedChildren
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createModulePage } from '@/router/createModulePage'
|
|
||||||
|
|
||||||
export const routeMeta = {
|
|
||||||
menuName: 'sysVersion',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createModulePage('sysVersion')
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createModulePage } from '@/router/createModulePage'
|
|
||||||
|
|
||||||
export const routeMeta = {
|
|
||||||
menuName: 'exportTemplate',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createModulePage('exportTemplate')
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createModulePage } from '@/router/createModulePage'
|
|
||||||
|
|
||||||
export const routeMeta = {
|
|
||||||
menuName: 'formCreate',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createModulePage('formCreate')
|
|
||||||
@@ -42,13 +42,24 @@ export type UserInfo = BaseEntity & {
|
|||||||
export type MenuMeta = {
|
export type MenuMeta = {
|
||||||
title: string
|
title: string
|
||||||
icon?: string
|
icon?: string
|
||||||
|
activeName?: string
|
||||||
keepAlive?: boolean
|
keepAlive?: boolean
|
||||||
closeTab?: boolean
|
closeTab?: boolean
|
||||||
defaultMenu?: boolean
|
defaultMenu?: boolean
|
||||||
activeName?: string
|
|
||||||
transitionType?: string
|
transitionType?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MenuParameter = BaseEntity & {
|
||||||
|
type: 'query' | 'params'
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuButton = BaseEntity & {
|
||||||
|
name: string
|
||||||
|
desc?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type MenuNode = BaseEntity & {
|
export type MenuNode = BaseEntity & {
|
||||||
parentId: number
|
parentId: number
|
||||||
path: string
|
path: string
|
||||||
@@ -57,7 +68,8 @@ export type MenuNode = BaseEntity & {
|
|||||||
component: string
|
component: string
|
||||||
sort: number
|
sort: number
|
||||||
meta: MenuMeta
|
meta: MenuMeta
|
||||||
menuBtn?: Array<{ ID: number; name: string; desc?: string }>
|
parameters?: MenuParameter[]
|
||||||
|
menuBtn?: MenuButton[]
|
||||||
btns?: Record<string, number>
|
btns?: Record<string, number>
|
||||||
children?: MenuNode[]
|
children?: MenuNode[]
|
||||||
}
|
}
|
||||||
@@ -82,6 +94,22 @@ export type CaptchaInfo = {
|
|||||||
openCaptcha: boolean
|
openCaptcha: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InitCheckResult = {
|
||||||
|
needInit: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InitDBPayload = {
|
||||||
|
adminPassword: string
|
||||||
|
dbType: 'mysql' | 'pgsql' | 'sqlite' | 'mssql'
|
||||||
|
host?: string
|
||||||
|
port?: string
|
||||||
|
userName?: string
|
||||||
|
password?: string
|
||||||
|
dbName: string
|
||||||
|
dbPath?: string
|
||||||
|
template?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiRecord = BaseEntity & {
|
export type ApiRecord = BaseEntity & {
|
||||||
path: string
|
path: string
|
||||||
description: string
|
description: string
|
||||||
@@ -89,6 +117,21 @@ export type ApiRecord = BaseEntity & {
|
|||||||
method: string
|
method: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SyncApiPayload = {
|
||||||
|
newApis: ApiRecord[]
|
||||||
|
deleteApis: ApiRecord[]
|
||||||
|
ignoreApis: ApiRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiGroupsPayload = {
|
||||||
|
groups: string[]
|
||||||
|
apiGroupMap: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthorityButtonSelection = {
|
||||||
|
selected: number[]
|
||||||
|
}
|
||||||
|
|
||||||
export type Dictionary = BaseEntity & {
|
export type Dictionary = BaseEntity & {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
Reference in New Issue
Block a user