Compare commits

...

109 Commits
v5.5.6 ... main

Author SHA1 Message Date
LinaBell
cf6c4c9aae fix: cannot read properties of null (reading 'nextSibling') (#6667) 2025-08-21 22:26:10 +08:00
Ken Hai
ffaf85c8f3 fix: 修复角色修改时VbenTree组件没有回显选中 (#6662)
* fix: 修复角色修改时VbenTree组件没有回显选中

* chore: use nextTick

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

* fix: merge

* chore: 更新

---------

Co-authored-by: haiyinlong <haiyinlong@uhigame.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-08-21 15:30:58 +08:00
panda7
2cc78f925f fix: the bug in the lock method of the vbenModal component (#6648) 2025-08-21 15:17:55 +08:00
ming4762
93f0eea4e7 fix: fix the issue of excessive line spacing in vbenForm (#6653)
* gap-2和 pb-4/2 重叠导致间距过宽,gap-x只保留列间距
2025-08-21 12:41:04 +08:00
谦元吉
58e3941810 chore(docs): update the component import of the form adapter description in the document (#6656) 2025-08-19 16:48:10 +08:00
Svend
3ad433a50b fix: 修复在 hash 路由模式下无法在新窗口打开路由的问题 (#6652)
此问题是由于 PR #6583 中新增的 `resolveHref` 函数导致的。其在 hash 路由模式下,得到的 URL 会包含 #/ 前缀。在经过 openRouteInNewWindow 的逻辑后就会出现两次 /# 前缀
2025-08-19 16:47:45 +08:00
ming4762
8ac2db5b7c fix: fix the issue of VbenForm compact reactive failure (#6654) 2025-08-19 16:46:14 +08:00
Elm1992
a441dcebae fix: meta.link invalid issue 2025-08-19 16:40:16 +08:00
Vben
ff4704d5ea chore: Upgrade vite to version 7.x (#6645) 2025-08-16 22:50:31 +08:00
菠萝吹雪
6ddfbd84b0 chore: modify the contributor showcase in the README (#6636) 2025-08-16 22:47:08 +08:00
ming4762
1e6417f95b feat: vBenForm add layout: inline (#6644) 2025-08-16 22:41:08 +08:00
vben
e147a9d2fd chore: release 5.5.9 2025-08-16 22:16:02 +08:00
谦元吉
4efebb8c0b fix:update (#6635) 2025-08-14 22:01:12 +08:00
gxc685
9ce0df88ae fix: 修复mock里面eventHandler重复导致无法启动 (#6631) 2025-08-14 22:00:54 +08:00
谦元吉
3cf0c0eb04 fix(@vben/backend-mock): go back to the last modification (#6634)
* fix(@vben/backend-mock): the version went back to the last submission, and the latest submission was completely useless.

* fix: resolve conflicts
2025-08-14 12:06:41 +08:00
谦元吉
ab7e363279 fix(@vben/backend-mock): fix all ts type errors in this module (#6613)
* fix(@vben/backend-mock): 修复所有 ts 类型报错

* fix(@vben/backend-mock): 修复该模块所有 ts 类型报错

* fix(@vben/backend-mock): 解决 coderabbitai

* fix(@vben/backend-mock): 解决 coderabbitai

* fix(@vben/backend-mock): 解决 coderabbitai
2025-08-12 17:23:39 +08:00
xueyang
9fc594434f perf: 优化useVbenForm样式 (#6611)
* perf(style): 优化useVbenForm垂直布局 actions 样式

* perf(style): 优化useVbenForm actions 布局样式

- 操作按钮组显示位置
```
actionPosition?: 'center' | 'left' | 'right';
```
- 操作按钮组的样式
```
actionType?: 'block' | 'inline'
inline: 行类显示,block: 新一行单独显示
```

* perf: 优化useVbenForm actions 布局样式

删除 actionType
增加 actionLayout
- actionLayout?: 'inline' | 'newLine' | 'rowEnd';
- newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
- 删除无用代码 queryFormStyle

* perf: 优化useVbenForm使用案例

* perf: 优化form组件样式

去掉padding,改为gap

* docs: update vben-form.md

* fix: 修复FormMessage位置

* perf: Avoid direct mutation of props object.

-  props.actionLayout = props.actionLayout || 'rowEnd';
-  props.actionPosition = props.actionPosition || 'right';
+  const actionLayout = props.actionLayout || 'rowEnd';
+  const actionPosition = props.actionPosition || 'right';

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

* fix: 修复 wrapperClass 权重

* fix: 全局搜索结果不匹配 #6603

* fix: 避免FormMessage溢出

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-07 23:48:34 +08:00
leo
b93e22c45a fix(@vben/layouts): respect base URL when opening route in new window (#6583)
Previously, the generated URL for opening routes in a new window did not include the router base,
which led to incorrect paths when the app was deployed under a subdirectory (e.g., /admin/).
This change ensures that the resolved path includes the configured base by using
router.resolve(path).href.
2025-07-29 13:46:05 +08:00
Jin Mao
193f5b6512 Merge branch 'main' into 2025072604 2025-07-28 15:53:04 +08:00
Jin Mao
cb3f96683f fix: 修复双列布局模式下,路由为hideInMenu时,空白右列 2025-07-28 15:50:21 +08:00
zhongming4762
06ffdf164a feat: add dingding login 2025-07-25 22:02:55 +08:00
ming4762
5b75e5e917 perf: perf the control logic of VbenModal full screen and header (#6566)
* resolve the issue of header=false and full screen button display but not operable
2025-07-25 21:45:45 +08:00
aonoa
fad0b49841 fix: adding roles does not automatically refresh (#6548)
* fix: adding roles does not automatically refresh

* style: fix code style err
2025-07-25 21:35:57 +08:00
Jin Mao
260e45cd7b Merge branch 'main' into feat/add-vben-modal-animation 2025-07-25 21:33:11 +08:00
vben
1575619d53 chore: release v5.5.8 2025-07-19 22:19:50 +08:00
Jin Mao
d5a36a167d fix: fix vxeTable commit proxy (#6536)
* fix: 修正use-vxe-grid中的代理配置提交类型

* chore: change config
2025-07-19 16:07:15 +08:00
panda7
fc9ea347ca Merge branch 'main' into feat/add-vben-modal-animation 2025-07-18 00:38:54 +08:00
 panda7
1a9b0509d5 feat: add animation effects to VbenModal component 2025-07-18 00:15:40 +08:00
panda7
07b64ad384 feat: add function support for formItemClass prop (#6511)
* feat: add function support for formItemClass prop

* feat: add try-catch to formItemClass function

* fix: formItemClass function ts error

---------

Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-07-17 09:37:39 +08:00
Jin Mao
1bc5d2986b chore: update-vxe-table (#6516)
* chore: update vxe-pc-ui,vxe-table

* fix(ui): 修复代理配置初始化方法名错误

* fix(ui): 修改远程表格刷新配置

* chroe: update vxeTable

更新到最新
2025-07-16 19:15:39 +08:00
HamWong
bb36cca315 fix: 锁定屏幕页面样式自适应 (#6480) 2025-07-15 09:08:08 +08:00
Jin Mao
b8bf482c6a Merge branch 'main' into form 2025-07-07 09:16:54 +08:00
Jin Mao
3b673ca915 Merge branch 'main' into feature/scroll_to_the_error_field 2025-07-07 08:21:50 +08:00
Jin Mao
bbf0287511 chore: fix lint warning (#6487) 2025-07-07 08:21:25 +08:00
panda7
d4786f3f75 Merge branch 'main' into feature/scroll_to_the_error_field 2025-07-06 21:19:52 +08:00
xue-jn
b333fd676d docs: update vben-drawer.md (#6478)
* docs: update vben-drawer.md

* docs: update vben-drawer.md

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-07-06 20:26:06 +08:00
sqchen
f1051c8773 feat: add scrollToFirstError to the form component 2025-07-05 00:50:53 +08:00
sqchen
243f3a201d feat: add scrollToFirstError to the form component 2025-07-05 00:19:12 +08:00
陈蔚然
2f7de243f6 Merge branch 'vbenjs:main' into slider-translate-captcha 2025-07-04 10:28:40 +08:00
chenweiran
1aafb43103 feat: 增加基于图片拼图切片平移的验证码 2025-07-04 10:26:02 +08:00
chenweiran
8ccd01ade5 feat: 增加基于图片拼图切片平移的验证码 2025-07-04 10:23:47 +08:00
Netfan
e6bfbce6cb revert: page height fixed,revert #6422 2025-07-03 22:29:48 +08:00
someone-cool
1f63aed64c fix(@vben/common-ui): add page footer's height to calc contentstyle (#6422) 2025-07-03 18:36:42 +08:00
xue-jn
253b0da7d2 fix: 接口返回子节点为空数组时,还会显示折叠箭头 (#6463) 2025-07-03 18:35:03 +08:00
RanMaoting
33a4d524db feat(@vben/plugins): 新增VxeGrid组件插槽类型定义 (#6452)
* feat(@vben/plugins): 新增VxeGrid组件插槽类型定义

Closes: #6451

* fix(@vben/plugins): 优化vxe-table组件的插槽类型定义

修复Omit导致的类型丢失
2025-07-03 18:34:37 +08:00
chenweiran
bbd8a53d9d feat: 增加基于图片拼图切片平移的验证码 2025-07-03 18:20:20 +08:00
chenweiran
8554924cb9 feat: 增加基于图片拼图切片平移的验证码 2025-07-03 17:43:19 +08:00
RanMaoting
fee811d950 fix: 优化组件方法透传并新增表单弹窗示例 (#6443) 2025-07-02 19:58:48 +08:00
Netfan
78076e70b4 chore: update deps 2025-07-02 16:55:55 +08:00
chewenye
b78bc65ce7 feat: 组件json-viewer支持bigint数据显示 cwy (#6377)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-29 04:32:30 +08:00
Utopia
b1fb623113 feat: 为 auth layout 添加 slot: logo, 提升组件的灵活性和可复用性 (#6442) 2025-06-27 19:23:24 +08:00
Stephen Chang
de14908fd3 fix(icon-picker): 解决icon-picker组件切换分页后,关键词检索失效问题 (#6437)
当icon-picker组件切换分页后,在输入关键词检索,但是分页没有重置,导致检索结果异常
2025-06-27 19:21:23 +08:00
Li Kui
5c3972196a fix: Add $t import to login expired modal (#6429)
closes #6230
2025-06-27 19:20:25 +08:00
CG.gatspy
3230781538 feat: [vben-tree]增加数据disabled (#6343)
* feat: [vben-tree]增加数据disabled

* Update packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

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-06-27 19:09:30 +08:00
broBinChen
e7fd0e3b6a feat(hooks): useHoverToggle的入参refElement支持传入响应式数组 (#6333)
* feat(hooks): useHoverToggle的入参refElement支持传入响应式数组

* feat(hooks): 1、增加 useHoverToggle 中 refElement 参数关于传入响应式数组的注释说明。 2、修改 watch 监听深度,仅需浅层监听 refs 变化。 3、使用 effectScope 管理 useElementHover 实例,避免 refs 变化时事件监听器累积导致的内存泄漏问题

* feat(hooks): 在useHoverToggle中增强 updateHovers  的边界处理,优化watch方案,只监听元素数量变化而不是整个数组变化,避免过度依赖收集

---------

Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-27 19:08:41 +08:00
yuhengshen
2f7d1f009d fix: 全屏状态下弹窗圆角优化 (#6413) 2025-06-24 23:41:54 +08:00
yuhengshen
946f91f387 feat: optimize modal dragging range(#6414)
* 当弹窗指定了容器时,拖拽将被限制在容器范围内
2025-06-24 17:05:59 +08:00
lghuahua
986eacae9a fix: improve request logic in api-component
* 修复某些情况下updateParam设置的参数可能不会提交给api的问题
* 修复在上一个请求尚未完成前如果params发生了变更,将不会再触发新的api请求
---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-06-20 08:42:45 +08:00
Netfan
97b8e28a2b docs: fix delete request usage (#6389) 2025-06-17 08:52:59 +08:00
Netfan
c0962fec18 fix: auto close popup on deactivated (#6368)
* 修复挂载到内容区域的弹窗和抽屉被意外关闭的问题
2025-06-11 12:20:52 +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
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
Netfan
081d08a7f8 fix: alert width fixed in small screen (#6312) 2025-05-30 19:54:26 +08:00
Netfan
96a10ca83f style: fix lint error (#6298) 2025-05-28 19:23:21 +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
liqiang0330
53304514b6 fix: Update index.ts (#6268)
* Update index.ts

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

* Update index.ts

增加空格!
2025-05-26 13:29:27 +08:00
Netfan
6fbf1387f5 fix: reset slider-captcha after login failed (#6275) 2025-05-25 16:04:56 +08:00
Netfan
e5c937396d fix: json-bigint parse used in vxeTable (#6271)
* 修复vxeTable不能加载json-bigint解析的数据的问题
2025-05-24 13:01:58 +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
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
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
zwtvip
f7bae8ac0f chore: export framework components for use in independent pages 2025-05-21 00:37:26 +08:00
wyc001122
8ac97688da fix(preferences): 更新内容内边距默认值 (#6233)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-20 09:50:23 +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
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
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
Netfan
e89cf400c0 fix: refresh command of tabbar issue, fixed: #6162 (#6169) 2025-05-12 23:34:08 +08:00
anyup
9e67929ee7 feat: support to refresh the tab page by route name (#6153)
Co-authored-by: anyup <anyupxing@163.com>
2025-05-10 22:33:31 +08:00
afe1
90625782c0 fix: delete useless code (#6143) 2025-05-08 16:51:12 +08:00
wyc001122
84ef207d9c docs(@vben/docs): update settings doc (#6128)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-07 12:04:48 +08:00
zyf0624
e68fff58e8 fix: tsconfig moduleResolution (#6122)
Co-authored-by: pzzyf <2279948211@qq.com>
2025-05-07 12:04:15 +08:00
Netfan
bf70539221 fix: missing argument for getPopupContainer 2025-05-06 22:48:03 +08:00
Leeson
5949c73a30 fix: delete Popconfirm being obscured by fixed columns (#6118)
* fix: delete Popconfirm being obscured by fixed columns

* fix: opened popConfirm will prevent the table from scrolling

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-05-06 22:33:17 +08:00
212 changed files with 8239 additions and 4422 deletions

View File

@@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature:
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
- If fixing bug:
- Provide a detailed description of the bug in the PR. Live demo preferred.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ vite.config.ts.*
*.sln
*.sw?
.history
.cursor

View File

@@ -140,8 +140,12 @@ pnpm build
## 貢献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

View File

@@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o
## Contributors
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

View File

@@ -140,8 +140,12 @@ pnpm build
## 贡献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord

View File

@@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);

View File

@@ -1,7 +1,9 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);

View File

@@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {

View File

@@ -0,0 +1,32 @@
import { eventHandler, setHeader } from 'h3';
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 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,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,6 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

View File

@@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result = 0;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
result = aValue ? 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;
}
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,4 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {

View File

@@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,

View File

@@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
@@ -31,12 +34,22 @@ export function verifyAccessToken(
return null;
}
const token = authHeader.split(' ')[1];
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
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 user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,

View File

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

View File

@@ -8,13 +8,7 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -82,16 +76,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,

View File

@@ -8,40 +8,42 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
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',
Switch: 'checked',
Upload: 'fileList',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
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 (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
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,3 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -33,7 +35,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
},
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

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

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

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

View File

@@ -8,13 +8,7 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -139,16 +133,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,

View File

@@ -8,32 +8,34 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
},
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 (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
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,3 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -33,7 +35,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
},
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

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

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

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

View File

@@ -8,13 +8,7 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,

View File

@@ -8,36 +8,38 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
// naive-ui组件的空值为null,不能是undefined否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Upload: 'fileList',
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
// naive-ui组件的空值为null,不能是undefined否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: '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 (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
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,3 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -33,7 +35,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
},
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

@@ -12,12 +12,16 @@ import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
initComponentAdapter();
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { NButton, NCard, useMessage } from 'naive-ui';
import { useVbenForm } from '#/adapter/form';
import { getAllMenusApi } from '#/api';
import modalDemo from './modal.vue';
const message = useMessage();
const [Form, formApi] = useVbenForm({
commonConfig: {
@@ -143,6 +145,10 @@ function setFormValues() {
date: Date.now(),
});
}
const [Modal, modalApi] = useVbenModal({
connectedComponent: modalDemo,
});
</script>
<template>
<Page
@@ -152,8 +158,12 @@ function setFormValues() {
<NCard title="基础表单">
<template #header-extra>
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
<NButton type="primary" @click="modalApi.open()" class="ml-2">
打开弹窗
</NButton>
</template>
<Form />
</NCard>
<Modal />
</Page>
</template>

View File

@@ -0,0 +1,71 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
defineOptions({
name: 'FormModelDemo',
});
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field1',
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field2',
label: '字段2',
rules: 'required',
},
{
component: 'Select',
componentProps: {
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
],
placeholder: '请输入',
},
fieldName: 'field3',
label: '字段3',
rules: 'required',
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
await formApi.validateAndSubmitForm();
// modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values } = modalApi.getData<Record<string, any>>();
if (values) {
formApi.setValues(values);
}
}
},
title: '内嵌表单示例',
});
</script>
<template>
<Modal>
<Form />
</Modal>
</template>

View File

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

View File

@@ -22,7 +22,7 @@ outline: deep
## 基础用法
使用 `useVbenDrawer` 创建最基础的模态框
使用 `useVbenDrawer` 创建最基础的抽屉
<DemoPreview dir="demos/vben-drawer/basic" />
@@ -52,7 +52,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
::: info 注意
- `VbenDrawer` 组件对参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- `VbenDrawer` 组件对参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性如默认隐藏全屏按钮修改默认ZIndex等。
@@ -77,7 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| connectedComponent | 连接另一个Drawer组件 | `Component` | - |
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
@@ -96,7 +96,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
| class | modal的class宽度通过这个配置 | `string` | - |
| contentClass | modal内容区域的class | `string` | - |
| footerClass | modal底部区域的class | `string` | - |

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

@@ -90,30 +90,52 @@ import { h } from 'vue';
import { globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,
@@ -304,10 +326,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
| wrapperClass | 表单的布局基于tailwindcss | `any` | - |
| actionWrapperClass | 表单操作区域class | `any` | - |
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
@@ -324,6 +348,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
::: tip handleValuesChange
@@ -394,7 +419,7 @@ export interface FormCommonConfig {
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: string;
formItemClass?: (() => string) | string;
/**
* 隐藏所有表单项label
* @default false

View File

@@ -56,6 +56,15 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
<DemoPreview dir="demos/vben-modal/shared-data" />
## 动画类型
通过 `animationType` 属性可以控制弹窗的动画效果:
- `slide`(默认):从顶部向下滑动进入/退出
- `scale`:缩放淡入/淡出效果
<DemoPreview dir="demos/vben-modal/animation-type" />
::: info 注意
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
@@ -112,6 +121,7 @@ const [Modal, modalApi] = useVbenModal({
| bordered | 是否显示border | `boolean` | `false` |
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - |
| animationType | 动画类型 | `'slide' \| 'scale'` | `'slide'` |
| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
::: info appendToMain

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

@@ -15,6 +15,7 @@ const [Form] = useVbenForm({
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
// 水平布局label和input在同一行
scrollToFirstError: true,
layout: 'horizontal',
schema: [
{

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { useVbenModal, VbenButton } from '@vben/common-ui';
const [SlideModal, slideModalApi] = useVbenModal({
animationType: 'slide',
});
const [ScaleModal, scaleModalApi] = useVbenModal({
animationType: 'scale',
});
function openSlideModal() {
slideModalApi.open();
}
function openScaleModal() {
scaleModalApi.open();
}
</script>
<template>
<div class="space-y-4">
<div class="flex gap-4">
<VbenButton @click="openSlideModal">滑动动画</VbenButton>
<VbenButton @click="openScaleModal">缩放动画</VbenButton>
</div>
<SlideModal title="滑动动画示例" class="w-[500px]">
<p>这是使用滑动动画的弹窗从顶部向下滑动进入</p>
</SlideModal>
<ScaleModal title="缩放动画示例" class="w-[500px]">
<p>这是使用缩放动画的弹窗以缩放淡入淡出的方式显示</p>
</ScaleModal>
</div>
</template>

View File

@@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
```ts
import { requestClient } from '#/api/request';
export async function deleteUserApi(user: UserInfo) {
return requestClient.delete<boolean>(`/user/${user.id}`, user);
export async function deleteUserApi(userId: number) {
return requestClient.delete<boolean>(`/user/${userId}`);
}
```

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.
:::
@@ -60,6 +60,29 @@ VITE_INJECT_APP_LOADING=true
VITE_ARCHIVER=true
```
```bash [.env.production]
# Public Path for Resources, must start and end with /
VITE_BASE=/
# API URL
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
# Whether to enable compression, can be set to none, brotli, gzip
VITE_COMPRESS=gzip
# Whether to enable PWA
VITE_PWA=false
# vue-router mode
VITE_ROUTER_HISTORY=hash
# Whether to inject global loading
VITE_INJECT_APP_LOADING=true
# Whether to generate dist.zip after packaging
VITE_ARCHIVER=true
```
:::
## Dynamic Configuration in Production Environment
@@ -115,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
@@ -142,6 +186,7 @@ import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description Project configuration file
* Only a part of the configuration in the project needs to be covered, and unnecessary configurations do not need to be covered. The default configuration will be automatically used
* !!! Please clear the cache after changing the configuration, otherwise it may not take effect
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
@@ -162,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',
@@ -172,10 +223,11 @@ const defaultPreferences: Preferences = {
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'modal',
loginExpiredMode: 'page',
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -191,18 +243,23 @@ const defaultPreferences: Preferences = {
enable: true,
icp: '',
icpLink: '',
settingShow: true,
},
footer: {
enable: true,
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: {
@@ -220,23 +277,31 @@ const defaultPreferences: Preferences = {
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
width: 230,
mixedWidth: 80,
width: 224,
},
tabbar: {
draggable: true,
enable: true,
height: 36,
height: 38,
keepAlive: true,
maxCount: 0,
middleClickToClose: false,
persist: true,
showIcon: true,
showMaximize: true,
showMore: true,
styleType: 'chrome',
wheelable: true,
},
theme: {
builtinType: 'default',
@@ -247,7 +312,7 @@ const defaultPreferences: Preferences = {
mode: 'dark',
radius: '0.5',
semiDarkHeader: false,
semiDarkSidebar: true,
semiDarkSidebar: false,
},
transition: {
enable: true,
@@ -261,9 +326,9 @@ const defaultPreferences: Preferences = {
languageToggle: true,
lockScreen: true,
notification: true,
refresh: true,
sidebarToggle: true,
themeToggle: true,
refresh: true,
},
};
```
@@ -288,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 */
@@ -318,6 +395,8 @@ interface AppPreferences {
* @zh_CN Whether to enable watermark
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
/** Whether breadcrumbs are enabled */
@@ -345,6 +424,8 @@ interface CopyrightPreferences {
icp: string;
/** Link to the ICP */
icpLink: string;
/** Whether to show in settings panel */
settingShow?: boolean;
}
interface FooterPreferences {
@@ -352,13 +433,19 @@ 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 */
menuAlign: LayoutHeaderMenuAlignType;
/** Header display mode */
mode: LayoutHeaderModeType;
}
@@ -366,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;
}
@@ -379,18 +468,30 @@ interface NavigationPreferences {
styleType: NavigationStyleType;
}
interface SidebarPreferences {
/** Automatically activate child menu when clicking on directory */
autoActivateChild: boolean;
/** Whether the sidebar is collapsed */
collapsed: boolean;
/** Whether the sidebar collapse button is visible */
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;
}
@@ -417,6 +518,10 @@ interface TabbarPreferences {
height: number;
/** Whether tab caching is enabled */
keepAlive: boolean;
/** Maximum number of tabs */
maxCount: number;
/** Whether to close tab when middle-clicked */
middleClickToClose: boolean;
/** Whether tabs are persistent */
persist: boolean;
/** Whether icons in multiple tabs are enabled */
@@ -427,6 +532,8 @@ interface TabbarPreferences {
showMore: boolean;
/** Tab style */
styleType: TabsStyleType;
/** Whether mouse wheel response is enabled */
wheelable: boolean;
}
interface ThemePreferences {
/** Built-in theme name */
@@ -514,5 +621,6 @@ interface Preferences {
- The `overridesPreferences` method only needs to override a part of the configurations in the project. There's no need to override configurations that are not needed; they will automatically use the default settings.
- Any configuration item can be overridden. You just need to override it within the `overridesPreferences` method. Do not modify the default configuration file.
- Please clear the cache after changing the configuration, otherwise it may not take effect.
:::

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

@@ -4,7 +4,6 @@
- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
- If you are using `vscode`, you need to install the following plugins:
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
@@ -157,7 +156,6 @@ The most effective solution is to perform Lint checks locally before committing.
The project defines corresponding hooks inside `lefthook.yml`:
- `pre-commit`: Runs before commit, used for code formatting and checking
- `code-workspace`: Updates VSCode workspace configuration
- `lint-md`: Formats Markdown files
- `lint-vue`: Formats and checks Vue files
@@ -167,7 +165,6 @@ The project defines corresponding hooks inside `lefthook.yml`:
- `lint-json`: Formats other JSON files
- `post-merge`: Runs after merge, used for automatic dependency installation
- `install`: Runs `pnpm install` to install new dependencies
- `commit-msg`: Runs during commit, used for checking commit message format

View File

@@ -18,7 +18,6 @@
### 友情链接
- 在您的网站上添加我们的友情链接,链接如下:
- 名称Vben Admin
- 链接https://www.vben.pro
- 描述Vben Admin 企业级开箱即用的中后台前端解决方案

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

@@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
```ts
import { requestClient } from '#/api/request';
export async function deleteUserApi(user: UserInfo) {
return requestClient.delete<boolean>(`/user/${user.id}`, user);
export async function deleteUserApi(userId: number) {
return requestClient.delete<boolean>(`/user/${userId}`);
}
```

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',
@@ -195,10 +222,11 @@ const defaultPreferences: Preferences = {
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'modal',
loginExpiredMode: 'page',
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -214,18 +242,23 @@ const defaultPreferences: Preferences = {
enable: true,
icp: '',
icpLink: '',
settingShow: true,
},
footer: {
enable: true,
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: {
@@ -243,23 +276,31 @@ const defaultPreferences: Preferences = {
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
width: 230,
mixedWidth: 80,
width: 224,
},
tabbar: {
draggable: true,
enable: true,
height: 36,
height: 38,
keepAlive: true,
maxCount: 0,
middleClickToClose: false,
persist: true,
showIcon: true,
showMaximize: true,
showMore: true,
styleType: 'chrome',
wheelable: true,
},
theme: {
builtinType: 'default',
@@ -270,7 +311,7 @@ const defaultPreferences: Preferences = {
mode: 'dark',
radius: '0.5',
semiDarkHeader: false,
semiDarkSidebar: true,
semiDarkSidebar: false,
},
transition: {
enable: true,
@@ -311,6 +352,18 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@@ -341,6 +394,8 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@@ -369,6 +424,8 @@ interface CopyrightPreferences {
icp: string;
/** 备案号链接 */
icpLink: string;
/** 设置面板是否显示*/
settingShow?: boolean;
}
interface FooterPreferences {
@@ -376,13 +433,19 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
menuAlign: LayoutHeaderMenuAlignType;
/** header显示模式 */
mode: LayoutHeaderModeType;
}
@@ -390,6 +453,8 @@ interface HeaderPreferences {
interface LogoPreferences {
/** logo是否可见 */
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */
source: string;
}
@@ -404,18 +469,30 @@ interface NavigationPreferences {
}
interface SidebarPreferences {
/** 点击目录时自动激活子菜单 */
autoActivateChild: boolean;
/** 侧边栏是否折叠 */
collapsed: boolean;
/** 侧边栏折叠按钮是否可见 */
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}
@@ -442,6 +519,10 @@ interface TabbarPreferences {
height: number;
/** 开启标签页缓存功能 */
keepAlive: boolean;
/** 限制最大数量 */
maxCount: number;
/** 是否点击中键时关闭标签 */
middleClickToClose: boolean;
/** 是否持久化标签 */
persist: boolean;
/** 是否开启多标签页图标 */
@@ -452,6 +533,8 @@ interface TabbarPreferences {
showMore: boolean;
/** 标签页风格 */
styleType: TabsStyleType;
/** 是否开启鼠标滚轮响应 */
wheelable: boolean;
}
interface ThemePreferences {

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

@@ -4,7 +4,6 @@
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
- 如果你使用的是 `vscode`,需要安装以下插件:
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
@@ -157,7 +156,6 @@ git hook 一般结合各种 lint在 git 提交代码的时候进行代码风
项目在 `lefthook.yml` 内部定义了相应的 hooks
- `pre-commit`: 在提交前运行,用于代码格式化和检查
- `code-workspace`: 更新 VSCode 工作区配置
- `lint-md`: 格式化 Markdown 文件
- `lint-vue`: 格式化并检查 Vue 文件
@@ -167,7 +165,6 @@ git hook 一般结合各种 lint在 git 提交代码的时候进行代码风
- `lint-json`: 格式化其他 JSON 文件
- `post-merge`: 在合并后运行,用于自动安装依赖
- `install`: 运行 `pnpm install` 安装新依赖
- `commit-msg`: 在提交时运行,用于检查提交信息格式

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.6",
"version": "5.5.9",
"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.9",
"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.9",
"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.9",
"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,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"moduleResolution": "bundler"
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.5.6",
"version": "5.5.9",
"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/vite-config",
"version": "5.5.6",
"version": "5.5.9",
"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.9",
"private": true,
"keywords": [
"monorepo",
@@ -98,7 +98,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@10.10.0",
"packageManager": "pnpm@10.14.0",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.6",
"version": "5.5.9",
"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.9",
"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.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { loadScript } from '../resources';
const testJsPath =
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';
describe('loadScript', () => {
beforeEach(() => {
// 每个测试前清空 head保证环境干净
document.head.innerHTML = '';
});
it('should resolve when the script loads successfully', async () => {
const promise = loadScript(testJsPath);
// 此时脚本元素已被创建并插入
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载成功
script.dispatchEvent(new Event('load'));
// 等待 promise resolve
await expect(promise).resolves.toBeUndefined();
});
it('should not insert duplicate script and resolve immediately if already loaded', async () => {
// 先手动插入一个相同 src 的 script
const existing = document.createElement('script');
existing.src = 'bar.js';
document.head.append(existing);
// 再次调用
const promise = loadScript('bar.js');
// 立即 resolve
await expect(promise).resolves.toBeUndefined();
// head 中只保留一个
const scripts = document.head.querySelectorAll('script[src="bar.js"]');
expect(scripts).toHaveLength(1);
});
it('should reject when the script fails to load', async () => {
const promise = loadScript('error.js');
const script = document.querySelector(
'script[src="error.js"]',
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载失败
script.dispatchEvent(new Event('error'));
await expect(promise).rejects.toThrow('Failed to load script: error.js');
});
it('should handle multiple concurrent calls and only insert one script tag', async () => {
const p1 = loadScript(testJsPath);
const p2 = loadScript(testJsPath);
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 触发一次 load两个 promise 都应该 resolve
script.dispatchEvent(new Event('load'));
await expect(p1).resolves.toBeUndefined();
await expect(p2).resolves.toBeUndefined();
// 只插入一次
const scripts = document.head.querySelectorAll(
`script[src="${testJsPath}"]`,
);
expect(scripts).toHaveLength(1);
});
});

View File

@@ -7,6 +7,7 @@ export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './resources';
export * from './state-handler';
export * from './to';
export * from './tree';

View File

@@ -0,0 +1,21 @@
/**
* 加载js文件
* @param src js文件地址
*/
function loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
// 如果已经加载过,直接 resolve
return resolve();
}
const script = document.createElement('script');
script.src = src;
script.addEventListener('load', () => resolve());
script.addEventListener('error', () =>
reject(new Error(`Failed to load script: ${src}`)),
);
document.head.append(script);
});
}
export { loadScript };

View File

@@ -30,7 +30,7 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.6",
"version": "5.5.9",
"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

@@ -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.9",
"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.9",
"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.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -34,27 +34,21 @@ const submitButtonOptions = computed(() => {
// return !!unref(rootProps).showCollapseButton;
// });
const queryFormStyle = computed(() => {
if (!unref(rootProps).actionWrapperClass) {
return {
'grid-column': `-2 / -1`,
marginLeft: 'auto',
};
}
return {};
});
async function handleSubmit(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const { valid } = await form.validate();
const props = unref(rootProps);
if (!props.formApi) {
return;
}
const { valid } = await props.formApi.validate();
if (!valid) {
return;
}
const values = toRaw(await unref(rootProps).formApi?.getValues());
await unref(rootProps).handleSubmit?.(values);
const values = toRaw(await props.formApi.getValues());
await props.handleSubmit?.(values);
}
async function handleReset(e: Event) {
@@ -81,22 +75,59 @@ watch(
},
);
const actionWrapperClass = computed(() => {
const props = unref(rootProps);
const actionLayout = props.actionLayout || 'rowEnd';
const actionPosition = props.actionPosition || 'right';
const cls = [
'flex',
'items-center',
'gap-3',
props.compact ? 'pb-2' : 'pb-4',
props.layout === 'vertical' ? 'self-end' : 'self-center',
props.layout === 'inline' ? '' : 'w-full',
props.actionWrapperClass,
];
switch (actionLayout) {
case 'newLine': {
cls.push('col-span-full');
break;
}
case 'rowEnd': {
cls.push('col-[-2/-1]');
break;
}
// 'inline' 不需要额外类名,保持默认
}
switch (actionPosition) {
case 'center': {
cls.push('justify-center');
break;
}
case 'left': {
cls.push('justify-start');
break;
}
default: {
// case 'right': 默认右对齐
cls.push('justify-end');
break;
}
}
return cls.join(' ');
});
defineExpose({
handleReset,
handleSubmit,
});
</script>
<template>
<div
:class="
cn(
'col-span-full w-full text-right',
rootProps.compact ? 'pb-2' : 'pb-6',
rootProps.actionWrapperClass,
)
"
:style="queryFormStyle"
>
<div :class="cn(actionWrapperClass)">
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
@@ -104,7 +135,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@@ -119,7 +149,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.DefaultButton"
v-if="resetButtonOptions.show"
class="ml-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
@@ -134,7 +163,6 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@@ -147,9 +175,9 @@ defineExpose({
<slot name="expand-before"></slot>
<VbenExpandableArrow
class="ml-[-0.3em]"
v-if="rootProps.showCollapseButton"
v-model:model-value="collapsed"
class="ml-2"
>
<span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>
</VbenExpandableArrow>

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 {
@@ -39,6 +39,7 @@ function getDefaultState(): VbenFormProps {
layout: 'horizontal',
resetButtonOptions: {},
schema: [],
scrollToFirstError: false,
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: {},
@@ -100,9 +101,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;
}
/**
@@ -236,6 +254,41 @@ export class FormApi {
});
}
/**
* 滚动到第一个错误字段
* @param errors 验证错误对象
*/
scrollToFirstError(errors: Record<string, any> | string) {
// https://github.com/logaretm/vee-validate/discussions/3835
const firstErrorFieldName =
typeof errors === 'string' ? errors : Object.keys(errors)[0];
if (!firstErrorFieldName) {
return;
}
let el = document.querySelector(
`[name="${firstErrorFieldName}"]`,
) as HTMLElement;
// 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的
if (!el) {
const componentRef = this.getFieldComponentRef(firstErrorFieldName);
if (componentRef && componentRef.$el instanceof HTMLElement) {
el = componentRef.$el;
}
}
if (el) {
// 滚动到错误字段,添加一些偏移量以确保字段完全可见
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
}
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
const form = await this.getForm();
form.setFieldValue(field, value, shouldValidate);
@@ -360,14 +413,21 @@ export class FormApi {
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
console.error('validate error', validateResult?.errors);
if (this.state?.scrollToFirstError) {
this.scrollToFirstError(validateResult.errors);
}
}
return validateResult;
}
async validateAndSubmitForm() {
const form = await this.getForm();
const { valid } = await form.validate();
const { valid, errors } = await form.validate();
if (!valid) {
if (this.state?.scrollToFirstError) {
this.scrollToFirstError(errors);
}
return;
}
return await this.submitForm();
@@ -379,6 +439,10 @@ export class FormApi {
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
console.error('validate error', validateResult?.errors);
if (this.state?.scrollToFirstError) {
this.scrollToFirstError(fieldName);
}
}
return validateResult;
}

View File

@@ -59,7 +59,7 @@ const values = useFormValues();
const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form;
const compact = formRenderProps.compact;
const compact = computed(() => formRenderProps.compact);
const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => {
@@ -295,7 +295,7 @@ onUnmounted(() => {
'form-is-required': shouldRequired,
'flex-col': isVertical,
'flex-row items-center': !isVertical,
'pb-6': !compact,
'pb-4': !compact,
'pb-2': compact,
}"
class="relative flex"
@@ -386,7 +386,7 @@ onUnmounted(() => {
</div>
<Transition name="slide-up" v-if="!compact">
<FormMessage class="absolute bottom-1" />
<FormMessage class="absolute" />
</Transition>
</div>
</FormItem>

View File

@@ -12,7 +12,12 @@ import type {
import { computed } from 'vue';
import { Form } from '@vben-core/shadcn-ui';
import { cn, isString, mergeWithArrayOverride } from '@vben-core/shared/utils';
import {
cn,
isFunction,
isString,
mergeWithArrayOverride,
} from '@vben-core/shared/utils';
import { provideFormRenderProps } from './context';
import { useExpandable } from './expandable';
@@ -36,6 +41,16 @@ const emits = defineEmits<{
submit: [event: any];
}>();
const wrapperClass = computed(() => {
const cls = ['flex'];
if (props.layout === 'inline') {
cls.push('flex-wrap gap-x-2');
} else {
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
}
return cn(...cls, props.wrapperClass);
});
provideFormRenderProps(props);
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
@@ -110,6 +125,17 @@ const computedSchema = computed(
? keepIndex <= index
: false;
// 处理函数形式的formItemClass
let resolvedSchemaFormItemClass = schema.formItemClass;
if (isFunction(schema.formItemClass)) {
try {
resolvedSchemaFormItemClass = schema.formItemClass();
} catch (error) {
console.error('Error calling formItemClass function:', error);
resolvedSchemaFormItemClass = '';
}
}
return {
colon,
disabled,
@@ -133,7 +159,7 @@ const computedSchema = computed(
'flex-shrink-0',
{ hidden },
formItemClass,
schema.formItemClass,
resolvedSchemaFormItemClass,
),
labelClass: cn(labelClass, schema.labelClass),
};
@@ -144,7 +170,7 @@ const computedSchema = computed(
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<div ref="wrapperRef" :class="wrapperClass">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

View File

@@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
import type { FormApi } from './form-api';
export type FormLayout = 'horizontal' | 'vertical';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type BaseFormComponentType =
| 'DefaultButton'
@@ -174,10 +174,10 @@ export interface FormCommonConfig {
*/
formFieldProps?: FormFieldOptions;
/**
* 所有表单项的栅格布局
* 所有表单项的栅格布局,支持函数形式
* @default ""
*/
formItemClass?: string;
formItemClass?: (() => string) | string;
/**
* 隐藏所有表单项label
* @default false
@@ -354,6 +354,15 @@ export interface VbenFormProps<
* 操作按钮是否反转(提交按钮前置)
*/
actionButtonsReverse?: boolean;
/**
* 操作按钮组的样式
* newLine: 在新行显示。rowEnd: 在行内显示靠右对齐默认。inline: 使用grid默认样式
*/
actionLayout?: 'inline' | 'newLine' | 'rowEnd';
/**
* 操作按钮组显示位置,默认靠右显示
*/
actionPosition?: 'center' | 'left' | 'right';
/**
* 表单操作区域class
*/
@@ -387,6 +396,12 @@ export interface VbenFormProps<
*/
resetButtonOptions?: ActionButtonOptions;
/**
* 验证失败时是否自动滚动到第一个错误字段
* @default false
*/
scrollToFirstError?: boolean;
/**
* 是否显示默认操作按钮
* @default true

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,

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