Compare commits

..

83 Commits

Author SHA1 Message Date
Netfan
67071326a1 chore: update electron dependence 2025-05-19 09:55:56 +08:00
Netfan
30387b94a2 feat: electron support 2025-05-19 09:55:51 +08:00
afe1
643f25347b perf: stub unbuild params (#6210) 2025-05-19 09:54:31 +08:00
wyc001122
49a739ffc6 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-19 09:54:30 +08:00
ming4762
128e749702 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-19 09:54:30 +08:00
wyc001122
c91fd146b8 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-19 09:54:29 +08:00
afe1
74874a88a6 fix: css style (#6176) 2025-05-19 09:54:28 +08:00
panda7
f0f890c583 fix: the mobile terminal can wrap lines and expand slot attributes (#6165)
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-05-19 09:54:27 +08:00
XiaoHetitu
ea48a01d8f 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-19 09:54:26 +08:00
chewenye
9311c658a1 types: 导出authentication组件的type,自定义toolbarList时类型使用ToolbarType (#6158)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-05-19 09:54:25 +08:00
Netfan
12151d9742 fix: refresh command of tabbar issue, fixed: #6162 (#6169) 2025-05-19 09:54:25 +08:00
anyup
de655af23b feat: support to refresh the tab page by route name (#6153)
Co-authored-by: anyup <anyupxing@163.com>
2025-05-19 09:54:24 +08:00
afe1
6fe909bfba fix: delete useless code (#6143) 2025-05-19 09:54:23 +08:00
wyc001122
899a99535f docs(@vben/docs): update settings doc (#6128)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-19 09:54:22 +08:00
zyf0624
29bdf2d13b fix: tsconfig moduleResolution (#6122)
Co-authored-by: pzzyf <2279948211@qq.com>
2025-05-19 09:54:22 +08:00
Netfan
4f633c1a9a fix: missing argument for getPopupContainer 2025-05-19 09:54:21 +08:00
Leeson
b2951b2975 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-19 09:54:20 +08:00
vben
d9f8722c27 chore: release v5.5.6 2025-05-19 09:54:19 +08:00
Jin Mao
1539fc738c fix: handle undefined children in generate-menus (#6117)
When children is undefined, use empty array as fallback to prevent potential runtime errors. This matches the behavior when hideChildrenInMenu is true.
2025-05-19 09:54:19 +08:00
LinaBell
200859fc7e fix: when keepAlive is enabled, returning directly through browser buttons/gestures will not close pop ups (#6113) 2025-05-19 09:54:18 +08:00
ming4762
36fb6b2222 fix: fix IconPicker props warning (#6108)
Invalid prop: type check failed for prop "onUpdate:value". Expected Function, got Array
2025-05-19 09:54:17 +08:00
vben
3304d54546 chore: remove prepare script from package.json 2025-05-19 09:54:16 +08:00
vben
ca57d223b4 chore: update prepare script in package.json to remove lefthook installation 2025-05-19 09:54:15 +08:00
Vben
0fa587c246 feat(project): migrate from husky and lint-staged to lefthook (#6104) 2025-05-19 09:54:11 +08:00
Vben
4fd2fee782 feat: support smooth auto-scroll to active menu item (#6102) 2025-05-19 09:54:00 +08:00
Vben
c4a4ca5232 chore: close eslint object sorting (#6101) 2025-05-19 09:53:59 +08:00
aonoa
73db7f3ef5 refactor: modify the default homepage path loaded from the preference… (#6099)
* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
2025-05-19 09:53:58 +08:00
Netfan
aef2134b60 fix: add triggerClass binding to PopoverTrigger and update icon-picker styles (#6095)
* Popover支持设置trigger的样式
* 修正icon-picker的input值更新
2025-05-19 09:53:58 +08:00
Netfan
f920895b8c fix: add missing translation for preferences drawer (#6094) 2025-05-19 09:53:57 +08:00
Netfan
10257e3611 fix: destroyOnClose incorrect default value, fixed #6092 (#6093) 2025-05-19 09:53:56 +08:00
ming4762
a88a2c8afa fix: fix LoginExpiredModal in some cases, message may be obscured (#6086) 2025-05-19 09:53:56 +08:00
Netfan
d92697e7f7 fix: show validation message as tooltip in compact form (#6087)
* 紧凑模式表单的校验消息将显示为一个tooltip
2025-05-19 09:53:55 +08:00
Jin Mao
e34285a51e perf: page componet supports custom height offset for flexible content height … (#6081)
* perf: Page supports custom height offset for flexible content height control.

允许通过 height 属性调整页面内容高度计算。修改了 Page 组件以支持自定义高度偏移量,用于更灵活的内容高度控制。

* chore: typo

* perf(page): replace height with heightOffset for flexible content sizing

The `height` prop was replaced with `heightOffset` to better describe its purpose when used with `autoContentHeight`. The new prop allows custom offset values (in pixels) to adjust content area sizing, with clearer documentation.
2025-05-19 09:53:54 +08:00
Netfan
721b5f3d3e fix: calculation for collapsing search form is incorrect while initially hidden (#6068)
* 修复当默认隐藏搜索表单时,折叠位置的计算不正确的问题
2025-05-19 09:53:53 +08:00
vben
5e694b3194 chore: update readme.md 2025-05-19 09:53:52 +08:00
Vben
bf5e697c42 chore: update dependencies and documentation, optimize build toolchain (#6060)
* chore: update packageManager version to pnpm@10.9.0 for compatibility improvements

* chore: Update dependent versions and configurations to improve compatibility and stability

- Update Node version to 22.1.0
- Updated pnpm version to 10.10.0
- Fixed syntax error in prettier command in lintstagedrc
- Update dependent versions in pnpm-lock.yaml to ensure consistency
- Update format and content in README documents to improve readability

* fix: lint error
2025-05-19 09:53:46 +08:00
vem
9437e97ce4 fix: Update existing route index to prevent 404 on user switch (#6003)
Co-authored-by: tars-macmini <vem@qq.com>
2025-05-19 09:51:07 +08:00
Netfan
77c09b4ddf fix: lock state will not change overflow style in drawer and modal (#6067)
* Modal和Drawer的锁定状态不再修改overflow样式
2025-05-19 09:51:06 +08:00
Gahotx
70d07c683a fix: add rounded corners to project and quick nav items (#5296) 2025-05-19 09:51:05 +08:00
Vben
e840905934 chore: update dependency version for improved stability and compatibility (#6023)
* chore: update dependency version for improved stability and compatibility

* fix: optimize clearPoints function in useCaptchaPoints hook to improve performance

* fix: make several props optional in various components for better flexibility
2025-05-19 09:50:59 +08:00
Trivikram Kamat
8b192d05fd fix: install corepack from npm (#5905)
* fix: install corepack from npm

* docs: install corepack from npm
2025-05-19 09:50:36 +08:00
ming4762
9f910c3486 perf: resolve duplicate component names (#6039) 2025-05-19 09:50:35 +08:00
vben
25b0822afd chore: release v5.5.5 2025-05-19 09:50:34 +08:00
Netfan
8292024fc7 feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密
* 将锁屏密码合并到accessStore中进行加密
2025-05-19 09:50:26 +08:00
Jin Mao
9a0e813143 docs: add deepWiki doc link (#6057) 2025-05-19 09:50:13 +08:00
ming4762
1a73329691 perf: improve destroyOnClose for VbenDrawer&VbenModal (#6051)
* fix: fix that the default value of modal destroyOnClose does not take effect

* perf: improve destroyOnClose for VbenDrawer
2025-05-19 09:50:12 +08:00
Netfan
ab161444fc fix: title of search button in vxeTable toolbar (#6046)
* 修改vxeTable工具栏里的搜索按钮的提示文案
2025-05-19 09:50:12 +08:00
Netfan
0243e68fbe feat: vbenSelect support prop allowClear (#6043) 2025-05-19 09:50:11 +08:00
Netfan
54c0d4ed83 docs: update example (#6036)
* 跟进后端菜单逻辑的修改,现已无需传递basicLayout布局
2025-05-19 09:50:10 +08:00
panda7
e320311128 fix: the initial value echo for the check-button-group (#6029)
Co-authored-by: sqchen <9110848@qq.com>
2025-05-19 09:50:09 +08:00
Netfan
1faa52335c fix: alert confirm state in beforeClose callback (#6019) 2025-05-19 09:50:09 +08:00
pangyajun123
9ff600657b fix: vxe-table theme token follow primary color (#6007) 2025-05-19 09:50:08 +08:00
wyc001122
d14527a292 fix: fix geader menu activation path (#5997)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-05-19 09:50:07 +08:00
Netfan
eb70c48d14 fix: alert send wrong confirm state to beforeClose (#5991)
* 修复alert在按下Esc或者点击遮罩关闭时,可能发送错误的isConfirm状态
2025-05-19 09:50:06 +08:00
Netfan
302de1deaf fix: destroyOnClose works within connectedComponent (#5989)
* 修复destroyOnClose没能销毁connectedComponent自身的问题
2025-05-19 09:50:06 +08:00
PIPEDREA_WZJ
ea0c2ad58b Update tailwindcss.md (#5602)
tailwindcss最新的版本已经是v4.x,vben中使用的是3.x的tailwindcss。在未进行兼容前,会出现运行失败的问题
2025-05-19 09:50:05 +08:00
yuh
678646b4ba feat: add examples: form-upload (#5955)
* feat: add examples: form-upload

* fix: upload: accept and label

* fix: upload: 设置表单值、图片预览
2025-05-19 09:50:04 +08:00
Netfan
a4da6dedf8 fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-05-19 09:50:04 +08:00
Netfan
332a8be29c feat: pass fieldsChanged into the handleValuesChange callback function (#5968)
* fieldsChanged(已被改变值的字段名)将传入handleValuesChange回调函数
2025-05-19 09:50:03 +08:00
ming4762
357ae0e565 perf: improve destroyOnClose for VbenModal (#5964) 2025-05-19 09:50:02 +08:00
LinaBell
fe2310300c perf: beforeClose of drawer support promise (#5932)
* perf: the beforeClose function of drawer is consistent with that of modal

* refactor: drawer test update
2025-05-19 09:50:01 +08:00
zhouda1fu
7e99c62abf fix: missing await in department form(#5967) 2025-05-19 09:50:01 +08:00
Netfan
a3b2d5c2e1 docs: update alert and apiComponent docs (#5961) 2025-05-19 09:50:00 +08:00
wyc001122
39f9c6a3f4 fix: determine if scrollbar has been totally scrolled (#5934)
* 修复在系统屏幕缩放比例不为100%的情况下,滚动组件对是否已滚动到边界的判断可能不正确的问题
2025-05-19 09:49:59 +08:00
ming4762
48c616ea81 fix: modal closing animation (#5960) 2025-05-19 09:49:58 +08:00
ming4762
06e279c917 feat: modal&drawer support center-footer slot (#5956) 2025-05-19 09:49:58 +08:00
lztb
ff65b061ef feat: vben-form添加arrayToStringFields属性 (#5957)
* feat: vben-form添加arrayToStringFields属性

* feat: 修改handleArrayToStringFields和handleStringToArrayFields中嵌套数组格式的处理不一致

---------

Co-authored-by: 米山 <17726957223@189.cn>
2025-05-19 09:49:57 +08:00
Netfan
b8f145aa7f feat: add more expose methods for apiComponent (#5958)
* 为ApiComponent组件添加getOptions和getValue导出方法。
2025-05-19 09:49:56 +08:00
Netfan
091596eda8 feat: add useAlertContext for Alert component (#5947)
* 新增Alert的子组件中获取弹窗上下文的能力
2025-05-19 09:49:55 +08:00
Netfan
17b0111424 fix: table actions in fixed column (#5945) 2025-05-19 09:49:55 +08:00
Netfan
6e5ddb6d25 feat: alert support customize footer (#5940)
* Alert组件支持自定义footer
2025-05-19 09:49:54 +08:00
Netfan
46a10959c8 fix: long navigation menu can be scrolled (#5939)
* 修复超长的导航菜单无法纵向滚动的问题
2025-05-19 09:49:53 +08:00
ming4762
382df99988 perf: improve destroyOnClose for VbenModal (#5935)
* perf: 优化Vben Modal destroyOnClose,解决destroyOnClose=false,Modal依旧会被销毁的问题

影响范围(重要):destroyOnClose默认为true,这会导致所有的modal都会默认渲染到body
radix-vue Dialog组件默认会销毁挂载的组件,所以即使destroyOnClose=false,Modal依旧会被销毁的问题
对于一些大表单重复渲染导致卡顿,ApiComponent也会频繁的加载数据

* fix: modal closing animation

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-05-19 09:49:52 +08:00
ming4762
d00b6bb83e perf: improve autoSelect of ApiComponent (#5936)
* fix: 修复autoSelect不生效的问题,props.valueField已经被omit了

* feat: ApiComponent autoSelect支持使用函数,可以满足灵活性要求更高的场景
2025-05-19 09:49:51 +08:00
Netfan
82a05e3b3a feat: api-component support autoSelect prop (#5931)
* feat: api-component support autoSelect prop

* docs: add version requirement
2025-05-19 09:49:51 +08:00
Netfan
d3d1e1616c docs: docs modal z-index fixed, update alert docs (#5930) 2025-05-19 09:49:50 +08:00
Netfan
a4682a7116 fix: alert animation (#5927) 2025-05-19 09:49:49 +08:00
Netfan
a8935aaf8c fix: builtin color change throttled in preference drawer (#5924)
修复偏好设置中的自定义主题色拖动选择颜色时页面会明显卡顿的问题
2025-05-19 09:49:48 +08:00
Netfan
a6b480fb8f fix: theme mode follow the system only auto (#5923)
* 修复主题在未设置为auto时,仍然会跟随系统主题变化的问题。
2025-05-19 09:49:48 +08:00
Netfan
a87fa3ffbe fix: alert action button focus, fixed #5921 (#5922)
* 修复Alert组件的按钮焦点切换问题
2025-05-19 09:49:47 +08:00
zhang
d450df946c chore: 导出框架自带的组件,方便独立页面使用 (#5876) 2025-05-19 09:49:46 +08:00
Netfan
20f902ed2c feat: electron control buttons 2025-04-09 04:05:27 +08:00
Netfan
7536be5e49 feat: electron support 2025-04-09 01:24:15 +08:00
227 changed files with 6243 additions and 8336 deletions

View File

@@ -19,9 +19,11 @@ 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.

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist-ssr
dist.zip
dist.tar
dist.war
dist-electron
.nitro
.output
*-dist.zip
@@ -49,4 +50,3 @@ vite.config.ts.*
*.sln
*.sw?
.history
.cursor

3
.npmrc
View File

@@ -1,5 +1,8 @@
registry = "https://registry.npmmirror.com"
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
public-hoist-pattern[]=lefthook
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss

View File

@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord

View File

@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord

View File

@@ -140,12 +140,8 @@ 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://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
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,12 +1,10 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse, useResponseSuccess } from '~/utils/response';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
@@ -30,7 +28,6 @@ export default defineEventHandler(async (event) => {
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
accessToken,
});
return accessToken;
});

View File

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

View File

@@ -1,6 +1,3 @@
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,4 +1,3 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
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,11 +1,6 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@@ -49,69 +44,30 @@ 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);
// 规范化 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';
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
listData.sort((a, b) => {
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;
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
} else {
result = aValue ? 1 : -1;
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
}
} else {
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',
});
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,42 +8,40 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
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;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [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;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { initSetupVbenForm, useVbenForm, z };
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -1,5 +1,3 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -35,7 +33,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

@@ -12,7 +12,6 @@ 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';
@@ -20,9 +19,6 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,

View File

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

View File

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

View File

@@ -8,34 +8,32 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
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;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [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;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { initSetupVbenForm, useVbenForm, z };
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -1,5 +1,3 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -35,7 +33,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

@@ -13,17 +13,12 @@ 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

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

View File

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

View File

@@ -8,38 +8,36 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
// naive-ui组件的空值为null,不能是undefined否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Upload: 'fileList',
},
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;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [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;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { initSetupVbenForm, useVbenForm, z };
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -1,5 +1,3 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
@@ -35,7 +33,7 @@ setupVbenVxeTable({
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },

View File

@@ -12,16 +12,12 @@ 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) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
initComponentAdapter();
// // 设置弹窗的默认配置
// setDefaultModalProps({

View File

@@ -1,13 +1,11 @@
<script lang="ts" setup>
import { Page, useVbenModal } from '@vben/common-ui';
import { Page } 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: {
@@ -145,10 +143,6 @@ function setFormValues() {
date: Date.now(),
});
}
const [Modal, modalApi] = useVbenModal({
connectedComponent: modalDemo,
});
</script>
<template>
<Page
@@ -158,12 +152,8 @@ const [Modal, modalApi] = useVbenModal({
<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

@@ -1,71 +0,0 @@
<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.9",
"version": "5.5.6",
"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 | 连接另一个Drawer组件 | `Component` | - |
| connectedComponent | 连接另一个Modal组件 | `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,12 +26,6 @@ outline: deep
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
## 自动显示 tooltip
通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
## API
### Props
@@ -43,8 +37,6 @@ 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,52 +90,30 @@ import { h } from 'vue';
import { globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
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'));
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 withDefaultPlaceholder = <T extends Component>(
component: T,
@@ -326,12 +304,10 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `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` | - |
@@ -348,7 +324,6 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
::: tip handleValuesChange
@@ -419,7 +394,7 @@ export interface FormCommonConfig {
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: (() => string) | string;
formItemClass?: string;
/**
* 隐藏所有表单项label
* @default false

View File

@@ -56,15 +56,6 @@ 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` 来更新状态。
@@ -121,7 +112,6 @@ 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

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

View File

@@ -1,36 +0,0 @@
<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(userId: number) {
return requestClient.delete<boolean>(`/user/${userId}`);
export async function deleteUserApi(user: UserInfo) {
return requestClient.delete<boolean>(`/user/${user.id}`, user);
}
```

View File

@@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
console.log(import.meta.env.VITE_PROT);
```
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. :::
:::
@@ -138,27 +138,6 @@ 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
@@ -207,12 +186,6 @@ 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',
@@ -227,7 +200,6 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -248,18 +220,15 @@ 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: {
@@ -279,14 +248,11 @@ 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: {
@@ -353,18 +319,6 @@ 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 */
@@ -395,8 +349,6 @@ interface AppPreferences {
* @zh_CN Whether to enable watermark
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
/** Whether breadcrumbs are enabled */
@@ -433,15 +385,11 @@ 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 */
@@ -453,8 +401,6 @@ 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;
}
@@ -476,22 +422,16 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** Whether to show title when sidebar is collapsed */
collapsedShowTitle: boolean;
/** Sidebar collapse width */
collapseWidth: number;
/** Whether the sidebar is visible */
enable: boolean;
/** Menu auto-expand state */
expandOnHover: boolean;
/** Whether the sidebar extension area is collapsed */
extraCollapse: boolean;
/** Sidebar extension area collapse width */
extraCollapsedWidth: number;
/** Whether the sidebar fixed button is visible */
fixedButton: boolean;
/** Whether the sidebar is hidden - css */
hidden: boolean;
/** Mixed sidebar width */
mixedWidth: number;
/** Sidebar width */
width: number;
}

View File

@@ -4,11 +4,10 @@ outline: deep
# Access Control
The framework has built-in three types of access control methods:
The framework has built-in two 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
@@ -152,43 +151,6 @@ 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,6 +4,7 @@
- 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
@@ -156,6 +157,7 @@ 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
@@ -165,6 +167,7 @@ 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,6 +18,7 @@
### 友情链接
- 在您的网站上添加我们的友情链接,链接如下:
- 名称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

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

View File

@@ -21,7 +21,7 @@
console.log(import.meta.env.VITE_PROT);
```
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. :::
:::
@@ -137,27 +137,6 @@ 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
@@ -206,12 +185,6 @@ 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',
@@ -226,7 +199,6 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -247,18 +219,15 @@ 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: {
@@ -278,14 +247,11 @@ 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: {
@@ -352,18 +318,6 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@@ -394,8 +348,6 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@@ -433,15 +385,11 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@@ -453,8 +401,6 @@ interface HeaderPreferences {
interface LogoPreferences {
/** logo是否可见 */
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */
source: string;
}
@@ -477,22 +423,16 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}

View File

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

View File

@@ -4,6 +4,7 @@
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
- 如果你使用的是 `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) - 单词语法检查
@@ -156,6 +157,7 @@ git hook 一般结合各种 lint在 git 提交代码的时候进行代码风
项目在 `lefthook.yml` 内部定义了相应的 hooks
- `pre-commit`: 在提交前运行,用于代码格式化和检查
- `code-workspace`: 更新 VSCode 工作区配置
- `lint-md`: 格式化 Markdown 文件
- `lint-vue`: 格式化并检查 Vue 文件
@@ -165,6 +167,7 @@ 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.9",
"version": "5.5.6",
"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.9",
"version": "5.5.6",
"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.9",
"version": "5.5.6",
"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.9",
"version": "5.5.6",
"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/tsconfig",
"version": "5.5.9",
"version": "5.5.6",
"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.9",
"version": "5.5.6",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -47,12 +47,15 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"dayjs": "catalog:",
"dotenv": "catalog:",
"electron": "catalog:",
"rollup": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"sass": "catalog:",
"vite": "catalog:",
"vite-plugin-compression": "catalog:",
"vite-plugin-dts": "catalog:",
"vite-plugin-electron": "catalog:",
"vite-plugin-electron-renderer": "catalog:",
"vite-plugin-html": "catalog:",
"vite-plugin-lazy-import": "catalog:"
}

View File

@@ -0,0 +1,40 @@
import type { PluginOption } from 'vite';
import type { CommonPluginOptions } from '../typing';
import fs from 'node:fs';
import electron from 'vite-plugin-electron/simple';
export const viteElectronPlugin = (
options: CommonPluginOptions,
): PluginOption => {
fs.rmSync('dist-electron', { force: true, recursive: true });
const isServe = !options.isBuild;
const isBuild = options.isBuild;
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/main',
sourcemap,
},
},
},
preload: {
input: 'electron/preload.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/preload',
sourcemap: sourcemap ? 'inline' : undefined, // #332
},
},
},
renderer: {},
});
};

View File

@@ -18,6 +18,7 @@ import { VitePWA } from 'vite-plugin-pwa';
import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteArchiverPlugin } from './archiver';
import { viteElectronPlugin } from './electron';
import { viteExtraAppConfigPlugin } from './extra-app-config';
import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
@@ -97,6 +98,7 @@ async function loadApplicationPlugins(
archiverPluginOptions,
compress,
compressTypes,
electron,
extraAppConfig,
html,
i18n,
@@ -213,6 +215,10 @@ async function loadApplicationPlugins(
return [await viteArchiverPlugin(archiverPluginOptions)];
},
},
{
condition: electron,
plugins: () => [viteElectronPlugin(commonOptions)],
},
]);
}

View File

@@ -3,23 +3,6 @@ import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
import type { PluginOptions } from 'vite-plugin-dts';
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
/**
* ImportMap 配置接口
* @description 用于配置模块导入映射,支持自定义导入路径和范围
* @example
* ```typescript
* {
* imports: {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* },
* scopes: {
* 'https://site.com/': {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* }
* }
* }
* ```
*/
interface IImportMap {
/** 模块导入映射 */
imports?: Record<string, string>;
@@ -202,6 +185,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
* @description 可选的压缩类型
*/
compressTypes?: ('brotli' | 'gzip')[];
/** 启用electron */
electron?: boolean;
/**
* 是否抽离配置文件
* @default false

View File

@@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.9",
"version": "5.5.6",
"private": true,
"keywords": [
"monorepo",
@@ -98,7 +98,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@10.10.0",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

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

View File

@@ -45,6 +45,7 @@ export {
Menu,
Minimize,
Minimize2,
Minus,
MoonStar,
Palette,
PanelLeft,
@@ -58,6 +59,8 @@ export {
Settings,
Shrink,
Square,
SquareArrowDownLeft,
SquareArrowUpRight,
SquareCheckBig,
SquareMinus,
Sun,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.9",
"version": "5.5.6",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -98,6 +98,8 @@
"@types/lodash.get": "catalog:",
"@types/lodash.isequal": "catalog:",
"@types/lodash.set": "catalog:",
"@types/nprogress": "catalog:"
"@types/nprogress": "catalog:",
"@vben-core/typings": "workspace:*",
"electron": "catalog:"
}
}

View File

@@ -1,82 +0,0 @@
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,7 +7,6 @@ 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

@@ -1,21 +0,0 @@
/**
* 加载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

@@ -28,10 +28,11 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
* @param path
*/
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
// const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
// const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
// openWindow(url, { target: '_blank' });
window.ipcRenderer.invoke('open-win', fullPath);
}
export { openRouteInNewWindow, openWindow };

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"compilerOptions": {
"types": ["@vben-core/typings/electron"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,27 @@
import type { IpcRendererEvent } from 'electron';
export type IpcRendererInvoke =
| 'app-close'
| 'app-maximize'
| 'app-minimize'
| 'is-maximized'
| 'open-win';
declare global {
interface Window {
ipcRenderer: {
invoke: (channel: IpcRendererInvoke, ...args: any[]) => Promise<any>;
off: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
on: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
send: (channel: string, data: any) => Promise<any>;
};
}
}
export {};

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.9",
"version": "5.5.6",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -27,6 +27,9 @@
},
"./vue-router": {
"types": "./vue-router.d.ts"
},
"./electron": {
"types": "./electron.d.ts"
}
},
"publishConfig": {

View File

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

View File

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

View File

@@ -10,12 +10,6 @@ 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,
@@ -29,7 +23,6 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"name": "Vben Admin",
"preferencesButtonPosition": "auto",
"watermark": false,
"zIndex": 200,
},
"breadcrumb": {
"enable": true,
@@ -50,18 +43,15 @@ 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": {
@@ -78,17 +68,14 @@ 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.9",
"version": "5.5.6",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -9,12 +9,6 @@ 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',
@@ -29,7 +23,6 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@@ -50,19 +43,15 @@ 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: {
@@ -82,14 +71,11 @@ 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,18 +33,6 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@@ -75,8 +63,6 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@@ -114,15 +100,11 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@@ -134,8 +116,6 @@ interface HeaderPreferences {
interface LogoPreferences {
/** logo是否可见 */
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */
source: string;
}
@@ -158,22 +138,16 @@ 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.9",
"version": "5.5.6",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -34,21 +34,27 @@ 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 props = unref(rootProps);
if (!props.formApi) {
return;
}
const { valid } = await props.formApi.validate();
const { valid } = await form.validate();
if (!valid) {
return;
}
const values = toRaw(await props.formApi.getValues());
await props.handleSubmit?.(values);
const values = toRaw(await unref(rootProps).formApi?.getValues());
await unref(rootProps).handleSubmit?.(values);
}
async function handleReset(e: Event) {
@@ -75,59 +81,22 @@ 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(actionWrapperClass)">
<div
:class="
cn(
'col-span-full w-full text-right',
rootProps.compact ? 'pb-2' : 'pb-6',
rootProps.actionWrapperClass,
)
"
:style="queryFormStyle"
>
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
@@ -135,6 +104,7 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@@ -149,6 +119,7 @@ defineExpose({
<component
:is="COMPONENT_MAP.DefaultButton"
v-if="resetButtonOptions.show"
class="ml-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
@@ -163,6 +134,7 @@ defineExpose({
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
@@ -175,9 +147,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 { isRef, toRaw } from 'vue';
import { toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import {
@@ -39,7 +39,6 @@ function getDefaultState(): VbenFormProps {
layout: 'horizontal',
resetButtonOptions: {},
schema: [],
scrollToFirstError: false,
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: {},
@@ -101,26 +100,9 @@ export class FormApi {
getFieldComponentRef<T = ComponentPublicInstance>(
fieldName: string,
): T | undefined {
let target = this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)
return this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as T)
: 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;
}
/**
@@ -254,41 +236,6 @@ 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);
@@ -413,21 +360,14 @@ 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, errors } = await form.validate();
const { valid } = await form.validate();
if (!valid) {
if (this.state?.scrollToFirstError) {
this.scrollToFirstError(errors);
}
return;
}
return await this.submitForm();
@@ -439,10 +379,6 @@ 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 = computed(() => formRenderProps.compact);
const compact = 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-4': !compact,
'pb-6': !compact,
'pb-2': compact,
}"
class="relative flex"
@@ -386,7 +386,7 @@ onUnmounted(() => {
</div>
<Transition name="slide-up" v-if="!compact">
<FormMessage class="absolute" />
<FormMessage class="absolute bottom-1" />
</Transition>
</div>
</FormItem>

View File

@@ -12,12 +12,7 @@ import type {
import { computed } from 'vue';
import { Form } from '@vben-core/shadcn-ui';
import {
cn,
isFunction,
isString,
mergeWithArrayOverride,
} from '@vben-core/shared/utils';
import { cn, isString, mergeWithArrayOverride } from '@vben-core/shared/utils';
import { provideFormRenderProps } from './context';
import { useExpandable } from './expandable';
@@ -41,16 +36,6 @@ 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);
@@ -125,17 +110,6 @@ 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,
@@ -159,7 +133,7 @@ const computedSchema = computed(
'flex-shrink-0',
{ hidden },
formItemClass,
resolvedSchemaFormItemClass,
schema.formItemClass,
),
labelClass: cn(labelClass, schema.labelClass),
};
@@ -170,7 +144,7 @@ const computedSchema = computed(
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<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' | 'inline' | 'vertical';
export type FormLayout = 'horizontal' | 'vertical';
export type BaseFormComponentType =
| 'DefaultButton'
@@ -174,10 +174,10 @@ export interface FormCommonConfig {
*/
formFieldProps?: FormFieldOptions;
/**
* 所有表单项的栅格布局,支持函数形式
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: (() => string) | string;
formItemClass?: string;
/**
* 隐藏所有表单项label
* @default false
@@ -354,15 +354,6 @@ export interface VbenFormProps<
* 操作按钮是否反转(提交按钮前置)
*/
actionButtonsReverse?: boolean;
/**
* 操作按钮组的样式
* newLine: 在新行显示。rowEnd: 在行内显示靠右对齐默认。inline: 使用grid默认样式
*/
actionLayout?: 'inline' | 'newLine' | 'rowEnd';
/**
* 操作按钮组显示位置,默认靠右显示
*/
actionPosition?: 'center' | 'left' | 'right';
/**
* 表单操作区域class
*/
@@ -396,12 +387,6 @@ 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, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
import { object } from 'zod';
import { getDefaultsForSchema } from 'zod-defaults';
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
@@ -52,12 +52,7 @@ 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;
}
}
});
@@ -69,38 +64,6 @@ 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