diff --git a/src/api/types.ts b/src/api/types.ts index 38e22f1..dd02f32 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,8 +1,9 @@ // 登录 export interface LoginData { - username: String - password: String - imageCode: String - grant_type: String - verifyCode: number + username: string + password?: string + imageCode?: string + grant_type: string + verifyCode?: number + refresh_token?: string } diff --git a/src/api/user.ts b/src/api/user.ts index 6f9e1c4..a6480c9 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,19 +1,31 @@ -import request from '@/utils/request' -import { LoginData } from './types.ts' +import createAxios from '@/utils/request' +import { useAdminInfo } from '@/stores/adminInfo' + +import { LoginData } from './types' // 获取公钥 export function gongkey(params: any) { - return request.request({ + return createAxios({ url: '/user-boot/user/generateSm2Key', method: 'get', params }) } //登录获取token -export function login(params:LoginData) { - return request.request({ +export function login(params: LoginData) { + return createAxios({ url: '/pqs-auth/oauth/token', method: 'post', params }) } + +// 刷新token +export function refreshToken(): Promise { + const adminInfo = useAdminInfo() + return login({ + grant_type: 'refresh_token', + refresh_token: adminInfo.refresh_token, + username: adminInfo.username + }) +} diff --git a/src/utils/request.ts b/src/utils/request.ts index cef491d..a1d3545 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,62 +1,229 @@ -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' +import type { AxiosRequestConfig, Method } from 'axios' +import axios from 'axios' +import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus' +import { refreshToken } from '@/api/user' +import router from '@/router/index' +import { useAdminInfo } from '@/stores/adminInfo' -class HttpRequest { - getInsideConfig() { - const config = { - baseURL: '/api', // 所有的请求地址前缀部分(没有后端请求不用写) - timeout: 80000, // 请求超时时间(毫秒) - - // headers: { - // 设置后端需要的传参类型 - // 'Content-Type': 'application/json', - // 'token': x-auth-token',//一开始就要token - // 'X-Requested-With': 'XMLHttpRequest', - // }, - } - return config - } +window.requests = [] +window.tokenRefreshing = false +const pendingMap = new Map() +const loadingInstance: LoadingInstance = { + target: null, + count: 0 +} + +/** + * 根据运行环境获取基础请求URL + */ +export const getUrl = (): string => { + if (import.meta.env.MODE == 'development') return '/api' + return window.location.protocol + '//' + window.location.host +} + +/** + * 创建`Axios` + * 默认开启`reductDataFormat(简洁响应)`,返回类型为`ApiPromise` + * 关闭`reductDataFormat`,返回类型则为`AxiosPromise` + */ +function createAxios>( + axiosConfig: AxiosRequestConfig, + options: Options = {}, + loading: LoadingOptions = {} +): T { + const adminInfo = useAdminInfo() + + const Axios = axios.create({ + baseURL: getUrl(), + timeout: 1000 * 10, + headers: {}, + responseType: 'json' + }) + + options = Object.assign( + { + CancelDuplicateRequest: true, // 是否开启取消重复请求, 默认为 true + loading: false, // 是否开启loading层效果, 默认为false + reductDataFormat: true, // 是否开启简洁的数据结构响应, 默认为true + showErrorMessage: true, // 是否开启接口错误信息展示,默认为true + showCodeMessage: true, // 是否开启code不为1时的信息提示, 默认为true + showSuccessMessage: false, // 是否开启code为1时的信息提示, 默认为false + anotherToken: '' // 当前请求使用另外的用户token + }, + options + ) // 请求拦截 - interceptors(instance: AxiosInstance, url: string | number | undefined) { - instance.interceptors.request.use( - config => { - const token: string | undefined = '' //getToken() //此处换成自己获取回来的token,通常存在在cookie或者store里面 - // 请求头携带token - if (token) { - config.headers['Authorization'] = token - config.headers.Authorization = +token - } else { - config.headers['Authorization'] = 'Basic bmpjbnRlc3Q6bmpjbnBxcw==' + Axios.interceptors.request.use( + config => { + removePending(config) + options.CancelDuplicateRequest && addPending(config) + // 创建loading实例 + if (options.loading) { + loadingInstance.count++ + if (loadingInstance.count === 1) { + loadingInstance.target = ElLoading.service(loading) } - - return config - }, - (error: any) => { - return Promise.reject(error) } - ) - //响应拦截 - instance.interceptors.response.use( - res => { - //返回数据 - const { data } = res - console.log('返回数据处理', data) - return data - }, - (error: any) => { - console.log('error==>', error) - return Promise.reject(error) + // 自动携带token + if (config.headers) { + const token = adminInfo.getToken() + if (token) (config.headers as anyObj).Authorization = token } - ) - } - request(options: AxiosRequestConfig) { - const instance = axios.create() - options = Object.assign(this.getInsideConfig(), options) - this.interceptors(instance, options.url) - return instance(options) + return config + }, + error => { + return Promise.reject(error) + } + ) + + // 响应拦截 + Axios.interceptors.response.use( + response => { + removePending(response.config) + options.loading && closeLoading(options) // 关闭loading + if (response.data.code === 'A0000') { + return options.reductDataFormat ? response.data : response + } else if (response.data.code == 'A0202') { + if (!window.tokenRefreshing) { + window.tokenRefreshing = true + return refreshToken() + .then(res => { + adminInfo.setToken(res.data.token, 'auth') + response.headers.Authorization = `${res.data.token}` + window.requests.forEach(cb => cb(res.data.token)) + window.requests = [] + return Axios(response.config) + }) + .catch(err => { + adminInfo.removeToken() + router.push({ name: 'login' }) + return Promise.reject(err) + }) + .finally(() => { + window.tokenRefreshing = false + }) + } else { + return new Promise(resolve => { + // 用函数形式将 resolve 存入,等待刷新后再执行 + window.requests.push((token: string) => { + response.headers.Authorization = `${token}` + resolve(Axios(response.config)) + }) + }) + } + } else if (response.data.code == 'A0024') { + // 登录失效 + ElNotification({ + type: 'error', + message: response.data.msg + }) + router.push({ name: 'login' }) + } else { + if (options.showCodeMessage) { + ElNotification({ + type: 'error', + message: response.data.message || '未知错误' + }) + } + } + }, + error => { + error.config && removePending(error.config) + options.loading && closeLoading(options) // 关闭loading + return Promise.reject(error) // 错误继续返回给到具体页面 + } + ) + return Axios(axiosConfig) as T +} + +export default createAxios + +/** + * 关闭Loading层实例 + */ +function closeLoading(options: Options) { + if (options.loading && loadingInstance.count > 0) loadingInstance.count-- + if (loadingInstance.count === 0) { + loadingInstance.target.close() + loadingInstance.target = null } } -const request = new HttpRequest() -export default request +/** + * 储存每个请求的唯一cancel回调, 以此为标识 + */ +function addPending(config: AxiosRequestConfig) { + const pendingKey = getPendingKey(config) + config.cancelToken = + config.cancelToken || + new axios.CancelToken(cancel => { + if (!pendingMap.has(pendingKey)) { + pendingMap.set(pendingKey, cancel) + } + }) +} + +/** + * 删除重复的请求 + */ +function removePending(config: AxiosRequestConfig) { + const pendingKey = getPendingKey(config) + if (pendingMap.has(pendingKey)) { + const cancelToken = pendingMap.get(pendingKey) + cancelToken(pendingKey) + pendingMap.delete(pendingKey) + } +} + +/** + * 生成每个请求的唯一key + */ +function getPendingKey(config: AxiosRequestConfig) { + let { data } = config + const { url, method, params, headers } = config + if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 + return [ + url, + method, + headers && (headers as anyObj).Authorization ? (headers as anyObj).Authorization : '', + headers && (headers as anyObj)['ba-user-token'] ? (headers as anyObj)['ba-user-token'] : '', + JSON.stringify(params), + JSON.stringify(data) + ].join('&') +} + +/** + * 根据请求方法组装请求数据/参数 + */ +export function requestPayload(method: Method, data: anyObj) { + if (method == 'GET') { + return { + params: data + } + } else if (method == 'POST') { + return { + data: data + } + } +} + +interface LoadingInstance { + target: any + count: number +} +interface Options { + // 是否开启取消重复请求, 默认为 true + CancelDuplicateRequest?: boolean + // 是否开启loading层效果, 默认为false + loading?: boolean + // 是否开启简洁的数据结构响应, 默认为true + reductDataFormat?: boolean + // 是否开启code不为A0000时的信息提示, 默认为true + showCodeMessage?: boolean + // 是否开启code为0时的信息提示, 默认为false + showSuccessMessage?: boolean + // 当前请求使用另外的用户token + anotherToken?: string +} diff --git a/vite.config.ts b/vite.config.ts index bd67627..ac78cf0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,19 +10,13 @@ export default defineConfig({ '/api': { // target: "http://192.168.1.115:10215", //黄正剑 // target: "http://192.168.1.22:10215", //超高压 - target: "http://192.168.1.9:10215", //数据中心 + target: 'http://192.168.1.9:10215', //数据中心 // target: "http://192.168.1.13:10215", //治理 // target: 'http://192.168.1.18:10215', // 河北 // target: "http://192.168.1.31:10215", // 海南 // target: "http://192.168.1.29:10215", // 冀北 changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), //路径重写,把'/api'替换为'' - }, - '/api1': { - //target: "http://192.168.1.65:7300/mock/6384a6175854f20022dc9300/jibei", - target: 'http://192.168.1.9:8088', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api1/, ''), //路径重写,把'/api'替换为'' + rewrite: path => path.replace(/^\/api/, '') //路径重写,把'/api'替换为'' } } },