This commit is contained in:
dap 2025-05-04 17:23:32 +08:00
commit b0763d6429
60 changed files with 498 additions and 210 deletions

View File

@ -1,6 +0,0 @@
echo Start running commit-msg hook...
# Check whether the git commit information is standardized
pnpm exec commitlint --edit "$1"
echo Run commit-msg hook done.

View File

@ -1,3 +0,0 @@
# 每次 git pull 之后, 安装依赖
pnpm install

View File

@ -1,7 +0,0 @@
# update `.vscode/vben-admin.code-workspace` file
pnpm vsh code-workspace --auto-commit
# Format and submit code according to lintstagedrc.js configuration
pnpm exec lint-staged
echo Run pre-commit hook done.

View File

@ -1,20 +0,0 @@
export default {
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
],
'*.{scss,less,styl,html,vue,css}': [
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write --parser json',
],
};

2
.npmrc
View File

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

View File

@ -220,7 +220,7 @@
"*.env": "$(capture).env.*", "*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME", "README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json", "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json", "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*" "tailwind.config.mjs": "postcss.*"
}, },
"commentTranslate.hover.enabled": false, "commentTranslate.hover.enabled": false,

View File

@ -1,6 +1,6 @@
import type { Router } from 'vue-router'; 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 { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils'; import { startProgress, stopProgress } from '@vben/utils';
@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
return decodeURIComponent( return decodeURIComponent(
(to.query?.redirect as string) || (to.query?.redirect as string) ||
userStore.userInfo?.homePath || userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH, preferences.app.defaultHomePath,
); );
} }
return true; return true;
@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
path: LOGIN_PATH, path: LOGIN_PATH,
// 如不需要,直接删除 query // 如不需要,直接删除 query
query: query:
to.fullPath === DEFAULT_HOME_PATH to.fullPath === preferences.app.defaultHomePath
? {} ? {}
: { redirect: encodeURIComponent(to.fullPath) }, : { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面 // 携带当前跳转的页面,登录后重新跳转该页面
@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true); accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH (to.path === preferences.app.defaultHomePath
? userInfo.homePath || DEFAULT_HOME_PATH ? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string; : to.fullPath)) as string;
return { return {

View File

@ -1,6 +1,7 @@
import type { RouteRecordRaw } from 'vue-router'; 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 { $t } from '#/locales'; import { $t } from '#/locales';
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
}, },
name: 'Root', name: 'Root',
path: '/', path: '/',
redirect: DEFAULT_HOME_PATH, redirect: preferences.app.defaultHomePath,
children: [], children: [],
}, },
{ {

View File

@ -4,7 +4,8 @@ import type { UserInfo } from '@vben/types';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; 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 { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
if (accessStore.loginExpired) { if (accessStore.loginExpired) {
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
} else { } else {
onSuccess ? await onSuccess?.() : await router.push(DEFAULT_HOME_PATH); onSuccess
? await onSuccess?.()
: await router.push(preferences.app.defaultHomePath);
} }
if (userInfo?.realName) { if (userInfo?.realName) {

View File

@ -78,7 +78,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
| --- | --- | --- | --- | | --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` | | appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - | | connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` | | destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| title | 标题 | `string\|slot` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |

View File

@ -98,8 +98,8 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
"postinstall": "pnpm -r run stub --if-present", "postinstall": "pnpm -r run stub --if-present",
// Only allow using pnpm // Only allow using pnpm
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
// Install husky // Install lefthook
"prepare": "is-ci || husky", "prepare": "is-ci || lefthook install",
// Preview the application // Preview the application
"preview": "turbo-run preview", "preview": "turbo-run preview",
// Package specification check // Package specification check

View File

@ -164,6 +164,7 @@ const defaultPreferences: Preferences = {
contentCompact: 'wide', contentCompact: 'wide',
defaultAvatar: defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp', 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
dynamicTitle: true, dynamicTitle: true,
enableCheckUpdates: true, enableCheckUpdates: true,
enablePreferences: true, enablePreferences: true,
@ -289,6 +290,8 @@ interface AppPreferences {
contentCompact: ContentCompactType; contentCompact: ContentCompactType;
// /** Default application avatar */ // /** Default application avatar */
defaultAvatar: string; defaultAvatar: string;
/** Default homepage path */
defaultHomePath: string;
// /** Enable dynamic title */ // /** Enable dynamic title */
dynamicTitle: boolean; dynamicTitle: boolean;
/** Whether to enable update checks */ /** Whether to enable update checks */

View File

@ -18,7 +18,7 @@ If you encounter a problem, you can start looking from the following aspects:
## Dependency Issues ## 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 ## About Cache Update Issues

View File

@ -33,8 +33,8 @@ The project integrates the following code verification tools:
- [Prettier](https://prettier.io/) for code formatting - [Prettier](https://prettier.io/) for code formatting
- [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages - [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages
- [Publint](https://publint.dev/) for checking the standard of npm packages - [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 - [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 ## 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. 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

View File

@ -98,8 +98,8 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
"postinstall": "pnpm -r run stub --if-present", "postinstall": "pnpm -r run stub --if-present",
// 只允许使用pnpm // 只允许使用pnpm
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
// husky的安装 // lefthook的安装
"prepare": "is-ci || husky", "prepare": "is-ci || lefthook install",
// 预览应用 // 预览应用
"preview": "turbo-run preview", "preview": "turbo-run preview",
// 包规范检查 // 包规范检查

View File

@ -187,6 +187,7 @@ const defaultPreferences: Preferences = {
contentCompact: 'wide', contentCompact: 'wide',
defaultAvatar: defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp', 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
dynamicTitle: true, dynamicTitle: true,
enableCheckUpdates: true, enableCheckUpdates: true,
enablePreferences: true, enablePreferences: true,
@ -312,6 +313,8 @@ interface AppPreferences {
contentCompact: ContentCompactType; contentCompact: ContentCompactType;
// /** 应用默认头像 */ // /** 应用默认头像 */
defaultAvatar: string; defaultAvatar: string;
/** 默认首页地址 */
defaultHomePath: string;
// /** 开启动态标题 */ // /** 开启动态标题 */
dynamicTitle: boolean; dynamicTitle: boolean;
/** 是否开启检查更新 */ /** 是否开启检查更新 */

View File

@ -18,7 +18,7 @@
## 依赖问题 ## 依赖问题
`Monorepo` 项目下,需要养成每次 `git pull`代码都要执行`pnpm install`的习惯,因为经常会有新的依赖包加入,项目在`.husky/git-merge`已经配置了自动执行`pnpm install`,但是有时候会出现问题,如果没有自动执行,建议手动执行一次。 `Monorepo` 项目下,需要养成每次 `git pull`代码都要执行`pnpm install`的习惯,因为经常会有新的依赖包加入,项目在`lefthook.yml`已经配置了自动执行`pnpm install`,但是有时候会出现问题,如果没有自动执行,建议手动执行一次。
## 关于缓存更新问题 ## 关于缓存更新问题

View File

@ -33,8 +33,8 @@
- [Prettier](https://prettier.io/) 用于代码格式化 - [Prettier](https://prettier.io/) 用于代码格式化
- [Commitlint](https://commitlint.js.org/) 用于检查 git 提交信息的规范 - [Commitlint](https://commitlint.js.org/) 用于检查 git 提交信息的规范
- [Publint](https://publint.dev/) 用于检查 npm 包的规范 - [Publint](https://publint.dev/) 用于检查 npm 包的规范
- [Lint Staged](https://github.com/lint-staged/lint-staged) 用于在 git 提交前运行代码校验
- [Cspell](https://cspell.org/) 用于检查拼写错误 - [Cspell](https://cspell.org/) 用于检查拼写错误
- [lefthook](https://github.com/evilmartians/lefthook) 用于管理 Git hooks在提交前自动运行代码校验和格式化
## ESLint ## ESLint
@ -148,18 +148,66 @@ cspell 配置文件为 `cspell.json`,可以根据项目需求进行修改。
git hook 一般结合各种 lint在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交 git hook 一般结合各种 lint在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
### husky ### lefthook
有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 husky 有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 lefthook
最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 husky 或者 pre-commit 在本地提交之前先做一次 Lint 校验。 最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 lefthook 在本地提交之前先做一次 Lint 校验。
项目在 `.husky` 内部定义了相应的 hooks 项目在 `lefthook.yml` 内部定义了相应的 hooks
#### 如何关闭 Husky - `pre-commit`: 在提交前运行,用于代码格式化和检查
如果你想关闭 Husky直接删除 `.husky` 目录即可。 - `code-workspace`: 更新 VSCode 工作区配置
- `lint-md`: 格式化 Markdown 文件
- `lint-vue`: 格式化并检查 Vue 文件
- `lint-js`: 格式化并检查 JavaScript/TypeScript 文件
- `lint-style`: 格式化并检查样式文件
- `lint-package`: 格式化 package.json
- `lint-json`: 格式化其他 JSON 文件
### lint-staged - `post-merge`: 在合并后运行,用于自动安装依赖
用于自动修复提交文件风格问题,其配置文件为 `.lintstagedrc.mjs`,可以根据项目需求进行修改。 - `install`: 运行 `pnpm install` 安装新依赖
- `commit-msg`: 在提交时运行,用于检查提交信息格式
- `commitlint`: 使用 commitlint 检查提交信息
#### 如何关闭 lefthook
如果你想关闭 lefthook有两种方式
::: code-group
```bash [临时关闭]
git commit -m 'feat: add home page' --no-verify
```
```bash [永久关闭]
# 删除 lefthook.yml 文件即可
rm lefthook.yml
```
:::
#### 如何修改 lefthook 配置
如果你想修改 lefthook 的配置,可以编辑 `lefthook.yml` 文件。例如:
```yaml
pre-commit:
parallel: true # 并行执行任务
jobs:
- name: lint-js
run: pnpm prettier --cache --ignore-unknown --write {staged_files}
glob: '*.{js,jsx,ts,tsx}'
```
其中:
- `parallel`: 是否并行执行任务
- `jobs`: 定义要执行的任务列表
- `name`: 任务名称
- `run`: 要执行的命令
- `glob`: 匹配的文件模式
- `{staged_files}`: 表示暂存的文件列表

View File

@ -70,7 +70,7 @@ export async function perfectionist(): Promise<Linter.Config[]> {
}, },
], ],
'perfectionist/sort-objects': [ 'perfectionist/sort-objects': [
'error', 'off',
{ {
customGroups: { customGroups: {
items: 'items', items: 'items',

View File

@ -28,6 +28,13 @@ const customConfig: Linter.Config[] = [
'perfectionist/sort-objects': 'off', 'perfectionist/sort-objects': 'off',
}, },
}, },
{
files: ['**/**.vue'],
ignores: restrictedImportIgnores,
rules: {
'perfectionist/sort-objects': 'off',
},
},
{ {
// apps内部的一些基础规则 // apps内部的一些基础规则
files: ['apps/**/**'], files: ['apps/**/**'],

76
lefthook.yml Normal file
View File

@ -0,0 +1,76 @@
# EXAMPLE USAGE:
#
# Refer for explanation to following link:
# https://lefthook.dev/configuration/
#
# pre-push:
# jobs:
# - name: packages audit
# tags:
# - frontend
# - security
# run: yarn audit
#
# - name: gems audit
# tags:
# - backend
# - security
# run: bundle audit
#
# pre-commit:
# parallel: true
# jobs:
# - run: yarn eslint {staged_files}
# glob: "*.{js,ts,jsx,tsx}"
#
# - name: rubocop
# glob: "*.rb"
# exclude:
# - config/application.rb
# - config/routes.rb
# run: bundle exec rubocop --force-exclusion {all_files}
#
# - name: govet
# files: git ls-files -m
# glob: "*.go"
# run: go vet {files}
#
# - script: "hello.js"
# runner: node
#
# - script: "hello.go"
# runner: go run
pre-commit:
parallel: true
commands:
code-workspace:
run: pnpm vsh code-workspace --auto-commit
lint-md:
run: pnpm prettier --cache --ignore-unknown --write {staged_files}
glob: '*.md'
lint-vue:
run: pnpm prettier --write {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.vue'
lint-js:
run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm eslint --cache --fix {staged_files}
glob: '*.{js,jsx,ts,tsx}'
lint-style:
run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.{scss,less,styl,html,vue,css}'
lint-package:
run: pnpm prettier --cache --write {staged_files}
glob: 'package.json'
lint-json:
run: pnpm prettier --cache --write --parser json {staged_files}
glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}'
post-merge:
commands:
install:
run: pnpm install
commit-msg:
commands:
commitlint:
run: pnpm exec commitlint --edit $1

View File

@ -48,14 +48,14 @@
"lint": "vsh lint", "lint": "vsh lint",
"postinstall": "pnpm -r run stub --if-present", "postinstall": "pnpm -r run stub --if-present",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"prepare": "is-ci || husky",
"preview": "turbo-run preview", "preview": "turbo-run preview",
"publint": "vsh publint", "publint": "vsh publint",
"reinstall": "pnpm clean --del-lock && pnpm install", "reinstall": "pnpm clean --del-lock && pnpm install",
"test:unit": "vitest run --dom", "test:unit": "vitest run --dom",
"test:e2e": "turbo run test:e2e", "test:e2e": "turbo run test:e2e",
"update:deps": "npx taze -r -w", "update:deps": "npx taze -r -w",
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile" "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile",
"catalog": "pnpx codemod pnpm/catalog"
}, },
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "catalog:", "@changesets/changelog-github": "catalog:",
@ -78,9 +78,8 @@
"cross-env": "catalog:", "cross-env": "catalog:",
"cspell": "catalog:", "cspell": "catalog:",
"happy-dom": "catalog:", "happy-dom": "catalog:",
"husky": "catalog:",
"is-ci": "catalog:", "is-ci": "catalog:",
"lint-staged": "catalog:", "lefthook": "catalog:",
"playwright": "catalog:", "playwright": "catalog:",
"rimraf": "catalog:", "rimraf": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",

View File

@ -11,6 +11,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"compact": false, "compact": false,
"contentCompact": "wide", "contentCompact": "wide",
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp", "defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
"defaultHomePath": "/analytics",
"dynamicTitle": true, "dynamicTitle": true,
"enableCheckUpdates": true, "enableCheckUpdates": true,
"enablePreferences": true, "enablePreferences": true,

View File

@ -11,6 +11,7 @@ const defaultPreferences: Preferences = {
contentCompact: 'wide', contentCompact: 'wide',
defaultAvatar: defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp', 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
dynamicTitle: true, dynamicTitle: true,
enableCheckUpdates: true, enableCheckUpdates: true,
enablePreferences: true, enablePreferences: true,

View File

@ -35,6 +35,8 @@ interface AppPreferences {
contentCompact: ContentCompactType; contentCompact: ContentCompactType;
// /** 应用默认头像 */ // /** 应用默认头像 */
defaultAvatar: string; defaultAvatar: string;
/** 默认首页地址 */
defaultHomePath: string;
// /** 开启动态标题 */ // /** 开启动态标题 */
dynamicTitle: boolean; dynamicTitle: boolean;
/** 是否开启检查更新 */ /** 是否开启检查更新 */

View File

@ -38,6 +38,7 @@
}, },
"dependencies": { "dependencies": {
"@vben-core/composables": "workspace:*", "@vben-core/composables": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*", "@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",

View File

@ -5,6 +5,7 @@ import type { FormSchema, MaybeComponentProps } from '../types';
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue'; import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
import { CircleAlert } from '@vben-core/icons';
import { import {
FormControl, FormControl,
FormDescription, FormDescription,
@ -12,6 +13,7 @@ import {
FormItem, FormItem,
FormMessage, FormMessage,
VbenRenderContent, VbenRenderContent,
VbenTooltip,
} from '@vben-core/shadcn-ui'; } from '@vben-core/shadcn-ui';
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils'; import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
@ -354,6 +356,24 @@ onUnmounted(() => {
</template> </template>
<!-- <slot></slot> --> <!-- <slot></slot> -->
</component> </component>
<VbenTooltip
v-if="compact && isInValid"
:delay-duration="300"
side="left"
>
<template #trigger>
<slot name="trigger">
<CircleAlert
:class="
cn(
'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',
)
"
/>
</slot>
</template>
<FormMessage />
</VbenTooltip>
</slot> </slot>
</FormControl> </FormControl>
<!-- 自定义后缀 --> <!-- 自定义后缀 -->
@ -365,7 +385,7 @@ onUnmounted(() => {
</FormDescription> </FormDescription>
</div> </div>
<Transition name="slide-up"> <Transition name="slide-up" v-if="!compact">
<FormMessage class="absolute bottom-1" /> <FormMessage class="absolute bottom-1" />
</Transition> </Transition>
</div> </div>

View File

@ -31,8 +31,8 @@ export function useVbenForm<
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots); h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
}, },
{ {
inheritAttrs: false,
name: 'VbenUseForm', name: 'VbenUseForm',
inheritAttrs: false,
}, },
); );
// Add reactivity support // Add reactivity support

View File

@ -31,6 +31,7 @@ import {
createSubMenuContext, createSubMenuContext,
useMenuStyle, useMenuStyle,
} from '../hooks'; } from '../hooks';
import { useMenuScroll } from '../hooks/use-menu-scroll';
import { flattedChildren } from '../utils'; import { flattedChildren } from '../utils';
import SubMenu from './sub-menu.vue'; import SubMenu from './sub-menu.vue';
@ -44,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
mode: 'vertical', mode: 'vertical',
rounded: true, rounded: true,
theme: 'dark', theme: 'dark',
scrollToActive: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -206,15 +208,19 @@ function handleResize() {
isFirstTimeRender = false; isFirstTimeRender = false;
} }
function getActivePaths() { const enableScroll = computed(
const activeItem = activePath.value && items.value[activePath.value]; () => props.scrollToActive && props.mode === 'vertical' && !props.collapse,
);
if (!activeItem || props.mode === 'horizontal' || props.collapse) { const { scrollToActiveItem } = useMenuScroll(activePath, {
return []; enable: enableScroll,
} delay: 320,
});
return activeItem.parentPaths; // activePath
} watch(activePath, () => {
scrollToActiveItem();
});
// //
function initMenu() { function initMenu() {
@ -318,6 +324,16 @@ function removeSubMenu(subMenu: MenuItemRegistered) {
function removeMenuItem(item: MenuItemRegistered) { function removeMenuItem(item: MenuItemRegistered) {
Reflect.deleteProperty(items.value, item.path); Reflect.deleteProperty(items.value, item.path);
} }
function getActivePaths() {
const activeItem = activePath.value && items.value[activePath.value];
if (!activeItem || props.mode === 'horizontal' || props.collapse) {
return [];
}
return activeItem.parentPaths;
}
</script> </script>
<template> <template>
<ul <ul

View File

@ -0,0 +1,46 @@
import type { Ref } from 'vue';
import { watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
interface UseMenuScrollOptions {
delay?: number;
enable?: boolean | Ref<boolean>;
}
export function useMenuScroll(
activePath: Ref<string | undefined>,
options: UseMenuScrollOptions = {},
) {
const { enable = true, delay = 320 } = options;
function scrollToActiveItem() {
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
if (!isEnabled) return;
const activeElement = document.querySelector(
`aside li[role=menuitem].is-active`,
);
if (activeElement) {
activeElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
}
const debouncedScroll = useDebounceFn(scrollToActiveItem, delay);
watch(activePath, () => {
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
if (!isEnabled) return;
debouncedScroll();
});
return {
scrollToActiveItem,
};
}

View File

@ -18,15 +18,9 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
collapse: false, collapse: false,
// theme: 'dark',
}); });
const forward = useForwardProps(props); const forward = useForwardProps(props);
// const emit = defineEmits<{
// 'update:openKeys': [key: Key[]];
// 'update:selectedKeys': [key: Key[]];
// }>();
</script> </script>
<template> <template>

View File

@ -41,6 +41,12 @@ interface MenuProps {
*/ */
rounded?: boolean; rounded?: boolean;
/**
* @zh_CN
* @default false
*/
scrollToActive?: boolean;
/** /**
* @zh_CN * @zh_CN
* @default dark * @default dark

View File

@ -35,7 +35,7 @@ interface Props extends DrawerProps {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
appendToMain: false, appendToMain: false,
closeIconPlacement: 'right', closeIconPlacement: 'right',
destroyOnClose: true, destroyOnClose: false,
drawerApi: undefined, drawerApi: undefined,
submitting: false, submitting: false,
zIndex: 1000, zIndex: 1000,

View File

@ -21,9 +21,7 @@ import VbenDrawer from './drawer.vue';
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT'); const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = { const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};
destroyOnClose: true,
};
export function setDefaultDrawerProps(props: Partial<DrawerProps>) { export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
Object.assign(DEFAULT_DRAWER_PROPS, props); Object.assign(DEFAULT_DRAWER_PROPS, props);
@ -66,9 +64,10 @@ export function useVbenDrawer<
slots, slots,
); );
}, },
// eslint-disable-next-line vue/one-component-per-file
{ {
inheritAttrs: false,
name: 'VbenParentDrawer', name: 'VbenParentDrawer',
inheritAttrs: false,
}, },
); );
return [Drawer, extendedApi as ExtendedDrawerApi] as const; return [Drawer, extendedApi as ExtendedDrawerApi] as const;
@ -107,9 +106,10 @@ export function useVbenDrawer<
return () => return () =>
h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots); h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
}, },
// eslint-disable-next-line vue/one-component-per-file
{ {
inheritAttrs: false,
name: 'VbenDrawer', name: 'VbenDrawer',
inheritAttrs: false,
}, },
); );
injectData.extendApi?.(extendedApi); injectData.extendApi?.(extendedApi);

View File

@ -34,7 +34,7 @@ interface Props extends ModalProps {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
appendToMain: false, appendToMain: false,
destroyOnClose: true, destroyOnClose: false,
modalApi: undefined, modalApi: undefined,
}); });

View File

@ -17,9 +17,7 @@ import VbenModal from './modal.vue';
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT'); const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = { const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {};
destroyOnClose: true,
};
export function setDefaultModalProps(props: Partial<ModalProps>) { export function setDefaultModalProps(props: Partial<ModalProps>) {
Object.assign(DEFAULT_MODAL_PROPS, props); Object.assign(DEFAULT_MODAL_PROPS, props);
@ -65,9 +63,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
slots, slots,
); );
}, },
// eslint-disable-next-line vue/one-component-per-file
{ {
inheritAttrs: false,
name: 'VbenParentModal', name: 'VbenParentModal',
inheritAttrs: false,
}, },
); );
return [Modal, extendedApi as ExtendedModalApi] as const; return [Modal, extendedApi as ExtendedModalApi] as const;
@ -114,9 +113,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
slots, slots,
); );
}, },
// eslint-disable-next-line vue/one-component-per-file
{ {
inheritAttrs: false,
name: 'VbenModal', name: 'VbenModal',
inheritAttrs: false,
}, },
); );
injectData.extendApi?.(extendedApi); injectData.extendApi?.(extendedApi);

View File

@ -21,6 +21,7 @@ interface Props extends PopoverRootProps {
class?: ClassType; class?: ClassType;
contentClass?: ClassType; contentClass?: ClassType;
contentProps?: PopoverContentProps; contentProps?: PopoverContentProps;
triggerClass?: ClassType;
} }
const props = withDefaults(defineProps<Props>(), {}); const props = withDefaults(defineProps<Props>(), {});
@ -32,6 +33,7 @@ const delegatedProps = computed(() => {
class: _cls, class: _cls,
contentClass: _, contentClass: _,
contentProps: _cProps, contentProps: _cProps,
triggerClass: _tClass,
...delegated ...delegated
} = props; } = props;
@ -43,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template> <template>
<PopoverRoot v-bind="forwarded"> <PopoverRoot v-bind="forwarded">
<PopoverTrigger> <PopoverTrigger :class="triggerClass">
<slot name="trigger"></slot> <slot name="trigger"></slot>
<PopoverContent <PopoverContent

View File

@ -12,7 +12,6 @@ interface Props extends TabsProps {}
defineOptions({ defineOptions({
name: 'VbenTabsChrome', name: 'VbenTabsChrome',
// eslint-disable-next-line perfectionist/sort-objects
inheritAttrs: false, inheritAttrs: false,
}); });

View File

@ -12,7 +12,7 @@ interface Props extends TabsProps {}
defineOptions({ defineOptions({
name: 'VbenTabs', name: 'VbenTabs',
// eslint-disable-next-line perfectionist/sort-objects
inheritAttrs: false, inheritAttrs: false,
}); });
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {

View File

@ -15,5 +15,5 @@ pnpm add @vben/constants
### 使用 ### 使用
```ts ```ts
import { DEFAULT_HOME_PATH } from '@vben/constants'; import { LOGIN_PATH } from '@vben/constants';
``` ```

View File

@ -3,11 +3,6 @@
*/ */
export const LOGIN_PATH = '/auth/login'; export const LOGIN_PATH = '/auth/login';
/**
* @zh_CN
*/
export const DEFAULT_HOME_PATH = '/analytics';
export interface LanguageOption { export interface LanguageOption {
label: string; label: string;
value: 'en-US' | 'zh-CN'; value: 'en-US' | 'zh-CN';

View File

@ -74,7 +74,7 @@ async function generateAccessible(
} }
// 生成菜单 // 生成菜单
const accessibleMenus = await generateMenus(accessibleRoutes, options.router); const accessibleMenus = generateMenus(accessibleRoutes, options.router);
return { accessibleMenus, accessibleRoutes }; return { accessibleMenus, accessibleRoutes };
} }

View File

@ -165,13 +165,18 @@ const searchInputProps = computed(() => {
}; };
}); });
function updateCurrentSelect(v: string) {
currentSelect.value = v;
}
defineExpose({ toggleOpenState, open, close }); defineExpose({ toggleOpenState, open, close });
</script> </script>
<template> <template>
<VbenPopover <VbenPopover
v-model:open="visible" v-model:open="visible"
:content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }" :content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
content-class="p-0 pt-3" content-class="p-0 pt-3 w-full"
trigger-class="w-full"
> >
<template #trigger> <template #trigger>
<template v-if="props.type === 'input'"> <template v-if="props.type === 'input'">
@ -183,6 +188,7 @@ defineExpose({ toggleOpenState, open, close });
role="combobox" role="combobox"
:aria-label="$t('ui.iconPicker.placeholder')" :aria-label="$t('ui.iconPicker.placeholder')"
aria-expanded="visible" aria-expanded="visible"
:[`onUpdate:${modelValueProp}`]="updateCurrentSelect"
v-bind="$attrs" v-bind="$attrs"
> >
<template #[iconSlot]> <template #[iconSlot]>

View File

@ -35,6 +35,16 @@ const getZIndex = computed(() => {
return props.zIndex || calcZIndex(); return props.zIndex || calcZIndex();
}); });
/**
* 排除ant-message和loading:9999的z-index
*/
const zIndexExcludeClass = ['ant-message', 'loading'];
function isZIndexExcludeClass(element: Element) {
return zIndexExcludeClass.some((className) =>
element.classList.contains(className),
);
}
/** /**
* 获取最大的zIndex值 * 获取最大的zIndex值
*/ */
@ -44,7 +54,11 @@ function calcZIndex() {
[...elements].forEach((element) => { [...elements].forEach((element) => {
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
const zIndex = style.getPropertyValue('z-index'); const zIndex = style.getPropertyValue('z-index');
if (zIndex && !Number.isNaN(Number.parseInt(zIndex))) { if (
zIndex &&
!Number.isNaN(Number.parseInt(zIndex)) &&
!isZIndexExcludeClass(element)
) {
maxZ = Math.max(maxZ, Number.parseInt(zIndex)); maxZ = Math.max(maxZ, Number.parseInt(zIndex));
} }
}); });

View File

@ -37,6 +37,7 @@ function handleMenuOpen(key: string, path: string[]) {
:menus="menus" :menus="menus"
:mode="mode" :mode="mode"
:rounded="rounded" :rounded="rounded"
scroll-to-active
:theme="theme" :theme="theme"
@open="handleMenuOpen" @open="handleMenuOpen"
@select="handleMenuSelect" @select="handleMenuSelect"

View File

@ -6,17 +6,37 @@ import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';
function useNavigation() { function useNavigation() {
const router = useRouter(); const router = useRouter();
const routes = router.getRoutes();
const routeMetaMap = new Map<string, RouteRecordNormalized>(); const routeMetaMap = new Map<string, RouteRecordNormalized>();
// 初始化路由映射
const initRouteMetaMap = () => {
const routes = router.getRoutes();
routes.forEach((route) => { routes.forEach((route) => {
routeMetaMap.set(route.path, route); routeMetaMap.set(route.path, route);
}); });
};
initRouteMetaMap();
// 监听路由变化
router.afterEach(() => {
initRouteMetaMap();
});
// 检查是否应该在新窗口打开
const shouldOpenInNewWindow = (path: string): boolean => {
if (isHttpUrl(path)) {
return true;
}
const route = routeMetaMap.get(path);
return route?.meta?.openInNewWindow ?? false;
};
const navigation = async (path: string) => { const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path); const route = routeMetaMap.get(path);
const { openInNewWindow = false, query = {} } = route?.meta ?? {}; const { openInNewWindow = false, query = {} } = route?.meta ?? {};
if (isHttpUrl(path)) { if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' }); openWindow(path, { target: '_blank' });
} else if (openInNewWindow) { } else if (openInNewWindow) {
@ -27,18 +47,14 @@ function useNavigation() {
query, query,
}); });
} }
} catch (error) {
console.error('Navigation failed:', error);
throw error;
}
}; };
const willOpenedByWindow = (path: string) => { const willOpenedByWindow = (path: string) => {
const route = routeMetaMap.get(path); return shouldOpenInNewWindow(path);
const { openInNewWindow = false } = route?.meta ?? {};
if (isHttpUrl(path)) {
return true;
} else if (openInNewWindow) {
return true;
} else {
return false;
}
}; };
return { navigation, willOpenedByWindow }; return { navigation, willOpenedByWindow };

View File

@ -11,7 +11,8 @@ defineOptions({
name: 'LanguageToggle', name: 'LanguageToggle',
}); });
async function handleUpdate(value: string) { async function handleUpdate(value: string | undefined) {
if (!value) return;
const locale = value as SupportedLanguagesType; const locale = value as SupportedLanguagesType;
updatePreferences({ updatePreferences({
app: { app: {

View File

@ -36,7 +36,8 @@ const menus = computed((): VbenDropdownMenuItem[] => [
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences(); const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
function handleUpdate(value: string) { function handleUpdate(value: string | undefined) {
if (!value) return;
updatePreferences({ updatePreferences({
app: { app: {
authPageLayout: value as AuthPageLayoutType, authPageLayout: value as AuthPageLayoutType,

View File

@ -79,14 +79,14 @@ const handleCheckboxChange = () => {
</SwitchItem> </SwitchItem>
<CheckboxItem <CheckboxItem
:items="[ :items="[
{ label: '收缩按钮', value: 'collapsed' }, { label: $t('preferences.sidebar.buttonCollapsed'), value: 'collapsed' },
{ label: '固定按钮', value: 'fixed' }, { label: $t('preferences.sidebar.buttonFixed'), value: 'fixed' },
]" ]"
multiple multiple
v-model="sidebarButtons" v-model="sidebarButtons"
:on-btn-click="handleCheckboxChange" :on-btn-click="handleCheckboxChange"
> >
按钮配置 {{ $t('preferences.sidebar.buttons') }}
</CheckboxItem> </CheckboxItem>
<NumberFieldItem <NumberFieldItem
v-model="sidebarWidth" v-model="sidebarWidth"

View File

@ -24,7 +24,7 @@ withDefaults(defineProps<{ shouldOnHover?: boolean }>(), {
shouldOnHover: false, shouldOnHover: false,
}); });
function handleChange(isDark: boolean) { function handleChange(isDark: boolean | undefined) {
updatePreferences({ updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' }, theme: { mode: isDark ? 'dark' : 'light' },
}); });

View File

@ -45,6 +45,9 @@
"fixed": "Fixed" "fixed": "Fixed"
}, },
"sidebar": { "sidebar": {
"buttons": "Show Buttons",
"buttonFixed": "Fixed",
"buttonCollapsed": "Collapsed",
"title": "Sidebar", "title": "Sidebar",
"width": "Width", "width": "Width",
"visible": "Show Sidebar", "visible": "Show Sidebar",

View File

@ -45,6 +45,9 @@
"fixed": "固定" "fixed": "固定"
}, },
"sidebar": { "sidebar": {
"buttons": "显示按钮",
"buttonFixed": "固定按钮",
"buttonCollapsed": "折叠按钮",
"title": "侧边栏", "title": "侧边栏",
"width": "宽度", "width": "宽度",
"visible": "显示侧边栏", "visible": "显示侧边栏",

View File

@ -69,7 +69,7 @@ describe('generateMenus', () => {
}, },
]; ];
const menus = await generateMenus(mockRoutes, mockRouter as any); const menus = generateMenus(mockRoutes, mockRouter as any);
expect(menus).toEqual(expectedMenus); expect(menus).toEqual(expectedMenus);
}); });
@ -82,7 +82,7 @@ describe('generateMenus', () => {
}, },
] as RouteRecordRaw[]; ] as RouteRecordRaw[];
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any); const menus = generateMenus(mockRoutesWithMeta, mockRouter as any);
expect(menus).toEqual([ expect(menus).toEqual([
{ {
badge: undefined, badge: undefined,
@ -109,7 +109,7 @@ describe('generateMenus', () => {
}, },
] as RouteRecordRaw[]; ] as RouteRecordRaw[];
const menus = await generateMenus(mockRoutesWithParams, mockRouter as any); const menus = generateMenus(mockRoutesWithParams, mockRouter as any);
expect(menus).toEqual([ expect(menus).toEqual([
{ {
badge: undefined, badge: undefined,
@ -141,10 +141,7 @@ describe('generateMenus', () => {
}, },
] as RouteRecordRaw[]; ] as RouteRecordRaw[];
const menus = await generateMenus( const menus = generateMenus(mockRoutesWithRedirect, mockRouter as any);
mockRoutesWithRedirect,
mockRouter as any,
);
expect(menus).toEqual([ expect(menus).toEqual([
// Assuming your generateMenus function excludes redirect routes from the menu // Assuming your generateMenus function excludes redirect routes from the menu
{ {
@ -195,7 +192,7 @@ describe('generateMenus', () => {
}); });
it('should generate menu list with correct order', async () => { it('should generate menu list with correct order', async () => {
const menus = await generateMenus(routes, router); const menus = generateMenus(routes, router);
const expectedMenus = [ const expectedMenus = [
{ {
badge: undefined, badge: undefined,
@ -230,7 +227,7 @@ describe('generateMenus', () => {
it('should handle empty routes', async () => { it('should handle empty routes', async () => {
const emptyRoutes: any[] = []; const emptyRoutes: any[] = [];
const menus = await generateMenus(emptyRoutes, router); const menus = generateMenus(emptyRoutes, router);
expect(menus).toEqual([]); expect(menus).toEqual([]);
}); });
}); });

View File

@ -1,30 +1,38 @@
import type { Router, RouteRecordRaw } from 'vue-router'; import type { Router, RouteRecordRaw } from 'vue-router';
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings'; import type {
ExRouteRecordRaw,
MenuRecordRaw,
RouteMeta,
} from '@vben-core/typings';
import { filterTree, mapTree } from '@vben-core/shared/utils'; import { filterTree, mapTree } from '@vben-core/shared/utils';
/** /**
* routes * routes
* @param routes * @param routes -
* @param router - Vue Router
* @returns
*/ */
async function generateMenus( function generateMenus(
routes: RouteRecordRaw[], routes: RouteRecordRaw[],
router: Router, router: Router,
): Promise<MenuRecordRaw[]> { ): MenuRecordRaw[] {
// 将路由列表转换为一个以 name 为键的对象映射 // 将路由列表转换为一个以 name 为键的对象映射
// 获取所有router最终的path及name
const finalRoutesMap: { [key: string]: string } = Object.fromEntries( const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
router.getRoutes().map(({ name, path }) => [name, path]), router.getRoutes().map(({ name, path }) => [name, path]),
); );
let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => { let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
// 路由表的路径写法有多种这里从router获取到最终的path并赋值 // 获取最终的路由路径
const path = finalRoutesMap[route.name as string] ?? route.path; const path = finalRoutesMap[route.name as string] ?? route.path ?? '';
// 转换为菜单结构 const {
// const path = matchRoute?.path ?? route.path; meta = {} as RouteMeta,
const { meta, name: routeName, redirect, children } = route; name: routeName,
redirect,
children = [],
} = route;
const { const {
activeIcon, activeIcon,
badge, badge,
@ -35,24 +43,27 @@ async function generateMenus(
link, link,
order, order,
title = '', title = '',
} = meta || {}; } = meta;
// 确保菜单名称不为空
const name = (title || routeName || '') as string; const name = (title || routeName || '') as string;
// 隐藏子菜单 // 处理子菜单
const resultChildren = hideChildrenInMenu const resultChildren = hideChildrenInMenu
? [] ? []
: (children as MenuRecordRaw[]); : (children as MenuRecordRaw[]);
// 将菜单的所有父级和父级菜单记录到菜单项内 // 设置子菜单的父子关系
if (resultChildren && resultChildren.length > 0) { if (resultChildren.length > 0) {
resultChildren.forEach((child) => { resultChildren.forEach((child) => {
child.parents = [...(route.parents || []), path]; child.parents = [...(route.parents ?? []), path];
child.parent = path; child.parent = path;
}); });
} }
// 隐藏子菜单
// 确定最终路径
const resultPath = hideChildrenInMenu ? redirect || path : link || path; const resultPath = hideChildrenInMenu ? redirect || path : link || path;
return { return {
activeIcon, activeIcon,
badge, badge,
@ -63,19 +74,17 @@ async function generateMenus(
order, order,
parent: route.parent, parent: route.parent,
parents: route.parents, parents: route.parents,
path: resultPath as string, path: resultPath,
show: !route?.meta?.hideInMenu, show: !meta.hideInMenu,
children: resultChildren || [], children: resultChildren,
}; };
}); });
// 对菜单进行排序避免order=0时被替换成999的问题 // 对菜单进行排序避免order=0时被替换成999的问题
menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999)); menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
const finalMenus = filterTree(menus, (menu) => { // 过滤掉隐藏的菜单项
return !!menu.show; return filterTree(menus, (menu) => !!menu.show);
});
return finalMenus;
} }
export { generateMenus }; export { generateMenus };

View File

@ -73,8 +73,8 @@ const withDefaultPlaceholder = <T extends Component>(
componentProps: Recordable<any> = {}, componentProps: Recordable<any> = {},
) => { ) => {
return defineComponent({ return defineComponent({
inheritAttrs: false,
name: component.name, name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => { setup: (props: any, { attrs, expose, slots }) => {
const placeholder = const placeholder =
props?.placeholder || props?.placeholder ||

View File

@ -1,6 +1,6 @@
import type { Router } from 'vue-router'; 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 { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils'; import { startProgress, stopProgress } from '@vben/utils';
@ -54,7 +54,7 @@ function setupAccessGuard(router: Router) {
return decodeURIComponent( return decodeURIComponent(
(to.query?.redirect as string) || (to.query?.redirect as string) ||
userStore.userInfo?.homePath || userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH, preferences.app.defaultHomePath,
); );
} }
return true; return true;
@ -73,7 +73,7 @@ function setupAccessGuard(router: Router) {
path: LOGIN_PATH, path: LOGIN_PATH,
// 如不需要,直接删除 query // 如不需要,直接删除 query
query: query:
to.fullPath === DEFAULT_HOME_PATH to.fullPath === preferences.app.defaultHomePath
? {} ? {}
: { redirect: encodeURIComponent(to.fullPath) }, : { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面 // 携带当前跳转的页面,登录后重新跳转该页面
@ -106,8 +106,8 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true); accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH (to.path === preferences.app.defaultHomePath
? userInfo.homePath || DEFAULT_HOME_PATH ? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string; : to.fullPath)) as string;
return { return {

View File

@ -1,6 +1,7 @@
import type { RouteRecordRaw } from 'vue-router'; 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 { $t } from '#/locales'; import { $t } from '#/locales';
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
}, },
name: 'Root', name: 'Root',
path: '/', path: '/',
redirect: DEFAULT_HOME_PATH, redirect: preferences.app.defaultHomePath,
children: [], children: [],
}, },
{ {

View File

@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; 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 { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
} else { } else {
onSuccess onSuccess
? await onSuccess?.() ? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH); : await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
} }
if (userInfo?.realName) { if (userInfo?.realName) {

View File

@ -21,22 +21,22 @@ catalog:
'@commitlint/cli': ^19.8.0 '@commitlint/cli': ^19.8.0
'@commitlint/config-conventional': ^19.8.0 '@commitlint/config-conventional': ^19.8.0
'@ctrl/tinycolor': ^4.1.0 '@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.25.1 '@eslint/js': ^9.26.0
'@faker-js/faker': ^9.7.0 '@faker-js/faker': ^9.7.0
'@iconify/json': ^2.2.332 '@iconify/json': ^2.2.334
'@iconify/tailwind': ^1.2.0 '@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.3.0 '@iconify/vue': ^5.0.0
'@intlify/core-base': ^11.1.3 '@intlify/core-base': ^11.1.3
'@intlify/unplugin-vue-i18n': ^6.0.8 '@intlify/unplugin-vue-i18n': ^6.0.8
'@jspm/generator': ^2.5.1 '@jspm/generator': ^2.5.1
'@manypkg/get-packages': ^2.2.2 '@manypkg/get-packages': ^3.0.0
'@nolebase/vitepress-plugin-git-changelog': ^2.17.0 '@nolebase/vitepress-plugin-git-changelog': ^2.17.0
'@playwright/test': ^1.52.0 '@playwright/test': ^1.52.0
'@pnpm/workspace.read-manifest': ^1000.1.4 '@pnpm/workspace.read-manifest': ^1000.1.4
'@stylistic/stylelint-plugin': ^3.1.2 '@stylistic/stylelint-plugin': ^3.1.2
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e '@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.16 '@tailwindcss/typography': ^0.5.16
'@tanstack/vue-query': ^5.74.7 '@tanstack/vue-query': ^5.75.1
'@tanstack/vue-store': ^0.7.0 '@tanstack/vue-store': ^0.7.0
'@types/archiver': ^6.0.3 '@types/archiver': ^6.0.3
'@types/eslint': ^9.6.1 '@types/eslint': ^9.6.1
@ -46,14 +46,14 @@ catalog:
'@types/lodash.get': ^4.4.9 '@types/lodash.get': ^4.4.9
'@types/lodash.isequal': ^4.5.8 '@types/lodash.isequal': ^4.5.8
'@types/lodash.set': ^4.3.9 '@types/lodash.set': ^4.3.9
'@types/node': ^22.15.2 '@types/node': ^22.15.3
'@types/nprogress': ^0.2.3 '@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3 '@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5 '@types/qrcode': ^1.5.5
'@types/qs': ^6.9.18 '@types/qs': ^6.9.18
'@types/sortablejs': ^1.15.8 '@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.31.0 '@typescript-eslint/eslint-plugin': ^8.31.1
'@typescript-eslint/parser': ^8.31.0 '@typescript-eslint/parser': ^8.31.1
'@vee-validate/zod': ^4.15.0 '@vee-validate/zod': ^4.15.0
'@vite-pwa/vitepress': ^1.0.0 '@vite-pwa/vitepress': ^1.0.0
'@vitejs/plugin-vue': ^5.2.3 '@vitejs/plugin-vue': ^5.2.3
@ -62,8 +62,8 @@ catalog:
'@vue/shared': ^3.5.13 '@vue/shared': ^3.5.13
'@vue/test-utils': ^2.4.6 '@vue/test-utils': ^2.4.6
'@vueuse/core': ^13.1.0 '@vueuse/core': ^13.1.0
'@vueuse/motion': ^3.0.3
'@vueuse/integrations': ^13.1.0 '@vueuse/integrations': ^13.1.0
'@vueuse/motion': ^3.0.3
ant-design-vue: ^4.2.6 ant-design-vue: ^4.2.6
archiver: ^7.0.1 archiver: ^7.0.1
autoprefixer: ^10.4.21 autoprefixer: ^10.4.21
@ -88,7 +88,7 @@ catalog:
dotenv: ^16.5.0 dotenv: ^16.5.0
echarts: ^5.6.0 echarts: ^5.6.0
element-plus: ^2.9.9 element-plus: ^2.9.9
eslint: ^9.25.1 eslint: ^9.26.0
eslint-config-turbo: ^2.5.2 eslint-config-turbo: ^2.5.2
eslint-plugin-command: ^3.2.0 eslint-plugin-command: ^3.2.0
eslint-plugin-eslint-comments: ^3.2.0 eslint-plugin-eslint-comments: ^3.2.0
@ -103,24 +103,23 @@ catalog:
eslint-plugin-unicorn: ^59.0.0 eslint-plugin-unicorn: ^59.0.0
eslint-plugin-unused-imports: ^4.1.4 eslint-plugin-unused-imports: ^4.1.4
eslint-plugin-vitest: ^0.5.4 eslint-plugin-vitest: ^0.5.4
eslint-plugin-vue: ^10.0.0 eslint-plugin-vue: ^10.1.0
execa: ^9.5.2 execa: ^9.5.2
find-up: ^7.0.0 find-up: ^7.0.0
get-port: ^7.1.0 get-port: ^7.1.0
globals: ^16.0.0 globals: ^16.0.0
h3: ^1.15.3 h3: ^1.15.3
happy-dom: ^17.4.4 happy-dom: ^17.4.6
html-minifier-terser: ^7.2.0 html-minifier-terser: ^7.2.0
husky: ^9.1.7
is-ci: ^4.1.0 is-ci: ^4.1.0
jsonc-eslint-parser: ^2.4.0 jsonc-eslint-parser: ^2.4.0
jsonwebtoken: ^9.0.2 jsonwebtoken: ^9.0.2
lint-staged: ^15.5.1 lefthook: ^1.11.12
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
lodash.get: ^4.4.2 lodash.get: ^4.4.2
lodash.set: ^4.3.2
lodash.isequal: ^4.5.0 lodash.isequal: ^4.5.0
lucide-vue-next: ^0.503.0 lodash.set: ^4.3.2
lucide-vue-next: ^0.507.0
medium-zoom: ^1.1.0 medium-zoom: ^1.1.0
naive-ui: ^2.41.0 naive-ui: ^2.41.0
nitropack: ^2.11.11 nitropack: ^2.11.11
@ -144,7 +143,7 @@ catalog:
radix-vue: ^1.9.17 radix-vue: ^1.9.17
resolve.exports: ^2.0.3 resolve.exports: ^2.0.3
rimraf: ^6.0.1 rimraf: ^6.0.1
rollup: ^4.40.0 rollup: ^4.40.1
rollup-plugin-visualizer: ^5.14.0 rollup-plugin-visualizer: ^5.14.0
sass: ^1.87.0 sass: ^1.87.0
secure-ls: ^2.0.0 secure-ls: ^2.0.0
@ -168,7 +167,7 @@ catalog:
unbuild: ^3.5.0 unbuild: ^3.5.0
unplugin-element-plus: ^0.10.0 unplugin-element-plus: ^0.10.0
vee-validate: ^4.15.0 vee-validate: ^4.15.0
vite: ^6.3.3 vite: ^6.3.4
vite-plugin-compression: ^0.5.1 vite-plugin-compression: ^0.5.1
vite-plugin-dts: ^4.5.3 vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2 vite-plugin-html: ^3.2.2
@ -184,9 +183,9 @@ catalog:
vue-json-viewer: ^3.0.4 vue-json-viewer: ^3.0.4
vue-router: ^4.5.1 vue-router: ^4.5.1
vue-tippy: ^6.7.0 vue-tippy: ^6.7.0
vue-tsc: 2.1.10 vue-tsc: 2.2.10
vxe-pc-ui: ^4.5.14 vxe-pc-ui: ^4.5.35
vxe-table: ^4.13.14 vxe-table: ^4.13.16
watermark-js-plus: ^1.6.0 watermark-js-plus: ^1.6.0
zod: ^3.24.3 zod: ^3.24.3
zod-defaults: ^0.1.3 zod-defaults: ^0.1.3

View File

@ -22,7 +22,6 @@ const DEFAULT_CONFIG = {
'@vben/backend-mock', '@vben/backend-mock',
'@vben/commitlint-config', '@vben/commitlint-config',
'@vben/eslint-config', '@vben/eslint-config',
'@vben/lint-staged-config',
'@vben/node-utils', '@vben/node-utils',
'@vben/prettier-config', '@vben/prettier-config',
'@vben/stylelint-config', '@vben/stylelint-config',