feat(project): 添加vben5前端

This commit is contained in:
wcg
2026-01-04 13:45:07 +08:00
parent 2c0689fe02
commit 51ee3fb460
839 changed files with 74231 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@@ -0,0 +1,5 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "vbenjs/vue-vben-admin" }
],
"commit": false,
"fixed": [["@vben-core/*", "@vben/*"]],
"snapshot": {
"prereleaseTemplate": "{tag}-{datetime}"
},
"privatePackages": { "version": true, "tag": true },
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/commitlint-config';

View File

@@ -0,0 +1,7 @@
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip

View File

@@ -0,0 +1,18 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
trim_trailing_whitespace = true
quote_type = single
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

11
Yi.Vben5.Vue3/.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

2
Yi.Vben5.Vue3/.gitconfig Normal file
View File

@@ -0,0 +1,2 @@
[core]
ignorecase = false

54
Yi.Vben5.Vue3/.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
node_modules
.DS_Store
dist
dist-ssr
dist.zip
dist.tar
dist.war
.nitro
.output
*-dist.zip
*-dist.tar
*-dist.war
coverage
*.local
**/.vitepress/cache
.cache
.turbo
.temp
dev-dist
.stylelintcache
yarn.lock
package-lock.json
pnpm-lock.yaml
.VSCodeCounter
**/backend-mock/data
# local env files
.env.local
.env.*.local
.eslintcache
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
vite.config.mts.*
vite.config.mjs.*
vite.config.js.*
vite.config.ts.*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 排除自动生成的类型文件
apps/web-antd/types/components.d.ts
.history

View File

@@ -0,0 +1,6 @@
ports:
- port: 5555
onOpen: open-preview
tasks:
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

View File

@@ -0,0 +1 @@
22.1.0

13
Yi.Vben5.Vue3/.npmrc Normal file
View File

@@ -0,0 +1,13 @@
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*
public-hoist-pattern[]=czg
strict-peer-dependencies=false
auto-install-peers=true
dedupe-peer-dependents=true

View File

@@ -0,0 +1,18 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

View File

@@ -0,0 +1 @@
export { default } from '@vben/prettier-config';

View File

@@ -0,0 +1,4 @@
dist
public
__tests__
coverage

323
Yi.Vben5.Vue3/CHANGELOG.md Normal file
View File

@@ -0,0 +1,323 @@
# 1.4.1
**FEATURES**
- Tinymce添加在antd原生表单/useVbenForm下的校验样式
- useVbenForm 增加 Cascader(级联选择器) 组件
**BUG FIX**
- 菜单管理 路由地址的必填项不生效
- withDefaultPlaceholder中placeholder 在keepalive & 语言切换 & tab切换 显示不变的问题
**REFACTOR**
- 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理
- 代码生成 字典下拉加载 改为每次进入编辑页面都加载
- ~~个人中心 账号绑定 样式/逻辑重构~~(回滚了 既要又要的问题)
- ~~个人中心 下拉卡片 昵称超长省略显示~~(回滚了 既要又要的问题)
# 1.4.0
**FEATURES**
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
-
- 菜单管理 级联删除 删除菜单和children
**REFACTOR**
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
- 流程图预览改为logicflow预览而非图片 ...然后后端又更新了 又改成iframe了
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
# 1.3.6
**BUG FIX**
- oss配置switch切换 导致报错`存储类型找不到`
- 文件上传无法正确清除(innerList)
# 1.3.5
**BUG FIX**
- 某些带Vxe表格弹窗 关闭后没有正常清理表格数据的问题
# 1.3.4
**BUG FIX**
- 文件上传多次触发导致数据不一致 https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC3BK6
**PREFORMANCE**
- 浏览器返回按钮/手势操作时 弹窗不会被关闭(keepAlive导致)
# 1.3.3
**BUG FIX**
- 工作流list展示在开启缩放会有误差导致触底逻辑不会触发
**OTHER**
- 代码生成预览对模板的提示...(下载都懒得点一下吗)
# 1.3.2
**REFACTOR**
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
- 菜单图标更新了一部分 sql同步更新
**OTHER**
- 暂时锁死vite依赖 i18n会报错
# 1.3.1
**REFACTOR**
- 所有Modal/Drawer表单关闭前会进行表单数据对比来弹出提示框
- 字典项颜色选择从`原生input type=color`改为`vue3-colorpicker`组件
- 全局Header: ClientID 更改大小写 [spring的问题导致](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS)
**BUG FIX**
- getVxePopupContainer逻辑调整 解决表格固定高度展开不全的问题
**FEATURES**
- 字典渲染支持loading(length为0情况)
**OTHERS**
- useForm的组件改为异步导入(官方更新) bootstrap.js体积从2M降到600K 首屏加载速度提升
# 1.3.0
注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用
- `component: 'ImageUploadOld'`
- `component: 'FileUploadOld'`
代替 **建议替换为新版本**
大致变动:
- `accept string[] -> string`
- `resultField 已经移除 统一使用ossId`
- `maxNumber -> maxCount`
具体参数查看: `apps/web-antd/src/components/upload/src/props.d.ts`
不再推荐使用useDescription, 这个版本会标记为@deprecated, 下个次版本将会移除
框架所有使用useDescription组件的会替换为原生(TODO)
**REFACTOR**
- **文件上传/图片上传重构(破坏性更新 不兼容之前的api)**
- **文件上传/图片上传不再支持url用法 强制使用ossId**
- TableSwitch组件重构
- 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页)
- 租户切换Select增加loading
- ~~modalLoading/drawerLoading改为调用内部的lock/unlock方法~~ 有待商榷暂时按老版本逻辑不变
- 登录验证码 增加loading
- DictEnum使用const代替enum
- TinyMCE组件重构 移除冗余代码/功能 增加loading
**ALPHA功能**
- 弹窗表单数据更改关闭时的提示框(可能最终不会加入) 测试页面: 参数管理
**BUG FIX**
- 重新登录 字典会unknown的情况[详细分析](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IBY27D)
- 测试菜单 请假申请 选中删除 需要根据状态判断
- 修复文件/图片在Safari中无法上传 file-type库与Safari不兼容导致
- 头像裁剪 图片加载失败一直处于loading无法上传
- 头像裁剪 私有桶会拼接timestamp参数导致sign计算异常无法上传 感谢cropperjs作者 https://github.com/fengyuanchen/cropperjs/issues/1230
- 租户选择下拉框会跟随body滚动(将下拉框样式的默认absolute改为fixed)
**OTHER**
- 字典管理 字典类型 表格选中行增加bold效果
- 全局圆角修改 与antd保持一致
- vditor(Markdown)升级3.10.9
- 老版本的文件/图片上传将于下个版本移除
- useDescription将于下个版本移除
- getVxePopupContainer与新版Vxe不兼容 先返回body(会导致滚动不跟随)后续版本再优化
# 1.2.3
**BUG FIX**
- `withDefaultPlaceholder`中将`placeholder`修改为computed, 解决后续使用`updateSchema`无法正常更新显示placeholder(响应式问题)
- 流程定义 修改accept类型 解决无法拖拽上传
**FEATURES**
- 增加`环境变量`打包配置demo -> build:antd:test
- 角色管理 勾选权限组件添加对错误用法的校验提示
**REFACTOR**
- OAuth内部逻辑重构 增加新的默认OAuth登录方式
- 重构部分setup组件为setup语法糖形式
# 1.2.2
**FEATURES**
- 代码生成支持路径方式生成
- 代码生成 支持选择表单生成类型(需要模板支持)
- 工作流 支持按钮权限
# 1.2.1
# BUG FIXES
- 客户端管理 错误的status disabled
- modal/drawer升级后zIndex(2000)会遮挡Tinymce的下拉框zIndex(1300)
# 1.2.0
**REFACTOR**
- 菜单选择组件重构为Table形式
- 字典相关功能重构 采用一个Map储存字典(之前为两个Map)
- 代码生成配置页面重构 去除步骤条
**Features**
- 对接后端工作流
- ~~通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理)~~
- getDict/getDictOptions 提取公共逻辑 减少冗余代码
- 字典新增对Number类型的支持 -> `getDictOptions('', true);`即可获取number类型的value
- 文件上传 增加上传进度条 下方上传提示
- 图片上传 增加上传进度条 下方上传提示
- oss下载进度提示
**BUG FIXES**
- 字典项为空时getDict方法无限调用接口(无奈兼容 不给字典item本来就是错误用法)
- 表格排序翻页会丢失排序参数
- 下载文件时(responseType === 'blob')需要判断下载失败(返回json而非二进制)的情况
- requestClient缺失i18n内容
**OTHERS**
- 用户管理 新增只获取一次(mounted)默认密码而非每次打开modal都获取
- `apps/web-antd/src/utils/dict.ts` `getDict`方法将于下个版本删除 使用`getDictOptions`替代
- VxeTable升级V4.10.0
- 移除`@deprecated` `apps/web-antd/src/adapter/vxe-table.ts``tableCheckboxEvent`方法
- 移除`由于更新方案弃用的` `apps/web-antd/src/adapter/vxe-table.ts``vxeSortEvent`方法
- 移除apps下的ele和naive目录
# 1.1.3
**REFACTOR**
- 重构: 判断vxe-table的复选框是否选中
**Bug Fixes**
- 节点树在编辑 & 空数组(不勾选)情况 勾选节点会造成watch延迟触发 导致会带上父节点id造成id重复
- 节点树在节点独立情况下的控制台warning: Invalid prop: type check failed for prop "value". Expected Array, got Object
**Others**
- 角色管理 优化Drawer布局
- unplugin-vue-components插件(默认未开启) 需要排除Button组件 全局已经默认导入了
**BUG FIXES**
- 操作日志详情 在description组件中json预览样式异常
- 微服务版本 区间查询和中文搜索条件一起使用 无法正确查询
# 1.1.2
**Features**
- Options转Enum工具函数
**OTHERS**
- 菜单管理 改为虚拟滚动
- 移除requestClient的一些冗余参数
- 主动退出登录(右上角个人选项)不需要带跳转地址
**BUG FIXES**
- 语言 漏加Content-Language请求头
- 用户管理/岗位管理 左边部门树错误emit导致会调用两次列表api
# 1.1.1
**REFACTOR**
- 使用VxeTable重构OAuth账号绑定列表(替代antdv的Table)
- commonDownloadExcel方法 支持处理区间选择器字段导出excel
**BUG FIXES**
- 修复在Modal/Drawer中使用VxeTable时, 第二次打开表单参数依旧为第一次提交的参数
**OTHERS**
- 废弃downloadExcel方法 统一使用commonDownloadExcel方法
# 1.1.0
**FEATURES**
- 支持离线图标功能(全局可在内网环境中使用)
**BUG FIXES**
- 在VxeTable固定列时, getPopupContainer会导致宽度不够, 弹出层样式异常 解决办法(将弹窗元素挂载到VXe滚动容器上)
**OTHERS**
- 代码生成 - 字段信息修改 改为minWidth 防止在高分辨率屏幕出现空白
# 1.0.0
**FEATURES**
- 用户管理 新增从参数取默认密码
- 全局表格加上id 方便进行缓存列排序的操作
- 支持菜单名称i18n
- 登录页 验证码登录
- Markdown编辑/预览组件(基于vditor)
- VxeTable搜索表单 enter提交
**BUG FIXES**
- 登录页面 关闭租户后下拉框没有正常隐藏
- 字典管理 关闭租户不应显示`同步租户字典`按钮
- 登录日志 漏掉了登录日志日期查询
- 登出相关逻辑在并发(非await)情况下重复执行的问题
- VxeTable在开启/关闭查询表单时 需要使用不同的padding
- VxeTable表格刷新 默认为reload 修改为在当前页刷新(query)
- 岗位管理 部门参数错误
- 角色管理 菜单分配 节点独立下的回显及提交问题
- 租户管理 套餐管理 回显时候`已选中节点`数量为0
- 用户管理 更新用户时打开drawer需要加载该部门下的岗位信息
**OTHERS**
- 登录页 租户选择框浮层固定高度[256px] 超过高度自动滚动
- 表单的Label默认方向改为`top` 支持\n换行
- 所有表格的搜索加上allowClear属性 支持清除
- vxe表格loading 只加载表格 不加载上面的表单
# 1.0.0-beta (2024-10-8)
**FEATURES**
- 基础功能已经开发完毕
- 工作流相关模块等待后端重构后开发

