mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-02 04:13:24 +08:00
Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
PORT=5320
|
PORT=5320
|
||||||
|
ACCESS_TOKEN_SECRET=access_token_secret
|
||||||
|
REFRESH_TOKEN_SECRET=refresh_token_secret
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
15
apps/backend-mock/api/auth/logout.post.ts
Normal file
15
apps/backend-mock/api/auth/logout.post.ts
Normal 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('');
|
||||||
|
});
|
||||||
33
apps/backend-mock/api/auth/refresh.post.ts
Normal file
33
apps/backend-mock/api/auth/refresh.post.ts
Normal 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;
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.';
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
apps/backend-mock/utils/cookie-utils.ts
Normal file
26
apps/backend-mock/utils/cookie-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
59
apps/backend-mock/utils/jwt-utils.ts
Normal file
59
apps/backend-mock/utils/jwt-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 (
|
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||||
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;
|
|
||||||
}
|
|
||||||
// 不进行任何处理,直接返回
|
|
||||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
|
||||||
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 });
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户权限码
|
* 获取用户权限码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户权限码
|
* 获取用户权限码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
/** 布局方式 */
|
/** 布局方式 */
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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> */
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
21
packages/@core/base/shared/src/utils/to.ts
Normal file
21
packages/@core/base/shared/src/utils/to.ts
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ interface AppPreferences {
|
|||||||
enableCheckUpdates: boolean;
|
enableCheckUpdates: boolean;
|
||||||
/** 是否显示偏好设置 */
|
/** 是否显示偏好设置 */
|
||||||
enablePreferences: boolean;
|
enablePreferences: boolean;
|
||||||
|
/**
|
||||||
|
* @zh_CN 是否开启refreshToken
|
||||||
|
*/
|
||||||
|
enableRefreshToken: boolean;
|
||||||
/** 是否移动端 */
|
/** 是否移动端 */
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
/** 布局方式 */
|
/** 布局方式 */
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
MenuItemRegistered,
|
MenuItemRegistered,
|
||||||
MenuProps,
|
MenuProps,
|
||||||
MenuProvider,
|
MenuProvider,
|
||||||
} from '../interface';
|
} from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubMenuProvider } from '../interface';
|
import type { SubMenuProvider } from '../types';
|
||||||
|
|
||||||
import { computed, getCurrentInstance } from 'vue';
|
import { computed, getCurrentInstance } from 'vue';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './preset-interceptors';
|
||||||
export * from './request-client';
|
export * from './request-client';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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请求方法
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户权限码
|
* 获取用户权限码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
94
pnpm-lock.yaml
generated
@@ -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: {}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user