This commit is contained in:
dap
2024-08-20 08:44:26 +08:00
99 changed files with 1312 additions and 785 deletions

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
export default eventHandler((event) => { import { verifyAccessToken } from '~/utils/jwt-utils';
const token = getHeader(event, 'Authorization'); import { unAuthorizedResponse } from '~/utils/response';
if (!token) { export default eventHandler((event) => {
setResponseStatus(event, 401); const userinfo = verifyAccessToken(event);
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); if (!userinfo) {
return unAuthorizedResponse(event);
} }
const username = Buffer.from(token, 'base64').toString('utf8');
const codes = const codes =
MOCK_CODES.find((item) => item.username === username)?.codes ?? []; MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(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) => { export default defineEventHandler(async (event) => {
const { password, username } = await readBody(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( const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password, (item) => item.username === username && item.password === password,
); );
if (!findUser) { if (!findUser) {
setResponseStatus(event, 403); clearRefreshTokenCookie(event);
return useResponseError('UnauthorizedException', '用户名或密码错误'); return forbiddenResponse(event);
} }
const accessToken = Buffer.from(username).toString('base64'); const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({ return useResponseSuccess({
...findUser,
accessToken, 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) => { import { verifyAccessToken } from '~/utils/jwt-utils';
const token = getHeader(event, 'Authorization'); import { unAuthorizedResponse } from '~/utils/response';
if (!token) { export default eventHandler((event) => {
setResponseStatus(event, 401); const userinfo = verifyAccessToken(event);
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); if (!userinfo) {
return unAuthorizedResponse(event);
} }
const username = Buffer.from(token, 'base64').toString('utf8');
const menus = const menus =
MOCK_MENUS.find((item) => item.username === username)?.menus ?? []; MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(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) => { export default eventHandler((event) => {
const token = getHeader(event, 'Authorization'); const userinfo = verifyAccessToken(event);
if (!token) { if (!userinfo) {
setResponseStatus(event, 401); return unAuthorizedResponse(event);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
} }
const username = Buffer.from(token, 'base64').toString('utf8'); return useResponseSuccess(userinfo);
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userInfo } = user;
return useResponseSuccess(userInfo);
}); });

View File

@@ -1,11 +1,4 @@
export default defineEventHandler((event) => { 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') { if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204; event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.'; event.node.res.statusMessage = 'No Content.';

View File

@@ -10,6 +10,11 @@
"start": "nitro dev" "start": "nitro dev"
}, },
"dependencies": { "dependencies": {
"jsonwebtoken": "^9.0.2",
"nitropack": "^2.9.7" "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, id: 0,
password: '123456', password: '123456',

View File

@@ -1,3 +1,5 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) { export function useResponseSuccess<T = any>(data: T) {
return { return {
code: 0, code: 0,
@@ -15,3 +17,13 @@ export function useResponseError(message: string, error: any = null) {
message, 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", "name": "@vben/web-antd",
"version": "5.1.0", "version": "5.1.1",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {

View File

@@ -21,6 +21,11 @@ export namespace AuthApi {
client_id: string; client_id: string;
expire_in: number; expire_in: number;
} }
export interface RefreshTokenResult {
data: string;
status: number;
}
} }
/** /**

View File

@@ -1,12 +1,15 @@
/** /**
* 该文件可自行根据业务逻辑进行调整 * 该文件可自行根据业务逻辑进行调整
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request'; import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
type HttpResponse,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { isString } from '@vben/utils'; import { isString } from '@vben/utils';
@@ -49,219 +52,235 @@ function createRequestClient(baseURL: string) {
joinParamsToUrl: false, joinParamsToUrl: false,
// 是否加入时间戳 // 是否加入时间戳
joinTime: false, joinTime: false,
// 为每个请求携带 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 { */
// 退出登录 async function doReAuthenticate() {
await authStore.logout(); 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);
* http状态码不为200会走到这里 } else {
* 其他会走到addResponseInterceptor await authStore.logout();
* @param msg }
* @returns void }
*/
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => { /**
* 刷新token逻辑
*/
async function doRefreshToken() {
// 不需要
// 保留此方法只是为了合并方便
return '';
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
client.addRequestInterceptor({
fulfilled: (config) => {
const accessStore = useAccessStore();
// 添加token
config.headers.Authorization = formatToken(accessStore.accessToken);
/** /**
* locale跟后台不一致 需要转换 * locale跟后台不一致 需要转换
*/ */
const language = preferences.app.locale.replace('-', '_'); const language = preferences.app.locale.replace('-', '_');
return { config.headers['Accept-Language'] = language;
// 为每个请求携带 Accept-Language config.headers.clientId = clientId;
'Accept-Language': language,
clientId, const { encrypt, formatDate, joinParamsToUrl, joinTime = true } = config;
}; const params = config.params || {};
const data = config.data || false;
formatDate && data && !isString(data) && formatRequestDate(data);
if (config.method?.toUpperCase() === 'GET') {
if (isString(params)) {
// 兼容restful风格
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`;
config.params = undefined;
} else {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(
params || {},
joinTimestamp(joinTime, false),
);
}
} else {
if (isString(params)) {
// 兼容restful风格
config.url = config.url + params;
config.params = undefined;
} else {
formatDate && formatRequestDate(params);
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 ||
config.data instanceof FormData)
) {
config.data = data;
config.params = params;
} else {
// 非GET请求如果没有提供data则将params视为data
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data),
);
}
}
}
// 全局开启 && 该请求开启 && 是post/put请求
if (
enableEncrypt &&
encrypt &&
['POST', 'PUT'].includes(config.method?.toUpperCase() || '')
) {
const aesKey = generateAesKey();
config.headers['encrypt-key'] = encryptUtil.encrypt(
encryptBase64(aesKey),
);
config.data =
typeof config.data === 'object'
? encryptWithAes(JSON.stringify(config.data), aesKey)
: encryptWithAes(config.data, aesKey);
}
return config;
}, },
}); });
client.addRequestInterceptor((config) => { client.addResponseInterceptor<HttpResponse>({
const { encrypt, formatDate, joinParamsToUrl, joinTime = true } = config; fulfilled: (response) => {
const params = config.params || {}; const encryptKey = (response.headers || {})['encrypt-key'];
const data = config.data || false; if (encryptKey) {
formatDate && data && !isString(data) && formatRequestDate(data); /** RSA私钥解密 拿到解密秘钥的base64 */
if (config.method?.toUpperCase() === 'GET') { const base64Str = encryptUtil.decrypt(encryptKey);
if (isString(params)) { /** base64 解码 得到请求头的 AES 秘钥 */
// 兼容restful风格 const aesSecret = decryptBase64(base64Str.toString());
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`; /** 使用aesKey解密 responseData */
config.params = undefined; const decryptData = decryptWithAes(
} else { response.data as unknown as string,
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。 aesSecret,
config.params = Object.assign(
params || {},
joinTimestamp(joinTime, false),
); );
/** 赋值 需要转为对象 */
response.data = JSON.parse(decryptData);
} }
} else {
if (isString(params)) { const { isReturnNativeResponse, isTransformResponse } = response.config;
// 兼容restful风格 // 是否返回原生响应头 比如:需要获取响应头时使用该属性
config.url = config.url + params; if (isReturnNativeResponse) {
config.params = undefined; return response;
} else { }
formatDate && formatRequestDate(params); // 不进行任何处理,直接返回
if ( // 用于页面代码可能需要直接获取codedatamessage这些信息时开启
Reflect.has(config, 'data') && if (!isTransformResponse) {
config.data && return response.data;
(Object.keys(config.data).length > 0 || }
config.data instanceof FormData)
) { const axiosResponseData = response.data;
config.data = data; if (!axiosResponseData) {
config.params = params; throw new Error($t('fallback.http.apiRequestFailed'));
} else { }
// 非GET请求如果没有提供data则将params视为data
config.data = params; // ruoyi-plus没有采用严格的{code, msg, data}模式
config.params = undefined; const { code, data, msg, ...other } = axiosResponseData;
// 这里逻辑可以根据项目进行修改
const hasSuccess = Reflect.has(axiosResponseData, 'code') && code === 200;
if (hasSuccess) {
let successMsg = msg;
if (isNull(successMsg) || isEmpty(successMsg)) {
successMsg = $t(`fallback.http.operationSuccess`);
} }
if (joinParamsToUrl) {
config.url = setObjToUrlParams( if (response.config.successMessageMode === 'modal') {
config.url as string, Modal.success({
Object.assign({}, config.params, config.data), content: successMsg,
); title: $t('fallback.http.successTip'),
});
} else if (response.config.successMessageMode === 'message') {
message.success(successMsg);
} }
// ruoyi-plus没有采用严格的{code, msg, data}模式
// 如果有data 直接返回data 没有data将剩余参数(...other)封装为data返回
// 需要考虑data为null的情况(比如查询为空)
if (data !== undefined) {
return data;
}
return other;
} }
} // 在此处根据自己项目的实际情况对不同的code执行不同的操作
// 全局开启 && 该请求开启 && 是post/put请求 // 如果不希望中断当前请求请return数据否则直接抛出异常即可
if ( let timeoutMsg = '';
enableEncrypt && switch (code) {
encrypt && case 401: {
['POST', 'PUT'].includes(config.method?.toUpperCase() || '') const _msg = '登录超时, 请重新登录';
) { const userStore = useAuthStore();
const aesKey = generateAesKey(); userStore.logout().then(() => {
config.headers['encrypt-key'] = encryptUtil.encrypt( /** 只弹窗一次 */
encryptBase64(aesKey), if (showTimeoutToast) {
); showTimeoutToast = false;
message.error(_msg);
config.data = /** 定时器 3s后再开启弹窗 */
typeof config.data === 'object' setTimeout(() => {
? encryptWithAes(JSON.stringify(config.data), aesKey) showTimeoutToast = true;
: encryptWithAes(config.data, aesKey); }, 3000);
} }
return config; });
}); // 不再执行下面逻辑
return;
client.addResponseInterceptor<HttpResponse>((response) => { }
const encryptKey = (response.headers || {})['encrypt-key']; default: {
if (encryptKey) { if (msg) {
/** RSA私钥解密 拿到解密秘钥的base64 */ timeoutMsg = msg;
const base64Str = encryptUtil.decrypt(encryptKey);
/** base64 解码 得到请求头的 AES 秘钥 */
const aesSecret = decryptBase64(base64Str.toString());
/** 使用aesKey解密 responseData */
const decryptData = decryptWithAes(
response.data as unknown as string,
aesSecret,
);
/** 赋值 需要转为对象 */
response.data = JSON.parse(decryptData);
}
const { isReturnNativeResponse, isTransformResponse } = response.config;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return response;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return response.data;
}
const axiosResponseData = response.data;
if (!axiosResponseData) {
throw new Error($t('fallback.http.apiRequestFailed'));
}
// ruoyi-plus没有采用严格的{code, msg, data}模式
const { code, data, msg, ...other } = axiosResponseData;
// 这里逻辑可以根据项目进行修改
const hasSuccess = Reflect.has(axiosResponseData, 'code') && code === 200;
if (hasSuccess) {
let successMsg = msg;
if (isNull(successMsg) || isEmpty(successMsg)) {
successMsg = $t(`fallback.http.operationSuccess`);
}
if (response.config.successMessageMode === 'modal') {
Modal.success({
content: successMsg,
title: $t('fallback.http.successTip'),
});
} else if (response.config.successMessageMode === 'message') {
message.success(successMsg);
}
// ruoyi-plus没有采用严格的{code, msg, data}模式
// 如果有data 直接返回data 没有data将剩余参数(...other)封装为data返回
// 需要考虑data为null的情况(比如查询为空)
if (data !== undefined) {
return data;
}
return other;
}
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
// 如果不希望中断当前请求请return数据否则直接抛出异常即可
let timeoutMsg = '';
switch (code) {
case 401: {
const _msg = '登录超时, 请重新登录';
const userStore = useAuthStore();
userStore.logout().then(() => {
/** 只弹窗一次 */
if (showTimeoutToast) {
showTimeoutToast = false;
message.error(_msg);
/** 定时器 3s后再开启弹窗 */
setTimeout(() => {
showTimeoutToast = true;
}, 3000);
} }
});
// 不再执行下面逻辑
return;
}
default: {
if (msg) {
timeoutMsg = msg;
} }
} }
}
// errorMessageMode='modal'的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误 // errorMessageMode='modal'的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示 // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
if (response.config.errorMessageMode === 'modal') { if (response.config.errorMessageMode === 'modal') {
Modal.error({ Modal.error({
content: timeoutMsg, content: timeoutMsg,
title: $t('fallback.http.errorTip'), title: $t('fallback.http.errorTip'),
}); });
} else if (response.config.errorMessageMode === 'message') { } else if (response.config.errorMessageMode === 'message') {
message.error(timeoutMsg); message.error(timeoutMsg);
} }
throw new Error(timeoutMsg || $t('fallback.http.apiRequestFailed')); throw new Error(timeoutMsg || $t('fallback.http.apiRequestFailed'));
},
}); });
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理, 如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
);
return client; return client;
} }
export const requestClient = createRequestClient(apiURL); export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -72,7 +72,7 @@ export const useAuthStore = defineStore('auth', () => {
}; };
} }
async function logout() { async function logout(redirect: boolean = true) {
try { try {
await doLogout(); await doLogout();
} catch (error) { } catch (error) {
@@ -84,9 +84,11 @@ export const useAuthStore = defineStore('auth', () => {
// 回登陆页带上当前路由地址 // 回登陆页带上当前路由地址
await router.replace({ await router.replace({
path: LOGIN_PATH, path: LOGIN_PATH,
query: { query: redirect
redirect: encodeURIComponent(router.currentRoute.value.fullPath), ? {
}, redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
}); });
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request'; import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string; accessToken: string;
desc: string; desc: string;
realName: string; realName: string;
refreshToken: string;
userId: string; userId: string;
username: 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); 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 { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request'; import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, 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; return client;
} }
export const requestClient = createRequestClient(apiURL); 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 type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; 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 { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
const router = useRouter();
async function handleLogout() { async function handleLogout() {
resetAllStores(); await authStore.logout(false);
resetRoutes();
await router.replace(LOGIN_PATH);
} }
function handleNoticeClear() { function handleNoticeClear() {

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request'; import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string; accessToken: string;
desc: string; desc: string;
realName: string; realName: string;
refreshToken: string;
userId: string; userId: string;
username: 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); 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 { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request'; import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from '#/naive'; import { message } from '#/naive';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, 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; return client;
} }
export const requestClient = createRequestClient(apiURL); 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 type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; 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 { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
const router = useRouter();
async function handleLogout() { async function handleLogout() {
resetAllStores(); await authStore.logout(false);
resetRoutes();
await router.replace(LOGIN_PATH);
} }
function handleNoticeClear() { function handleNoticeClear() {

View File

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

View File

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

View File

@@ -163,70 +163,105 @@ export async function deleteUserApi(user: UserInfo) {
/** /**
* 该文件可自行根据业务逻辑进行调整 * 该文件可自行根据业务逻辑进行调整
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request'; import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, 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; return client;
} }
export const requestClient = createRequestClient(apiURL); 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); 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 ## 数据 Mock
::: tip 生产环境 Mock ::: tip 生产环境 Mock

View File

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

View File

@@ -28,7 +28,12 @@ apps/web-native
## Mock 服务精简 ## Mock 服务精简
如果你不需要`Mock`服务,你可以直接删除`apps/backend-mock`文件夹即可 如果你不需要`Mock`服务,你可以直接删除`apps/backend-mock`文件夹。同时在你的应用下`.env.development`文件中删除`VITE_NITRO_MOCK`变量
```bash
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false
```
## 安装依赖 ## 安装依赖
@@ -47,6 +52,11 @@ pnpm install
```json ```json
{ {
"scripts": { "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:antd": "pnpm -F @vben/web-antd run dev",
"dev:docs": "pnpm -F @vben/docs run dev", "dev:docs": "pnpm -F @vben/docs run dev",
"dev:ele": "pnpm -F @vben/web-ele run dev", "dev:ele": "pnpm -F @vben/web-ele run dev",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "vben-admin-pro", "name": "vben-admin-pro",
"version": "5.1.0", "version": "5.1.1",
"private": true, "private": true,
"keywords": [ "keywords": [
"monorepo", "monorepo",

View File

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

View File

@@ -34,7 +34,7 @@
/* Used for destructive actions such as <Button variant="destructive"> */ /* 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%; --destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */ /* Used for success actions such as <message> */
@@ -110,7 +110,7 @@
--muted-foreground: 217.9 10.6% 64.9%; --muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%; --accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%; --accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%; --border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%; --input: 215 27.9% 16.9%;
@@ -136,7 +136,7 @@
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%; --accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%; --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%; --destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
@@ -162,7 +162,7 @@
--muted-foreground: 0 0% 63.9%; --muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%; --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 0 0% 14.9%; --input: 0 0% 14.9%;
@@ -188,7 +188,7 @@
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%; --accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
@@ -214,7 +214,7 @@
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%; --accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
@@ -240,7 +240,7 @@
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%; --accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%; --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%; --destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
@@ -266,7 +266,7 @@
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%; --accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%; --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%; --destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
@@ -318,7 +318,7 @@
--muted-foreground: 24 5.4% 63.9%; --muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%; --accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%; --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%; --destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%; --border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%; --input: 12 6.5% 15.1%;
@@ -344,7 +344,7 @@
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
@@ -370,7 +370,7 @@
--muted-foreground: 0 0% 63.9%; --muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%; --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 0 0% 14.9%; --input: 0 0% 14.9%;
@@ -396,7 +396,7 @@
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%; --accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 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%; --muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%; --accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%; --accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%; --border: 215 27.9% 16.9%;
--input: 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"> */ /* Used for destructive actions such as <Button variant="destructive"> */
--destructive: 0 78% 68%; --destructive: 359.33 100% 65.1%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */ /* Used for success actions such as <message> */

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ export * from './inference';
export * from './letter'; export * from './letter';
export * from './merge'; export * from './merge';
export * from './nprogress'; export * from './nprogress';
export * from './to';
export * from './tree'; export * from './tree';
export * from './unique'; export * from './unique';
export * from './update-css-variables'; 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", "name": "@vben-core/typings",
"version": "5.1.0", "version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin", "homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import type { VbenLayoutProps } from './vben-layout';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { computed, ref, watch } 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 { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import { import {
@@ -330,11 +333,12 @@ const maskStyle = computed((): CSSProperties => {
const showHeaderToggleButton = computed(() => { const showHeaderToggleButton = computed(() => {
return ( return (
props.headerToggleSidebarButton && props.isMobile ||
isSideMode.value && (props.headerToggleSidebarButton &&
!isSidebarMixedNav.value && isSideMode.value &&
!isMixedNav.value && !isSidebarMixedNav.value &&
!props.isMobile !isMixedNav.value &&
!props.isMobile)
); );
}); });
@@ -421,8 +425,12 @@ function handleClickMask() {
sidebarCollapse.value = true; sidebarCollapse.value = true;
} }
function handleOpenMenu() { function handleHeaderToggle() {
sidebarCollapse.value = false; if (props.isMobile) {
sidebarCollapse.value = false;
} else {
emit('toggleSidebar');
}
} }
</script> </script>
@@ -473,27 +481,36 @@ function handleOpenMenu() {
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in" class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
> >
<div <div
:class="{
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,
}"
:style="headerWrapperStyle" :style="headerWrapperStyle"
class="overflow-hidden shadow-[0_16px_24px_hsl(var(--background))] transition-all duration-200" class="overflow-hidden transition-all duration-200"
> >
<LayoutHeader <LayoutHeader
v-if="headerVisible" v-if="headerVisible"
:full-width="!isSideMode" :full-width="!isSideMode"
:height="headerHeight" :height="headerHeight"
:is-mixed-nav="isMixedNav"
:is-mobile="isMobile" :is-mobile="isMobile"
:show="!isFullContent && !headerHidden" :show="!isFullContent && !headerHidden"
:show-toggle-btn="showHeaderToggleButton"
:sidebar-width="sidebarWidth" :sidebar-width="sidebarWidth"
:theme="headerTheme" :theme="headerTheme"
:width="mainStyle.width" :width="mainStyle.width"
:z-index="headerZIndex" :z-index="headerZIndex"
@open-menu="handleOpenMenu"
@toggle-sidebar="() => emit('toggleSidebar')"
> >
<template v-if="showHeaderLogo" #logo> <template v-if="showHeaderLogo" #logo>
<slot name="logo"></slot> <slot name="logo"></slot>
</template> </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> <slot name="header"></slot>
</LayoutHeader> </LayoutHeader>

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import type {
MenuItemRegistered, MenuItemRegistered,
MenuProps, MenuProps,
MenuProvider, MenuProvider,
} from '../interface'; } from '../types';
import { import {
computed, computed,

View File

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

View File

@@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { HoverCardContentProps } from '@vben-core/shadcn-ui'; import type { HoverCardContentProps } from '@vben-core/shadcn-ui';
import type { import type { MenuItemRegistered, MenuProvider, SubMenuProps } from '../types';
MenuItemRegistered,
MenuProvider,
SubMenuProps,
} from '../interface';
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
@@ -74,7 +70,6 @@ const contentProps = computed((): HoverCardContentProps => {
collisionPadding: { top: 20 }, collisionPadding: { top: 20 },
side, side,
sideOffset: isHorizontal ? 5 : 10, sideOffset: isHorizontal ? 5 : 10,
// sideOffset: 10,
}; };
}); });
@@ -216,7 +211,7 @@ onBeforeUnmount(() => {
]" ]"
:content-props="contentProps" :content-props="contentProps"
:open="true" :open="true"
:open-delay="30" :open-delay="0"
> >
<template #trigger> <template #trigger>
<SubMenuContent <SubMenuContent

View File

@@ -1,4 +1,4 @@
import type { MenuProvider, SubMenuProvider } from '../interface'; import type { MenuProvider, SubMenuProvider } from '../types';
import { getCurrentInstance, inject, provide } from 'vue'; import { getCurrentInstance, inject, provide } from 'vue';

View File

@@ -1,4 +1,4 @@
import type { SubMenuProvider } from '../interface'; import type { SubMenuProvider } from '../types';
import { computed, getCurrentInstance } from 'vue'; import { computed, getCurrentInstance } from 'vue';

View File

@@ -1,3 +1,3 @@
export * from './components/normal-menu'; export * from './components/normal-menu';
export type * from './interface';
export { default as Menu } from './menu.vue'; export { default as Menu } from './menu.vue';
export type * from './types';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MenuRecordRaw } from '@vben-core/typings'; import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuProps } from './interface'; import type { MenuProps } from './types';
import { useForwardProps } from '@vben-core/composables'; import { useForwardProps } from '@vben-core/composables';

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ const tabsView = computed((): TabConfig[] => {
/> />
<Pin <Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable" v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all" class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)" @click.stop="() => emit('unpin', tab)"
/> />
</div> </div>
@@ -150,18 +150,6 @@ const tabsView = computed((): TabConfig[] => {
<style scoped> <style scoped>
.tabs-chrome { .tabs-chrome {
/* .dragging { */
/* .tabs-chrome__item-main {
@apply pr-0;
} */
/* .tabs-chrome__extra {
@apply hidden;
} */
/* } */
&__item:not(.dragging) { &__item:not(.dragging) {
@apply cursor-pointer; @apply cursor-pointer;

View File

@@ -100,7 +100,7 @@ const tabsView = computed((): TabConfig[] => {
/> />
<Pin <Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable" v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all" class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)" @click.stop="() => emit('unpin', tab)"
/> />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ export function useAntdDesignTokens() {
colorBgLayout: '', colorBgLayout: '',
colorBgMask: '', colorBgMask: '',
colorBorder: '', colorBorder: '',
colorBorderSecondary: '',
colorError: '', colorError: '',
colorInfo: '', colorInfo: '',
colorPrimary: '', colorPrimary: '',
@@ -48,7 +49,8 @@ export function useAntdDesignTokens() {
getCssVariableValue('--primary-foreground'); getCssVariableValue('--primary-foreground');
tokens.colorBorder = getCssVariableValue('--border'); tokens.colorBorderSecondary = tokens.colorBorder =
getCssVariableValue('--border');
tokens.colorBgElevated = getCssVariableValue('--popover'); tokens.colorBgElevated = getCssVariableValue('--popover');

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
export * from './preset-interceptors';
export * from './request-client'; export * from './request-client';
export type * from './types'; export type * from './types';

View File

@@ -1,10 +1,19 @@
import type { import type { AxiosInstance, AxiosResponse } from 'axios';
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
const errorHandler = (res: Error) => Promise.reject(res); import type {
RequestInterceptorConfig,
ResponseInterceptorConfig,
} from '../types';
const defaultRequestInterceptorConfig: RequestInterceptorConfig = {
fulfilled: (response) => response,
rejected: (error) => Promise.reject(error),
};
const defaultResponseInterceptorConfig: ResponseInterceptorConfig = {
fulfilled: (response: AxiosResponse) => response,
rejected: (error) => Promise.reject(error),
};
class InterceptorManager { class InterceptorManager {
private axiosInstance: AxiosInstance; private axiosInstance: AxiosInstance;
@@ -13,28 +22,18 @@ class InterceptorManager {
this.axiosInstance = instance; this.axiosInstance = instance;
} }
addRequestInterceptor( addRequestInterceptor({
fulfilled: ( fulfilled,
config: InternalAxiosRequestConfig, rejected,
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>, }: RequestInterceptorConfig = defaultRequestInterceptorConfig) {
rejected?: (error: any) => any, this.axiosInstance.interceptors.request.use(fulfilled, rejected);
) {
this.axiosInstance.interceptors.request.use(
fulfilled,
rejected || errorHandler,
);
} }
addResponseInterceptor<T = any>( addResponseInterceptor<T = any>({
fulfilled: ( fulfilled,
response: AxiosResponse<T>, rejected,
) => AxiosResponse | Promise<AxiosResponse>, }: ResponseInterceptorConfig<T> = defaultResponseInterceptorConfig) {
rejected?: (error: any) => any, this.axiosInstance.interceptors.response.use(fulfilled, rejected);
) {
this.axiosInstance.interceptors.response.use(
fulfilled,
rejected || errorHandler,
);
} }
} }

View File

@@ -0,0 +1,124 @@
import type { RequestClient } from './request-client';
import type { MakeErrorMessageFn, ResponseInterceptorConfig } from './types';
import { $t } from '@vben/locales';
import axios from 'axios';
export const authenticateResponseInterceptor = ({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken,
formatToken,
}: {
client: RequestClient;
doReAuthenticate: () => Promise<void>;
doRefreshToken: () => Promise<string>;
enableRefreshToken: boolean;
formatToken: (token: string) => null | string;
}): ResponseInterceptorConfig => {
return {
rejected: async (error) => {
const { config, response } = error;
// 如果不是 401 错误,直接抛出异常
if (response?.status !== 401) {
throw error;
}
// 判断是否启用了 refreshToken 功能
// 如果没有启用或者已经是重试请求了,直接跳转到重新登录
if (!enableRefreshToken || config.__isRetryRequest) {
await doReAuthenticate();
throw error;
}
// 如果正在刷新 token则将请求加入队列等待刷新完成
if (client.isRefreshing) {
return new Promise((resolve) => {
client.refreshTokenQueue.push((newToken: string) => {
config.headers.Authorization = formatToken(newToken);
resolve(client.request(config.url, { ...config }));
});
});
}
// 标记开始刷新 token
client.isRefreshing = true;
// 标记当前请求为重试请求,避免无限循环
config.__isRetryRequest = true;
try {
const newToken = await doRefreshToken();
// 处理队列中的请求
client.refreshTokenQueue.forEach((callback) => callback(newToken));
// 清空队列
client.refreshTokenQueue = [];
return client.request(error.config.url, { ...error.config });
} catch (refreshError) {
// 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面)
client.refreshTokenQueue.forEach((callback) => callback(''));
client.refreshTokenQueue = [];
console.error('Refresh token failed, please login again.');
throw refreshError;
} finally {
client.isRefreshing = false;
}
},
};
};
export const errorMessageResponseInterceptor = (
makeErrorMessage?: MakeErrorMessageFn,
): ResponseInterceptorConfig => {
return {
rejected: (error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = $t('fallback.http.networkError');
} else if (error?.message?.includes?.('timeout')) {
errMsg = $t('fallback.http.requestTimeout');
}
if (errMsg) {
makeErrorMessage?.(errMsg);
return Promise.reject(error);
}
let errorMessage = error?.response?.data?.error?.message ?? '';
const status = error?.response?.status;
switch (status) {
case 400: {
errorMessage = $t('fallback.http.badRequest');
break;
}
case 401: {
errorMessage = $t('fallback.http.unauthorized');
break;
}
case 403: {
errorMessage = $t('fallback.http.forbidden');
break;
}
case 404: {
errorMessage = $t('fallback.http.notFound');
break;
}
case 408: {
errorMessage = $t('fallback.http.requestTimeout');
break;
}
default: {
errorMessage = $t('fallback.http.internalServerError');
}
}
makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
};
};

View File

@@ -3,17 +3,8 @@ import type {
AxiosRequestConfig, AxiosRequestConfig,
AxiosResponse, AxiosResponse,
CreateAxiosDefaults, CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios'; } from 'axios';
import type {
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
} from './types';
import { $t } from '@vben/locales';
import { merge } from '@vben/utils'; import { merge } from '@vben/utils';
import axios from 'axios'; import axios from 'axios';
@@ -21,16 +12,19 @@ import axios from 'axios';
import { FileDownloader } from './modules/downloader'; import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor'; import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader'; import { FileUploader } from './modules/uploader';
import { type RequestClientOptions } from './types';
class RequestClient { class RequestClient {
private instance: AxiosInstance; private readonly instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private makeErrorMessage: MakeErrorMessageFn | undefined;
private makeRequestHeaders: MakeRequestHeadersFn | undefined;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor']; public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor']; public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download']; public download: FileDownloader['download'];
// 是否正在刷新token
public isRefreshing = false;
// 刷新token队列
public refreshTokenQueue: ((token: string) => void)[] = [];
public upload: FileUploader['upload']; public upload: FileUploader['upload'];
/** /**
@@ -38,7 +32,6 @@ class RequestClient {
* @param options - Axios请求配置可选 * @param options - Axios请求配置可选
*/ */
constructor(options: RequestClientOptions = {}) { constructor(options: RequestClientOptions = {}) {
this.bindMethods();
// 合并默认配置和传入的配置 // 合并默认配置和传入的配置
const defaultConfig: CreateAxiosDefaults = { const defaultConfig: CreateAxiosDefaults = {
headers: { headers: {
@@ -47,18 +40,11 @@ class RequestClient {
// 默认超时时间 // 默认超时时间
timeout: 10_000, timeout: 10_000,
}; };
const { const { ...axiosConfig } = options;
makeAuthorization,
makeErrorMessage,
makeRequestHeaders,
...axiosConfig
} = options;
const requestConfig = merge(axiosConfig, defaultConfig); const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig); this.instance = axios.create(requestConfig);
this.makeAuthorization = makeAuthorization;
this.makeRequestHeaders = makeRequestHeaders; this.bindMethods();
this.makeErrorMessage = makeErrorMessage;
// 实例化拦截器管理器 // 实例化拦截器管理器
const interceptorManager = new InterceptorManager(this.instance); const interceptorManager = new InterceptorManager(this.instance);
@@ -73,9 +59,6 @@ class RequestClient {
// 实例化文件下载器 // 实例化文件下载器
const fileDownloader = new FileDownloader(this); const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader); this.download = fileDownloader.download.bind(fileDownloader);
// 设置默认的拦截器
this.setupInterceptors();
} }
private bindMethods() { private bindMethods() {
@@ -93,93 +76,6 @@ class RequestClient {
}); });
} }
private setupDefaultResponseInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] =
`Bearer ${token}`;
}
const requestHeader = this.makeRequestHeaders?.(config);
if (requestHeader) {
for (const [key, value] of Object.entries(requestHeader)) {
config.headers[key] = value;
}
}
return config;
},
(error: any) => Promise.reject(error),
);
this.addResponseInterceptor(
(response: AxiosResponse) => {
return response;
},
(error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = $t('fallback.http.networkError');
} else if (error?.message?.includes?.('timeout')) {
errMsg = $t('fallback.http.requestTimeout');
}
if (errMsg) {
this.makeErrorMessage?.(errMsg);
return Promise.reject(error);
}
let errorMessage = error?.response?.data?.error?.message ?? '';
const status = error?.response?.status;
switch (status) {
case 400: {
errorMessage = $t('fallback.http.badRequest');
break;
}
case 401: {
errorMessage = $t('fallback.http.unauthorized');
this.makeAuthorization?.().unAuthorizedHandler?.();
break;
}
case 403: {
errorMessage = $t('fallback.http.forbidden');
break;
}
// 404请求不存在
case 404: {
errorMessage = $t('fallback.http.notFound');
break;
}
case 408: {
errorMessage = $t('fallback.http.requestTimeout');
break;
}
default: {
errorMessage = $t('fallback.http.internalServerError');
}
}
this.makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
);
}
private setupInterceptors() {
// 默认拦截器
this.setupDefaultResponseInterceptor();
}
/** /**
* DELETE请求方法 * DELETE请求方法
*/ */

View File

@@ -1,4 +1,8 @@
import type { CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; import type {
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios';
type RequestContentType = type RequestContentType =
| 'application/json;charset=utf-8' | 'application/json;charset=utf-8'
@@ -6,42 +10,26 @@ type RequestContentType =
| 'application/x-www-form-urlencoded;charset=utf-8' | 'application/x-www-form-urlencoded;charset=utf-8'
| 'multipart/form-data;charset=utf-8'; | 'multipart/form-data;charset=utf-8';
interface MakeAuthorization { type RequestClientOptions = CreateAxiosDefaults;
key?: string;
tokenHandler: () => { refreshToken: string; token: string } | null; interface RequestInterceptorConfig {
unAuthorizedHandler?: () => Promise<void>; fulfilled?: (
config: InternalAxiosRequestConfig,
) =>
| InternalAxiosRequestConfig<any>
| Promise<InternalAxiosRequestConfig<any>>;
rejected?: (error: any) => any;
} }
interface MakeRequestHeaders { interface ResponseInterceptorConfig<T = any> {
'Accept-Language'?: string; fulfilled?: (
response: AxiosResponse<T>,
) => AxiosResponse | Promise<AxiosResponse>;
rejected?: (error: any) => any;
} }
type MakeAuthorizationFn = (
config?: InternalAxiosRequestConfig,
) => MakeAuthorization;
type MakeRequestHeadersFn = (
config?: InternalAxiosRequestConfig,
) => MakeRequestHeaders;
type MakeErrorMessageFn = (message: string) => void; type MakeErrorMessageFn = (message: string) => void;
interface RequestClientOptions extends CreateAxiosDefaults {
/**
* 用于生成Authorization
*/
makeAuthorization?: MakeAuthorizationFn;
/**
* 用于生成错误消息
*/
makeErrorMessage?: MakeErrorMessageFn;
/**
* 用于生成请求头
*/
makeRequestHeaders?: MakeRequestHeadersFn;
}
interface HttpResponse<T = any> { interface HttpResponse<T = any> {
code: number; code: number;
data: T; data: T;
@@ -50,11 +38,11 @@ interface HttpResponse<T = any> {
export type { export type {
HttpResponse, HttpResponse,
MakeAuthorizationFn,
MakeErrorMessageFn, MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions, RequestClientOptions,
RequestContentType, RequestContentType,
RequestInterceptorConfig,
ResponseInterceptorConfig,
}; };
export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined; export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
return !paths.includes(getTabPath(item)); return !paths.includes(getTabPath(item));
}); });
this.updateCacheTab(); this.updateCacheTabs();
}, },
/** /**
* @zh_CN 关闭标签页 * @zh_CN 关闭标签页
@@ -141,7 +141,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
this.tabs.splice(tabIndex, 1, mergedTab); this.tabs.splice(tabIndex, 1, mergedTab);
} }
this.updateCacheTab(); this.updateCacheTabs();
}, },
/** /**
* @zh_CN 关闭所有标签页 * @zh_CN 关闭所有标签页
@@ -150,7 +150,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
const newTabs = this.tabs.filter((tab) => isAffixTab(tab)); const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1); this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
await this._goToDefaultTab(router); await this._goToDefaultTab(router);
this.updateCacheTab(); this.updateCacheTabs();
}, },
/** /**
* @zh_CN 关闭左侧标签页 * @zh_CN 关闭左侧标签页
@@ -230,7 +230,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
// 关闭不是激活选项卡 // 关闭不是激活选项卡
if (getTabPath(currentRoute.value) !== getTabPath(tab)) { if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
this._close(tab); this._close(tab);
this.updateCacheTab(); this.updateCacheTabs();
return; return;
} }
const index = this.getTabs.findIndex( const index = this.getTabs.findIndex(
@@ -339,7 +339,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
); );
if (findTab) { if (findTab) {
findTab.meta.newTabTitle = undefined; findTab.meta.newTabTitle = undefined;
await this.updateCacheTab(); await this.updateCacheTabs();
} }
}, },
@@ -367,7 +367,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (findTab) { if (findTab) {
findTab.meta.newTabTitle = title; findTab.meta.newTabTitle = title;
await this.updateCacheTab(); await this.updateCacheTabs();
} }
}, },
@@ -417,7 +417,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
/** /**
* 根据当前打开的选项卡更新缓存 * 根据当前打开的选项卡更新缓存
*/ */
async updateCacheTab() { async updateCacheTabs() {
const cacheMap = new Set<string>(); const cacheMap = new Set<string>();
for (const tab of this.tabs) { for (const tab of this.tabs) {
@@ -426,7 +426,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (!keepAlive) { if (!keepAlive) {
continue; continue;
} }
tab.matched.forEach((t, i) => { (tab.matched || []).forEach((t, i) => {
if (i > 0) { if (i > 0) {
cacheMap.add(t.name as string); cacheMap.add(t.name as string);
} }

View File

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

View File

@@ -6,3 +6,8 @@
overscroll-behavior: none; overscroll-behavior: none;
color: inherit; color: inherit;
} }
.ant-message-notice-content,
.ant-notification-notice {
@apply dark:border-border/60 dark:border;
}

View File

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

View File

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

View File

@@ -2,4 +2,4 @@
VITE_APP_TITLE=Vben Admin VITE_APP_TITLE=Vben Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离 # 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-antd VITE_APP_NAMESPACE=vben-web-play

View File

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

View File

@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request'; import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@@ -12,10 +12,14 @@ export namespace AuthApi {
accessToken: string; accessToken: string;
desc: string; desc: string;
realName: string; realName: string;
refreshToken: string;
userId: string; userId: string;
username: 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); 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,102 @@
/** /**
* 该文件可自行根据业务逻辑进行调整 * 该文件可自行根据业务逻辑进行调整
*/ */
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request'; import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, 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; return client;
} }
export const requestClient = createRequestClient(apiURL); 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 type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; 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 { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
@@ -14,16 +13,10 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
@@ -100,12 +93,8 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
const router = useRouter();
async function handleLogout() { async function handleLogout() {
resetAllStores(); await authStore.logout(false);
resetRoutes();
await router.replace(LOGIN_PATH);
} }
function handleNoticeClear() { function handleNoticeClear() {

View File

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

94
pnpm-lock.yaml generated
View File

@@ -116,9 +116,19 @@ importers:
apps/backend-mock: apps/backend-mock:
dependencies: dependencies:
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
nitropack: nitropack:
specifier: ^2.9.7 specifier: ^2.9.7
version: 2.9.7(encoding@0.1.13) version: 2.9.7(encoding@0.1.13)
devDependencies:
'@types/jsonwebtoken':
specifier: ^9.0.6
version: 9.0.6
h3:
specifier: ^1.12.0
version: 1.12.0
apps/web-antd: apps/web-antd:
dependencies: dependencies:
@@ -3408,7 +3418,6 @@ packages:
'@ls-lint/ls-lint@2.2.3': '@ls-lint/ls-lint@2.2.3':
resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==} resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
cpu: [x64, arm64, s390x]
os: [darwin, linux, win32] os: [darwin, linux, win32]
hasBin: true hasBin: true
@@ -3934,6 +3943,9 @@ packages:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
'@types/jsonwebtoken@9.0.6':
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
'@types/katex@0.16.7': '@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
@@ -4609,6 +4621,9 @@ packages:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -5403,6 +5418,9 @@ packages:
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
echarts@5.5.1: echarts@5.5.1:
resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
@@ -6747,6 +6765,16 @@ packages:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -6861,12 +6889,27 @@ packages:
lodash.defaults@4.2.0: lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isarguments@3.1.0: lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6: lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.kebabcase@4.1.1: lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
@@ -6879,6 +6922,9 @@ packages:
lodash.mergewith@4.6.2: lodash.mergewith@4.6.2:
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash.snakecase@4.1.1: lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
@@ -12753,6 +12799,10 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.4.0 '@types/node': 22.4.0
'@types/jsonwebtoken@9.0.6':
dependencies:
'@types/node': 22.4.0
'@types/katex@0.16.7': {} '@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -13601,6 +13651,8 @@ snapshots:
buffer-crc32@1.0.0: {} buffer-crc32@1.0.0: {}
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
buffer@6.0.3: buffer@6.0.3:
@@ -14459,6 +14511,10 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
echarts@5.5.1: echarts@5.5.1:
dependencies: dependencies:
tslib: 2.3.0 tslib: 2.3.0
@@ -16019,6 +16075,30 @@ snapshots:
jsonpointer@5.0.1: {} jsonpointer@5.0.1: {}
jsonwebtoken@9.0.2:
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.6.3
jwa@1.4.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@3.2.2:
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -16159,10 +16239,20 @@ snapshots:
lodash.defaults@4.2.0: {} lodash.defaults@4.2.0: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {} lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {} lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.kebabcase@4.1.1: {} lodash.kebabcase@4.1.1: {}
lodash.memoize@4.1.2: {} lodash.memoize@4.1.2: {}
@@ -16171,6 +16261,8 @@ snapshots:
lodash.mergewith@4.6.2: {} lodash.mergewith@4.6.2: {}
lodash.once@4.1.1: {}
lodash.snakecase@4.1.1: {} lodash.snakecase@4.1.1: {}
lodash.sortby@4.7.0: {} lodash.sortby@4.7.0: {}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/turbo-run", "name": "@vben/turbo-run",
"version": "5.1.0", "version": "5.1.1",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/vsh", "name": "@vben/vsh",
"version": "5.1.0", "version": "5.1.1",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",