21
Yi.Vben5.Vue3/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 dubai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

128
Yi.Vben5.Vue3/README.md Normal file
View File

@@ -0,0 +1,128 @@
## **简介**
基于 [ruoyi-plus-vben & vben5 & ant-design-vue ](https://gitee.com/dapppp/ruoyi-plus-vben.git) 的 前端项目
**完全兼容意框架[Yi.Admin](https://gitee.com/ccnetcore/Yi) rbac模块**
| 组件/框架 | 版本 |
| :------------- | :----- |
| vben | 5.5.6 |
| ant-design-vue | 4.2.6 |
| vue | 3.5.13 |
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
## 提示
该仓库使用vben最新版本v5开发
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
**后端需要开启”furion格式的规范化api“**路径在Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
## 预览
[预览地址点这里](https://yi.wjys.top/)
## 文档
[原ruoyi-plus-vben 框架文档](https://dapdap.top/)
[Vben V5 文档地址](https://doc.vben.pro/)
[后端 Yi 框架 文档地址](https://gitee.com/ccnetcore/Yi)
## 🚀系统截图
![image-20260101175759249](/resource/image-20260101175759249.png)
![image-20260101175912025](/resource/image-20260101175912025.png)
![image-20260101180006771](/resource/image-20260101180006771.png)
## 安装使用
前置准备环境(只能用pnpm)
```json
"packageManager": "pnpm",
"engines": {
"node": ">=20.15.0",
"pnpm": "latest"
},
```
- 获取项目代码
```bash
git clone -b only-front --single-branch https://gitee.com/vichen2021/yiabp-mini.git
```
2. 安装依赖
```bash
cd yiabp-mini
pnpm install
```
- 菜单图标替换
参考 [菜单图标替换](https://dapdap.top/guide/quick-start.html#%E8%8F%9C%E5%8D%95%E5%9B%BE%E6%A0%87%E5%AF%BC%E5%85%A5)
2. **推荐** 使用菜单自行配置 (跟 cloud 版本打开方式一致)
![图片](https://gitee.com/dapppp/ruoyi-plus-vben/raw/main/preview/菜单修改.png)
使用内嵌 iframe 方式需要解决跨域问题 可参考[nginx.conf](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/script/docker/nginx/conf/nginx.conf#LC87)配置
- 运行
```bash
pnpm dev:antd
```
4. 打包
```bash
pnpm build:antd
```
## 这是一个特性 而不是一个bug!
1. 菜单管理可分配 但只有`admin`/`superadmin`角色能访问 其他角色访问会到403页面
2. 租户相关菜单可分配 但只有`superadmin`角色能访问 其他角色访问会到403页面
3. 分配的租户管理员无法修改自己的角色的菜单(即管理员角色的菜单) 防止自己把自己权限弄没了
## Git 贡献提交规范
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## 浏览器支持
最低适配应该为`Chrome 88+`以上浏览器 详见 [css - where](https://developer.mozilla.org/en-US/docs/Web/CSS/:where#browser_compatibility)
本地开发推荐使用`Chrome` 最新版本浏览器
支持现代浏览器,不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |

View File

@@ -0,0 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

View File

@@ -0,0 +1,15 @@
# @vben/backend-mock
## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app
```bash
# development
$ pnpm run start
# production mode
$ pnpm run build
```

View File

@@ -0,0 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(codes);
});

View File

@@ -0,0 +1,36 @@
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
clearRefreshTokenCookie(event);
return forbiddenResponse(event, 'Username or password is incorrect.');
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
});
});

View File

@@ -0,0 +1,15 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});

View File

@@ -0,0 +1,33 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});

View File

@@ -0,0 +1,28 @@
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const data = `
{
"code": 0,
"message": "success",
"data": [
{
"id": 123456789012345678901234567890123456789012345678901234567890,
"name": "John Doe",
"age": 30,
"email": "john-doe@demo.com"
},
{
"id": 987654321098765432109876543210987654321098765432109876543210,
"name": "Jane Smith",
"age": 25,
"email": "jane@demo.com"
}
]
}
`;
setHeader(event, 'Content-Type', 'application/json');
return data;
});

View File

@@ -0,0 +1,13 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});

View File

@@ -0,0 +1,5 @@
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
return useResponseError(`${status}`);
});

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(1000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(2000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,61 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
pid: 0,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
),
remark: faker.lorem.sentence(),
};
if (faker.datatype.boolean()) {
dataItem.children = Array.from(
{ length: faker.number.int({ min: 1, max: 5 }) },
() => ({
id: faker.string.uuid(),
pid: dataItem.id,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
),
remark: faker.lorem.sentence(),
}),
);
}
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(10);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const listData = structuredClone(mockData);
return useResponseSuccess(listData);
});

View File

@@ -0,0 +1,12 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(MOCK_MENU_LIST);
});

View File

@@ -0,0 +1,28 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
const namesMap: Record<string, any> = {};
function getNames(menus: any[]) {
menus.forEach((menu) => {
namesMap[menu.name] = String(menu.id);
if (menu.children) {
getNames(menu.children);
}
});
}
getNames(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, name } = getQuery(event);
return (name as string) in namesMap &&
(!id || namesMap[name as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,28 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };
function getPaths(menus: any[]) {
menus.forEach((menu) => {
pathMap[menu.path] = String(menu.id);
if (menu.children) {
getPaths(menu.children);
}
});
}
getPaths(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, path } = getQuery(event);
return (path as string) in pathMap &&
(!id || pathMap[path as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,83 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const menuIds = getMenuIds(MOCK_MENU_LIST);
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
permissions: faker.helpers.arrayElements(menuIds),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
MaxResultCount = 20,
name,
id,
remark,
startTime,
endTime,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
return usePageResponseSuccess(page as string, MaxResultCount as string, listData);
});

View File

@@ -0,0 +1,73 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem = {
id: faker.string.uuid(),
imageUrl: faker.image.avatar(),
imageUrl2: faker.image.avatar(),
open: faker.datatype.boolean(),
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
productName: faker.commerce.productName(),
price: faker.commerce.price(),
currency: faker.finance.currencyCode(),
quantity: faker.number.int({ min: 1, max: 100 }),
available: faker.datatype.boolean(),
category: faker.commerce.department(),
releaseDate: faker.date.past(),
rating: faker.number.float({ min: 1, max: 5 }),
description: faker.commerce.productDescription(),
weight: faker.number.float({ min: 0.1, max: 10 }),
color: faker.color.human(),
inProduction: faker.datatype.boolean(),
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
const { page, MaxResultCount, sortBy, sortOrder } = getQuery(event);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
}
});
}
return usePageResponseSuccess(page as string, MaxResultCount as string, listData);
});

