mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-29 22:56:41 +08:00
feat: optimistic subpackage @sa/alova
This commit is contained in:
parent
24bb6d95cb
commit
6d84ede6ac
@ -3,7 +3,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./client": "./src/client.ts"
|
"./fetch": "./src/fetch.ts",
|
||||||
|
"./client": "./src/client.ts",
|
||||||
|
"./mock": "./src/mock.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@ -11,6 +13,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@alova/mock": "^2.0.7",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"alova": "3.0.20"
|
"alova": "3.0.20"
|
||||||
}
|
}
|
||||||
|
2
packages/alova/src/fetch.ts
Normal file
2
packages/alova/src/fetch.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import adapterFetch from 'alova/fetch';
|
||||||
|
export default adapterFetch;
|
1
packages/alova/src/mock.ts
Normal file
1
packages/alova/src/mock.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@alova/mock';
|
@ -162,6 +162,9 @@ importers:
|
|||||||
|
|
||||||
packages/alova:
|
packages/alova:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@alova/mock':
|
||||||
|
specifier: ^2.0.7
|
||||||
|
version: 2.0.7(alova@3.0.20)
|
||||||
'@sa/utils':
|
'@sa/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
@ -284,9 +287,17 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@alova/mock@2.0.7':
|
||||||
|
resolution: {integrity: sha512-4W8Ncsmj7cdjzZk7f2zFqc32aoYQNoDJS3z7W0nqAkTJ7KR8ZiGaHA5dJovyXnLphmTeyWS3yHMVWnesI7y4ig==}
|
||||||
|
peerDependencies:
|
||||||
|
alova: ^3.0.20
|
||||||
|
|
||||||
'@alova/shared@1.0.5':
|
'@alova/shared@1.0.5':
|
||||||
resolution: {integrity: sha512-/a2Qm+xebQJ1OlIgpslK+UL1J7yhkt1/Mqdq58a22+fSVdANukmUcF4j4w1DF3lxZ04SrqP+2oJprJ8UOvM+9Q==}
|
resolution: {integrity: sha512-/a2Qm+xebQJ1OlIgpslK+UL1J7yhkt1/Mqdq58a22+fSVdANukmUcF4j4w1DF3lxZ04SrqP+2oJprJ8UOvM+9Q==}
|
||||||
|
|
||||||
|
'@alova/shared@1.0.6':
|
||||||
|
resolution: {integrity: sha512-W89j64InjFIsW/u5YmYvpXGWz8JerBAYWyu/Fc7xfc5B+95SSA3ybW4nyHacBUW6yYQyGZwa8S8bVPePqa7bmA==}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -4224,8 +4235,15 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@alova/mock@2.0.7(alova@3.0.20)':
|
||||||
|
dependencies:
|
||||||
|
'@alova/shared': 1.0.6
|
||||||
|
alova: 3.0.20
|
||||||
|
|
||||||
'@alova/shared@1.0.5': {}
|
'@alova/shared@1.0.5': {}
|
||||||
|
|
||||||
|
'@alova/shared@1.0.6': {}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.3.5
|
'@jridgewell/gen-mapping': 0.3.5
|
||||||
|
56
src/serviceAlova/api/auth.ts
Normal file
56
src/serviceAlova/api/auth.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { alova } from '../request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login
|
||||||
|
*
|
||||||
|
* @param userName User name
|
||||||
|
* @param password Password
|
||||||
|
*/
|
||||||
|
export function fetchLogin(userName: string, password: string) {
|
||||||
|
return alova.Post<Api.Auth.LoginToken>('/auth/login', { userName, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get user info */
|
||||||
|
export function fetchGetUserInfo() {
|
||||||
|
return alova.Get<Api.Auth.UserInfo>('/auth/getUserInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send captcha to target phone */
|
||||||
|
export function sendCaptcha(phone: string) {
|
||||||
|
return alova.Post<null>('/auth/sendCaptcha', { phone });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify captcha */
|
||||||
|
export function verifyCaptcha(phone: string, code: string) {
|
||||||
|
return alova.Post<null>('/auth/verifyCaptcha', { phone, code });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh token
|
||||||
|
*
|
||||||
|
* @param refreshToken Refresh token
|
||||||
|
*/
|
||||||
|
export function fetchRefreshToken(refreshToken: string) {
|
||||||
|
return alova.Post<Api.Auth.LoginToken>(
|
||||||
|
'/auth/refreshToken',
|
||||||
|
{ refreshToken },
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
authRole: 'refreshToken'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return custom backend error
|
||||||
|
*
|
||||||
|
* @param code error code
|
||||||
|
* @param msg error message
|
||||||
|
*/
|
||||||
|
export function fetchCustomBackendError(code: string, msg: string) {
|
||||||
|
return alova.Get('/auth/error', {
|
||||||
|
params: { code, msg },
|
||||||
|
shareRequest: false
|
||||||
|
});
|
||||||
|
}
|
2
src/serviceAlova/api/index.ts
Normal file
2
src/serviceAlova/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './route';
|
20
src/serviceAlova/api/route.ts
Normal file
20
src/serviceAlova/api/route.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { alova } from '../request';
|
||||||
|
|
||||||
|
/** get constant routes */
|
||||||
|
export function fetchGetConstantRoutes() {
|
||||||
|
return alova.Get<Api.Route.MenuRoute[]>('/route/getConstantRoutes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** get user routes */
|
||||||
|
export function fetchGetUserRoutes() {
|
||||||
|
return alova.Get<Api.Route.UserRoute>('/route/getUserRoutes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the route is exist
|
||||||
|
*
|
||||||
|
* @param routeName route name
|
||||||
|
*/
|
||||||
|
export function fetchIsRouteExist(routeName: string) {
|
||||||
|
return alova.Get<boolean>('/route/isRouteExist', { params: { routeName } });
|
||||||
|
}
|
56
src/serviceAlova/mocks/feature-users-20241014.ts
Normal file
56
src/serviceAlova/mocks/feature-users-20241014.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { defineMock } from '@sa/alova/mock';
|
||||||
|
|
||||||
|
// you can separate the mock data into multiple files dependent on your project versions
|
||||||
|
export default defineMock({
|
||||||
|
'[POST]/systemManage/addUser': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'[POST]/systemManage/updateUser': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'[DELETE]/systemManage/deleteUser': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'[DELETE]/systemManage/batchDeleteUser': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'[POST]/auth/sendCaptcha': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'[POST]/auth/verifyCaptcha': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'/mock/getLastTime': () => {
|
||||||
|
return {
|
||||||
|
code: '0000',
|
||||||
|
msg: 'success',
|
||||||
|
data: {
|
||||||
|
time: new Date().toLocaleTimeString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
115
src/serviceAlova/request/index.ts
Normal file
115
src/serviceAlova/request/index.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { createAlovaRequest } from '@sa/alova';
|
||||||
|
import { createAlovaMockAdapter } from '@sa/alova/mock';
|
||||||
|
import adapterFetch from '@sa/alova/fetch';
|
||||||
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import { getServiceBaseURL } from '@/utils/service';
|
||||||
|
import featureUsers20241014 from '../mocks/feature-users-20241014';
|
||||||
|
import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
|
||||||
|
import type { RequestInstanceState } from './type';
|
||||||
|
|
||||||
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
|
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
|
||||||
|
const state: RequestInstanceState = {
|
||||||
|
errMsgStack: []
|
||||||
|
};
|
||||||
|
const mockAdapter = createAlovaMockAdapter([featureUsers20241014], {
|
||||||
|
// using requestAdapter if not match mock request
|
||||||
|
httpAdapter: adapterFetch(),
|
||||||
|
|
||||||
|
// response delay time
|
||||||
|
delay: 1000,
|
||||||
|
|
||||||
|
// global mock toggle
|
||||||
|
enable: true,
|
||||||
|
matchMode: 'methodurl'
|
||||||
|
});
|
||||||
|
export const alova = createAlovaRequest(
|
||||||
|
{
|
||||||
|
baseURL,
|
||||||
|
requestAdapter: import.meta.env.DEV ? mockAdapter : adapterFetch()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest({ config }) {
|
||||||
|
const Authorization = getAuthorization();
|
||||||
|
config.headers.Authorization = Authorization;
|
||||||
|
config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2';
|
||||||
|
},
|
||||||
|
tokenRefresher: {
|
||||||
|
async isExpired(response) {
|
||||||
|
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||||
|
const { code } = await response.clone().json();
|
||||||
|
return expiredTokenCodes.includes(String(code));
|
||||||
|
},
|
||||||
|
async handler() {
|
||||||
|
await handleRefreshToken();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async isBackendSuccess(response) {
|
||||||
|
// when the backend response code is "0000"(default), it means the request is success
|
||||||
|
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
|
||||||
|
const resp = response.clone();
|
||||||
|
const data = await resp.json();
|
||||||
|
return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
|
||||||
|
},
|
||||||
|
async transformBackendResponse(response) {
|
||||||
|
return (await response.clone().json()).data;
|
||||||
|
},
|
||||||
|
async onError(error, response) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
let message = error.message;
|
||||||
|
let responseCode = '';
|
||||||
|
if (response) {
|
||||||
|
const data = await response?.clone().json();
|
||||||
|
message = data.msg;
|
||||||
|
responseCode = String(data.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
showErrorMsg(state, message);
|
||||||
|
authStore.resetStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutAndCleanup() {
|
||||||
|
handleLogout();
|
||||||
|
window.removeEventListener('beforeunload', handleLogout);
|
||||||
|
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||||
|
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||||
|
if (logoutCodes.includes(responseCode)) {
|
||||||
|
handleLogout();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||||
|
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||||
|
if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) {
|
||||||
|
state.errMsgStack = [...(state.errMsgStack || []), message];
|
||||||
|
|
||||||
|
// prevent the user from refreshing the page
|
||||||
|
window.addEventListener('beforeunload', handleLogout);
|
||||||
|
|
||||||
|
window.$dialog?.error({
|
||||||
|
title: $t('common.error'),
|
||||||
|
content: message,
|
||||||
|
positiveText: $t('common.confirm'),
|
||||||
|
maskClosable: false,
|
||||||
|
closeOnEsc: false,
|
||||||
|
onPositiveClick() {
|
||||||
|
logoutAndCleanup();
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
logoutAndCleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
showErrorMsg(state, message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
53
src/serviceAlova/request/shared.ts
Normal file
53
src/serviceAlova/request/shared.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
|
import { localStg } from '@/utils/storage';
|
||||||
|
import { fetchRefreshToken } from '../api';
|
||||||
|
import type { RequestInstanceState } from './type';
|
||||||
|
|
||||||
|
export function getAuthorization() {
|
||||||
|
const token = localStg.get('token');
|
||||||
|
const Authorization = token ? `Bearer ${token}` : null;
|
||||||
|
|
||||||
|
return Authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** refresh token */
|
||||||
|
export async function handleRefreshToken() {
|
||||||
|
const { resetStore } = useAuthStore();
|
||||||
|
|
||||||
|
const rToken = localStg.get('refreshToken') || '';
|
||||||
|
const refreshTokenMethod = fetchRefreshToken(rToken);
|
||||||
|
|
||||||
|
// set the refreshToken role, so that the request will not be intercepted
|
||||||
|
refreshTokenMethod.meta.authRole = 'refreshToken';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await refreshTokenMethod;
|
||||||
|
localStg.set('token', data.token);
|
||||||
|
localStg.set('refreshToken', data.refreshToken);
|
||||||
|
} catch (error) {
|
||||||
|
resetStore();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
||||||
|
if (!state.errMsgStack?.length) {
|
||||||
|
state.errMsgStack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExist = state.errMsgStack.includes(message);
|
||||||
|
|
||||||
|
if (!isExist) {
|
||||||
|
state.errMsgStack.push(message);
|
||||||
|
|
||||||
|
window.$message?.error(message, {
|
||||||
|
onLeave: () => {
|
||||||
|
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
state.errMsgStack = [];
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
4
src/serviceAlova/request/type.ts
Normal file
4
src/serviceAlova/request/type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface RequestInstanceState {
|
||||||
|
/** the request error message stack */
|
||||||
|
errMsgStack: string[];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user