From bac1632457916309661a0a65594d46ae30792f76 Mon Sep 17 00:00:00 2001 From: Soybean Date: Tue, 16 Jan 2024 01:50:12 +0800 Subject: [PATCH] feat(projects): @sa/axios: createRequest, createFlatRequest, createHookRequest --- eslint.config.js | 3 +- package.json | 2 +- packages/axios/package.json | 17 ++ packages/axios/src/constant.ts | 5 + packages/axios/src/index.ts | 176 ++++++++++++++++++ packages/axios/src/options.ts | 44 +++++ packages/axios/src/shared.ts | 28 +++ packages/axios/src/type.ts | 97 ++++++++++ packages/{request => axios}/tsconfig.json | 0 packages/hooks/package.json | 3 + packages/hooks/src/use-request.ts | 79 ++++++++ packages/{request => ofetch}/package.json | 3 +- .../src/ofetch => ofetch/src}/index.ts | 4 +- packages/ofetch/tsconfig.json | 20 ++ packages/request/src/axios/index.ts | 10 - packages/request/src/index.ts | 4 - packages/utils/package.json | 3 +- packages/utils/src/index.ts | 1 + packages/utils/src/nanoid.ts | 3 + pnpm-lock.yaml | 52 +++++- src/service/api/auth.ts | 23 ++- src/service/api/route.ts | 4 +- src/service/request/index.ts | 91 +++++++-- src/store/modules/auth/index.ts | 47 +++-- src/store/modules/route/index.ts | 16 +- src/typings/app.d.ts | 12 +- 26 files changed, 672 insertions(+), 75 deletions(-) create mode 100644 packages/axios/package.json create mode 100644 packages/axios/src/constant.ts create mode 100644 packages/axios/src/index.ts create mode 100644 packages/axios/src/options.ts create mode 100644 packages/axios/src/shared.ts create mode 100644 packages/axios/src/type.ts rename packages/{request => axios}/tsconfig.json (100%) create mode 100644 packages/hooks/src/use-request.ts rename packages/{request => ofetch}/package.json (79%) rename packages/{request/src/ofetch => ofetch/src}/index.ts (62%) create mode 100644 packages/ofetch/tsconfig.json delete mode 100644 packages/request/src/axios/index.ts delete mode 100644 packages/request/src/index.ts create mode 100644 packages/utils/src/nanoid.ts diff --git a/eslint.config.js b/eslint.config.js index c2329f667..26fba6857 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,7 +9,8 @@ export default defineConfig( { ignores: ['index', 'App', '[id]'] } - ] + ], + 'no-empty-function': 'off' } } ); diff --git a/package.json b/package.json index 9e1a456cf..870de3ffd 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "dependencies": { "@better-scroll/core": "2.5.1", "@iconify/vue": "4.1.1", + "@sa/axios": "workspace:*", "@sa/color-palette": "workspace:*", "@sa/hooks": "workspace:*", "@sa/materials": "workspace:*", - "@sa/request": "workspace:*", "@sa/utils": "workspace:*", "@vueuse/core": "10.7.2", "clipboard": "2.0.11", diff --git a/packages/axios/package.json b/packages/axios/package.json new file mode 100644 index 000000000..11b1fc70f --- /dev/null +++ b/packages/axios/package.json @@ -0,0 +1,17 @@ +{ + "name": "@sa/axios", + "version": "1.0.0", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "axios": "1.6.5", + "axios-retry": "^4.0.0" + } +} diff --git a/packages/axios/src/constant.ts b/packages/axios/src/constant.ts new file mode 100644 index 000000000..d6c5a3389 --- /dev/null +++ b/packages/axios/src/constant.ts @@ -0,0 +1,5 @@ +/** request id key */ +export const REQUEST_ID_KEY = 'X-Request-Id'; + +/** the backend error code key */ +export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'; diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts new file mode 100644 index 000000000..bda1d8ab0 --- /dev/null +++ b/packages/axios/src/index.ts @@ -0,0 +1,176 @@ +import axios, { AxiosError } from 'axios'; +import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; +import axiosRetry from 'axios-retry'; +import { nanoid } from '@sa/utils'; +import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options'; +import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant'; +import type { + CustomAxiosRequestConfig, + FlatRequestInstance, + MappedType, + RequestInstance, + RequestOption, + ResponseType +} from './type'; + +function createCommonRequest( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const opts = createDefaultOptions(options); + + const axiosConf = createAxiosConfig(axiosConfig); + const instance = axios.create(axiosConf); + + const cancelTokenSourceMap = new Map(); + + // config axios retry + const retryOptions = createRetryOptions(axiosConf); + axiosRetry(instance, retryOptions); + + instance.interceptors.request.use(conf => { + const config: InternalAxiosRequestConfig = { ...conf }; + + // set request id + const requestId = nanoid(); + config.headers.set(REQUEST_ID_KEY, requestId); + + // config cancel token + const cancelTokenSource = axios.CancelToken.source(); + config.cancelToken = cancelTokenSource.token; + cancelTokenSourceMap.set(requestId, cancelTokenSource); + + // handle config by hook + const handledConfig = opts.onRequest?.(config) || config; + + return handledConfig; + }); + + instance.interceptors.response.use( + async response => { + if (opts.isBackendSuccess(response)) { + return Promise.resolve(response); + } + + const fail = await opts.onBackendFail(response, instance); + if (fail) { + return fail; + } + + const backendError = new AxiosError( + 'the backend request error', + BACKEND_ERROR_CODE, + response.config, + response, + response.request + ); + + await opts.onError(backendError); + + return Promise.reject(backendError); + }, + async (error: AxiosError) => { + await opts.onError(error); + + return Promise.reject(error); + } + ); + + function cancelRequest(requestId: string) { + const cancelTokenSource = cancelTokenSourceMap.get(requestId); + if (cancelTokenSource) { + cancelTokenSource.cancel(); + cancelTokenSourceMap.delete(requestId); + } + } + + function cancelAllRequest() { + cancelTokenSourceMap.forEach(cancelTokenSource => { + cancelTokenSource.cancel(); + }); + cancelTokenSourceMap.clear(); + } + + return { + instance, + opts, + cancelRequest, + cancelAllRequest + }; +} + +/** + * create a request instance + * + * @param axiosConfig axios config + * @param options request options + */ +export function createRequest( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); + + const request: RequestInstance = async function request( + config: CustomAxiosRequestConfig + ) { + const response: AxiosResponse = await instance(config); + + const responseType = response.config?.responseType || 'json'; + + if (responseType === 'json') { + return opts.transformBackendResponse(response); + } + + return response.data as MappedType; + } as RequestInstance; + + request.cancelRequest = cancelRequest; + request.cancelAllRequest = cancelAllRequest; + + return request; +} + +/** + * create a flat request instance + * + * The response data is a flat object: { data: any, error: AxiosError } + * + * @param axiosConfig axios config + * @param options request options + */ +export function createFlatRequest( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); + + const flatRequest: FlatRequestInstance = async function flatRequest( + config: CustomAxiosRequestConfig + ) { + try { + const response: AxiosResponse = await instance(config); + + const responseType = response.config?.responseType || 'json'; + + if (responseType === 'json') { + const data = opts.transformBackendResponse(response); + + return { data, error: null }; + } + + return { data: response.data as MappedType, error: null }; + } catch (error) { + return { data: null, error }; + } + } as FlatRequestInstance; + + flatRequest.cancelRequest = cancelRequest; + flatRequest.cancelAllRequest = cancelAllRequest; + + return flatRequest; +} + +export { BACKEND_ERROR_CODE, REQUEST_ID_KEY }; +export type * from './type'; +export type { CreateAxiosDefaults, AxiosError }; diff --git a/packages/axios/src/options.ts b/packages/axios/src/options.ts new file mode 100644 index 000000000..5c62c19be --- /dev/null +++ b/packages/axios/src/options.ts @@ -0,0 +1,44 @@ +import type { CreateAxiosDefaults } from 'axios'; +import type { IAxiosRetryConfig } from 'axios-retry'; +import { isHttpSuccess } from './shared'; +import type { RequestOption } from './type'; + +export function createDefaultOptions(options?: Partial>) { + const opts: RequestOption = { + onRequest: async config => config, + isBackendSuccess: _response => true, + onBackendFail: async () => {}, + transformBackendResponse: async response => response.data, + onError: async () => {} + }; + + Object.assign(opts, options); + + return opts; +} + +export function createRetryOptions(config?: Partial) { + const retryConfig: IAxiosRetryConfig = { + retries: 3 + }; + + Object.assign(retryConfig, config); + + return retryConfig; +} + +export function createAxiosConfig(config?: Partial) { + const TEN_SECONDS = 10 * 1000; + + const axiosConfig: CreateAxiosDefaults = { + timeout: TEN_SECONDS, + headers: { + 'Content-Type': 'application/json' + }, + validateStatus: isHttpSuccess + }; + + Object.assign(axiosConfig, config); + + return axiosConfig; +} diff --git a/packages/axios/src/shared.ts b/packages/axios/src/shared.ts new file mode 100644 index 000000000..7afd32b1e --- /dev/null +++ b/packages/axios/src/shared.ts @@ -0,0 +1,28 @@ +import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +export function getContentType(config: InternalAxiosRequestConfig) { + const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json'; + + return contentType; +} + +/** + * check if http status is success + * + * @param status + */ +export function isHttpSuccess(status: number) { + const isSuccessCode = status >= 200 && status < 300; + return isSuccessCode || status === 304; +} + +/** + * is response json + * + * @param response axios response + */ +export function isResponseJson(response: AxiosResponse) { + const { responseType } = response.config; + + return responseType === 'json' || responseType === undefined; +} diff --git a/packages/axios/src/type.ts b/packages/axios/src/type.ts new file mode 100644 index 000000000..7c617768f --- /dev/null +++ b/packages/axios/src/type.ts @@ -0,0 +1,97 @@ +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +export type ContentType = + | 'text/html' + | 'text/plain' + | 'multipart/form-data' + | 'application/json' + | 'application/x-www-form-urlencoded' + | 'application/octet-stream'; + +export interface RequestOption { + /** + * The hook before request + * + * For example: You can add header token in this hook + * + * @param config Axios config + */ + onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise; + /** + * The hook to check backend response is success or not + * + * @param response Axios response + */ + isBackendSuccess: (response: AxiosResponse) => boolean; + /** + * The hook after backend request fail + * + * For example: You can handle the expired token in this hook + * + * @param response Axios response + * @param instance Axios instance + * @returns + */ + onBackendFail: ( + response: AxiosResponse, + instance: AxiosInstance + ) => Promise | Promise; + /** + * transform backend response when the responseType is json + * + * @param response Axios response + */ + transformBackendResponse(response: AxiosResponse): any | Promise; + /** + * The hook to handle error + * + * For example: You can show error message in this hook + * + * @param error + */ + onError: (error: AxiosError) => void | Promise; +} + +interface ResponseMap { + blob: Blob; + text: string; + arrayBuffer: ArrayBuffer; + stream: ReadableStream; + document: Document; +} +export type ResponseType = keyof ResponseMap | 'json'; + +export type MappedType = R extends keyof ResponseMap + ? ResponseMap[R] + : JsonType; + +export type CustomAxiosRequestConfig = Omit & { + responseType?: R; +}; + +/** The request instance */ +export interface RequestInstance { + (config: CustomAxiosRequestConfig): Promise>; + cancelRequest: (requestId: string) => void; + cancelAllRequest: () => void; +} + +export type FlatResponseSuccessData = { + data: T; + error: null; +}; + +export type FlatResponseFailData = { + data: null; + error: AxiosError; +}; + +export type FlatResponseData = FlatResponseSuccessData | FlatResponseFailData; + +export interface FlatRequestInstance { + ( + config: CustomAxiosRequestConfig + ): Promise>>; + cancelRequest: (requestId: string) => void; + cancelAllRequest: () => void; +} diff --git a/packages/request/tsconfig.json b/packages/axios/tsconfig.json similarity index 100% rename from packages/request/tsconfig.json rename to packages/axios/tsconfig.json diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 6abfa905e..8329fd657 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -8,5 +8,8 @@ "*": { "*": ["./src/*"] } + }, + "dependencies": { + "@sa/axios": "workspace:*" } } diff --git a/packages/hooks/src/use-request.ts b/packages/hooks/src/use-request.ts new file mode 100644 index 000000000..e44e6dd7e --- /dev/null +++ b/packages/hooks/src/use-request.ts @@ -0,0 +1,79 @@ +import { ref } from 'vue'; +import type { Ref } from 'vue'; +import { createFlatRequest } from '@sa/axios'; +import type { + AxiosError, + CreateAxiosDefaults, + CustomAxiosRequestConfig, + MappedType, + RequestOption, + ResponseType +} from '@sa/axios'; +import useLoading from './use-loading'; + +export type HookRequestInstanceResponseSuccessData = { + data: Ref; + error: Ref; +}; + +export type HookRequestInstanceResponseFailData = { + data: Ref; + error: Ref>; +}; + +export type HookRequestInstanceResponseData = { + loading: Ref; +} & (HookRequestInstanceResponseSuccessData | HookRequestInstanceResponseFailData); + +export interface HookRequestInstance { + ( + config: CustomAxiosRequestConfig + ): HookRequestInstanceResponseData>; + cancelRequest: (requestId: string) => void; + cancelAllRequest: () => void; +} + +/** + * create a hook request instance + * + * @param axiosConfig + * @param options + */ +export default function createHookRequest( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const request = createFlatRequest(axiosConfig, options); + + const hookRequest: HookRequestInstance = function hookRequest( + config: CustomAxiosRequestConfig + ) { + const { loading, startLoading, endLoading } = useLoading(); + + const data = ref | null>(null); + const error = ref> | null>(null); + + startLoading(); + + request(config).then(res => { + if (res.data) { + data.value = res.data; + } else { + error.value = res.error; + } + + endLoading(); + }); + + return { + loading, + data, + error + }; + } as HookRequestInstance; + + hookRequest.cancelRequest = request.cancelRequest; + hookRequest.cancelAllRequest = request.cancelAllRequest; + + return hookRequest; +} diff --git a/packages/request/package.json b/packages/ofetch/package.json similarity index 79% rename from packages/request/package.json rename to packages/ofetch/package.json index c25fb98ed..239b41fbf 100644 --- a/packages/request/package.json +++ b/packages/ofetch/package.json @@ -1,5 +1,5 @@ { - "name": "@sa/request", + "name": "@sa/fetch", "version": "1.0.0", "exports": { ".": "./src/index.ts" @@ -10,7 +10,6 @@ } }, "dependencies": { - "axios": "1.6.5", "ofetch": "1.3.3" } } diff --git a/packages/request/src/ofetch/index.ts b/packages/ofetch/src/index.ts similarity index 62% rename from packages/request/src/ofetch/index.ts rename to packages/ofetch/src/index.ts index 9ae37e249..dce1ed44a 100644 --- a/packages/request/src/ofetch/index.ts +++ b/packages/ofetch/src/index.ts @@ -1,10 +1,10 @@ import { ofetch } from 'ofetch'; import type { FetchOptions } from 'ofetch'; -export function createOfetch(options: FetchOptions) { +export function createRequest(options: FetchOptions) { const request = ofetch.create(options); return request; } -export default createOfetch; +export default createRequest; diff --git a/packages/ofetch/tsconfig.json b/packages/ofetch/tsconfig.json new file mode 100644 index 000000000..5823ed54e --- /dev/null +++ b/packages/ofetch/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "ESNext"], + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/request/src/axios/index.ts b/packages/request/src/axios/index.ts deleted file mode 100644 index f50e1e57f..000000000 --- a/packages/request/src/axios/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; -import type { CreateAxiosDefaults } from 'axios'; - -export function createAxios(config?: CreateAxiosDefaults) { - const instance = axios.create(config); - - return instance; -} - -export default createAxios; diff --git a/packages/request/src/index.ts b/packages/request/src/index.ts deleted file mode 100644 index bcec1689c..000000000 --- a/packages/request/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createAxios } from './axios'; -import { createOfetch } from './ofetch'; - -export { createAxios, createOfetch }; diff --git a/packages/utils/package.json b/packages/utils/package.json index 3ad050a10..062739f3c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -12,7 +12,8 @@ "dependencies": { "colord": "2.9.3", "crypto-js": "4.2.0", - "localforage": "1.10.0" + "localforage": "1.10.0", + "nanoid": "5.0.4" }, "devDependencies": { "@types/crypto-js": "4.2.1" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index cb239b128..a12ed34e7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ export * from './color'; export * from './crypto'; export * from './storage'; +export * from './nanoid'; diff --git a/packages/utils/src/nanoid.ts b/packages/utils/src/nanoid.ts new file mode 100644 index 000000000..5cc1d2e45 --- /dev/null +++ b/packages/utils/src/nanoid.ts @@ -0,0 +1,3 @@ +import { nanoid } from 'nanoid'; + +export { nanoid }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 910efd714..58ad122e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@iconify/vue': specifier: 4.1.1 version: 4.1.1(vue@3.4.14) + '@sa/axios': + specifier: workspace:* + version: link:packages/axios '@sa/color-palette': specifier: workspace:* version: link:packages/color-palette @@ -23,9 +26,6 @@ importers: '@sa/materials': specifier: workspace:* version: link:packages/materials - '@sa/request': - specifier: workspace:* - version: link:packages/request '@sa/utils': specifier: workspace:* version: link:packages/utils @@ -160,6 +160,18 @@ importers: specifier: 1.8.27 version: 1.8.27(typescript@5.3.3) + packages/axios: + dependencies: + '@sa/utils': + specifier: workspace:* + version: link:../utils + axios: + specifier: 1.6.5 + version: 1.6.5 + axios-retry: + specifier: ^4.0.0 + version: 4.0.0(axios@1.6.5) + packages/color-palette: dependencies: colord: @@ -172,7 +184,11 @@ importers: specifier: 1.0.0-rc.36 version: 1.0.0-rc.36(@algolia/client-search@4.22.1)(@types/node@20.11.2)(nprogress@0.2.0)(postcss@5.2.18)(sass@1.69.7)(search-insights@2.13.0)(typescript@5.3.3) - packages/hooks: {} + packages/hooks: + dependencies: + '@sa/axios': + specifier: workspace:* + version: link:../axios packages/materials: dependencies: @@ -190,11 +206,8 @@ importers: specifier: 0.8.1 version: 0.8.1 - packages/request: + packages/ofetch: dependencies: - axios: - specifier: 1.6.5 - version: 1.6.5 ofetch: specifier: 1.3.3 version: 1.3.3 @@ -236,6 +249,9 @@ importers: localforage: specifier: 1.10.0 version: 1.10.0 + nanoid: + specifier: 5.0.4 + version: 5.0.4 devDependencies: '@types/crypto-js': specifier: 4.2.1 @@ -2564,6 +2580,15 @@ packages: engines: {node: '>= 0.4'} dev: true + /axios-retry@4.0.0(axios@1.6.5): + resolution: {integrity: sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA==} + peerDependencies: + axios: 0.x || 1.x + dependencies: + axios: 1.6.5 + is-retry-allowed: 2.2.0 + dev: false + /axios@1.6.5: resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} dependencies: @@ -5096,6 +5121,11 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + dev: false + /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -6079,6 +6109,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.4: + resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} diff --git a/src/service/api/auth.ts b/src/service/api/auth.ts index c0692d35c..5e57fb4f0 100644 --- a/src/service/api/auth.ts +++ b/src/service/api/auth.ts @@ -7,9 +7,10 @@ import { request } from '../request'; * @param password Password */ export function fetchLogin(userName: string, password: string) { - return request>('/auth/login', { + return request({ + url: '/auth/login', method: 'post', - body: { + data: { userName, password } @@ -18,7 +19,7 @@ export function fetchLogin(userName: string, password: string) { /** Get user info */ export function fetchGetUserInfo() { - return request>('/auth/getUserInfo'); + return request({ url: '/auth/getUserInfo' }); } /** @@ -27,10 +28,22 @@ export function fetchGetUserInfo() { * @param refreshToken Refresh token */ export function fetchRefreshToken(refreshToken: string) { - return request>('/auth/refreshToken', { + return request({ + url: '/auth/refreshToken', method: 'post', - body: { + data: { refreshToken } }); } + +export function fetchDebug() { + return request({ + url: '/debug-post', + method: 'post', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + data: { + a: '1' + } + }); +} diff --git a/src/service/api/route.ts b/src/service/api/route.ts index 54c347e2e..0f8b5a325 100644 --- a/src/service/api/route.ts +++ b/src/service/api/route.ts @@ -6,7 +6,7 @@ import { request } from '../request'; * @param example Whether to use example data, default: 0 */ export function fetchGetUserRoutes(example: '0' | '1' = '0') { - return request>('/route/getUserRoutes', { params: { example } }); + return request({ url: '/route/getUserRoutes', params: { example } }); } /** @@ -16,5 +16,5 @@ export function fetchGetUserRoutes(example: '0' | '1' = '0') { * @param example Whether to use example data, default: 0 */ export function fetchIsRouteExist(routeName: string, example: '0' | '1' = '0') { - return request>('/route/isRouteExist', { params: { routeName, example } }); + return request({ url: '/route/isRouteExist', params: { routeName, example } }); } diff --git a/src/service/request/index.ts b/src/service/request/index.ts index b9de8b577..7f9552c09 100644 --- a/src/service/request/index.ts +++ b/src/service/request/index.ts @@ -1,4 +1,4 @@ -import { createOfetch as createRequest } from '@sa/request'; +import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios'; import { localStg } from '@/utils/storage'; import { createProxyPattern, createServiceConfig } from '~/env.config'; @@ -6,20 +6,89 @@ const { baseURL, otherBaseURL } = createServiceConfig(import.meta.env); const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'Y'; -export const request = createRequest({ - baseURL: isHttpProxy ? createProxyPattern() : baseURL, - headers: { - apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2' +export const request = createFlatRequest( + { + baseURL: isHttpProxy ? createProxyPattern() : baseURL, + headers: { + apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2' + } }, - onRequest({ options }) { - if (options.headers) { + { + async onRequest(config) { + const { headers } = config; + + // set token const token = localStg.get('token'); + const Authorization = token ? `Bearer ${token}` : null; + Object.assign(headers, { Authorization }); + + return config; + }, + isBackendSuccess(response) { + // when the backend response code is "0000", it means the request is success + // you can change this logic by yourself + return response.data.code === '0000'; + }, + async onBackendFail(_response) { + // when the backend response code is not 200, it means the request is fail + // for example: the token is expired, refetch token and retry request + }, + transformBackendResponse(response) { + return response.data.data; + }, + onError(error) { + // when the request is fail, you can show error message + + let message = error.message; - const Authorization = token ? `Bearer ${token}` : ''; + // show backend error message + if (error.code === BACKEND_ERROR_CODE) { + message = error.request?.data.msg || message; + } - Object.assign(options.headers, { Authorization }); + window.$message?.error(message); } } -}); +); -export const demoRequest = createRequest({ baseURL: isHttpProxy ? createProxyPattern('demo') : otherBaseURL.demo }); +export const demoRequest = createRequest( + { + baseURL: isHttpProxy ? createProxyPattern('demo') : otherBaseURL.demo + }, + { + async onRequest(config) { + const { headers } = config; + + // set token + const token = localStg.get('token'); + const Authorization = token ? `Bearer ${token}` : null; + Object.assign(headers, { Authorization }); + + return config; + }, + isBackendSuccess(response) { + // when the backend response code is 200, it means the request is success + // you can change this logic by yourself + return response.data.status === '200'; + }, + async onBackendFail(_response) { + // when the backend response code is not 200, it means the request is fail + // for example: the token is expired, refetch token and retry request + }, + transformBackendResponse(response) { + return response.data.result; + }, + onError(error) { + // when the request is fail, you can show error message + + let message = error.message; + + // show backend error message + if (error.code === BACKEND_ERROR_CODE) { + message = error.request?.data.message || message; + } + + window.$message?.error(message); + } + } +); diff --git a/src/store/modules/auth/index.ts b/src/store/modules/auth/index.ts index 72d630b44..3f2c686ca 100644 --- a/src/store/modules/auth/index.ts +++ b/src/store/modules/auth/index.ts @@ -45,27 +45,28 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => { async function login(userName: string, password: string) { startLoading(); - try { - const { data: loginToken } = await fetchLogin(userName, password); + const { data: loginToken, error } = await fetchLogin(userName, password); - await loginByToken(loginToken); + if (!error) { + const pass = await loginByToken(loginToken); - await routeStore.initAuthRoute(); + if (pass) { + await routeStore.initAuthRoute(); - await redirectFromLogin(); + await redirectFromLogin(); - if (routeStore.isInitAuthRoute) { - window.$notification?.success({ - title: $t('page.login.common.loginSuccess'), - content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }), - duration: 4500 - }); + if (routeStore.isInitAuthRoute) { + window.$notification?.success({ + title: $t('page.login.common.loginSuccess'), + content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }) + }); + } } - } catch { + } else { resetStore(); - } finally { - endLoading(); } + + endLoading(); } async function loginByToken(loginToken: Api.Auth.LoginToken) { @@ -73,14 +74,20 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => { localStg.set('token', loginToken.token); localStg.set('refreshToken', loginToken.refreshToken); - const { data: info } = await fetchGetUserInfo(); + const { data: info, error } = await fetchGetUserInfo(); + + if (!error) { + // 2. store user info + localStg.set('userInfo', info); - // 2. store user info - localStg.set('userInfo', info); + // 3. update auth route + token.value = loginToken.token; + Object.assign(userInfo, info); + + return true; + } - // 3. update auth route - token.value = loginToken.token; - Object.assign(userInfo, info); + return false; } return { diff --git a/src/store/modules/route/index.ts b/src/store/modules/route/index.ts index 79fd5d3b6..46abf1497 100644 --- a/src/store/modules/route/index.ts +++ b/src/store/modules/route/index.ts @@ -167,17 +167,19 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => { /** Init dynamic auth route */ async function initDynamicAuthRoute() { - const { - data: { routes, home } - } = await fetchGetUserRoutes(); + const { data, error } = await fetchGetUserRoutes(); - handleAuthRoutes(routes); + if (!error) { + const { routes, home } = data; - setRouteHome(home); + handleAuthRoutes(routes); - handleUpdateRootRouteRedirect(home); + setRouteHome(home); - setIsInitAuthRoute(true); + handleUpdateRootRouteRedirect(home); + + setIsInitAuthRoute(true); + } } /** diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index 590aba87e..509e610ef 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -426,9 +426,19 @@ declare namespace App { /** The backend service response code */ code: string; /** The backend service response message */ - message: string; + msg: string; /** The backend service response data */ data: T; }; + + /** The demo backend service response data */ + type DemoResponse = { + /** The backend service response code */ + status: string; + /** The backend service response message */ + message: string; + /** The backend service response data */ + result: T; + }; } }