View File

@@ -0,0 +1 @@
export default defineEventHandler(() => 'Test get handler');

View File

@@ -0,0 +1 @@
export default defineEventHandler(() => 'Test post handler');

View File

@@ -0,0 +1,13 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess({
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
});
// return useResponseError("test")
});

View File

@@ -0,0 +1,10 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(userinfo);
});

View File

@@ -0,0 +1,7 @@
import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
};
export default errorHandler;

View File

@@ -0,0 +1,19 @@
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {
event.node.res.setHeader(
'Access-Control-Allow-Origin',
event.headers.get('Origin') ?? '*',
);
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
} else if (
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
event.path.startsWith('/api/')
) {
await sleep(Math.floor(Math.random() * 2000));
return forbiddenResponse(event, '演示环境,禁止修改');
}
});

View File

@@ -0,0 +1,20 @@
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();
export default defineNitroConfig({
devErrorHandler: errorHandler,
errorHandler: '~/error',
routeRules: {
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers':
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
},
},
},
});

View File

@@ -0,0 +1,21 @@
{
"name": "@vben/backend-mock",
"version": "0.0.1",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"@faker-js/faker": "catalog:",
"jsonwebtoken": "catalog:",
"nitropack": "catalog:"
},
"devDependencies": {
"@types/jsonwebtoken": "catalog:",
"h3": "catalog:"
}
}

View File

@@ -0,0 +1,13 @@
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>
<h2>Mock service is starting</h2>
<ul>
<li><a href="/api/user">/api/user/info</a></li>
<li><a href="/api/menu">/api/menu/all</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/auth/login">/api/auth/login</a></li>
<li><a href="/api/upload">/api/upload</a></li>
</ul>
`;
});

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./.nitro/types/tsconfig.json"
}

View File

@@ -0,0 +1,26 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event<EventHandlerRequest>,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60, // unit: seconds
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}

View File

@@ -0,0 +1,59 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event<EventHandlerRequest>,
): null | Omit<UserInfo, 'password'> {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit<UserInfo, 'password'> {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}

View File

@@ -0,0 +1,390 @@
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
homePath?: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
realName: 'Vben',
roles: ['super'],
username: 'vben',
},
{
id: 1,
password: '123456',
realName: 'Admin',
roles: ['admin'],
username: 'admin',
homePath: '/workspace',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
homePath: '/analytics',
},
];
export const MOCK_CODES = [
// super
{
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
username: 'vben',
},
{
// admin
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
username: 'admin',
},
{
// user
codes: ['AC_1000001', 'AC_1000002'],
username: 'jack',
},
];
const dashboardMenus = [
{
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/dashboard',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
title: 'page.dashboard.analytics',
},
},
{
name: 'Workspace',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
},
},
],
},
];
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
const roleWithMenus = {
admin: {
component: '/demos/access/admin-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
},
super: {
component: '/demos/access/super-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
},
user: {
component: '/demos/access/user-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
},
};
return [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: 'demos.title',
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
{
name: 'AccessDemos',
path: '/demosaccess',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions',
},
redirect: '/demos/access/page-control',
children: [
{
name: 'AccessPageControlDemo',
path: '/demos/access/page-control',
component: '/demos/access/index',
meta: {
icon: 'mdi:page-previous-outline',
title: 'demos.access.pageAccess',
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.buttonControl',
},
},
{
name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403',
component: '/demos/access/menu-visible-403',
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: 'demos.access.menuVisible403',
},
},
roleWithMenus[role],
],
},
],
},
];
};
export const MOCK_MENUS = [
{
menus: [...dashboardMenus, ...createDemosMenus('super')],
username: 'vben',
},
{
menus: [...dashboardMenus, ...createDemosMenus('admin')],
username: 'admin',
},
{
menus: [...dashboardMenus, ...createDemosMenus('user')],
username: 'jack',
},
];
export const MOCK_MENU_LIST = [
{
id: 1,
name: 'Workspace',
status: 1,
type: 'menu',
icon: 'mdi:dashboard',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
affixTab: true,
order: 0,
},
},
{
id: 2,
meta: {
icon: 'carbon:settings',
order: 9997,
title: 'system.title',
badge: 'new',
badgeType: 'normal',
badgeVariants: 'primary',
},
status: 1,
type: 'catalog',
name: 'System',
path: '/system',
children: [
{
id: 201,
pid: 2,
path: '/system/menu',
name: 'SystemMenu',
authCode: 'System:Menu:List',
status: 1,
type: 'menu',
meta: {
icon: 'carbon:menu',
title: 'system.menu.title',
},
component: '/system/menu/list',
children: [
{
id: 20_101,
pid: 201,
name: 'SystemMenuCreate',
status: 1,
type: 'button',
authCode: 'System:Menu:Create',
meta: { title: 'common.create' },
},
{
id: 20_102,
pid: 201,
name: 'SystemMenuEdit',
status: 1,
type: 'button',
authCode: 'System:Menu:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_103,
pid: 201,
name: 'SystemMenuDelete',
status: 1,
type: 'button',
authCode: 'System:Menu:Delete',
meta: { title: 'common.delete' },
},
],
},
{
id: 202,
pid: 2,
path: '/system/dept',
name: 'SystemDept',
status: 1,
type: 'menu',
authCode: 'System:Dept:List',
meta: {
icon: 'carbon:container-services',
title: 'system.dept.title',
},
component: '/system/dept/list',
children: [
{
id: 20_401,
pid: 201,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
authCode: 'System:Dept:Create',
meta: { title: 'common.create' },
},
{
id: 20_402,
pid: 201,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
authCode: 'System:Dept:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_403,
pid: 201,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
authCode: 'System:Dept:Delete',
meta: { title: 'common.delete' },
},
],
},
],
},
{
id: 9,
meta: {
badgeType: 'dot',
order: 9998,
title: 'demos.vben.title',
icon: 'carbon:data-center',
},
name: 'Project',
path: '/vben-admin',
type: 'catalog',
status: 1,
children: [
{
id: 901,
pid: 9,
name: 'VbenDocument',
path: '/vben-admin/document',
component: 'IFrameView',
type: 'embedded',
status: 1,
meta: {
icon: 'carbon:book',
iframeSrc: 'https://doc.vben.pro',
title: 'demos.vben.document',
},
},
{
id: 902,
pid: 9,
name: 'VbenGithub',
path: '/vben-admin/github',
component: 'IFrameView',
type: 'link',
status: 1,
meta: {
icon: 'carbon:logo-github',
link: 'https://github.com/vbenjs/vue-vben-admin',
title: 'Github',
},
},
{
id: 903,
pid: 9,
name: 'VbenAntdv',
path: '/vben-admin/antdv',
component: 'IFrameView',
type: 'link',
status: 0,
meta: {
icon: 'carbon:hexagon-vertical-solid',
badgeType: 'dot',
link: 'https://ant.vben.pro',
title: 'demos.vben.antdv',
},
},
],
},
{
id: 10,
component: '_core/about/index',
type: 'menu',
status: 1,
meta: {
icon: 'lucide:copyright',
order: 9999,
title: 'demos.vben.about',
},
name: 'About',
path: '/about',
},
];
export function getMenuIds(menus: any[]) {
const ids: number[] = [];
menus.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) {
ids.push(...getMenuIds(item.children));
}
});
return ids;
}

View File

@@ -0,0 +1,68 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
data,
error: null,
message: 'ok',
};
}
export function usePageResponseSuccess<T = any>(
page: number | string,
MaxResultCount: number | string,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(
Number.parseInt(`${page}`),
Number.parseInt(`${MaxResultCount}`),
list,
);
return {
...useResponseSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function useResponseError(message: string, error: any = null) {
return {
code: -1,
data: null,
error,
message,
};
}
export function forbiddenResponse(
event: H3Event<EventHandlerRequest>,
message = 'Forbidden Exception',
) {
setResponseStatus(event, 403);
return useResponseError(message, message);
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function pagination<T = any>(
pageNo: number,
MaxResultCount: number,
array: T[],
): T[] {
const offset = (pageNo - 1) * Number(MaxResultCount);
return offset + Number(MaxResultCount) >= array.length
? array.slice(offset)
: array.slice(offset, offset + Number(MaxResultCount));
}

View File

@@ -0,0 +1,8 @@
# 应用标题
VITE_APP_TITLE=Yi Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-antd
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

View File

@@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api/app
VITE_VISUALIZER=true

View File

@@ -0,0 +1,25 @@
# 端口号
VITE_PORT=5666
VITE_BASE=/
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 后台请求路径 具体在vite.config.mts配置代理
# VITE_GLOB_API_URL=http://101.37.70.137:19001/api/app
VITE_GLOB_API_URL=http://192.168.1.101:19001/api/app
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=false
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
VITE_GLOB_DEMO_MODE=false

View File

@@ -0,0 +1,35 @@
VITE_BASE=/
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=gzip
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
# 后端接口地址
# VITE_GLOB_API_URL=/prod-api
VITE_GLOB_API_URL=https://yiapi.wjys.top/api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=false
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启SSE
VITE_GLOB_SSE_ENABLE=false
VITE_GLOB_DEMO_MODE=true

View File

@@ -0,0 +1,35 @@
# 该文件是为了给一个增加环境变量打包的例子
# 对应在根目录package.json -> build:antd:test 命令
VITE_BASE=/
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=gzip
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
# 后端接口地址
VITE_GLOB_API_URL=/test-api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
# 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 开启SSE
VITE_GLOB_SSE_ENABLE=true

View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,64 @@
{
"name": "@vben/web-antd",
"version": "1.4.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@tinymce/tinymce-vue": "^6.0.1",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"dayjs": "catalog:",
"echarts": "^5.5.1",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"pinia": "catalog:",
"tinymce": "^7.3.0",
"unplugin-vue-components": "^0.27.3",
"vue": "catalog:",
"vue-router": "catalog:",
"vue3-colorpicker": "^2.3.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

View File

@@ -0,0 +1,258 @@
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
computed,
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
import { FileUploadOld, ImageUploadOld } from '#/components/upload-old';
const RichTextarea = defineAsyncComponent(() =>
import('#/components/tinymce/index').then((res) => res.Tinymce),
);
const FileUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.FileUpload),
);
const ImageUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.ImageUpload),
);
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
// 改为placeholder 解决在keepalive & 语言切换 & tab切换 显示不变的问题
const computedPlaceholder = computed(
() =>
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`),
);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(
component,
{
...componentProps,
placeholder: computedPlaceholder.value,
...props,
...attrs,
ref: innerRef,
},
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Cascader'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'FileUpload'
| 'FileUploadOld'
| 'IconPicker'
| 'ImageUpload'
| 'ImageUploadOld'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'RichTextarea'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Cascader: withDefaultPlaceholder(Cascader, 'select'),
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
ImageUpload,
FileUpload,
RichTextarea,
ImageUploadOld,
FileUploadOld,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };

