diff --git a/Yi.Vben5.Vue3/.browserslistrc b/Yi.Vben5.Vue3/.browserslistrc
new file mode 100644
index 00000000..dc3bc09a
--- /dev/null
+++ b/Yi.Vben5.Vue3/.browserslistrc
@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11
diff --git a/Yi.Vben5.Vue3/.changeset/README.md b/Yi.Vben5.Vue3/.changeset/README.md
new file mode 100644
index 00000000..5654e898
--- /dev/null
+++ b/Yi.Vben5.Vue3/.changeset/README.md
@@ -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)
diff --git a/Yi.Vben5.Vue3/.changeset/config.json b/Yi.Vben5.Vue3/.changeset/config.json
new file mode 100644
index 00000000..f954fb4b
--- /dev/null
+++ b/Yi.Vben5.Vue3/.changeset/config.json
@@ -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": []
+}
diff --git a/Yi.Vben5.Vue3/.commitlintrc.js b/Yi.Vben5.Vue3/.commitlintrc.js
new file mode 100644
index 00000000..02e33fa6
--- /dev/null
+++ b/Yi.Vben5.Vue3/.commitlintrc.js
@@ -0,0 +1 @@
+export { default } from '@vben/commitlint-config';
diff --git a/Yi.Vben5.Vue3/.dockerignore b/Yi.Vben5.Vue3/.dockerignore
new file mode 100644
index 00000000..52b833a9
--- /dev/null
+++ b/Yi.Vben5.Vue3/.dockerignore
@@ -0,0 +1,7 @@
+node_modules
+.git
+.gitignore
+*.md
+dist
+.turbo
+dist.zip
diff --git a/Yi.Vben5.Vue3/.editorconfig b/Yi.Vben5.Vue3/.editorconfig
new file mode 100644
index 00000000..179aec6f
--- /dev/null
+++ b/Yi.Vben5.Vue3/.editorconfig
@@ -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
diff --git a/Yi.Vben5.Vue3/.gitattributes b/Yi.Vben5.Vue3/.gitattributes
new file mode 100644
index 00000000..d4e5bd3e
--- /dev/null
+++ b/Yi.Vben5.Vue3/.gitattributes
@@ -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
\ No newline at end of file
diff --git a/Yi.Vben5.Vue3/.gitconfig b/Yi.Vben5.Vue3/.gitconfig
new file mode 100644
index 00000000..4b28a69c
--- /dev/null
+++ b/Yi.Vben5.Vue3/.gitconfig
@@ -0,0 +1,2 @@
+[core]
+ ignorecase = false
diff --git a/Yi.Vben5.Vue3/.gitignore b/Yi.Vben5.Vue3/.gitignore
new file mode 100644
index 00000000..96ab4755
--- /dev/null
+++ b/Yi.Vben5.Vue3/.gitignore
@@ -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
diff --git a/Yi.Vben5.Vue3/.gitpod.yml b/Yi.Vben5.Vue3/.gitpod.yml
new file mode 100644
index 00000000..5fda2cf7
--- /dev/null
+++ b/Yi.Vben5.Vue3/.gitpod.yml
@@ -0,0 +1,6 @@
+ports:
+ - port: 5555
+ onOpen: open-preview
+tasks:
+ - init: npm i -g corepack && pnpm install
+ command: pnpm run dev:play
diff --git a/Yi.Vben5.Vue3/.node-version b/Yi.Vben5.Vue3/.node-version
new file mode 100644
index 00000000..ee5c2446
--- /dev/null
+++ b/Yi.Vben5.Vue3/.node-version
@@ -0,0 +1 @@
+22.1.0
diff --git a/Yi.Vben5.Vue3/.npmrc b/Yi.Vben5.Vue3/.npmrc
new file mode 100644
index 00000000..21147aff
--- /dev/null
+++ b/Yi.Vben5.Vue3/.npmrc
@@ -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
diff --git a/Yi.Vben5.Vue3/.prettierignore b/Yi.Vben5.Vue3/.prettierignore
new file mode 100644
index 00000000..d0b0ca13
--- /dev/null
+++ b/Yi.Vben5.Vue3/.prettierignore
@@ -0,0 +1,18 @@
+dist
+dev-dist
+.local
+.output.js
+node_modules
+.nvmrc
+coverage
+CODEOWNERS
+.nitro
+.output
+
+
+**/*.svg
+**/*.sh
+
+public
+.npmrc
+*-lock.yaml
diff --git a/Yi.Vben5.Vue3/.prettierrc.mjs b/Yi.Vben5.Vue3/.prettierrc.mjs
new file mode 100644
index 00000000..3e25d2cf
--- /dev/null
+++ b/Yi.Vben5.Vue3/.prettierrc.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/prettier-config';
diff --git a/Yi.Vben5.Vue3/.stylelintignore b/Yi.Vben5.Vue3/.stylelintignore
new file mode 100644
index 00000000..f4b2db2c
--- /dev/null
+++ b/Yi.Vben5.Vue3/.stylelintignore
@@ -0,0 +1,4 @@
+dist
+public
+__tests__
+coverage
diff --git a/Yi.Vben5.Vue3/CHANGELOG.md b/Yi.Vben5.Vue3/CHANGELOG.md
new file mode 100644
index 00000000..cb359556
--- /dev/null
+++ b/Yi.Vben5.Vue3/CHANGELOG.md
@@ -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**
+
+- 基础功能已经开发完毕
+- 工作流相关模块等待后端重构后开发
diff --git a/Yi.Vben5.Vue3/LICENSE b/Yi.Vben5.Vue3/LICENSE
new file mode 100644
index 00000000..6b0d5105
--- /dev/null
+++ b/Yi.Vben5.Vue3/LICENSE
@@ -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.
diff --git a/Yi.Vben5.Vue3/README.md b/Yi.Vben5.Vue3/README.md
new file mode 100644
index 00000000..4913f471
--- /dev/null
+++ b/Yi.Vben5.Vue3/README.md
@@ -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)
+
+## 提示
+
+该仓库使用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)
+
+## 🚀系统截图
+
+
+
+
+
+
+
+
+## 安装使用
+
+前置准备环境(只能用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 版本打开方式一致)
+
+
+
+使用内嵌 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
+
+| [
](http://godban.github.io/browsers-support-badges/)IE | [
](http://godban.github.io/browsers-support-badges/)Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari |
+| :-: | :-: | :-: | :-: | :-: |
+| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/.env b/Yi.Vben5.Vue3/apps/backend-mock/.env
new file mode 100644
index 00000000..b20c4a65
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/.env
@@ -0,0 +1,3 @@
+PORT=5320
+ACCESS_TOKEN_SECRET=access_token_secret
+REFRESH_TOKEN_SECRET=refresh_token_secret
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/README.md b/Yi.Vben5.Vue3/apps/backend-mock/README.md
new file mode 100644
index 00000000..401bda76
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/README.md
@@ -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
+```
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/auth/codes.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/codes.ts
new file mode 100644
index 00000000..7ba01270
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/codes.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/auth/login.post.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/login.post.ts
new file mode 100644
index 00000000..df5737a2
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/login.post.ts
@@ -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,
+ });
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/auth/logout.post.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/logout.post.ts
new file mode 100644
index 00000000..ac6afe94
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/logout.post.ts
@@ -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('');
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/auth/refresh.post.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/refresh.post.ts
new file mode 100644
index 00000000..7df4d34f
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/auth/refresh.post.ts
@@ -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;
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/demo/bigint.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/demo/bigint.ts
new file mode 100644
index 00000000..880cc5ea
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/demo/bigint.ts
@@ -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;
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/menu/all.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/menu/all.ts
new file mode 100644
index 00000000..580cee4f
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/menu/all.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/status.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/status.ts
new file mode 100644
index 00000000..41773e1d
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/status.ts
@@ -0,0 +1,5 @@
+export default eventHandler((event) => {
+ const { status } = getQuery(event);
+ setResponseStatus(event, Number(status));
+ return useResponseError(`${status}`);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/.post.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/.post.ts
new file mode 100644
index 00000000..c529ea1b
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/.post.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].delete.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].delete.ts
new file mode 100644
index 00000000..e48f051c
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].delete.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].put.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].put.ts
new file mode 100644
index 00000000..aa55c085
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/[id].put.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/list.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/list.ts
new file mode 100644
index 00000000..ae819b62
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/dept/list.ts
@@ -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 = {
+ 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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/list.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/list.ts
new file mode 100644
index 00000000..5328b2fd
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/list.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/name-exists.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/name-exists.ts
new file mode 100644
index 00000000..5599c22b
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/name-exists.ts
@@ -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 = {};
+
+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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/path-exists.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/path-exists.ts
new file mode 100644
index 00000000..64774f79
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/menu/path-exists.ts
@@ -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 = { '/': 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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/system/role/list.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/system/role/list.ts
new file mode 100644
index 00000000..2d6feae4
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/system/role/list.ts
@@ -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 = {
+ 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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/table/list.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/table/list.ts
new file mode 100644
index 00000000..b360f8db
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/table/list.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/test.get.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/test.get.ts
new file mode 100644
index 00000000..ca4a500b
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/test.get.ts
@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test get handler');
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/test.post.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/test.post.ts
new file mode 100644
index 00000000..698cf211
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/test.post.ts
@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test post handler');
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/upload.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/upload.ts
new file mode 100644
index 00000000..1bb9e602
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/upload.ts
@@ -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")
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/api/user/info.ts b/Yi.Vben5.Vue3/apps/backend-mock/api/user/info.ts
new file mode 100644
index 00000000..cfa2346c
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/api/user/info.ts
@@ -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);
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/error.ts b/Yi.Vben5.Vue3/apps/backend-mock/error.ts
new file mode 100644
index 00000000..e20beac4
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/error.ts
@@ -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;
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/middleware/1.api.ts b/Yi.Vben5.Vue3/apps/backend-mock/middleware/1.api.ts
new file mode 100644
index 00000000..b51f22d8
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/middleware/1.api.ts
@@ -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, '演示环境,禁止修改');
+ }
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/nitro.config.ts b/Yi.Vben5.Vue3/apps/backend-mock/nitro.config.ts
new file mode 100644
index 00000000..c0fc13e2
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/nitro.config.ts
@@ -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': '*',
+ },
+ },
+ },
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/package.json b/Yi.Vben5.Vue3/apps/backend-mock/package.json
new file mode 100644
index 00000000..cc0b8d53
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/package.json
@@ -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:"
+ }
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/routes/[...].ts b/Yi.Vben5.Vue3/apps/backend-mock/routes/[...].ts
new file mode 100644
index 00000000..99f544b6
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/routes/[...].ts
@@ -0,0 +1,13 @@
+export default defineEventHandler(() => {
+ return `
+Hello Vben Admin
+Mock service is starting
+
+`;
+});
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.build.json b/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.build.json
new file mode 100644
index 00000000..64f86c6b
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.json b/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.json
new file mode 100644
index 00000000..43008af1
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "./.nitro/types/tsconfig.json"
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/utils/cookie-utils.ts b/Yi.Vben5.Vue3/apps/backend-mock/utils/cookie-utils.ts
new file mode 100644
index 00000000..78f3aab7
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/utils/cookie-utils.ts
@@ -0,0 +1,26 @@
+import type { EventHandlerRequest, H3Event } from 'h3';
+
+export function clearRefreshTokenCookie(event: H3Event) {
+ deleteCookie(event, 'jwt', {
+ httpOnly: true,
+ sameSite: 'none',
+ secure: true,
+ });
+}
+
+export function setRefreshTokenCookie(
+ event: H3Event,
+ refreshToken: string,
+) {
+ setCookie(event, 'jwt', refreshToken, {
+ httpOnly: true,
+ maxAge: 24 * 60 * 60, // unit: seconds
+ sameSite: 'none',
+ secure: true,
+ });
+}
+
+export function getRefreshTokenFromCookie(event: H3Event) {
+ const refreshToken = getCookie(event, 'jwt');
+ return refreshToken;
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/utils/jwt-utils.ts b/Yi.Vben5.Vue3/apps/backend-mock/utils/jwt-utils.ts
new file mode 100644
index 00000000..8cfc6843
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/utils/jwt-utils.ts
@@ -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,
+): null | Omit {
+ 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 {
+ 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;
+ }
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/utils/mock-data.ts b/Yi.Vben5.Vue3/apps/backend-mock/utils/mock-data.ts
new file mode 100644
index 00000000..192f30a0
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/utils/mock-data.ts
@@ -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;
+}
diff --git a/Yi.Vben5.Vue3/apps/backend-mock/utils/response.ts b/Yi.Vben5.Vue3/apps/backend-mock/utils/response.ts
new file mode 100644
index 00000000..53bb7ced
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/backend-mock/utils/response.ts
@@ -0,0 +1,68 @@
+import type { EventHandlerRequest, H3Event } from 'h3';
+
+export function useResponseSuccess(data: T) {
+ return {
+ code: 0,
+ data,
+ error: null,
+ message: 'ok',
+ };
+}
+
+export function usePageResponseSuccess(
+ 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,
+ message = 'Forbidden Exception',
+) {
+ setResponseStatus(event, 403);
+ return useResponseError(message, message);
+}
+
+export function unAuthorizedResponse(event: H3Event) {
+ setResponseStatus(event, 401);
+ return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
+}
+
+export function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function pagination(
+ 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));
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/.env b/Yi.Vben5.Vue3/apps/web-antd/.env
new file mode 100644
index 00000000..1850fc64
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/.env
@@ -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
diff --git a/Yi.Vben5.Vue3/apps/web-antd/.env.analyze b/Yi.Vben5.Vue3/apps/web-antd/.env.analyze
new file mode 100644
index 00000000..dacf2fed
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api/app
+
+VITE_VISUALIZER=true
diff --git a/Yi.Vben5.Vue3/apps/web-antd/.env.development b/Yi.Vben5.Vue3/apps/web-antd/.env.development
new file mode 100644
index 00000000..435f9410
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/.env.development
@@ -0,0 +1,25 @@
+# 端口号
+VITE_PORT=5666
+
+VITE_BASE=/
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=false
+# 是否打开 devtools,true 为打开,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
\ No newline at end of file
diff --git a/Yi.Vben5.Vue3/apps/web-antd/.env.production b/Yi.Vben5.Vue3/apps/web-antd/.env.production
new file mode 100644
index 00000000..40f2fd1c
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/.env.production
@@ -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
+
diff --git a/Yi.Vben5.Vue3/apps/web-antd/.env.test b/Yi.Vben5.Vue3/apps/web-antd/.env.test
new file mode 100644
index 00000000..f66a206f
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/.env.test
@@ -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
+
diff --git a/Yi.Vben5.Vue3/apps/web-antd/index.html b/Yi.Vben5.Vue3/apps/web-antd/index.html
new file mode 100644
index 00000000..33d34a9e
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+ <%= VITE_APP_TITLE %>
+
+
+
+
+
+
+
diff --git a/Yi.Vben5.Vue3/apps/web-antd/package.json b/Yi.Vben5.Vue3/apps/web-antd/package.json
new file mode 100644
index 00000000..e5e9941c
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/package.json
@@ -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"
+ }
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/postcss.config.mjs b/Yi.Vben5.Vue3/apps/web-antd/postcss.config.mjs
new file mode 100644
index 00000000..3d807045
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/adapter/component/index.ts b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/component/index.ts
new file mode 100644
index 00000000..2b3c1522
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/component/index.ts
@@ -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 = (
+ component: T,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ 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 = {};
+ 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> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // 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 };
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/adapter/form.ts b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/form.ts
new file mode 100644
index 00000000..93b62621
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/form.ts
@@ -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({
+ 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;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
+export type FormSchemaGetter = () => VbenFormSchema[];
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/adapter/vxe-table.ts b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/vxe-table.ts
new file mode 100644
index 00000000..31849aea
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/adapter/vxe-table.ts
@@ -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[1],
+) {
+ return tableApi?.grid?.getCheckboxRecords?.()?.length > 0;
+}
+
+/**
+ * 通用的 排序参数添加到请求参数中
+ * @param params 请求参数
+ * @param sortList vxe-table的排序参数
+ */
+export function addSortParams(
+ params: Record,
+ 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;
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/api/common.d.ts b/Yi.Vben5.Vue3/apps/web-antd/src/api/common.d.ts
new file mode 100644
index 00000000..22edf6a7
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/api/common.d.ts
@@ -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 {
+ 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;
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/api/core/auth.ts b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/auth.ts
new file mode 100644
index 00000000..76cd8ac5
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/auth.ts
@@ -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(
+ '/account/login',
+ { ...data, clientId },
+ {
+ encrypt: true,
+ },
+ );
+}
+
+/**
+ * 用户登出
+ * @returns void
+ */
+export async function doLogout() {
+ const resp = await requestClient.post>(
+ '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('/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('/tenant/select');
+}
+
+/**
+ * vben的 先不删除
+ * @returns string[]
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
+
+/**
+ * 绑定第三方账号
+ * @param source 绑定的来源
+ * @returns 跳转url
+ */
+export function authBinding(source: string, tenantId: string) {
+ return requestClient.get(`/auth/binding/${source}`, {
+ params: {
+ domain: window.location.host,
+ tenantId,
+ },
+ });
+}
+
+/**
+ * 取消绑定
+ * @param id id
+ */
+export function authUnbinding(id: string) {
+ return requestClient.deleteWithMsg(`/auth/unlock/${id}`);
+}
+
+/**
+ * oauth授权回调
+ * @param data oauth授权
+ * @returns void
+ */
+export function authCallback(data: AuthApi.OAuthLoginParams) {
+ return requestClient.post('/auth/social/callback', data);
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/api/core/captcha.ts b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/captcha.ts
new file mode 100644
index 00000000..734ff637
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/captcha.ts
@@ -0,0 +1,42 @@
+import { requestClient } from '#/api/request';
+
+/**
+ * 发送短信验证码
+ * @param phonenumber 手机号
+ * @returns void
+ */
+export function sendSmsCode(phonenumber: string) {
+ return requestClient.get('/resource/sms/code', {
+ params: { phonenumber },
+ });
+}
+
+/**
+ * 发送邮件验证码
+ * @param email 邮箱
+ * @returns void
+ */
+export function sendEmailCode(email: string) {
+ return requestClient.get('/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('/account/captcha-image');
+}
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/api/core/index.ts b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/index.ts
new file mode 100644
index 00000000..04256867
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/index.ts
@@ -0,0 +1,4 @@
+export * from './auth';
+export * from './menu';
+export * from './upload';
+export * from './user';
diff --git a/Yi.Vben5.Vue3/apps/web-antd/src/api/core/menu.ts b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/menu.ts
new file mode 100644
index 00000000..64a71444
--- /dev/null
+++ b/Yi.Vben5.Vue3/apps/web-antd/src/api/core/menu.ts
@@ -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