Compare commits

...

15 Commits

Author SHA1 Message Date
vben
e7a4ab70d5 chore: release v5.1.1 [skip ci] 2024-08-19 23:32:24 +08:00
Vben
1db87ff7ce fix: fix keepAlive parameter error (#4194)
* fix: mock server deployment error

* chore: typo
2024-08-19 23:28:14 +08:00
Li Kui
01d60336a6 feat: refactor and improve the request client and support refreshToken (#4157)
* feat: refreshToken

* chore: store refreshToken

* chore: generate token using jsonwebtoken

* chore: set refreshToken in httpOnly cookie

* perf: authHeader verify

* chore: add add response interceptor

* chore: test refresh

* chore: handle logout

* chore: type

* chore: update pnpm-lock.yaml

* chore: remove test code

* chore: add todo comment

* chore: update pnpm-lock.yaml

* chore: remove default interceptors

* chore: copy codes

* chore: handle refreshToken invalid

* chore: add refreshToken preference

* chore: typo

* chore: refresh token逻辑调整

* refactor: interceptor presets

* chore: copy codes

* fix: ci errors

* chore: add missing await

* feat: 完善refresh-token逻辑及文档

* fix: ci error

* chore: filename

---------

Co-authored-by: vince <vince292007@gmail.com>
2024-08-19 22:59:42 +08:00
vben
f8485e8861 fix: ci error 2024-08-18 08:46:22 +08:00
Vben
9120d20143 chore: update deps and docs (#4184)
* chore: update deps

* chore: update docs

* chore: update menu
2024-08-18 08:43:00 +08:00
Vben
5f41c51770 perf: supplement login interface documents and add configuration parameters (#4175) 2024-08-17 22:38:37 +08:00
Vben
3c17f4e9f8 perf: all icons used in the core are offline (#4173)
* perf: all icons used in the core are offline

* chore: update default icon

* chore: update shadow
2024-08-17 21:11:07 +08:00
Vben
66808582ff fix: header theme color error (#4170)
* fix: header theme color error

* fix: ci error

* fix: ci error
2024-08-16 22:47:28 +08:00
Vben
0faf7810b6 perf: optimization of tabbar display (#4169)
* perf: optimization of tabbar display

* fix: ci error

* chore: typo

* chore: typo
2024-08-16 22:20:18 +08:00
dependabot[bot]
8987067b5a chore(deps): bump the non-breaking-changes group with 12 updates (#4165)
* chore(deps): bump the non-breaking-changes group with 12 updates

Bumps the non-breaking-changes group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [turbo](https://github.com/vercel/turborepo) | `2.0.13` | `2.0.14` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `5.4.0` | `5.4.1` |
| [vue](https://github.com/vuejs/core) | `3.4.37` | `3.4.38` |
| [execa](https://github.com/sindresorhus/execa) | `9.3.0` | `9.3.1` |
| [eslint-config-turbo](https://github.com/vercel/turborepo/tree/HEAD/packages/eslint-config-turbo) | `2.0.13` | `2.0.14` |
| [eslint-plugin-no-only-tests](https://github.com/levibuzolic/eslint-plugin-no-only-tests) | `3.1.0` | `3.3.0` |
| [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) | `3.1.3` | `3.2.0` |
| [stylelint](https://github.com/stylelint/stylelint) | `16.8.1` | `16.8.2` |
| [pinia](https://github.com/vuejs/pinia) | `2.2.1` | `2.2.2` |
| [@vue/shared](https://github.com/vuejs/core/tree/HEAD/packages/shared) | `3.4.37` | `3.4.38` |
| [watermark-js-plus](https://github.com/zhensherlock/watermark-js-plus) | `1.5.2` | `1.5.3` |
| [publint](https://github.com/bluwy/publint/tree/HEAD/pkg) | `0.2.9` | `0.2.10` |


Updates `turbo` from 2.0.13 to 2.0.14
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.0.13...v2.0.14)

Updates `vite` from 5.4.0 to 5.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.1/packages/vite)

Updates `vue` from 3.4.37 to 3.4.38
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/v3.4.38/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.4.37...v3.4.38)

Updates `execa` from 9.3.0 to 9.3.1
- [Release notes](https://github.com/sindresorhus/execa/releases)
- [Commits](https://github.com/sindresorhus/execa/compare/v9.3.0...v9.3.1)

Updates `eslint-config-turbo` from 2.0.13 to 2.0.14
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/commits/v2.0.14/packages/eslint-config-turbo)

Updates `eslint-plugin-no-only-tests` from 3.1.0 to 3.3.0
- [Release notes](https://github.com/levibuzolic/eslint-plugin-no-only-tests/releases)
- [Changelog](https://github.com/levibuzolic/eslint-plugin-no-only-tests/blob/main/CHANGELOG.md)
- [Commits](https://github.com/levibuzolic/eslint-plugin-no-only-tests/compare/v3.1.0...v3.3.0)

Updates `eslint-plugin-perfectionist` from 3.1.3 to 3.2.0
- [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases)
- [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md)
- [Commits](https://github.com/azat-io/eslint-plugin-perfectionist/compare/v3.1.3...v3.2.0)

Updates `stylelint` from 16.8.1 to 16.8.2
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.8.1...16.8.2)

Updates `pinia` from 2.2.1 to 2.2.2
- [Release notes](https://github.com/vuejs/pinia/releases)
- [Commits](https://github.com/vuejs/pinia/compare/pinia@2.2.1...pinia@2.2.2)

Updates `@vue/shared` from 3.4.37 to 3.4.38
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/v3.4.38/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.4.38/packages/shared)

Updates `watermark-js-plus` from 1.5.2 to 1.5.3
- [Release notes](https://github.com/zhensherlock/watermark-js-plus/releases)
- [Changelog](https://github.com/zhensherlock/watermark-js-plus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zhensherlock/watermark-js-plus/compare/v1.5.2...v1.5.3)

Updates `publint` from 0.2.9 to 0.2.10
- [Release notes](https://github.com/bluwy/publint/releases)
- [Commits](https://github.com/bluwy/publint/commits/v0.2.10/pkg)

---
updated-dependencies:
- dependency-name: turbo
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vite
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: execa
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: eslint-config-turbo
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: eslint-plugin-no-only-tests
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: eslint-plugin-perfectionist
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: stylelint
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: pinia
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@vue/shared"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: watermark-js-plus
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: publint
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: vben <ann.vben@gmail.com>
2024-08-16 09:47:45 +08:00
Vben
d71a20ad0a perf: improve tabs-view scrolling (#4164) 2024-08-15 23:30:07 +08:00
P2K0
eb280ffeb7 feat: add left and right scroll buttons to Tabs bar (#4161) (#4162) 2024-08-15 21:54:55 +08:00
Vben
debb32d353 fix: page spinner is styled incorrectly when scrolling (#4163)
* feat: add contributor information to documents

* fix: page spinner is styled incorrectly when scrolling
2024-08-15 21:48:52 +08:00
Zhang Zhi Chao
11551903f0 fix: newTabTitle does not work as expected (#4160)
* fix: cloneDeep tab close #4158

* Revert "fix: cloneDeep tab close #4158"

This reverts commit 8e2f4b39ad.

* fix:  deep clone meta.newTabTitle
2024-08-15 21:47:54 +08:00
dependabot[bot]
187f946d2a chore(deps): bump the non-breaking-changes group with 8 updates (#4155)
Bumps the non-breaking-changes group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.2.0` | `22.3.0` |
| [@vitejs/plugin-vue-jsx](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue-jsx) | `4.0.0` | `4.0.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.4.9` | `3.4.10` |
| [turbo](https://github.com/vercel/turborepo) | `2.0.12` | `2.0.13` |
| [postcss-preset-env](https://github.com/csstools/postcss-plugins/tree/HEAD/plugin-packs/postcss-preset-env) | `10.0.0` | `10.0.1` |
| [vite-plugin-dts](https://github.com/qmhc/vite-plugin-dts) | `4.0.2` | `4.0.3` |
| [eslint-config-turbo](https://github.com/vercel/turborepo/tree/HEAD/packages/eslint-config-turbo) | `2.0.12` | `2.0.13` |
| [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) | `50.2.0` | `50.2.2` |


Updates `@types/node` from 22.2.0 to 22.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-vue-jsx` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue-jsx/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue-jsx@4.0.1/packages/plugin-vue-jsx)

Updates `tailwindcss` from 3.4.9 to 3.4.10
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.10/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.9...v3.4.10)

Updates `turbo` from 2.0.12 to 2.0.13
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.0.12...v2.0.13)

Updates `postcss-preset-env` from 10.0.0 to 10.0.1
- [Changelog](https://github.com/csstools/postcss-plugins/blob/main/plugin-packs/postcss-preset-env/CHANGELOG.md)
- [Commits](https://github.com/csstools/postcss-plugins/commits/HEAD/plugin-packs/postcss-preset-env)

Updates `vite-plugin-dts` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/qmhc/vite-plugin-dts/releases)
- [Changelog](https://github.com/qmhc/vite-plugin-dts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/qmhc/vite-plugin-dts/compare/v4.0.2...v4.0.3)

Updates `eslint-config-turbo` from 2.0.12 to 2.0.13
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/commits/v2.0.13/packages/eslint-config-turbo)

Updates `eslint-plugin-jsdoc` from 50.2.0 to 50.2.2
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v50.2.0...v50.2.2)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@vitejs/plugin-vue-jsx"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: tailwindcss
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: turbo
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: postcss-preset-env
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vite-plugin-dts
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: eslint-config-turbo
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 21:23:13 +08:00
165 changed files with 3763 additions and 2596 deletions

View File

@@ -42,9 +42,9 @@ version-resolver:
minor:
labels:
- "minor"
- "feature"
patch:
labels:
- "feature"
- "patch"
- "bug"
- "maintenance"

View File

@@ -27,7 +27,7 @@ jobs:
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build
run: pnpm build:play && pnpm build:docs
- name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@@ -66,7 +66,7 @@ jobs:
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build
run: pnpm run build:antd
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@@ -97,7 +97,7 @@ jobs:
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build
run: pnpm run build:ele
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@@ -128,7 +128,7 @@ jobs:
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build
run: pnpm run build:naive
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5

2
.npmrc
View File

@@ -1,4 +1,4 @@
# registry = "https://registry.npmmirror.com"
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier

View File

@@ -78,7 +78,7 @@ pnpm build
## 変更ログ
[CHANGELOG](./CHANGELOG.zh_CN.md)
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 貢献方法

View File

@@ -77,7 +77,7 @@ pnpm build
## Change Log
[CHANGELOG](./CHANGELOG.zh_CN.md)
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## How to contribute

View File

@@ -126,6 +126,10 @@ pnpm build
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## Contributor
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">

View File

@@ -1 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

View File

@@ -2,7 +2,7 @@
## Description
Vben Admin 数据 mock 服务没有对接任何的数据库所有数据都是模拟的用于前端开发时提供数据支持。线上环境不再提供mock集成可自行部署服务或者对接真实数据mock.js 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app

View File

@@ -1,15 +1,14 @@
export default eventHandler((event) => {
const token = getHeader(event, 'Authorization');
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const username = Buffer.from(token, 'base64').toString('utf8');
const codes =
MOCK_CODES.find((item) => item.username === username)?.codes ?? [];
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(codes);
});

View File

@@ -1,20 +1,36 @@
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
setResponseStatus(event, 403);
return useResponseError('UnauthorizedException', '用户名或密码错误');
clearRefreshTokenCookie(event);
return forbiddenResponse(event);
}
const accessToken = Buffer.from(username).toString('base64');
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
// TODO: refresh token
refreshToken: accessToken,
});
});

View File

@@ -0,0 +1,15 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});

View File

@@ -0,0 +1,33 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});

View File

@@ -1,14 +1,13 @@
export default eventHandler((event) => {
const token = getHeader(event, 'Authorization');
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const username = Buffer.from(token, 'base64').toString('utf8');
const menus =
MOCK_MENUS.find((item) => item.username === username)?.menus ?? [];
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});

View File

@@ -1,14 +1,11 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const token = getHeader(event, 'Authorization');
if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const username = Buffer.from(token, 'base64').toString('utf8');
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userInfo } = user;
return useResponseSuccess(userInfo);
return useResponseSuccess(userinfo);
});

View File

@@ -1,11 +1,4 @@
export default defineEventHandler((event) => {
// setResponseHeaders(event, {
// 'Access-Control-Allow-Credentials': 'true',
// 'Access-Control-Allow-Headers': '*',
// 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
// 'Access-Control-Allow-Origin': '*',
// 'Access-Control-Expose-Headers': '*',
// });
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';

View File

@@ -6,10 +6,15 @@
"license": "MIT",
"author": "",
"scripts": {
"start": "nitro dev",
"build": "nitro build"
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"jsonwebtoken": "^9.0.2",
"nitropack": "^2.9.7"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.6",
"h3": "^1.12.0"
}
}

View File

@@ -0,0 +1,26 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event<EventHandlerRequest>,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}

View File

@@ -0,0 +1,59 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '2h' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event<EventHandlerRequest>,
): null | Omit<UserInfo, 'password'> {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit<UserInfo, 'password'> {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}

View File

@@ -1,4 +1,12 @@
export const MOCK_USERS = [
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',

View File

@@ -1,3 +1,5 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
@@ -15,3 +17,13 @@ export function useResponseError(message: string, error: any = null) {
message,
};
}
export function forbiddenResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 403);
return useResponseError('ForbiddenException', 'Forbidden Exception');
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -40,11 +40,11 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.12",
"pinia": "2.2.1",
"vue": "^3.4.37",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
}
}

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request';
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
@@ -25,6 +29,22 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return requestClient.post('/auth/logout');
}
/**
* 获取用户权限码
*/

View File

@@ -1,67 +1,101 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request';
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,
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
/**
* 重新认证逻辑
*/
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.setLoginExpired(true);
} else {
await authStore.logout();
}
throw new Error(`Error ${status}: ${msg}`);
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
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, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -2,10 +2,9 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
await authStore.logout(false);
}
function handleNoticeClear() {

View File

@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/vben/about/index.vue'),
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),

View File

@@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
@@ -33,13 +33,11 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken, refreshToken } = await loginApi(params);
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
@@ -77,16 +75,19 @@ export const useAuthStore = defineStore('auth', () => {
};
}
async function logout() {
async function logout(redirect: boolean = true) {
await logoutApi();
resetAllStores();
accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-ele",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -40,11 +40,11 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"dayjs": "^1.11.12",
"element-plus": "^2.8.0",
"pinia": "2.2.1",
"vue": "^3.4.37",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
},
"devDependencies": {

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request';
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
@@ -25,6 +29,22 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return requestClient.post('/auth/logout');
}
/**
* 获取用户权限码
*/

View File

@@ -1,67 +1,101 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus';
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,
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => ElMessage.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
/**
* 重新认证逻辑
*/
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.setLoginExpired(true);
} else {
await authStore.logout();
}
throw new Error(`Error ${status}: ${msg}`);
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
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, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => ElMessage.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -2,10 +2,9 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
await authStore.logout(false);
}
function handleNoticeClear() {

View File

@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/vben/about/index.vue'),
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),

View File

@@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
@@ -33,13 +33,12 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken, refreshToken } = await loginApi(params);
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
@@ -77,16 +76,19 @@ export const useAuthStore = defineStore('auth', () => {
};
}
async function logout() {
async function logout(redirect: boolean = true) {
await logoutApi();
resetAllStores();
accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-naive",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -40,10 +40,10 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"naive-ui": "^2.39.0",
"pinia": "2.2.1",
"vue": "^3.4.37",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
}
}

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request';
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
@@ -25,6 +29,22 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return requestClient.post('/auth/logout');
}
/**
* 获取用户权限码
*/

View File

@@ -1,66 +1,100 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from '#/naive';
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,
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
/**
* 重新认证逻辑
*/
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.setLoginExpired(true);
} else {
await authStore.logout();
}
throw new Error(`Error ${status}: ${msg}`);
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
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, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -2,10 +2,9 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
await authStore.logout(false);
}
function handleNoticeClear() {

View File

@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/vben/about/index.vue'),
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),

View File

@@ -9,7 +9,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
import { notification } from '#/naive';
@@ -33,13 +33,12 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken, refreshToken } = await loginApi(params);
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
@@ -77,16 +76,19 @@ export const useAuthStore = defineStore('auth', () => {
};
}
async function logout() {
async function logout(redirect: boolean = true) {
await logoutApi();
resetAllStores();
accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}

View File

@@ -2,6 +2,10 @@ import type { DefaultTheme, HeadConfig } from 'vitepress';
import { resolve } from 'node:path';
import {
GitChangelog,
GitChangelogMarkdownSection,
} from '@nolebase/vitepress-plugin-git-changelog/vite';
import { type PwaOptions, withPwa } from '@vite-pwa/vitepress';
import { defineConfigWithTheme } from 'vitepress';
@@ -9,7 +13,7 @@ import { version } from '../../package.json';
export default withPwa(
defineConfigWithTheme({
description: 'Vben Admin& 企业级管理系统框架',
description: 'Vben Admin & 企业级管理系统框架',
head: head(),
lang: 'zh',
pwa: pwa(),
@@ -98,6 +102,12 @@ export default withPwa(
json: {
stringify: true,
},
plugins: [
GitChangelog({
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
}),
GitChangelogMarkdownSection(),
],
server: {
fs: {
allow: ['../..'],
@@ -274,6 +284,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
{
text: '深入',
items: [
{ link: 'in-depth/login', text: '登录' },
// { link: 'in-depth/layout', text: '布局' },
{ link: 'in-depth/theme', text: '主题' },
{ link: 'in-depth/access', text: '权限' },

View File

@@ -1,6 +1,7 @@
// https://vitepress.dev/guide/custom-theme
import type { Theme } from 'vitepress';
import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
import DefaultTheme from 'vitepress/theme';
import SiteLayout from './components/site-layout.vue';
@@ -9,11 +10,13 @@ import { initHmPlugin } from './plugins/hm';
import './styles';
import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
export default {
enhanceApp({ app }) {
// ...
app.component('VbenContributors', VbenContributors);
app.use(NolebaseGitChangelogPlugin);
// 百度统计
initHmPlugin();
},

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"scripts": {
"build": "vitepress build",
@@ -11,8 +11,9 @@
"medium-zoom": "^1.1.0"
},
"devDependencies": {
"@nolebase/vitepress-plugin-git-changelog": "^2.4.0",
"@vite-pwa/vitepress": "^0.5.0",
"vitepress": "^1.3.2",
"vue": "^3.4.37"
"vitepress": "^1.3.3",
"vue": "^3.4.38"
}
}

View File

@@ -52,8 +52,18 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
// 构建项目并分析
"build:analyze": "turbo build:analyze",
// 构建docker镜像
// 构建本地 docker 镜像
"build:docker": "./build-local-docker-image.sh",
// 单独构建 web-antd 应用
"build:antd": "pnpm run build --filter=@vben/web-antd",
// 单独构建文档
"build:docs": "pnpm run build --filter=@vben/docs",
// 单独构建 web-ele 应用
"build:ele": "pnpm run build --filter=@vben/web-ele",
// 单独构建 web-naive 应用
"build:naive": "pnpm run build --filter=@vben/naive",
// 单独构建 playground 应用
"build:play": "pnpm run build --filter=@vben/playground",
// changeset 版本管理
"changeset": "pnpm exec changeset",
// 检查项目各种问题
@@ -78,10 +88,10 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
"dev:docs": "pnpm -F @vben/docs run dev",
// 启动web-ele应用
"dev:ele": "pnpm -F @vben/web-ele run dev",
// 启动演示应用
"dev:play": "pnpm -F @vben/playground run dev",
// 启动web-naive应用
"dev:naive": "pnpm -F @vben/web-naive run dev",
// 启动演示应用
"dev:play": "pnpm -F @vben/playground run dev",
// 格式化代码
"format": "vsh lint --format",
// lint 代码

View File

@@ -64,7 +64,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/vben/about/index.vue'),
component: () => import('#/views/_core/about/index.vue'),
meta: {
badgeType: 'dot',
badgeVariants: 'destructive',

View File

@@ -163,70 +163,105 @@ export async function deleteUserApi(user: UserInfo) {
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request';
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,
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
/**
* 重新认证逻辑
*/
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.setLoginExpired(true);
} else {
await authStore.logout();
}
throw new Error(`Error ${status}: ${msg}`);
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
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, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
```
### 多个接口地址
@@ -244,6 +279,46 @@ export const requestClient = createRequestClient(apiURL);
export const otherRequestClient = createRequestClient(otherApiURL);
```
## 刷新Token
项目中默认提供了刷新 Token 的逻辑,只需要按照下面的配置即可开启:
- 确保当前启用了刷新 Token 的配置
调整对应应用目录下的`preferences.ts`,确保`enableRefreshToken='true'`。
```ts
import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
enableRefreshToken: true,
},
});
```
在 `src/api/request.ts` 中配置 `doRefreshToken` 方法即可:
```ts
// 这里调整为你的token格式
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
// 这里调整为你的刷新token接口
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
```
## 数据 Mock
::: tip 生产环境 Mock

View File

@@ -184,6 +184,7 @@ const defaultPreferences: Preferences = {
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,
enableRefreshToken: false,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
@@ -200,7 +201,7 @@ const defaultPreferences: Preferences = {
styleType: 'normal',
},
copyright: {
companyName: 'Vben Admin',
companyName: 'Vben',
companySiteLink: 'https://www.vben.pro',
date: '2024',
enable: true,
@@ -310,6 +311,10 @@ interface AppPreferences {
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/**
* @zh_CN 是否开启refreshToken
*/
enableRefreshToken: boolean;
/** 是否移动端 */
isMobile: boolean;
/** 布局方式 */

View File

@@ -0,0 +1,131 @@
# 登录
本文介绍如何去改造自己的应用程序登录页。
## 登录页面调整
如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的 `props` 参数来实现。
![login](/guide/login.png)
只需要在应用下的 `src/router/routes/core.ts` 内,配置`AuthPageLayout``props`参数即可:
```ts {4-8}
{
component: AuthPageLayout,
props: {
sloganImage: "xxx/xxx.png",
pageTitle: "开箱即用的大型中后台管理系统",
pageDescription: "工程化、高性能、跨组件库的前端模版",
toolbar: true,
toolbarList: () => ['color', 'language', 'layout', 'theme'],
}
// ...
},
```
::: tip
如果这些配置不能满足你的需求,你可以自行实现登录页面。直接实现自己的 `AuthPageLayout`即可。
:::
## 登录表单调整
如果你想调整登录表单的相关内容,你可以在应用下的 `src/views/_core/authentication/login.vue` 内,配置`AuthenticationLogin` 组件参数即可:
```vue
<AuthenticationLogin
:loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin"
/>
```
::: details AuthenticationLogin 组件参数
```ts
{
/**
* @zh_CN 验证码登录路径
*/
codeLoginPath?: string;
/**
* @zh_CN 忘记密码路径
*/
forgetPasswordPath?: string;
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 密码占位符
*/
passwordPlaceholder?: string;
/**
* @zh_CN 二维码登录路径
*/
qrCodeLoginPath?: string;
/**
* @zh_CN 注册路径
*/
registerPath?: string;
/**
* @zh_CN 是否显示验证码登录
*/
showCodeLogin?: boolean;
/**
* @zh_CN 是否显示忘记密码
*/
showForgetPassword?: boolean;
/**
* @zh_CN 是否显示二维码登录
*/
showQrcodeLogin?: boolean;
/**
* @zh_CN 是否显示注册按钮
*/
showRegister?: boolean;
/**
* @zh_CN 是否显示记住账号
*/
showRememberMe?: boolean;
/**
* @zh_CN 是否显示第三方登录
*/
showThirdPartyLogin?: boolean;
/**
* @zh_CN 登录框子标题
*/
subTitle?: string;
/**
* @zh_CN 登录框标题
*/
title?: string;
/**
* @zh_CN 用户名占位符
*/
usernamePlaceholder?: string;
}
```
:::
::: tip
如果这些配置不能满足你的需求,你可以自行实现登录表单及相关登录逻辑。
:::

View File

@@ -223,7 +223,7 @@ css 变量内的颜色,必须使用 `hsl` 格式,如 `0 0% 100%`,不需要
你只需要在你的项目中覆盖你想要修改的 CSS 变量即可。例如,要更改默认卡片背景色,你可以在你的 CSS 文件中添加以下内容进行覆盖:
### 默认主题下
### 默认主题下
```css
:root {
@@ -1222,7 +1222,7 @@ export const overridesPreferences = defineOverridesPreferences({
侧边栏颜色通过`--sidebar`变量来配置
### 默认主题下
### 默认主题下
```css
:root {
@@ -1244,7 +1244,7 @@ export const overridesPreferences = defineOverridesPreferences({
侧边栏颜色通过`--header`变量来配置
### 默认主题下
### 默认主题下
```css
:root {

View File

@@ -52,6 +52,11 @@ pnpm install
```json
{
"scripts": {
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build:docs": "pnpm run build --filter=@vben/docs",
"build:ele": "pnpm run build --filter=@vben/web-ele",
"build:naive": "pnpm run build --filter=@vben/web-naive",
"build:play": "pnpm run build --filter=@vben/playground",
"dev:antd": "pnpm -F @vben/web-antd run dev",
"dev:docs": "pnpm -F @vben/docs run dev",
"dev:ele": "pnpm -F @vben/web-ele run dev",

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

View File

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

View File

@@ -27,7 +27,7 @@
}
},
"dependencies": {
"eslint-config-turbo": "^2.0.12",
"eslint-config-turbo": "^2.0.14",
"eslint-plugin-command": "^0.2.3",
"eslint-plugin-import-x": "^3.1.0"
},
@@ -39,11 +39,11 @@
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jsdoc": "^50.2.0",
"eslint-plugin-jsdoc": "^50.2.2",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-n": "^17.10.2",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-perfectionist": "^3.1.3",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-perfectionist": "^3.2.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-regexp": "^2.6.0",
"eslint-plugin-unicorn": "^55.0.0",

View File

@@ -42,7 +42,8 @@ export async function typescript(): Promise<Linter.Config[]> {
},
],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
// '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': [

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -32,7 +32,7 @@
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.3.3",
"stylelint": "^16.8.1",
"stylelint": "^16.8.2",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -33,7 +33,7 @@
"chalk": "^5.3.0",
"consola": "^3.2.3",
"dayjs": "^1.11.12",
"execa": "^9.3.0",
"execa": "^9.3.1",
"find-up": "^7.0.0",
"nanoid": "^5.0.7",
"ora": "^8.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -46,7 +46,7 @@
"tailwindcss": "^3.4.3"
},
"dependencies": {
"@iconify/json": "^2.2.237",
"@iconify/json": "^2.2.238",
"@iconify/tailwind": "^1.1.2",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@tailwindcss/typography": "^0.5.14",
@@ -55,8 +55,8 @@
"postcss": "^8.4.41",
"postcss-antd-fixes": "^0.2.0",
"postcss-import": "^16.1.0",
"postcss-preset-env": "^10.0.0",
"tailwindcss": "^3.4.9",
"postcss-preset-env": "^10.0.1",
"tailwindcss": "^3.4.10",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -20,6 +20,6 @@
],
"dependencies": {
"@vben/types": "workspace:*",
"vite": "^5.4.0"
"vite": "^5.4.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/vite-config",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -42,15 +42,15 @@
"@types/html-minifier-terser": "^7.0.2",
"@vben/node-utils": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"dayjs": "^1.11.12",
"dotenv": "^16.4.5",
"rollup": "^4.20.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.8",
"vite": "^5.4.0",
"vite": "^5.4.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "4.0.2",
"vite-plugin-dts": "4.0.3",
"vite-plugin-html": "^3.2.2"
}
}

View File

@@ -23,7 +23,7 @@ import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
import { viteMetadataPlugin } from './inject-metadata';
import { viteLicensePlugin } from './license';
import { viteNitroMockPlugin } from './nitor-mock';
import { viteNitroMockPlugin } from './nitro-mock';
import { vitePrintPlugin } from './print';
/**

View File

@@ -23,7 +23,9 @@ export const viteNitroMockPlugin = ({
const pkg = await getPackage(mockServerPackage);
if (!pkg) {
consola.error(`Package ${mockServerPackage} not found.`);
consola.log(
`Package ${mockServerPackage} not found. Skip mock server.`,
);
return;
}

View File

@@ -1,6 +1,6 @@
{
"name": "vben-admin-pro",
"version": "5.1.0",
"version": "5.1.1",
"private": true,
"keywords": [
"monorepo",
@@ -29,6 +29,11 @@
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:docker": "./build-local-docker-image.sh",
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build:docs": "pnpm run build --filter=@vben/docs",
"build:ele": "pnpm run build --filter=@vben/web-ele",
"build:naive": "pnpm run build --filter=@vben/web-naive",
"build:play": "pnpm run build --filter=@vben/playground",
"changeset": "pnpm exec changeset",
"check": "pnpm run check:circular && pnpm run check:dep && pnpm run check:type && pnpm check:cspell",
"check:circular": "vsh check-circular",
@@ -41,8 +46,8 @@
"dev:antd": "pnpm -F @vben/web-antd run dev",
"dev:docs": "pnpm -F @vben/docs run dev",
"dev:ele": "pnpm -F @vben/web-ele run dev",
"dev:play": "pnpm -F @vben/playground run dev",
"dev:naive": "pnpm -F @vben/web-naive run dev",
"dev:play": "pnpm -F @vben/playground run dev",
"format": "vsh lint --format",
"lint": "vsh lint",
"postinstall": "turbo run stub",
@@ -60,7 +65,7 @@
"@changesets/cli": "^2.27.7",
"@ls-lint/ls-lint": "^2.2.3",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.2.0",
"@types/node": "^22.4.0",
"@vben/commitlint-config": "workspace:*",
"@vben/eslint-config": "workspace:*",
"@vben/prettier-config": "workspace:*",
@@ -71,30 +76,30 @@
"@vben/vite-config": "workspace:*",
"@vben/vsh": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"cspell": "^8.13.3",
"cspell": "^8.14.1",
"husky": "^9.1.4",
"is-ci": "^3.0.1",
"jsdom": "^24.1.1",
"lint-staged": "^15.2.9",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.9",
"turbo": "^2.0.12",
"tailwindcss": "^3.4.10",
"turbo": "^2.0.14",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
"vite": "^5.4.0",
"vite": "^5.4.1",
"vitest": "^2.0.5",
"vue": "^3.4.37",
"vue": "^3.4.38",
"vue-tsc": "^2.0.29"
},
"engines": {
"node": ">=20",
"pnpm": ">=9"
},
"packageManager": "pnpm@9.7.0",
"packageManager": "pnpm@9.7.1",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
@@ -102,9 +107,10 @@
}
},
"overrides": {
"@ctrl/tinycolor": "^4.1.0",
"clsx": "^2.1.1",
"vue": "^3.4.37"
"@ctrl/tinycolor": "4.1.0",
"clsx": "2.1.1",
"pinia": "2.2.2",
"vue": "3.4.38"
},
"neverBuiltDependencies": [
"canvas",

View File

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

View File

@@ -34,7 +34,7 @@
/* Used for destructive actions such as <Button variant="destructive"> */
--destructive: 0 78% 68%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */
@@ -110,7 +110,7 @@
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
@@ -136,7 +136,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -162,7 +162,7 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
@@ -188,7 +188,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -214,7 +214,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -240,7 +240,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -266,7 +266,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -318,7 +318,7 @@
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
@@ -344,14 +344,14 @@
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--sidebar: 240 10% 3.9%;
--sidebar-deep: 240 10% 3.9%;
--header: 240 4.9% 83.9%;
--header: 240 10% 3.9%;
}
.dark[data-theme='neutral'],
@@ -370,7 +370,7 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
@@ -396,7 +396,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -422,7 +422,7 @@
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;

View File

@@ -33,7 +33,7 @@
/* Used for destructive actions such as <Button variant="destructive"> */
--destructive: 0 78% 68%;
--destructive: 359.33 100% 65.1%;
--destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -35,7 +35,7 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"lucide-vue-next": "^0.427.0",
"vue": "^3.4.37"
"lucide-vue-next": "^0.428.0",
"vue": "^3.4.38"
}
}

View File

@@ -4,7 +4,7 @@ import { Icon } from '@iconify/vue';
function createIconifyIcon(icon: string) {
return defineComponent({
name: `SvgIcon-${icon}`,
name: `Icon-${icon}`,
setup(props, { attrs }) {
return () => h(Icon, { icon, ...props, ...attrs });
},

View File

@@ -1,5 +1,4 @@
export * from './create-icon';
export * from './lucide';
export * from './mdi';
export * from '@iconify/vue';

View File

@@ -1,7 +1,9 @@
export {
ArrowDown,
ArrowLeft,
ArrowLeftFromLine as MdiMenuOpen,
ArrowLeftToLine,
ArrowRightFromLine as MdiMenuClose,
ArrowRightLeft,
ArrowRightToLine,
ArrowUp,
@@ -9,11 +11,14 @@ export {
Bell,
BookOpenText,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
CircleHelp,
Copy,
CornerDownLeft,
Disc3 as IconDefault,
Disc as IconDefault,
Ellipsis,
ExternalLink,
Eye,
@@ -35,6 +40,8 @@ export {
Palette,
PanelLeft,
PanelRight,
Pin,
PinOff,
RotateCw,
Search,
SearchX,

View File

@@ -1,19 +0,0 @@
import { createIconifyIcon } from './create-icon';
export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
export const MdiWechat = createIconifyIcon('mdi:wechat');
export const MdiGithub = createIconifyIcon('mdi:github');
export const MdiGoogle = createIconifyIcon('mdi:google');
export const MdiQqchat = createIconifyIcon('mdi:qqchat');
export const MdiPin = createIconifyIcon('mdi:pin');
export const MdiPinOff = createIconifyIcon('mdi:pin-off');
export const MdiMenuClose = createIconifyIcon('mdi:menu-close');
export const MdiMenuOpen = createIconifyIcon('mdi:menu-open');

View File

@@ -5,6 +5,7 @@ export default defineBuildConfig({
declaration: true,
entries: [
'src/index',
'src/store',
'src/constants/index',
'src/utils/index',
'src/color/index',

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -44,6 +44,11 @@
"types": "./src/cache/index.ts",
"development": "./src/cache/index.ts",
"default": "./dist/cache/index.mjs"
},
"./store": {
"types": "./src/store.ts",
"development": "./src/store.ts",
"default": "./dist/store.mjs"
}
},
"publishConfig": {
@@ -56,7 +61,8 @@
},
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
"@vue/shared": "^3.4.37",
"@tanstack/vue-store": "^0.5.5",
"@vue/shared": "^3.4.38",
"clsx": "^2.1.1",
"defu": "^6.1.4",
"lodash.clonedeep": "^4.5.0",

View File

@@ -3,6 +3,7 @@
* @en_US Layout content height
*/
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
/**
* @zh_CN 默认命名空间

View File

@@ -1,4 +1,5 @@
export * from './cache';
export * from './color';
export * from './constants';
export * from './store';
export * from './utils';

View File

@@ -0,0 +1 @@
export * from '@tanstack/vue-store';

View File

@@ -1,140 +1,127 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getElementVisibleHeight } from './dom'; // 假设函数位于 utils.ts 中
import { getElementVisibleRect } from './dom'; // 假设函数位于 utils.ts 中
describe('getElementVisibleHeight', () => {
// Mocking the getBoundingClientRect method
const mockGetBoundingClientRect = vi.fn();
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
beforeAll(() => {
// Mock getBoundingClientRect method
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect;
describe('getElementVisibleRect', () => {
// 设置浏览器视口尺寸的 mock
beforeEach(() => {
vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(
800,
);
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(
1000,
);
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000);
});
afterAll(() => {
// Restore original getBoundingClientRect method
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
});
it('should return 0 if the element is null or undefined', () => {
expect(getElementVisibleHeight(null)).toBe(0);
expect(getElementVisibleHeight()).toBe(0);
});
it('should return the visible height of the element', () => {
// Mock the getBoundingClientRect return value
mockGetBoundingClientRect.mockReturnValue({
bottom: 500,
height: 400,
it('should return default rect if element is undefined', () => {
expect(getElementVisibleRect()).toEqual({
bottom: 0,
height: 0,
left: 0,
right: 0,
toJSON: () => ({}),
top: 0,
width: 0,
});
});
it('should return default rect if element is null', () => {
expect(getElementVisibleRect(null)).toEqual({
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
});
});
it('should return correct visible rect when element is fully visible', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 400,
height: 300,
left: 200,
right: 600,
top: 100,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 400,
height: 300,
left: 200,
right: 600,
top: 100,
width: 0,
x: 0,
y: 0,
width: 400,
});
const mockElement = document.createElement('div');
document.body.append(mockElement);
// Mocking window.innerHeight and document.documentElement.clientHeight
const originalInnerHeight = window.innerHeight;
const originalClientHeight = document.documentElement.clientHeight;
Object.defineProperty(window, 'innerHeight', {
value: 600,
writable: true,
});
Object.defineProperty(document.documentElement, 'clientHeight', {
value: 600,
writable: true,
});
expect(getElementVisibleHeight(mockElement)).toBe(400);
// Restore original values
Object.defineProperty(window, 'innerHeight', {
value: originalInnerHeight,
writable: true,
});
Object.defineProperty(document.documentElement, 'clientHeight', {
value: originalClientHeight,
writable: true,
});
mockElement.remove();
});
it('should return the visible height when element is partially out of viewport', () => {
// Mock the getBoundingClientRect return value
mockGetBoundingClientRect.mockReturnValue({
bottom: 300,
height: 400,
left: 0,
right: 0,
toJSON: () => ({}),
top: -100,
width: 0,
x: 0,
y: 0,
it('should return correct visible rect when element is partially off-screen at the top', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 200,
height: 250,
left: 100,
right: 500,
top: -50,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 200,
height: 200,
left: 100,
right: 500,
top: 0,
width: 400,
});
const mockElement = document.createElement('div');
document.body.append(mockElement);
// Mocking window.innerHeight and document.documentElement.clientHeight
const originalInnerHeight = window.innerHeight;
const originalClientHeight = document.documentElement.clientHeight;
Object.defineProperty(window, 'innerHeight', {
value: 600,
writable: true,
});
Object.defineProperty(document.documentElement, 'clientHeight', {
value: 600,
writable: true,
});
expect(getElementVisibleHeight(mockElement)).toBe(300);
// Restore original values
Object.defineProperty(window, 'innerHeight', {
value: originalInnerHeight,
writable: true,
});
Object.defineProperty(document.documentElement, 'clientHeight', {
value: originalClientHeight,
writable: true,
});
mockElement.remove();
});
it('should return 0 if the element is completely out of viewport', () => {
// Mock the getBoundingClientRect return value
mockGetBoundingClientRect.mockReturnValue({
bottom: -100,
height: 400,
left: 0,
right: 0,
toJSON: () => ({}),
top: -500,
width: 0,
x: 0,
y: 0,
it('should return correct visible rect when element is partially off-screen at the right', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 400,
height: 300,
left: 800,
right: 1200,
top: 100,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 400,
height: 300,
left: 800,
right: 1000,
top: 100,
width: 200,
});
});
const mockElement = document.createElement('div');
document.body.append(mockElement);
it('should return all zeros when element is completely off-screen', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 1200,
height: 300,
left: 1100,
right: 1400,
top: 900,
width: 300,
}),
} as HTMLElement;
expect(getElementVisibleHeight(mockElement)).toBe(0);
mockElement.remove();
expect(getElementVisibleRect(element)).toEqual({
bottom: 800,
height: 0,
left: 1100,
right: 1000,
top: 900,
width: 0,
});
});
});

View File

@@ -1,12 +1,28 @@
export interface VisibleDomRect {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
}
/**
* 获取元素可见高度
* 获取元素可见信息
* @param element
*/
function getElementVisibleHeight(
export function getElementVisibleRect(
element?: HTMLElement | null | undefined,
): number {
): VisibleDomRect {
if (!element) {
return 0;
return {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
};
}
const rect = element.getBoundingClientRect();
const viewHeight = Math.max(
@@ -17,7 +33,20 @@ function getElementVisibleHeight(
const top = Math.max(rect.top, 0);
const bottom = Math.min(rect.bottom, viewHeight);
return Math.max(0, bottom - top);
}
const viewWidth = Math.max(
document.documentElement.clientWidth,
window.innerWidth,
);
export { getElementVisibleHeight };
const left = Math.max(rect.left, 0);
const right = Math.min(rect.right, viewWidth);
return {
bottom,
height: Math.max(0, bottom - top),
left,
right,
top,
width: Math.max(0, right - left),
};
}

View File

@@ -5,6 +5,7 @@ export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './to';
export * from './tree';
export * from './unique';
export * from './update-css-variables';

View File

@@ -0,0 +1,21 @@
/**
* @param { Readonly<Promise> } promise
* @param {object=} errorExt - Additional Information you can pass to the err object
* @return { Promise }
*/
export async function to<T, U = Error>(
promise: Readonly<Promise<T>>,
errorExt?: object,
): Promise<[null, T] | [U, undefined]> {
try {
const data = await promise;
const result: [null, T] = [null, data];
return result;
} catch (error) {
if (errorExt) {
const parsedError = Object.assign({}, error, errorExt);
return [parsedError as U, undefined];
}
return [error as U, undefined];
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -38,7 +38,7 @@
}
},
"dependencies": {
"vue": "^3.4.37",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
}
}

View File

@@ -107,20 +107,23 @@ type MergeAll<
? MergeAll<Rest, Merge<R, F>>
: R;
export {
type AnyFunction,
type AnyNormalFunction,
type AnyPromiseFunction,
type DeepPartial,
type DeepReadonly,
type IntervalHandle,
type MaybeComputedRef,
type MaybeReadonlyRef,
type Merge,
type MergeAll,
type NonNullable,
type Nullable,
type ReadonlyRecordable,
type Recordable,
type TimeoutHandle,
type EmitType = (name: Name, ...args: any[]) => void;
export type {
AnyFunction,
AnyNormalFunction,
AnyPromiseFunction,
DeepPartial,
DeepReadonly,
EmitType,
IntervalHandle,
MaybeComputedRef,
MaybeReadonlyRef,
Merge,
MergeAll,
NonNullable,
Nullable,
ReadonlyRecordable,
Recordable,
TimeoutHandle,
};

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -36,10 +36,10 @@
},
"dependencies": {
"@vben-core/shared": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"radix-vue": "^1.9.4",
"sortablejs": "^1.15.2",
"vue": "^3.4.37"
"vue": "^3.4.38"
},
"devDependencies": {
"@types/sortablejs": "^1.15.8"

View File

@@ -1,4 +1,4 @@
export * from './use-content-height';
export * from './use-content-style';
export * from './use-namespace';
export * from './use-sortable';
export {

View File

@@ -1,47 +0,0 @@
import { computed, onMounted, ref, watch } from 'vue';
import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
getElementVisibleHeight,
} from '@vben-core/shared';
import { useCssVar, useDebounceFn, useWindowSize } from '@vueuse/core';
/**
* @zh_CN 获取内容高度(可视区域,不包含滚动条)
*/
function useContentHeight() {
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
const contentStyles = computed(() => {
return {
height: `var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT})`,
};
});
return { contentHeight, contentStyles };
}
/**
* @zh_CN 创建内容高度监听
*/
function useContentHeightListener() {
const contentElement = ref<HTMLDivElement | null>(null);
const { height, width } = useWindowSize();
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
const debouncedCalcHeight = useDebounceFn(() => {
contentHeight.value = `${getElementVisibleHeight(contentElement.value)}px`;
}, 200);
watch([height, width], () => {
debouncedCalcHeight();
});
onMounted(() => {
debouncedCalcHeight();
});
return { contentElement };
}
export { useContentHeight, useContentHeightListener };

View File

@@ -0,0 +1,59 @@
import type { CSSProperties } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
getElementVisibleRect,
type VisibleDomRect,
} from '@vben-core/shared';
import { useCssVar, useDebounceFn } from '@vueuse/core';
/**
* @zh_CN content style
*/
function useContentStyle() {
let resizeObserver: null | ResizeObserver = null;
const contentElement = ref<HTMLDivElement | null>(null);
const visibleDomRect = ref<null | VisibleDomRect>(null);
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH);
const overlayStyle = computed((): CSSProperties => {
const { height, left, top, width } = visibleDomRect.value ?? {};
return {
height: `${height}px`,
left: `${left}px`,
position: 'fixed',
top: `${top}px`,
width: `${width}px`,
zIndex: 1000,
};
});
const debouncedCalcHeight = useDebounceFn(
(_entries: ResizeObserverEntry[]) => {
visibleDomRect.value = getElementVisibleRect(contentElement.value);
contentHeight.value = `${visibleDomRect.value.height}px`;
contentWidth.value = `${visibleDomRect.value.width}px`;
},
100,
);
onMounted(() => {
if (contentElement.value && !resizeObserver) {
resizeObserver = new ResizeObserver(debouncedCalcHeight);
resizeObserver.observe(contentElement.value);
}
});
onUnmounted(() => {
resizeObserver?.disconnect();
resizeObserver = null;
});
return { contentElement, overlayStyle, visibleDomRect };
}
export { useContentStyle };

View File

@@ -39,7 +39,7 @@ describe('useSortable', () => {
expect(Sortable.default.create).toHaveBeenCalledWith(
mockElement,
expect.objectContaining({
animation: 100,
animation: 300,
delay: 400,
delayOnTouchOnly: true,
...customOptions,

View File

@@ -18,7 +18,7 @@ function useSortable<T extends HTMLElement>(
// Sortable?.default?.mount?.(AutoScroll);
const sortable = Sortable?.default?.create?.(sortableContainer, {
animation: 100,
animation: 300,
delay: 400,
delayOnTouchOnly: true,
...options,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -31,7 +31,7 @@
"dependencies": {
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.37"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.38"
}
}

View File

@@ -14,6 +14,7 @@ const defaultPreferences: Preferences = {
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,
enableRefreshToken: false,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',

View File

@@ -116,7 +116,6 @@ class PreferenceManager {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
// updateCSSVariables(this.state);
});
}

View File

@@ -40,6 +40,10 @@ interface AppPreferences {
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/**
* @zh_CN 是否开启refreshToken
*/
enableRefreshToken: boolean;
/** 是否移动端 */
isMobile: boolean;
/** 布局方式 */

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -41,7 +41,7 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.37"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.38"
}
}

View File

@@ -4,7 +4,7 @@ import type { ContentCompactType } from '@vben-core/typings';
import type { CSSProperties } from 'vue';
import { computed } from 'vue';
import { useContentHeightListener } from '@vben-core/composables';
import { useContentStyle } from '@vben-core/composables';
interface Props {
/**
@@ -24,7 +24,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {});
const { contentElement } = useContentHeightListener();
const { contentElement, overlayStyle } = useContentStyle();
const style = computed((): CSSProperties => {
const {
@@ -53,7 +53,8 @@ const style = computed((): CSSProperties => {
</script>
<template>
<main ref="contentElement" :style="style" class="bg-background-deep">
<main ref="contentElement" :style="style" class="bg-background-deep relative">
<slot :overlay-style="overlayStyle" name="overlay"></slot>
<slot></slot>
</main>
</template>

View File

@@ -2,9 +2,6 @@
import type { CSSProperties } from 'vue';
import { computed, useSlots } from 'vue';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
interface Props {
/**
* 横屏
@@ -14,11 +11,6 @@ interface Props {
* 高度
*/
height: number;
/**
* 是否混合导航
* @default false
*/
isMixedNav: boolean;
/**
* 是否移动端
*/
@@ -27,11 +19,6 @@ interface Props {
* 是否显示
*/
show: boolean;
/**
* 是否显示关闭菜单按钮
*/
showToggleBtn: boolean;
/**
* 侧边菜单宽度
*/
@@ -52,8 +39,6 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{ openMenu: []; toggleSidebar: [] }>();
const slots = useSlots();
const style = computed((): CSSProperties => {
@@ -72,10 +57,6 @@ const logoStyle = computed((): CSSProperties => {
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
};
});
function handleToggleMenu() {
props.isMobile ? emit('openMenu') : emit('toggleSidebar');
}
</script>
<template>
@@ -87,13 +68,9 @@ function handleToggleMenu() {
<div v-if="slots.logo" :style="logoStyle">
<slot name="logo"></slot>
</div>
<VbenIconButton
v-if="showToggleBtn || isMobile"
class="my-0 ml-2 mr-1 rounded-md"
@click="handleToggleMenu"
>
<Menu class="size-4" />
</VbenIconButton>
<slot name="toggle-button"> </slot>
<slot></slot>
</header>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { MdiMenuClose, MdiMenuOpen } from '@vben-core/icons';
import { ChevronsLeft, ChevronsRight } from '@vben-core/icons';
const collapsed = defineModel<boolean>('collapsed');
@@ -10,10 +10,10 @@ function handleCollapsed() {
<template>
<div
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm p-1 transition-all duration-300"
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm p-1"
@click.stop="handleCollapsed"
>
<MdiMenuClose v-if="collapsed" class="size-4" />
<MdiMenuOpen v-else class="size-4" />
<ChevronsRight v-if="collapsed" class="size-4" />
<ChevronsLeft v-else class="size-4" />
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { MdiPin, MdiPinOff } from '@vben-core/icons';
import { Pin, PinOff } from '@vben-core/icons';
const expandOnHover = defineModel<boolean>('expandOnHover');
@@ -10,10 +10,10 @@ function toggleFixed() {
<template>
<div
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm p-1 transition-all duration-300"
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm p-[5px] transition-all duration-300"
@click="toggleFixed"
>
<MdiPinOff v-if="!expandOnHover" />
<MdiPin v-else />
<PinOff v-if="!expandOnHover" class="size-3.5" />
<Pin v-else class="size-3.5" />
</div>
</template>

View File

@@ -4,6 +4,9 @@ import type { VbenLayoutProps } from './vben-layout';
import type { CSSProperties } from 'vue';
import { computed, ref, watch } from 'vue';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import {
@@ -330,11 +333,12 @@ const maskStyle = computed((): CSSProperties => {
const showHeaderToggleButton = computed(() => {
return (
props.headerToggleSidebarButton &&
isSideMode.value &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
!props.isMobile
props.isMobile ||
(props.headerToggleSidebarButton &&
isSideMode.value &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
!props.isMobile)
);
});
@@ -421,8 +425,12 @@ function handleClickMask() {
sidebarCollapse.value = true;
}
function handleOpenMenu() {
sidebarCollapse.value = false;
function handleHeaderToggle() {
if (props.isMobile) {
sidebarCollapse.value = false;
} else {
emit('toggleSidebar');
}
}
</script>
@@ -473,6 +481,9 @@ function handleOpenMenu() {
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
>
<div
:class="{
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,
}"
:style="headerWrapperStyle"
class="overflow-hidden transition-all duration-200"
>
@@ -480,20 +491,26 @@ function handleOpenMenu() {
v-if="headerVisible"
:full-width="!isSideMode"
:height="headerHeight"
:is-mixed-nav="isMixedNav"
:is-mobile="isMobile"
:show="!isFullContent && !headerHidden"
:show-toggle-btn="showHeaderToggleButton"
:sidebar-width="sidebarWidth"
:theme="headerTheme"
:width="mainStyle.width"
:z-index="headerZIndex"
@open-menu="handleOpenMenu"
@toggle-sidebar="() => emit('toggleSidebar')"
>
<template v-if="showHeaderLogo" #logo>
<slot name="logo"></slot>
</template>
<template #toggle-button>
<VbenIconButton
v-if="showHeaderToggleButton"
class="my-0 ml-2 mr-1 rounded-md"
@click="handleHeaderToggle"
>
<Menu class="size-4" />
</VbenIconButton>
</template>
<slot name="header"></slot>
</LayoutHeader>
@@ -519,6 +536,10 @@ function handleOpenMenu() {
class="transition-[margin-top] duration-200"
>
<slot name="content"></slot>
<template #overlay="{ overlayStyle }">
<slot :overlay-style="overlayStyle" name="content-overlay"></slot>
</template>
</LayoutContent>
<LayoutFooter

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -42,7 +42,7 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.37"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.38"
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { MenuItemProps, MenuItemRegistered } from '../interface';
import type { MenuItemProps, MenuItemRegistered } from '../types';
import { computed, onBeforeUnmount, onMounted, reactive, useSlots } from 'vue';

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