View File

@@ -0,0 +1,56 @@
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { isArray } from 'lodash-es';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
RichTextarea: 'modelValue',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (
[false, null, undefined].includes(value) ||
(isArray(value) && value.length === 0)
) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
}
const useVbenForm = useForm<ComponentType>;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };
export type FormSchemaGetter = () => VbenFormSchema[];

View File

@@ -0,0 +1,137 @@
import type { VxeGridPropTypes } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'totalCount',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
// 溢出展示形式
showOverflow: true,
pagerConfig: {
// 默认条数
pageSize: 10,
// 分页可选条数
pageSizes: [10, 20, 30, 40, 50],
},
rowConfig: {
// 鼠标移入行显示 hover 样式
isHover: true,
// 点击行高亮
isCurrent: false,
},
columnConfig: {
// 可拖拽列宽
resizable: true,
},
// 右上角工具栏
toolbarConfig: {
// 自定义列
custom: true,
customOptions: {
icon: 'vxe-icon-setting',
},
// 最大化
zoom: true,
// 刷新
refresh: true,
refreshOptions: {
// 默认为reload 修改为在当前页刷新
code: 'query',
},
},
// 圆角按钮
round: true,
// 表格尺寸
size: 'medium',
customConfig: {
// 表格右上角自定义列配置 是否保存到localStorage
// 必须存在id参数才能使用
storage: false,
},
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
/**
* 判断vxe-table的复选框是否选中
* @param tableApi api
* @returns boolean
*/
export function vxeCheckboxChecked(
tableApi: ReturnType<typeof useVbenVxeGrid>[1],
) {
return tableApi?.grid?.getCheckboxRecords?.()?.length > 0;
}
/**
* 通用的 排序参数添加到请求参数中
* @param params 请求参数
* @param sortList vxe-table的排序参数
*/
export function addSortParams(
params: Record<string, any>,
sortList: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[],
) {
// 这里是排序取消 length为0 就不添加参数了
if (sortList.length === 0) {
return;
}
// 支持单/多字段排序
const orderByColumn = sortList.map((item) => item.field).join(',');
const isAsc = sortList.map((item) => item.order).join(',');
params.orderByColumn = orderByColumn;
params.isAsc = isAsc;
}

View File

@@ -0,0 +1,42 @@
export type ID = number | string;
export type IDS = (number | string)[];
export interface BaseEntity {
createBy?: string;
createDept?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
}
/**
* 分页信息
* @param rows 结果集
* @param total 总数
*/
export interface PageResult<T = any> {
items: T[];
totalCount: number;
}
/**
* 分页查询参数
*
* 排序支持的用法如下:
* {isAsc:"asc",orderByColumn:"id"} order by id asc
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
*
* @param SkipCount 当前页
* @param MaxResultCount 每页大小
* @param orderByColumn 排序字段
* @param isAsc 是否升序
*/
export interface PageQuery {
isAsc?: string;
orderByColumn?: string;
SkipCount?: number;
MaxResultCount?: number;
[key: string]: any;
}

View File

@@ -0,0 +1,204 @@
import type { GrantType } from '@vben/common-ui';
import type { HttpResponse } from '@vben/request';
import { h } from 'vue';
import { useAppConfig } from '@vben/hooks';
import { Modal } from 'ant-design-vue';
import { requestClient } from '#/api/request';
const { clientId, sseEnable } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
export namespace AuthApi {
/**
* @description: 所有登录类型都需要用到的
* @param clientId 客户端ID 这里为必填项 但是在loginApi内部处理了 所以为可选
* @param grantType 授权/登录类型
* @param tenantId 租户id
*/
export interface BaseLoginParams {
clientId?: string;
grantType: GrantType;
tenantId: string;
}
/**
* @description: oauth登录需要用到的参数
* @param socialCode 第三方参数
* @param socialState 第三方参数
* @param source 与后端的 justauth.type.xxx的回调地址的source对应
*/
export interface OAuthLoginParams extends BaseLoginParams {
socialCode: string;
socialState: string;
source: string;
}
/**
* @description: 验证码登录需要用到的参数
* @param code 验证码 可选(未开启验证码情况)
* @param uuid 验证码ID 可选(未开启验证码情况)
* @param username 用户名
* @param password 密码
*/
export interface SimpleLoginParams extends BaseLoginParams {
code?: string;
uuid?: string;
username: string;
password: string;
}
export type LoginParams = OAuthLoginParams | SimpleLoginParams;
// /** 登录接口参数 */
// export interface LoginParams {
// code?: string;
// grantType: string;
// password: string;
// tenantId: string;
// username: string;
// uuid?: string;
// }
/** 登录接口返回值 */
export interface LoginResult {
access_token: string;
client_id: string;
expire_in: number;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>(
'/account/login',
{ ...data, clientId },
{
encrypt: true,
},
);
}
/**
* 用户登出
* @returns void
*/
export async function doLogout() {
const resp = await requestClient.post<HttpResponse<void>>(
'account/logout',
null,
{
isTransformResponse: false,
},
);
// 无奈之举 对错误用法的提示
if (resp.code === 401 && import.meta.env.DEV) {
Modal.destroyAll();
Modal.warn({
title: '后端配置出现错误',
centered: true,
content: h('div', { class: 'flex flex-col gap-2' }, [
`检测到你的logout接口返回了401, 导致前端一直进入循环逻辑???`,
...Array.from({ length: 3 }, () =>
h(
'span',
{ class: 'font-bold text-red-500 text-[18px]' },
'去检查你的后端配置!别盯着前端找问题了!这不是前端问题!',
),
),
]),
});
}
}
/**
* 关闭sse连接
* @returns void
*/
export function seeConnectionClose() {
/**
* 未开启sse 不需要处理
*/
if (!sseEnable) {
return;
}
return requestClient.get<void>('/resource/sse/close');
}
/**
* @param companyName 租户/公司名称
* @param domain 绑定域名(不带http(s)://) 可选
* @param tenantId 租户id
*/
export interface TenantOption {
companyName: string;
domain?: string;
tenantId: string;
}
/**
* @param tenantEnabled 是否启用租户
* @param voList 租户列表
*/
export interface TenantResp {
tenantEnabled: boolean;
voList: TenantOption[];
}
/**
* 获取租户列表 下拉框使用
*/
export function tenantList() {
return requestClient.get<TenantResp>('/tenant/select');
}
/**
* vben的 先不删除
* @returns string[]
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}
/**
* 绑定第三方账号
* @param source 绑定的来源
* @returns 跳转url
*/
export function authBinding(source: string, tenantId: string) {
return requestClient.get<string>(`/auth/binding/${source}`, {
params: {
domain: window.location.host,
tenantId,
},
});
}
/**
* 取消绑定
* @param id id
*/
export function authUnbinding(id: string) {
return requestClient.deleteWithMsg<void>(`/auth/unlock/${id}`);
}
/**
* oauth授权回调
* @param data oauth授权
* @returns void
*/
export function authCallback(data: AuthApi.OAuthLoginParams) {
return requestClient.post<void>('/auth/social/callback', data);
}

View File

@@ -0,0 +1,42 @@
import { requestClient } from '#/api/request';
/**
* 发送短信验证码
* @param phonenumber 手机号
* @returns void
*/
export function sendSmsCode(phonenumber: string) {
return requestClient.get<void>('/resource/sms/code', {
params: { phonenumber },
});
}
/**
* 发送邮件验证码
* @param email 邮箱
* @returns void
*/
export function sendEmailCode(email: string) {
return requestClient.get<void>('/resource/email/code', {
params: { email },
});
}
/**
* @param img 图片验证码 需要和base64拼接
* @param isEnableCaptcha 是否开启
* @param uuid 验证码ID
*/
export interface CaptchaResponse {
isEnableCaptcha: boolean;
img: string;
uuid: string;
}
/**
* 图片验证码
* @returns resp
*/
export function captchaImage() {
return requestClient.get<CaptchaResponse>('/account/captcha-image');
}

View File

@@ -0,0 +1,4 @@
export * from './auth';
export * from './menu';
export * from './upload';
export * from './user';

View File

@@ -0,0 +1,45 @@
import { requestClient } from '#/api/request';
/**
* @description: 菜单meta
* @param title 菜单名
* @param icon 菜单图标
* @param noCache 是否不缓存
* @param link 外链链接
*/
export interface MenuMeta {
icon: string;
link?: string;
noCache: boolean;
title: string;
}
/**
* @description: 菜单
* @param name 菜单名
* @param path 菜单路径
* @param hidden 是否隐藏
* @param component 组件名称 Layout
* @param alwaysShow 总是显示
* @param query 路由参数(json形式)
* @param meta 路由信息
* @param children 子路由信息
*/
export interface Menu {
alwaysShow?: boolean;
children: Menu[];
component: string;
hidden: boolean;
meta: MenuMeta;
name: string;
path: string;
query?: string;
redirect?: string;
}
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get<Menu[]>('/account/Vue3Router/vben5');
}

