78 Commits

Author SHA1 Message Date
dap
a10a981fab Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-30 11:13:25 +08:00
Jin Mao
20c15f352f 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-04-29 18:15:12 +08:00
Netfan
8aa7dabeff fix: calculation for collapsing search form is incorrect while initially hidden (#6068)
* 修复当默认隐藏搜索表单时,折叠位置的计算不正确的问题
2025-04-28 23:20:33 +08:00
vben
78c7c1589a chore: update readme.md 2025-04-28 23:11:34 +08:00
Vben
dd833ca56b 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-04-28 23:08:05 +08:00
vem
681c1dc267 fix: Update existing route index to prevent 404 on user switch (#6003)
Co-authored-by: tars-macmini <vem@qq.com>
2025-04-28 18:19:47 +08:00
Netfan
4545422ee0 fix: lock state will not change overflow style in drawer and modal (#6067)
* Modal和Drawer的锁定状态不再修改overflow样式
2025-04-28 17:02:54 +08:00
dap
5f26f5662e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-28 13:19:57 +08:00
Gahotx
ca94ca906f fix: add rounded corners to project and quick nav items (#5296) 2025-04-27 22:50:42 +08:00
Vben
76de450c71 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-04-27 22:06:49 +08:00
Trivikram Kamat
dd2b1ed580 fix: install corepack from npm (#5905)
* fix: install corepack from npm

* docs: install corepack from npm
2025-04-27 22:03:35 +08:00
ming4762
baec89f896 perf: resolve duplicate component names (#6039) 2025-04-27 22:02:38 +08:00
vben
7c7051a11e chore: release v5.5.5 2025-04-27 21:45:10 +08:00
Netfan
aa27a2f7a1 feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密
* 将锁屏密码合并到accessStore中进行加密
2025-04-27 20:59:10 +08:00
Jin Mao
9ee6d06d50 docs: add deepWiki doc link (#6057) 2025-04-27 20:54:07 +08:00
ming4762
0cc1cb5a7b 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-04-27 11:26:50 +08:00
dap
e662681ce2 fix: 拖拽上传在单文件时的样式 2025-04-27 09:57:16 +08:00
Netfan
0a9fc4e02d fix: title of search button in vxeTable toolbar (#6046)
* 修改vxeTable工具栏里的搜索按钮的提示文案
2025-04-26 01:08:41 +08:00
Netfan
be840460d8 feat: vbenSelect support prop allowClear (#6043) 2025-04-25 23:37:03 +08:00
Netfan
cb45987fe2 docs: update example (#6036)
* 跟进后端菜单逻辑的修改,现已无需传递basicLayout布局
2025-04-25 11:44:47 +08:00
panda7
5ffd7db8e0 fix: the initial value echo for the check-button-group (#6029)
Co-authored-by: sqchen <9110848@qq.com>
2025-04-25 08:35:03 +08:00
Netfan
14377705e7 fix: alert confirm state in beforeClose callback (#6019) 2025-04-23 12:20:52 +08:00
dap
23503778d4 chore: 去除vxe的锁定 2025-04-22 17:56:22 +08:00
dap
f54fab0bae Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-22 17:55:44 +08:00
pangyajun123
b985ff0584 fix: vxe-table theme token follow primary color (#6007) 2025-04-21 19:15:05 +08:00
dap
eff2f2a0b1 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-20 09:14:37 +08:00
dap
664fa800cd chore: 搜定vxe版本 2025-04-20 09:13:08 +08:00
dap
5dc4448c01 docs: changelog 2025-04-20 09:06:32 +08:00
dap
ccfe992779 chore: 暂时锁定vxe版本(样式问题) 2025-04-20 09:05:57 +08:00
dap
583504495d feat: 对模板的说明... 2025-04-20 08:53:50 +08:00
dap
7fb4bf3431 fix: 工作流list展示在开启缩放会有误差导致触底逻辑不会触发 2025-04-20 08:42:19 +08:00
wyc001122
b148b8ec92 fix: fix geader menu activation path (#5997)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-19 14:35:33 +08:00
Netfan
79de6bcbf7 fix: alert send wrong confirm state to beforeClose (#5991)
* 修复alert在按下Esc或者点击遮罩关闭时,可能发送错误的isConfirm状态
2025-04-17 22:23:05 +08:00
Netfan
14bd6dd25d fix: destroyOnClose works within connectedComponent (#5989)
* 修复destroyOnClose没能销毁connectedComponent自身的问题
2025-04-17 20:25:49 +08:00
dap
9b577261e2 chore: vite6.3.1已经修复开发/打包问题 解除版本锁定 2025-04-17 14:21:47 +08:00
PIPEDREA_WZJ
7f269e0d69 Update tailwindcss.md (#5602)
tailwindcss最新的版本已经是v4.x,vben中使用的是3.x的tailwindcss。在未进行兼容前,会出现运行失败的问题
2025-04-17 14:01:39 +08:00
yuh
4baec83db5 feat: add examples: form-upload (#5955)
* feat: add examples: form-upload

* fix: upload: accept and label

* fix: upload: 设置表单值、图片预览
2025-04-17 14:00:46 +08:00
dap
7d8416890b docs: changelog 2025-04-16 21:53:29 +08:00
dap
2e2ffcd59e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 21:33:11 +08:00
dap
2046bfa846 chore: 暂时锁定vite版本 会导致i18n插件打包失败 2025-04-16 19:57:07 +08:00
dap
0446adf778 refactor: 菜单图标更新 2025-04-16 17:38:11 +08:00
Netfan
f7a4d13a4c fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-04-16 14:11:04 +08:00
dap
e587256425 update: placeholder update 2025-04-16 13:53:58 +08:00
Netfan
0936861da1 feat: pass fieldsChanged into the handleValuesChange callback function (#5968)
* fieldsChanged(已被改变值的字段名)将传入handleValuesChange回调函数
2025-04-16 11:29:01 +08:00
ming4762
3318d76bab perf: improve destroyOnClose for VbenModal (#5964) 2025-04-16 11:28:36 +08:00
LinaBell
8f3881eabf perf: beforeClose of drawer support promise (#5932)
* perf: the beforeClose function of drawer is consistent with that of modal

* refactor: drawer test update
2025-04-16 11:27:13 +08:00
zhouda1fu
5252480b09 fix: missing await in department form(#5967) 2025-04-16 11:22:59 +08:00
dap
f096dfc6e6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 10:09:48 +08:00
Netfan
d18f56177c docs: update alert and apiComponent docs (#5961) 2025-04-15 20:52:23 +08:00
wyc001122
333998b518 fix: determine if scrollbar has been totally scrolled (#5934)
* 修复在系统屏幕缩放比例不为100%的情况下,滚动组件对是否已滚动到边界的判断可能不正确的问题
2025-04-15 20:51:38 +08:00
ming4762
3fb4fba1cb fix: modal closing animation (#5960) 2025-04-15 18:49:57 +08:00
ming4762
c7e6210c8d feat: modal&drawer support center-footer slot (#5956) 2025-04-15 16:04:44 +08:00
lztb
d864085c13 feat: vben-form添加arrayToStringFields属性 (#5957)
* feat: vben-form添加arrayToStringFields属性

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

---------

Co-authored-by: 米山 <17726957223@189.cn>
2025-04-15 16:03:20 +08:00
Netfan
fcdc1a1602 feat: add more expose methods for apiComponent (#5958)
* 为ApiComponent组件添加getOptions和getValue导出方法。
2025-04-15 15:32:30 +08:00
Netfan
bf7496f0d5 feat: add useAlertContext for Alert component (#5947)
* 新增Alert的子组件中获取弹窗上下文的能力
2025-04-15 00:00:05 +08:00
Netfan
9700150653 fix: table actions in fixed column (#5945) 2025-04-14 19:56:52 +08:00
Netfan
f0e9e55af2 feat: alert support customize footer (#5940)
* Alert组件支持自定义footer
2025-04-14 11:48:21 +08:00
Netfan
ff88274554 fix: long navigation menu can be scrolled (#5939)
* 修复超长的导航菜单无法纵向滚动的问题
2025-04-14 11:18:33 +08:00
ming4762
afce9dc5c0 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-04-13 23:02:07 +08:00
ming4762
b5700bd0b1 perf: improve autoSelect of ApiComponent (#5936)
* fix: 修复autoSelect不生效的问题,props.valueField已经被omit了

* feat: ApiComponent autoSelect支持使用函数,可以满足灵活性要求更高的场景
2025-04-13 20:03:18 +08:00
dap
e085083e42 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 22:28:23 +08:00
dap
a47910f650 refactor: 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况) 2025-04-12 15:01:28 +08:00
Netfan
a8c4786311 feat: api-component support autoSelect prop (#5931)
* feat: api-component support autoSelect prop

* docs: add version requirement
2025-04-12 14:02:35 +08:00
Netfan
2971ccc0b7 docs: docs modal z-index fixed, update alert docs (#5930) 2025-04-12 13:41:40 +08:00
dap
4ead56eaf1 fix: onClosed 2025-04-12 10:44:53 +08:00
dap
4fad8d77de refactor: 角色管理 auto 2025-04-12 10:42:16 +08:00
dap
9db1087d32 update: 岗位 useBeforeCloseDiff 2025-04-12 10:38:14 +08:00
Netfan
4a2c7b313f fix: alert animation (#5927) 2025-04-12 10:37:47 +08:00
dap
0f5fc5f54c fix: onClosed 2025-04-12 10:34:44 +08:00
dap
76108e7b8f refactor: 宽度设置为auto(根据子元素宽度动态变化) 2025-04-12 10:31:43 +08:00
dap
6018817906 chore: version动态获取 2025-04-12 10:20:12 +08:00
dap
7e4bdf7bd6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 09:56:51 +08:00
dap
32117574f6 chore: version update 2025-04-12 09:54:50 +08:00
dap
a48dfa1de2 fix: 新增dictType不显示 2025-04-12 09:44:27 +08:00
Netfan
36bf6fc149 fix: builtin color change throttled in preference drawer (#5924)
修复偏好设置中的自定义主题色拖动选择颜色时页面会明显卡顿的问题
2025-04-12 01:44:08 +08:00
Netfan
f46ec30995 fix: theme mode follow the system only auto (#5923)
* 修复主题在未设置为auto时,仍然会跟随系统主题变化的问题。
2025-04-12 01:16:57 +08:00
Netfan
9bd5a190c2 fix: alert action button focus, fixed #5921 (#5922)
* 修复Alert组件的按钮焦点切换问题
2025-04-12 00:59:56 +08:00
zhang
86da3cedc2 chore: 导出框架自带的组件,方便独立页面使用 (#5876) 2025-04-09 16:16:56 +08:00
197 changed files with 2310 additions and 922 deletions

View File

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

View File

@@ -15,6 +15,6 @@ export default {
], ],
'package.json': ['prettier --cache --write'], 'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [ '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json', 'prettier --cache --write --parser json',
], ],
}; };

View File

@@ -1 +1 @@
20.14.0 22.1.0

View File

@@ -14,7 +14,7 @@
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.cursorBlinking": "expand", "editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": false, "editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off", "editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on", "editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
@@ -91,6 +91,7 @@
"**/bower_components": true, "**/bower_components": true,
"**/.turbo": true, "**/.turbo": true,
"**/.idea": true, "**/.idea": true,
"**/.vitepress": true,
"**/tmp": true, "**/tmp": true,
"**/.git": true, "**/.git": true,
"**/.svn": true, "**/.svn": true,
@@ -113,6 +114,8 @@
"**/yarn.lock": true "**/yarn.lock": true
}, },
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search // search
"search.searchEditor.singleClickBehaviour": "peekDefinition", "search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false, "search.followSymlinks": false,

View File

@@ -1,3 +1,24 @@
# 1.3.3
**BUG FIX**
- 工作流list展示在开启缩放会有误差导致触底逻辑不会触发
**OTHER**
- 代码生成预览对模板的提示...(下载都懒得点一下吗)
# 1.3.2
**REFACTOR**
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
- 菜单图标更新了一部分 sql同步更新
**OTHER**
- 暂时锁死vite依赖 i18n会报错
# 1.3.1 # 1.3.1
**REFACTOR** **REFACTOR**

View File

@@ -1,8 +1,13 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br> <div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1> <h1>Vue Vben Admin</h1>
</div> </div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
@@ -15,27 +20,27 @@ Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術
## アップグレード通知 ## アップグレード通知
これは最新バージョン5.0であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。 これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
## 特徴 ## 特徴
- **最新技術スタック**: Vue 3やViteなどの最先端フロントエンド技術で開発 - **最新技術スタック**Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**: アプリケーション規模のJavaScriptのための言語 - **TypeScript**アプリケーション規模のJavaScriptのための言語
- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富 - **テーマ**複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**: 完全な内蔵国際化サポート - **国際化**完全な内蔵国際化サポート
- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵 - **権限管理**動的ルートベースの権限生成ソリューションを内蔵
## プレビュー ## プレビュー
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト - [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
テストアカウント: vben/123456 テストアカウントvben/123456
<p align="center"> <div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</p> </div>
### Gitpodを使用 ### Gitpodを使用
@@ -49,30 +54,27 @@ GitpodGitHub用の無料オンライン開発環境でプロジェクト
## インストールと使用 ## インストールと使用
- プロジェクトコードを取得 1. プロジェクトコードを取得
```bash ```bash
git clone https://github.com/vbenjs/vue-vben-admin.git git clone https://github.com/vbenjs/vue-vben-admin.git
``` ```
- 依存関係のインストール 2. 依存関係のインストール
```bash ```bash
cd vue-vben-admin cd vue-vben-admin
npm i -g corepack
corepack enable
pnpm install pnpm install
``` ```
- 実行 3. 実行
```bash ```bash
pnpm dev pnpm dev
``` ```
- ビルド 4. ビルド
```bash ```bash
pnpm build pnpm build
@@ -86,40 +88,39 @@ pnpm build
ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。 ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。
**Pull Request:** **Pull Request プロセス:**
1. コードをフォーク 1. コードをフォーク
2. 自分のブランチを作成: `git checkout -b feat/xxxx` 2. 自分のブランチを作成`git checkout -b feat/xxxx`
3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'` 3. 変更をコミット`git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ: `git push origin feat/xxxx` 4. ブランチをプッシュ`git push origin feat/xxxx`
5. `pull request`を送信 5. `pull request`を送信
## Git貢献提出規則 ## Git貢献提出規則
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 新機能の追加 - `feat` 新機能の追加
- `fix` 問題/バグの修正 - `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない - `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上 - `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング - `refactor` リファクタリング
- `revert` 変更の取り消し - `revert` 変更の取り消し
- `test` テスト関連 - `test` テスト関連
- `docs` ドキュメント/注釈 - `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など - `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション - `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更 - `types` 型定義ファイルの変更
- `wip` 開発中
## ブラウザサポート ## ブラウザサポート
ローカル開発には`Chrome 80+`ブラウザを推奨します ローカル開発には `Chrome 80+` ブラウザを推奨します
モダンブラウザをサポートし、IEはサポートしません モダンブラウザをサポートし、IEはサポートしません
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: | | :-: | :-: | :-: | :-: |
| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン | | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## メンテナー ## メンテナー
@@ -140,8 +141,7 @@ pnpm build
## 貢献者 ## 貢献者
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" <img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a> </a>
## Discord ## Discord

View File

@@ -1,8 +1,13 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br> <div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1> <h1>Vue Vben Admin</h1>
</div> </div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
@@ -17,7 +22,7 @@ Vue Vben Admin is a free and open source middle and back-end template. Using the
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2). This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
## Feature ## Features
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite - **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
- **TypeScript**: A language for application-scale JavaScript - **TypeScript**: A language for application-scale JavaScript
@@ -31,11 +36,11 @@ This is the latest version, 5.0, and it is not compatible with previous versions
Test Account: vben/123456 Test Account: vben/123456
<p align="center"> <div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png"> <img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</p> </div>
### Use Gitpod ### Use Gitpod
@@ -47,31 +52,29 @@ Open the project in Gitpod (free online dev environment for GitHub) and start co
[Document](https://doc.vben.pro/) [Document](https://doc.vben.pro/)
## Install and use ## Install and Use
- Get the project code 1. Get the project code
```bash ```bash
git clone https://github.com/vbenjs/vue-vben-admin.git git clone https://github.com/vbenjs/vue-vben-admin.git
``` ```
- Installation dependencies 2. Install dependencies
```bash ```bash
cd vue-vben-admin cd vue-vben-admin
npm i -g corepack
corepack enable
pnpm install pnpm install
``` ```
- run 3. Run
```bash ```bash
pnpm dev pnpm dev
``` ```
- build 4. Build
```bash ```bash
pnpm build pnpm build
@@ -81,44 +84,43 @@ pnpm build
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) [CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## How to contribute ## How to Contribute
You are very welcome to join[Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) Or submit a Pull Request You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
**Pull Request:** **Pull Request Process:**
1. Fork code! 1. Fork the code
2. Create your own branch: `git checkout -b feat/xxxx` 2. Create your branch: `git checkout -b feat/xxxx`
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'` 3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
4. Push your branch: `git push origin feat/xxxx` 4. Push your branch: `git push origin feat/xxxx`
5. submit`pull request` 5. Submit `pull request`
## Git Contribution submission specification ## Git Contribution Submission Specification
- reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` Add new features - `feat` Add new features
- `fix` Fix the problem/BUG - `fix` Fix the problem/BUG
- `style` The code style is related and does not affect the running result - `style` The code style is related and does not affect the running result
- `perf` Optimization/performance improvement - `perf` Optimization/performance improvement
- `refactor` Refactor - `refactor` Refactor
- `revert` Undo edit - `revert` Undo edit
- `test` Test related - `test` Test related
- `docs` Documentation/notes - `docs` Documentation/notes
- `chore` Dependency update/scaffolding configuration modification etc. - `chore` Dependency update/scaffolding configuration modification etc.
- `ci` Continuous integration - `ci` Continuous integration
- `types` Type definition file changes - `types` Type definition file changes
- `wip` In development
## Browser support ## Browser Support
The `Chrome 80+` browser is recommended for local development The `Chrome 80+` browser is recommended for local development
Support modern browsers, not IE Support modern browsers, not IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: | | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer ## Maintainer
@@ -136,11 +138,10 @@ If you think this project is helpful to you, you can help the author buy a cup o
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## Contributor ## Contributors
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" <img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a> </a>
## Discord ## Discord

View File

@@ -76,7 +76,7 @@ admin 账号: admin admin123
git clone https://gitee.com/dapppp/ruoyi-plus-vben5.git git clone https://gitee.com/dapppp/ruoyi-plus-vben5.git
``` ```
- 安装依赖 2. 安装依赖
```bash ```bash
cd ruoyi-plus-vben5 cd ruoyi-plus-vben5
@@ -150,7 +150,7 @@ VITE_GLOB_WEBSOCKET_ENABLE=false
pnpm dev:antd pnpm dev:antd
``` ```
- 打包 4. 打包
```bash ```bash
pnpm build:antd pnpm build:antd
@@ -164,21 +164,21 @@ pnpm build:antd
## Git 贡献提交规范 ## Git 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能 - `feat` 增加新功能
- `fix` 修复问题/BUG - `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的 - `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升 - `perf` 优化/性能提升
- `refactor` 重构 - `refactor` 重构
- `revert` 撤销修改 - `revert` 撤销修改
- `test` 测试相关 - `test` 测试相关
- `docs` 文档/注释 - `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等 - `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进 - `workflow` 工作流改进
- `ci` 持续集成 - `ci` 持续集成
- `types` 类型定义文件更改 - `types` 类型定义文件更改
- `wip` 开发中 - `wip` 开发中
## 浏览器支持 ## 浏览器支持
@@ -186,7 +186,7 @@ pnpm build:antd
本地开发推荐使用`Chrome` 最新版本浏览器 本地开发推荐使用`Chrome` 最新版本浏览器
支持现代浏览器, 不支持 IE 支持现代浏览器不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: | | :-: | :-: | :-: | :-: | :-: |

View File

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

View File

@@ -7,6 +7,7 @@ export default defineEventHandler(() => {
<li><a href="/api/menu">/api/menu/all</a></li> <li><a href="/api/menu">/api/menu/all</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li> <li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/auth/login">/api/auth/login</a></li> <li><a href="/api/auth/login">/api/auth/login</a></li>
<li><a href="/api/upload">/api/upload</a></li>
</ul> </ul>
`; `;
}); });

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { import {
computed,
defineAsyncComponent, defineAsyncComponent,
defineComponent, defineComponent,
getCurrentInstance, getCurrentInstance,
@@ -88,18 +87,13 @@ const withDefaultPlaceholder = <T extends Component>(
componentProps: Recordable<any> = {}, componentProps: Recordable<any> = {},
) => { ) => {
return defineComponent({ return defineComponent({
inheritAttrs: false,
name: component.name, name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => { setup: (props: any, { attrs, expose, slots }) => {
/** const placeholder =
* 需要使用computed 否则后续updateSchema更新的placeholder无法显示(响应式问题) props?.placeholder ||
*/ attrs?.placeholder ||
const placeholder = computed( $t(`ui.placeholder.${type}`);
() =>
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`),
);
// 透传组件暴露的方法 // 透传组件暴露的方法
const innerRef = ref(); const innerRef = ref();
@@ -118,7 +112,7 @@ const withDefaultPlaceholder = <T extends Component>(
component, component,
{ {
...componentProps, ...componentProps,
placeholder: placeholder.value, placeholder,
...props, ...props,
...attrs, ...attrs,
ref: innerRef, ref: innerRef,
@@ -168,20 +162,34 @@ async function initComponentAdapter() {
// 如果你的组件体积比较大,可以使用异步加载 // 如果你的组件体积比较大,可以使用异步加载
// Button: () => // Button: () =>
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', { ApiSelect: withDefaultPlaceholder(
component: Select, {
loadingSlot: 'suffixIcon', ...ApiComponent,
visibleEvent: 'onDropdownVisibleChange', name: 'ApiSelect',
modelPropName: 'value', },
}), 'select',
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { {
component: TreeSelect, component: Select,
fieldNames: { label: 'label', value: 'value', children: 'children' }, loadingSlot: 'suffixIcon',
loadingSlot: 'suffixIcon', visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value', modelPropName: 'value',
optionsPropName: 'treeData', },
visibleEvent: 'onVisibleChange', ),
}), ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete, AutoComplete,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,

View File

@@ -98,7 +98,7 @@ Upload.Dragger只会影响样式
{{ $t('component.upload.upload') }} {{ $t('component.upload.upload') }}
</a-button> </a-button>
</div> </div>
<div v-if="enableDragUpload && innerFileList?.length < maxCount"> <div v-if="enableDragUpload">
<p class="ant-upload-drag-icon"> <p class="ant-upload-drag-icon">
<InboxOutlined /> <InboxOutlined />
</p> </p>

View File

@@ -2,6 +2,11 @@ import type { RouteRecordStringComponent } from '@vben/types';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
const {
version,
// vite inject-metadata 插件注入的全局变量
} = __VBEN_ADMIN_METADATA__ || {};
/** /**
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面 * 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
*/ */
@@ -134,8 +139,8 @@ export const localMenuList: RouteRecordStringComponent[] = [
icon: 'lucide:book-open-text', icon: 'lucide:book-open-text',
keepAlive: true, keepAlive: true,
title: '更新记录', title: '更新记录',
badge: '1.3.0', badge: `当前: ${version}`,
badgeVariants: '#CC0033', badgeVariants: 'bg-primary',
}, },
}, },
], ],

View File

@@ -51,7 +51,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -60,7 +60,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -79,6 +79,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 120, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -86,6 +86,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 120, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -95,7 +95,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -71,7 +71,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -39,11 +39,13 @@ export const columns: VxeGridProps['columns'] = [
{ {
field: 'orderNum', field: 'orderNum',
title: '排序', title: '排序',
width: 180, resizable: false,
width: 'auto',
}, },
{ {
field: 'status', field: 'status',
width: 180, resizable: false,
width: 'auto',
title: '状态', title: '状态',
slots: { slots: {
default: ({ row }) => { default: ({ row }) => {

View File

@@ -45,7 +45,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -77,7 +77,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const { dictCode, dictType } = drawerApi.getData() as DrawerProps; const { dictCode, dictType } = drawerApi.getData() as DrawerProps;
isUpdate.value = !!dictCode; isUpdate.value = !!dictCode;
formApi.setFieldValue('dictType', dictType); await formApi.setFieldValue('dictType', dictType);
if (dictCode && isUpdate.value) { if (dictCode && isUpdate.value) {
const record = await dictDetailInfo(dictCode); const record = await dictDetailInfo(dictCode);

View File

@@ -39,7 +39,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -145,7 +145,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 200, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -69,7 +69,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -81,7 +81,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -69,7 +69,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -65,7 +65,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -8,6 +8,7 @@ import { addFullName, cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { postAdd, postInfo, postUpdate } from '#/api/system/post'; import { postAdd, postInfo, postUpdate } from '#/api/system/post';
import { getDeptTree } from '#/api/system/user'; import { getDeptTree } from '#/api/system/user';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -50,8 +51,16 @@ async function setupDeptSelect() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -67,31 +76,33 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const record = await postInfo(id); const record = await postInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
} }
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? postUpdate(data) : postAdd(data)); await (isUpdate.value ? postUpdate(data) : postAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>

View File

@@ -37,6 +37,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -94,7 +94,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -226,7 +226,11 @@ function handleAssignRole(record: Role) {
</MenuItem> </MenuItem>
</Menu> </Menu>
</template> </template>
<a-button size="small" type="link"> <a-button
size="small"
type="link"
v-access:code="'system:role:edit'"
>
{{ $t('pages.common.more') }} {{ $t('pages.common.more') }}
</a-button> </a-button>
</Dropdown> </Dropdown>

View File

@@ -52,7 +52,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
onBeforeClose, onBeforeClose,
onCancel: handleClosed, onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
onOpenChange: async (isOpen) => { onOpenChange: async (isOpen) => {
if (!isOpen) { if (!isOpen) {

View File

@@ -68,7 +68,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 200, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -29,7 +29,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -87,7 +87,7 @@ export const columns: VxeGridProps['columns'] = [
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
resizable: false, resizable: false,
width: 180, width: 'auto',
}, },
]; ];

View File

@@ -20,7 +20,7 @@ import {
} from '@vben/icons'; } from '@vben/icons';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { Skeleton, Tree } from 'ant-design-vue'; import { Alert, Skeleton, Tree } from 'ant-design-vue';
import { previewCode } from '#/api/tool/gen'; import { previewCode } from '#/api/tool/gen';
@@ -185,6 +185,11 @@ const { copy } = useClipboard({ legacy: true });
</div> </div>
</template> </template>
</Tree> </Tree>
<Alert
class="mt-2"
show-icon
message="👆显示的名称为模板的文件名,非最终下载文件名..."
/>
</div> </div>
<CodeMirror <CodeMirror
v-model="codeContent" v-model="codeContent"

View File

@@ -33,7 +33,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 200, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -91,7 +91,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 210, resizable: false,
width: 'auto',
}, },
]; ];

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PropType } from 'vue';
import type { CategoryTree } from '#/api/workflow/category/model'; import type { CategoryTree } from '#/api/workflow/category/model';
import { onMounted, type PropType, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue'; import { SyncOutlined } from '@ant-design/icons-vue';
import { InputSearch, Skeleton, Tree } from 'ant-design-vue'; import { InputSearch, Skeleton, Tree } from 'ant-design-vue';

View File

@@ -63,7 +63,7 @@ export const columns: VxeGridProps['columns'] = [
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
resizable: false, resizable: false,
width: 200, width: 'auto',
}, },
]; ];

View File

@@ -75,7 +75,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
const [BasicDrawer, modalApi] = useVbenModal({ const [BasicDrawer, modalApi] = useVbenModal({
onBeforeClose, onBeforeClose,
onCancel: handleClosed, onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {

View File

@@ -28,6 +28,7 @@ import { categoryTree } from '#/api/workflow/category';
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task'; import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components'; import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
import { bottomOffset } from './constant';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -140,7 +141,9 @@ const handleScroll = debounce(async (e: Event) => {
// e.target.scrollHeight 是元素的总高度。 // e.target.scrollHeight 是元素的总高度。
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement; const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
// 判断是否滚动到底部 // 判断是否滚动到底部
const isBottom = scrollTop + clientHeight >= scrollHeight; const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
console.log('scrollTop + clientHeight', scrollTop + clientHeight);
console.log('scrollHeight', scrollHeight);
// 滚动到底部且没有加载完成 // 滚动到底部且没有加载完成
if (isBottom && !isLoadComplete.value) { if (isBottom && !isLoadComplete.value) {

View File

@@ -0,0 +1,7 @@
/**
* 底部偏移量
* 在缩放时会差大概0.5px 导致触底逻辑不会触发
* 在这里设置手动补偿
* @see https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC28RE#note_40175381
*/
export const bottomOffset = 2;

View File

@@ -24,6 +24,7 @@ import { cloneDeep, debounce } from 'lodash-es';
import { pageByCurrent } from '#/api/workflow/instance'; import { pageByCurrent } from '#/api/workflow/instance';
import { ApprovalCard, ApprovalPanel } from '../components'; import { ApprovalCard, ApprovalPanel } from '../components';
import { bottomOffset } from './constant';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -95,7 +96,7 @@ const handleScroll = debounce(async (e: Event) => {
// e.target.scrollHeight 是元素的总高度。 // e.target.scrollHeight 是元素的总高度。
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement; const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
// 判断是否滚动到底部 // 判断是否滚动到底部
const isBottom = scrollTop + clientHeight >= scrollHeight; const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
// 滚动到底部且没有加载完成 // 滚动到底部且没有加载完成
if (isBottom && !isLoadComplete.value) { if (isBottom && !isLoadComplete.value) {

View File

@@ -26,6 +26,7 @@ import { categoryTree } from '#/api/workflow/category';
import { pageByTaskCopy } from '#/api/workflow/task'; import { pageByTaskCopy } from '#/api/workflow/task';
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components'; import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
import { bottomOffset } from './constant';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -99,7 +100,7 @@ const handleScroll = debounce(async (e: Event) => {
// e.target.scrollHeight 是元素的总高度。 // e.target.scrollHeight 是元素的总高度。
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement; const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
// 判断是否滚动到底部 // 判断是否滚动到底部
const isBottom = scrollTop + clientHeight >= scrollHeight; const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
// 滚动到底部且没有加载完成 // 滚动到底部且没有加载完成
if (isBottom && !isLoadComplete.value) { if (isBottom && !isLoadComplete.value) {

View File

@@ -26,6 +26,7 @@ import { categoryTree } from '#/api/workflow/category';
import { pageByTaskFinish } from '#/api/workflow/task'; import { pageByTaskFinish } from '#/api/workflow/task';
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components'; import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
import { bottomOffset } from './constant';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -99,7 +100,7 @@ const handleScroll = debounce(async (e: Event) => {
// e.target.scrollHeight 是元素的总高度。 // e.target.scrollHeight 是元素的总高度。
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement; const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
// 判断是否滚动到底部 // 判断是否滚动到底部
const isBottom = scrollTop + clientHeight >= scrollHeight; const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
// 滚动到底部且没有加载完成 // 滚动到底部且没有加载完成
if (isBottom && !isLoadComplete.value) { if (isBottom && !isLoadComplete.value) {

View File

@@ -27,6 +27,7 @@ import { categoryTree } from '#/api/workflow/category';
import { pageByTaskWait } from '#/api/workflow/task'; import { pageByTaskWait } from '#/api/workflow/task';
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components'; import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
import { bottomOffset } from './constant';
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE; const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -100,7 +101,7 @@ const handleScroll = debounce(async (e: Event) => {
// e.target.scrollHeight 是元素的总高度。 // e.target.scrollHeight 是元素的总高度。
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement; const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
// 判断是否滚动到底部 // 判断是否滚动到底部
const isBottom = scrollTop + clientHeight >= scrollHeight; const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
// 滚动到底部且没有加载完成 // 滚动到底部且没有加载完成
if (isBottom && !isLoadComplete.value) { if (isBottom && !isLoadComplete.value) {

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Space } from 'ant-design-vue'; import { Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { sseList } from './api'; import { sseList } from './api';
import sendMsgModal from './send-msg-modal.vue'; import sendMsgModal from './send-msg-modal.vue';
@@ -31,7 +33,8 @@ const gridOptions: VxeGridProps = {
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: '操作', title: '操作',
width: 180, resizable: false,
width: 'auto',
}, },
], ],
height: 'auto', height: 'auto',

View File

@@ -104,6 +104,11 @@
--vp-custom-block-tip-text: var(--vp-c-text-1); --vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft); --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
/**
* modal zIndex
*/
--popup-z-index: 1000;
} }
@media (min-width: 640px) { @media (min-width: 640px) {

View File

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

View File

@@ -12,6 +12,12 @@ Alert提供的功能与Modal类似但只适用于简单应用场景。例如
::: :::
::: tip 注意
Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下不支持HMR热更新代码变更后需要关闭这些弹窗后重新打开。
:::
::: tip README ::: tip README
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。 下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
@@ -32,6 +38,23 @@ Alert提供的功能与Modal类似但只适用于简单应用场景。例如
<DemoPreview dir="demos/vben-alert/prompt" /> <DemoPreview dir="demos/vben-alert/prompt" />
## useAlertContext
当弹窗的content、footer、icon使用自定义组件时在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
::: tip 注意
`useAlertContext`只能用在setup或者函数式组件中。
:::
### Methods
| 方法 | 描述 | 类型 | 版本要求 |
| --------- | ------------------ | -------- | -------- |
| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4 |
| doCancel | 调用弹窗的取消操作 | ()=>void | >5.5.4 |
## 类型说明 ## 类型说明
```ts ```ts
@@ -69,8 +92,14 @@ export type AlertProps = {
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/ /** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean; contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
showCancel?: boolean; showCancel?: boolean;
/** 弹窗标题 */ /** 弹窗标题 */

View File

@@ -131,26 +131,37 @@ function fetchApi(): Promise<Record<string, any>> {
### Props ### Props
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - | | modelValue(v-model) | 当前值 | `any` | - | - |
| component | 欲包装的组件(以下称为目标组件) | `Component` | - | | component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` | | numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | | api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | - |
| params | 传递给api的参数 | `Record<string, any>` | - | | params | 传递给api的参数 | `Record<string, any>` | - | - |
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | | resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
| labelField | label字段名 | `string` | `label` | | labelField | label字段名 | `string` | `label` | - |
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | | childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
| valueField | value字段名 | `string` | `value` | | valueField | value字段名 | `string` | `value` | - |
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | | optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
| modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` | | modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
| immediate | 是否立即调用api | `boolean` | `true` | | immediate | 是否立即调用api | `boolean` | `true` | - |
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | | alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | | beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | | afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | | options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | | visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | | loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
#### autoSelect 自动设置选项
如果当前值为undefined在选项数据成功加载之后自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
- `"first"`:自动选择第一个选项
- `"last"`:自动选择最后一个选项
- `"one"`:有且仅有一个选项时,自动选择它
- `自定义函数`自定义选择逻辑函数的参数为options返回值为选择的选项
- `false`:不自动选择选项
### Methods ### Methods
@@ -158,3 +169,5 @@ function fetchApi(): Promise<Record<string, any>> {
| --- | --- | --- | --- | | --- | --- | --- | --- |
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 | | getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
| updateParam | 设置接口请求参数将与params属性合并 | (newParams: Record<string, any>)=>void | >5.5.4 | | updateParam | 设置接口请求参数将与params属性合并 | (newParams: Record<string, any>)=>void | >5.5.4 |
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
| getValue | 获取当前值 | ()=>any | >5.5.4 |

View File

@@ -127,13 +127,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。 除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
| 插槽名 | 描述 | | 插槽名 | 描述 |
| -------------- | ------------------- | | -------------- | -------------------------------------------------- |
| default | 默认插槽 - 弹窗内容 | | default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 | | prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 | | center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
| close-icon | 关闭按钮图标 | | append-footer | 确认按钮右侧 |
| extra | 额外内容(标题右侧) | | close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
### drawerApi ### drawerApi

View File

@@ -310,7 +310,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| actionWrapperClass | 表单操作区域class | `any` | - | | actionWrapperClass | 表单操作区域class | `any` | - |
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - | | handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - | | handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - | | handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` | | actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - | | resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - | | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
@@ -325,6 +325,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false | | submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false | | compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
::: tip handleValuesChange
`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组包含了所有被改变的字段名。注意第二个参数仅在v5.5.4(不含)以上版本可用并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话请注意该参数并不会包含映射后的字段名。
:::
::: tip fieldMappingTime ::: tip fieldMappingTime
此属性用于将表单内的数组值映射成 2 个字段它应当传入一个数组数组的每一项是一个映射规则规则的第一个成员是一个字符串表示需要映射的字段名第二个成员是一个数组表示映射后的字段名第三个成员是一个可选的格式掩码用于格式化日期时间字段也可以提供一个格式化函数参数分别为当前值和当前字段名返回格式化后的值。如果明确地将格式掩码设为null则原值映射而不进行格式化适用于非日期时间字段。例如`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]``timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime``endTime`字段上。每一项的第三个参数是一个可选的格式掩码, 此属性用于将表单内的数组值映射成 2 个字段它应当传入一个数组数组的每一项是一个映射规则规则的第一个成员是一个字符串表示需要映射的字段名第二个成员是一个数组表示映射后的字段名第三个成员是一个可选的格式掩码用于格式化日期时间字段也可以提供一个格式化函数参数分别为当前值和当前字段名返回格式化后的值。如果明确地将格式掩码设为null则原值映射而不进行格式化适用于非日期时间字段。例如`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]``timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime``endTime`字段上。每一项的第三个参数是一个可选的格式掩码,

View File

@@ -59,8 +59,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
::: info 注意 ::: info 注意
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。另外,如果设置了`destroyOnClose`内部Modal及其子组件会在被关闭后<b>完全销毁</b>。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性如默认隐藏全屏按钮修改默认ZIndex等。 - 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性如默认隐藏全屏按钮修改默认ZIndex等。
::: :::
@@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
| --- | --- | --- | --- | | --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` | | appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - | | connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` | | destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| title | 标题 | `string\|slot` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |
@@ -138,11 +137,12 @@ const [Modal, modalApi] = useVbenModal({
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。 除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
| 插槽名 | 描述 | | 插槽名 | 描述 |
| -------------- | ------------------- | | -------------- | -------------------------------------------------- |
| default | 默认插槽 - 弹窗内容 | | default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 | | prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 | | center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
| append-footer | 确认按钮右侧 |
### modalApi ### modalApi

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h, ref } from 'vue';
import { alert, confirm, VbenButton } from '@vben/common-ui'; import { alert, confirm, VbenButton } from '@vben/common-ui';
import { Checkbox, message } from 'ant-design-vue';
function showConfirm() { function showConfirm() {
confirm('This is an alert message') confirm('This is an alert message')
.then(() => { .then(() => {
@@ -18,6 +22,34 @@ function showIconConfirm() {
}); });
} }
function showfooterConfirm() {
const checked = ref(false);
confirm({
cancelText: '不要虾扯蛋',
confirmText: '是的我们都是NPC',
content:
'刚才发生的事情,为什么我似乎早就经历过一般?\n我甚至能在事情发生过程中潜意识里预知到接下来会发生什么。\n\n听起来挺玄乎的你有过这种感觉吗',
footer: () =>
h(
Checkbox,
{
checked: checked.value,
class: 'flex-1',
'onUpdate:checked': (v) => (checked.value = v),
},
'不再提示',
),
icon: 'question',
title: '未解之谜',
}).then(() => {
if (checked.value) {
message.success('我不会再拿这个问题烦你了');
} else {
message.info('下次还要继续问你哟');
}
});
}
function showAsyncConfirm() { function showAsyncConfirm() {
confirm({ confirm({
beforeClose({ isConfirm }) { beforeClose({ isConfirm }) {
@@ -37,6 +69,7 @@ function showAsyncConfirm() {
<div class="flex gap-4"> <div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton> <VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton> <VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton> <VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div> </div>
</template> </template>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h } from 'vue'; import { h } from 'vue';
import { alert, prompt, VbenButton } from '@vben/common-ui'; import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
import { Input, RadioGroup } from 'ant-design-vue'; import { Input, RadioGroup, Select } from 'ant-design-vue';
import { BadgeJapaneseYen } from 'lucide-vue-next'; import { BadgeJapaneseYen } from 'lucide-vue-next';
function showPrompt() { function showPrompt() {
@@ -18,18 +18,32 @@ function showPrompt() {
}); });
} }
function showSelectPrompt() { function showSlotsPrompt() {
prompt({ prompt({
component: Input, component: () => {
componentProps: { // 获取弹窗上下文。注意只能在setup或者函数式组件中调用
placeholder: '请输入', const { doConfirm } = useAlertContext();
prefix: '充值金额', return h(
type: 'number', Input,
{
onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
// 调用弹窗提供的确认方法
doConfirm();
}
},
placeholder: '请输入',
prefix: '充值金额:',
type: 'number',
},
{
addonAfter: () => h(BadgeJapaneseYen),
},
);
}, },
componentSlots: { content:
addonAfter: () => h(BadgeJapaneseYen), '此弹窗演示了如何使用自定义插槽并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
},
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
icon: 'question', icon: 'question',
modelPropName: 'value', modelPropName: 'value',
}).then((val) => { }).then((val) => {
@@ -37,6 +51,29 @@ function showSelectPrompt() {
}); });
} }
function showSelectPrompt() {
prompt({
component: Select,
componentProps: {
options: [
{ label: 'Option A', value: 'Option A' },
{ label: 'Option B', value: 'Option B' },
{ label: 'Option C', value: 'Option C' },
],
placeholder: '请选择',
// 弹窗会设置body的pointer-events为none这回影响下拉框的点击事件
popupClassName: 'pointer-events-auto',
},
content: '此弹窗演示了如何使用component传递自定义组件',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
if (val) {
alert(`你选择了${val}`);
}
});
}
function sleep(ms: number) { function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
@@ -44,7 +81,6 @@ function sleep(ms: number) {
function showAsyncPrompt() { function showAsyncPrompt() {
prompt({ prompt({
async beforeClose(scope) { async beforeClose(scope) {
console.log(scope);
if (scope.isConfirm) { if (scope.isConfirm) {
if (scope.value) { if (scope.value) {
// 模拟异步操作如果不成功可以返回false // 模拟异步操作如果不成功可以返回false
@@ -75,6 +111,7 @@ function showAsyncPrompt() {
<template> <template>
<div class="flex gap-4"> <div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton> <VbenButton @click="showPrompt">Prompt</VbenButton>
<VbenButton @click="showSlotsPrompt"> Prompt With slots </VbenButton>
<VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton> <VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton> <VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
</div> </div>

View File

@@ -58,7 +58,7 @@ Open a terminal in your code directory and execute the following commands:
cd vue-vben-admin cd vue-vben-admin
# Enable the project-specified version of pnpm # Enable the project-specified version of pnpm
corepack enable npm i -g corepack
# Install dependencies # Install dependencies
pnpm install pnpm install

View File

@@ -114,8 +114,6 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
```ts ```ts
const dashboardMenus = [ const dashboardMenus = [
{ {
// 这里固定写死 BasicLayout不可更改
component: 'BasicLayout',
meta: { meta: {
order: -1, order: -1,
title: 'page.dashboard.title', title: 'page.dashboard.title',
@@ -144,6 +142,16 @@ const dashboardMenus = [
}, },
], ],
}, },
{
name: 'Test',
path: '/test',
component: '/test/index',
meta: {
title: 'page.test',
// 部分特殊页面如果不需要基础布局页面顶部和侧边栏可将noBasicLayout设置为true
noBasicLayout: true,
},
},
]; ];
``` ```

View File

@@ -58,7 +58,7 @@ git clone https://gitee.com/annsion/vue-vben-admin.git
cd vue-vben-admin cd vue-vben-admin
# 使用项目指定的pnpm版本进行依赖安装 # 使用项目指定的pnpm版本进行依赖安装
corepack enable npm i -g corepack
# 安装依赖 # 安装依赖
pnpm install pnpm install

View File

@@ -11,3 +11,7 @@
当前只有对应的包下面存在 `tailwind.config.mjs` 文件才会启用 tailwindcss 的编译,否则不会启用 tailwindcss。如果你是纯粹的 SDK 包,不需要使用 tailwindcss可以不用创建 `tailwind.config.mjs` 文件。 当前只有对应的包下面存在 `tailwind.config.mjs` 文件才会启用 tailwindcss 的编译,否则不会启用 tailwindcss。如果你是纯粹的 SDK 包,不需要使用 tailwindcss可以不用创建 `tailwind.config.mjs` 文件。
::: :::
## 提示
`tailwindcss`已至v4.x版本使用方法与`tailwindcss: ^3.4.17`有差异v4.0无法与v3.x版本兼容在开发前请确认`package.json`中的`tailwindcss`版本。

View File

@@ -20,6 +20,9 @@ hero:
- theme: alt - theme: alt
text: 在 GitHub 查看 text: 在 GitHub 查看
link: https://github.com/vbenjs/vue-vben-admin link: https://github.com/vbenjs/vue-vben-admin
- theme: alt
text: DeepWiki 文档
link: https://deepwiki.com/vbenjs/vue-vben-admin
features: features:
- icon: 🚀 - icon: 🚀

View File

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

View File

@@ -10,7 +10,15 @@ export async function vue(): Promise<Linter.Config[]> {
interopDefault(import('@typescript-eslint/parser')), interopDefault(import('@typescript-eslint/parser')),
] as const); ] as const);
const flatEssential = pluginVue.configs?.['flat/essential'] || [];
const flatStronglyRecommended =
pluginVue.configs?.['flat/strongly-recommended'] || [];
const flatRecommended = pluginVue.configs?.['flat/recommended'] || [];
return [ return [
...flatEssential,
...flatStronglyRecommended,
...flatRecommended,
{ {
files: ['**/*.vue'], files: ['**/*.vue'],
languageOptions: { languageOptions: {
@@ -43,12 +51,9 @@ export async function vue(): Promise<Linter.Config[]> {
plugins: { plugins: {
vue: pluginVue, vue: pluginVue,
}, },
processor: pluginVue.processors['.vue'], processor: pluginVue.processors?.['.vue'],
rules: { rules: {
...pluginVue.configs.base.rules, ...pluginVue.configs?.base?.rules,
...pluginVue.configs['vue3-essential'].rules,
...pluginVue.configs['vue3-strongly-recommended'].rules,
...pluginVue.configs['vue3-recommended'].rules,
'vue/attribute-hyphenation': [ 'vue/attribute-hyphenation': [
'error', 'error',
@@ -131,7 +136,6 @@ export async function vue(): Promise<Linter.Config[]> {
'vue/require-default-prop': 'error', 'vue/require-default-prop': 'error',
'vue/require-explicit-emits': 'error', 'vue/require-explicit-emits': 'error',
'vue/require-prop-types': 'off', 'vue/require-prop-types': 'off',
'vue/script-setup-uses-vars': 'error',
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'vue/space-infix-ops': 'error', 'vue/space-infix-ops': 'error',
'vue/space-unary-ops': ['error', { nonwords: false, words: true }], 'vue/space-unary-ops': ['error', { nonwords: false, words: true }],

View File

@@ -43,6 +43,7 @@ export default {
'stylelint-scss', 'stylelint-scss',
], ],
rules: { rules: {
'at-rule-no-deprecated': null,
'at-rule-no-unknown': [ 'at-rule-no-unknown': [
true, true,
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,149 +3,328 @@ import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
import type { PluginOptions } from 'vite-plugin-dts'; import type { PluginOptions } from 'vite-plugin-dts';
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa'; 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 { interface IImportMap {
/** 模块导入映射 */
imports?: Record<string, string>; imports?: Record<string, string>;
/** 作用域特定的导入映射 */
scopes?: { scopes?: {
[scope: string]: Record<string, string>; [scope: string]: Record<string, string>;
}; };
} }
/**
* 打印插件配置选项
* @description 用于配置控制台打印信息
*/
interface PrintPluginOptions { interface PrintPluginOptions {
/** /**
* 打印的数据 * 打印的数据映射
* @description 键值对形式的数据,将在控制台打印
* @example
* ```typescript
* {
* 'App Version': '1.0.0',
* 'Build Time': '2024-01-01'
* }
* ```
*/ */
infoMap?: Record<string, string | undefined>; infoMap?: Record<string, string | undefined>;
} }
/**
* Nitro Mock 插件配置选项
* @description 用于配置 Nitro Mock 服务器的行为
*/
interface NitroMockPluginOptions { interface NitroMockPluginOptions {
/** /**
* mock server 包名 * Mock 服务器包名
* @default '@vbenjs/nitro-mock'
*/ */
mockServerPackage?: string; mockServerPackage?: string;
/** /**
* mock 服务端口 * Mock 服务端口
* @default 3000
*/ */
port?: number; port?: number;
/** /**
* mock 日志是否打印 * 是否打印 Mock 日志
* @default false
*/ */
verbose?: boolean; verbose?: boolean;
} }
/**
* 归档插件配置选项
* @description 用于配置构建产物的压缩归档
*/
interface ArchiverPluginOptions { interface ArchiverPluginOptions {
/** /**
* 输出文件名 * 输出文件名
* @default dist * @default 'dist'
*/ */
name?: string; name?: string;
/** /**
* 输出目录 * 输出目录
* @default . * @default '.'
*/ */
outputDir?: string; outputDir?: string;
} }
/** /**
* importmap 插件配置 * ImportMap 插件配置
* @description 用于配置模块的 CDN 导入
*/ */
interface ImportmapPluginOptions { interface ImportmapPluginOptions {
/** /**
* CDN 供应商 * CDN 供应商
* @default jspm.io * @default 'jspm.io'
* @description 支持 esm.sh 和 jspm.io 两种 CDN 供应商
*/ */
defaultProvider?: 'esm.sh' | 'jspm.io'; defaultProvider?: 'esm.sh' | 'jspm.io';
/** importmap 配置 */ /**
* ImportMap 配置数组
* @description 配置需要从 CDN 导入的包
* @example
* ```typescript
* [
* { name: 'vue' },
* { name: 'pinia', range: '^2.0.0' }
* ]
* ```
*/
importmap?: Array<{ name: string; range?: string }>; importmap?: Array<{ name: string; range?: string }>;
/** 手动配置importmap */ /**
* 手动配置 ImportMap
* @description 自定义 ImportMap 配置
*/
inputMap?: IImportMap; inputMap?: IImportMap;
} }
/** /**
* 用于判断是否需要加载插件 * 条件插件配置
* @description 用于根据条件动态加载插件
*/ */
interface ConditionPlugin { interface ConditionPlugin {
// 判断条件 /**
* 判断条件
* @description 当条件为 true 时加载插件
*/
condition?: boolean; condition?: boolean;
// 插件对象 /**
* 插件对象
* @description 返回插件数组或 Promise
*/
plugins: () => PluginOption[] | PromiseLike<PluginOption[]>; plugins: () => PluginOption[] | PromiseLike<PluginOption[]>;
} }
/**
* 通用插件配置选项
* @description 所有插件共用的基础配置
*/
interface CommonPluginOptions { interface CommonPluginOptions {
/** 是否开启devtools */ /**
* 是否开启开发工具
* @default false
*/
devtools?: boolean; devtools?: boolean;
/** 环境变量 */ /**
* 环境变量
* @description 自定义环境变量
*/
env?: Record<string, any>; env?: Record<string, any>;
/** 是否注入metadata */ /**
* 是否注入元数据
* @default true
*/
injectMetadata?: boolean; injectMetadata?: boolean;
/** 是否构建模式 */ /**
* 是否为构建模式
* @default false
*/
isBuild?: boolean; isBuild?: boolean;
/** 构建模式 */ /**
* 构建模式
* @default 'development'
*/
mode?: string; mode?: string;
/** 开启依赖分析 */ /**
* 是否开启依赖分析
* @default false
* @description 使用 rollup-plugin-visualizer 分析依赖
*/
visualizer?: boolean | PluginVisualizerOptions; visualizer?: boolean | PluginVisualizerOptions;
} }
/**
* 应用插件配置选项
* @description 用于配置应用构建时的插件选项
*/
interface ApplicationPluginOptions extends CommonPluginOptions { interface ApplicationPluginOptions extends CommonPluginOptions {
/** 开启后会在打包dist同级生成dist.zip */ /**
* 是否开启压缩归档
* @default false
* @description 开启后会在打包目录生成 zip 文件
*/
archiver?: boolean; archiver?: boolean;
/** 压缩归档插件配置 */ /**
* 压缩归档插件配置
* @description 配置压缩归档的行为
*/
archiverPluginOptions?: ArchiverPluginOptions; archiverPluginOptions?: ArchiverPluginOptions;
/** 开启 gzip|brotli 压缩 */ /**
* 是否开启压缩
* @default false
* @description 支持 gzip 和 brotli 压缩
*/
compress?: boolean; compress?: boolean;
/** 压缩类型 */ /**
* 压缩类型
* @default ['gzip']
* @description 可选的压缩类型
*/
compressTypes?: ('brotli' | 'gzip')[]; compressTypes?: ('brotli' | 'gzip')[];
/** 在构建的时候抽离配置文件 */ /**
* 是否抽离配置文件
* @default false
* @description 在构建时抽离配置文件
*/
extraAppConfig?: boolean; extraAppConfig?: boolean;
/** 是否开启html插件 */ /**
* 是否开启 HTML 插件
* @default true
*/
html?: boolean; html?: boolean;
/** 是否开启i18n */ /**
* 是否开启国际化
* @default false
*/
i18n?: boolean; i18n?: boolean;
/** 是否开启 importmap CDN */ /**
* 是否开启 ImportMap CDN
* @default false
*/
importmap?: boolean; importmap?: boolean;
/** importmap 插件配置 */ /**
* ImportMap 插件配置
*/
importmapOptions?: ImportmapPluginOptions; importmapOptions?: ImportmapPluginOptions;
/** 是否注入app loading */ /**
* 是否注入应用加载动画
* @default true
*/
injectAppLoading?: boolean; injectAppLoading?: boolean;
/** 是否注入全局scss */ /**
* 是否注入全局 SCSS
* @default true
*/
injectGlobalScss?: boolean; injectGlobalScss?: boolean;
/** 是否注入版权信息 */ /**
* 是否注入版权信息
* @default true
*/
license?: boolean; license?: boolean;
/** 是否开启nitro mock */ /**
* 是否开启 Nitro Mock
* @default false
*/
nitroMock?: boolean; nitroMock?: boolean;
/** nitro mock 插件配置 */ /**
* Nitro Mock 插件配置
*/
nitroMockOptions?: NitroMockPluginOptions; nitroMockOptions?: NitroMockPluginOptions;
/** 开启控制台自定义打印 */ /**
* 是否开启控制台打印
* @default false
*/
print?: boolean; print?: boolean;
/** 打印插件配置 */ /**
* 打印插件配置
*/
printInfoMap?: PrintPluginOptions['infoMap']; printInfoMap?: PrintPluginOptions['infoMap'];
/** 是否开启pwa */ /**
* 是否开启 PWA
* @default false
*/
pwa?: boolean; pwa?: boolean;
/** pwa 插件配置 */ /**
* PWA 插件配置
*/
pwaOptions?: Partial<PwaPluginOptions>; pwaOptions?: Partial<PwaPluginOptions>;
/** 是否开启vxe-table懒加载 */ /**
* 是否开启 VXE Table 懒加载
* @default false
*/
vxeTableLazyImport?: boolean; vxeTableLazyImport?: boolean;
} }
/**
* 库插件配置选项
* @description 用于配置库构建时的插件选项
*/
interface LibraryPluginOptions extends CommonPluginOptions { interface LibraryPluginOptions extends CommonPluginOptions {
/** 开启 dts 输出 */ /**
* 是否开启 DTS 输出
* @default true
* @description 生成 TypeScript 类型声明文件
*/
dts?: boolean | PluginOptions; dts?: boolean | PluginOptions;
} }
/**
* 应用配置选项类型
*/
type ApplicationOptions = ApplicationPluginOptions; type ApplicationOptions = ApplicationPluginOptions;
/**
* 库配置选项类型
*/
type LibraryOptions = LibraryPluginOptions; type LibraryOptions = LibraryPluginOptions;
/**
* 应用配置定义函数类型
* @description 用于定义应用构建配置
*/
type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{ type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{
/** 应用插件配置 */
application?: ApplicationOptions; application?: ApplicationOptions;
/** Vite 配置 */
vite?: UserConfig; vite?: UserConfig;
}>; }>;
/**
* 库配置定义函数类型
* @description 用于定义库构建配置
*/
type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{ type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
/** 库插件配置 */
library?: LibraryOptions; library?: LibraryOptions;
/** Vite 配置 */
vite?: UserConfig; vite?: UserConfig;
}>; }>;
/**
* 配置定义类型
* @description 应用或库的配置定义
*/
type DefineConfig = DefineApplicationOptions | DefineLibraryOptions; type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
export type { export type {

View File

@@ -1,6 +1,6 @@
{ {
"name": "vben-admin-monorepo", "name": "vben-admin-monorepo",
"version": "5.5.4", "version": "5.5.5",
"private": true, "private": true,
"keywords": [ "keywords": [
"monorepo", "monorepo",
@@ -96,7 +96,7 @@
"node": ">=20.10.0", "node": ">=20.10.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.12.0"
}, },
"packageManager": "pnpm@9.15.9", "packageManager": "pnpm@10.10.0",
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {
@@ -107,7 +107,7 @@
"@ast-grep/napi": "catalog:", "@ast-grep/napi": "catalog:",
"@ctrl/tinycolor": "catalog:", "@ctrl/tinycolor": "catalog:",
"clsx": "catalog:", "clsx": "catalog:",
"esbuild": "0.24.0", "esbuild": "0.25.3",
"pinia": "catalog:", "pinia": "catalog:",
"vue": "catalog:" "vue": "catalog:"
}, },

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line vue/prefer-import-from-vue
import { isFunction, isObject, isString } from '@vue/shared'; import { isFunction, isObject, isString } from '@vue/shared';
/** /**

View File

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

View File

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

View File

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

View File

@@ -2,14 +2,16 @@ import type { DeepPartial } from '@vben-core/typings';
import type { InitialOptions, Preferences } from './types'; import type { InitialOptions, Preferences } from './types';
import { markRaw, reactive, readonly, watch } from 'vue';
import { StorageManager } from '@vben-core/shared/cache'; import { StorageManager } from '@vben-core/shared/cache';
import { isMacOs, merge } from '@vben-core/shared/utils'; import { isMacOs, merge } from '@vben-core/shared/utils';
import { import {
breakpointsTailwind, breakpointsTailwind,
useBreakpoints, useBreakpoints,
useDebounceFn, useDebounceFn,
} from '@vueuse/core'; } from '@vueuse/core';
import { markRaw, reactive, readonly, watch } from 'vue';
import { defaultPreferences } from './config'; import { defaultPreferences } from './config';
import { updateCSSVariables } from './update-css-variables'; import { updateCSSVariables } from './update-css-variables';
@@ -37,106 +39,6 @@ class PreferenceManager {
); );
} }
/**
* 保存偏好设置
* @param {Preferences} preference - 需要保存的偏好设置
*/
private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference);
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
}
/**
* 处理更新的键值
* 根据更新的键值执行相应的操作。
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
*/
private handleUpdates(updates: DeepPartial<Preferences>) {
const themeUpdates = updates.theme || {};
const appUpdates = updates.app || {};
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
updateCSSVariables(this.state);
}
if (
Reflect.has(appUpdates, 'colorGrayMode') ||
Reflect.has(appUpdates, 'colorWeakMode')
) {
this.updateColorMode(this.state);
}
}
private initPlatform() {
const dom = document.documentElement;
dom.dataset.platform = isMacOs() ? 'macOs' : 'window';
}
/**
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
*/
private loadCachedPreferences() {
return this.cache?.getItem<Preferences>(STORAGE_KEY);
}
/**
* 加载偏好设置
* @returns {Preferences} 加载的偏好设置
*/
private loadPreferences(): Preferences {
return this.loadCachedPreferences() || { ...defaultPreferences };
}
/**
* 监听状态和系统偏好设置的变化。
*/
private setupWatcher() {
if (this.isInitialized) {
return;
}
// 监听断点,判断是否移动端
const breakpoints = useBreakpoints(breakpointsTailwind);
const isMobile = breakpoints.smaller('md');
watch(
() => isMobile.value,
(val) => {
this.updatePreferences({
app: { isMobile: val },
});
},
{ immediate: true },
);
// 监听系统主题偏好设置变化
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
});
}
/**
* 更新页面颜色模式(灰色、色弱)
* @param preference
*/
private updateColorMode(preference: Preferences) {
if (preference.app) {
const { colorGrayMode, colorWeakMode } = preference.app;
const dom = document.documentElement;
const COLOR_WEAK = 'invert-mode';
const COLOR_GRAY = 'grayscale-mode';
colorWeakMode
? dom.classList.add(COLOR_WEAK)
: dom.classList.remove(COLOR_WEAK);
colorGrayMode
? dom.classList.add(COLOR_GRAY)
: dom.classList.remove(COLOR_GRAY);
}
}
clearCache() { clearCache() {
[STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => { [STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => {
this.cache?.removeItem(key); this.cache?.removeItem(key);
@@ -220,6 +122,113 @@ class PreferenceManager {
this.handleUpdates(updates); this.handleUpdates(updates);
this.savePreferences(this.state); this.savePreferences(this.state);
} }
/**
* 保存偏好设置
* @param {Preferences} preference - 需要保存的偏好设置
*/
private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference);
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
}
/**
* 处理更新的键值
* 根据更新的键值执行相应的操作。
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
*/
private handleUpdates(updates: DeepPartial<Preferences>) {
const themeUpdates = updates.theme || {};
const appUpdates = updates.app || {};
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
updateCSSVariables(this.state);
}
if (
Reflect.has(appUpdates, 'colorGrayMode') ||
Reflect.has(appUpdates, 'colorWeakMode')
) {
this.updateColorMode(this.state);
}
}
private initPlatform() {
const dom = document.documentElement;
dom.dataset.platform = isMacOs() ? 'macOs' : 'window';
}
/**
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
*/
private loadCachedPreferences() {
return this.cache?.getItem<Preferences>(STORAGE_KEY);
}
/**
* 加载偏好设置
* @returns {Preferences} 加载的偏好设置
*/
private loadPreferences(): Preferences {
return this.loadCachedPreferences() || { ...defaultPreferences };
}
/**
* 监听状态和系统偏好设置的变化。
*/
private setupWatcher() {
if (this.isInitialized) {
return;
}
// 监听断点,判断是否移动端
const breakpoints = useBreakpoints(breakpointsTailwind);
const isMobile = breakpoints.smaller('md');
watch(
() => isMobile.value,
(val) => {
this.updatePreferences({
app: { isMobile: val },
});
},
{ immediate: true },
);
// 监听系统主题偏好设置变化
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
// 如果偏好设置中主题模式为auto则跟随系统更新
if (this.state.theme.mode === 'auto') {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
// 恢复为auto模式
this.updatePreferences({
theme: { mode: 'auto' },
});
}
});
}
/**
* 更新页面颜色模式(灰色、色弱)
* @param preference
*/
private updateColorMode(preference: Preferences) {
if (preference.app) {
const { colorGrayMode, colorWeakMode } = preference.app;
const dom = document.documentElement;
const COLOR_WEAK = 'invert-mode';
const COLOR_GRAY = 'grayscale-mode';
colorWeakMode
? dom.classList.add(COLOR_WEAK)
: dom.classList.remove(COLOR_WEAK);
colorGrayMode
? dom.classList.add(COLOR_GRAY)
: dom.classList.remove(COLOR_GRAY);
}
}
} }
const preferencesManager = new PreferenceManager(); const preferencesManager = new PreferenceManager();

View File

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

View File

@@ -62,7 +62,7 @@ async function handleReset(e: Event) {
e?.stopPropagation(); e?.stopPropagation();
const props = unref(rootProps); const props = unref(rootProps);
const values = toRaw(props.formApi?.getValues()); const values = toRaw(await props.formApi?.getValues());
if (isFunction(props.handleReset)) { if (isFunction(props.handleReset)) {
await props.handleReset?.(values); await props.handleReset?.(values);

View File

@@ -295,6 +295,7 @@ export class FormApi {
return true; return true;
}); });
const filteredFields = fieldMergeFn(fields, form.values); const filteredFields = fieldMergeFn(fields, form.values);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate); form.setValues(filteredFields, shouldValidate);
} }
@@ -304,6 +305,7 @@ export class FormApi {
const form = await this.getForm(); const form = await this.getForm();
await form.submitForm(); await form.submitForm();
const rawValues = toRaw(await this.getValues()); const rawValues = toRaw(await this.getValues());
this.handleArrayToStringFields(rawValues);
await this.state?.handleSubmit?.(rawValues); await this.state?.handleSubmit?.(rawValues);
return rawValues; return rawValues;
@@ -392,10 +394,53 @@ export class FormApi {
return this.form; return this.form;
} }
private handleArrayToStringFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return;
}
const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) =>
Array.isArray(value) ? value.join(sep) : value,
);
};
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
if (arrayToStringFields.every((item) => typeof item === 'string')) {
const lastItem =
arrayToStringFields[arrayToStringFields.length - 1] || '';
const fields =
lastItem.length === 1
? arrayToStringFields.slice(0, -1)
: arrayToStringFields;
const separator = lastItem.length === 1 ? lastItem : ',';
processFields(fields, separator);
return;
}
// 处理嵌套数组格式 [['field1'], ';']
arrayToStringFields.forEach((fieldConfig) => {
if (Array.isArray(fieldConfig)) {
const [fields, separator = ','] = fieldConfig;
// 根据类型定义fields 应该始终是字符串数组
if (!Array.isArray(fields)) {
console.warn(
`Invalid field configuration: fields should be an array of strings, got ${typeof fields}`,
);
return;
}
processFields(fields, separator);
}
});
};
private handleRangeTimeValue = (originValues: Record<string, any>) => { private handleRangeTimeValue = (originValues: Record<string, any>) => {
const values = { ...originValues }; const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime; const fieldMappingTime = this.state?.fieldMappingTime;
this.handleStringToArrayFields(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) { if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values; return values;
} }
@@ -441,6 +486,80 @@ export class FormApi {
return values; return values;
}; };
private handleStringToArrayFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return;
}
const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) => {
if (typeof value !== 'string') {
return value;
}
// 处理空字符串的情况
if (value === '') {
return [];
}
// 处理复杂分隔符的情况
const escapedSeparator = sep.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
return value.split(new RegExp(escapedSeparator));
});
};
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
if (arrayToStringFields.every((item) => typeof item === 'string')) {
const lastItem =
arrayToStringFields[arrayToStringFields.length - 1] || '';
const fields =
lastItem.length === 1
? arrayToStringFields.slice(0, -1)
: arrayToStringFields;
const separator = lastItem.length === 1 ? lastItem : ',';
processFields(fields, separator);
return;
}
// 处理嵌套数组格式 [['field1'], ';']
arrayToStringFields.forEach((fieldConfig) => {
if (Array.isArray(fieldConfig)) {
const [fields, separator = ','] = fieldConfig;
if (Array.isArray(fields)) {
processFields(fields, separator);
} else if (typeof originValues[fields] === 'string') {
const value = originValues[fields];
if (value === '') {
originValues[fields] = [];
} else {
const escapedSeparator = separator.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
originValues[fields] = value.split(new RegExp(escapedSeparator));
}
}
}
});
};
private processFields = (
fields: string[],
separator: string,
originValues: Record<string, any>,
transformFn: (value: any, separator: string) => any,
) => {
fields.forEach((field) => {
const value = originValues[field];
if (value === undefined || value === null) {
return;
}
originValues[field] = transformFn(value, separator);
});
};
private updateState() { private updateState() {
const currentSchema = this.state?.schema ?? []; const currentSchema = this.state?.schema ?? [];
const prevSchema = this.prevState?.schema ?? []; const prevSchema = this.prevState?.schema ?? [];

View File

@@ -2,13 +2,18 @@ import type { FormRenderProps } from '../types';
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'; import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'; import {
breakpointsTailwind,
useBreakpoints,
useElementVisibility,
} from '@vueuse/core';
/** /**
* 动态计算行数 * 动态计算行数
*/ */
export function useExpandable(props: FormRenderProps) { export function useExpandable(props: FormRenderProps) {
const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef'); const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');
const isVisible = useElementVisibility(wrapperRef);
const rowMapping = ref<Record<number, number>>({}); const rowMapping = ref<Record<number, number>>({});
// 是否已经计算过一次 // 是否已经计算过一次
const isCalculated = ref(false); const isCalculated = ref(false);
@@ -31,6 +36,7 @@ export function useExpandable(props: FormRenderProps) {
() => props.showCollapseButton, () => props.showCollapseButton,
() => breakpoints.active().value, () => breakpoints.active().value,
() => props.schema?.length, () => props.schema?.length,
() => isVisible.value,
], ],
async ([val]) => { async ([val]) => {
if (val) { if (val) {

View File

@@ -232,6 +232,12 @@ export type FieldMappingTime = [
)?, )?,
][]; ][];
export type ArrayToStringFields = Array<
| [string[], string?] // 嵌套数组格式,可选分隔符
| string // 单个字段,使用默认分隔符
| string[] // 简单数组格式,最后一个元素可以是分隔符
>;
export interface FormSchema< export interface FormSchema<
T extends BaseFormComponentType = BaseFormComponentType, T extends BaseFormComponentType = BaseFormComponentType,
> extends FormCommonConfig { > extends FormCommonConfig {
@@ -266,6 +272,10 @@ export interface FormFieldProps extends FormSchema {
export interface FormRenderProps< export interface FormRenderProps<
T extends BaseFormComponentType = BaseFormComponentType, T extends BaseFormComponentType = BaseFormComponentType,
> { > {
/**
* 表单字段数组映射字符串配置 默认使用","
*/
arrayToStringFields?: ArrayToStringFields;
/** /**
* 是否展开在showCollapseButton=true下生效 * 是否展开在showCollapseButton=true下生效
*/ */
@@ -296,6 +306,10 @@ export interface FormRenderProps<
* 组件集合 * 组件集合
*/ */
componentMap: Record<BaseFormComponentType, Component>; componentMap: Record<BaseFormComponentType, Component>;
/**
* 表单字段映射到时间格式
*/
fieldMappingTime?: FieldMappingTime;
/** /**
* 表单实例 * 表单实例
*/ */
@@ -308,10 +322,15 @@ export interface FormRenderProps<
* 表单定义 * 表单定义
*/ */
schema?: FormSchema<T>[]; schema?: FormSchema<T>[];
/** /**
* 是否显示展开/折叠 * 是否显示展开/折叠
*/ */
showCollapseButton?: boolean; showCollapseButton?: boolean;
/**
* 格式化日期
*/
/** /**
* 表单栅格布局 * 表单栅格布局
* @default "grid-cols-1" * @default "grid-cols-1"
@@ -339,6 +358,11 @@ export interface VbenFormProps<
* 表单操作区域class * 表单操作区域class
*/ */
actionWrapperClass?: ClassType; actionWrapperClass?: ClassType;
/**
* 表单字段数组映射字符串配置 默认使用","
*/
arrayToStringFields?: ArrayToStringFields;
/** /**
* 表单字段映射 * 表单字段映射
*/ */
@@ -354,11 +378,15 @@ export interface VbenFormProps<
/** /**
* 表单值变化回调 * 表单值变化回调
*/ */
handleValuesChange?: (values: Record<string, any>) => void; handleValuesChange?: (
values: Record<string, any>,
fieldsChanged: string[],
) => void;
/** /**
* 重置按钮参数 * 重置按钮参数
*/ */
resetButtonOptions?: ActionButtonOptions; resetButtonOptions?: ActionButtonOptions;
/** /**
* 是否显示默认操作按钮 * 是否显示默认操作按钮
* @default true * @default true

View File

@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Recordable } from '@vben-core/typings';
import type { ExtendedFormApi, VbenFormProps } from './types'; import type { ExtendedFormApi, VbenFormProps } from './types';
// import { toRaw, watch } from 'vue'; // import { toRaw, watch } from 'vue';
import { nextTick, onMounted, watch } from 'vue'; import { nextTick, onMounted, watch } from 'vue';
// import { isFunction } from '@vben-core/shared/utils';
import { useForwardPriorityValues } from '@vben-core/composables'; import { useForwardPriorityValues } from '@vben-core/composables';
import { cloneDeep } from '@vben-core/shared/utils'; import { cloneDeep, get, isEqual, set } from '@vben-core/shared/utils';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
@@ -61,16 +62,46 @@ function handleKeyDownEnter(event: KeyboardEvent) {
} }
const handleValuesChangeDebounced = useDebounceFn(async () => { const handleValuesChangeDebounced = useDebounceFn(async () => {
forward.value.handleValuesChange?.(
cloneDeep(await forward.value.formApi.getValues()),
);
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm(); state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
}, 300); }, 300);
const valuesCache: Recordable<any> = {};
onMounted(async () => { onMounted(async () => {
// 只在挂载后开始监听form.values会有一个初始化的过程 // 只在挂载后开始监听form.values会有一个初始化的过程
await nextTick(); await nextTick();
watch(() => form.values, handleValuesChangeDebounced, { deep: true }); watch(
() => form.values,
async (newVal) => {
if (forward.value.handleValuesChange) {
const fields = state.value.schema?.map((item) => {
return item.fieldName;
});
if (fields && fields.length > 0) {
const changedFields: string[] = [];
fields.forEach((field) => {
const newFieldValue = get(newVal, field);
const oldFieldValue = get(valuesCache, field);
if (!isEqual(newFieldValue, oldFieldValue)) {
changedFields.push(field);
set(valuesCache, field, newFieldValue);
}
});
if (changedFields.length > 0) {
// 调用handleValuesChange回调传入所有表单值的深拷贝和变更的字段列表
forward.value.handleValuesChange(
cloneDeep(await forward.value.formApi.getValues()),
changedFields,
);
}
}
}
handleValuesChangeDebounced();
},
{ deep: true },
);
}); });
</script> </script>

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import { VbenIcon } from '@vben-core/shadcn-ui';
import { useMenuContext } from '../hooks'; import { useMenuContext } from '../hooks';
interface Props extends MenuItemProps { interface Props extends MenuItemProps {
isMenuMore: boolean; isMenuMore?: boolean;
isTopLevelMenuSubmenu: boolean; isTopLevelMenuSubmenu: boolean;
level?: number; level?: number;
} }

View File

@@ -208,6 +208,8 @@ onBeforeUnmount(() => {
nsMenu.e('popup-container'), nsMenu.e('popup-container'),
is(rootMenu.theme, true), is(rootMenu.theme, true),
opened ? '' : 'hidden', opened ? '' : 'hidden',
'overflow-auto',
'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
]" ]"
:content-props="contentProps" :content-props="contentProps"
:open="true" :open="true"

View File

@@ -7,7 +7,7 @@ import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
import { h, nextTick, ref, render } from 'vue'; import { h, nextTick, ref, render } from 'vue';
import { useSimpleLocale } from '@vben-core/composables'; import { useSimpleLocale } from '@vben-core/composables';
import { Input } from '@vben-core/shadcn-ui'; import { Input, VbenRenderContent } from '@vben-core/shadcn-ui';
import { isFunction, isString } from '@vben-core/shared/utils'; import { isFunction, isString } from '@vben-core/shared/utils';
import Alert from './alert.vue'; import Alert from './alert.vue';
@@ -146,11 +146,7 @@ export async function vbenPrompt<T = any>(
const inputComponentRef = ref<null | VNode>(null); const inputComponentRef = ref<null | VNode>(null);
const staticContents: Component[] = []; const staticContents: Component[] = [];
if (isString(content)) { staticContents.push(h(VbenRenderContent, { content, renderBr: true }));
staticContents.push(h('span', content));
} else if (content) {
staticContents.push(content as Component);
}
const modelPropName = _modelPropName || 'modelValue'; const modelPropName = _modelPropName || 'modelValue';
const componentProps = { ..._componentProps }; const componentProps = { ..._componentProps };

View File

@@ -2,6 +2,8 @@ import type { Component, VNode, VNodeArrayChildren } from 'vue';
import type { Recordable } from '@vben-core/typings'; import type { Recordable } from '@vben-core/typings';
import { createContext } from '@vben-core/shadcn-ui';
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type BeforeCloseScope = { export type BeforeCloseScope = {
@@ -34,8 +36,14 @@ export type AlertProps = {
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/ /** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean; contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
showCancel?: boolean; showCancel?: boolean;
/** 弹窗标题 */ /** 弹窗标题 */
@@ -64,3 +72,28 @@ export type PromptProps<T = any> = {
/** 输入组件的值属性名 */ /** 输入组件的值属性名 */
modelPropName?: string; modelPropName?: string;
} & Omit<AlertProps, 'beforeClose'>; } & Omit<AlertProps, 'beforeClose'>;
/**
* Alert上下文
*/
export type AlertContext = {
/** 执行取消操作 */
doCancel: () => void;
/** 执行确认操作 */
doConfirm: () => void;
};
export const [injectAlertContext, provideAlertContext] =
createContext<AlertContext>('VbenAlertContext');
/**
* 获取Alert上下文
* @returns AlertContext
*/
export function useAlertContext() {
const context = injectAlertContext();
if (!context) {
throw new Error('useAlertContext must be used within an AlertProvider');
}
return context;
}

View File

@@ -3,7 +3,7 @@ import type { Component } from 'vue';
import type { AlertProps } from './alert'; import type { AlertProps } from './alert';
import { computed, h, nextTick, ref, watch } from 'vue'; import { computed, h, nextTick, ref } from 'vue';
import { useSimpleLocale } from '@vben-core/composables'; import { useSimpleLocale } from '@vben-core/composables';
import { import {
@@ -28,6 +28,8 @@ import {
import { globalShareState } from '@vben-core/shared/global-state'; import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
import { provideAlertContext } from './alert';
const props = withDefaults(defineProps<AlertProps>(), { const props = withDefaults(defineProps<AlertProps>(), {
bordered: true, bordered: true,
buttonAlign: 'end', buttonAlign: 'end',
@@ -39,14 +41,16 @@ const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale(); const { $t } = useSimpleLocale();
const components = globalShareState.getComponents(); const components = globalShareState.getComponents();
const isConfirm = ref(false); const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick(); function onAlertClosed() {
if (val) { emits('closed', isConfirm.value);
isConfirm.value = false; isConfirm.value = false;
} else { }
emits('closed', isConfirm.value);
} function onEscapeKeyDown() {
}); isConfirm.value = false;
}
const getIconRender = computed(() => { const getIconRender = computed(() => {
let iconRender: Component | null = null; let iconRender: Component | null = null;
if (props.icon) { if (props.icon) {
@@ -89,6 +93,22 @@ const getIconRender = computed(() => {
} }
return iconRender; return iconRender;
}); });
function doCancel() {
handleCancel();
handleOpenChange(false);
}
function doConfirm() {
handleConfirm();
handleOpenChange(false);
}
provideAlertContext({
doCancel,
doConfirm,
});
function handleConfirm() { function handleConfirm() {
isConfirm.value = true; isConfirm.value = true;
emits('confirm'); emits('confirm');
@@ -100,6 +120,7 @@ function handleCancel() {
const loading = ref(false); const loading = ref(false);
async function handleOpenChange(val: boolean) { async function handleOpenChange(val: boolean) {
await nextTick(); // 等待标记isConfirm状态
if (!val && props.beforeClose) { if (!val && props.beforeClose) {
loading.value = true; loading.value = true;
try { try {
@@ -120,15 +141,17 @@ async function handleOpenChange(val: boolean) {
<AlertDialogContent <AlertDialogContent
:open="open" :open="open"
:centered="centered" :centered="centered"
:overlay-blur="overlayBlur"
@opened="emits('opened')" @opened="emits('opened')"
@closed="onAlertClosed"
@escape-key-down="onEscapeKeyDown"
:class=" :class="
cn( cn(
containerClass, containerClass,
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]', 'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
{ {
'border-border border': bordered, 'border-border border': bordered,
'shadow-3xl': !bordered, 'shadow-3xl': !bordered,
'top-1/2 !-translate-y-1/2': centered,
}, },
) )
" "
@@ -138,7 +161,7 @@ async function handleOpenChange(val: boolean) {
<div class="flex items-center"> <div class="flex items-center">
<component :is="getIconRender" class="mr-2" /> <component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span> <span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel"> <AlertDialogCancel v-if="showCancel" as-child>
<VbenButton <VbenButton
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -152,22 +175,27 @@ async function handleOpenChange(val: boolean) {
</div> </div>
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<div class="m-4 mb-6 min-h-[30px]"> <div class="m-4 min-h-[30px]">
<VbenRenderContent :content="content" render-br /> <VbenRenderContent :content="content" render-br />
</div> </div>
<VbenLoading v-if="loading && contentMasking" :spinning="loading" /> <VbenLoading v-if="loading && contentMasking" :spinning="loading" />
</AlertDialogDescription> </AlertDialogDescription>
<div class="flex justify-end gap-x-2" :class="`justify-${buttonAlign}`"> <div
<AlertDialogCancel v-if="showCancel" :disabled="loading"> class="flex items-center justify-end gap-x-2"
:class="`justify-${buttonAlign}`"
>
<VbenRenderContent :content="footer" />
<AlertDialogCancel v-if="showCancel" as-child>
<component <component
:is="components.DefaultButton || VbenButton" :is="components.DefaultButton || VbenButton"
:disabled="loading"
variant="ghost" variant="ghost"
@click="handleCancel" @click="handleCancel"
> >
{{ cancelText || $t('cancel') }} {{ cancelText || $t('cancel') }}
</component> </component>
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction> <AlertDialogAction as-child>
<component <component
:is="components.PrimaryButton || VbenButton" :is="components.PrimaryButton || VbenButton"
:loading="loading" :loading="loading"

View File

@@ -1,5 +1,10 @@
export * from './alert'; export type {
AlertProps,
BeforeCloseScope,
IconType,
PromptProps,
} from './alert';
export { useAlertContext } from './alert';
export { default as Alert } from './alert.vue'; export { default as Alert } from './alert.vue';
export { export {
vbenAlert as alert, vbenAlert as alert,

View File

@@ -9,7 +9,11 @@ vi.mock('@vben-core/shared/store', () => {
return { return {
isFunction: (fn: any) => typeof fn === 'function', isFunction: (fn: any) => typeof fn === 'function',
Store: class { Store: class {
get state() {
return this._state;
}
private _state: DrawerState; private _state: DrawerState;
private options: any; private options: any;
constructor(initialState: DrawerState, options: any) { constructor(initialState: DrawerState, options: any) {
@@ -25,10 +29,6 @@ vi.mock('@vben-core/shared/store', () => {
this._state = fn(this._state); this._state = fn(this._state);
this.options.onUpdate(); this.options.onUpdate();
} }
get state() {
return this._state;
}
}, },
}; };
}); });
@@ -54,7 +54,6 @@ describe('drawerApi', () => {
}); });
it('should close the drawer if onBeforeClose allows it', () => { it('should close the drawer if onBeforeClose allows it', () => {
drawerApi.open();
drawerApi.close(); drawerApi.close();
expect(drawerApi.store.state.isOpen).toBe(false); expect(drawerApi.store.state.isOpen).toBe(false);
}); });

View File

@@ -86,7 +86,8 @@ export class DrawerApi {
} }
/** /**
* 关闭弹窗 * 关闭抽屉
* @description 关闭抽屉时会调用 onBeforeClose 钩子函数,如果 onBeforeClose 返回 false则不关闭弹窗
*/ */
async close() { async close() {
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗 // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗

View File

@@ -52,6 +52,10 @@ export interface DrawerProps {
* 弹窗描述 * 弹窗描述
*/ */
description?: string; description?: string;
/**
* 在关闭时销毁抽屉
*/
destroyOnClose?: boolean;
/** /**
* 是否显示底部 * 是否显示底部
* @default true * @default true
@@ -143,10 +147,6 @@ export interface DrawerApiOptions extends DrawerState {
* 独立的抽屉组件 * 独立的抽屉组件
*/ */
connectedComponent?: Component; connectedComponent?: Component;
/**
* 在关闭时销毁抽屉。仅在使用 connectedComponent 时有效
*/
destroyOnClose?: boolean;
/** /**
* 关闭前的回调,返回 false 可以阻止关闭 * 关闭前的回调,返回 false 可以阻止关闭
* @returns * @returns

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