mirror of
https://github.com/flipped-aurora/gin-vue-admin.git
synced 2026-09-25 05:50:19 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98ccdc6295 | ||
|
|
d143b326b4 |
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -622,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
|
||||
|
||||
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
|
||||
|
||||
### **工具文件清单**
|
||||
|
||||
#### `request.js` — HTTP 请求封装(核心)
|
||||
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
|
||||
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
|
||||
- 用法:`import service from '@/utils/request'`
|
||||
|
||||
#### `date.js` — 日期格式化
|
||||
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
|
||||
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
|
||||
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
|
||||
- 用法:`import { formatTimeToStr } from '@/utils/date'`
|
||||
|
||||
#### `format.js` — 数据展示格式化(综合工具)
|
||||
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
|
||||
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
|
||||
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
|
||||
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
|
||||
- `getDictFunc(type)` — 异步获取指定类型的字典数据
|
||||
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
|
||||
- `onDownloadFile(url)` — 触发文件下载
|
||||
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
|
||||
- `CreateUUID()` — 生成 UUID v4 字符串
|
||||
- `getBaseUrl()` — 获取当前环境的 API BaseURL
|
||||
- **以上所有格式化场景优先使用此文件中的工具函数**
|
||||
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
|
||||
|
||||
#### `dictionary.js` — 字典数据获取
|
||||
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
|
||||
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
|
||||
- 用法:`import { getDict } from '@/utils/dictionary'`
|
||||
|
||||
#### `stringFun.js` — 字符串处理
|
||||
- `toUpperCase(str)` — 首字母转大写
|
||||
- `toLowerCase(str)` — 首字母转小写
|
||||
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
|
||||
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
|
||||
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
|
||||
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
|
||||
|
||||
#### `params.js` — 系统参数获取
|
||||
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
|
||||
- **获取系统配置参数时,优先使用此工具**
|
||||
- 用法:`import { getParams } from '@/utils/params'`
|
||||
|
||||
#### `bus.js` — 全局事件总线
|
||||
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
|
||||
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
|
||||
- 用法:`import { emitter } from '@/utils/bus'`
|
||||
|
||||
#### `closeThisPage.js` — 关闭当前标签页
|
||||
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
|
||||
- **在需要程序化关闭当前页面时,必须使用此工具**
|
||||
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
|
||||
|
||||
#### `downloadImg.js` — 图片下载
|
||||
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
|
||||
- **需要下载图片时,优先使用此工具**
|
||||
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
|
||||
|
||||
#### `image.js` — 图片压缩
|
||||
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
|
||||
- **上传图片前需要做压缩处理时,使用此工具**
|
||||
- 用法:`import ImageCompress from '@/utils/image'`
|
||||
|
||||
#### `event.js` — DOM 事件监听管理
|
||||
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
|
||||
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
|
||||
- **手动操作 DOM 事件时,使用此工具以确保安全性**
|
||||
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
|
||||
|
||||
#### `env.js` — 环境判断
|
||||
- `isDev` — 是否为开发环境(Boolean)
|
||||
- `isProd` — 是否为生产环境(Boolean)
|
||||
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
|
||||
- 用法:`import { isDev, isProd } from '@/utils/env'`
|
||||
|
||||
#### `doc.js` — 外部文档跳转
|
||||
- `toDoc(url)` — 在新标签页打开指定 URL
|
||||
- 用法:`import { toDoc } from '@/utils/doc'`
|
||||
|
||||
#### `fmtRouterTitle.js` — 路由标题格式化
|
||||
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
|
||||
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
|
||||
|
||||
#### `page.js` — 页面标题生成
|
||||
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
|
||||
- 用法:`import getPageTitle from '@/utils/page'`
|
||||
|
||||
#### `asyncRouter.js` — 异步路由处理
|
||||
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
|
||||
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
|
||||
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
|
||||
|
||||
#### `btnAuth.js` — 按钮权限
|
||||
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
|
||||
- **实现按钮级别权限控制时,必须使用此 Hook**
|
||||
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
|
||||
|
||||
### **使用强制要求**
|
||||
|
||||
| 场景 | 必须使用的工具 |
|
||||
|------|----------------|
|
||||
| 发送 HTTP 请求 | `@/utils/request` |
|
||||
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
|
||||
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
|
||||
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
|
||||
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
|
||||
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
|
||||
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
|
||||
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
|
||||
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
|
||||
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
|
||||
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
|
||||
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -622,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
|
||||
|
||||
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
|
||||
|
||||
### **工具文件清单**
|
||||
|
||||
#### `request.js` — HTTP 请求封装(核心)
|
||||
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
|
||||
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
|
||||
- 用法:`import service from '@/utils/request'`
|
||||
|
||||
#### `date.js` — 日期格式化
|
||||
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
|
||||
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
|
||||
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
|
||||
- 用法:`import { formatTimeToStr } from '@/utils/date'`
|
||||
|
||||
#### `format.js` — 数据展示格式化(综合工具)
|
||||
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
|
||||
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
|
||||
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
|
||||
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
|
||||
- `getDictFunc(type)` — 异步获取指定类型的字典数据
|
||||
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
|
||||
- `onDownloadFile(url)` — 触发文件下载
|
||||
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
|
||||
- `CreateUUID()` — 生成 UUID v4 字符串
|
||||
- `getBaseUrl()` — 获取当前环境的 API BaseURL
|
||||
- **以上所有格式化场景优先使用此文件中的工具函数**
|
||||
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
|
||||
|
||||
#### `dictionary.js` — 字典数据获取
|
||||
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
|
||||
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
|
||||
- 用法:`import { getDict } from '@/utils/dictionary'`
|
||||
|
||||
#### `stringFun.js` — 字符串处理
|
||||
- `toUpperCase(str)` — 首字母转大写
|
||||
- `toLowerCase(str)` — 首字母转小写
|
||||
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
|
||||
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
|
||||
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
|
||||
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
|
||||
|
||||
#### `params.js` — 系统参数获取
|
||||
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
|
||||
- **获取系统配置参数时,优先使用此工具**
|
||||
- 用法:`import { getParams } from '@/utils/params'`
|
||||
|
||||
#### `bus.js` — 全局事件总线
|
||||
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
|
||||
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
|
||||
- 用法:`import { emitter } from '@/utils/bus'`
|
||||
|
||||
#### `closeThisPage.js` — 关闭当前标签页
|
||||
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
|
||||
- **在需要程序化关闭当前页面时,必须使用此工具**
|
||||
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
|
||||
|
||||
#### `downloadImg.js` — 图片下载
|
||||
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
|
||||
- **需要下载图片时,优先使用此工具**
|
||||
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
|
||||
|
||||
#### `image.js` — 图片压缩
|
||||
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
|
||||
- **上传图片前需要做压缩处理时,使用此工具**
|
||||
- 用法:`import ImageCompress from '@/utils/image'`
|
||||
|
||||
#### `event.js` — DOM 事件监听管理
|
||||
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
|
||||
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
|
||||
- **手动操作 DOM 事件时,使用此工具以确保安全性**
|
||||
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
|
||||
|
||||
#### `env.js` — 环境判断
|
||||
- `isDev` — 是否为开发环境(Boolean)
|
||||
- `isProd` — 是否为生产环境(Boolean)
|
||||
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
|
||||
- 用法:`import { isDev, isProd } from '@/utils/env'`
|
||||
|
||||
#### `doc.js` — 外部文档跳转
|
||||
- `toDoc(url)` — 在新标签页打开指定 URL
|
||||
- 用法:`import { toDoc } from '@/utils/doc'`
|
||||
|
||||
#### `fmtRouterTitle.js` — 路由标题格式化
|
||||
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
|
||||
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
|
||||
|
||||
#### `page.js` — 页面标题生成
|
||||
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
|
||||
- 用法:`import getPageTitle from '@/utils/page'`
|
||||
|
||||
#### `asyncRouter.js` — 异步路由处理
|
||||
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
|
||||
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
|
||||
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
|
||||
|
||||
#### `btnAuth.js` — 按钮权限
|
||||
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
|
||||
- **实现按钮级别权限控制时,必须使用此 Hook**
|
||||
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
|
||||
|
||||
### **使用强制要求**
|
||||
|
||||
| 场景 | 必须使用的工具 |
|
||||
|------|----------------|
|
||||
| 发送 HTTP 请求 | `@/utils/request` |
|
||||
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
|
||||
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
|
||||
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
|
||||
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
|
||||
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
|
||||
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
|
||||
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
|
||||
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
|
||||
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
|
||||
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
|
||||
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -622,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
|
||||
|
||||
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
|
||||
|
||||
### **工具文件清单**
|
||||
|
||||
#### `request.js` — HTTP 请求封装(核心)
|
||||
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
|
||||
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
|
||||
- 用法:`import service from '@/utils/request'`
|
||||
|
||||
#### `date.js` — 日期格式化
|
||||
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
|
||||
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
|
||||
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
|
||||
- 用法:`import { formatTimeToStr } from '@/utils/date'`
|
||||
|
||||
#### `format.js` — 数据展示格式化(综合工具)
|
||||
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
|
||||
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
|
||||
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
|
||||
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
|
||||
- `getDictFunc(type)` — 异步获取指定类型的字典数据
|
||||
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
|
||||
- `onDownloadFile(url)` — 触发文件下载
|
||||
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
|
||||
- `CreateUUID()` — 生成 UUID v4 字符串
|
||||
- `getBaseUrl()` — 获取当前环境的 API BaseURL
|
||||
- **以上所有格式化场景优先使用此文件中的工具函数**
|
||||
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
|
||||
|
||||
#### `dictionary.js` — 字典数据获取
|
||||
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
|
||||
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
|
||||
- 用法:`import { getDict } from '@/utils/dictionary'`
|
||||
|
||||
#### `stringFun.js` — 字符串处理
|
||||
- `toUpperCase(str)` — 首字母转大写
|
||||
- `toLowerCase(str)` — 首字母转小写
|
||||
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
|
||||
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
|
||||
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
|
||||
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
|
||||
|
||||
#### `params.js` — 系统参数获取
|
||||
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
|
||||
- **获取系统配置参数时,优先使用此工具**
|
||||
- 用法:`import { getParams } from '@/utils/params'`
|
||||
|
||||
#### `bus.js` — 全局事件总线
|
||||
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
|
||||
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
|
||||
- 用法:`import { emitter } from '@/utils/bus'`
|
||||
|
||||
#### `closeThisPage.js` — 关闭当前标签页
|
||||
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
|
||||
- **在需要程序化关闭当前页面时,必须使用此工具**
|
||||
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
|
||||
|
||||
#### `downloadImg.js` — 图片下载
|
||||
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
|
||||
- **需要下载图片时,优先使用此工具**
|
||||
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
|
||||
|
||||
#### `image.js` — 图片压缩
|
||||
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
|
||||
- **上传图片前需要做压缩处理时,使用此工具**
|
||||
- 用法:`import ImageCompress from '@/utils/image'`
|
||||
|
||||
#### `event.js` — DOM 事件监听管理
|
||||
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
|
||||
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
|
||||
- **手动操作 DOM 事件时,使用此工具以确保安全性**
|
||||
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
|
||||
|
||||
#### `env.js` — 环境判断
|
||||
- `isDev` — 是否为开发环境(Boolean)
|
||||
- `isProd` — 是否为生产环境(Boolean)
|
||||
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
|
||||
- 用法:`import { isDev, isProd } from '@/utils/env'`
|
||||
|
||||
#### `doc.js` — 外部文档跳转
|
||||
- `toDoc(url)` — 在新标签页打开指定 URL
|
||||
- 用法:`import { toDoc } from '@/utils/doc'`
|
||||
|
||||
#### `fmtRouterTitle.js` — 路由标题格式化
|
||||
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
|
||||
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
|
||||
|
||||
#### `page.js` — 页面标题生成
|
||||
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
|
||||
- 用法:`import getPageTitle from '@/utils/page'`
|
||||
|
||||
#### `asyncRouter.js` — 异步路由处理
|
||||
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
|
||||
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
|
||||
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
|
||||
|
||||
#### `btnAuth.js` — 按钮权限
|
||||
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
|
||||
- **实现按钮级别权限控制时,必须使用此 Hook**
|
||||
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
|
||||
|
||||
### **使用强制要求**
|
||||
|
||||
| 场景 | 必须使用的工具 |
|
||||
|------|----------------|
|
||||
| 发送 HTTP 请求 | `@/utils/request` |
|
||||
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
|
||||
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
|
||||
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
|
||||
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
|
||||
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
|
||||
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
|
||||
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
|
||||
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
|
||||
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
|
||||
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
|
||||
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -622,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
|
||||
|
||||
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
|
||||
|
||||
### **工具文件清单**
|
||||
|
||||
#### `request.js` — HTTP 请求封装(核心)
|
||||
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
|
||||
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
|
||||
- 用法:`import service from '@/utils/request'`
|
||||
|
||||
#### `date.js` — 日期格式化
|
||||
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
|
||||
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
|
||||
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
|
||||
- 用法:`import { formatTimeToStr } from '@/utils/date'`
|
||||
|
||||
#### `format.js` — 数据展示格式化(综合工具)
|
||||
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
|
||||
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
|
||||
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
|
||||
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
|
||||
- `getDictFunc(type)` — 异步获取指定类型的字典数据
|
||||
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
|
||||
- `onDownloadFile(url)` — 触发文件下载
|
||||
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
|
||||
- `CreateUUID()` — 生成 UUID v4 字符串
|
||||
- `getBaseUrl()` — 获取当前环境的 API BaseURL
|
||||
- **以上所有格式化场景优先使用此文件中的工具函数**
|
||||
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
|
||||
|
||||
#### `dictionary.js` — 字典数据获取
|
||||
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
|
||||
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
|
||||
- 用法:`import { getDict } from '@/utils/dictionary'`
|
||||
|
||||
#### `stringFun.js` — 字符串处理
|
||||
- `toUpperCase(str)` — 首字母转大写
|
||||
- `toLowerCase(str)` — 首字母转小写
|
||||
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
|
||||
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
|
||||
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
|
||||
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
|
||||
|
||||
#### `params.js` — 系统参数获取
|
||||
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
|
||||
- **获取系统配置参数时,优先使用此工具**
|
||||
- 用法:`import { getParams } from '@/utils/params'`
|
||||
|
||||
#### `bus.js` — 全局事件总线
|
||||
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
|
||||
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
|
||||
- 用法:`import { emitter } from '@/utils/bus'`
|
||||
|
||||
#### `closeThisPage.js` — 关闭当前标签页
|
||||
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
|
||||
- **在需要程序化关闭当前页面时,必须使用此工具**
|
||||
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
|
||||
|
||||
#### `downloadImg.js` — 图片下载
|
||||
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
|
||||
- **需要下载图片时,优先使用此工具**
|
||||
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
|
||||
|
||||
#### `image.js` — 图片压缩
|
||||
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
|
||||
- **上传图片前需要做压缩处理时,使用此工具**
|
||||
- 用法:`import ImageCompress from '@/utils/image'`
|
||||
|
||||
#### `event.js` — DOM 事件监听管理
|
||||
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
|
||||
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
|
||||
- **手动操作 DOM 事件时,使用此工具以确保安全性**
|
||||
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
|
||||
|
||||
#### `env.js` — 环境判断
|
||||
- `isDev` — 是否为开发环境(Boolean)
|
||||
- `isProd` — 是否为生产环境(Boolean)
|
||||
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
|
||||
- 用法:`import { isDev, isProd } from '@/utils/env'`
|
||||
|
||||
#### `doc.js` — 外部文档跳转
|
||||
- `toDoc(url)` — 在新标签页打开指定 URL
|
||||
- 用法:`import { toDoc } from '@/utils/doc'`
|
||||
|
||||
#### `fmtRouterTitle.js` — 路由标题格式化
|
||||
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
|
||||
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
|
||||
|
||||
#### `page.js` — 页面标题生成
|
||||
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
|
||||
- 用法:`import getPageTitle from '@/utils/page'`
|
||||
|
||||
#### `asyncRouter.js` — 异步路由处理
|
||||
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
|
||||
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
|
||||
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
|
||||
|
||||
#### `btnAuth.js` — 按钮权限
|
||||
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
|
||||
- **实现按钮级别权限控制时,必须使用此 Hook**
|
||||
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
|
||||
|
||||
### **使用强制要求**
|
||||
|
||||
| 场景 | 必须使用的工具 |
|
||||
|------|----------------|
|
||||
| 发送 HTTP 请求 | `@/utils/request` |
|
||||
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
|
||||
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
|
||||
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
|
||||
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
|
||||
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
|
||||
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
|
||||
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
|
||||
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
|
||||
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
|
||||
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
|
||||
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -622,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
|
||||
|
||||
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
|
||||
|
||||
### **工具文件清单**
|
||||
|
||||
#### `request.js` — HTTP 请求封装(核心)
|
||||
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
|
||||
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
|
||||
- 用法:`import service from '@/utils/request'`
|
||||
|
||||
#### `date.js` — 日期格式化
|
||||
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
|
||||
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
|
||||
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
|
||||
- 用法:`import { formatTimeToStr } from '@/utils/date'`
|
||||
|
||||
#### `format.js` — 数据展示格式化(综合工具)
|
||||
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
|
||||
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
|
||||
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
|
||||
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
|
||||
- `getDictFunc(type)` — 异步获取指定类型的字典数据
|
||||
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
|
||||
- `onDownloadFile(url)` — 触发文件下载
|
||||
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
|
||||
- `CreateUUID()` — 生成 UUID v4 字符串
|
||||
- `getBaseUrl()` — 获取当前环境的 API BaseURL
|
||||
- **以上所有格式化场景优先使用此文件中的工具函数**
|
||||
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
|
||||
|
||||
#### `dictionary.js` — 字典数据获取
|
||||
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
|
||||
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
|
||||
- 用法:`import { getDict } from '@/utils/dictionary'`
|
||||
|
||||
#### `stringFun.js` — 字符串处理
|
||||
- `toUpperCase(str)` — 首字母转大写
|
||||
- `toLowerCase(str)` — 首字母转小写
|
||||
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
|
||||
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
|
||||
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
|
||||
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
|
||||
|
||||
#### `params.js` — 系统参数获取
|
||||
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
|
||||
- **获取系统配置参数时,优先使用此工具**
|
||||
- 用法:`import { getParams } from '@/utils/params'`
|
||||
|
||||
#### `bus.js` — 全局事件总线
|
||||
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
|
||||
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
|
||||
- 用法:`import { emitter } from '@/utils/bus'`
|
||||
|
||||
#### `closeThisPage.js` — 关闭当前标签页
|
||||
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
|
||||
- **在需要程序化关闭当前页面时,必须使用此工具**
|
||||
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
|
||||
|
||||
#### `downloadImg.js` — 图片下载
|
||||
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
|
||||
- **需要下载图片时,优先使用此工具**
|
||||
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
|
||||
|
||||
#### `image.js` — 图片压缩
|
||||
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
|
||||
- **上传图片前需要做压缩处理时,使用此工具**
|
||||
- 用法:`import ImageCompress from '@/utils/image'`
|
||||
|
||||
#### `event.js` — DOM 事件监听管理
|
||||
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
|
||||
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
|
||||
- **手动操作 DOM 事件时,使用此工具以确保安全性**
|
||||
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
|
||||
|
||||
#### `env.js` — 环境判断
|
||||
- `isDev` — 是否为开发环境(Boolean)
|
||||
- `isProd` — 是否为生产环境(Boolean)
|
||||
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
|
||||
- 用法:`import { isDev, isProd } from '@/utils/env'`
|
||||
|
||||
#### `doc.js` — 外部文档跳转
|
||||
- `toDoc(url)` — 在新标签页打开指定 URL
|
||||
- 用法:`import { toDoc } from '@/utils/doc'`
|
||||
|
||||
#### `fmtRouterTitle.js` — 路由标题格式化
|
||||
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
|
||||
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
|
||||
|
||||
#### `page.js` — 页面标题生成
|
||||
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
|
||||
- 用法:`import getPageTitle from '@/utils/page'`
|
||||
|
||||
#### `asyncRouter.js` — 异步路由处理
|
||||
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
|
||||
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
|
||||
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
|
||||
|
||||
#### `btnAuth.js` — 按钮权限
|
||||
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
|
||||
- **实现按钮级别权限控制时,必须使用此 Hook**
|
||||
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
|
||||
|
||||
### **使用强制要求**
|
||||
|
||||
| 场景 | 必须使用的工具 |
|
||||
|------|----------------|
|
||||
| 发送 HTTP 请求 | `@/utils/request` |
|
||||
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
|
||||
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
|
||||
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
|
||||
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
|
||||
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
|
||||
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
|
||||
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
|
||||
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
|
||||
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
|
||||
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
|
||||
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -321,3 +321,61 @@ func (s *SystemApiApi) FreshCasbin(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("刷新成功", c)
|
||||
}
|
||||
|
||||
// GetApiRoles
|
||||
// @Tags SysApi
|
||||
// @Summary 获取拥有指定API权限的角色ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param path query string true "API路径"
|
||||
// @Param method query string true "请求方法"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
|
||||
// @Router /api/getApiRoles [get]
|
||||
func (s *SystemApiApi) GetApiRoles(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
method := c.Query("method")
|
||||
if path == "" || method == "" {
|
||||
response.FailWithMessage("API路径和请求方法不能为空", c)
|
||||
return
|
||||
}
|
||||
authorityIds, err := casbinService.GetAuthoritiesByApi(path, method)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if authorityIds == nil {
|
||||
authorityIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(authorityIds, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetApiRoles
|
||||
// @Tags SysApi
|
||||
// @Summary 全量覆盖某API关联的角色列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /api/setApiRoles [post]
|
||||
func (s *SystemApiApi) SetApiRoles(c *gin.Context) {
|
||||
var req systemReq.SetApiAuthorities
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.Path == "" || req.Method == "" {
|
||||
response.FailWithMessage("API路径和请求方法不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
// 刷新casbin缓存使策略立即生效
|
||||
_ = casbinService.FreshCasbin()
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
|
||||
@@ -200,3 +201,57 @@ func (a *AuthorityApi) SetDataAuthority(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
// GetUsersByAuthority
|
||||
// @Tags Authority
|
||||
// @Summary 获取拥有指定角色的用户ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param authorityId query uint true "角色ID"
|
||||
// @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功"
|
||||
// @Router /authority/getUsersByAuthority [get]
|
||||
func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) {
|
||||
var req systemReq.SetRoleUsers
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if userIds == nil {
|
||||
userIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(userIds, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetRoleUsers
|
||||
// @Tags Authority
|
||||
// @Summary 全量覆盖某角色关联的用户列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /authority/setRoleUsers [post]
|
||||
func (a *AuthorityApi) SetRoleUsers(c *gin.Context) {
|
||||
var req systemReq.SetRoleUsers
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.AuthorityId == 0 {
|
||||
response.FailWithMessage("角色ID不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
@@ -244,6 +244,76 @@ func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) {
|
||||
response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetMenuRoles
|
||||
// @Tags AuthorityMenu
|
||||
// @Summary 获取拥有指定菜单的角色ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param menuId query uint true "菜单ID"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
|
||||
// @Router /menu/getMenuRoles [get]
|
||||
func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) {
|
||||
var req systemReq.SetMenuAuthorities
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.MenuId == 0 {
|
||||
response.FailWithMessage("菜单ID不能为空", c)
|
||||
return
|
||||
}
|
||||
authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if authorityIds == nil {
|
||||
authorityIds = []uint{}
|
||||
}
|
||||
defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if defaultRouterAuthorityIds == nil {
|
||||
defaultRouterAuthorityIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(gin.H{
|
||||
"authorityIds": authorityIds,
|
||||
"defaultRouterAuthorityIds": defaultRouterAuthorityIds,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetMenuRoles
|
||||
// @Tags AuthorityMenu
|
||||
// @Summary 全量覆盖某菜单关联的角色列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /menu/setMenuRoles [post]
|
||||
func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) {
|
||||
var req systemReq.SetMenuAuthorities
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.MenuId == 0 {
|
||||
response.FailWithMessage("菜单ID不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
// GetMenuList
|
||||
// @Tags Menu
|
||||
// @Summary 分页获取基础menu列表
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
@@ -55,6 +57,17 @@ func (s *SkillsApi) SaveSkill(c *gin.Context) {
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) DeleteSkill(c *gin.Context) {
|
||||
var req request.SkillDeleteRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.Delete(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("删除技能失败", zap.Error(err))
|
||||
response.FailWithMessage("删除技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) CreateScript(c *gin.Context) {
|
||||
var req request.SkillScriptCreateRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
@@ -217,3 +230,34 @@ func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) PackageSkill(c *gin.Context) {
|
||||
var req request.SkillPackageRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
fileName, data, err := skillsService.Package(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("打包技能失败", zap.Error(err))
|
||||
response.FailWithMessage("打包技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/zip")
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+fileName+"\"")
|
||||
c.Data(http.StatusOK, "application/zip", data)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) DownloadOnlineSkill(c *gin.Context) {
|
||||
var req request.DownloadOnlineSkillReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := skillsService.DownloadOnlineSkill(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("下载在线技能失败", zap.Error(err))
|
||||
response.FailWithMessage("下载在线技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("下载成功", c)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package global
|
||||
// 目前只有Version正式使用 其余为预留
|
||||
const (
|
||||
// Version 当前版本号
|
||||
Version = "v2.8.9"
|
||||
Version = "v2.9.0"
|
||||
// AppName 应用名称
|
||||
AppName = "Gin-Vue-Admin"
|
||||
// Description 应用描述
|
||||
|
||||
+21
-3
@@ -6,7 +6,11 @@ toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go v1.55.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1
|
||||
github.com/casbin/casbin/v2 v2.103.0
|
||||
github.com/casbin/gorm-adapter/v3 v3.32.0
|
||||
github.com/dzwvip/gorm-oracle v0.1.2
|
||||
@@ -46,6 +50,7 @@ require (
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/text v0.24.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/datatypes v1.2.5
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
@@ -61,6 +66,21 @@ require (
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aws/smithy-go v1.24.1 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
@@ -108,7 +128,6 @@ require (
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
@@ -178,7 +197,6 @@ require (
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/hints v1.1.2 // indirect
|
||||
gorm.io/plugin/dbresolver v1.5.3 // indirect
|
||||
modernc.org/fileutil v1.3.0 // indirect
|
||||
|
||||
+40
-7
@@ -54,8 +54,46 @@ github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 h1:+mQ8NQBh7B7c2FBtppRnwkrmuwFON1XQQ+5yblomZKk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3/go.mod h1:u67RKh3BRmS4FYLH+rN3N4T5fqpd9m2ttAwBJYEdosU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
@@ -273,10 +311,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -782,7 +816,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -111,7 +111,7 @@ func Routers() *gin.Engine {
|
||||
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
|
||||
systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志
|
||||
systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发
|
||||
systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器
|
||||
systemRouter.InitSkillsRouter(PrivateGroup,PublicGroup) // Skills 定义器
|
||||
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
|
||||
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
|
||||
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
// @Tag.Description 用户
|
||||
|
||||
// @title Gin-Vue-Admin Swagger API接口文档
|
||||
// @version v2.8.9
|
||||
// @version v2.9.0
|
||||
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
|
||||
@@ -12,3 +12,10 @@ type SearchApiParams struct {
|
||||
OrderKey string `json:"orderKey"` // 排序
|
||||
Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true
|
||||
}
|
||||
|
||||
// SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表
|
||||
type SetApiAuthorities struct {
|
||||
Path string `json:"path" form:"path"` // API路径
|
||||
Method string `json:"method" form:"method"` // 请求方法
|
||||
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ type AddMenuAuthorityInfo struct {
|
||||
AuthorityId uint `json:"authorityId"` // 角色ID
|
||||
}
|
||||
|
||||
// SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表
|
||||
type SetMenuAuthorities struct {
|
||||
MenuId uint `json:"menuId" form:"menuId"` // 菜单ID
|
||||
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
|
||||
}
|
||||
|
||||
func DefaultMenu() []system.SysBaseMenu {
|
||||
return []system.SysBaseMenu{{
|
||||
GVA_MODEL: global.GVA_MODEL{ID: 1},
|
||||
|
||||
@@ -11,6 +11,16 @@ type SkillDetailRequest struct {
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillDeleteRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillPackageRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillSaveRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
@@ -62,3 +72,9 @@ type SkillGlobalConstraintSaveRequest struct {
|
||||
Content string `json:"content"`
|
||||
SyncTools []string `json:"syncTools"`
|
||||
}
|
||||
|
||||
type DownloadOnlineSkillReq struct {
|
||||
Tool string `json:"tool" binding:"required"`
|
||||
ID uint `json:"id" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -66,4 +66,12 @@ type GetUserList struct {
|
||||
NickName string `json:"nickName" form:"nickName"`
|
||||
Phone string `json:"phone" form:"phone"`
|
||||
Email string `json:"email" form:"email"`
|
||||
OrderKey string `json:"orderKey" form:"orderKey"` // 排序
|
||||
Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true
|
||||
}
|
||||
|
||||
// SetRoleUsers 通过角色ID全量覆盖关联用户列表
|
||||
type SetRoleUsers struct {
|
||||
AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID
|
||||
UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.Router
|
||||
apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息
|
||||
apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api
|
||||
apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api
|
||||
apiRouter.POST("setApiRoles", apiRouterApi.SetApiRoles) // 全量覆盖API关联角色
|
||||
}
|
||||
{
|
||||
apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api
|
||||
apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表
|
||||
apiRouterWithoutRecord.GET("getApiRoles", apiRouterApi.GetApiRoles) // 获取API关联角色ID列表
|
||||
}
|
||||
{
|
||||
apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限
|
||||
|
||||
@@ -16,8 +16,10 @@ func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) {
|
||||
authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色
|
||||
authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色
|
||||
authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限
|
||||
authorityRouter.POST("setRoleUsers", authorityApi.SetRoleUsers) // 全量覆盖角色关联用户
|
||||
}
|
||||
{
|
||||
authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表
|
||||
authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表
|
||||
authorityRouterWithoutRecord.GET("getUsersByAuthority", authorityApi.GetUsersByAuthority) // 获取角色关联用户ID列表
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
|
||||
menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系
|
||||
menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单
|
||||
menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单
|
||||
menuRouter.POST("setMenuRoles", authorityMenuApi.SetMenuRoles) // 全量覆盖菜单关联角色
|
||||
}
|
||||
{
|
||||
menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树
|
||||
@@ -22,6 +23,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
|
||||
menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由
|
||||
menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu
|
||||
menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单
|
||||
menuRouterWithoutRecord.GET("getMenuRoles", authorityMenuApi.GetMenuRoles) // 获取菜单关联角色ID列表
|
||||
}
|
||||
return menuRouter
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import "github.com/gin-gonic/gin"
|
||||
|
||||
type SkillsRouter struct{}
|
||||
|
||||
func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
|
||||
func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) {
|
||||
skillsRouter := Router.Group("skills")
|
||||
skillsRouterPub := pubRouter.Group("skills")
|
||||
{
|
||||
skillsRouter.GET("getTools", skillsApi.GetTools)
|
||||
skillsRouter.POST("getSkillList", skillsApi.GetSkillList)
|
||||
skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail)
|
||||
skillsRouter.POST("saveSkill", skillsApi.SaveSkill)
|
||||
skillsRouter.POST("deleteSkill", skillsApi.DeleteSkill)
|
||||
skillsRouter.POST("createScript", skillsApi.CreateScript)
|
||||
skillsRouter.POST("getScript", skillsApi.GetScript)
|
||||
skillsRouter.POST("saveScript", skillsApi.SaveScript)
|
||||
@@ -25,5 +27,9 @@ func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
|
||||
skillsRouter.POST("saveTemplate", skillsApi.SaveTemplate)
|
||||
skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint)
|
||||
skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint)
|
||||
skillsRouter.POST("packageSkill", skillsApi.PackageSkill)
|
||||
}
|
||||
{
|
||||
skillsRouterPub.POST("downloadOnlineSkill", skillsApi.DownloadOnlineSkill)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,3 +331,82 @@ func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint)
|
||||
}
|
||||
return *authority.ParentId, nil
|
||||
}
|
||||
|
||||
// GetUserIdsByAuthorityId 获取拥有指定角色的所有用户ID
|
||||
func (authorityService *AuthorityService) GetUserIdsByAuthorityId(authorityId uint) (userIds []uint, err error) {
|
||||
var records []system.SysUserAuthority
|
||||
err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range records {
|
||||
userIds = append(userIds, r.SysUserId)
|
||||
}
|
||||
return userIds, nil
|
||||
}
|
||||
|
||||
// SetRoleUsers 全量覆盖某角色关联的用户列表
|
||||
// 入参:角色ID + 目标用户ID列表,保存时将该角色的关联关系完全替换为传入列表
|
||||
func (authorityService *AuthorityService) SetRoleUsers(authorityId uint, userIds []uint) error {
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 查出当前拥有该角色的所有用户ID
|
||||
var existingRecords []system.SysUserAuthority
|
||||
if err := tx.Where("sys_authority_authority_id = ?", authorityId).Find(&existingRecords).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentSet := make(map[uint]struct{})
|
||||
for _, r := range existingRecords {
|
||||
currentSet[r.SysUserId] = struct{}{}
|
||||
}
|
||||
|
||||
targetSet := make(map[uint]struct{})
|
||||
for _, id := range userIds {
|
||||
targetSet[id] = struct{}{}
|
||||
}
|
||||
|
||||
// 2. 删除该角色所有已有的用户关联
|
||||
if err := tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", authorityId).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 对被移除的用户:若该角色是其主角色,则将主角色切换为其剩余的其他角色
|
||||
for userId := range currentSet {
|
||||
if _, ok := targetSet[userId]; ok {
|
||||
continue // 仍在目标列表中,不处理
|
||||
}
|
||||
var user system.SysUser
|
||||
if err := tx.First(&user, "id = ?", userId).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if user.AuthorityId == authorityId {
|
||||
// 从剩余关联(已删除当前角色后)中找另一个角色作为主角色
|
||||
var another system.SysUserAuthority
|
||||
if err := tx.Where("sys_user_id = ?", userId).First(&another).Error; err != nil {
|
||||
// 没有其他角色,主角色保持不变,不做处理
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&system.SysUser{}).Where("id = ?", userId).
|
||||
Update("authority_id", another.SysAuthorityAuthorityId).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 批量插入新的关联记录
|
||||
if len(userIds) > 0 {
|
||||
newRecords := make([]system.SysUserAuthority, 0, len(userIds))
|
||||
for _, userId := range userIds {
|
||||
newRecords = append(newRecords, system.SysUserAuthority{
|
||||
SysUserId: userId,
|
||||
SysAuthorityAuthorityId: authorityId,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&newRecords).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,3 +171,45 @@ func (casbinService *CasbinService) FreshCasbin() (err error) {
|
||||
err = e.LoadPolicy()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAuthoritiesByApi 获取拥有指定API权限的所有角色ID
|
||||
func (casbinService *CasbinService) GetAuthoritiesByApi(path, method string) (authorityIds []uint, err error) {
|
||||
var rules []gormadapter.CasbinRule
|
||||
err = global.GVA_DB.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Find(&rules).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rules {
|
||||
id, e := strconv.Atoi(r.V0)
|
||||
if e == nil {
|
||||
authorityIds = append(authorityIds, uint(id))
|
||||
}
|
||||
}
|
||||
return authorityIds, nil
|
||||
}
|
||||
|
||||
// SetApiAuthorities 全量覆盖某API关联的角色列表
|
||||
func (casbinService *CasbinService) SetApiAuthorities(path, method string, authorityIds []uint) error {
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 删除该API所有已有的角色关联
|
||||
if err := tx.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Delete(&gormadapter.CasbinRule{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. 批量插入新的关联记录
|
||||
if len(authorityIds) > 0 {
|
||||
newRules := make([]gormadapter.CasbinRule, 0, len(authorityIds))
|
||||
for _, authorityId := range authorityIds {
|
||||
newRules = append(newRules, gormadapter.CasbinRule{
|
||||
Ptype: "p",
|
||||
V0: strconv.Itoa(int(authorityId)),
|
||||
V1: path,
|
||||
V2: method,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&newRules).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -314,6 +314,65 @@ func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (
|
||||
return menus, err
|
||||
}
|
||||
|
||||
// GetAuthoritiesByMenuId 获取拥有指定菜单的所有角色ID
|
||||
func (menuService *MenuService) GetAuthoritiesByMenuId(menuId uint) (authorityIds []uint, err error) {
|
||||
var records []system.SysAuthorityMenu
|
||||
err = global.GVA_DB.Where("sys_base_menu_id = ?", menuId).Find(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range records {
|
||||
id, e := strconv.Atoi(r.AuthorityId)
|
||||
if e == nil {
|
||||
authorityIds = append(authorityIds, uint(id))
|
||||
}
|
||||
}
|
||||
return authorityIds, nil
|
||||
}
|
||||
|
||||
// GetDefaultRouterAuthorityIds 获取将指定菜单设为首页的角色ID列表
|
||||
func (menuService *MenuService) GetDefaultRouterAuthorityIds(menuId uint) (authorityIds []uint, err error) {
|
||||
var menu system.SysBaseMenu
|
||||
err = global.GVA_DB.First(&menu, menuId).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var authorities []system.SysAuthority
|
||||
err = global.GVA_DB.Where("default_router = ?", menu.Name).Find(&authorities).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, auth := range authorities {
|
||||
authorityIds = append(authorityIds, auth.AuthorityId)
|
||||
}
|
||||
return authorityIds, nil
|
||||
}
|
||||
|
||||
// SetMenuAuthorities 全量覆盖某菜单关联的角色列表
|
||||
func (menuService *MenuService) SetMenuAuthorities(menuId uint, authorityIds []uint) error {
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 删除该菜单所有已有的角色关联
|
||||
if err := tx.Where("sys_base_menu_id = ?", menuId).Delete(&system.SysAuthorityMenu{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. 批量插入新的关联记录
|
||||
if len(authorityIds) > 0 {
|
||||
menuIdStr := strconv.Itoa(int(menuId))
|
||||
newRecords := make([]system.SysAuthorityMenu, 0, len(authorityIds))
|
||||
for _, authorityId := range authorityIds {
|
||||
newRecords = append(newRecords, system.SysAuthorityMenu{
|
||||
MenuId: menuIdStr,
|
||||
AuthorityId: strconv.Itoa(int(authorityId)),
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&newRecords).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UserAuthorityDefaultRouter 用户角色默认路由检查
|
||||
//
|
||||
// Author [SliverHorn](https://github.com/SliverHorn)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -158,6 +163,107 @@ func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) Delete(_ context.Context, req request.SkillDeleteRequest) error {
|
||||
if strings.TrimSpace(req.Tool) == "" {
|
||||
return errors.New("工具类型不能为空")
|
||||
}
|
||||
if !isSafeName(req.Skill) {
|
||||
return errors.New("技能名称不合法")
|
||||
}
|
||||
skillDir, err := s.skillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(skillDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New("技能不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return errors.New("技能目录异常")
|
||||
}
|
||||
return os.RemoveAll(skillDir)
|
||||
}
|
||||
|
||||
func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) {
|
||||
if strings.TrimSpace(req.Tool) == "" {
|
||||
return "", nil, errors.New("工具类型不能为空")
|
||||
}
|
||||
if !isSafeName(req.Skill) {
|
||||
return "", nil, errors.New("技能名称不合法")
|
||||
}
|
||||
|
||||
skillDir, err := s.skillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
info, err := os.Stat(skillDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil, errors.New("技能不存在")
|
||||
}
|
||||
return "", nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", nil, errors.New("技能目录异常")
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
zw := zip.NewWriter(buf)
|
||||
|
||||
walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
rel, err := filepath.Rel(skillDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
zipName := filepath.ToSlash(rel)
|
||||
if d.IsDir() {
|
||||
_, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/")
|
||||
return err
|
||||
}
|
||||
|
||||
fileInfo, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := zip.FileInfoHeader(fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = zipName
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zw.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(content)
|
||||
return err
|
||||
})
|
||||
if walkErr != nil {
|
||||
_ = zw.Close()
|
||||
return "", nil, walkErr
|
||||
}
|
||||
|
||||
if err = zw.Close(); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return req.Skill + ".zip", buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
|
||||
if !isSafeName(req.Skill) {
|
||||
return "", "", errors.New("技能名称不合法")
|
||||
@@ -279,6 +385,136 @@ func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.Skil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error {
|
||||
skillsDir, err := s.toolSkillsDir(req.Tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"plugin_id": req.ID,
|
||||
"version": req.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("构建下载请求失败: %w", err)
|
||||
}
|
||||
|
||||
downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("构建下载请求失败: %w", err)
|
||||
}
|
||||
downloadReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
downloadResp, err := http.DefaultClient.Do(downloadReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("下载技能失败: %w", err)
|
||||
}
|
||||
defer downloadResp.Body.Close()
|
||||
|
||||
if downloadResp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode)
|
||||
}
|
||||
|
||||
metaBody, err := io.ReadAll(downloadResp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取下载结果失败: %w", err)
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(metaBody, &meta); err != nil {
|
||||
return fmt.Errorf("解析下载结果失败: %w", err)
|
||||
}
|
||||
|
||||
realDownloadURL := strings.TrimSpace(meta.Data.URL)
|
||||
if realDownloadURL == "" {
|
||||
return errors.New("下载结果缺少 url")
|
||||
}
|
||||
|
||||
zipResp, err := http.Get(realDownloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("下载压缩包失败: %w", err)
|
||||
}
|
||||
defer zipResp.Body.Close()
|
||||
|
||||
if zipResp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode)
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "gva-skill-*.zip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建临时文件失败: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
if _, err = io.Copy(tmpFile, zipResp.Body); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("保存技能包失败: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
if err = extractZipToDir(tmpPath, skillsDir); err != nil {
|
||||
return fmt.Errorf("解压技能包失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZipToDir(zipPath, destDir string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
name := filepath.FromSlash(f.Name)
|
||||
if strings.Contains(name, "..") {
|
||||
continue
|
||||
}
|
||||
|
||||
target := filepath.Join(destDir, name)
|
||||
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(target, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, rc)
|
||||
rc.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
|
||||
toolDir, ok := skillToolDirs[tool]
|
||||
if !ok {
|
||||
|
||||
@@ -109,7 +109,25 @@ func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (lis
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload("Authority").Find(&userList).Error
|
||||
|
||||
orderStr := "id desc"
|
||||
if info.OrderKey != "" {
|
||||
allowedOrders := map[string]bool{
|
||||
"id": true,
|
||||
"username": true,
|
||||
"nick_name": true,
|
||||
"phone": true,
|
||||
"email": true,
|
||||
}
|
||||
if allowedOrders[info.OrderKey] {
|
||||
orderStr = info.OrderKey
|
||||
if info.Desc {
|
||||
orderStr = info.OrderKey + " desc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = db.Limit(limit).Offset(offset).Order(orderStr).Preload("Authorities").Preload("Authority").Find(&userList).Error
|
||||
return userList, total, err
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "角色", Method: "PUT", Path: "/authority/updateAuthority", Description: "更新角色信息"},
|
||||
{ApiGroup: "角色", Method: "POST", Path: "/authority/getAuthorityList", Description: "获取角色列表"},
|
||||
{ApiGroup: "角色", Method: "POST", Path: "/authority/setDataAuthority", Description: "设置角色资源权限"},
|
||||
{ApiGroup: "角色", Method: "GET", Path: "/authority/getUsersByAuthority", Description: "获取角色关联用户ID列表"},
|
||||
{ApiGroup: "角色", Method: "POST", Path: "/authority/setRoleUsers", Description: "全量覆盖角色关联用户"},
|
||||
|
||||
{ApiGroup: "casbin", Method: "POST", Path: "/casbin/updateCasbin", Description: "更改角色api权限"},
|
||||
{ApiGroup: "casbin", Method: "POST", Path: "/casbin/getPolicyPathByAuthorityId", Description: "获取权限列表"},
|
||||
@@ -118,6 +120,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillList", Description: "获取技能列表"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillDetail", Description: "获取技能详情"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveSkill", Description: "保存技能定义"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/deleteSkill", Description: "删除技能"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/createScript", Description: "创建技能脚本"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getScript", Description: "读取技能脚本"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveScript", Description: "保存技能脚本"},
|
||||
@@ -132,6 +135,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveTemplate", Description: "保存技能模板"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getGlobalConstraint", Description: "读取全局约束"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveGlobalConstraint", Description: "保存全局约束"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/packageSkill", Description: "打包技能"},
|
||||
|
||||
{ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"},
|
||||
{ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"},
|
||||
|
||||
@@ -74,6 +74,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/authority/deleteAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/authority/getAuthorityList", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/authority/setDataAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/authority/getUsersByAuthority", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/authority/setRoleUsers", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/menu/getMenu", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/menu/getMenuList", V2: "POST"},
|
||||
@@ -120,6 +122,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getSkillList", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getSkillDetail", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveSkill", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/deleteSkill", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/createScript", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getScript", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveScript", V2: "POST"},
|
||||
@@ -134,6 +137,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveTemplate", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getGlobalConstraint", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveGlobalConstraint", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/packageSkill", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/customer/customer", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/customer/customer", V2: "PUT"},
|
||||
@@ -256,6 +260,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "8881", V1: "/authority/deleteAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/authority/getAuthorityList", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/authority/setDataAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/authority/getUsersByAuthority", V2: "GET"},
|
||||
{Ptype: "p", V0: "8881", V1: "/authority/setRoleUsers", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/menu/getMenu", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/menu/getMenuList", V2: "POST"},
|
||||
{Ptype: "p", V0: "8881", V1: "/menu/addBaseMenu", V2: "POST"},
|
||||
@@ -297,6 +303,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "9528", V1: "/authority/deleteAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "9528", V1: "/authority/getAuthorityList", V2: "POST"},
|
||||
{Ptype: "p", V0: "9528", V1: "/authority/setDataAuthority", V2: "POST"},
|
||||
{Ptype: "p", V0: "9528", V1: "/authority/getUsersByAuthority", V2: "GET"},
|
||||
{Ptype: "p", V0: "9528", V1: "/authority/setRoleUsers", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "9528", V1: "/menu/getMenu", V2: "POST"},
|
||||
{Ptype: "p", V0: "9528", V1: "/menu/getMenuList", V2: "POST"},
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -21,13 +22,13 @@ type AwsS3 struct{}
|
||||
//@author: [WqyJh](https://github.com/WqyJh)
|
||||
//@object: *AwsS3
|
||||
//@function: UploadFile
|
||||
//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket
|
||||
//@description: Upload file to Aws S3 using aws-sdk-go-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html
|
||||
//@param: file *multipart.FileHeader
|
||||
//@return: string, string, error
|
||||
|
||||
func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
|
||||
session := newSession()
|
||||
uploader := s3manager.NewUploader(session)
|
||||
client := newS3Client()
|
||||
uploader := manager.NewUploader(client)
|
||||
|
||||
fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
|
||||
filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey
|
||||
@@ -38,10 +39,10 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
|
||||
}
|
||||
defer f.Close() // 创建文件 defer 关闭
|
||||
|
||||
_, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket),
|
||||
Key: aws.String(filename),
|
||||
Body: f,
|
||||
_, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket),
|
||||
Key: aws.String(filename),
|
||||
Body: f,
|
||||
ContentType: aws.String(file.Header.Get("Content-Type")),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -55,44 +56,59 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
|
||||
//@author: [WqyJh](https://github.com/WqyJh)
|
||||
//@object: *AwsS3
|
||||
//@function: DeleteFile
|
||||
//@description: Delete file from Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-delete-bucket-item
|
||||
//@param: file *multipart.FileHeader
|
||||
//@return: string, string, error
|
||||
//@description: Delete file from Aws S3 using aws-sdk-go-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html
|
||||
//@param: key string
|
||||
//@return: error
|
||||
|
||||
func (*AwsS3) DeleteFile(key string) error {
|
||||
session := newSession()
|
||||
svc := s3.New(session)
|
||||
client := newS3Client()
|
||||
filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key
|
||||
bucket := global.GVA_CONFIG.AwsS3.Bucket
|
||||
|
||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(filename),
|
||||
})
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error()))
|
||||
return errors.New("function svc.DeleteObject() failed, err:" + err.Error())
|
||||
global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error()))
|
||||
return errors.New("function client.DeleteObject() failed, err:" + err.Error())
|
||||
}
|
||||
|
||||
_ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
|
||||
waiter := s3.NewObjectNotExistsWaiter(client)
|
||||
_ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(filename),
|
||||
})
|
||||
}, 30*time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSession Create S3 session
|
||||
func newSession() *session.Session {
|
||||
sess, _ := session.NewSession(&aws.Config{
|
||||
Region: aws.String(global.GVA_CONFIG.AwsS3.Region),
|
||||
Endpoint: aws.String(global.GVA_CONFIG.AwsS3.Endpoint), //minio在这里设置地址,可以兼容
|
||||
S3ForcePathStyle: aws.Bool(global.GVA_CONFIG.AwsS3.S3ForcePathStyle),
|
||||
DisableSSL: aws.Bool(global.GVA_CONFIG.AwsS3.DisableSSL),
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
global.GVA_CONFIG.AwsS3.SecretID,
|
||||
global.GVA_CONFIG.AwsS3.SecretKey,
|
||||
// newS3Client creates an S3 v2 client with static credentials and optional custom endpoint.
|
||||
// minio在这里设置Endpoint地址,可以兼容
|
||||
func newS3Client() *s3.Client {
|
||||
cfg := global.GVA_CONFIG.AwsS3
|
||||
|
||||
awsCfg, _ := config.LoadDefaultConfig(context.TODO(),
|
||||
config.WithRegion(cfg.Region),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
cfg.SecretID,
|
||||
cfg.SecretKey,
|
||||
"",
|
||||
),
|
||||
)),
|
||||
)
|
||||
|
||||
return s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
if cfg.Endpoint != "" {
|
||||
endpoint := cfg.Endpoint
|
||||
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
||||
if cfg.DisableSSL {
|
||||
endpoint = "http://" + endpoint
|
||||
} else {
|
||||
endpoint = "https://" + endpoint
|
||||
}
|
||||
}
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
}
|
||||
o.UsePathStyle = cfg.S3ForcePathStyle
|
||||
})
|
||||
return sess
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -18,8 +19,8 @@ import (
|
||||
type CloudflareR2 struct{}
|
||||
|
||||
func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, fileName string, err error) {
|
||||
session := c.newSession()
|
||||
client := s3manager.NewUploader(session)
|
||||
client := c.newR2Client()
|
||||
uploader := manager.NewUploader(client)
|
||||
|
||||
fileKey := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
|
||||
fileName = fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.Path, fileKey)
|
||||
@@ -30,56 +31,55 @@ func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, f
|
||||
}
|
||||
defer f.Close() // 创建文件 defer 关闭
|
||||
|
||||
input := &s3manager.UploadInput{
|
||||
_, err = uploader.Upload(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(global.GVA_CONFIG.CloudflareR2.Bucket),
|
||||
Key: aws.String(fileName),
|
||||
Body: f,
|
||||
}
|
||||
|
||||
_, err = client.Upload(input)
|
||||
})
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error()))
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL,
|
||||
fileName),
|
||||
fileKey,
|
||||
nil
|
||||
return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, fileName), fileKey, nil
|
||||
}
|
||||
|
||||
func (c *CloudflareR2) DeleteFile(key string) error {
|
||||
session := newSession()
|
||||
svc := s3.New(session)
|
||||
client := c.newR2Client()
|
||||
filename := global.GVA_CONFIG.CloudflareR2.Path + "/" + key
|
||||
bucket := global.GVA_CONFIG.CloudflareR2.Bucket
|
||||
|
||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(filename),
|
||||
})
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error()))
|
||||
return errors.New("function svc.DeleteObject() failed, err:" + err.Error())
|
||||
global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error()))
|
||||
return errors.New("function client.DeleteObject() failed, err:" + err.Error())
|
||||
}
|
||||
|
||||
_ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
|
||||
waiter := s3.NewObjectNotExistsWaiter(client)
|
||||
_ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(filename),
|
||||
})
|
||||
}, 30*time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*CloudflareR2) newSession() *session.Session {
|
||||
endpoint := fmt.Sprintf("%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID)
|
||||
func (*CloudflareR2) newR2Client() *s3.Client {
|
||||
endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID)
|
||||
|
||||
return session.Must(session.NewSession(&aws.Config{
|
||||
Region: aws.String("auto"),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
cfg, _ := config.LoadDefaultConfig(context.TODO(),
|
||||
config.WithRegion("auto"),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
global.GVA_CONFIG.CloudflareR2.AccessKeyID,
|
||||
global.GVA_CONFIG.CloudflareR2.SecretAccessKey,
|
||||
"",
|
||||
),
|
||||
}))
|
||||
)),
|
||||
)
|
||||
|
||||
return s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gin-vue-admin",
|
||||
"version": "2.8.9",
|
||||
"version": "2.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node openDocument.js && vite --host --mode development",
|
||||
|
||||
@@ -174,3 +174,33 @@ export const enterSyncApi = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拥有指定API权限的角色ID列表
|
||||
* @param {string} path API路径
|
||||
* @param {string} method 请求方法
|
||||
* @returns {Promise<number[]>} 角色ID数组
|
||||
*/
|
||||
export const getApiRoles = (path, method) => {
|
||||
return service({
|
||||
url: '/api/getApiRoles',
|
||||
method: 'get',
|
||||
params: { path, method }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量覆盖某API关联的角色列表
|
||||
* @param {Object} data
|
||||
* @param {string} data.path API路径
|
||||
* @param {string} data.method 请求方法
|
||||
* @param {number[]} data.authorityIds 角色ID列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const setApiRoles = (data) => {
|
||||
return service({
|
||||
url: '/api/setApiRoles',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,3 +83,31 @@ export const updateAuthority = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拥有指定角色的用户ID列表
|
||||
* @param {number} authorityId 角色ID
|
||||
* @returns {Promise<number[]>} 用户ID数组
|
||||
*/
|
||||
export const getUsersByAuthorityId = (authorityId) => {
|
||||
return service({
|
||||
url: '/authority/getUsersByAuthority',
|
||||
method: 'get',
|
||||
params: { authorityId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量覆盖某角色关联的用户列表
|
||||
* @param {Object} data
|
||||
* @param {number} data.authorityId 角色ID
|
||||
* @param {number[]} data.userIds 用户ID列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const setRoleUsers = (data) => {
|
||||
return service({
|
||||
url: '/authority/setRoleUsers',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,3 +111,31 @@ export const getBaseMenuById = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拥有指定菜单的角色ID列表
|
||||
* @param {number} menuId 菜单ID
|
||||
* @returns {Promise<number[]>} 角色ID数组
|
||||
*/
|
||||
export const getMenuRoles = (menuId) => {
|
||||
return service({
|
||||
url: '/menu/getMenuRoles',
|
||||
method: 'get',
|
||||
params: { menuId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量覆盖某菜单关联的角色列表
|
||||
* @param {Object} data
|
||||
* @param {number} data.menuId 菜单ID
|
||||
* @param {number[]} data.authorityIds 角色ID列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const setMenuRoles = (data) => {
|
||||
return service({
|
||||
url: '/menu/setMenuRoles',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ export const saveSkill = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteSkill = (data) => {
|
||||
return service({
|
||||
url: '/skills/deleteSkill',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const createSkillScript = (data) => {
|
||||
return service({
|
||||
url: '/skills/createScript',
|
||||
@@ -142,3 +150,20 @@ export const saveGlobalConstraint = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const packageSkill = (data) => {
|
||||
return service({
|
||||
url: '/skills/packageSkill',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export const downloadOnlineSkill = (data) => {
|
||||
return service({
|
||||
url: '/skills/downloadOnlineSkill',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"/src/view/superAdmin/params/sysParams.vue": "SysParams",
|
||||
"/src/view/superAdmin/user/user.vue": "User",
|
||||
"/src/view/system/state.vue": "State",
|
||||
"/src/view/systemTools/apiToken/index.vue": "Index",
|
||||
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
|
||||
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
|
||||
"/src/view/systemTools/autoCode/index.vue": "AutoCode",
|
||||
@@ -72,7 +73,9 @@
|
||||
"/src/view/systemTools/formCreate/index.vue": "FormGenerator",
|
||||
"/src/view/systemTools/index.vue": "System",
|
||||
"/src/view/systemTools/installPlugin/index.vue": "Index",
|
||||
"/src/view/systemTools/loginLog/index.vue": "Index",
|
||||
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
||||
"/src/view/systemTools/skills/index.vue": "Skills",
|
||||
"/src/view/systemTools/sysError/sysError.vue": "SysError",
|
||||
"/src/view/systemTools/system/system.vue": "Config",
|
||||
"/src/view/systemTools/version/version.vue": "SysVersion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl border border-black/10 bg-white text-black/80 dark:text-slate-400 dark:bg-slate-900 dark:text-white/80"
|
||||
class="rounded-lg border border-black/10 bg-white text-black/80 dark:text-slate-400 dark:bg-slate-900 dark:text-white/80"
|
||||
:class="[customClass || '', withoutPadding ? 'p-0' : 'p-4']"
|
||||
>
|
||||
<div v-if="title" class="flex justify-between items-center">
|
||||
@@ -11,7 +11,7 @@
|
||||
v-if="showAction"
|
||||
class="text-xs text-black/60 dark:text-white/60 hover:text-active cursor-pointer"
|
||||
>
|
||||
查看更多
|
||||
更多
|
||||
</div>
|
||||
</div>
|
||||
<div :class="title ? 'mt-3' : ''">
|
||||
@@ -42,3 +42,4 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
@@ -1,72 +1,67 @@
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div
|
||||
v-for="(item, index) in notices"
|
||||
:key="index"
|
||||
class="flex items-center gap-3 py-1"
|
||||
>
|
||||
<div
|
||||
class="shrink-0 rounded-full border border-black/10 px-2 py-0.5 text-[11px] leading-4 text-black/70 dark:border-white/10 dark:text-white/70"
|
||||
>
|
||||
{{ item.typeTitle }}
|
||||
</div>
|
||||
<el-tooltip effect="light" :content="item.title" placement="top">
|
||||
<div class="min-w-0 text-xs text-black/70 dark:text-white/70 line-clamp-1">
|
||||
{{ item.title }}
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<el-scrollbar max-height="320px">
|
||||
<div class="space-y-2 pr-1">
|
||||
<div
|
||||
v-for="(item, index) in notices"
|
||||
:key="index"
|
||||
class="group rounded-lg border border-black/10 bg-white/70 p-3 transition-all duration-200 hover:-translate-y-0.5 hover:border-black/20 hover:shadow-sm dark:border-white/10 dark:bg-white/[0.02] dark:hover:border-white/20"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full" :class="item.dotClass" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="rounded-md px-2 py-0.5 text-[11px] font-semibold leading-4" :class="item.tagClass">
|
||||
{{ item.typeTitle }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] text-black/45 dark:text-white/45">{{ item.time }}</span>
|
||||
</div>
|
||||
<el-tooltip effect="light" :content="item.title" placement="top">
|
||||
<p class="mt-1.5 line-clamp-2 text-sm text-black/75 dark:text-white/75">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const notices = [
|
||||
{
|
||||
type: 'success',
|
||||
typeTitle: '通知',
|
||||
title: '授权后将进入专属飞书群,获取官方辅助。'
|
||||
time: '今天',
|
||||
title: '购买商业授权后可进入专属技术支持通道,加快问题排查和版本升级效率。',
|
||||
dotClass: 'bg-cyan-500',
|
||||
tagClass: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
typeTitle: '警告',
|
||||
title: '授权可获得插件市场极大优惠价格。'
|
||||
typeTitle: '活动',
|
||||
time: '2天前',
|
||||
title: '插件市场正在进行限时优惠活动,授权用户可获得更低的插件采购成本。',
|
||||
dotClass: 'bg-emerald-500',
|
||||
tagClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200'
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
typeTitle: '违规',
|
||||
title: '未授权商用将有可能被资源采集工具爬取并追责。'
|
||||
typeTitle: '合规',
|
||||
time: '3天前',
|
||||
title: '未授权商用存在合规风险,建议团队尽快完成授权以保障项目持续交付。',
|
||||
dotClass: 'bg-amber-500',
|
||||
tagClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
typeTitle: '信息',
|
||||
title: '再次感谢您对开源事业的支持'
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
typeTitle: '公告',
|
||||
title: '让创意更有价值。'
|
||||
},
|
||||
{
|
||||
type: 'success',
|
||||
typeTitle: '通知',
|
||||
title: '让劳动更有意义。'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
typeTitle: '警告',
|
||||
title: '让思维更有深度。'
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
typeTitle: '错误',
|
||||
title: '让生活更有趣味。'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
typeTitle: '信息',
|
||||
title: '让公司更有活力。'
|
||||
typeTitle: '服务',
|
||||
time: '5天前',
|
||||
title: '授权用户可获得官方长期维护承诺,包含安全修复与关键版本升级支持。',
|
||||
dotClass: 'bg-violet-500',
|
||||
tagClass: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
<template>
|
||||
<div class="mt-4 w-full">
|
||||
<div class="text-xs tracking-wide text-black/60 dark:text-white/60">快捷入口</div>
|
||||
<div class="mt-3 grid grid-cols-3 gap-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(item, index) in shortcuts"
|
||||
:key="index"
|
||||
class="flex flex-col items-center group cursor-pointer"
|
||||
@click="toPath(item)"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg border border-black/10 dark:border-white/10 flex items-center justify-center text-black/70 dark:text-white/70 group-hover:bg-[var(--el-color-primary)] group-hover:text-white transition-colors"
|
||||
<template>
|
||||
<div class="h-full space-y-5">
|
||||
<div>
|
||||
<div class="mb-2 text-xs tracking-wide text-black/55 dark:text-white/55">常用入口</div>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
v-for="(item, index) in shortcuts"
|
||||
:key="index"
|
||||
class="group flex w-full items-center gap-3 rounded-lg border border-black/10 bg-white/70 p-2.5 text-left transition-all duration-200 hover:border-[var(--el-color-primary)] hover:shadow-sm dark:border-white/10 dark:bg-white/[0.02]"
|
||||
type="button"
|
||||
@click="toPath(item)"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-black/70 dark:text-white/70">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<span
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-700 transition-colors group-hover:bg-[var(--el-color-primary)] group-hover:text-white dark:bg-slate-800 dark:text-slate-200"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</span>
|
||||
<span class="min-w-0 text-sm text-black/75 dark:text-white/75">{{ item.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-xs tracking-wide text-black/60 dark:text-white/60">最近访问</div>
|
||||
<div class="mt-3 grid grid-cols-3 gap-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(item, index) in recentVisits"
|
||||
:key="index"
|
||||
class="flex flex-col items-center group cursor-pointer"
|
||||
@click="openLink(item)"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg border border-black/10 dark:border-white/10 flex items-center justify-center text-black/70 dark:text-white/70 group-hover:bg-[var(--el-color-primary)] group-hover:text-white transition-colors"
|
||||
<div>
|
||||
<div class="mb-2 text-xs tracking-wide text-black/55 dark:text-white/55">常用外链</div>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="(item, index) in recentVisits"
|
||||
:key="index"
|
||||
class="flex w-full items-center justify-between rounded-lg border border-black/10 bg-white/70 px-3 py-2 text-left transition-all duration-200 hover:border-[var(--el-color-primary)] hover:shadow-sm dark:border-white/10 dark:bg-white/[0.02]"
|
||||
type="button"
|
||||
@click="openLink(item)"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-black/70 dark:text-white/70">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<span class="flex items-center gap-2 text-sm text-black/75 dark:text-white/75">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span class="text-xs text-black/45 dark:text-white/45">打开</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Menu,
|
||||
@@ -51,6 +53,7 @@
|
||||
Memo
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const toPath = (item) => {
|
||||
@@ -58,52 +61,22 @@
|
||||
}
|
||||
|
||||
const openLink = (item) => {
|
||||
window.open(item.path, '_blank')
|
||||
window.open(item.path, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
icon: Menu,
|
||||
title: '菜单管理',
|
||||
path: 'menu'
|
||||
},
|
||||
{
|
||||
icon: Link,
|
||||
title: 'API管理',
|
||||
path: 'api'
|
||||
},
|
||||
{
|
||||
icon: Service,
|
||||
title: '角色管理',
|
||||
path: 'authority'
|
||||
},
|
||||
{
|
||||
icon: User,
|
||||
title: '用户管理',
|
||||
path: 'user'
|
||||
},
|
||||
{
|
||||
icon: Files,
|
||||
title: '自动化包',
|
||||
path: 'autoPkg'
|
||||
},
|
||||
{
|
||||
icon: Memo,
|
||||
title: '自动代码',
|
||||
path: 'autoCode'
|
||||
}
|
||||
{ icon: Menu, title: '菜单管理', path: 'menu' },
|
||||
{ icon: Link, title: 'API管理', path: 'api' },
|
||||
{ icon: Service, title: '角色管理', path: 'authority' },
|
||||
{ icon: User, title: '用户管理', path: 'user' },
|
||||
{ icon: Files, title: '自动化包', path: 'autoPkg' },
|
||||
{ icon: Memo, title: '自动代码', path: 'autoCode' }
|
||||
]
|
||||
|
||||
const recentVisits = [
|
||||
{
|
||||
icon: Reading,
|
||||
title: '授权购买',
|
||||
path: 'https://plugin.gin-vue-admin.com/license'
|
||||
},
|
||||
{
|
||||
icon: Document,
|
||||
title: '插件市场',
|
||||
path: 'https://plugin.gin-vue-admin.com/#/layout/home'
|
||||
}
|
||||
{ icon: Reading, title: '授权购买', path: 'https://plugin.gin-vue-admin.com/license' },
|
||||
{ icon: Document, title: '插件市场', path: 'https://plugin.gin-vue-admin.com/#/layout/home' },
|
||||
{ icon: Link, title: '项目仓库', path: 'https://github.com/flipped-aurora/gin-vue-admin' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-full gva-container2 overflow-auto bg-white text-black dark:bg-slate-800 dark:text-white"
|
||||
>
|
||||
<div class="p-4 lg:p-6">
|
||||
<template>
|
||||
<div class="h-full gva-container2 overflow-auto bg-slate-50/60 dark:bg-slate-900">
|
||||
<div class="space-y-4 p-4 lg:p-6">
|
||||
<section
|
||||
class="relative overflow-hidden rounded-xl border border-slate-200/80 bg-white px-5 py-6 shadow-sm dark:border-slate-700 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900"
|
||||
>
|
||||
|
||||
<div class="relative flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs tracking-[0.2em] text-slate-500 dark:text-slate-400">DASHBOARD</p>
|
||||
<h1 class="mt-2 text-xl font-semibold text-slate-900 dark:text-slate-100 lg:text-2xl">
|
||||
欢迎回来,开始今天的Coding节奏
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ today }} · 已为你聚合核心业务数据、插件动态和系统公告
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" @click="goLicense">购买商业授权</el-button>
|
||||
<el-button @click="goPluginMarket">插件市场</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<gva-card>
|
||||
<gva-chart :type="1" title="访问人数" />
|
||||
@@ -15,41 +34,52 @@
|
||||
</gva-card>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-12 items-start">
|
||||
<div class="grid grid-cols-1 gap-4 xl:col-span-8 self-start content-start">
|
||||
<div class="grid grid-cols-1 items-stretch gap-4 xl:grid-cols-12">
|
||||
<div class="grid grid-cols-1 gap-4 content-start xl:col-span-8 xl:h-full">
|
||||
<gva-card title="内容数据">
|
||||
<gva-chart :type="4" />
|
||||
</gva-card>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<gva-card title="最新插件">
|
||||
<gva-plugin-table />
|
||||
</gva-card>
|
||||
</div>
|
||||
<gva-card title="最新插件">
|
||||
<gva-plugin-table />
|
||||
</gva-card>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<gva-card title="最新更新">
|
||||
<gva-table />
|
||||
</gva-card>
|
||||
</div>
|
||||
<gva-card title="最新更新">
|
||||
<gva-table />
|
||||
</gva-card>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 xl:col-span-4 self-start content-start">
|
||||
<gva-card title="快捷功能" show-action>
|
||||
<div class="flex flex-col gap-4 xl:col-span-4 xl:h-full">
|
||||
<gva-card title="快捷功能" show-action custom-class="min-h-[300px]">
|
||||
<gva-quick-link />
|
||||
</gva-card>
|
||||
<gva-card title="公告" show-action>
|
||||
<gva-card title="公告" show-action custom-class="min-h-[300px]">
|
||||
<gva-notice />
|
||||
</gva-card>
|
||||
<gva-card title="文档" show-action>
|
||||
<gva-card title="文档" show-action custom-class="min-h-[120px]">
|
||||
<gva-wiki />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
without-padding
|
||||
custom-class="overflow-hidden"
|
||||
<div
|
||||
class="relative min-h-[200px] flex-1 overflow-hidden rounded-lg border border-slate-200 bg-slate-900 p-5 text-white shadow-sm dark:border-slate-700"
|
||||
>
|
||||
<gva-banner />
|
||||
</gva-card>
|
||||
|
||||
<div class="relative">
|
||||
<div class="inline-flex rounded-full bg-white/10 px-3 py-1 text-xs">商业授权</div>
|
||||
<h3 class="mt-3 text-lg font-semibold">解锁完整商用支持与专属服务</h3>
|
||||
<p class="mt-2 text-sm text-slate-200/90">
|
||||
购买授权后可获得专属支持通道、插件优惠与商用合规保障,帮助团队更稳定地推进项目交付。
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-white/10 px-2.5 py-1">专属技术支持</span>
|
||||
<span class="rounded-full bg-white/10 px-2.5 py-1">插件优惠权益</span>
|
||||
<span class="rounded-full bg-white/10 px-2.5 py-1">商用授权凭证</span>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center gap-3">
|
||||
<el-button type="primary" @click="goLicense">立即购买</el-button>
|
||||
<el-button link class="!text-cyan-300" @click="goPluginMarket">查看插件市场</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,8 +95,7 @@
|
||||
GvaWiki,
|
||||
GvaNotice,
|
||||
GvaQuickLink,
|
||||
GvaCard,
|
||||
GvaBanner
|
||||
GvaCard
|
||||
} from './components'
|
||||
|
||||
const today = computed(() => {
|
||||
@@ -81,9 +110,19 @@
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
})
|
||||
|
||||
const goLicense = () => {
|
||||
window.open('https://plugin.gin-vue-admin.com/license', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
const goPluginMarket = () => {
|
||||
window.open('https://plugin.gin-vue-admin.com', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Dashboard'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
|
||||
@@ -112,6 +112,14 @@
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="user"
|
||||
type="primary"
|
||||
link
|
||||
@click="openAssignRoleDrawer(scope.row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
@@ -395,6 +403,35 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 分配给角色抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignRoleDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">分配角色 - {{ assignApiRow.description }}</span>
|
||||
<div>
|
||||
<el-button @click="assignRoleDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignRoleSubmitting" @click="confirmAssignRole">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该API的角色关联关系,并自动刷新Casbin缓存" />
|
||||
<el-tree
|
||||
ref="roleTreeRef"
|
||||
v-loading="assignRoleLoading"
|
||||
:data="authorityTreeData"
|
||||
:props="{ label: 'authorityName', children: 'children' }"
|
||||
node-key="authorityId"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -410,11 +447,14 @@
|
||||
syncApi,
|
||||
getApiGroups,
|
||||
ignoreApi,
|
||||
enterSyncApi
|
||||
enterSyncApi,
|
||||
getApiRoles,
|
||||
setApiRoles
|
||||
} from '@/api/api'
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref } from 'vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ExportExcel from '@/components/exportExcel/exportExcel.vue'
|
||||
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue'
|
||||
@@ -823,6 +863,52 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分配给角色
|
||||
const assignRoleDrawerVisible = ref(false)
|
||||
const assignApiRow = ref({})
|
||||
const authorityTreeData = ref([])
|
||||
const assignRoleLoading = ref(false)
|
||||
const assignRoleSubmitting = ref(false)
|
||||
const roleTreeRef = ref(null)
|
||||
|
||||
const openAssignRoleDrawer = async (row) => {
|
||||
assignApiRow.value = row
|
||||
assignRoleDrawerVisible.value = true
|
||||
assignRoleLoading.value = true
|
||||
const [authRes, rolesRes] = await Promise.all([
|
||||
getAuthorityList(),
|
||||
getApiRoles(row.path, row.method)
|
||||
])
|
||||
if (authRes.code === 0) {
|
||||
authorityTreeData.value = authRes.data
|
||||
}
|
||||
if (rolesRes.code === 0 && rolesRes.data) {
|
||||
nextTick(() => {
|
||||
roleTreeRef.value?.setCheckedKeys(rolesRes.data)
|
||||
})
|
||||
}
|
||||
assignRoleLoading.value = false
|
||||
}
|
||||
|
||||
const confirmAssignRole = async () => {
|
||||
assignRoleSubmitting.value = true
|
||||
try {
|
||||
const checkedKeys = roleTreeRef.value?.getCheckedKeys(false) || []
|
||||
const res = await setApiRoles({
|
||||
path: assignApiRow.value.path,
|
||||
method: assignApiRow.value.method,
|
||||
authorityIds: checkedKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignRoleDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignRoleSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
min-width="180"
|
||||
prop="authorityName"
|
||||
/>
|
||||
<el-table-column align="left" label="操作" width="460">
|
||||
<el-table-column align="left" label="操作" width="560">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="setting"
|
||||
@@ -29,6 +29,13 @@
|
||||
@click="openDrawer(scope.row)"
|
||||
>设置权限</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="user"
|
||||
type="primary"
|
||||
link
|
||||
@click="openAssignDrawer(scope.row)"
|
||||
>分配给用户</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
@@ -134,6 +141,65 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 分配给用户抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">分配用户 - {{ assignRow.authorityName }}</span>
|
||||
<div>
|
||||
<el-button @click="assignDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignSubmitting" @click="confirmAssign">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该角色的用户关联关系;若用户仅剩此一个角色,移除后其主角色保持不变" />
|
||||
<div class="gva-search-box">
|
||||
<el-form :inline="true" :model="userSearchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="userSearchInfo.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="userSearchInfo.nickName" placeholder="请输入昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="searchUserData">查 询</el-button>
|
||||
<el-button icon="refresh" @click="resetUserSearch">重 置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table
|
||||
ref="userTableRef"
|
||||
v-loading="assignLoading"
|
||||
:data="userTableData"
|
||||
row-key="ID"
|
||||
:default-sort="{ prop: 'ID', order: 'descending' }"
|
||||
@sort-change="sortChange"
|
||||
@select="handleSelect"
|
||||
@select-all="handleSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="ID" width="80" sortable="custom" />
|
||||
<el-table-column label="用户名" prop="userName" min-width="120" />
|
||||
<el-table-column label="昵称" prop="nickName" min-width="120" />
|
||||
</el-table>
|
||||
<div class="flex justify-center mt-4">
|
||||
<el-pagination
|
||||
:current-page="userSearchInfo.page"
|
||||
:page-size="userSearchInfo.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="userTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handleUserPageChange"
|
||||
@size-change="handleUserSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -143,17 +209,21 @@
|
||||
deleteAuthority,
|
||||
createAuthority,
|
||||
updateAuthority,
|
||||
copyAuthority
|
||||
copyAuthority,
|
||||
getUsersByAuthorityId,
|
||||
setRoleUsers
|
||||
} from '@/api/authority'
|
||||
import { getUserList } from '@/api/user'
|
||||
|
||||
import Menus from '@/view/superAdmin/authority/components/menus.vue'
|
||||
import Apis from '@/view/superAdmin/authority/components/apis.vue'
|
||||
import Datas from '@/view/superAdmin/authority/components/datas.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAppStore } from "@/pinia"
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
|
||||
defineOptions({
|
||||
name: 'Authority'
|
||||
@@ -403,6 +473,109 @@
|
||||
authorityForm.value && authorityForm.value.clearValidate()
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
|
||||
// 分配给用户
|
||||
const assignDrawerVisible = ref(false)
|
||||
const assignRow = ref({})
|
||||
const userTableData = ref([])
|
||||
const userTotal = ref(0)
|
||||
const userSearchInfo = ref({ page: 1, pageSize: 10, username: '', nickName: '', orderKey: 'id', desc: true })
|
||||
const assignLoading = ref(false)
|
||||
const assignSubmitting = ref(false)
|
||||
const userTableRef = ref(null)
|
||||
|
||||
const selectedUserIds = ref(new Set())
|
||||
|
||||
const openAssignDrawer = async (row) => {
|
||||
assignRow.value = row
|
||||
userSearchInfo.value = { page: 1, pageSize: 10, username: '', nickName: '' }
|
||||
selectedUserIds.value = new Set()
|
||||
assignDrawerVisible.value = true
|
||||
const res = await getUsersByAuthorityId(row.authorityId)
|
||||
if (res.code === 0 && res.data) {
|
||||
selectedUserIds.value = new Set(res.data)
|
||||
}
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const getUserData = async () => {
|
||||
assignLoading.value = true
|
||||
const res = await getUserList(userSearchInfo.value)
|
||||
if (res.code === 0) {
|
||||
userTableData.value = res.data.list
|
||||
userTotal.value = res.data.total
|
||||
await nextTick()
|
||||
userTableData.value.forEach((user) => {
|
||||
userTableRef.value && userTableRef.value.toggleRowSelection(user, selectedUserIds.value.has(user.ID))
|
||||
})
|
||||
}
|
||||
assignLoading.value = false
|
||||
}
|
||||
|
||||
const handleSelect = (selection, row) => {
|
||||
if (selection.some(u => u.ID === row.ID)) {
|
||||
selectedUserIds.value.add(row.ID)
|
||||
} else {
|
||||
selectedUserIds.value.delete(row.ID)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = (selection) => {
|
||||
const selectedIds = new Set(selection.map(u => u.ID))
|
||||
userTableData.value.forEach((user) => {
|
||||
if (selectedIds.has(user.ID)) {
|
||||
selectedUserIds.value.add(user.ID)
|
||||
} else {
|
||||
selectedUserIds.value.delete(user.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sortChange = ({ prop, order }) => {
|
||||
if (prop) {
|
||||
userSearchInfo.value.orderKey = prop === 'ID' ? 'id' : toSQLLine(prop)
|
||||
userSearchInfo.value.desc = order === 'descending'
|
||||
}
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const searchUserData = () => {
|
||||
userSearchInfo.value.page = 1
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const resetUserSearch = () => {
|
||||
userSearchInfo.value = { page: 1, pageSize: 10, username: '', nickName: '' }
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const handleUserPageChange = (page) => {
|
||||
userSearchInfo.value.page = page
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const handleUserSizeChange = (size) => {
|
||||
userSearchInfo.value.pageSize = size
|
||||
userSearchInfo.value.page = 1
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const confirmAssign = async () => {
|
||||
assignSubmitting.value = true
|
||||
try {
|
||||
const res = await setRoleUsers({
|
||||
authorityId: assignRow.value.authorityId,
|
||||
userIds: [...selectedUserIds.value]
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -90,6 +90,14 @@
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="user"
|
||||
@click="openAssignRoleDrawer(scope.row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@@ -508,6 +516,35 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 分配给角色抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignRoleDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">分配角色 - {{ assignMenuRow.meta?.title }}</span>
|
||||
<div>
|
||||
<el-button @click="assignRoleDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignRoleSubmitting" @click="confirmAssignRole">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该菜单的角色关联关系;作为角色首页的菜单不可取消勾选" />
|
||||
<el-tree
|
||||
ref="roleTreeRef"
|
||||
v-loading="assignRoleLoading"
|
||||
:data="authorityTreeData"
|
||||
:props="{ label: 'authorityName', children: 'children', disabled: isRoleDisabled }"
|
||||
node-key="authorityId"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -517,12 +554,15 @@
|
||||
getMenuList,
|
||||
addBaseMenu,
|
||||
deleteBaseMenu,
|
||||
getBaseMenuById
|
||||
getBaseMenuById,
|
||||
getMenuRoles,
|
||||
setMenuRoles
|
||||
} from '@/api/menu'
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import icon from '@/view/superAdmin/menu/icon.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { reactive, ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { QuestionFilled, InfoFilled, Delete } from '@element-plus/icons-vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
@@ -771,6 +811,63 @@
|
||||
setOptions()
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 分配给角色
|
||||
const assignRoleDrawerVisible = ref(false)
|
||||
const assignMenuRow = ref({})
|
||||
const authorityTreeData = ref([])
|
||||
const assignRoleLoading = ref(false)
|
||||
const assignRoleSubmitting = ref(false)
|
||||
const roleTreeRef = ref(null)
|
||||
const defaultRouterAuthorityIds = ref(new Set())
|
||||
|
||||
const isRoleDisabled = (data) => {
|
||||
return defaultRouterAuthorityIds.value.has(data.authorityId)
|
||||
}
|
||||
|
||||
const openAssignRoleDrawer = async (row) => {
|
||||
assignMenuRow.value = row
|
||||
defaultRouterAuthorityIds.value = new Set()
|
||||
assignRoleDrawerVisible.value = true
|
||||
assignRoleLoading.value = true
|
||||
// 并行加载角色树和当前菜单已分配的角色
|
||||
const [authRes, rolesRes] = await Promise.all([
|
||||
getAuthorityList(),
|
||||
getMenuRoles(row.ID)
|
||||
])
|
||||
if (authRes.code === 0) {
|
||||
authorityTreeData.value = authRes.data
|
||||
}
|
||||
if (rolesRes.code === 0 && rolesRes.data) {
|
||||
if (rolesRes.data.defaultRouterAuthorityIds) {
|
||||
defaultRouterAuthorityIds.value = new Set(rolesRes.data.defaultRouterAuthorityIds)
|
||||
}
|
||||
nextTick(() => {
|
||||
roleTreeRef.value?.setCheckedKeys(rolesRes.data.authorityIds || [])
|
||||
})
|
||||
}
|
||||
assignRoleLoading.value = false
|
||||
}
|
||||
|
||||
const confirmAssignRole = async () => {
|
||||
assignRoleSubmitting.value = true
|
||||
try {
|
||||
const checkedKeys = roleTreeRef.value?.getCheckedKeys(false) || []
|
||||
const halfCheckedKeys = roleTreeRef.value?.getHalfCheckedKeys() || []
|
||||
const authorityIds = [...checkedKeys, ...halfCheckedKeys]
|
||||
const res = await setMenuRoles({
|
||||
menuId: assignMenuRow.value.ID,
|
||||
authorityIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignRoleDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignRoleSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
>新增用户</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table :data="tableData" row-key="ID">
|
||||
<el-table :data="tableData" row-key="ID" :default-sort="{ prop: 'ID', order: 'descending' }" @sort-change="sortChange">
|
||||
<el-table-column align="left" label="头像" min-width="75">
|
||||
<template #default="scope">
|
||||
<CustomPic style="margin-top: 8px" :pic-src="scope.row.headerImg" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="ID" min-width="50" prop="ID" />
|
||||
<el-table-column align="left" label="ID" min-width="50" prop="ID" sortable="custom" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="用户名"
|
||||
@@ -267,6 +267,7 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import SelectImage from '@/components/selectImage/selectImage.vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
|
||||
defineOptions({
|
||||
name: 'User'
|
||||
@@ -293,6 +294,8 @@
|
||||
phone: '',
|
||||
email: ''
|
||||
}
|
||||
orderKey.value = 'id'
|
||||
desc.value = true
|
||||
getTableData()
|
||||
}
|
||||
// 初始化相关
|
||||
@@ -321,6 +324,16 @@
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const orderKey = ref('id')
|
||||
const desc = ref(true)
|
||||
|
||||
const sortChange = ({ prop, order }) => {
|
||||
if (prop) {
|
||||
orderKey.value = prop === 'ID' ? 'id' : toSQLLine(prop)
|
||||
desc.value = order === 'descending'
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
@@ -337,6 +350,8 @@
|
||||
const table = await getUserList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
orderKey: orderKey.value,
|
||||
desc: desc.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
<template>
|
||||
<fc-designer ref="designer" :config="config" height="calc(100vh - 160px)" />
|
||||
<div class="form-designer-container">
|
||||
<fc-designer ref="designer" :config="config" height="calc(100vh - 160px)">
|
||||
<template #handle>
|
||||
<el-button type="primary" size="small" plain @click="exportVueTemplate">
|
||||
解析为 Vue 原生标签
|
||||
</el-button>
|
||||
</template>
|
||||
</fc-designer>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="生成的 Vue 模板代码" width="70%" top="5vh">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="25"
|
||||
v-model="vueCode"
|
||||
readonly
|
||||
class="code-input"
|
||||
resize="none"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="copyCode">一键复制</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,8 +37,172 @@
|
||||
})
|
||||
|
||||
const designer = ref(null)
|
||||
const dialogVisible = ref(false)
|
||||
const vueCode = ref('')
|
||||
|
||||
const config = {
|
||||
fieldReadonly: false
|
||||
fieldReadonly: false,
|
||||
useTemplate: true
|
||||
}
|
||||
|
||||
const kebabCase = (str) => {
|
||||
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
}
|
||||
|
||||
const generateVueCode = (rules, options) => {
|
||||
let formDataInit = []
|
||||
let formRules = []
|
||||
|
||||
const parseRule = (rule) => {
|
||||
if (rule.type === 'row') {
|
||||
const propsStr = rule.props ? Object.entries(rule.props).map(([k, v]) => `:${k}="${v}"`).join(' ') : ''
|
||||
let childrenStr = rule.children ? rule.children.map(c => parseRule(c)).join('\n') : ''
|
||||
return `\n <el-row ${propsStr}>${childrenStr}\n </el-row>`
|
||||
}
|
||||
if (rule.type === 'col') {
|
||||
const propsStr = rule.props ? Object.entries(rule.props).map(([k, v]) => `:${k}="${v}"`).join(' ') : ''
|
||||
let childrenStr = rule.children ? rule.children.map(c => parseRule(c)).join('\n') : ''
|
||||
return `\n <el-col ${propsStr}>${childrenStr}\n </el-col>`
|
||||
}
|
||||
|
||||
if (!rule.field) return ''
|
||||
|
||||
let tag = rule.type
|
||||
|
||||
const typeMap = {
|
||||
input: 'el-input',
|
||||
inputNumber: 'el-input-number',
|
||||
select: 'el-select',
|
||||
radio: 'el-radio-group',
|
||||
checkbox: 'el-checkbox-group',
|
||||
switch: 'el-switch',
|
||||
timePicker: 'el-time-picker',
|
||||
datePicker: 'el-date-picker',
|
||||
slider: 'el-slider',
|
||||
rate: 'el-rate',
|
||||
colorPicker: 'el-color-picker',
|
||||
cascader: 'el-cascader',
|
||||
upload: 'el-upload'
|
||||
}
|
||||
|
||||
const elTag = typeMap[tag] || (tag.startsWith('el-') ? tag : `el-${tag}`)
|
||||
|
||||
let propsStr = ''
|
||||
if (rule.props) {
|
||||
for (const [key, value] of Object.entries(rule.props)) {
|
||||
if (value === null || value === undefined) continue
|
||||
if (typeof value === 'boolean') {
|
||||
propsStr += value ? ` ${kebabCase(key)}` : ` :${kebabCase(key)}="false"`
|
||||
} else if (typeof value === 'string') {
|
||||
propsStr += ` ${kebabCase(key)}="${value}"`
|
||||
} else {
|
||||
propsStr += ` :${kebabCase(key)}='${JSON.stringify(value)}'`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let innerContent = ''
|
||||
if (rule.options && Array.isArray(rule.options)) {
|
||||
if (tag === 'select') {
|
||||
innerContent = rule.options.map(opt => `\n <el-option label="${opt.label}" value="${opt.value}" />`).join('') + '\n '
|
||||
} else if (tag === 'radio') {
|
||||
innerContent = rule.options.map(opt => `\n <el-radio label="${opt.value}">${opt.label}</el-radio>`).join('') + '\n '
|
||||
} else if (tag === 'checkbox') {
|
||||
innerContent = rule.options.map(opt => `\n <el-checkbox label="${opt.value}">${opt.label}</el-checkbox>`).join('') + '\n '
|
||||
}
|
||||
}
|
||||
|
||||
let initVal = rule.value !== undefined ? rule.value : (tag === 'checkbox' ? [] : null)
|
||||
formDataInit.push(` ${rule.field}: ${JSON.stringify(initVal)}`)
|
||||
|
||||
if (rule.$required || (rule.effect && rule.effect.required)) {
|
||||
formRules.push(` ${rule.field}: [{ required: true, message: '${rule.title}不能为空', trigger: 'blur' }]`)
|
||||
} else if (rule.validate) {
|
||||
formRules.push(` ${rule.field}: ${JSON.stringify(rule.validate)}`)
|
||||
}
|
||||
|
||||
return `
|
||||
<el-form-item label="${rule.title}" prop="${rule.field}">
|
||||
<${elTag} v-model="formData.${rule.field}"${propsStr}>${innerContent}</${elTag}>
|
||||
</el-form-item>`
|
||||
}
|
||||
|
||||
const formItems = rules.map(parseRule).join('')
|
||||
|
||||
const formConfig = options.form || {}
|
||||
let formPropsStr = []
|
||||
if (formConfig.labelWidth) formPropsStr.push(`label-width="${formConfig.labelWidth}"`)
|
||||
if (formConfig.size) formPropsStr.push(`size="${formConfig.size}"`)
|
||||
if (formConfig.labelPosition) formPropsStr.push(`label-position="${formConfig.labelPosition}"`)
|
||||
if (formConfig.hideRequiredAsterisk) formPropsStr.push(`hide-required-asterisk`)
|
||||
|
||||
// 8. 拼装成标准的 <template> 和 <script setup> 闭环代码
|
||||
return `<template>
|
||||
<div>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" ${formPropsStr.join(' ')}>
|
||||
${formItems}
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const formData = reactive({
|
||||
${formDataInit.join(',\n')}
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
${formRules.join(',\n')}
|
||||
})
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('表单校验通过,准备提交')
|
||||
console.log('提交的数据: ', formData)
|
||||
} else {
|
||||
ElMessage.error('表单校验失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
if (!formRef.value) return
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
<\/script>
|
||||
`
|
||||
}
|
||||
|
||||
const exportVueTemplate = () => {
|
||||
const rules = designer.value.getRule()
|
||||
const options = designer.value.getOption()
|
||||
|
||||
vueCode.value = generateVueCode(rules, options)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(vueCode.value)
|
||||
ElMessage.success('代码已成功复制到剪贴板!')
|
||||
dialogVisible.value = false
|
||||
} catch (err) {
|
||||
ElMessage.error('复制失败,请手动选择复制')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<warning-bar
|
||||
href="https://plugin.gin-vue-admin.com/license"
|
||||
@@ -34,7 +34,10 @@
|
||||
<el-card shadow="never" class="!border-none flex-1 mt-2 flex flex-col min-h-0">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-bold">Skills</span>
|
||||
<el-button type="primary" link icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||
<div class="flex gap-1">
|
||||
<el-button type="primary" link icon="Download" @click="openOnlineDrawer">在线</el-button>
|
||||
<el-button type="primary" link icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="skillFilter"
|
||||
@@ -52,8 +55,20 @@
|
||||
:index="skill"
|
||||
class="!h-10 !leading-10 !my-1 !mx-1 !rounded-[4px]"
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="truncate" :title="skill">{{ skill }}</span>
|
||||
<div class="w-full flex items-center justify-between min-w-0">
|
||||
<div class="flex items-center min-w-0 gap-1">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="truncate" :title="skill">{{ skill }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
icon="Delete"
|
||||
@click.stop="handleDeleteSkill(skill)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
@@ -73,7 +88,10 @@
|
||||
<span>{{ activeSkill }}</span>
|
||||
<el-tag size="small" type="info">Skill</el-tag>
|
||||
</div>
|
||||
<el-button type="primary" icon="Check" @click="saveCurrentSkill">保存配置</el-button>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button icon="Download" @click="packageCurrentSkill">打包</el-button>
|
||||
<el-button type="primary" icon="Check" @click="saveCurrentSkill">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="h-full">
|
||||
@@ -375,6 +393,28 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="downloadTargetDialogVisible" title="选择下载目标" width="420px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="下载到">
|
||||
<el-select v-model="downloadTarget" placeholder="请选择工具" class="w-full">
|
||||
<el-option
|
||||
v-for="item in downloadTargetOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="text-xs text-gray-500">
|
||||
可下载到单个 AI 工具,也可选择“全部工具”。
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeDownloadTargetDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmDownloadSkill">开始下载</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="editorVisible" size="70%" destroy-on-close :with-header="false">
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -398,6 +438,86 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 在线 Skills 抽屉 -->
|
||||
<el-drawer
|
||||
v-model="onlineDrawerVisible"
|
||||
size="90%"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">在线 Skills</span>
|
||||
<el-button @click="onlineDrawerVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-4">
|
||||
<el-form :inline="true" :model="onlineSearchInfo">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="onlineSearchInfo.name" placeholder="搜索技能名称" clearable @keyup.enter="searchOnlineSkills" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="searchOnlineSkills">查 询</el-button>
|
||||
<el-button icon="Refresh" @click="resetOnlineSearch">重 置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="onlineLoading" :data="onlineSkillList" stripe>
|
||||
<el-table-column label="封面" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.picture"
|
||||
:src="row.picture"
|
||||
style="width: 50px; height: 50px"
|
||||
fit="cover"
|
||||
class="rounded"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" prop="name" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<a
|
||||
class="text-blue-500 hover:text-blue-700 cursor-pointer"
|
||||
:href="`https://plugin.gin-vue-admin.com/details/${row.ID}`"
|
||||
target="_blank"
|
||||
>{{ row.name }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="简介" prop="resume" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="版本" prop="actVersion" width="100" />
|
||||
<el-table-column label="下载量" prop="downloadCount" width="90" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.money === 0"
|
||||
type="primary"
|
||||
link
|
||||
icon="Download"
|
||||
:loading="downloadingIds.has(row.ID)"
|
||||
@click="handleDownloadSkill(row)"
|
||||
>下载</el-button>
|
||||
<a
|
||||
v-else
|
||||
class="text-blue-500 hover:text-blue-700 text-sm"
|
||||
:href="`https://plugin.gin-vue-admin.com/details/${row.ID}`"
|
||||
target="_blank"
|
||||
>去购买</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-center mt-4">
|
||||
<el-pagination
|
||||
:current-page="onlineSearchInfo.page"
|
||||
:page-size="onlineSearchInfo.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="onlineTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handleOnlinePageChange"
|
||||
@size-change="handleOnlineSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -411,6 +531,7 @@
|
||||
getSkillList,
|
||||
getSkillDetail,
|
||||
saveSkill,
|
||||
deleteSkill,
|
||||
createSkillScript,
|
||||
getSkillScript,
|
||||
saveSkillScript,
|
||||
@@ -424,8 +545,11 @@
|
||||
getSkillTemplate,
|
||||
saveSkillTemplate,
|
||||
getGlobalConstraint,
|
||||
saveGlobalConstraint
|
||||
saveGlobalConstraint,
|
||||
packageSkill,
|
||||
downloadOnlineSkill
|
||||
} from '@/api/skills'
|
||||
import { getShopPluginList } from '@/api/plugin/api'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import 'ace-builds/src-noconflict/mode-javascript'
|
||||
import 'ace-builds/src-noconflict/mode-python'
|
||||
@@ -650,6 +774,37 @@
|
||||
loadSkillDetail(skillName)
|
||||
}
|
||||
|
||||
async function handleDeleteSkill(skillName) {
|
||||
if (!activeTool.value || !skillName) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除技能「${skillName}」吗?将同时删除其 scripts/resources/references/templates 文件。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await deleteSkill({ tool: activeTool.value, skill: skillName })
|
||||
if (res.code !== 0) {
|
||||
return
|
||||
}
|
||||
if (activeSkill.value === skillName) {
|
||||
resetDetail()
|
||||
}
|
||||
await loadSkills()
|
||||
ElMessage.success('删除成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
newSkill.name = ''
|
||||
newSkill.description = ''
|
||||
@@ -733,6 +888,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
function extractFileNameFromDisposition(disposition) {
|
||||
if (!disposition) return ''
|
||||
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf8Match?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Match[1])
|
||||
} catch (e) {
|
||||
return utf8Match[1]
|
||||
}
|
||||
}
|
||||
const normalMatch = disposition.match(/filename="?([^";]+)"?/i)
|
||||
return normalMatch?.[1] || ''
|
||||
}
|
||||
|
||||
async function packageCurrentSkill() {
|
||||
if (!activeTool.value || !activeSkill.value) {
|
||||
ElMessage.warning('请先选择技能')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await packageSkill({ tool: activeTool.value, skill: activeSkill.value })
|
||||
const blob = res instanceof Blob ? res : (res?.data instanceof Blob ? res.data : null)
|
||||
if (!blob) {
|
||||
ElMessage.error('打包失败')
|
||||
return
|
||||
}
|
||||
const contentType = String(res?.headers?.['content-type'] || blob.type || '').toLowerCase()
|
||||
const disposition = String(res?.headers?.['content-disposition'] || '')
|
||||
const isZipResponse = contentType.includes('application/zip') || disposition.toLowerCase().includes('filename=')
|
||||
const isErrorBlob = contentType.includes('application/json') || contentType.includes('text/plain')
|
||||
if (!isZipResponse || isErrorBlob) {
|
||||
let msg = '打包失败'
|
||||
try {
|
||||
const text = await blob.text()
|
||||
if (text) {
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
msg = json?.msg || msg
|
||||
} catch (e) {
|
||||
msg = text
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse error
|
||||
}
|
||||
ElMessage.error(msg)
|
||||
return
|
||||
}
|
||||
const fileName = extractFileNameFromDisposition(disposition) || `${activeSkill.value}.zip`
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('打包成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('打包失败')
|
||||
}
|
||||
}
|
||||
|
||||
function appendMarkdown(content) {
|
||||
form.markdown = `${form.markdown || ''}${content}`
|
||||
}
|
||||
@@ -1064,4 +1282,179 @@
|
||||
function skillsFilesToRows(list) {
|
||||
return (list || []).map((name) => ({ name }))
|
||||
}
|
||||
|
||||
// ===== 在线 Skills =====
|
||||
const onlineDrawerVisible = ref(false)
|
||||
const onlineSkillList = ref([])
|
||||
const onlineTotal = ref(0)
|
||||
const onlineSearchInfo = reactive({ page: 1, pageSize: 10, name: '' })
|
||||
const onlineLoading = ref(false)
|
||||
const downloadingIds = reactive(new Set())
|
||||
const downloadTargetDialogVisible = ref(false)
|
||||
const downloadTarget = ref('')
|
||||
const downloadRow = ref(null)
|
||||
|
||||
const ALL_TOOLS_DOWNLOAD_TARGET = '__all__'
|
||||
|
||||
const downloadTargetOptions = computed(() => {
|
||||
const options = tools.value.map((item) => ({
|
||||
label: item.label || item.key,
|
||||
value: item.key
|
||||
}))
|
||||
options.push({
|
||||
label: '全部工具',
|
||||
value: ALL_TOOLS_DOWNLOAD_TARGET
|
||||
})
|
||||
return options
|
||||
})
|
||||
|
||||
const pluginMarketLoginURL = 'https://plugin.gin-vue-admin.com'
|
||||
|
||||
const isPluginMarketAuthError = (message) => {
|
||||
const msg = (message || '').toString()
|
||||
return msg.includes('插件市场登录') || msg.includes('401')
|
||||
}
|
||||
|
||||
const promptPluginMarketLogin = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('请先登录插件市场后再下载技能,是否现在前往登录?', '提示', {
|
||||
confirmButtonText: '前往插件市场',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
window.open(pluginMarketLoginURL, '_blank')
|
||||
} catch (e) {
|
||||
// 用户取消时不需要额外提示
|
||||
}
|
||||
}
|
||||
|
||||
const openOnlineDrawer = () => {
|
||||
onlineSearchInfo.page = 1
|
||||
onlineSearchInfo.pageSize = 10
|
||||
onlineSearchInfo.name = ''
|
||||
onlineDrawerVisible.value = true
|
||||
getOnlineSkills()
|
||||
}
|
||||
|
||||
const getOnlineSkills = async () => {
|
||||
onlineLoading.value = true
|
||||
const res = await getShopPluginList({
|
||||
page: onlineSearchInfo.page,
|
||||
pageSize: onlineSearchInfo.pageSize,
|
||||
category: 6,
|
||||
name: onlineSearchInfo.name || undefined,
|
||||
updateTime: 1
|
||||
})
|
||||
if (res.code === 0) {
|
||||
onlineSkillList.value = res.data.list
|
||||
onlineTotal.value = res.data.total
|
||||
}
|
||||
onlineLoading.value = false
|
||||
}
|
||||
|
||||
const searchOnlineSkills = () => {
|
||||
onlineSearchInfo.page = 1
|
||||
getOnlineSkills()
|
||||
}
|
||||
|
||||
const resetOnlineSearch = () => {
|
||||
onlineSearchInfo.name = ''
|
||||
onlineSearchInfo.page = 1
|
||||
getOnlineSkills()
|
||||
}
|
||||
|
||||
const handleOnlinePageChange = (page) => {
|
||||
onlineSearchInfo.page = page
|
||||
getOnlineSkills()
|
||||
}
|
||||
|
||||
const handleOnlineSizeChange = (size) => {
|
||||
onlineSearchInfo.pageSize = size
|
||||
onlineSearchInfo.page = 1
|
||||
getOnlineSkills()
|
||||
}
|
||||
|
||||
const getToolLabel = (key) => {
|
||||
return tools.value.find((item) => item.key === key)?.label || key
|
||||
}
|
||||
|
||||
const closeDownloadTargetDialog = () => {
|
||||
downloadTargetDialogVisible.value = false
|
||||
downloadRow.value = null
|
||||
}
|
||||
|
||||
const handleDownloadSkill = (row) => {
|
||||
downloadRow.value = row
|
||||
downloadTarget.value = activeTool.value || tools.value[0]?.key || ''
|
||||
downloadTargetDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDownloadSkill = async () => {
|
||||
if (!downloadRow.value) {
|
||||
ElMessage.warning('未找到待下载技能')
|
||||
return
|
||||
}
|
||||
const targetTools = downloadTarget.value === ALL_TOOLS_DOWNLOAD_TARGET
|
||||
? tools.value.map((item) => item.key).filter(Boolean)
|
||||
: [downloadTarget.value].filter(Boolean)
|
||||
if (!targetTools.length) {
|
||||
ElMessage.warning('请选择下载目标')
|
||||
return
|
||||
}
|
||||
|
||||
const row = downloadRow.value
|
||||
closeDownloadTargetDialog()
|
||||
downloadingIds.add(row.ID)
|
||||
const successTools = []
|
||||
const failedTools = []
|
||||
try {
|
||||
for (const tool of targetTools) {
|
||||
try {
|
||||
const res = await downloadOnlineSkill({ tool, id: row.ID, version: row.actVersion })
|
||||
if (res.code === 0) {
|
||||
successTools.push(tool)
|
||||
continue
|
||||
}
|
||||
if (isPluginMarketAuthError(res.msg)) {
|
||||
await promptPluginMarketLogin()
|
||||
return
|
||||
}
|
||||
failedTools.push(`${getToolLabel(tool)}: ${res.msg || '下载失败'}`)
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.msg || e?.message || ''
|
||||
if (e?.response?.status === 401 || isPluginMarketAuthError(msg)) {
|
||||
await promptPluginMarketLogin()
|
||||
return
|
||||
}
|
||||
failedTools.push(`${getToolLabel(tool)}: 下载失败`)
|
||||
}
|
||||
}
|
||||
|
||||
if (successTools.includes(activeTool.value)) {
|
||||
await loadSkills()
|
||||
}
|
||||
|
||||
if (failedTools.length === 0) {
|
||||
const successLabels = successTools.map((tool) => getToolLabel(tool)).join('、')
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: targetTools.length > 1 ? `${row.name} 已下载到:${successLabels}` : `${row.name} 下载成功`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (successTools.length === 0) {
|
||||
ElMessage({ type: 'error', message: failedTools[0] || '下载失败,请重试' })
|
||||
return
|
||||
}
|
||||
const successLabels = successTools.map((tool) => getToolLabel(tool)).join('、')
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${row.name} 部分下载成功。成功:${successLabels};失败:${failedTools.join(';')}`
|
||||
})
|
||||
} finally {
|
||||
downloadingIds.delete(row.ID)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="Oss类型">
|
||||
<el-select v-model="config.system['oss-type']" class="w-full">
|
||||
<el-option value="local">本地</el-option>
|
||||
<el-option value="qiniu">七牛</el-option>
|
||||
<el-option value="tencent-cos">腾讯云COS</el-option>
|
||||
<el-option value="aliyun-oss">阿里云OSS</el-option>
|
||||
<el-option value="huawei-obs">华为云OBS</el-option>
|
||||
<el-option value="cloudflare-r2">cloudflare R2</el-option>
|
||||
<el-option value="local" label="本地" />
|
||||
<el-option value="qiniu" label="七牛" />
|
||||
<el-option value="tencent-cos" label="腾讯云COS" />
|
||||
<el-option value="aliyun-oss" label="阿里云OSS" />
|
||||
<el-option value="huawei-obs" label="华为云OBS" />
|
||||
<el-option value="cloudflare-r2" label="cloudflare R2" />
|
||||
<el-option value="minio">MinIO</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="多点登录拦截">
|
||||
@@ -857,6 +858,48 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="config.system['oss-type'] === 'minio'">
|
||||
<h2>MinIO上传配置</h2>
|
||||
<el-form-item label="Endpoint">
|
||||
<el-input
|
||||
v-model.trim="config.minio.endpoint"
|
||||
placeholder="请输入Endpoint,如 127.0.0.1:9000"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Access Key ID">
|
||||
<el-input
|
||||
v-model.trim="config.minio['access-key-id']"
|
||||
placeholder="请输入Access Key ID"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Access Key Secret">
|
||||
<el-input
|
||||
v-model.trim="config.minio['access-key-secret']"
|
||||
placeholder="请输入Access Key Secret"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储桶名称">
|
||||
<el-input
|
||||
v-model.trim="config.minio['bucket-name']"
|
||||
placeholder="请输入存储桶名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="访问域名">
|
||||
<el-input
|
||||
v-model.trim="config.minio['bucket-url']"
|
||||
placeholder="请输入访问域名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Base Path">
|
||||
<el-input
|
||||
v-model.trim="config.minio['base-path']"
|
||||
placeholder="请输入Base Path"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启SSL">
|
||||
<el-switch v-model="config.minio['use-ssl']" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Excel上传配置" name="11" class="mt-3.5">
|
||||
<el-form-item label="合成目标地址">
|
||||
@@ -999,6 +1042,7 @@
|
||||
'aliyun-oss': {},
|
||||
'hua-wei-obs': {},
|
||||
'cloudflare-r2': {},
|
||||
minio: {},
|
||||
captcha: {},
|
||||
zap: {},
|
||||
local: {},
|
||||
|
||||
+6
-10
@@ -7,7 +7,7 @@ import vuePlugin from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueFilePathPlugin from './vitePlugin/componentName/index.js'
|
||||
import { svgBuilder } from 'vite-auto-import-svg'
|
||||
import vueRootValidator from 'vite-check-multiple-dom';
|
||||
import vueRootValidator from 'vite-check-multiple-dom'
|
||||
import { AddSecret } from './vitePlugin/secret'
|
||||
import UnoCSS from '@unocss/vite'
|
||||
|
||||
@@ -15,7 +15,6 @@ import UnoCSS from '@unocss/vite'
|
||||
export default ({ mode }) => {
|
||||
AddSecret('')
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
|
||||
viteLogo(env)
|
||||
|
||||
const timestamp = Date.parse(new Date())
|
||||
@@ -37,9 +36,9 @@ export default ({ mode }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const base = "/"
|
||||
const root = "./"
|
||||
const outDir = "dist"
|
||||
const base = '/'
|
||||
const root = './'
|
||||
const outDir = 'dist'
|
||||
|
||||
const config = {
|
||||
base: base, // 编译后js导入的资源路径
|
||||
@@ -48,9 +47,6 @@ export default ({ mode }) => {
|
||||
resolve: {
|
||||
alias
|
||||
},
|
||||
define: {
|
||||
'process.env': {}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
@@ -72,12 +68,12 @@ export default ({ mode }) => {
|
||||
rewrite: (path) =>
|
||||
path.replace(new RegExp('^' + env.VITE_BASE_API), '')
|
||||
},
|
||||
"/plugin": {
|
||||
'/plugin': {
|
||||
// 需要代理的路径 例如 '/api'
|
||||
target: `https://plugin.gin-vue-admin.com/api/`, // 代理到 目标路径
|
||||
changeOrigin: true,
|
||||
rewrite: (path) =>
|
||||
path.replace(new RegExp("^/plugin"), '')
|
||||
path.replace(new RegExp('^/plugin'), '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ const extractComponentName = (fileContent) => {
|
||||
// Vite 插件定义
|
||||
const vueFilePathPlugin = (outputFilePath) => {
|
||||
let root
|
||||
|
||||
let isDev = false
|
||||
const generatePathNameMap = () => {
|
||||
const vueFiles = [
|
||||
...getAllVueFiles(path.join(root, 'src/view')),
|
||||
@@ -70,12 +70,15 @@ const vueFilePathPlugin = (outputFilePath) => {
|
||||
name: 'vue-file-path-plugin',
|
||||
configResolved(resolvedConfig) {
|
||||
root = resolvedConfig.root
|
||||
if (resolvedConfig.mode === 'development') {
|
||||
isDev = true
|
||||
}
|
||||
},
|
||||
buildStart() {
|
||||
generatePathNameMap()
|
||||
},
|
||||
buildEnd() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDev) {
|
||||
watchDirectoryChanges()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user