View File

@@ -0,0 +1,47 @@
import type { AxiosRequestConfig } from '@vben/request';
import { requestClient } from '#/api/request';
/**
* Axios上传进度事件
*/
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
/**
* 默认上传结果
*/
export interface UploadResult {
url: string;
fileName: string;
ossId: string;
}
/**
* 通过单文件上传接口
* @param file 上传的文件
* @param options 一些配置项
* @param options.onUploadProgress 上传进度事件
* @param options.signal 上传取消信号
* @param options.otherData 其他请求参数 后端拓展可能会用到
* @returns 上传结果
*/
export function uploadApi(
file: Blob | File,
options?: {
onUploadProgress?: AxiosProgressEvent;
otherData?: Record<string, any>;
signal?: AbortSignal;
},
) {
const { onUploadProgress, signal, otherData = {} } = options ?? {};
return requestClient.upload<UploadResult>(
'/resource/oss/upload',
{ file, ...otherData },
{ onUploadProgress, signal, timeout: 60_000 },
);
}
/**
* 上传api type
*/
export type UploadApi = typeof uploadApi;

View File

@@ -0,0 +1,47 @@
import { requestClient } from '#/api/request';
export interface Role {
dataScope: string;
flag: boolean;
roleId: number;
roleKey: string;
roleName: string;
roleSort: number;
status: string;
superAdmin: boolean;
}
export interface User {
avatar: string;
createTime: string;
deptId: number;
deptName: string;
email: string;
loginDate: string;
loginIp: string;
nickName: string;
phonenumber: string;
remark: string;
roles: Role[];
sex: string;
status: string;
tenantId: string;
userId: number;
userName: string;
userType: string;
}
export interface UserInfoResp {
permissionCodes: string[];
roles: string[];
roleCodes: string[];
user: User;
}
/**
* 获取用户信息
* 存在返回null的情况(401) 不会抛出异常 需要手动抛异常
*/
export async function getUserInfoApi() {
return requestClient.get<null | UserInfoResp>('account');
}

View File

@@ -0,0 +1,28 @@
import { requestClient } from './request';
/**
* @description: contentType
*/
export const ContentTypeEnum = {
// form-data upload
FORM_DATA: 'multipart/form-data;charset=UTF-8',
// form-data qs
FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
// json
JSON: 'application/json;charset=UTF-8',
} as const;
/**
* 通用下载接口 封装一层
* @param url 请求地址
* @param data 请求参数
* @returns blob二进制
*/
export function commonExport(url: string, data: Record<string, any>) {
return requestClient.post<Blob>(url, data, {
data,
headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
isTransformResponse: false,
responseType: 'blob',
});
}

View File

@@ -0,0 +1 @@
export * from './core';

View File

@@ -0,0 +1,90 @@
import { requestClient } from '#/api/request';
export interface CommandStats {
name: string;
value: string;
}
export interface RedisInfo {
[key: string]: string;
}
export interface CacheInfo {
commandStats: CommandStats[];
dbSize: number;
info: RedisInfo;
}
export interface CacheName {
cacheName: string;
remark: string;
}
export interface CacheValue {
cacheName: string;
cacheKey: string;
cacheValue: string;
}
/**
*
* @returns redis信息
*/
export function redisCacheInfo() {
return requestClient.get<CacheInfo>('/monitor/cache');
}
/**
* 查询缓存名称列表
* @returns 缓存名称列表
*/
export function listCacheName() {
return requestClient.get<CacheName[]>('/monitor-cache/name');
}
/**
* 查询缓存键名列表
* @param cacheName 缓存名称
* @returns 缓存键名列表
*/
export function listCacheKey(cacheName: string) {
return requestClient.get<string[]>(`/monitor-cache/key/${cacheName}`);
}
/**
* 查询缓存内容
* @param cacheName 缓存名称
* @param cacheKey 缓存键名
* @returns 缓存内容
*/
export function getCacheValue(cacheName: string, cacheKey: string) {
return requestClient.get<CacheValue>(
`/monitor-cache/value/${cacheName}/${cacheKey}`,
);
}
/**
* 清理指定名称缓存
* @param cacheName 缓存名称
*/
export function clearCacheName(cacheName: string) {
return requestClient.deleteWithMsg<void>(`/monitor-cache/key/${cacheName}`);
}
/**
* 清理指定键名缓存
* @param cacheName 缓存名称
* @param cacheKey 缓存键名
*/
export function clearCacheKey(cacheName: string, cacheKey: string) {
return requestClient.deleteWithMsg<void>(
`/monitor-cache/value/${cacheName}/${cacheKey}`,
);
}
/**
* 清理全部缓存
*/
export function clearCacheAll() {
return requestClient.deleteWithMsg<void>('/monitor-cache/clear');
}

View File

@@ -0,0 +1,61 @@
import type { LoginLog } from './model';
import type { IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
loginInfoClean = '/login-log/clean',
loginInfoExport = '/login-log/export',
root = '/login-log',
userUnlock = '/login-log/unlock',
}
/**
* 登录日志列表
* @param params 查询参数
* @returns list[]
*/
export function loginInfoList(params?: PageQuery) {
return requestClient.get<PageResult<LoginLog>>(Api.root, { params });
}
/**
* 导出登录日志
* @param data 表单参数
* @returns excel
*/
export function loginInfoExport(data: any) {
return commonExport(Api.loginInfoExport, data);
}
/**
* 移除登录日志
* @param infoIds 登录日志id数组
* @returns void
*/
export function loginInfoRemove(infoIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: infoIds.join(',') },
});
}
/**
* 账号解锁
* @param username 用户名(账号)
* @returns void
*/
export function userUnlock(username: string) {
return requestClient.get<void>(`${Api.userUnlock}/${username}`, {
successMessageMode: 'message',
});
}
/**
* 清空全部登录日志
* @returns void
*/
export function loginInfoClean() {
return requestClient.deleteWithMsg<void>(Api.loginInfoClean);
}

View File

@@ -0,0 +1,11 @@
export interface LoginLog {
id: string;
loginUser: string;
loginLocation: string;
loginIp: string;
browser: string;
os: string;
logMsg: string;
creationTime: string;
creatorId: string | null;
}

View File

@@ -0,0 +1,46 @@
import type { OnlineUser } from './model';
import type { IDS, PageQuery, PageResult } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
root = '/online',
}
/**
* 当前账号的在线设备 个人中心使用
* @returns OnlineUser[]
*/
export function onlineDeviceList() {
return requestClient.get<PageResult<OnlineUser>>(Api.root);
}
/**
* 这里的分页参数无效 返回的是全部的分页
* @param params 请求参数
* @returns 结果
*/
export function onlineList(params?: PageQuery) {
return requestClient.get<PageResult<OnlineUser>>(Api.root, { params });
}
/**
* 强制下线
* @param tokenId 连接Id
* @returns void
*/
export function forceLogout(tokenId: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: tokenId.join(',') },
});
}
/**
* 个人中心用的 跟上面的不同是用的Post
* @param tokenId 连接Id
* @returns void
*/
export function forceLogout2(tokenId: string) {
return requestClient.deleteWithMsg<void>(`${Api.root}/myself/${tokenId}`);
}

View File

@@ -0,0 +1,10 @@
export interface OnlineUser {
connnectionId?: string;
userId?: string;
userName?: string;
loginTime: number;
ipaddr?: string;
loginLocation?: string;
os?: string;
browser?: string;
}

View File

