diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3581e1884..b998f21e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,4 +14,4 @@ jobs: with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false - automatic_release_tag: "9.2.0" + automatic_release_tag: "9.2.1" diff --git a/Directory.Packages.props b/Directory.Packages.props index d38fbca96..82b823da1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,10 @@ 8.3.5 - 2.15.1 + 2.15.2 3.3.5 - 9.2.0 - 9.2.0 + 9.2.1 + 9.2.1 9.0.4 9.0.4 9.0.4 @@ -12,7 +12,7 @@ - + @@ -188,6 +188,7 @@ + @@ -300,17 +301,18 @@ - - - - - - - - + + + + + + + + + @@ -320,6 +322,7 @@ + diff --git a/apps/vben5/.gitpod.yml b/apps/vben5/.gitpod.yml index fb75b433d..5fda2cf70 100644 --- a/apps/vben5/.gitpod.yml +++ b/apps/vben5/.gitpod.yml @@ -2,5 +2,5 @@ ports: - port: 5555 onOpen: open-preview tasks: - - init: corepack enable && pnpm install + - init: npm i -g corepack && pnpm install command: pnpm run dev:play diff --git a/apps/vben5/.node-version b/apps/vben5/.node-version index 48b14e6b2..ee5c24469 100644 --- a/apps/vben5/.node-version +++ b/apps/vben5/.node-version @@ -1 +1 @@ -20.14.0 +22.1.0 diff --git a/apps/vben5/.npmrc b/apps/vben5/.npmrc index f4a1ad483..21147aff2 100644 --- a/apps/vben5/.npmrc +++ b/apps/vben5/.npmrc @@ -1,5 +1,5 @@ registry = "https://registry.npmmirror.com" -public-hoist-pattern[]=husky +public-hoist-pattern[]=lefthook public-hoist-pattern[]=eslint public-hoist-pattern[]=prettier public-hoist-pattern[]=prettier-plugin-tailwindcss diff --git a/apps/vben5/README.ja-JP.md b/apps/vben5/README.ja-JP.md index baa4cc448..f7847a1d9 100644 --- a/apps/vben5/README.ja-JP.md +++ b/apps/vben5/README.ja-JP.md @@ -1,8 +1,13 @@ -
VbenAdmin Logo

+
+ + VbenAdmin Logo + +
+
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) -

Vue Vben Admin

+

Vue Vben Admin

