97 Commits

Author SHA1 Message Date
dap
4baa0aed8b refactor: 这是iframe页面! iframe页面! iframe页面! 不是我写的真服了 2025-06-16 11:17:02 +08:00
尘墨
b2d3cf10aa !42 perf: 优化顶部栏头像展开用户信息绑定
Merge pull request !42 from 尘墨/main
2025-06-13 07:22:39 +00:00
dap
63d2b38fd1 refactor: tinymce在modal下全屏 2025-06-12 14:58:15 +08:00
dap
78cd6677c3 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-11 21:01:42 +08:00
Netfan
c0962fec18 fix: auto close popup on deactivated (#6368)
* 修复挂载到内容区域的弹窗和抽屉被意外关闭的问题
2025-06-11 12:20:52 +08:00
dap
d38093ca7d refactor: Partial 2025-06-10 10:53:08 +08:00
dap
687c33ec29 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-10 10:51:00 +08:00
XiaoHetitu
8ba7bdf2bd fix(button): 为按钮添加type属性防止表单提交意外触发表单验证机制 (#6340)
在按钮组件中,按钮元素缺少type="button"属性可能导致在表单中意外提交。添加此属性以确保按钮行为符合预期。

Co-authored-by: yuanwj <ywj6792341@qq.com>
2025-06-08 17:56:24 +08:00
zyy
b015fbc9fc fix: [adapter] 表格配置类型报错 (#6327)
配置toolbarConfig中的search时会有类型报错
2025-06-08 17:53:55 +08:00
broBinChen
b69320c070 feat(hooks): support separate enter/leave delays in useHoverToggle (#6325)
Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-08 17:53:29 +08:00
zhang
dcccc213ce fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提… (#6300)
* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提交给后台保存

* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串'undefined’提交给后台保存
2025-06-08 17:51:16 +08:00
ali-pay
c0e601c020 fix: menu type is not 'button' (#6277)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:50:44 +08:00
RanMaoting
017ed1a9e1 types: 为useVbenVxeGrid添加泛型声明,使grid实例上能正确获取到行数据的类型 (#5653)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:43:02 +08:00
dap
598f371568 refactor: 代码生成 导入 hover样式 2025-06-08 12:53:05 +08:00
dap
ca2aadaf4a refactor: 租户套餐 新增 过滤租户相关菜单 2025-06-06 12:02:56 +08:00
dap
616db1c127 docs: changelog 2025-06-05 21:35:59 +08:00
dap
08de1a6f19 refactor: 代码生成 字典下拉加载改为每次都重新加载 2025-06-05 21:33:53 +08:00
dap
006370798b refactor: tinymce+表单 校验失败样式 2025-06-05 21:18:36 +08:00
dap
831700660c refactor: 角色去掉moredropdown 改为平铺 2025-06-04 21:53:11 +08:00
dap
a53b9382f5 refactor: 排序 默认值 2025-06-04 19:49:03 +08:00
dap
703586123a refactor: 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理 2025-06-04 19:06:18 +08:00
dap
0295418f79 fix: 菜单管理 路由地址的必填项不生效 2025-06-04 17:53:09 +08:00
dap
14b0d9b50f Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-04 09:18:54 +08:00
vben
b9aef618fe chore: release 5.5.7 2025-06-04 05:33:06 +08:00
Netfan
4102cc2211 feat: improve vbenCheckButtonGroup (#6329)
* 按钮组支持单选清除和多选限制最大选项数

* 按钮组支持icon插槽来定制图标
2025-06-03 23:11:56 +08:00
chewenye
ea776aa710 types: 扩展user-dropdown组件的menus类型,支持iconify (#6283)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-03 06:07:06 +08:00
huanghezhen
feb96dc8ea fix: resolve onClosed method failure in connectedComponent of useVbenModal (#6309) 2025-06-02 08:16:48 +08:00
wyc001122
470fd43b49 fix: 修复使用useVbenVxeGrid配置hasEmptyText、hasEmptyRender不生效的问题 (#6310) 2025-06-02 08:16:26 +08:00
zhang
76d106e474 fix: When defaultHomePage is inconsistent with user.homePath, the pa… (#6299)
* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal

* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal
2025-06-02 08:07:06 +08:00
wyc001122
78c3c9da6f docs(settings): 完善'生产环境动态配置'步骤 (#6297) 2025-06-02 08:05:23 +08:00
dap
8b7d717b21 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-31 18:53:41 +08:00
Netfan
081d08a7f8 fix: alert width fixed in small screen (#6312) 2025-05-30 19:54:26 +08:00
dap
0da75418d0 refactor: 菜单管理 刷新接口后 记录展开行的情况 (改为vxe配置) 2025-05-30 17:51:06 +08:00
dap
55f0da3085 refactor: tree-search 2025-05-30 14:24:40 +08:00
dap
3849800388 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-30 09:20:33 +08:00
dap
f913955259 docs: changelog 2025-05-29 19:45:38 +08:00
dap
8d6ef40d3e refactor: 适配新工作流预览 由logicflow改为iframe 真服了 2025-05-29 19:30:57 +08:00
dap
2141c93399 docs: readme 2025-05-29 11:17:29 +08:00
dap
906502f49b refactor: 字典项列表样式重构(下个版本再介入) 2025-05-29 11:10:46 +08:00
Netfan
96a10ca83f style: fix lint error (#6298) 2025-05-28 19:23:21 +08:00
dap
1f68fd31b7 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-28 17:13:50 +08:00
wyc001122
f31360ba4e feat: support for hybrid permission access control mode (#6294)
* feat: 添加混合权限访问控制模式

* feat: 文档补充
2025-05-28 17:01:58 +08:00
wyc001122
4eb16d6d3a fix: fix table-title slot not work (#6295) 2025-05-28 17:01:11 +08:00
dap
9db6ade1ed fix: oss配置是否默认取值 2025-05-27 19:35:22 +08:00
dap
2569e1da0d refactor: 字典项布局重构(未完成) 2025-05-27 14:27:01 +08:00
dap
2de9cd2334 docs: changelog 2025-05-27 11:52:41 +08:00
dap
739e04816a refactor: 只有超管才能对菜单进行增删改操作 2025-05-27 11:47:23 +08:00
dap
d9c57dfb61 feat: 流程预览dot画布+夜间背景色 2025-05-26 17:26:13 +08:00
dap
2217c96cd9 chore: ele版本会使用这个文件 只是为了不报错未找到对应组件才新建的这个文件 无实际意义 2025-05-26 17:08:15 +08:00
dap
752c1ac3ed refactor: 字典项布局重构(未完成) 2025-05-26 17:05:52 +08:00
liqiang0330
53304514b6 fix: Update index.ts (#6268)
* Update index.ts

VxeGridPropTypes.原文件缺少这个,现在补全!

* Update index.ts

增加空格!
2025-05-26 13:29:27 +08:00
dap
cf913f8b8d docs: changelog 2025-05-26 13:27:42 +08:00
dap
ad9c465622 update: remove unused file 2025-05-26 13:25:44 +08:00
dap
bea7c1a094 refactor: preserveTreeTableState 2025-05-26 09:28:54 +08:00
dap
95859e36a2 update: menuName i18n 2025-05-26 09:26:34 +08:00
dap
8bfc482a7f feat: logicflow显示流程图 2025-05-25 16:38:13 +08:00
Netfan
6fbf1387f5 fix: reset slider-captcha after login failed (#6275) 2025-05-25 16:04:56 +08:00
dap
c3edbec3f0 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-25 11:48:58 +08:00
Netfan
e5c937396d fix: json-bigint parse used in vxeTable (#6271)
* 修复vxeTable不能加载json-bigint解析的数据的问题
2025-05-24 13:01:58 +08:00
dap
45de9b7547 docs: changelog 2025-05-24 12:46:50 +08:00
dap
6daedd1de5 feat: 菜单级联删除 2025-05-24 12:45:18 +08:00
dap
c45eed90d9 fix: dept list 列宽调整 2025-05-24 11:59:39 +08:00
dap
845719d951 chore: version update 2025-05-23 16:29:49 +08:00
dap
4ef974ca4e feat: 跳转菜单图标更新 2025-05-23 16:07:17 +08:00
littlesparklet
af186f878d fix: repair the unexpected form default value (#5567)
* fix: Fix inconsistent spacing around search form (issue #5429)

* fix: repair the unexpected default value in validated form.(issue #5451)

* Update packages/@core/ui-kit/form-ui/src/use-form-context.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-23 16:05:11 +08:00
dap
2dc7e564b2 feat: 升级需要执行sql的提示 2025-05-23 15:55:25 +08:00
wyc001122
97894a940e feat: optimize logo display (#6267)
* feat(VbenAvatar): add fit property to VbenAvatar component

* feat(VbenLogo): add fit property to VbenLogo component

* feat(VbenLogo): add logo fit preference configuration

- Add preferences.logo.fit setting for logo display control
- Include corresponding documentation for the new preference

* feat(preferences): add default value for logo.fit preference

- Set default configuration for logo fit behavior
- Ensures consistent logo display across applications

* test(preferences): update configuration snapshots

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-23 15:24:01 +08:00
yingzi2019
48d70182b4 feat: improve check updates (#6257)
Co-authored-by: monkey <maotao@tutamail.com>
2025-05-23 15:23:06 +08:00
Netfan
a1091bad46 feat: enhances compatibility with APIs returning large numeric values (#6250) 2025-05-23 15:22:18 +08:00
zhang
9f9be21e2a fix: component Input is not registered when initialize page (#6246)
* fix: Component Input is not registered when initialize page

* fix: Component Input is not registered when initialize page
2025-05-23 15:21:09 +08:00
panda7
a2bdcd6e49 feat: ellipsis text automatically displays tooltip based on ellipsis (#6244)
* feat: ellipsis text automatically displays tooltip based on ellipsis

* feat: ellipsis text automatically displays tooltip based on ellipsis

---------

Co-authored-by: sqchen <9110848@qq.com>
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-05-23 15:20:38 +08:00
dap
a38f2de982 refactor: 所有本地路由(除个人中心/workflow-iframe)改为从后端返回 适配 2025-05-23 14:30:12 +08:00
dap
d039c53053 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-22 09:20:32 +08:00
ali-pay
11b2b5bcc2 fix: 修复菜单管理中按钮类型值错误的问题 (#6255) 2025-05-22 09:09:31 +08:00
LinaBell
ebef2c91e2 fix: tab cannot be displayed correctly after browser refresh (#6256) 2025-05-22 09:04:40 +08:00
Netfan
0c3edb10b0 fix: getFieldComponentRef will return actual ref within AsyncComponentWrapper (#6252)
修复异步加载组件时,表单的getFieldComponentRef方法没能获取到正确的组件实例
2025-05-21 14:48:51 +08:00
dap
801514dbe3 feat: 支持pdf预览(原生 浏览器接管) 2025-05-21 11:54:55 +08:00
dap
2dce7718d6 refactor: fallbackImageBase64 2025-05-20 21:16:43 +08:00
dap
8a8e090792 Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev 2025-05-20 12:56:14 +08:00
dap
6fc2c4e3cc Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-20 12:56:05 +08:00
wyc001122
8ac97688da fix(preferences): 更新内容内边距默认值 (#6233)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-20 09:50:23 +08:00
陈绍华
c89ec0088b style: 更改OSS配置的状态方法添加 TS 类型
Signed-off-by: 陈绍华 <marlboro027@foxmail.com>
2025-05-20 01:40:04 +00:00
dap
0a076f5e6e fix: 更新后的padding设置为0 2025-05-19 21:52:16 +08:00
dap
4fd68bc083 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-19 21:33:49 +08:00
李轻舟
2efacb3e5b docs: Update build.md (#6228) 2025-05-19 16:30:39 +08:00
wyc001122
dae46abb71 feat: additional-settings (#6225)
* feat(preferences): 补充VbenAdminLayout传入属性(来自偏好设置)

* docs(@vben/docs):update settings doc

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-19 16:29:15 +08:00
wyc001122
5ee2a74e2d fix(use-design-tokens): 完善element-plus暗色主题颜色 (#6224)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-19 16:27:34 +08:00
dap
79d89005b6 对错误用法的提示(完全无奈...) 2025-05-18 14:28:57 +08:00
afe1
d0b8349a2d perf: stub unbuild params (#6210) 2025-05-18 10:35:20 +08:00
wyc001122
34c4ecb047 fix: in mixed layout mode, the sidebar does not display when the first child node is an external link (#6219)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-18 10:34:41 +08:00
ming4762
3d9dba965f perf: perf the control logic of Tab (#6220)
* perf: perf the control logic of Tab

* 每个标签页Tab使用唯一的key来控制关闭打开等逻辑
* 统一函数获取tab的key
* 通过3种方式设置tab key:1、使用router query参数pageKey 2、使用路由meta参数fullPathKey设置使用fullPath或path作为key
* 单个路由可以打开多个标签页
* 如果设置fullPathKey为false,则query变更不会打开新的标签(这很实用)

* perf: perf the control logic of Tab

* perf: perf the control logic of Tab

* 测试用例适配

* perf: perf the control logic of Tab

* 解决AI提示的警告
2025-05-18 10:33:02 +08:00
dap
96b8ae94fd feat: 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化) 2025-05-16 16:48:22 +08:00
wyc001122
024c01d350 fix(@vben-core/shadcn-ui): fix disabled functionality not working in VbenTree component (#6205)
* fix(@vben-core/shadcn-ui): fix disabled functionality not working in VbenTree component

* fix(@vben-core/shadcn-ui): add cursor-not-allowed className when disabled and disable onfocus

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-05-16 14:13:43 +08:00
afe1
2adb8acd80 fix: css style (#6176) 2025-05-16 09:40:40 +08:00
panda7
a23bc4cb5c fix: the mobile terminal can wrap lines and expand slot attributes (#6165)
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-05-16 09:40:05 +08:00
XiaoHetitu
cf17a45d8d feat(tabs): 支持计算属性作为标签标题,解决 #6170 的问题 (#6163)
* feat(tabs): 支持动态函数作为标签标题

修改 `setTabTitle` 和 `tabsView` 逻辑,允许传入函数作为标签标题,以便动态生成标题内容

* feat(tabbar): 添加动态设置标签页标题功能

允许设置静态字符串或动态函数作为标签标题,支持根据状态或语言变化动态更新标题

* refactor(tabs): 移除冗余的newTabTitle2变量并优化标题设置逻辑

移除tabs组件中冗余的newTabTitle2变量,直接使用newTabTitle作为标题来源。同时,优化use-tabs和tabbar模块的标题设置逻辑,支持ComputedRef作为动态标题,提升代码简洁性和可维护性。

---------

Co-authored-by: yuanwj <ywj6792341@qq.com>
2025-05-16 09:37:50 +08:00
chewenye
b46ebe756e types: 导出authentication组件的type,自定义toolbarList时类型使用ToolbarType (#6158)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-05-16 09:36:49 +08:00
157 changed files with 2117 additions and 557 deletions

View File

@@ -241,6 +241,7 @@
"napi",
"nolebase",
"rollup",
"tinymce",
"vitest"
]
}

View File

@@ -1,3 +1,32 @@
# 1.4.1
**FEATURES**
- Tinymce添加在antd原生表单/useVbenForm下的校验样式
**BUG FIX**
- 菜单管理 路由地址的必填项不生效
**REFACTOR**
- 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理
- 代码生成 字典下拉加载 改为每次进入编辑页面都加载
# 1.4.0
**FEATURES**
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
-
- 菜单管理 级联删除 删除菜单和children
**REFACTOR**
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
- 流程图预览改为logicflow预览而非图片 ...然后后端又更新了 又改成iframe了
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
# 1.3.6
**BUG FIX**

View File

@@ -6,7 +6,7 @@
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
目前对应后端版本: **分布式5.3.1/微服务2.3.0**
目前对应后端版本: **分布式5.4.0/微服务2.4.0**
V1.1.0版本已支持离线图标
@@ -18,7 +18,7 @@ V1.2.0版本对接warmflow工作流
| 组件/框架 | 版本 |
| :------------- | :----- |
| vben | 5.5.4 |
| vben | 5.5.6 |
| ant-design-vue | 4.2.6 |
| vue | 3.5.13 |
@@ -46,14 +46,6 @@ admin 账号: admin admin123
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
## 关于表单
如果你觉得`useVbenForm`难度很大, 完全可以**使用原生antd表单**进行开发, 不一定非得用`useVbenForm`进行开发
`apps/web-antd/src/views/system/notice/notice-modal.vue``通知公告modal`使用**原生antd form**进行(反向🤔)重构, 不想用`useVbenForm`可参考该页面进行表单开发
复杂表单(如各种联动, 需要自定义样式布局, 需要自定义组件)**优先使用原生表单**(反正说了也没人听听😅)
## 预览图
![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/1.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/2.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/3.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/4.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/5.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/6.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/7.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/8.png) ![图片](https://gitee.com/dapppp/ruoyi-plus-vben5/raw/main/scripts/preview/9.png)

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

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "1.3.6",
"version": "1.4.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -10,44 +10,46 @@ import { $t } from '@vben/locales';
import { isArray } from 'lodash-es';
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
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',
// 一些组件是 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;
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;
},
},
// 选择项目必填国际化适配
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 { useVbenForm, z };
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -1,7 +1,12 @@
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(
@@ -90,8 +95,32 @@ export async function loginApi(data: AuthApi.LoginParams) {
* 用户登出
* @returns void
*/
export function doLogout() {
return requestClient.post<void>('/auth/logout');
export async function doLogout() {
const resp = await requestClient.post<HttpResponse<void>>(
'/auth/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]' },
'去检查你的后端配置!别盯着前端找问题了!这不是前端问题!',
),
),
]),
});
}
}
/**

View File

@@ -39,6 +39,11 @@ const { apiURL, clientId, enableEncrypt } = useAppConfig(
*/
let isLogoutProcessing = false;
/**
* 定义一个401专用异常 用于可能会用到的区分场景?
*/
export class UnauthorizedException extends Error {}
function createRequestClient(baseURL: string) {
const client = new RequestClient({
// 后端地址
@@ -228,7 +233,7 @@ function createRequestClient(baseURL: string) {
case 401: {
// 已经在登出过程中 不再执行
if (isLogoutProcessing) {
throw new Error(timeoutMsg);
throw new UnauthorizedException(timeoutMsg);
}
isLogoutProcessing = true;
const _msg = $t('http.loginTimeout');
@@ -238,7 +243,7 @@ function createRequestClient(baseURL: string) {
isLogoutProcessing = false;
});
// 不再执行下面逻辑
throw new Error(_msg);
throw new UnauthorizedException(_msg);
}
default: {
if (msg) {

View File

@@ -81,3 +81,12 @@ export function tenantPackageMenuTreeSelect(packageId: ID) {
`${Api.tenantPackageMenuTreeselect}/${packageId}`,
);
}
/**
* 批量删除菜单
* @param menuIds 菜单ids
* @returns void
*/
export function menuCascadeRemove(menuIds: IDS) {
return requestClient.deleteWithMsg<void>(`${Api.root}/cascade/${menuIds}`);
}

View File

@@ -37,7 +37,7 @@ export function ossConfigRemove(ossConfigIds: IDS) {
// 更改OSS配置的状态
export function ossConfigChangeStatus(data: any) {
const requestData = {
const requestData: Partial<OssConfig> = {
ossConfigId: data.ossConfigId,
status: data.status,
configKey: data.configKey,

View File

@@ -94,7 +94,7 @@ export function pageByCurrent(params?: PageQuery) {
*/
export function flowInfo(businessId: string) {
return requestClient.get<FlowInfoResponse>(
`/workflow/instance/flowImage/${businessId}`,
`/workflow/instance/flowHisTaskList/${businessId}`,
);
}

View File

@@ -36,6 +36,6 @@ export interface Flow {
}
export interface FlowInfoResponse {
image: string;
instanceId: string;
list: Flow[];
}

View File

@@ -8,6 +8,8 @@ import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
import { useUploadTip } from './upload-tip';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
@@ -28,6 +30,8 @@ const tokenTheme = computed(() => {
token: tokens,
};
});
useUploadTip();
</script>
<template>

View File

@@ -13,6 +13,7 @@ import { setupGlobalComponent } from '#/components/global';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
@@ -20,6 +21,9 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,

View File

@@ -202,13 +202,31 @@ const events = computed(() => {
</template>
<style lang="scss">
/***
由于modal/drawer的zIndex升级后为2000
这里会造成遮挡 修改为更高的zIndex
*/
// 展开层元素z-index
$dropdown-index: 2025;
@mixin tinymce-valid-fail($color) {
.app-tinymce {
// 最外层的tinymce容器
.tox-tinymce {
border-color: $color;
}
// focus样式
.tox .tox-edit-area::before {
border-color: $color;
border-right: none;
border-left: none;
}
}
}
.tox.tox-silver-sink.tox-tinymce-aux {
/** 该样式默认为1300的zIndex */
z-index: 2025;
z-index: $dropdown-index;
}
.tox-fullscreen .tox.tox-tinymce-aux {
z-index: $dropdown-index !important;
}
.app-tinymce {
@@ -218,5 +236,29 @@ const events = computed(() => {
.tox-promotion {
display: none;
}
/** 保持focus时与primary色一致 */
.tox .tox-edit-area::before {
border-color: hsl(var(--primary));
}
}
// antd原生表单 校验失败样式
.ant-form-item:has(.ant-form-item-explain-error) {
$error-color: #ff3860;
@include tinymce-valid-fail($error-color);
}
// useVbenForm 校验失败样式
.form-valid-error {
$error-color: hsl(var(--destructive));
@include tinymce-valid-fail($error-color);
}
// 全屏下样式处理 不去掉transform位置会异常
div[role='dialog']:has(.tox.tox-tinymce.tox-fullscreen) {
transform: none !important;
}
</style>

View File

@@ -138,8 +138,8 @@ watch(
:avatar
:menus
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
:description="userStore.userInfo?.email"
:tag-text="userStore.userInfo?.username"
@logout="handleLogout"
/>
</template>

View File

@@ -1,6 +1,7 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
RouteMeta,
RouteRecordStringComponent,
} from '@vben/types';
@@ -21,6 +22,37 @@ import { localMenuList } from './routes/local';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
/**
* 后端返回的meta有时候不包括需要的信息 比如activePath等
* 在这里定义映射
*/
const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
'/system/role-auth/user/:roleId(\\d+)': {
activePath: '/system/role',
requireHomeRedirect: true,
},
'/system/oss-config/index': {
activePath: '/system/oss',
requireHomeRedirect: true,
},
'/tool/gen-edit/index/:tableId(\\d+)': {
activePath: '/tool/gen',
requireHomeRedirect: true,
},
'/workflow/design/index': {
activePath: '/workflow/processDefinition',
requireHomeRedirect: true,
},
'/workflow/leaveEdit/index': {
activePath: '/demo/leave',
requireHomeRedirect: true,
},
};
/**
* 后台路由转vben路由
* @param menuList 后台菜单
@@ -98,6 +130,17 @@ function backMenuToVbenMenu(
path: menu.path,
};
// 处理meta映射
if (Object.keys(routeMetaMapping).includes(vbenRoute.path)) {
const routeMeta = routeMetaMapping[vbenRoute.path];
if (routeMeta) {
vbenRoute.meta = {
...vbenRoute.meta,
...(routeMeta as RouteMeta),
};
}
}
// 添加路由参数信息
if (menu.query) {
try {

View File

@@ -9,6 +9,7 @@ const {
/**
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
* 也可以直接在菜单管理配置
*/
const localRoutes: RouteRecordStringComponent[] = [
{
@@ -22,69 +23,6 @@ const localRoutes: RouteRecordStringComponent[] = [
name: 'Profile',
path: '/profile',
},
{
component: '/system/oss-config/index',
meta: {
activePath: '/system/oss',
icon: 'ant-design:setting-outlined',
title: 'oss配置',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'OssConfig',
path: '/system/oss-config',
},
{
component: '/tool/gen/edit-gen',
meta: {
activePath: '/tool/gen',
icon: 'tabler:code',
title: '生成配置',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'GenConfig',
path: '/code-gen/edit/:tableId',
},
{
component: '/system/role-assign/index',
meta: {
activePath: '/system/role',
icon: 'eos-icons:role-binding-outlined',
title: '分配角色',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'RoleAssign',
path: '/system/role-assign/:roleId',
},
{
component: '/workflow/components/flow-designer',
meta: {
activePath: '/workflow/processDefinition',
icon: 'fluent-mdl2:flow',
title: '流程设计',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'WorkflowDesigner',
path: '/workflow/designer',
},
/**
* 需要添加iframe路由 同目录的./workflow-iframe.ts
*/
{
component: 'workflow/leave/leave-form',
meta: {
icon: 'flat-color-icons:leave',
title: '请假申请',
activePath: '/demo/leave',
hideInMenu: true,
requireHomeRedirect: true,
},
name: 'WorkflowLeaveIndex',
path: '/workflow/leaveEdit/index',
},
];
/**

View File

@@ -118,6 +118,7 @@ export const useAuthStore = defineStore('auth', () => {
roles,
userId: user.userId,
username: user.userName,
email: user.email ?? '',
};
userStore.setUserInfo(userInfo);
/**

View File

@@ -0,0 +1,36 @@
import { onMounted } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import { Modal } from 'ant-design-vue';
export function useUploadTip() {
const readTip = useLocalStorage<boolean>('__upload_tip_read_5.4.0', false);
onMounted(() => {
if (readTip.value || !import.meta.env.DEV) {
return;
}
const modalInstance = Modal.info({
title: '提示',
centered: true,
content:
'如果你的版本是从低版本升级到后端>5.4.0, 记得执行升级sql, 否则跳转页面(如oss 代码生成配置)等会404',
okButtonProps: { disabled: true },
onOk() {
modalInstance.destroy();
readTip.value = true;
},
});
let time = 3;
const interval = setInterval(() => {
modalInstance.update({
okText: time === 0 ? '我知道了, 不再弹出' : `${time}秒后关闭`,
okButtonProps: { disabled: time > 0 },
});
if (time <= 0) {
clearInterval(interval);
}
time--;
}, 1000);
});
}

View File

@@ -1,3 +1,4 @@
import { UnauthorizedException } from '#/api/request';
import { dictDataInfo } from '#/api/system/dict/dict-data';
import { useDictStore } from '#/store/dict';
@@ -27,9 +28,16 @@ function fetchAndCacheDictData<T>(
// 内部处理了push的逻辑 这里不用push
setDictInfo(dictName, resp, formatNumber);
})
.catch(() => {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
.catch((error) => {
/**
* 需要判断是否为401抛出的特定异常 401清除缓存
* 其他error清除缓存会导致无限循环调用字典接口 则不做处理
*/
if (error instanceof UnauthorizedException) {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
}
// 其他不做处理
})
.finally(() => {
// 移除请求状态缓存

View File

@@ -30,7 +30,6 @@ export const columns: VxeGridProps['columns'] = [
field: 'deptName',
title: '部门名称',
treeNode: true,
width: 200,
},
{
field: 'deptCategory',
@@ -39,13 +38,9 @@ export const columns: VxeGridProps['columns'] = [
{
field: 'orderNum',
title: '排序',
resizable: false,
width: 'auto',
},
{
field: 'status',
resizable: false,
width: 'auto',
title: '状态',
slots: {
default: ({ row }) => {
@@ -62,7 +57,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 200,
resizable: false,
width: 'auto',
},
];
@@ -99,6 +95,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'orderNum',
label: '显示排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Input',

View File

@@ -0,0 +1,6 @@
<template>
<div>
ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件
无实际意义
</div>
</template>

View File

@@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'dictSort',
label: '显示排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Textarea',

View File

@@ -0,0 +1,281 @@
<!-- 使用vxe实现成本最小 且自带虚拟滚动 -->
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { DictType } from '#/api/system/dict/dict-type-model';
import { h, ref, shallowRef, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { cn } from '@vben/utils';
import {
DeleteOutlined,
EditOutlined,
ExportOutlined,
PlusOutlined,
SyncOutlined,
} from '@ant-design/icons-vue';
import {
Alert,
Input,
Modal,
Popconfirm,
Space,
Tooltip,
} from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
dictTypeExport,
dictTypeList,
dictTypeRemove,
refreshDictTypeCache,
} from '#/api/system/dict/dict-type';
import { commonDownloadExcel } from '#/utils/file/download';
import { emitter } from '../mitt';
import dictTypeModal from './dict-type-modal.vue';
const tableAllData = shallowRef<DictType[]>([]);
const gridOptions: VxeGridProps = {
columns: [
{
title: 'name',
field: 'render',
slots: { default: 'render' },
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async () => {
const resp = await dictTypeList();
total.value = resp.total;
tableAllData.value = resp.rows;
return resp;
},
},
},
rowConfig: {
keyField: 'dictId',
// 高亮当前行
isCurrent: true,
},
cellConfig: {
height: 60,
},
showHeader: false,
toolbarConfig: {
enabled: false,
},
// 开启虚拟滚动
scrollY: {
enabled: false,
gt: 0,
},
rowClassName: 'cursor-pointer',
id: 'system-dict-data-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
gridEvents: {
cellClick: ({ row }) => {
handleRowClick(row);
},
},
});
const [DictTypeModal, modalApi] = useVbenModal({
connectedComponent: dictTypeModal,
});
function handleAdd() {
modalApi.setData({});
modalApi.open();
}
async function handleEdit(record: DictType) {
modalApi.setData({ id: record.dictId });
modalApi.open();
}
async function handleDelete(row: DictType) {
await dictTypeRemove([row.dictId]);
await tableApi.query();
}
async function handleReset() {
currentRowId.value = '';
searchValue.value = '';
await tableApi.query();
}
function handleDownloadExcel() {
commonDownloadExcel(dictTypeExport, '字典类型数据');
}
function handleRefreshCache() {
Modal.confirm({
title: '提示',
content: '确认刷新字典类型缓存吗?',
okButtonProps: {
danger: true,
},
onOk: async () => {
await refreshDictTypeCache();
await tableApi.query();
},
});
}
const lastDictType = ref<string>('');
const currentRowId = ref<null | number | string>(null);
function handleRowClick(row: DictType) {
if (lastDictType.value === row.dictType) {
return;
}
currentRowId.value = row.dictId;
emitter.emit('rowClick', row.dictType);
}
const searchValue = ref('');
const total = ref(0);
watch(searchValue, (value) => {
if (!tableApi) {
return;
}
if (value) {
const names = tableAllData.value.filter((item) =>
item.dictName.includes(searchValue.value),
);
const types = tableAllData.value.filter((item) =>
item.dictType.includes(searchValue.value),
);
const filtered = [...new Set([...names, ...types])];
total.value = filtered.length;
tableApi.grid.loadData(filtered);
} else {
total.value = tableAllData.value.length;
tableApi.grid.loadData(tableAllData.value);
}
});
</script>
<template>
<div
:class="
cn(
'bg-background flex max-h-[100vh] w-[360px] flex-col overflow-y-hidden',
'rounded-lg',
'dict-type-card',
)
"
>
<div :class="cn('flex items-center justify-between', 'border-b px-4 py-2')">
<span class="font-semibold">字典项列表</span>
<Space>
<Tooltip title="刷新缓存">
<a-button
v-access:code="['system:dict:edit']"
:icon="h(SyncOutlined)"
@click="handleRefreshCache"
/>
</Tooltip>
<Tooltip :title="$t('pages.common.export')">
<a-button
v-access:code="['system:dict:export']"
:icon="h(ExportOutlined)"
@click="handleDownloadExcel"
/>
</Tooltip>
<Tooltip :title="$t('pages.common.add')">
<a-button
v-access:code="['system:dict:add']"
:icon="h(PlusOutlined)"
@click="handleAdd"
/>
</Tooltip>
</Space>
</div>
<div class="flex flex-1 flex-col overflow-y-hidden p-4">
<Alert
class="mb-4"
show-icon
message="如果你的数据量大 自行开启虚拟滚动"
/>
<Input
placeholder="搜索字典项名称/类型"
v-model:value="searchValue"
allow-clear
>
<template #addonAfter>
<Tooltip title="重置/刷新">
<SyncOutlined
v-access:code="['system:dict:edit']"
@click="handleReset"
/>
</Tooltip>
</template>
</Input>
<BasicTable class="flex-1 overflow-hidden">
<template #render="{ row: item }">
<div :class="cn('flex items-center justify-between px-2 py-2')">
<div class="flex flex-col items-baseline overflow-hidden">
<span class="font-medium">{{ item.dictName }}</span>
<div
class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ item.dictType }}
</div>
</div>
<div class="flex items-center gap-3 text-[17px]">
<EditOutlined
class="text-primary"
v-access:code="['system:dict:edit']"
@click.stop="handleEdit(item)"
/>
<Popconfirm
placement="left"
:title="`确认删除 [${item.dictName}]?`"
@confirm="handleDelete(item)"
>
<DeleteOutlined
v-access:code="['system:dict:remove']"
class="text-destructive"
@click.stop=""
/>
</Popconfirm>
</div>
</div>
</template>
</BasicTable>
</div>
<div class="border-t px-4 py-3"> {{ total }} 条数据</div>
<DictTypeModal @reload="tableApi.query()" />
</div>
</template>
<style lang="scss">
.dict-type-card {
.vxe-grid {
padding: 12px 0 0;
.vxe-body--row {
&.row--current {
// 选中行背景色
background-color: hsl(var(--accent-hover)) !important;
}
}
}
.ant-alert {
padding: 6px 12px;
}
}
</style>

View File

@@ -219,6 +219,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'orderNum',
help: '排序, 数字越小越靠前',
label: '显示排序',
defaultValue: 0,
rules: 'required',
},
{
@@ -237,6 +238,7 @@ export const drawerSchema: FormSchemaGetter = () => [
if (model.isFrame !== '0') {
return z
.string({ message: '请输入路由地址' })
.min(1, '请输入路由地址')
.refine((val) => !val.startsWith('/'), {
message: '路由地址不需要带/',
});

View File

@@ -4,16 +4,17 @@ import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Menu } from '#/api/system/menu/model';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useAccess } from '@vben/access';
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
import { eachTree, getVxePopupContainer } from '@vben/utils';
import { $t } from '@vben/locales';
import { eachTree, getVxePopupContainer, treeToList } from '@vben/utils';
import { Popconfirm, Space } from 'ant-design-vue';
import { Popconfirm, Space, Switch, Tooltip } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { menuList, menuRemove } from '#/api/system/menu';
import { menuCascadeRemove, menuList, menuRemove } from '#/api/system/menu';
import { columns, querySchema } from './data';
import menuDrawer from './menu-drawer.vue';
@@ -67,6 +68,8 @@ const gridOptions: VxeGridProps = {
rowField: 'menuId',
// 自动转换为tree 由vxe处理 无需手动转换
transform: true,
// 刷新接口后 记录展开行的情况
reserve: true,
},
id: 'system-menu-index',
};
@@ -111,11 +114,41 @@ async function handleEdit(record: Menu) {
drawerApi.open();
}
/**
* 是否级联删除
*/
const cascadingDeletion = ref(false);
async function handleDelete(row: Menu) {
await menuRemove([row.menuId]);
if (cascadingDeletion.value) {
// 级联删除
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
await menuCascadeRemove(menuAndChildren.map((item) => item.menuId));
} else {
// 单删除
await menuRemove([row.menuId]);
}
await tableApi.query();
}
function removeConfirmTitle(row: Menu) {
const menuName = $t(row.menuName);
if (!cascadingDeletion.value) {
return `是否确认删除 [${menuName}] ?`;
}
const menuAndChildren = treeToList([row], { id: 'menuId' });
if (menuAndChildren.length === 1) {
return `是否确认删除 [${menuName}] ?`;
}
return `是否确认删除 [${menuName}] 及 [${menuAndChildren.length - 1}]个子项目 ?`;
}
/**
* 编辑/添加成功后刷新表格
*/
async function afterEditOrAdd() {
tableApi.query();
}
/**
* 全部展开/折叠
* @param expand 是否展开
@@ -128,6 +161,9 @@ function setExpandOrCollapse(expand: boolean) {
/**
* 与后台逻辑相同
* 只有租户管理和超级管理能访问菜单管理
* 注意: 只有超管才能对菜单进行`增删改`操作
* 注意: 只有超管才能对菜单进行`增删改`操作
* 注意: 只有超管才能对菜单进行`增删改`操作
*/
const { hasAccessByRoles } = useAccess();
const isAdmin = computed(() => {
@@ -140,6 +176,16 @@ const isAdmin = computed(() => {
<BasicTable table-title="菜单列表" table-title-help="双击展开/收起子菜单">
<template #toolbar-tools>
<Space>
<Tooltip title="删除菜单以及子菜单">
<div
v-access:role="['superadmin']"
v-access:code="['system:menu:remove']"
class="flex items-center"
>
<span class="mr-2 text-sm text-[#666666]">级联删除</span>
<Switch v-model:checked="cascadingDeletion" />
</div>
</Tooltip>
<a-button @click="setExpandOrCollapse(false)">
{{ $t('pages.common.collapse') }}
</a-button>
@@ -149,6 +195,7 @@ const isAdmin = computed(() => {
<a-button
type="primary"
v-access:code="['system:menu:add']"
v-access:role="['superadmin']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
@@ -159,6 +206,7 @@ const isAdmin = computed(() => {
<Space>
<ghost-button
v-access:code="['system:menu:edit']"
v-access:role="['superadmin']"
@click="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
@@ -168,6 +216,7 @@ const isAdmin = computed(() => {
v-if="row.menuType !== 'F'"
class="btn-success"
v-access:code="['system:menu:add']"
v-access:role="['superadmin']"
@click="handleSubAdd(row)"
>
{{ $t('pages.common.add') }}
@@ -175,12 +224,13 @@ const isAdmin = computed(() => {
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
:title="removeConfirmTitle(row)"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['system:menu:remove']"
v-access:role="['superadmin']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
@@ -189,7 +239,7 @@ const isAdmin = computed(() => {
</Space>
</template>
</BasicTable>
<MenuDrawer @reload="tableApi.query()" />
<MenuDrawer @reload="afterEditOrAdd" />
</Page>
<Fallback v-else description="您没有菜单管理的访问权限" status="403" />
</template>

View File

@@ -28,7 +28,10 @@ export const querySchema: FormSchemaGetter = () => [
{
component: 'Select',
componentProps: {
options: getDictOptions(DictEnum.SYS_YES_NO),
options: [
{ label: '是', value: '0' },
{ label: '否', value: '1' },
],
},
fieldName: 'status',
label: '是否默认',

View File

@@ -131,6 +131,8 @@ const { hasAccessByCodes } = useAccess();
v-model:value="row.status"
:api="() => ossConfigChangeStatus(row)"
:disabled="!hasAccessByCodes(['system:ossConfig:edit'])"
checked-text=""
un-checked-text=""
@reload="tableApi.query()"
/>
</template>

View File

@@ -0,0 +1,11 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
未修改文件名 而是新加了这个文件
-->
<script setup lang="ts">
import OssConfigPage from '#/views/system/oss-config/index.vue';
</script>
<template>
<OssConfigPage />
</template>

View File

@@ -0,0 +1,2 @@
/** 支持的图片列表 */
export const supportImageList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

View File

@@ -73,9 +73,3 @@ export const columns: VxeGridProps['columns'] = [
width: 'auto',
},
];
/**
* 图片加载失败的fallback
*/
export const fallbackImageBase64 =
'data:image/png;base64,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';

View File

@@ -0,0 +1 @@
data:image/png;base64,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

View File

@@ -31,7 +31,11 @@ const [BasicModal, modalApi] = useVbenModal({
title="文件上传"
>
<div class="flex flex-col gap-4">
<FileUpload v-model:value="fileList" :enable-drag-upload="true" />
<FileUpload
v-model:value="fileList"
:enable-drag-upload="true"
:max-count="3"
/>
</div>
</BasicModal>
</template>

View File

@@ -31,7 +31,7 @@ const [BasicModal, modalApi] = useVbenModal({
title="图片上传"
>
<div class="flex flex-col gap-4">
<ImageUpload v-model:value="fileList" />
<ImageUpload v-model:value="fileList" :max-count="3" />
</div>
</BasicModal>
</template>

View File

@@ -33,7 +33,9 @@ import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
import { calculateFileSize } from '#/utils/file';
import { downloadByData } from '#/utils/file/download';
import { columns, fallbackImageBase64, querySchema } from './data';
import { supportImageList } from './constant';
import { columns, querySchema } from './data';
import fallbackImageBase64 from './fallback-image.txt?raw';
import fileUploadModal from './file-upload-modal.vue';
import imageUploadModal from './image-upload-modal.vue';
@@ -154,7 +156,7 @@ function handleMultiDelete() {
const router = useRouter();
function handleToSettings() {
router.push('/system/oss-config');
router.push('/system/oss-config/index');
}
const preview = ref(false);
@@ -163,10 +165,32 @@ onMounted(async () => {
preview.value = previewStr === 'true';
});
/**
* 根据拓展名判断是否是图片
* @param ext 拓展名
*/
function isImageFile(ext: string) {
const supportList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
return supportList.some((item) => ext.toLocaleLowerCase().includes(item));
return supportImageList.some((item) =>
ext.toLocaleLowerCase().includes(item),
);
}
/**
* 判断是否是pdf文件
* @param ext 扩展名
*/
function isPdfFile(ext: string) {
return ext.toLocaleLowerCase().includes('pdf');
}
/**
* pdf预览 使用浏览器接管
* @param url 文件地址
*/
function pdfPreview(url: string) {
window.open(url);
}
const [ImageUploadModal, imageUploadApi] = useVbenModal({
connectedComponent: imageUploadModal,
});
@@ -230,6 +254,12 @@ const [FileUploadModal, fileUploadApi] = useVbenModal({
</div>
</template>
</Image>
<!-- pdf预览 使用浏览器开新窗口 -->
<span
v-else-if="preview && isPdfFile(row.url)"
class="icon-[vscode-icons--file-type-pdf2] size-10 cursor-pointer"
@click.stop="pdfPreview(row.url)"
></span>
<span v-else>{{ row.url }}</span>
</template>
<template #action="{ row }">

View File

@@ -111,6 +111,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'postSort',
label: '岗位排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'RadioGroup',

View File

@@ -0,0 +1,11 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
未修改文件名 而是新加了这个文件
-->
<script setup lang="ts">
import RoleAssignPage from '#/views/system/role-assign/index.vue';
</script>
<template>
<RoleAssignPage />
</template>

View File

@@ -127,6 +127,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'roleSort',
label: '角色排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Select',

View File

@@ -11,14 +11,7 @@ import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import {
Dropdown,
Menu,
MenuItem,
Modal,
Popconfirm,
Space,
} from 'ant-design-vue';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
@@ -142,7 +135,7 @@ function handleAuthEdit(record: Role) {
const router = useRouter();
function handleAssignRole(record: Role) {
router.push(`/system/role-assign/${record.roleId}`);
router.push(`/system/role-auth/user/${record.roleId}`);
}
</script>
@@ -200,6 +193,18 @@ function handleAssignRole(record: Role) {
>
{{ $t('pages.common.edit') }}
</ghost-button>
<ghost-button
v-access:code="['system:role:edit']"
@click.stop="handleAuthEdit(row)"
>
权限
</ghost-button>
<ghost-button
v-access:code="['system:role:edit']"
@click.stop="handleAssignRole(row)"
>
分配
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
@@ -215,25 +220,6 @@ function handleAssignRole(record: Role) {
</ghost-button>
</Popconfirm>
</Space>
<Dropdown placement="bottomRight">
<template #overlay>
<Menu>
<MenuItem key="1" @click="handleAuthEdit(row)">
数据权限
</MenuItem>
<MenuItem key="2" @click="handleAssignRole(row)">
分配用户
</MenuItem>
</Menu>
</template>
<a-button
size="small"
type="link"
v-access:code="'system:role:edit'"
>
{{ $t('pages.common.more') }}
</a-button>
</Dropdown>
</template>
</template>
</BasicTable>

View File

@@ -10,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import { tenantPackageMenuTreeSelect } from '#/api/system/menu';
import {
packageAdd,
packageInfo,
@@ -40,30 +40,18 @@ const [BasicForm, formApi] = useVbenForm({
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await tenantPackageMenuTreeSelect(id);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
// 0为新增使用 获取除了`租户管理`的所有菜单
const resp = await tenantPackageMenuTreeSelect(id ?? 0);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
}
async function customFormValueGetter() {

View File

@@ -0,0 +1,6 @@
<template>
<div>
ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件
无实际意义
</div>
</template>

View File

@@ -80,6 +80,7 @@ onMounted(loadTree);
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"
size="small"
allow-clear
>
<template #enterButton>
<a-button @click="handleReload">
@@ -102,9 +103,9 @@ onMounted(loadTree);
@select="$emit('select')"
>
<template #title="{ label }">
<span v-if="label.indexOf(searchValue) > -1">
<span v-if="label.includes(searchValue)">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
<span class="text-primary">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,

View File

@@ -4,9 +4,10 @@ import type { Ref } from 'vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { GenInfo } from '#/api/tool/gen/model';
import { inject } from 'vue';
import { inject, onMounted, reactive } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
import { validRules, vxeTableColumns } from './gen-data';
@@ -15,8 +16,26 @@ import { validRules, vxeTableColumns } from './gen-data';
*/
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
const dictOptions = reactive<{ label: string; value: string }[]>([
{ label: '未设置', value: '' },
]);
/**
* 加载字典下拉数据
*/
onMounted(async () => {
const resp = await dictOptionSelectList();
const options = resp.map((dict) => ({
label: `${dict.dictName} | ${dict.dictType}`,
value: dict.dictType,
}));
dictOptions.push(...options);
});
const gridOptions: VxeGridProps = {
columns: vxeTableColumns,
columns: vxeTableColumns(dictOptions),
keepSource: true,
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
editRules: validRules,

View File

@@ -2,14 +2,10 @@ import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { reactive } from 'vue';
import { getPopupContainer } from '@vben/utils';
import { Checkbox, Input, Select } from 'ant-design-vue';
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
const JavaTypes: string[] = [
'Long',
'String',
@@ -45,24 +41,6 @@ const componentsOptions = [
{ label: '富文本', value: 'editor' },
];
const dictOptions = reactive<{ label: string; value: string }[]>([
{ label: '未设置', value: '' },
]);
/**
* 在这里初始化字典下拉框
*/
(async function init() {
const ret = await dictOptionSelectList();
ret.forEach((dict) => {
const option = {
label: `${dict.dictName} | ${dict.dictType}`,
value: dict.dictType,
};
dictOptions.push(option);
});
})();
function renderBooleanTag(row: Recordable<any>, field: string) {
const value = row[field] ? '是' : '否';
const className = row[field] ? 'text-green-500' : 'text-red-500';
@@ -78,7 +56,10 @@ export const validRules: VxeGridProps['editRules'] = {
javaField: [{ required: true, message: '请输入' }],
};
export const vxeTableColumns: VxeGridProps['columns'] = [
// 内部依赖的字典从外部通过函数传入
export const vxeTableColumns: (
dictOptions: { label: string; value: string }[],
) => VxeGridProps['columns'] = (dictOptions) => [
{
title: '序号',
type: 'seq',

View File

@@ -0,0 +1,10 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
-->
<script setup lang="ts">
import EditGenPage from './edit-gen.vue';
</script>
<template>
<EditGenPage />
</template>

View File

@@ -110,7 +110,7 @@ function handlePreview(record: Recordable<any>) {
const router = useRouter();
function handleEdit(record: Recordable<any>) {
router.push(`/code-gen/edit/${record.tableId}`);
router.push(`/tool/gen-edit/index/${record.tableId}`);
}
async function handleSync(record: Recordable<any>) {

View File

@@ -91,6 +91,7 @@ const gridOptions: VxeGridProps = {
toolbarConfig: {
enabled: false,
},
cellClassName: 'cursor-pointer',
};
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });

View File

@@ -42,6 +42,7 @@ import { renderDict } from '#/utils/render';
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
import ApprovalDetails from './approval-details.vue';
import FlowPreview from './flow-preview.vue';
import { approveWithReasonModal } from './helper';
import userSelectModal from './user-select-modal.vue';
@@ -442,10 +443,7 @@ async function handleCopy(text: string) {
/>
</TabPane>
<TabPane key="2" tab="审批流程图">
<img
:src="`data:image/png;base64,${currentFlowInfo.image}`"
class="rounded-lg border"
/>
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
</TabPane>
</Tabs>
</div>

View File

@@ -6,6 +6,7 @@ import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { useEventListener } from '@vueuse/core';
import { Alert } from 'ant-design-vue';
defineOptions({ name: 'FlowDesigner' });
@@ -48,5 +49,13 @@ useEventListener('message', messageHandler);
</script>
<template>
<iframe :src="url" class="size-full"></iframe>
<div class="size-full">
<Alert
class="mx-4 my-2"
type="warning"
:show-icon="true"
message="这是iframe页面! iframe页面! iframe页面! 不是我写的真服了"
/>
<iframe :src="url" class="size-full"></iframe>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { useAppConfig } from '@vben/hooks';
import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
defineOptions({ name: 'FlowPreview' });
const props = defineProps<{ instanceId: string }>();
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore();
const params = {
Authorization: `Bearer ${accessStore.accessToken}`,
id: props.instanceId,
clientid: clientId,
type: 'FlowChart',
};
/**
* iframe地址
*/
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
</script>
<template>
<iframe :src="url" class="h-[500px] w-full border"></iframe>
</template>

View File

@@ -0,0 +1,11 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
未修改文件名 而是新加了这个文件
-->
<script setup lang="ts">
import LeaveFormPage from './leave-form.vue';
</script>
<template>
<LeaveFormPage />
</template>

View File

@@ -73,6 +73,7 @@ onMounted(loadTree);
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"
size="small"
allow-clear
>
<template #enterButton>
<a-button @click="handleReload">
@@ -95,9 +96,9 @@ onMounted(loadTree);
@select="$emit('select')"
>
<template #title="{ label }">
<span v-if="label.indexOf(searchValue) > -1">
<span v-if="label.includes(searchValue)">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
<span class="text-primary">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,

View File

@@ -0,0 +1,11 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
未修改文件名 而是新加了这个文件
-->
<script setup lang="ts">
import FlowDesignerPage from '../components/flow-designer.vue';
</script>
<template>
<FlowDesignerPage />
</template>

View File

@@ -154,7 +154,7 @@ const router = useRouter();
*/
function handleDesign(row: any, disabled: boolean) {
router.push({
path: '/workflow/designer',
path: '/workflow/design/index',
query: { definitionId: row.id, disabled: String(disabled) },
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"scripts": {
"build": "vitepress build",

View File

@@ -26,6 +26,12 @@ outline: deep
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
## 自动显示 tooltip
通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
## API
### Props
@@ -37,6 +43,8 @@ outline: deep
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
| tooltip | 启用文本提示 | `boolean` | `true` |
| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` |
| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` |
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
| tooltipColor | 提示文本的颜色 | `string` | - |
| tooltipFontSize | 提示文本的大小 | `string` | - |

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
const text = `
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
`;
</script>
<template>
<EllipsisText :line="2" :tooltip-when-ellipsis="true">
{{ text }}
</EllipsisText>
<EllipsisText :line="3" :tooltip-when-ellipsis="true">
{{ text }}
</EllipsisText>
</template>

View File

@@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
console.log(import.meta.env.VITE_PROT);
```
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. :::
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
:::
@@ -138,6 +138,27 @@ To add a new dynamically modifiable configuration item, simply follow the steps
}
```
- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as:
```ts
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
return {
apiURL: VITE_GLOB_API_URL,
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
};
}
```
At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
```ts
@@ -186,6 +207,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@@ -200,6 +227,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -220,15 +248,18 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
enable: true,
fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
},
navigation: {
@@ -248,11 +279,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {
@@ -319,6 +353,18 @@ interface AppPreferences {
compact: boolean;
/** Whether to enable content compact mode */
contentCompact: ContentCompactType;
/** Content compact width */
contentCompactWidth: number;
/** Content padding */
contentPadding: number;
/** Content bottom padding */
contentPaddingBottom: number;
/** Content left padding */
contentPaddingLeft: number;
/** Content right padding */
contentPaddingRight: number;
/** Content top padding */
contentPaddingTop: number;
// /** Default application avatar */
defaultAvatar: string;
/** Default homepage path */
@@ -349,6 +395,8 @@ interface AppPreferences {
* @zh_CN Whether to enable watermark
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
/** Whether breadcrumbs are enabled */
@@ -385,11 +433,15 @@ interface FooterPreferences {
enable: boolean;
/** Whether the footer is fixed */
fixed: boolean;
/** Footer height */
height: number;
}
interface HeaderPreferences {
/** Whether the header is enabled */
enable: boolean;
/** Header height */
height: number;
/** Whether the header is hidden, css-hidden */
hidden: boolean;
/** Header menu alignment */
@@ -401,6 +453,8 @@ interface HeaderPreferences {
interface LogoPreferences {
/** Whether the logo is visible */
enable: boolean;
/** Logo image fitting method */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** Logo URL */
source: string;
}
@@ -422,16 +476,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** Whether to show title when sidebar is collapsed */
collapsedShowTitle: boolean;
/** Sidebar collapse width */
collapseWidth: number;
/** Whether the sidebar is visible */
enable: boolean;
/** Menu auto-expand state */
expandOnHover: boolean;
/** Whether the sidebar extension area is collapsed */
extraCollapse: boolean;
/** Sidebar extension area collapse width */
extraCollapsedWidth: number;
/** Whether the sidebar fixed button is visible */
fixedButton: boolean;
/** Whether the sidebar is hidden - css */
hidden: boolean;
/** Mixed sidebar width */
mixedWidth: number;
/** Sidebar width */
width: number;
}

View File

@@ -4,10 +4,11 @@ outline: deep
# Access Control
The framework has built-in two types of access control methods:
The framework has built-in three types of access control methods:
- Determining whether a menu or button can be accessed based on user roles
- Determining whether a menu or button can be accessed through an API
- Mixed mode: Using both frontend and backend access control simultaneously
## Frontend Access Control
@@ -151,6 +152,43 @@ const dashboardMenus = [
At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible.
## Mixed Access Control
**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution.
**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management.
### Steps
- Ensure the current mode is set to mixed access control
Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`.
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
accessMode: 'mixed',
},
});
```
- Configure frontend route permissions
Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode.
- Configure backend menu interface
Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode.
- Ensure roles and permissions match
Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes.
At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality.
## Fine-grained Control of Buttons
In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles.

View File

@@ -214,7 +214,7 @@ server {
使用 nginx 处理项目部署后的跨域问题
1. 配置前端项目接口地址,在项目目录下的``.env.production`文件中配置:
1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置:
```bash
VITE_GLOB_API_URL=/api

View File

@@ -339,6 +339,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key默认true
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false
@@ -502,6 +506,13 @@ interface RouteMeta {
用于配置页面的徽标颜色。
### fullPathKey
- 类型:`boolean`
- 默认值:`true`
是否将路由的完整路径作为tab key默认true
### activePath
- 类型:`string`
@@ -602,3 +613,32 @@ const { refresh } = useRefresh();
refresh();
</script>
```
## 标签页与路由控制
在某些场景下需要单个路由打开多个标签页或者修改路由的query不打开新的标签页
每个标签页Tab使用唯一的key标识设置Tab key有三种方式优先级由高到低
- 使用路由query参数pageKey
```vue
<script setup lang="ts">
import { useRouter } from 'vue-router';
// 跳转路由
const router = useRouter();
router.push({
path: 'path',
query: {
pageKey: 'key',
},
});
```
- 路由的完整路径作为key
`meta` 属性中的 `fullPathKey`不为false则使用路由`fullPath`作为key
- 路由的path作为key
`meta` 属性中的 `fullPathKey`为false则使用路由`path`作为key

View File

@@ -21,7 +21,7 @@
console.log(import.meta.env.VITE_PROT);
```
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. :::
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
:::
@@ -137,6 +137,27 @@ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
}
```
- 在 `packages/effects/hooks/src/use-app-config.ts` 中,新增对应的配置项,如:
```ts
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
return {
apiURL: VITE_GLOB_API_URL,
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
};
}
```
到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。
```ts
@@ -185,6 +206,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@@ -199,6 +226,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -219,15 +247,18 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
enable: true,
fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
},
navigation: {
@@ -247,11 +278,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {
@@ -318,6 +352,18 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@@ -348,6 +394,8 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@@ -385,11 +433,15 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@@ -401,6 +453,8 @@ interface HeaderPreferences {
interface LogoPreferences {
/** logo是否可见 */
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */
source: string;
}
@@ -423,16 +477,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}

View File

@@ -4,10 +4,11 @@ outline: deep
# 权限
框架内置了种权限控制方式:
框架内置了种权限控制方式:
- 通过用户角色来判断菜单或者按钮是否可以访问
- 通过接口来判断菜单或者按钮是否可以访问
- 混合模式:同时使用前端和后端权限控制
## 前端访问控制
@@ -159,6 +160,43 @@ const dashboardMenus = [
到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。
## 混合访问控制
**实现原理**: 混合模式同时结合了前端访问控制和后端访问控制两种方式。系统会并行处理前端固定路由权限和后端动态菜单数据,最终将两部分路由合并,提供更灵活的权限控制方案。
**优点**: 兼具前端控制的性能优势和后端控制的灵活性,适合复杂业务场景下的权限管理。
### 步骤
- 确保当前模式为混合访问控制模式
调整对应应用目录下的`preferences.ts`,确保`accessMode='mixed'`。
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
accessMode: 'mixed',
},
});
```
- 配置前端路由权限
同[前端访问控制](#前端访问控制)模式的路由权限配置方式。
- 配置后端菜单接口
同[后端访问控制](#后端访问控制)模式的接口配置方式。
- 确保角色和权限匹配
需要同时满足前端路由权限配置和后端菜单数据返回的要求,确保用户角色与两种模式的权限配置都匹配。
到这里,就已经配置完成,混合模式会自动合并前端和后端的路由,提供完整的权限控制功能。
## 按钮细粒度控制
在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -12,7 +12,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild"
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"keywords": [
"monorepo",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -60,8 +60,9 @@ type BreadcrumbStyleType = 'background' | 'normal';
* 权限模式
* backend 后端权限模式
* frontend 前端权限模式
* mixed 混合权限模式
*/
type AccessModeType = 'backend' | 'frontend';
type AccessModeType = 'backend' | 'frontend' | 'mixed';
/**
* 导航风格

View File

@@ -12,6 +12,10 @@ interface BasicUserInfo {
* 头像
*/
avatar: string;
/**
* 邮箱
*/
email: string;
/**
* 用户权限
*/

View File

@@ -1,3 +1,8 @@
import type { RouteLocationNormalized } from 'vue-router';
export type TabDefinition = RouteLocationNormalized;
export interface TabDefinition extends RouteLocationNormalized {
/**
* 标签页的key
*/
key?: string;
}

View File

@@ -43,6 +43,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key默认true
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -10,6 +10,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"colorWeakMode": false,
"compact": false,
"contentCompact": "wide",
"contentCompactWidth": 1200,
"contentPadding": 0,
"contentPaddingBottom": 0,
"contentPaddingLeft": 0,
"contentPaddingRight": 0,
"contentPaddingTop": 0,
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
"defaultHomePath": "/analytics",
"dynamicTitle": true,
@@ -23,6 +29,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"name": "Vben Admin",
"preferencesButtonPosition": "auto",
"watermark": false,
"zIndex": 200,
},
"breadcrumb": {
"enable": true,
@@ -43,15 +50,18 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"footer": {
"enable": false,
"fixed": false,
"height": 32,
},
"header": {
"enable": true,
"height": 50,
"hidden": false,
"menuAlign": "start",
"mode": "fixed",
},
"logo": {
"enable": true,
"fit": "contain",
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
},
"navigation": {
@@ -68,14 +78,17 @@ exports[`defaultPreferences immutability test > should not modify the config obj
},
"sidebar": {
"autoActivateChild": false,
"collapseWidth": 60,
"collapsed": false,
"collapsedButton": true,
"collapsedShowTitle": false,
"enable": true,
"expandOnHover": true,
"extraCollapse": false,
"extraCollapsedWidth": 60,
"fixedButton": true,
"hidden": false,
"mixedWidth": 80,
"width": 224,
},
"tabbar": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -9,6 +9,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@@ -23,6 +29,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -43,15 +50,19 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
enable: true,
fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
},
navigation: {
@@ -71,11 +82,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {

View File

@@ -33,6 +33,18 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@@ -63,6 +75,8 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@@ -100,11 +114,15 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@@ -116,6 +134,8 @@ interface HeaderPreferences {
interface LogoPreferences {
/** logo是否可见 */
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */
source: string;
}
@@ -138,16 +158,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings';
import type { FormActions, FormSchema, VbenFormProps } from './types';
import { toRaw } from 'vue';
import { isRef, toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import {
@@ -100,9 +100,26 @@ export class FormApi {
getFieldComponentRef<T = ComponentPublicInstance>(
fieldName: string,
): T | undefined {
return this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as T)
let target = this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)
: undefined;
if (
target &&
target.$.type.name === 'AsyncComponentWrapper' &&
target.$.subTree.ref
) {
if (Array.isArray(target.$.subTree.ref)) {
if (
target.$.subTree.ref.length > 0 &&
isRef(target.$.subTree.ref[0]?.r)
) {
target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance;
}
} else if (isRef(target.$.subTree.ref.r)) {
target = target.$.subTree.ref.r.value as ComponentPublicInstance;
}
}
return target as T;
}
/**

View File

@@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui';
import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
import { useForm } from 'vee-validate';
import { object } from 'zod';
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
import { getDefaultsForSchema } from 'zod-defaults';
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
@@ -52,7 +52,12 @@ export function useFormInitial(
if (Reflect.has(item, 'defaultValue')) {
set(initialValues, item.fieldName, item.defaultValue);
} else if (item.rules && !isString(item.rules)) {
// 检查规则是否适合提取默认值
const customDefaultValue = getCustomDefaultValue(item.rules);
zodObject[item.fieldName] = item.rules;
if (customDefaultValue !== undefined) {
initialValues[item.fieldName] = customDefaultValue;
}
}
});
@@ -64,6 +69,38 @@ export function useFormInitial(
}
return mergeWithArrayOverride(initialValues, zodDefaults);
}
// 自定义默认值提取逻辑
function getCustomDefaultValue(rule: any): any {
if (rule instanceof ZodString) {
return ''; // 默认为空字符串
} else if (rule instanceof ZodNumber) {
return null; // 默认为 null避免显示 0
} else if (rule instanceof ZodObject) {
// 递归提取嵌套对象的默认值
const defaultValues: Record<string, any> = {};
for (const [key, valueSchema] of Object.entries(rule.shape)) {
defaultValues[key] = getCustomDefaultValue(valueSchema);
}
return defaultValues;
} else if (rule instanceof ZodIntersection) {
// 对于交集类型从schema 提取默认值
const leftDefaultValue = getCustomDefaultValue(rule._def.left);
const rightDefaultValue = getCustomDefaultValue(rule._def.right);
// 如果左右两边都能提取默认值,合并它们
if (
typeof leftDefaultValue === 'object' &&
typeof rightDefaultValue === 'object'
) {
return { ...leftDefaultValue, ...rightDefaultValue };
}
// 否则优先使用左边的默认值
return leftDefaultValue ?? rightDefaultValue;
} else {
return undefined; // 其他类型不提供默认值
}
}
return {
delegatedSlots,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -34,7 +34,6 @@ const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
buttonAlign: 'end',
centered: true,
containerClass: 'w-[520px]',
});
const emits = defineEmits(['closed', 'confirm', 'opened']);
const open = defineModel<boolean>('open', { default: false });
@@ -148,7 +147,7 @@ async function handleOpenChange(val: boolean) {
:class="
cn(
containerClass,
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,

View File

@@ -1,7 +1,15 @@
<script lang="ts" setup>
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
import { computed, provide, ref, unref, useId, watch } from 'vue';
import {
computed,
onDeactivated,
provide,
ref,
unref,
useId,
watch,
} from 'vue';
import {
useIsMobile,
@@ -94,6 +102,16 @@ const {
// },
// );
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
if (!appendToMain.value) {
props.drawerApi?.close();
}
});
function interactOutside(e: Event) {
if (!closeOnClickModal.value || submitting.value) {
e.preventDefault();

View File

@@ -9,7 +9,6 @@ import {
h,
inject,
nextTick,
onDeactivated,
provide,
reactive,
ref,
@@ -72,13 +71,6 @@ export function useVbenDrawer<
},
);
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
(extendedApi as ExtendedDrawerApi)?.close?.();
});
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
}

View File

@@ -1,7 +1,16 @@
<script lang="ts" setup>
import type { ExtendedModalApi, ModalProps } from './modal';
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
import {
computed,
nextTick,
onDeactivated,
provide,
ref,
unref,
useId,
watch,
} from 'vue';
import {
useIsMobile,
@@ -135,6 +144,16 @@ watch(
// },
// );
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
if (!appendToMain.value) {
props.modalApi?.close();
}
});
function handleFullscreen() {
props.modalApi?.setState((prev) => {
// if (prev.fullscreen) {

View File

@@ -5,7 +5,6 @@ import {
h,
inject,
nextTick,
onDeactivated,
provide,
reactive,
ref,
@@ -71,13 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
},
);
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
(extendedApi as ExtendedModalApi)?.close?.();
});
return [Modal, extendedApi as ExtendedModalApi] as const;
}
@@ -94,8 +86,9 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
injectData.options?.onOpenChange?.(isOpen);
};
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
options.onClosed?.();
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
@@ -129,6 +122,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
},
);
injectData.extendApi?.(extendedApi);
return [Modal, extendedApi] as const;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.6",
"version": "5.5.7",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

View File

@@ -5,6 +5,8 @@ import type {
AvatarRootProps,
} from 'radix-vue';
import type { CSSProperties } from 'vue';
import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
@@ -16,6 +18,7 @@ interface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps {
class?: ClassType;
dot?: boolean;
dotClass?: ClassType;
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
size?: number;
}
@@ -28,6 +31,15 @@ const props = withDefaults(defineProps<Props>(), {
as: 'button',
dot: false,
dotClass: 'bg-green-500',
fit: 'cover',
});
const imageStyle = computed<CSSProperties>(() => {
const { fit } = props;
if (fit) {
return { objectFit: fit };
}
return {};
});
const text = computed(() => {
@@ -51,7 +63,7 @@ const rootStyle = computed(() => {
class="relative flex flex-shrink-0 items-center"
>
<Avatar :class="props.class" class="size-full">
<AvatarImage :alt="alt" :src="src" />
<AvatarImage :alt="alt" :src="src" :style="imageStyle" />
<AvatarFallback>{{ text }}</AvatarFallback>
</Avatar>
<span

View File

@@ -29,14 +29,25 @@ export type ValueType = boolean | number | string;
export interface VbenButtonGroupProps
extends Pick<VbenButtonProps, 'disabled'> {
/** 单选模式下允许清除选中 */
allowClear?: boolean;
/** 值改变前的回调 */
beforeChange?: (
value: ValueType,
isChecked: boolean,
) => boolean | PromiseLike<boolean | undefined> | undefined;
/** 按钮样式 */
btnClass?: any;
/** 按钮间隔距离 */
gap?: number;
/** 多选模式下限制最多选择的数量。0表示不限制 */
maxCount?: number;
/** 是否允许多选 */
multiple?: boolean;
options?: { label: CustomRenderType; value: ValueType }[];
/** 选项 */
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
/** 显示图标 */
showIcon?: boolean;
/** 尺寸 */
size?: 'large' | 'middle' | 'small';
}

View File

@@ -19,6 +19,8 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
multiple: false,
showIcon: true,
size: 'middle',
allowClear: false,
maxCount: 0,
});
const emit = defineEmits(['btnClick']);
const btnDefaultProps = computed(() => {
@@ -82,12 +84,22 @@ async function onBtnClick(value: ValueType) {
if (innerValue.value.includes(value)) {
innerValue.value = innerValue.value.filter((item) => item !== value);
} else {
if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {
innerValue.value = innerValue.value.slice(0, props.maxCount - 1);
}
innerValue.value.push(value);
}
modelValue.value = innerValue.value;
} else {
innerValue.value = [value];
modelValue.value = value;
if (props.allowClear && innerValue.value.includes(value)) {
innerValue.value = [];
modelValue.value = undefined;
emit('btnClick', undefined);
return;
} else {
innerValue.value = [value];
modelValue.value = value;
}
}
emit('btnClick', value);
}
@@ -110,16 +122,23 @@ async function onBtnClick(value: ValueType) {
v-bind="btnDefaultProps"
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
@click="onBtnClick(btn.value)"
type="button"
>
<div class="icon-wrapper" v-if="props.showIcon">
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
<slot
name="icon"
:loading="loadingValues.includes(btn.value)"
:checked="innerValue.includes(btn.value)"
>
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</slot>
</div>
<slot name="option" :label="btn.label" :value="btn.value">
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
<VbenRenderContent :content="btn.label" />
</slot>
</Button>
@@ -127,6 +146,9 @@ async function onBtnClick(value: ValueType) {
</template>
<style lang="scss" scoped>
.vben-check-button-group {
display: flex;
flex-wrap: wrap;
&:deep(.size-large) button {
.icon-wrapper {
margin-right: 0.3rem;
@@ -159,5 +181,16 @@ async function onBtnClick(value: ValueType) {
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-right-width: 0;
}
&.no-gap {
:deep(button + button) {
margin-right: -1px;
border-left-width: 1px;
}
}
}
</style>

View File

@@ -6,6 +6,10 @@ interface Props {
* @zh_CN 是否收起文本
*/
collapsed?: boolean;
/**
* @zh_CN Logo 图片适应方式
*/
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/**
* @zh_CN Logo 跳转地址
*/
@@ -38,6 +42,7 @@ withDefaults(defineProps<Props>(), {
logoSize: 32,
src: '',
theme: 'light',
fit: 'cover',
});
</script>
@@ -53,6 +58,7 @@ withDefaults(defineProps<Props>(), {
:alt="text"
:src="src"
:size="logoSize"
:fit="fit"
class="relative rounded-none bg-transparent"
/>
<template v-if="!collapsed">

View File

@@ -80,7 +80,7 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{

View File

@@ -224,15 +224,20 @@ defineExpose({
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': disabled,
})
"
v-bind="
Object.assign(item.bind, {
onfocus: disabled ? 'this.blur()' : undefined,
})
"
v-bind="item.bind"
@select="
(event) => {
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
onSelect(item, event.detail.isSelected);
!disabled && onSelect(item, event.detail.isSelected);
}
"
@toggle="
@@ -240,7 +245,7 @@ defineExpose({
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
onToggle(item);
!disabled && onToggle(item);
}
"
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
@@ -262,10 +267,11 @@ defineExpose({
<Checkbox
v-if="multiple"
:checked="isSelected"
:disabled="disabled"
:indeterminate="isIndeterminate"
@click="
() => {
handleSelect();
!disabled && handleSelect();
// onSelect(item, !isSelected);
}
"
@@ -276,7 +282,7 @@ defineExpose({
(_event) => {
// $event.stopPropagation();
// $event.preventDefault();
handleSelect();
!disabled && handleSelect();
// onSelect(item, !isSelected);
}
"

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