@@ -0,0 +1,48 @@
import type { OperationLog } from './model';
import type { IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
operLogClean = '/operation-log/clean',
operLogExport = '/operation-log/export',
root = '/operation-log',
}
/**
* 操作日志分页
* @param params 查询参数
* @returns 分页结果
*/
export function operLogList(params?: PageQuery) {
return requestClient.get<PageResult<OperationLog>>(Api.root, {
params,
});
}
/**
* 删除操作日志
* @param operIds id/ids
*/
export function operLogRemove(operIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: operIds.join(',') },
});
}
/**
* 清空全部分页日志
*/
export function operLogClean() {
return requestClient.deleteWithMsg<void>(Api.operLogClean);
}
/**
* 导出操作日志
* @param data 查询参数
*/
export function operLogExport(data: Partial<OperationLog>) {
return commonExport(Api.operLogExport, data);
}

View File

@@ -0,0 +1,14 @@
export interface OperationLog {
id: string;
title: string;
operType: string;
requestMethod: string;
operUser: string;
operIp: string;
operLocation: string;
method: string;
requestParam: string;
requestResult: string;
creationTime: string;
creatorId: string | null;
}

View File

@@ -0,0 +1,322 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
stringify,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message, Modal } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import {
decryptBase64,
decryptWithAes,
encryptBase64,
encryptWithAes,
generateAesKey,
} from '#/utils/encryption/crypto';
import * as encryptUtil from '#/utils/encryption/jsencrypt';
const { apiURL, clientId, enableEncrypt, demoMode } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
/**
* 是否已经处在登出过程中了 一个标志位
* 主要是防止一个页面会请求多个api 都401 会导致登出执行多次
*/
let isLogoutProcessing = false;
/**
* 定义一个401专用异常 用于可能会用到的区分场景?
*/
export class UnauthorizedException extends Error {}
/**
* 演示模式错误,用于标识演示环境禁止修改的错误
*/
export class DemoModeException extends Error {
constructor(message: string) {
super(message);
this.name = 'DemoModeException';
// 添加标记,用于错误拦截器识别
(this as any).__isDemoModeError = true;
}
}
function createRequestClient(baseURL: string) {
const client = new RequestClient({
// 后端地址
baseURL,
// 消息提示类型
errorMessageMode: 'message',
// 是否返回原生响应 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
// 不需要
// 保留此方法只是为了合并方便
return '';
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
client.addRequestInterceptor({
fulfilled: (config) => {
// 演示模式:拦截所有修改操作
if (demoMode) {
const method = config.method?.toUpperCase() || '';
const isModifyMethod = ['DELETE', 'PATCH', 'POST', 'PUT'].includes(
method,
);
// 排除登录等认证接口,允许通过
const isAuthPath =
config.url?.includes('/auth/') ||
config.url?.includes('/login') ||
config.url?.includes('/logout');
if (isModifyMethod && !isAuthPath) {
// 显示错误提示
message.error('演示环境,禁止修改');
// 抛出演示模式错误,错误拦截器会识别并跳过处理
throw new DemoModeException('演示环境,禁止修改');
}
}
const accessStore = useAccessStore();
// 添加token
config.headers.Authorization = formatToken(accessStore.accessToken);
/**
* locale跟后台不一致 需要转换
*/
const language = preferences.app.locale.replace('-', '_');
config.headers['Accept-Language'] = language;
config.headers['Content-Language'] = language;
/**
* 添加全局clientId
* 关于header的clientId被错误绑定到实体类
* https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS
*/
config.headers.ClientID = clientId;
/**
* 格式化get/delete参数
* 如果包含自定义的paramsSerializer则不走此逻辑
*/
if (
['DELETE', 'GET'].includes(config.method?.toUpperCase() || '') &&
config.params &&
!config.paramsSerializer
) {
/**
* 1. 格式化参数 微服务在传递区间时间选择(后端的params Map类型参数)需要格式化key 否则接收不到
* 2. 数组参数需要格式化 后端才能正常接收 会变成arr=1&arr=2&arr=3的格式来接收
*/
config.paramsSerializer = (params) =>
stringify(params, { arrayFormat: 'repeat' });
}
const { encrypt } = config;
// 全局开启请求加密功能 && 该请求开启 && 是post/put请求
if (
enableEncrypt &&
encrypt &&
['POST', 'PUT'].includes(config.method?.toUpperCase() || '')
) {
const aesKey = generateAesKey();
config.headers['encrypt-key'] = encryptUtil.encrypt(
encryptBase64(aesKey),
);
config.data =
typeof config.data === 'object'
? encryptWithAes(JSON.stringify(config.data), aesKey)
: encryptWithAes(config.data, aesKey);
}
return config;
},
});
// 通用的错误处理, 如果没有进入上面的错误处理逻辑,就会进入这里
// 主要处理http状态码不为200(如网络异常/离线)的情况 必须放在在下面的响应拦截器之前
const errorInterceptor = errorMessageResponseInterceptor(
(msg: string, error: any) => {
// 如果是演示模式错误,已经在请求拦截器中提示过了,这里不再提示
if (error?.__isDemoModeError || error?.name === 'DemoModeException') {
return;
}
message.error(msg);
},
);
client.addResponseInterceptor(errorInterceptor);
client.addResponseInterceptor<HttpResponse>({
fulfilled: async (response) => {
const encryptKey = (response.headers ?? {})['encrypt-key'];
if (encryptKey) {
/** RSA私钥解密 拿到解密秘钥的base64 */
const base64Str = encryptUtil.decrypt(encryptKey);
/** base64 解码 得到请求头的 AES 秘钥 */
const aesSecret = decryptBase64(base64Str.toString());
/** 使用aesKey解密 responseData */
const decryptData = decryptWithAes(
response.data as unknown as string,
aesSecret,
);
/** 赋值 需要转为对象 */
response.data = JSON.parse(decryptData);
}
const { isReturnNativeResponse, isTransformResponse } = response.config;
// 是否返回原生响应 比如:需要获取响应时使用该属性
if (isReturnNativeResponse) {
return response;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
/**
* 需要判断下载二进制的情况 正常是返回二进制 报错会返回json
* 当type为blob且content-type为application/json时 则判断已经下载出错
*/
if (
response.config.responseType === 'blob' &&
response.headers['content-type']?.includes?.('application/json')
) {
// 这时候的data为blob类型
const blob = response.data as unknown as Blob;
// 拿到字符串转json对象
response.data = JSON.parse(await blob.text());
// 然后按正常逻辑执行下面的代码(判断业务状态码)
} else {
// 其他情况 直接返回
return response.data;
}
}
const axiosResponseData = response.data;
if (!axiosResponseData) {
throw new Error($t('http.apiRequestFailed'));
}
console.log('axiosResponseData', axiosResponseData);
// 适配新的后端数据结构: { statusCode, data, succeeded, errors, extras, timestamp }
const { statusCode, data, succeeded, errors, extras, timestamp } =
axiosResponseData;
// 业务状态码为200且succeeded为true则请求成功
const hasSuccess = statusCode === 200 && succeeded === true;
if (hasSuccess) {
const successMsg = $t(`http.operationSuccess`);
if (response.config.successMessageMode === 'modal') {
Modal.success({
content: successMsg,
title: $t('http.successTip'),
});
} else if (response.config.successMessageMode === 'message') {
message.success(successMsg);
}
// 直接返回data字段
return data;
}
// 在此处根据自己项目的实际情况对不同的statusCode执行不同的操作
// 如果不希望中断当前请求请return数据否则直接抛出异常即可
let timeoutMsg = '';
switch (statusCode) {
case 401: {
// 已经在登出过程中 不再执行
if (isLogoutProcessing) {
throw new UnauthorizedException(timeoutMsg);
}
isLogoutProcessing = true;
const _msg = $t('http.loginTimeout');
const userStore = useAuthStore();
userStore.logout().finally(() => {
message.error(_msg);
isLogoutProcessing = false;
});
// 不再执行下面逻辑
throw new UnauthorizedException(_msg);
}
default: {
// 优先使用errors字段作为错误信息
if (errors && Array.isArray(errors) && errors.length > 0) {
timeoutMsg = errors.join(', ');
} else if (typeof errors === 'string') {
timeoutMsg = errors;
} else {
timeoutMsg = $t('http.apiRequestFailed');
}
}
}
// errorMessageMode='modal'的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
if (response.config.errorMessageMode === 'modal') {
Modal.error({
content: timeoutMsg,
title: $t('http.errorTip'),
});
} else if (response.config.errorMessageMode === 'message') {
message.error(timeoutMsg);
}
throw new Error(timeoutMsg || $t('http.apiRequestFailed'));
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -0,0 +1,12 @@
import type { ServerInfo } from './model';
import { requestClient } from '#/api/request';
/**
* 获取服务器信息
* @returns 服务器信息
*/
export function getServerInfo() {
return requestClient.get<ServerInfo>('/monitor-server/info');
}

View File

@@ -0,0 +1,46 @@
export interface CpuInfo {
coreTotal: number;
logicalProcessors: number;
cpuRate: number;
}
export interface MemoryInfo {
totalRAM: string;
usedRam: string;
freeRam: string;
ramRate: number;
}
export interface SystemInfo {
computerName: string;
osName: string;
serverIP: string;
osArch: string;
}
export interface AppInfo {
name: string;
version: string;
startTime: string;
runTime: string;
rootPath: string;
webRootPath: string;
}
export interface DiskInfo {
diskName: string;
typeName: string;
totalSize: string;
availableFreeSpace: string;
used: string;
availablePercent: number;
}
export interface ServerInfo {
cpu: CpuInfo;
memory: MemoryInfo;
sys: SystemInfo;
app: AppInfo;
disk: DiskInfo[];
}

View File

@@ -0,0 +1,77 @@
import type { Client } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
clientChangeStatus = '/client/changeStatus',
clientExport = '/client/export',
clientList = '/client/list',
root = '/client',
}
/**
* 查询客户端分页列表
* @param params 请求参数
* @returns 列表
*/
export function clientList(params?: PageQuery) {
return requestClient.get<PageResult<Client>>(Api.clientList, { params });
}
/**
* 导出客户端excel
* @param data 请求参数
*/
export function clientExport(data: Partial<Client>) {
return commonExport(Api.clientExport, data);
}
/**
* 客户端详情
* @param id id
* @returns 详情
*/
export function clientInfo(id: ID) {
return requestClient.get<Client>(`${Api.root}/${id}`);
}
/**
* 客户端新增
* @param data 参数
*/
export function clientAdd(data: Partial<Client>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 客户端修改
* @param data 参数
*/
export function clientUpdate(data: Partial<Client>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 客户端状态修改
* @param data 状态
*/
export function clientChangeStatus(data: any) {
const requestData = {
clientId: data.clientId,
status: data.status,
};
return requestClient.putWithMsg<void>(Api.clientChangeStatus, requestData);
}
/**
* 客户端删除
* @param ids id集合
*/
export function clientRemove(ids: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: ids.join(',') },
});
}

View File

@@ -0,0 +1,12 @@
export interface Client {
id: number;
clientId: string;
clientKey: string;
clientSecret: string;
grantTypeList: string[];
grantType: string;
deviceType: string;
activeTimeout: number;
timeout: number;
status: string;
}

View File

@@ -0,0 +1,78 @@
import type { SysConfig } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
configExport = '/config/export',
configInfoByKey = '/config/config-key',
configList = '/config/list',
configRefreshCache = '/config/refreshCache',
root = '/config',
}
/**
* 系统参数分页列表
* @param params 请求参数
* @returns 列表
*/
export function configList(params?: PageQuery) {
return requestClient.get<PageResult<SysConfig>>(Api.root, { params });
}
export function configInfo(configId: ID) {
return requestClient.get<SysConfig>(`${Api.root}/${configId}`);
}
/**
* 导出
* @param data 参数
*/
export function configExport(data: Partial<SysConfig>) {
return commonExport(Api.configExport, data);
}
/**
* 刷新缓存
* @returns void
*/
export function configRefreshCache() {
return requestClient.deleteWithMsg<void>(Api.configRefreshCache);
}
/**
* 更新系统配置
* @param data 参数
*/
export function configUpdate(data: Partial<SysConfig>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 新增系统配置
* @param data 参数
*/
export function configAdd(data: Partial<SysConfig>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 删除配置
* @param configIds ids
*/
export function configRemove(configIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: configIds.join(',') },
});
}
/**
* 获取配置信息
* @param configKey configKey
* @returns value
*/
export function configInfoByKey(configKey: string) {
return requestClient.get<string>(`${Api.configInfoByKey}/${configKey}`);
}

View File

@@ -0,0 +1,14 @@
export interface SysConfig {
id: string;
configName: string;
configKey: string;
configValue: string;
configType: string | null;
orderNum: number;
remark: string | null;
isDeleted: boolean;
creationTime: string;
creatorId: string | null;
lastModifierId: string | null;
lastModificationTime: string | null;
}

View File

@@ -0,0 +1,64 @@
import type { Dept } from './model';
import type { ID } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
deptList = '/dept/list',
deptNodeInfo = '/dept/list/exclude',
root = '/dept',
}
/**
* 部门列表
* @returns list
*/
export function deptList(params?: { deptName?: string; status?: string }) {
return requestClient.get<Dept[]>(Api.deptList, { params });
}
/**
* 查询部门列表(排除节点)
* @param deptId 部门ID
* @returns void
*/
export function deptNodeList(deptId: ID) {
return requestClient.get<Dept[]>(`${Api.deptNodeInfo}/${deptId}`);
}
/**
* 部门详情
* @param deptId 部门id
* @returns 部门信息
*/
export function deptInfo(deptId: ID) {
return requestClient.get<Dept>(`${Api.root}/${deptId}`);
}
/**
* 部门新增
* @param data 参数
*/
export function deptAdd(data: Partial<Dept>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 部门更新
* @param data 参数
*/
export function deptUpdate(data: Partial<Dept>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 注意这里只允许单删除
* @param deptId ID
* @returns void
*/
export function deptRemove(deptId: ID) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: deptId },
});
}