[![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などの最先端フロントエンド技術で開発 -- **TypeScript**: アプリケーション規模のJavaScriptのための言語 -- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富 -- **国際化**: 完全な内蔵国際化サポート -- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵 +- **最新技術スタック**:Vue 3やViteなどの最先端フロントエンド技術で開発 +- **TypeScript**:アプリケーション規模のJavaScriptのための言語 +- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富 +- **国際化**:完全な内蔵国際化サポート +- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵 ## プレビュー - [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト -テストアカウント: vben/123456 +テストアカウント:vben/123456 -

- VbenAdmin Logo - VbenAdmin Logo - VbenAdmin Logo -

+
+ VbenAdmin Logo + VbenAdmin Logo + VbenAdmin Logo +
### Gitpodを使用 @@ -49,30 +54,27 @@ Gitpod(GitHub用の無料オンライン開発環境)でプロジェクト ## インストールと使用 -- プロジェクトコードを取得 +1. プロジェクトコードを取得 ```bash git clone https://github.com/vbenjs/vue-vben-admin.git ``` -- 依存関係のインストール +2. 依存関係のインストール ```bash cd vue-vben-admin - -corepack enable - +npm i -g corepack pnpm install - ``` -- 実行 +3. 実行 ```bash pnpm dev ``` -- ビルド +4. ビルド ```bash pnpm build @@ -86,40 +88,39 @@ pnpm build ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。 -**Pull Request:** +**Pull Request プロセス:** -1. コードをフォーク! -2. 自分のブランチを作成: `git checkout -b feat/xxxx` -3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'` -4. ブランチをプッシュ: `git push origin feat/xxxx` +1. コードをフォーク +2. 自分のブランチを作成:`git checkout -b feat/xxxx` +3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'` +4. ブランチをプッシュ:`git push origin feat/xxxx` 5. `pull request`を送信 ## Git貢献提出規則 -- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) - - - `feat` 新機能の追加 - - `fix` 問題/バグの修正 - - `style` コードスタイルに関連し、実行結果に影響しない - - `perf` 最適化/パフォーマンス向上 - - `refactor` リファクタリング - - `revert` 変更の取り消し - - `test` テスト関連 - - `docs` ドキュメント/注釈 - - `chore` 依存関係の更新/スキャフォールディング設定の変更など - - `ci` 継続的インテグレーション - - `types` 型定義ファイルの変更 - - `wip` 開発中 +参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) + +- `feat` 新機能の追加 +- `fix` 問題/バグの修正 +- `style` コードスタイルに関連し、実行結果に影響しない +- `perf` 最適化/パフォーマンス向上 +- `refactor` リファクタリング +- `revert` 変更の取り消し +- `test` テスト関連 +- `docs` ドキュメント/注釈 +- `chore` 依存関係の更新/スキャフォールディング設定の変更など +- `ci` 継続的インテグレーション +- `types` 型定義ファイルの変更 ## ブラウザサポート -ローカル開発には`Chrome 80+`ブラウザを推奨します +ローカル開発には `Chrome 80+` ブラウザを推奨します モダンブラウザをサポートし、IEはサポートしません -| [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | -| :-: | :-: | :-: | :-: | :-: | -| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン | +| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | +| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン | ## メンテナー @@ -140,8 +141,7 @@ pnpm build ## 貢献者 - Contributors + Contributors ## Discord diff --git a/apps/vben5/README.md b/apps/vben5/README.md index e84c8392e..e027949ab 100644 --- a/apps/vben5/README.md +++ b/apps/vben5/README.md @@ -1,8 +1,13 @@ -
VbenAdmin Logo

+
+ + VbenAdmin Logo + +
+
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) -

Vue Vben Admin

+

Vue Vben Admin

[![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). -## Feature +## Features - **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite - **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 -

- VbenAdmin Logo - VbenAdmin Logo - VbenAdmin Logo -

+
+ VbenAdmin Logo + VbenAdmin Logo + VbenAdmin Logo +
### 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/) -## Install and use +## Install and Use -- Get the project code +1. Get the project code ```bash git clone https://github.com/vbenjs/vue-vben-admin.git ``` -- Installation dependencies +2. Install dependencies ```bash cd vue-vben-admin - -corepack enable - +npm i -g corepack pnpm install ``` -- run +3. Run ```bash pnpm dev ``` -- build +4. Build ```bash pnpm build @@ -81,44 +84,43 @@ pnpm build [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! -2. Create your own branch: `git checkout -b feat/xxxx` +1. Fork the code +2. Create your branch: `git checkout -b feat/xxxx` 3. Submit your changes: `git commit -am 'feat(function): add xxxxx'` 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 - - `fix` Fix the problem/BUG - - `style` The code style is related and does not affect the running result - - `perf` Optimization/performance improvement - - `refactor` Refactor - - `revert` Undo edit - - `test` Test related - - `docs` Documentation/notes - - `chore` Dependency update/scaffolding configuration modification etc. - - `ci` Continuous integration - - `types` Type definition file changes - - `wip` In development +- `feat` Add new features +- `fix` Fix the problem/BUG +- `style` The code style is related and does not affect the running result +- `perf` Optimization/performance improvement +- `refactor` Refactor +- `revert` Undo edit +- `test` Test related +- `docs` Documentation/notes +- `chore` Dependency update/scaffolding configuration modification etc. +- `ci` Continuous integration +- `types` Type definition file changes -## Browser support +## Browser Support The `Chrome 80+` browser is recommended for local development Support modern browsers, not IE -| [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | -| :-: | :-: | :-: | :-: | :-: | -| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | +| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | +| last 2 versions | last 2 versions | last 2 versions | last 2 versions | ## Maintainer @@ -136,11 +138,10 @@ If you think this project is helpful to you, you can help the author buy a cup o Paypal Me -## Contributor +## Contributors - Contributors + Contributors ## Discord diff --git a/apps/vben5/README.zh-CN.md b/apps/vben5/README.zh-CN.md index e2f05bb98..5a6b191b8 100644 --- a/apps/vben5/README.zh-CN.md +++ b/apps/vben5/README.zh-CN.md @@ -1,8 +1,13 @@ -
VbenAdmin Logo

+
+ + VbenAdmin Logo + +
+
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) -

Vue Vben Admin

+

Vue Vben Admin

[![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,31 +20,31 @@ Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的 ## 升级提示 -该版本为最新版本`5.0`, 与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2) +该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2) ## 特性 - **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发 -- **TypeScript**: 应用程序级 JavaScript 的语言 +- **TypeScript**:应用程序级 JavaScript 的语言 - **主题**:提供多套主题色彩,可配置自定义主题 - **国际化**:内置完善的国际化方案 -- **权限** 内置完善的动态路由权限生成方案 +- **权限**:内置完善的动态路由权限生成方案 ## 预览 - [Vben Admin](https://vben.pro/) - 完整版中文站点 -测试账号: vben/123456 +测试账号:vben/123456 -

- VbenAdmin Logo - VbenAdmin Logo - VbenAdmin Logo -

+
+ VbenAdmin Logo + VbenAdmin Logo + VbenAdmin Logo +
### 使用 Gitpod -在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码. +在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。 [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin) @@ -49,29 +54,27 @@ Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的 ## 安装使用 -- 获取项目代码 +1. 获取项目代码 ```bash git clone https://github.com/vbenjs/vue-vben-admin.git ``` -- 安装依赖 +2. 安装依赖 ```bash cd vue-vben-admin - -corepack enable - +npm i -g corepack pnpm install ``` -- 运行 +3. 运行 ```bash pnpm dev ``` -- 打包 +4. 打包 ```bash pnpm build @@ -85,68 +88,66 @@ pnpm build 非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。 -**Pull Request:** +**Pull Request 流程:** -1. Fork 代码! -2. 创建自己的分支: `git checkout -b feature/xxxx` -3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` -4. 推送您的分支: `git push origin feature/xxxx` -5. 提交`pull request` +1. Fork 代码 +2. 创建自己的分支:`git checkout -b feature/xxxx` +3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'` +4. 推送您的分支:`git push origin feature/xxxx` +5. 提交 `pull request` ## Git 贡献提交规范 -- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) - - - `feat` 增加新功能 - - `fix` 修复问题/BUG - - `style` 代码风格相关无影响运行结果的 - - `perf` 优化/性能提升 - - `refactor` 重构 - - `revert` 撤销修改 - - `test` 测试相关 - - `docs` 文档/注释 - - `chore` 依赖更新/脚手架配置修改等 - - `ci` 持续集成 - - `types` 类型定义文件更改 - - `wip` 开发中 +参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) + +- `feat` 增加新功能 +- `fix` 修复问题/BUG +- `style` 代码风格相关无影响运行结果的 +- `perf` 优化/性能提升 +- `refactor` 重构 +- `revert` 撤销修改 +- `test` 测试相关 +- `docs` 文档/注释 +- `chore` 依赖更新/脚手架配置修改等 +- `ci` 持续集成 +- `types` 类型定义文件更改 ## 浏览器支持 -本地开发推荐使用`Chrome 80+` 浏览器 +本地开发推荐使用 `Chrome 80+` 浏览器 -支持现代浏览器, 不支持 IE +支持现代浏览器,不支持 IE -| [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | -| :-: | :-: | :-: | :-: | :-: | -| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | +| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | +| last 2 versions | last 2 versions | last 2 versions | last 2 versions | ## 维护者 [@Vben](https://github.com/anncwb) -## Star History +## Star 历史 [![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date) ## 捐赠 -如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持! +如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持! ![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) Paypal Me -## Contributor +## 贡献者 - Contributors + Contributors ## Discord - [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions) -## License +## 许可证 [MIT © Vben-2020](./LICENSE) diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index 0f267f1d4..03fa292ee 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -1,6 +1,6 @@ { "name": "@abp/app-antd", - "version": "9.1.3", + "version": "9.2.0", "homepage": "https://github.com/colinin/abp-next-admin", "bugs": "https://github.com/colinin/abp-next-admin/issues", "repository": { @@ -64,7 +64,6 @@ "@vueuse/core": "catalog:", "ant-design-vue": "catalog:", "dayjs": "catalog:", - "oidc-client-ts": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/vben5/apps/app-antd/src/adapter/component/index.ts b/apps/vben5/apps/app-antd/src/adapter/component/index.ts index db70ef19d..4ab140011 100644 --- a/apps/vben5/apps/app-antd/src/adapter/component/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/component/index.ts @@ -3,11 +3,18 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component, SetupContext } from 'vue'; +import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; +import type { Recordable } from '@vben/types'; -import { h } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + getCurrentInstance, + h, + ref, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; @@ -15,42 +22,92 @@ import { $t } from '@vben/locales'; import { FeatureStateCheck, GlobalFeatureStateCheck } from '@abp/features'; import { PermissionStateCheck } from '@abp/permissions'; import { TenantSelect } from '@abp/saas'; -import { - AutoComplete, - Button, - Checkbox, - CheckboxGroup, - DatePicker, - Divider, - Empty, - Input, - InputNumber, - InputPassword, - InputSearch, - Mentions, - notification, - Radio, - RadioGroup, - RangePicker, - Rate, - Select, - Space, - Switch, - Textarea, - TimePicker, - Tree, - TreeSelect, - Upload, -} from 'ant-design-vue'; +import { notification } from 'ant-design-vue'; + +const AutoComplete = defineAsyncComponent( + () => import('ant-design-vue/es/auto-complete'), +); +const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); +const Checkbox = defineAsyncComponent( + () => import('ant-design-vue/es/checkbox'), +); +const CheckboxGroup = defineAsyncComponent(() => + import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), +); +const DatePicker = defineAsyncComponent( + () => import('ant-design-vue/es/date-picker'), +); +const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider')); +const Empty = defineAsyncComponent(() => import('ant-design-vue/es/empty')); +const Input = defineAsyncComponent(() => import('ant-design-vue/es/input')); +const InputNumber = defineAsyncComponent( + () => import('ant-design-vue/es/input-number'), +); +const InputSearch = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.InputSearch), +); +const InputPassword = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.InputPassword), +); +const Mentions = defineAsyncComponent( + () => import('ant-design-vue/es/mentions'), +); +const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio')); +const RadioGroup = defineAsyncComponent(() => + import('ant-design-vue/es/radio').then((res) => res.RadioGroup), +); +const RangePicker = defineAsyncComponent(() => + import('ant-design-vue/es/date-picker').then((res) => res.RangePicker), +); +const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate')); +const Select = defineAsyncComponent(() => import('ant-design-vue/es/select')); +const Space = defineAsyncComponent(() => import('ant-design-vue/es/space')); +const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch')); +const Textarea = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.Textarea), +); +const TimePicker = defineAsyncComponent( + () => import('ant-design-vue/es/time-picker'), +); +const Tree = defineAsyncComponent(() => import('ant-design-vue/es/tree')); +const TreeSelect = defineAsyncComponent( + () => import('ant-design-vue/es/tree-select'), +); +const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); const withDefaultPlaceholder = ( component: T, type: 'input' | 'select', + componentProps: Recordable = {}, ) => { - return (props: any, { attrs, slots }: Omit) => { - const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); - return h(component, { ...props, ...attrs, placeholder }, slots); - }; + return defineComponent({ + name: component.name, + inheritAttrs: false, + setup: (props: any, { attrs, expose, slots }) => { + const placeholder = + props?.placeholder || + attrs?.placeholder || + $t(`ui.placeholder.${type}`); + // 透传组件暴露的方法 + const innerRef = ref(); + const publicApi: Recordable = {}; + expose(publicApi); + const instance = getCurrentInstance(); + instance?.proxy?.$nextTick(() => { + for (const key in innerRef.value) { + if (typeof innerRef.value[key] === 'function') { + publicApi[key] = innerRef.value[key]; + } + } + }); + return () => + h( + component, + { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, + slots, + ); + }, + }); }; // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 @@ -63,12 +120,16 @@ export type ComponentType = | 'DatePicker' | 'DefaultButton' | 'Divider' + | 'Empty' + | 'FeatureStateCheck' + | 'GlobalFeatureStateCheck' | 'IconPicker' | 'Input' | 'InputNumber' | 'InputPassword' | 'InputSearch' | 'Mentions' + | 'PermissionStateCheck' | 'PrimaryButton' | 'Radio' | 'RadioGroup' @@ -77,6 +138,7 @@ export type ComponentType = | 'Select' | 'Space' | 'Switch' + | 'TenantSelect' | 'Textarea' | 'TimePicker' | 'Tree' @@ -89,38 +151,34 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: Select, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - visibleEvent: 'onDropdownVisibleChange', - }, - slots, - ); - }, - ApiTreeSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: TreeSelect, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - slots, - ); - }, + ApiSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiSelect', + }, + 'select', + { + component: Select, + loadingSlot: 'suffixIcon', + visibleEvent: 'onDropdownVisibleChange', + modelPropName: 'value', + }, + ), + ApiTreeSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiTreeSelect', + }, + 'select', + { + component: TreeSelect, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + optionsPropName: 'treeData', + visibleEvent: 'onVisibleChange', + }, + ), AutoComplete, Checkbox, CheckboxGroup, @@ -131,17 +189,15 @@ async function initComponentAdapter() { }, Divider, Empty, - IconPicker: (props, { attrs, slots }) => { - return h( - IconPicker, - { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs }, - slots, - ); - }, + IconPicker: withDefaultPlaceholder(IconPicker, 'select', { + iconSlot: 'addonAfter', + inputComponent: Input, + modelValueProp: 'value', + }), Input: withDefaultPlaceholder(Input, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input'), - InputPassword: withDefaultPlaceholder(InputPassword, 'input'), InputSearch: withDefaultPlaceholder(InputSearch, 'input'), + InputPassword: withDefaultPlaceholder(InputPassword, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 PrimaryButton: (props, { attrs, slots }) => { diff --git a/apps/vben5/apps/app-antd/src/adapter/form.ts b/apps/vben5/apps/app-antd/src/adapter/form.ts index 4a55850ec..983a7f516 100644 --- a/apps/vben5/apps/app-antd/src/adapter/form.ts +++ b/apps/vben5/apps/app-antd/src/adapter/form.ts @@ -8,7 +8,7 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -async function initVbenForm() { +async function initSetupVbenForm() { setupVbenForm({ config: { // ant design vue组件库默认都是 v-model:value @@ -43,7 +43,7 @@ async function initVbenForm() { const useVbenForm = useForm; -export { initVbenForm, useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/vben5/apps/app-antd/src/api/core/index.ts b/apps/vben5/apps/app-antd/src/api/core/index.ts index 93c65bd62..0c81c06cd 100644 --- a/apps/vben5/apps/app-antd/src/api/core/index.ts +++ b/apps/vben5/apps/app-antd/src/api/core/index.ts @@ -1,2 +1 @@ -export * from './menu'; export { useAbpConfigApi } from './useAbpConfigApi'; diff --git a/apps/vben5/apps/app-antd/src/api/core/menu.ts b/apps/vben5/apps/app-antd/src/api/core/menu.ts deleted file mode 100644 index 9ef60b11c..000000000 --- a/apps/vben5/apps/app-antd/src/api/core/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RouteRecordStringComponent } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户所有菜单 - */ -export async function getAllMenusApi() { - return requestClient.get('/menu/all'); -} diff --git a/apps/vben5/apps/app-antd/src/api/request.ts b/apps/vben5/apps/app-antd/src/api/request.ts deleted file mode 100644 index c607daf6e..000000000 --- a/apps/vben5/apps/app-antd/src/api/request.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 该文件可自行根据业务逻辑进行调整 - */ -import type { HttpResponse } from '@vben/request'; - -import { useAppConfig } from '@vben/hooks'; -import { preferences } from '@vben/preferences'; -import { - authenticateResponseInterceptor, - errorMessageResponseInterceptor, - RequestClient, -} from '@vben/request'; -import { useAccessStore } from '@vben/stores'; - -import { message } from 'ant-design-vue'; - -import { useAuthStore } from '#/store'; - -// import { refreshTokenApi } from './core'; - -const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); - -function createRequestClient(baseURL: string) { - const client = new RequestClient({ - baseURL, - }); - - /** - * 重新认证逻辑 - */ - async function doReAuthenticate() { - console.warn('Access token or refresh token is invalid or expired. '); - const accessStore = useAccessStore(); - const authStore = useAuthStore(); - accessStore.setAccessToken(null); - if ( - preferences.app.loginExpiredMode === 'modal' && - accessStore.isAccessChecked - ) { - accessStore.setLoginExpired(true); - } else { - await authStore.logout(); - } - } - - /** - * 刷新token逻辑 - */ - async function doRefreshToken() { - const accessStore = useAccessStore(); - // const resp = await refreshTokenApi(); - // const newToken = resp.data; - // accessStore.setAccessToken(newToken); - return String(accessStore.accessToken); - } - - function formatToken(token: null | string) { - return token ? `Bearer ${token}` : null; - } - - // 请求头处理 - client.addRequestInterceptor({ - fulfilled: async (config) => { - const accessStore = useAccessStore(); - - config.headers.Authorization = formatToken(accessStore.accessToken); - config.headers['Accept-Language'] = preferences.app.locale; - return config; - }, - }); - - // response数据解构 - client.addResponseInterceptor({ - fulfilled: (response) => { - const { data: responseData, status } = response; - - const { code, data } = responseData; - if (status >= 200 && status < 400 && code === 0) { - return data; - } - - throw Object.assign({}, response, { response }); - }, - }); - - // token过期的处理 - client.addResponseInterceptor( - authenticateResponseInterceptor({ - client, - doReAuthenticate, - doRefreshToken, - enableRefreshToken: preferences.app.enableRefreshToken, - formatToken, - }), - ); - - // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 - client.addResponseInterceptor( - errorMessageResponseInterceptor((msg: string, error) => { - // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg - // 当前mock接口返回的错误字段是 error 或者 message - const responseData = error?.response?.data ?? {}; - const errorMessage = responseData?.error ?? responseData?.message ?? ''; - // 如果没有错误信息,则会根据状态码进行提示 - message.error(errorMessage || msg); - }), - ); - - return client; -} - -export const requestClient = createRequestClient(apiURL); - -export const baseRequestClient = new RequestClient({ baseURL: apiURL }); diff --git a/apps/vben5/apps/app-antd/src/auth/authService.ts b/apps/vben5/apps/app-antd/src/auth/authService.ts deleted file mode 100644 index aaeaa626b..000000000 --- a/apps/vben5/apps/app-antd/src/auth/authService.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useAppConfig } from '@vben/hooks'; - -import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; - -const { authority, audience, clientId, clientSecret, disablePKCE } = - useAppConfig(import.meta.env, import.meta.env.PROD); - -const userManager = new UserManager({ - authority, - client_id: clientId, - client_secret: clientSecret, - redirect_uri: `${window.location.origin}/signin-callback`, - response_type: 'code', - scope: audience, - post_logout_redirect_uri: `${window.location.origin}/`, - silent_redirect_uri: `${window.location.origin}/silent-renew.html`, - automaticSilentRenew: true, - loadUserInfo: true, - userStore: new WebStorageStateStore({ store: window.localStorage }), - disablePKCE, -}); - -export default { - async login() { - return userManager.signinRedirect(); - }, - - async logout() { - return userManager.signoutRedirect(); - }, - - async refreshToken() { - return userManager.signinSilent(); - }, - - async getAccessToken() { - const user = await userManager.getUser(); - return user?.access_token; - }, - - async isAuthenticated() { - const user = await userManager.getUser(); - return !!user && !user.expired; - }, - - async handleCallback() { - return userManager.signinRedirectCallback(); - }, - - async getUser() { - return userManager.getUser(); - }, -}; diff --git a/apps/vben5/apps/app-antd/src/bootstrap.ts b/apps/vben5/apps/app-antd/src/bootstrap.ts index 520ec1e1f..d34a6f46b 100644 --- a/apps/vben5/apps/app-antd/src/bootstrap.ts +++ b/apps/vben5/apps/app-antd/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { registerLoadingDirective } from '@vben/common-ui'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; @@ -12,7 +12,7 @@ import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; -import { initVbenForm } from './adapter/form'; +import { initSetupVbenForm } from './adapter/form'; import { initRequestClient } from './adapter/request'; import App from './app.vue'; import { router } from './router'; @@ -20,11 +20,30 @@ import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); - await initVbenForm(); + + // 初始化表单组件 + await initSetupVbenForm(); + + // 初始化axios initRequestClient(); + // // 设置弹窗的默认配置 + // setDefaultModalProps({ + // fullscreenButton: false, + // }); + // // 设置抽屉的默认配置 + // setDefaultDrawerProps({ + // zIndex: 1020, + // }); + const app = createApp(App); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + // 配置 pinia-tore await initStores(app, { namespace }); @@ -35,11 +54,16 @@ async function bootstrap(namespace: string) { registerAccessDirective(app); // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); initTippy(app); // 配置路由及路由守卫 app.use(router); + // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); + app.use(MotionPlugin); + // 动态更新标题 watchEffect(() => { if (preferences.app.dynamicTitle) { diff --git a/apps/vben5/apps/app-antd/src/router/guard.ts b/apps/vben5/apps/app-antd/src/router/guard.ts index cbb5235ec..e12766f91 100644 --- a/apps/vben5/apps/app-antd/src/router/guard.ts +++ b/apps/vben5/apps/app-antd/src/router/guard.ts @@ -1,6 +1,6 @@ import type { Router } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; @@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) { return decodeURIComponent( (to.query?.redirect as string) || userStore.userInfo?.homePath || - DEFAULT_HOME_PATH, + preferences.app.defaultHomePath, ); } return true; @@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) { path: LOGIN_PATH, // 如不需要,直接删除 query query: - to.fullPath === DEFAULT_HOME_PATH + to.fullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(to.fullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 @@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) { accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); const redirectPath = (from.query.redirect ?? - (to.path === DEFAULT_HOME_PATH - ? userInfo.homePath || DEFAULT_HOME_PATH + (to.path === preferences.app.defaultHomePath + ? userInfo.homePath || preferences.app.defaultHomePath : to.fullPath)) as string; return { diff --git a/apps/vben5/apps/app-antd/src/router/routes/core.ts b/apps/vben5/apps/app-antd/src/router/routes/core.ts index 5952b5926..959ed35c1 100644 --- a/apps/vben5/apps/app-antd/src/router/routes/core.ts +++ b/apps/vben5/apps/app-antd/src/router/routes/core.ts @@ -1,6 +1,7 @@ import type { RouteRecordRaw } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; import { AuthPageLayout, BasicLayout } from '#/layouts'; import { $t } from '#/locales'; @@ -45,7 +46,7 @@ const coreRoutes: RouteRecordRaw[] = [ }, name: 'Root', path: '/', - redirect: DEFAULT_HOME_PATH, + redirect: preferences.app.defaultHomePath, children: [], }, { diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 6773557c0..7492529a5 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -5,10 +5,12 @@ import type { Recordable, UserInfo } from '@vben/types'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { + useOidcClient, usePhoneLoginApi, useProfileApi, useQrCodeLoginApi, @@ -20,7 +22,6 @@ import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; -import authService from '#/auth/authService'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { @@ -35,12 +36,13 @@ export const useAuthStore = defineStore('auth', () => { const userStore = useUserStore(); const abpStore = useAbpStore(); const router = useRouter(); + const oidcClient = useOidcClient(); const loginLoading = ref(false); async function refreshSession() { - if (await authService.getAccessToken()) { - const user = await authService.refreshToken(); + if (await oidcClient.getAccessToken()) { + const user = await oidcClient.refreshToken(); const newToken = `${user?.token_type} ${user?.access_token}`; accessStore.setAccessToken(newToken); if (user?.refresh_token) { @@ -60,12 +62,12 @@ export const useAuthStore = defineStore('auth', () => { } async function oidcLogin() { - await authService.login(); + await oidcClient.login(); } async function oidcCallback() { try { - const user = await authService.handleCallback(); + const user = await oidcClient.handleCallback(); return await _loginSuccess({ accessToken: user.access_token, tokenType: user.token_type, @@ -126,9 +128,9 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - if (await authService.getAccessToken()) { + if (await oidcClient.getAccessToken()) { accessStore.setAccessToken(null); - await authService.logout(); + await oidcClient.logout(); } } catch { // 不做任何处理 @@ -203,7 +205,9 @@ export const useAuthStore = defineStore('auth', () => { } else { onSuccess ? await onSuccess?.() - : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); + : await router.push( + userInfo.homePath || preferences.app.defaultHomePath, + ); } if (userInfo?.realName) { diff --git a/apps/vben5/apps/backend-mock/api/demo/bigint.ts b/apps/vben5/apps/backend-mock/api/demo/bigint.ts new file mode 100644 index 000000000..880cc5ea8 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/demo/bigint.ts @@ -0,0 +1,28 @@ +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const data = ` + { + "code": 0, + "message": "success", + "data": [ + { + "id": 123456789012345678901234567890123456789012345678901234567890, + "name": "John Doe", + "age": 30, + "email": "john-doe@demo.com" + }, + { + "id": 987654321098765432109876543210987654321098765432109876543210, + "name": "Jane Smith", + "age": 25, + "email": "jane@demo.com" + } + ] + } + `; + setHeader(event, 'Content-Type', 'application/json'); + return data; +}); diff --git a/apps/vben5/apps/backend-mock/api/system/dept/.post.ts b/apps/vben5/apps/backend-mock/api/system/dept/.post.ts new file mode 100644 index 000000000..c529ea1bd --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/dept/.post.ts @@ -0,0 +1,15 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { + sleep, + unAuthorizedResponse, + useResponseSuccess, +} from '~/utils/response'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + await sleep(600); + return useResponseSuccess(null); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/dept/[id].delete.ts b/apps/vben5/apps/backend-mock/api/system/dept/[id].delete.ts new file mode 100644 index 000000000..e48f051cc --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/dept/[id].delete.ts @@ -0,0 +1,15 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { + sleep, + unAuthorizedResponse, + useResponseSuccess, +} from '~/utils/response'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + await sleep(1000); + return useResponseSuccess(null); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/dept/[id].put.ts b/apps/vben5/apps/backend-mock/api/system/dept/[id].put.ts new file mode 100644 index 000000000..aa55c0857 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/dept/[id].put.ts @@ -0,0 +1,15 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { + sleep, + unAuthorizedResponse, + useResponseSuccess, +} from '~/utils/response'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + await sleep(2000); + return useResponseSuccess(null); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/dept/list.ts b/apps/vben5/apps/backend-mock/api/system/dept/list.ts new file mode 100644 index 000000000..ae819b622 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/dept/list.ts @@ -0,0 +1,61 @@ +import { faker } from '@faker-js/faker'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; + +const formatterCN = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +function generateMockDataList(count: number) { + const dataList = []; + + for (let i = 0; i < count; i++) { + const dataItem: Record = { + id: faker.string.uuid(), + pid: 0, + name: faker.commerce.department(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2021-01-01', to: '2022-12-31' }), + ), + remark: faker.lorem.sentence(), + }; + if (faker.datatype.boolean()) { + dataItem.children = Array.from( + { length: faker.number.int({ min: 1, max: 5 }) }, + () => ({ + id: faker.string.uuid(), + pid: dataItem.id, + name: faker.commerce.department(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2023-01-01', to: '2023-12-31' }), + ), + remark: faker.lorem.sentence(), + }), + ); + } + dataList.push(dataItem); + } + + return dataList; +} + +const mockData = generateMockDataList(10); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + const listData = structuredClone(mockData); + + return useResponseSuccess(listData); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/menu/list.ts b/apps/vben5/apps/backend-mock/api/system/menu/list.ts new file mode 100644 index 000000000..5328b2fdf --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/menu/list.ts @@ -0,0 +1,12 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { MOCK_MENU_LIST } from '~/utils/mock-data'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + return useResponseSuccess(MOCK_MENU_LIST); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/menu/name-exists.ts b/apps/vben5/apps/backend-mock/api/system/menu/name-exists.ts new file mode 100644 index 000000000..5599c22b3 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/menu/name-exists.ts @@ -0,0 +1,28 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { MOCK_MENU_LIST } from '~/utils/mock-data'; +import { unAuthorizedResponse } from '~/utils/response'; + +const namesMap: Record = {}; + +function getNames(menus: any[]) { + menus.forEach((menu) => { + namesMap[menu.name] = String(menu.id); + if (menu.children) { + getNames(menu.children); + } + }); +} +getNames(MOCK_MENU_LIST); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const { id, name } = getQuery(event); + + return (name as string) in namesMap && + (!id || namesMap[name as string] !== String(id)) + ? useResponseSuccess(true) + : useResponseSuccess(false); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/menu/path-exists.ts b/apps/vben5/apps/backend-mock/api/system/menu/path-exists.ts new file mode 100644 index 000000000..64774f790 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/menu/path-exists.ts @@ -0,0 +1,28 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { MOCK_MENU_LIST } from '~/utils/mock-data'; +import { unAuthorizedResponse } from '~/utils/response'; + +const pathMap: Record = { '/': 0 }; + +function getPaths(menus: any[]) { + menus.forEach((menu) => { + pathMap[menu.path] = String(menu.id); + if (menu.children) { + getPaths(menu.children); + } + }); +} +getPaths(MOCK_MENU_LIST); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const { id, path } = getQuery(event); + + return (path as string) in pathMap && + (!id || pathMap[path as string] !== String(id)) + ? useResponseSuccess(true) + : useResponseSuccess(false); +}); diff --git a/apps/vben5/apps/backend-mock/api/system/role/list.ts b/apps/vben5/apps/backend-mock/api/system/role/list.ts new file mode 100644 index 000000000..4d5f923e9 --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/system/role/list.ts @@ -0,0 +1,83 @@ +import { faker } from '@faker-js/faker'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data'; +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; + +const formatterCN = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +const menuIds = getMenuIds(MOCK_MENU_LIST); + +function generateMockDataList(count: number) { + const dataList = []; + + for (let i = 0; i < count; i++) { + const dataItem: Record = { + id: faker.string.uuid(), + name: faker.commerce.product(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), + ), + permissions: faker.helpers.arrayElements(menuIds), + remark: faker.lorem.sentence(), + }; + + dataList.push(dataItem); + } + + return dataList; +} + +const mockData = generateMockDataList(100); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + const { + page = 1, + pageSize = 20, + name, + id, + remark, + startTime, + endTime, + status, + } = getQuery(event); + let listData = structuredClone(mockData); + if (name) { + listData = listData.filter((item) => + item.name.toLowerCase().includes(String(name).toLowerCase()), + ); + } + if (id) { + listData = listData.filter((item) => + item.id.toLowerCase().includes(String(id).toLowerCase()), + ); + } + if (remark) { + listData = listData.filter((item) => + item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), + ); + } + if (startTime) { + listData = listData.filter((item) => item.createTime >= startTime); + } + if (endTime) { + listData = listData.filter((item) => item.createTime <= endTime); + } + if (['0', '1'].includes(status as string)) { + listData = listData.filter((item) => item.status === Number(status)); + } + return usePageResponseSuccess(page as string, pageSize as string, listData); +}); diff --git a/apps/vben5/apps/backend-mock/api/table/list.ts b/apps/vben5/apps/backend-mock/api/table/list.ts index 55b88eaaa..3e6f705b8 100644 --- a/apps/vben5/apps/backend-mock/api/table/list.ts +++ b/apps/vben5/apps/backend-mock/api/table/list.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; function generateMockDataList(count: number) { const dataList = []; diff --git a/apps/vben5/apps/backend-mock/api/upload.ts b/apps/vben5/apps/backend-mock/api/upload.ts new file mode 100644 index 000000000..1bb9e602d --- /dev/null +++ b/apps/vben5/apps/backend-mock/api/upload.ts @@ -0,0 +1,13 @@ +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse } from '~/utils/response'; + +export default eventHandler((event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + return useResponseSuccess({ + url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', + }); + // return useResponseError("test") +}); diff --git a/apps/vben5/apps/backend-mock/middleware/1.api.ts b/apps/vben5/apps/backend-mock/middleware/1.api.ts index 84e2ce0e0..bad9a41a1 100644 --- a/apps/vben5/apps/backend-mock/middleware/1.api.ts +++ b/apps/vben5/apps/backend-mock/middleware/1.api.ts @@ -1,7 +1,19 @@ -export default defineEventHandler((event) => { +import { forbiddenResponse, sleep } from '~/utils/response'; + +export default defineEventHandler(async (event) => { + event.node.res.setHeader( + 'Access-Control-Allow-Origin', + event.headers.get('Origin') ?? '*', + ); if (event.method === 'OPTIONS') { event.node.res.statusCode = 204; event.node.res.statusMessage = 'No Content.'; return 'OK'; + } else if ( + ['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) && + event.path.startsWith('/api/system/') + ) { + await sleep(Math.floor(Math.random() * 2000)); + return forbiddenResponse(event, '演示环境,禁止修改'); } }); diff --git a/apps/vben5/apps/backend-mock/nitro.config.ts b/apps/vben5/apps/backend-mock/nitro.config.ts index c2d7297fb..c0fc13e2e 100644 --- a/apps/vben5/apps/backend-mock/nitro.config.ts +++ b/apps/vben5/apps/backend-mock/nitro.config.ts @@ -9,7 +9,8 @@ export default defineNitroConfig({ cors: true, headers: { 'Access-Control-Allow-Credentials': 'true', - 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Headers': + 'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With', 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'Access-Control-Allow-Origin': '*', 'Access-Control-Expose-Headers': '*', diff --git a/apps/vben5/apps/backend-mock/routes/[...].ts b/apps/vben5/apps/backend-mock/routes/[...].ts index 70c5f7c74..99f544b66 100644 --- a/apps/vben5/apps/backend-mock/routes/[...].ts +++ b/apps/vben5/apps/backend-mock/routes/[...].ts @@ -7,6 +7,7 @@ export default defineEventHandler(() => {
  • /api/menu/all
  • /api/auth/codes
  • /api/auth/login
  • +
  • /api/upload
  • `; }); diff --git a/apps/vben5/apps/backend-mock/utils/cookie-utils.ts b/apps/vben5/apps/backend-mock/utils/cookie-utils.ts index 0d92f577d..78f3aab73 100644 --- a/apps/vben5/apps/backend-mock/utils/cookie-utils.ts +++ b/apps/vben5/apps/backend-mock/utils/cookie-utils.ts @@ -14,7 +14,7 @@ export function setRefreshTokenCookie( ) { setCookie(event, 'jwt', refreshToken, { httpOnly: true, - maxAge: 24 * 60 * 60 * 1000, + maxAge: 24 * 60 * 60, // unit: seconds sameSite: 'none', secure: true, }); diff --git a/apps/vben5/apps/backend-mock/utils/mock-data.ts b/apps/vben5/apps/backend-mock/utils/mock-data.ts index 8fa12fe57..192f30a00 100644 --- a/apps/vben5/apps/backend-mock/utils/mock-data.ts +++ b/apps/vben5/apps/backend-mock/utils/mock-data.ts @@ -185,3 +185,206 @@ export const MOCK_MENUS = [ username: 'jack', }, ]; + +export const MOCK_MENU_LIST = [ + { + id: 1, + name: 'Workspace', + status: 1, + type: 'menu', + icon: 'mdi:dashboard', + path: '/workspace', + component: '/dashboard/workspace/index', + meta: { + icon: 'carbon:workspace', + title: 'page.dashboard.workspace', + affixTab: true, + order: 0, + }, + }, + { + id: 2, + meta: { + icon: 'carbon:settings', + order: 9997, + title: 'system.title', + badge: 'new', + badgeType: 'normal', + badgeVariants: 'primary', + }, + status: 1, + type: 'catalog', + name: 'System', + path: '/system', + children: [ + { + id: 201, + pid: 2, + path: '/system/menu', + name: 'SystemMenu', + authCode: 'System:Menu:List', + status: 1, + type: 'menu', + meta: { + icon: 'carbon:menu', + title: 'system.menu.title', + }, + component: '/system/menu/list', + children: [ + { + id: 20_101, + pid: 201, + name: 'SystemMenuCreate', + status: 1, + type: 'button', + authCode: 'System:Menu:Create', + meta: { title: 'common.create' }, + }, + { + id: 20_102, + pid: 201, + name: 'SystemMenuEdit', + status: 1, + type: 'button', + authCode: 'System:Menu:Edit', + meta: { title: 'common.edit' }, + }, + { + id: 20_103, + pid: 201, + name: 'SystemMenuDelete', + status: 1, + type: 'button', + authCode: 'System:Menu:Delete', + meta: { title: 'common.delete' }, + }, + ], + }, + { + id: 202, + pid: 2, + path: '/system/dept', + name: 'SystemDept', + status: 1, + type: 'menu', + authCode: 'System:Dept:List', + meta: { + icon: 'carbon:container-services', + title: 'system.dept.title', + }, + component: '/system/dept/list', + children: [ + { + id: 20_401, + pid: 201, + name: 'SystemDeptCreate', + status: 1, + type: 'button', + authCode: 'System:Dept:Create', + meta: { title: 'common.create' }, + }, + { + id: 20_402, + pid: 201, + name: 'SystemDeptEdit', + status: 1, + type: 'button', + authCode: 'System:Dept:Edit', + meta: { title: 'common.edit' }, + }, + { + id: 20_403, + pid: 201, + name: 'SystemDeptDelete', + status: 1, + type: 'button', + authCode: 'System:Dept:Delete', + meta: { title: 'common.delete' }, + }, + ], + }, + ], + }, + { + id: 9, + meta: { + badgeType: 'dot', + order: 9998, + title: 'demos.vben.title', + icon: 'carbon:data-center', + }, + name: 'Project', + path: '/vben-admin', + type: 'catalog', + status: 1, + children: [ + { + id: 901, + pid: 9, + name: 'VbenDocument', + path: '/vben-admin/document', + component: 'IFrameView', + type: 'embedded', + status: 1, + meta: { + icon: 'carbon:book', + iframeSrc: 'https://doc.vben.pro', + title: 'demos.vben.document', + }, + }, + { + id: 902, + pid: 9, + name: 'VbenGithub', + path: '/vben-admin/github', + component: 'IFrameView', + type: 'link', + status: 1, + meta: { + icon: 'carbon:logo-github', + link: 'https://github.com/vbenjs/vue-vben-admin', + title: 'Github', + }, + }, + { + id: 903, + pid: 9, + name: 'VbenAntdv', + path: '/vben-admin/antdv', + component: 'IFrameView', + type: 'link', + status: 0, + meta: { + icon: 'carbon:hexagon-vertical-solid', + badgeType: 'dot', + link: 'https://ant.vben.pro', + title: 'demos.vben.antdv', + }, + }, + ], + }, + { + id: 10, + component: '_core/about/index', + type: 'menu', + status: 1, + meta: { + icon: 'lucide:copyright', + order: 9999, + title: 'demos.vben.about', + }, + name: 'About', + path: '/about', + }, +]; + +export function getMenuIds(menus: any[]) { + const ids: number[] = []; + menus.forEach((item) => { + ids.push(item.id); + if (item.children && item.children.length > 0) { + ids.push(...getMenuIds(item.children)); + } + }); + return ids; +} diff --git a/apps/vben5/apps/web-antd/.env b/apps/vben5/apps/web-antd/.env index c14a467fb..19735f36f 100644 --- a/apps/vben5/apps/web-antd/.env +++ b/apps/vben5/apps/web-antd/.env @@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Antd # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 VITE_APP_NAMESPACE=vben-web-antd + +# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 +VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key diff --git a/apps/vben5/apps/web-antd/package.json b/apps/vben5/apps/web-antd/package.json index 487622574..6dcb91848 100644 --- a/apps/vben5/apps/web-antd/package.json +++ b/apps/vben5/apps/web-antd/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-antd", - "version": "5.5.3", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/vben5/apps/web-antd/src/adapter/component/index.ts b/apps/vben5/apps/web-antd/src/adapter/component/index.ts index 17436a834..d452d5210 100644 --- a/apps/vben5/apps/web-antd/src/adapter/component/index.ts +++ b/apps/vben5/apps/web-antd/src/adapter/component/index.ts @@ -3,48 +3,103 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component, SetupContext } from 'vue'; +import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; +import type { Recordable } from '@vben/types'; -import { h } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + getCurrentInstance, + h, + ref, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { - AutoComplete, - Button, - Checkbox, - CheckboxGroup, - DatePicker, - Divider, - Input, - InputNumber, - InputPassword, - Mentions, - notification, - Radio, - RadioGroup, - RangePicker, - Rate, - Select, - Space, - Switch, - Textarea, - TimePicker, - TreeSelect, - Upload, -} from 'ant-design-vue'; +import { notification } from 'ant-design-vue'; + +const AutoComplete = defineAsyncComponent( + () => import('ant-design-vue/es/auto-complete'), +); +const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); +const Checkbox = defineAsyncComponent( + () => import('ant-design-vue/es/checkbox'), +); +const CheckboxGroup = defineAsyncComponent(() => + import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), +); +const DatePicker = defineAsyncComponent( + () => import('ant-design-vue/es/date-picker'), +); +const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider')); +const Input = defineAsyncComponent(() => import('ant-design-vue/es/input')); +const InputNumber = defineAsyncComponent( + () => import('ant-design-vue/es/input-number'), +); +const InputPassword = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.InputPassword), +); +const Mentions = defineAsyncComponent( + () => import('ant-design-vue/es/mentions'), +); +const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio')); +const RadioGroup = defineAsyncComponent(() => + import('ant-design-vue/es/radio').then((res) => res.RadioGroup), +); +const RangePicker = defineAsyncComponent(() => + import('ant-design-vue/es/date-picker').then((res) => res.RangePicker), +); +const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate')); +const Select = defineAsyncComponent(() => import('ant-design-vue/es/select')); +const Space = defineAsyncComponent(() => import('ant-design-vue/es/space')); +const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch')); +const Textarea = defineAsyncComponent(() => + import('ant-design-vue/es/input').then((res) => res.Textarea), +); +const TimePicker = defineAsyncComponent( + () => import('ant-design-vue/es/time-picker'), +); +const TreeSelect = defineAsyncComponent( + () => import('ant-design-vue/es/tree-select'), +); +const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); const withDefaultPlaceholder = ( component: T, type: 'input' | 'select', + componentProps: Recordable = {}, ) => { - return (props: any, { attrs, slots }: Omit) => { - const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); - return h(component, { ...props, ...attrs, placeholder }, slots); - }; + return defineComponent({ + name: component.name, + inheritAttrs: false, + setup: (props: any, { attrs, expose, slots }) => { + const placeholder = + props?.placeholder || + attrs?.placeholder || + $t(`ui.placeholder.${type}`); + // 透传组件暴露的方法 + const innerRef = ref(); + const publicApi: Recordable = {}; + expose(publicApi); + const instance = getCurrentInstance(); + instance?.proxy?.$nextTick(() => { + for (const key in innerRef.value) { + if (typeof innerRef.value[key] === 'function') { + publicApi[key] = innerRef.value[key]; + } + } + }); + return () => + h( + component, + { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, + slots, + ); + }, + }); }; // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 @@ -81,38 +136,34 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: Select, - loadingSlot: 'suffixIcon', - visibleEvent: 'onDropdownVisibleChange', - modelPropName: 'value', - }, - slots, - ); - }, - ApiTreeSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: TreeSelect, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - slots, - ); - }, + ApiSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiSelect', + }, + 'select', + { + component: Select, + loadingSlot: 'suffixIcon', + visibleEvent: 'onDropdownVisibleChange', + modelPropName: 'value', + }, + ), + ApiTreeSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiTreeSelect', + }, + 'select', + { + component: TreeSelect, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + optionsPropName: 'treeData', + visibleEvent: 'onVisibleChange', + }, + ), AutoComplete, Checkbox, CheckboxGroup, @@ -122,13 +173,11 @@ async function initComponentAdapter() { return h(Button, { ...props, attrs, type: 'default' }, slots); }, Divider, - IconPicker: (props, { attrs, slots }) => { - return h( - IconPicker, - { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs }, - slots, - ); - }, + IconPicker: withDefaultPlaceholder(IconPicker, 'select', { + iconSlot: 'addonAfter', + inputComponent: Input, + modelValueProp: 'value', + }), Input: withDefaultPlaceholder(Input, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'), diff --git a/apps/vben5/apps/web-antd/src/adapter/form.ts b/apps/vben5/apps/web-antd/src/adapter/form.ts index 65ff793b6..983a7f516 100644 --- a/apps/vben5/apps/web-antd/src/adapter/form.ts +++ b/apps/vben5/apps/web-antd/src/adapter/form.ts @@ -8,40 +8,42 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -setupVbenForm({ - config: { - // ant design vue组件库默认都是 v-model:value - baseModelPropName: 'value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', - // 一些组件是 v-model:checked 或者 v-model:fileList - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - Switch: 'checked', - Upload: 'fileList', + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - // 输入项目必填国际化适配 - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, }, - // 选择项目必填国际化适配 - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/vben5/apps/web-antd/src/adapter/vxe-table.ts b/apps/vben5/apps/web-antd/src/adapter/vxe-table.ts index d296b2050..7de2859de 100644 --- a/apps/vben5/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/vben5/apps/web-antd/src/adapter/vxe-table.ts @@ -1,3 +1,5 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; @@ -33,7 +35,7 @@ setupVbenVxeTable({ round: true, showOverflow: true, size: 'small', - }, + } as VxeTableGridOptions, }); // 表格配置项可以用 cellRender: { name: 'CellImage' }, diff --git a/apps/vben5/apps/web-antd/src/bootstrap.ts b/apps/vben5/apps/web-antd/src/bootstrap.ts index b46bfbcf2..ec7211254 100644 --- a/apps/vben5/apps/web-antd/src/bootstrap.ts +++ b/apps/vben5/apps/web-antd/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { registerLoadingDirective } from '@vben/common-ui/es/loading'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; @@ -12,6 +12,7 @@ import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; @@ -19,6 +20,9 @@ async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, @@ -30,6 +34,12 @@ async function bootstrap(namespace: string) { const app = createApp(App); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); @@ -40,11 +50,16 @@ async function bootstrap(namespace: string) { registerAccessDirective(app); // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); initTippy(app); // 配置路由及路由守卫 app.use(router); + // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); + app.use(MotionPlugin); + // 动态更新标题 watchEffect(() => { if (preferences.app.dynamicTitle) { diff --git a/apps/vben5/apps/web-antd/src/layouts/basic.vue b/apps/vben5/apps/web-antd/src/layouts/basic.vue index 51412956a..1481dc5a8 100644 --- a/apps/vben5/apps/web-antd/src/layouts/basic.vue +++ b/apps/vben5/apps/web-antd/src/layouts/basic.vue @@ -110,7 +110,7 @@ watch( async (enable) => { if (enable) { await updateWatermark({ - content: `${userStore.userInfo?.username}`, + content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, }); } else { destroyWatermark(); diff --git a/apps/vben5/apps/web-antd/src/router/guard.ts b/apps/vben5/apps/web-antd/src/router/guard.ts index cbb5235ec..a1ad6d88c 100644 --- a/apps/vben5/apps/web-antd/src/router/guard.ts +++ b/apps/vben5/apps/web-antd/src/router/guard.ts @@ -1,6 +1,6 @@ import type { Router } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; @@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) { // 记录已经加载的页面 const loadedPaths = new Set(); - router.beforeEach(async (to) => { + router.beforeEach((to) => { to.meta.loaded = loadedPaths.has(to.path); // 页面加载进度条 @@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) { return decodeURIComponent( (to.query?.redirect as string) || userStore.userInfo?.homePath || - DEFAULT_HOME_PATH, + preferences.app.defaultHomePath, ); } return true; @@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) { path: LOGIN_PATH, // 如不需要,直接删除 query query: - to.fullPath === DEFAULT_HOME_PATH + to.fullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(to.fullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 @@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) { accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); const redirectPath = (from.query.redirect ?? - (to.path === DEFAULT_HOME_PATH - ? userInfo.homePath || DEFAULT_HOME_PATH + (to.path === preferences.app.defaultHomePath + ? userInfo.homePath || preferences.app.defaultHomePath : to.fullPath)) as string; return { diff --git a/apps/vben5/apps/web-antd/src/router/routes/core.ts b/apps/vben5/apps/web-antd/src/router/routes/core.ts index 7218da228..949b0b65a 100644 --- a/apps/vben5/apps/web-antd/src/router/routes/core.ts +++ b/apps/vben5/apps/web-antd/src/router/routes/core.ts @@ -1,11 +1,12 @@ import type { RouteRecordRaw } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; -import { AuthPageLayout, BasicLayout } from '#/layouts'; import { $t } from '#/locales'; -import Login from '#/views/_core/authentication/login.vue'; +const BasicLayout = () => import('#/layouts/basic.vue'); +const AuthPageLayout = () => import('#/layouts/auth.vue'); /** 全局404页面 */ const fallbackNotFoundRoute: RouteRecordRaw = { component: () => import('#/views/_core/fallback/not-found.vue'), @@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [ }, name: 'Root', path: '/', - redirect: DEFAULT_HOME_PATH, + redirect: preferences.app.defaultHomePath, children: [], }, { @@ -50,7 +51,7 @@ const coreRoutes: RouteRecordRaw[] = [ { name: 'Login', path: 'login', - component: Login, + component: () => import('#/views/_core/authentication/login.vue'), meta: { title: $t('page.auth.login'), }, diff --git a/apps/vben5/apps/web-antd/src/store/auth.ts b/apps/vben5/apps/web-antd/src/store/auth.ts index 9d64d2058..bd496d1ee 100644 --- a/apps/vben5/apps/web-antd/src/store/auth.ts +++ b/apps/vben5/apps/web-antd/src/store/auth.ts @@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { notification } from 'ant-design-vue'; @@ -54,7 +55,9 @@ export const useAuthStore = defineStore('auth', () => { } else { onSuccess ? await onSuccess?.() - : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); + : await router.push( + userInfo.homePath || preferences.app.defaultHomePath, + ); } if (userInfo?.realName) { diff --git a/apps/vben5/apps/web-ele/.env b/apps/vben5/apps/web-ele/.env index 87cb3df14..bb57c8651 100644 --- a/apps/vben5/apps/web-ele/.env +++ b/apps/vben5/apps/web-ele/.env @@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Ele # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 VITE_APP_NAMESPACE=vben-web-ele + +# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 +VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key diff --git a/apps/vben5/apps/web-ele/package.json b/apps/vben5/apps/web-ele/package.json index 0b21070d7..386c36840 100644 --- a/apps/vben5/apps/web-ele/package.json +++ b/apps/vben5/apps/web-ele/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-ele", - "version": "5.5.3", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/vben5/apps/web-ele/src/adapter/component/index.ts b/apps/vben5/apps/web-ele/src/adapter/component/index.ts index 6152ed318..e2f533cf5 100644 --- a/apps/vben5/apps/web-ele/src/adapter/component/index.ts +++ b/apps/vben5/apps/web-ele/src/adapter/component/index.ts @@ -3,45 +3,160 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component, SetupContext } from 'vue'; +import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { h } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + getCurrentInstance, + h, + ref, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; -import { - ElButton, - ElCheckbox, - ElCheckboxButton, - ElCheckboxGroup, - ElDatePicker, - ElDivider, - ElInput, - ElInputNumber, - ElNotification, - ElRadio, - ElRadioButton, - ElRadioGroup, - ElSelectV2, - ElSpace, - ElSwitch, - ElTimePicker, - ElTreeSelect, - ElUpload, -} from 'element-plus'; +import { ElNotification } from 'element-plus'; + +const ElButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/button/index'), + import('element-plus/es/components/button/style/css'), + ]).then(([res]) => res.ElButton), +); +const ElCheckbox = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox/style/css'), + ]).then(([res]) => res.ElCheckbox), +); +const ElCheckboxButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox-button/style/css'), + ]).then(([res]) => res.ElCheckboxButton), +); +const ElCheckboxGroup = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/checkbox/index'), + import('element-plus/es/components/checkbox-group/style/css'), + ]).then(([res]) => res.ElCheckboxGroup), +); +const ElDatePicker = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/date-picker/index'), + import('element-plus/es/components/date-picker/style/css'), + ]).then(([res]) => res.ElDatePicker), +); +const ElDivider = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/divider/index'), + import('element-plus/es/components/divider/style/css'), + ]).then(([res]) => res.ElDivider), +); +const ElInput = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/input/index'), + import('element-plus/es/components/input/style/css'), + ]).then(([res]) => res.ElInput), +); +const ElInputNumber = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/input-number/index'), + import('element-plus/es/components/input-number/style/css'), + ]).then(([res]) => res.ElInputNumber), +); +const ElRadio = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio/style/css'), + ]).then(([res]) => res.ElRadio), +); +const ElRadioButton = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio-button/style/css'), + ]).then(([res]) => res.ElRadioButton), +); +const ElRadioGroup = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/radio/index'), + import('element-plus/es/components/radio-group/style/css'), + ]).then(([res]) => res.ElRadioGroup), +); +const ElSelectV2 = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/select-v2/index'), + import('element-plus/es/components/select-v2/style/css'), + ]).then(([res]) => res.ElSelectV2), +); +const ElSpace = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/space/index'), + import('element-plus/es/components/space/style/css'), + ]).then(([res]) => res.ElSpace), +); +const ElSwitch = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/switch/index'), + import('element-plus/es/components/switch/style/css'), + ]).then(([res]) => res.ElSwitch), +); +const ElTimePicker = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/time-picker/index'), + import('element-plus/es/components/time-picker/style/css'), + ]).then(([res]) => res.ElTimePicker), +); +const ElTreeSelect = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/tree-select/index'), + import('element-plus/es/components/tree-select/style/css'), + ]).then(([res]) => res.ElTreeSelect), +); +const ElUpload = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/upload/index'), + import('element-plus/es/components/upload/style/css'), + ]).then(([res]) => res.ElUpload), +); const withDefaultPlaceholder = ( component: T, type: 'input' | 'select', + componentProps: Recordable = {}, ) => { - return (props: any, { attrs, slots }: Omit) => { - const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); - return h(component, { ...props, ...attrs, placeholder }, slots); - }; + return defineComponent({ + name: component.name, + inheritAttrs: false, + setup: (props: any, { attrs, expose, slots }) => { + const placeholder = + props?.placeholder || + attrs?.placeholder || + $t(`ui.placeholder.${type}`); + // 透传组件暴露的方法 + const innerRef = ref(); + const publicApi: Recordable = {}; + expose(publicApi); + const instance = getCurrentInstance(); + instance?.proxy?.$nextTick(() => { + for (const key in innerRef.value) { + if (typeof innerRef.value[key] === 'function') { + publicApi[key] = innerRef.value[key]; + } + } + }); + return () => + h( + component, + { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, + slots, + ); + }, + }); }; // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 @@ -69,37 +184,33 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: ElSelectV2, - loadingSlot: 'loading', - visibleEvent: 'onVisibleChange', - }, - slots, - ); - }, - ApiTreeSelect: (props, { attrs, slots }) => { - return h( - ApiComponent, - { - placeholder: $t('ui.placeholder.select'), - ...props, - ...attrs, - component: ElTreeSelect, - props: { label: 'label', children: 'children' }, - nodeKey: 'value', - loadingSlot: 'loading', - optionsPropName: 'data', - visibleEvent: 'onVisibleChange', - }, - slots, - ); - }, + ApiSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiSelect', + }, + 'select', + { + component: ElSelectV2, + loadingSlot: 'loading', + visibleEvent: 'onVisibleChange', + }, + ), + ApiTreeSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiTreeSelect', + }, + 'select', + { + component: ElTreeSelect, + props: { label: 'label', children: 'children' }, + nodeKey: 'value', + loadingSlot: 'loading', + optionsPropName: 'data', + visibleEvent: 'onVisibleChange', + }, + ), Checkbox: ElCheckbox, CheckboxGroup: (props, { attrs, slots }) => { let defaultSlot; @@ -129,19 +240,11 @@ async function initComponentAdapter() { return h(ElButton, { ...props, attrs, type: 'primary' }, slots); }, Divider: ElDivider, - IconPicker: (props, { attrs, slots }) => { - return h( - IconPicker, - { - iconSlot: 'append', - modelValueProp: 'model-value', - inputComponent: ElInput, - ...props, - ...attrs, - }, - slots, - ); - }, + IconPicker: withDefaultPlaceholder(IconPicker, 'select', { + iconSlot: 'append', + modelValueProp: 'model-value', + inputComponent: ElInput, + }), Input: withDefaultPlaceholder(ElInput, 'input'), InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'), RadioGroup: (props, { attrs, slots }) => { diff --git a/apps/vben5/apps/web-ele/src/adapter/form.ts b/apps/vben5/apps/web-ele/src/adapter/form.ts index 13ae9c428..936c3fe4c 100644 --- a/apps/vben5/apps/web-ele/src/adapter/form.ts +++ b/apps/vben5/apps/web-ele/src/adapter/form.ts @@ -8,32 +8,34 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -setupVbenForm({ - config: { - modelPropNameMap: { - Upload: 'fileList', - CheckboxGroup: 'model-value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + modelPropNameMap: { + Upload: 'fileList', + CheckboxGroup: 'model-value', + }, }, - }, - defineRules: { - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; + defineRules: { + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, }, - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/vben5/apps/web-ele/src/adapter/vxe-table.ts b/apps/vben5/apps/web-ele/src/adapter/vxe-table.ts index 44b31eaed..40b8179d3 100644 --- a/apps/vben5/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/vben5/apps/web-ele/src/adapter/vxe-table.ts @@ -1,3 +1,5 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; @@ -33,7 +35,7 @@ setupVbenVxeTable({ round: true, showOverflow: true, size: 'small', - }, + } as VxeTableGridOptions, }); // 表格配置项可以用 cellRender: { name: 'CellImage' }, diff --git a/apps/vben5/apps/web-ele/src/bootstrap.ts b/apps/vben5/apps/web-ele/src/bootstrap.ts index b62094f28..e5befb5a7 100644 --- a/apps/vben5/apps/web-ele/src/bootstrap.ts +++ b/apps/vben5/apps/web-ele/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { registerLoadingDirective } from '@vben/common-ui'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; import '@vben/styles'; @@ -13,12 +13,17 @@ import { ElLoading } from 'element-plus'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, @@ -32,6 +37,12 @@ async function bootstrap(namespace: string) { // 注册Element Plus提供的v-loading指令 app.directive('loading', ElLoading.directive); + // 注册Vben提供的v-loading和v-spinning指令 + registerLoadingDirective(app, { + loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); @@ -42,11 +53,16 @@ async function bootstrap(namespace: string) { registerAccessDirective(app); // 初始化 tippy + const { initTippy } = await import('@vben/common-ui/es/tippy'); initTippy(app); // 配置路由及路由守卫 app.use(router); + // 配置Motion插件 + const { MotionPlugin } = await import('@vben/plugins/motion'); + app.use(MotionPlugin); + // 动态更新标题 watchEffect(() => { if (preferences.app.dynamicTitle) { diff --git a/apps/vben5/apps/web-ele/src/layouts/basic.vue b/apps/vben5/apps/web-ele/src/layouts/basic.vue index 51412956a..1481dc5a8 100644 --- a/apps/vben5/apps/web-ele/src/layouts/basic.vue +++ b/apps/vben5/apps/web-ele/src/layouts/basic.vue @@ -110,7 +110,7 @@ watch( async (enable) => { if (enable) { await updateWatermark({ - content: `${userStore.userInfo?.username}`, + content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, }); } else { destroyWatermark(); diff --git a/apps/vben5/apps/web-ele/src/router/guard.ts b/apps/vben5/apps/web-ele/src/router/guard.ts index cbb5235ec..a1ad6d88c 100644 --- a/apps/vben5/apps/web-ele/src/router/guard.ts +++ b/apps/vben5/apps/web-ele/src/router/guard.ts @@ -1,6 +1,6 @@ import type { Router } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; @@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) { // 记录已经加载的页面 const loadedPaths = new Set(); - router.beforeEach(async (to) => { + router.beforeEach((to) => { to.meta.loaded = loadedPaths.has(to.path); // 页面加载进度条 @@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) { return decodeURIComponent( (to.query?.redirect as string) || userStore.userInfo?.homePath || - DEFAULT_HOME_PATH, + preferences.app.defaultHomePath, ); } return true; @@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) { path: LOGIN_PATH, // 如不需要,直接删除 query query: - to.fullPath === DEFAULT_HOME_PATH + to.fullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(to.fullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 @@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) { accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); const redirectPath = (from.query.redirect ?? - (to.path === DEFAULT_HOME_PATH - ? userInfo.homePath || DEFAULT_HOME_PATH + (to.path === preferences.app.defaultHomePath + ? userInfo.homePath || preferences.app.defaultHomePath : to.fullPath)) as string; return { diff --git a/apps/vben5/apps/web-ele/src/router/routes/core.ts b/apps/vben5/apps/web-ele/src/router/routes/core.ts index 7218da228..949b0b65a 100644 --- a/apps/vben5/apps/web-ele/src/router/routes/core.ts +++ b/apps/vben5/apps/web-ele/src/router/routes/core.ts @@ -1,11 +1,12 @@ import type { RouteRecordRaw } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; -import { AuthPageLayout, BasicLayout } from '#/layouts'; import { $t } from '#/locales'; -import Login from '#/views/_core/authentication/login.vue'; +const BasicLayout = () => import('#/layouts/basic.vue'); +const AuthPageLayout = () => import('#/layouts/auth.vue'); /** 全局404页面 */ const fallbackNotFoundRoute: RouteRecordRaw = { component: () => import('#/views/_core/fallback/not-found.vue'), @@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [ }, name: 'Root', path: '/', - redirect: DEFAULT_HOME_PATH, + redirect: preferences.app.defaultHomePath, children: [], }, { @@ -50,7 +51,7 @@ const coreRoutes: RouteRecordRaw[] = [ { name: 'Login', path: 'login', - component: Login, + component: () => import('#/views/_core/authentication/login.vue'), meta: { title: $t('page.auth.login'), }, diff --git a/apps/vben5/apps/web-ele/src/store/auth.ts b/apps/vben5/apps/web-ele/src/store/auth.ts index 639fb0372..74fadfe24 100644 --- a/apps/vben5/apps/web-ele/src/store/auth.ts +++ b/apps/vben5/apps/web-ele/src/store/auth.ts @@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; -import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { ElNotification } from 'element-plus'; @@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => { } else { onSuccess ? await onSuccess?.() - : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); + : await router.push( + userInfo.homePath || preferences.app.defaultHomePath, + ); } if (userInfo?.realName) { diff --git a/apps/vben5/apps/web-ele/src/views/demos/form/basic.vue b/apps/vben5/apps/web-ele/src/views/demos/form/basic.vue index 771665a66..0ecab5864 100644 --- a/apps/vben5/apps/web-ele/src/views/demos/form/basic.vue +++ b/apps/vben5/apps/web-ele/src/views/demos/form/basic.vue @@ -1,7 +1,7 @@ + diff --git a/apps/vben5/docs/src/demos/vben-alert/confirm/index.vue b/apps/vben5/docs/src/demos/vben-alert/confirm/index.vue new file mode 100644 index 000000000..723445d96 --- /dev/null +++ b/apps/vben5/docs/src/demos/vben-alert/confirm/index.vue @@ -0,0 +1,75 @@ + + diff --git a/apps/vben5/docs/src/demos/vben-alert/prompt/index.vue b/apps/vben5/docs/src/demos/vben-alert/prompt/index.vue new file mode 100644 index 000000000..a19221347 --- /dev/null +++ b/apps/vben5/docs/src/demos/vben-alert/prompt/index.vue @@ -0,0 +1,118 @@ + + diff --git a/apps/vben5/docs/src/demos/vben-ellipsis-text/auto-display/index.vue b/apps/vben5/docs/src/demos/vben-ellipsis-text/auto-display/index.vue new file mode 100644 index 000000000..fb5a32a50 --- /dev/null +++ b/apps/vben5/docs/src/demos/vben-ellipsis-text/auto-display/index.vue @@ -0,0 +1,16 @@ + + diff --git a/apps/vben5/docs/src/en/guide/essentials/development.md b/apps/vben5/docs/src/en/guide/essentials/development.md index da7cfd8ce..9ff832b7b 100644 --- a/apps/vben5/docs/src/en/guide/essentials/development.md +++ b/apps/vben5/docs/src/en/guide/essentials/development.md @@ -98,8 +98,8 @@ The execution command is: `pnpm run [script]` or `npm run [script]`. "postinstall": "pnpm -r run stub --if-present", // Only allow using pnpm "preinstall": "npx only-allow pnpm", - // Install husky - "prepare": "is-ci || husky", + // Install lefthook + "prepare": "is-ci || lefthook install", // Preview the application "preview": "turbo-run preview", // Package specification check @@ -150,6 +150,73 @@ To run the `docs` application: pnpm dev:docs ``` +### Distinguishing Build Environments + +In actual business development, multiple environments are usually distinguished during the build process, such as the test environment `test` and the production environment `build`. + +At this point, you can modify three files and add corresponding script configurations to distinguish between production environments. + +Take the addition of the test environment `test` to `@vben/web-antd` as an example: + +- `apps\web-antd\package.json` + +```json +"scripts": { + "build:prod": "pnpm vite build --mode production", + "build:test": "pnpm vite build --mode test", + "build:analyze": "pnpm vite build --mode analyze", + "dev": "pnpm vite --mode development", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit --skipLibCheck" +} +``` + +Add the command `"build:test"` and change the original `"build"` to `"build:prod"` to avoid building packages for two environments simultaneously. + +- `package.json` + +```json +"scripts": { + "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build", + "build:analyze": "turbo build:analyze", + "build:antd": "pnpm run build --filter=@vben/web-antd", + "build-test:antd": "pnpm run build --filter=@vben/web-antd build:test", + + ······ +} +``` + +Add the command to build the test environment in the root directory `package.json`. + +- `turbo.json` + +```json +"tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": [ + "dist/**", + "dist.zip", + ".vitepress/dist.zip", + ".vitepress/dist/**" + ] + }, + + "build-test:antd": { + "dependsOn": ["@vben/web-antd#build:test"], + "outputs": ["dist/**"] + }, + + "@vben/web-antd#build:test": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + + ······ +``` + +Add the relevant dependent commands in `turbo.json`. + ## Public Static Resources If you need to use public static resources in the project, such as images, static HTML, etc., and you want to directly import them in the development process through `src="/xxx.png"`. diff --git a/apps/vben5/docs/src/en/guide/essentials/settings.md b/apps/vben5/docs/src/en/guide/essentials/settings.md index a3cd579ec..59a6c5c01 100644 --- a/apps/vben5/docs/src/en/guide/essentials/settings.md +++ b/apps/vben5/docs/src/en/guide/essentials/settings.md @@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/ console.log(import.meta.env.VITE_PROT); ``` -- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. ::: +- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. ::: @@ -60,6 +60,29 @@ VITE_INJECT_APP_LOADING=true VITE_ARCHIVER=true ``` +```bash [.env.production] +# Public Path for Resources, must start and end with / +VITE_BASE=/ + +# API URL +VITE_GLOB_API_URL=https://mock-napi.vben.pro/api + +# Whether to enable compression, can be set to none, brotli, gzip +VITE_COMPRESS=gzip + +# Whether to enable PWA +VITE_PWA=false + +# vue-router mode +VITE_ROUTER_HISTORY=hash + +# Whether to inject global loading +VITE_INJECT_APP_LOADING=true + +# Whether to generate dist.zip after packaging +VITE_ARCHIVER=true +``` + ::: ## Dynamic Configuration in Production Environment @@ -115,6 +138,27 @@ To add a new dynamically modifiable configuration item, simply follow the steps } ``` +- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as: + + ```ts + export function useAppConfig( + env: Record, + isProduction: boolean, + ): ApplicationConfig { + // In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable + const config = isProduction + ? window._VBEN_ADMIN_PRO_APP_CONF_ + : (env as VbenAdminProAppConfigRaw); + + const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++] + + return { + apiURL: VITE_GLOB_API_URL, + otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++] + }; + } + ``` + At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item. ```ts @@ -142,6 +186,7 @@ import { defineOverridesPreferences } from '@vben/preferences'; /** * @description Project configuration file * Only a part of the configuration in the project needs to be covered, and unnecessary configurations do not need to be covered. The default configuration will be automatically used + * !!! Please clear the cache after changing the configuration, otherwise it may not take effect */ export const overridesPreferences = defineOverridesPreferences({ // overrides @@ -162,8 +207,15 @@ const defaultPreferences: Preferences = { colorWeakMode: false, compact: false, contentCompact: 'wide', + contentCompactWidth: 1200, + contentPadding: 0, + contentPaddingBottom: 0, + contentPaddingLeft: 0, + contentPaddingRight: 0, + contentPaddingTop: 0, defaultAvatar: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp', + defaultHomePath: '/analytics', dynamicTitle: true, enableCheckUpdates: true, enablePreferences: true, @@ -171,10 +223,11 @@ const defaultPreferences: Preferences = { isMobile: false, layout: 'sidebar-nav', locale: 'zh-CN', - loginExpiredMode: 'modal', + loginExpiredMode: 'page', name: 'Vben Admin', preferencesButtonPosition: 'auto', watermark: false, + zIndex: 200, }, breadcrumb: { enable: true, @@ -190,18 +243,23 @@ const defaultPreferences: Preferences = { enable: true, icp: '', icpLink: '', + settingShow: true, }, footer: { - enable: true, + enable: false, fixed: false, + height: 32, }, header: { enable: true, + height: 50, hidden: false, + menuAlign: 'start', mode: 'fixed', }, logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { @@ -219,23 +277,31 @@ const defaultPreferences: Preferences = { sidebar: { autoActivateChild: false, collapsed: false, + collapsedButton: true, collapsedShowTitle: false, + collapseWidth: 60, enable: true, expandOnHover: true, - extraCollapse: true, + extraCollapse: false, + extraCollapsedWidth: 60, + fixedButton: true, hidden: false, - width: 230, + mixedWidth: 80, + width: 224, }, tabbar: { draggable: true, enable: true, - height: 36, + height: 38, keepAlive: true, + maxCount: 0, + middleClickToClose: false, persist: true, showIcon: true, showMaximize: true, showMore: true, styleType: 'chrome', + wheelable: true, }, theme: { builtinType: 'default', @@ -246,7 +312,7 @@ const defaultPreferences: Preferences = { mode: 'dark', radius: '0.5', semiDarkHeader: false, - semiDarkSidebar: true, + semiDarkSidebar: false, }, transition: { enable: true, @@ -260,9 +326,9 @@ const defaultPreferences: Preferences = { languageToggle: true, lockScreen: true, notification: true, + refresh: true, sidebarToggle: true, themeToggle: true, - refresh: true, }, }; ``` @@ -287,8 +353,22 @@ interface AppPreferences { compact: boolean; /** Whether to enable content compact mode */ contentCompact: ContentCompactType; + /** Content compact width */ + contentCompactWidth: number; + /** Content padding */ + contentPadding: number; + /** Content bottom padding */ + contentPaddingBottom: number; + /** Content left padding */ + contentPaddingLeft: number; + /** Content right padding */ + contentPaddingRight: number; + /** Content top padding */ + contentPaddingTop: number; // /** Default application avatar */ defaultAvatar: string; + /** Default homepage path */ + defaultHomePath: string; // /** Enable dynamic title */ dynamicTitle: boolean; /** Whether to enable update checks */ @@ -315,6 +395,8 @@ interface AppPreferences { * @zh_CN Whether to enable watermark */ watermark: boolean; + /** z-index */ + zIndex: number; } interface BreadcrumbPreferences { /** Whether breadcrumbs are enabled */ @@ -342,6 +424,8 @@ interface CopyrightPreferences { icp: string; /** Link to the ICP */ icpLink: string; + /** Whether to show in settings panel */ + settingShow?: boolean; } interface FooterPreferences { @@ -349,13 +433,19 @@ interface FooterPreferences { enable: boolean; /** Whether the footer is fixed */ fixed: boolean; + /** Footer height */ + height: number; } interface HeaderPreferences { /** Whether the header is enabled */ enable: boolean; + /** Header height */ + height: number; /** Whether the header is hidden, css-hidden */ hidden: boolean; + /** Header menu alignment */ + menuAlign: LayoutHeaderMenuAlignType; /** Header display mode */ mode: LayoutHeaderModeType; } @@ -363,6 +453,8 @@ interface HeaderPreferences { interface LogoPreferences { /** Whether the logo is visible */ enable: boolean; + /** Logo image fitting method */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** Logo URL */ source: string; } @@ -376,18 +468,30 @@ interface NavigationPreferences { styleType: NavigationStyleType; } interface SidebarPreferences { + /** Automatically activate child menu when clicking on directory */ + autoActivateChild: boolean; /** Whether the sidebar is collapsed */ collapsed: boolean; + /** Whether the sidebar collapse button is visible */ + collapsedButton: boolean; /** Whether to show title when sidebar is collapsed */ collapsedShowTitle: boolean; + /** Sidebar collapse width */ + collapseWidth: number; /** Whether the sidebar is visible */ enable: boolean; /** Menu auto-expand state */ expandOnHover: boolean; /** Whether the sidebar extension area is collapsed */ extraCollapse: boolean; + /** Sidebar extension area collapse width */ + extraCollapsedWidth: number; + /** Whether the sidebar fixed button is visible */ + fixedButton: boolean; /** Whether the sidebar is hidden - css */ hidden: boolean; + /** Mixed sidebar width */ + mixedWidth: number; /** Sidebar width */ width: number; } @@ -414,6 +518,10 @@ interface TabbarPreferences { height: number; /** Whether tab caching is enabled */ keepAlive: boolean; + /** Maximum number of tabs */ + maxCount: number; + /** Whether to close tab when middle-clicked */ + middleClickToClose: boolean; /** Whether tabs are persistent */ persist: boolean; /** Whether icons in multiple tabs are enabled */ @@ -424,6 +532,8 @@ interface TabbarPreferences { showMore: boolean; /** Tab style */ styleType: TabsStyleType; + /** Whether mouse wheel response is enabled */ + wheelable: boolean; } interface ThemePreferences { /** Built-in theme name */ @@ -511,5 +621,6 @@ interface Preferences { - The `overridesPreferences` method only needs to override a part of the configurations in the project. There's no need to override configurations that are not needed; they will automatically use the default settings. - Any configuration item can be overridden. You just need to override it within the `overridesPreferences` method. Do not modify the default configuration file. +- Please clear the cache after changing the configuration, otherwise it may not take effect. ::: diff --git a/apps/vben5/docs/src/en/guide/in-depth/access.md b/apps/vben5/docs/src/en/guide/in-depth/access.md index 05997d7d5..545dddabd 100644 --- a/apps/vben5/docs/src/en/guide/in-depth/access.md +++ b/apps/vben5/docs/src/en/guide/in-depth/access.md @@ -4,10 +4,11 @@ outline: deep # Access Control -The framework has built-in two types of access control methods: +The framework has built-in three types of access control methods: - Determining whether a menu or button can be accessed based on user roles - Determining whether a menu or button can be accessed through an API +- Mixed mode: Using both frontend and backend access control simultaneously ## Frontend Access Control @@ -151,6 +152,43 @@ const dashboardMenus = [ At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible. +## Mixed Access Control + +**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution. + +**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management. + +### Steps + +- Ensure the current mode is set to mixed access control + +Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`. + +```ts +import { defineOverridesPreferences } from '@vben/preferences'; + +export const overridesPreferences = defineOverridesPreferences({ + // overrides + app: { + accessMode: 'mixed', + }, +}); +``` + +- Configure frontend route permissions + +Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode. + +- Configure backend menu interface + +Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode. + +- Ensure roles and permissions match + +Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes. + +At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality. + ## Fine-grained Control of Buttons In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles. diff --git a/apps/vben5/docs/src/en/guide/in-depth/theme.md b/apps/vben5/docs/src/en/guide/in-depth/theme.md index 11c9c992b..164ac1797 100644 --- a/apps/vben5/docs/src/en/guide/in-depth/theme.md +++ b/apps/vben5/docs/src/en/guide/in-depth/theme.md @@ -28,9 +28,10 @@ You can check the list below to understand all the available variables. ```css :root { - --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, - 'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-family: + -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue', + arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; /* Default background color of ...etc */ --background: 0 0% 100%; @@ -322,9 +323,10 @@ type BuiltinThemeType = ```css :root { - --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, - 'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-family: + -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue', + arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; /* Default background color of ...etc */ --background: 0 0% 100%; diff --git a/apps/vben5/docs/src/en/guide/introduction/quick-start.md b/apps/vben5/docs/src/en/guide/introduction/quick-start.md index d4829dcef..757679c39 100644 --- a/apps/vben5/docs/src/en/guide/introduction/quick-start.md +++ b/apps/vben5/docs/src/en/guide/introduction/quick-start.md @@ -58,7 +58,7 @@ Open a terminal in your code directory and execute the following commands: cd vue-vben-admin # Enable the project-specified version of pnpm -corepack enable +npm i -g corepack # Install dependencies pnpm install diff --git a/apps/vben5/docs/src/en/guide/other/faq.md b/apps/vben5/docs/src/en/guide/other/faq.md index da908f893..50cc13359 100644 --- a/apps/vben5/docs/src/en/guide/other/faq.md +++ b/apps/vben5/docs/src/en/guide/other/faq.md @@ -18,7 +18,7 @@ If you encounter a problem, you can start looking from the following aspects: ## Dependency Issues -In a `Monorepo` project, it is necessary to develop the habit of executing `pnpm install` every time you `git pull` the code, as new dependency packages are often added. The project has already configured automatic execution of `pnpm install` in `.husky/git-merge`, but sometimes there might be issues. If it does not execute automatically, it is recommended to execute it manually once. +In a `Monorepo` project, it's important to get into the habit of running `pnpm install` after every `git pull` because new dependencies are often added. The project has configured automatic execution of `pnpm install` in `lefthook.yml`, but sometimes there might be issues. If it does not execute automatically, it is recommended to execute it manually once. ## About Cache Update Issues diff --git a/apps/vben5/docs/src/en/guide/project/standard.md b/apps/vben5/docs/src/en/guide/project/standard.md index 23770c548..e5417ce7c 100644 --- a/apps/vben5/docs/src/en/guide/project/standard.md +++ b/apps/vben5/docs/src/en/guide/project/standard.md @@ -33,8 +33,8 @@ The project integrates the following code verification tools: - [Prettier](https://prettier.io/) for code formatting - [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages - [Publint](https://publint.dev/) for checking the standard of npm packages -- [Lint Staged](https://github.com/lint-staged/lint-staged) for running code verification before git commits - [Cspell](https://cspell.org/) for checking spelling errors +- [lefthook](https://github.com/evilmartians/lefthook) for managing Git hooks, automatically running code checks and formatting before commits ## ESLint @@ -148,18 +148,66 @@ The cspell configuration file is `cspell.json`, which can be modified according Git hooks are generally combined with various lints to check code style during git commits. If the check fails, the commit will not proceed. Developers need to modify and resubmit. -### husky +### lefthook -One issue is that the check will verify all code, but we only want to check the code we are committing. This is where husky comes in. +One issue is that the check will verify all code, but we only want to check the code we are committing. This is where lefthook comes in. -The most effective solution is to perform Lint checks locally before committing. A common practice is to use husky or pre-commit to perform a Lint check before local submission. +The most effective solution is to perform Lint checks locally before committing. A common practice is to use lefthook to perform a Lint check before local submission. -The project defines corresponding hooks inside `.husky`. +The project defines corresponding hooks inside `lefthook.yml`: -#### How to Disable Husky +- `pre-commit`: Runs before commit, used for code formatting and checking -If you want to disable Husky, simply delete the .husky directory. + - `code-workspace`: Updates VSCode workspace configuration + - `lint-md`: Formats Markdown files + - `lint-vue`: Formats and checks Vue files + - `lint-js`: Formats and checks JavaScript/TypeScript files + - `lint-style`: Formats and checks style files + - `lint-package`: Formats package.json + - `lint-json`: Formats other JSON files -### lint-staged +- `post-merge`: Runs after merge, used for automatic dependency installation -Used for automatically fixing style issues of committed files. Its configuration file is `.lintstagedrc.mjs`, which can be modified according to project needs. + - `install`: Runs `pnpm install` to install new dependencies + +- `commit-msg`: Runs during commit, used for checking commit message format + - `commitlint`: Uses commitlint to check commit messages + +#### How to Disable lefthook + +If you want to disable lefthook, there are two ways: + +::: code-group + +```bash [Temporary disable] +git commit -m 'feat: add home page' --no-verify +``` + +```bash [Permanent disable] +# Simply delete the lefthook.yml file +rm lefthook.yml +``` + +::: + +#### How to Modify lefthook Configuration + +If you want to modify lefthook's configuration, you can edit the `lefthook.yml` file. For example: + +```yaml +pre-commit: + parallel: true # Execute tasks in parallel + jobs: + - name: lint-js + run: pnpm prettier --cache --ignore-unknown --write {staged_files} + glob: '*.{js,jsx,ts,tsx}' +``` + +Where: + +- `parallel`: Whether to execute tasks in parallel +- `jobs`: Defines the list of tasks to execute +- `name`: Task name +- `run`: Command to execute +- `glob`: File pattern to match +- `{staged_files}`: Represents the list of staged files diff --git a/apps/vben5/docs/src/guide/essentials/build.md b/apps/vben5/docs/src/guide/essentials/build.md index ab9828fd5..134a632a7 100644 --- a/apps/vben5/docs/src/guide/essentials/build.md +++ b/apps/vben5/docs/src/guide/essentials/build.md @@ -47,7 +47,7 @@ cd apps/web-antd/dist # 本地预览,默认端口8080 live-server # 指定端口 -live-server --port 9000 +live-server --port=9000 ``` ## 压缩 @@ -214,7 +214,7 @@ server { 使用 nginx 处理项目部署后的跨域问题 -1. 配置前端项目接口地址,在项目目录下的``.env.production`文件中配置: +1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置: ```bash VITE_GLOB_API_URL=/api diff --git a/apps/vben5/docs/src/guide/essentials/development.md b/apps/vben5/docs/src/guide/essentials/development.md index 556e4c9dd..cb55b6b5c 100644 --- a/apps/vben5/docs/src/guide/essentials/development.md +++ b/apps/vben5/docs/src/guide/essentials/development.md @@ -98,8 +98,8 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如 "postinstall": "pnpm -r run stub --if-present", // 只允许使用pnpm "preinstall": "npx only-allow pnpm", - // husky的安装 - "prepare": "is-ci || husky", + // lefthook的安装 + "prepare": "is-ci || lefthook install", // 预览应用 "preview": "turbo-run preview", // 包规范检查 @@ -150,6 +150,73 @@ pnpm dev:ele pnpm dev:docs ``` +## 区分构建环境 + +在实际的业务开发中,通常会在构建时区分多种环境,如测试环境`test`、生产环境`build`等。 + +此时可以修改三个文件,在其中增加对应的脚本配置来达到区分生产环境的效果。 + +以`@vben/web-antd`添加测试环境`test`为例: + +- `apps\web-antd\package.json` + +```json +"scripts": { + "build:prod": "pnpm vite build --mode production", + "build:test": "pnpm vite build --mode test", + "build:analyze": "pnpm vite build --mode analyze", + "dev": "pnpm vite --mode development", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit --skipLibCheck" +}, +``` + +增加命令`"build:test"`, 并将原`"build"`改为`"build:prod"`以避免同时构建两个环境的包。 + +- `package.json` + +```json +"scripts": { + "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build", + "build:analyze": "turbo build:analyze", + "build:antd": "pnpm run build --filter=@vben/web-antd", + "build-test:antd": "pnpm run build --filter=@vben/web-antd build:test", + + ······ +} +``` + +在根目录`package.json`中加入构建测试环境的命令 + +- `turbo.json` + +```json +"tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": [ + "dist/**", + "dist.zip", + ".vitepress/dist.zip", + ".vitepress/dist/**" + ] + }, + + "build-test:antd": { + "dependsOn": ["@vben/web-antd#build:test"], + "outputs": ["dist/**"] + }, + + "@vben/web-antd#build:test": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + + ······ +``` + +在`turbo.json`中加入相关依赖的命令 + ## 公共静态资源 项目中需要使用到的公共静态资源,如:图片、静态HTML等,需要在开发中通过 `src="/xxx.png"` 直接引入的。 diff --git a/apps/vben5/docs/src/guide/essentials/route.md b/apps/vben5/docs/src/guide/essentials/route.md index d8a938dda..985c48a9b 100644 --- a/apps/vben5/docs/src/guide/essentials/route.md +++ b/apps/vben5/docs/src/guide/essentials/route.md @@ -339,6 +339,10 @@ interface RouteMeta { | 'success' | 'warning' | string; + /** + * 路由的完整路径作为key(默认true) + */ + fullPathKey?: boolean; /** * 当前路由的子级在菜单中不展现 * @default false @@ -502,6 +506,13 @@ interface RouteMeta { 用于配置页面的徽标颜色。 +### fullPathKey + +- 类型:`boolean` +- 默认值:`true` + +是否将路由的完整路径作为tab key(默认true) + ### activePath - 类型:`string` @@ -602,3 +613,32 @@ const { refresh } = useRefresh(); refresh(); ``` + +## 标签页与路由控制 + +在某些场景下,需要单个路由打开多个标签页,或者修改路由的query不打开新的标签页 + +每个标签页Tab使用唯一的key标识,设置Tab key有三种方式,优先级由高到低: + +- 使用路由query参数pageKey + +```vue +