View File

@@ -0,0 +1,14 @@
export interface Dept {
creationTime: string;
creatorId?: string | null;
state: boolean;
deptName: string;
deptCode?: string;
leader?: string;
leaderName?: string;
parentId: string | null;
remark?: string;
orderNum: number;
id: string;
children?: Dept[];
}

View File

@@ -0,0 +1,17 @@
export interface DictData {
id: string;
isDeleted: boolean;
orderNum: number;
state: boolean;
remark: string | null;
listClass: string | null;
cssClass: string | null;
dictType: string;
dictLabel: string | null;
dictValue: string;
isDefault: boolean;
creationTime: string;
creatorId: string | null;
lastModifierId: string | null;
lastModificationTime: string | null;
}

View File

@@ -0,0 +1,78 @@
import type { DictData } from './dict-data-model';
import type { ID, IDS, PageQuery } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
dictDataExport = '/dict/data/export',
dictDataInfo = '/dictionary/dic-type',
dictDataList = '/dict/data/list',
root = '/dictionary',
}
/**
* 主要是DictTag组件使用
* @param dictType 字典类型
* @returns 字典数据
*/
export function dictDataInfo(dictType: string) {
return requestClient.get<DictData[]>(`${Api.dictDataInfo}/${dictType}`);
}
/**
* 字典数据
* @param params 查询参数
* @returns 字典数据列表
*/
export function dictDataList(params?: PageQuery) {
return requestClient.get<DictData[]>(Api.root, { params });
}
/**
* 导出字典数据
* @param data 表单参数
* @returns blob
*/
export function dictDataExport(data: Partial<DictData>) {
return commonExport(Api.dictDataExport, data);
}
/**
* 删除
* @param dictIds 字典ID Array
* @returns void
*/
export function dictDataRemove(dictIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: dictIds.join(',') },
});
}
/**
* 新增
* @param data 表单参数
* @returns void
*/
export function dictDataAdd(data: Partial<DictData>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 修改
* @param data 表单参数
* @returns void
*/
export function dictDataUpdate(data: Partial<DictData>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 查询字典数据详细
* @param id 字典ID
* @returns 字典数据
*/
export function dictDetailInfo(id: ID) {
return requestClient.get<DictData>(`${Api.root}/${id}`);
}

View File

@@ -0,0 +1,13 @@
export interface DictType {
id: string;
isDeleted: boolean;
orderNum: number;
state: boolean | null;
dictName: string;
dictType: string;
remark: string | null;
creationTime: string;
creatorId: string | null;
lastModifierId: string | null;
lastModificationTime: string | null;
}

View File

@@ -0,0 +1,87 @@
import type { DictType } from './dict-type-model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
dictOptionSelectList = '/dictionary-type/select-data-list',
dictTypeExport = '/dictionary-type/export',
dictTypeList = '/dictionary-type/list',
dictTypeRefreshCache = '/dictionary-type/refreshCache',
root = '/dictionary-type',
}
/**
* 获取字典类型列表
* @param params 请求参数
* @returns list
*/
export function dictTypeList(params?: PageQuery) {
return requestClient.get<PageResult<DictType>>(Api.root, { params });
}
/**
* 导出字典类型列表
* @param data 表单参数
* @returns blob
*/
export function dictTypeExport(data: Partial<DictType>) {
return commonExport(Api.dictTypeExport, data);
}
/**
* 删除字典类型
* @param dictIds 字典类型id数组
* @returns void
*/
export function dictTypeRemove(dictIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: dictIds.join(',') },
});
}
/**
* 刷新字典缓存
* @returns void
*/
export function refreshDictTypeCache() {
return requestClient.deleteWithMsg<void>(Api.dictTypeRefreshCache);
}
/**
* 新增
* @param data 表单参数
* @returns void
*/
export function dictTypeAdd(data: Partial<DictType>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 修改
* @param data 表单参数
* @returns void
*/
export function dictTypeUpdate(data: Partial<DictType>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 查询详情
* @param dictId 字典类型id
* @returns 信息
*/
export function dictTypeInfo(dictId: ID) {
return requestClient.get<DictType>(`${Api.root}/${dictId}`);
}
/**
* 这个在ele用到 v5用不上
* 下拉框 返回值和list一样
* @returns options
*/
export function dictOptionSelectList() {
return requestClient.get<DictType[]>(Api.dictOptionSelectList);
}

View File

@@ -0,0 +1,84 @@
import type { Menu, MenuOption, MenuQuery, MenuResp } from './model';
import type { ID, IDS } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
menuList = '/menu/list',
menuTreeSelect = '/menu/tree',
root = '/menu',
tenantPackageMenuTreeselect = '/menu/tenantPackageMenuTreeselect',
}
/**
* 菜单列表
* @param params 参数
* @returns 列表
*/
export function menuList(params?: MenuQuery) {
return requestClient.get<Menu[]>(Api.menuList, { params });
}
/**
* 菜单详情
* @param menuId 菜单id
* @returns 菜单详情
*/
export function menuInfo(menuId: ID) {
return requestClient.get<Menu>(`${Api.root}/${menuId}`);
}
/**
* 菜单新增
* @param data 参数
*/
export function menuAdd(data: Partial<Menu>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 菜单更新
* @param data 参数
*/
export function menuUpdate(data: Partial<Menu>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 菜单删除
* @param menuIds ids
*/
export function menuRemove(menuIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: menuIds.join(',') },
});
}
/**
* 下拉框使用 返回所有的菜单
* @returns []
*/
export function menuTreeSelect() {
return requestClient.get<MenuOption[]>(Api.menuTreeSelect);
}
/**
* 租户套餐使用
* @param packageId packageId
* @returns resp
*/
export function tenantPackageMenuTreeSelect(packageId: ID) {
return requestClient.get<MenuResp>(
`${Api.tenantPackageMenuTreeselect}/${packageId}`,
);
}
/**
* 批量删除菜单
* @param menuIds 菜单ids
* @returns void
*/
export function menuCascadeRemove(menuIds: IDS) {
return requestClient.deleteWithMsg<void>(`${Api.root}/cascade/${menuIds}`);
}

View File

@@ -0,0 +1,57 @@
export interface Menu {
id: string;
isDeleted: boolean;
creationTime: string;
creatorId: string | null;
lastModifierId: string | null;
lastModificationTime: string | null;
orderNum: number;
state: boolean;
menuName: string;
routerName?: string | null;
menuType: string;
permissionCode?: string | null;
parentId: string;
menuIcon?: string | null;
router?: string | null;
isLink: boolean;
isCache: boolean;
isShow: boolean;
remark?: string | null;
component?: string | null;
query?: string | null;
children?: Menu[];
}
/**
* @description 菜单信息
* @param menuName 菜单名称
*/
export interface MenuOption {
id: string;
parentId: string;
orderNum: number;
menuName: string;
menuType: string;
menuIcon?: string | null;
children?: MenuOption[] | null;
}
/**
* @description 菜单返回
* @param checkedKeys 选中的菜单id
* @param menus 菜单信息
*/
export interface MenuResp {
checkedKeys: string[];
menus: MenuOption[];
}
/**
* 菜单表单查询
*/
export interface MenuQuery {
menuName?: string;
isShow?: boolean;
state?: boolean;
}

View File

@@ -0,0 +1,53 @@
import type { Notice } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
root = '/notice',
}
/**
* 通知公告分页
* @param params 分页参数
* @returns 分页结果
*/
export function noticeList(params?: PageQuery) {
return requestClient.get<PageResult<Notice>>(Api.root, { params });
}
/**
* 通知公告详情
* @param id id
* @returns 详情
*/
export function noticeInfo(id: ID) {
return requestClient.get<Notice>(`${Api.root}/${id}`);
}
/**
* 通知公告新增
* @param data 参数
*/
export function noticeAdd(data: Partial<Notice>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 通知公告更新
* @param data 参数
*/
export function noticeUpdate(data: any) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 通知公告删除
* @param ids ids
*/
export function noticeRemove(ids: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: ids.join(',') },
});
}

View File

@@ -0,0 +1,13 @@
export interface Notice {
id: string;
title: string;
type: string;
content: string;
state: boolean;
isDeleted: boolean;
creationTime: string;
creatorId?: string | null;
lastModifierId?: string | null;
lastModificationTime?: string | null;
orderNum: number;
}

View File

@@ -0,0 +1,48 @@
import type { OssConfig } from './model';
import type { ID, IDS, PageQuery } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
ossConfigChangeStatus = '/resource/oss/config/changeStatus',
ossConfigList = '/resource/oss/config/list',
root = '/resource/oss/config',
}
// 获取OSS配置列表
export function ossConfigList(params?: PageQuery) {
return requestClient.get<OssConfig[]>(Api.ossConfigList, { params });
}
// 获取OSS配置的信息
export function ossConfigInfo(ossConfigId: ID) {
return requestClient.get<OssConfig>(`${Api.root}/${ossConfigId}`);
}
// 添加新的OSS配置
export function ossConfigAdd(data: Partial<OssConfig>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
// 更新现有的OSS配置
export function ossConfigUpdate(data: Partial<OssConfig>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
// 删除OSS配置
export function ossConfigRemove(ossConfigIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: ossConfigIds.join(',') },
});
}
// 更改OSS配置的状态
export function ossConfigChangeStatus(data: any) {
const requestData: Partial<OssConfig> = {
ossConfigId: data.ossConfigId,
status: data.status,
configKey: data.configKey,
};
return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);
}

View File

@@ -0,0 +1,16 @@
export interface OssConfig {
ossConfigId: number;
configKey: string;
accessKey: string;
secretKey: string;
bucketName: string;
prefix: string;
endpoint: string;
domain: string;
isHttps: string;
region: string;
status: string;
ext1: string;
remark: string;
accessPolicy: string;
}

View File

@@ -0,0 +1,77 @@
import type { AxiosRequestConfig } from '@vben/request';
import type { OssFile } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import { ContentTypeEnum } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
ossDownload = '/resource/oss/download',
ossInfo = '/resource/oss/listByIds',
ossList = '/resource/oss/list',
ossUpload = '/resource/oss/upload',
root = '/resource/oss',
}
/**
* 文件list
* @param params 参数
* @returns 分页
*/
export function ossList(params?: PageQuery) {
return requestClient.get<PageResult<OssFile>>(Api.ossList, { params });
}
/**
* 查询文件信息 返回为数组
* @param ossIds id数组
* @returns 信息数组
*/
export function ossInfo(ossIds: ID | IDS) {
return requestClient.get<OssFile[]>(`${Api.ossInfo}/${ossIds}`);
}
/**
* @deprecated 使用apps/web-antd/src/api/core/upload.ts uploadApi方法
* @param file 文件
* @returns void
*/
export function ossUpload(file: Blob | File) {
const formData = new FormData();
formData.append('file', file);
return requestClient.postWithMsg(Api.ossUpload, formData, {
headers: { 'Content-Type': ContentTypeEnum.FORM_DATA },
timeout: 30 * 1000,
});
}
/**
* 下载文件 返回为二进制
* @param ossId ossId
* @param onDownloadProgress 下载进度(可选)
* @returns blob
*/
export function ossDownload(
ossId: ID,
onDownloadProgress?: AxiosRequestConfig['onDownloadProgress'],
) {
return requestClient.get<Blob>(`${Api.ossDownload}/${ossId}`, {
responseType: 'blob',
timeout: 30 * 1000,
isTransformResponse: false,
onDownloadProgress,
});
}
/**
* 删除文件
* @param ossIds id数组
* @returns void
*/
export function ossRemove(ossIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: ossIds.join(',') },
});
}

View File

@@ -0,0 +1,28 @@
export interface OssFile {
ossId: string;
fileName: string;
originalName: string;
fileSuffix: string;
url: string;
createTime: string;
createBy: number;
createByName: string;
service: string;
}
export interface OssConfig {
ossConfigId: number;
configKey: string;
accessKey: string;
secretKey: string;
bucketName: string;
prefix: string;
endpoint: string;
domain: string;
isHttps: string;
region: string;
status: string;
ext1: string;
remark: string;
accessPolicy: string;
}

View File

@@ -0,0 +1,77 @@
import type { Post } from './model';
import type { ID, IDS, PageQuery } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
postExport = '/post/export',
postList = '/post/list',
postSelect = '/post/select-data-list',
root = '/post',
}
/**
* 获取岗位列表
* @param params 参数
* @returns Post[]
*/
export function postList(params?: PageQuery) {
return requestClient.get<Post[]>(Api.root, { params });
}
/**
* 导出岗位信息
* @param data 请求参数
* @returns blob
*/
export function postExport(data: Partial<Post>) {
return commonExport(Api.postExport, data);
}
/**
* 查询岗位信息
* @param id 岗位id
* @returns 岗位信息
*/
export function postInfo(id: ID) {
return requestClient.get<Post>(`${Api.root}/${id}`);
}
/**
* 岗位新增
* @param data 参数
* @returns void
*/
export function postAdd(data: Partial<Post>) {
return requestClient.postWithMsg<void>(Api.root, data);
}
/**
* 岗位更新
* @param data 参数
* @returns void
*/
export function postUpdate(data: Partial<Post>) {
return requestClient.putWithMsg<void>(`${Api.root}/${data.id}`, data);
}
/**
* 岗位删除
* @param postIds ids
* @returns void
*/
export function postRemove(postIds: IDS) {
return requestClient.deleteWithMsg<void>(Api.root, {
params: { ids: postIds.join(',') },
});
}
/**
* 获取岗位下拉列表
* @returns 岗位
*/
export function postOptionSelect(deptId: string) {
return requestClient.get<Post[]>(`${Api.postSelect}?keywords=${deptId}`);
}

Some files were not shown because too many files have changed in this diff Show More