434 lines
17 KiB
TypeScript
434 lines
17 KiB
TypeScript
import type { AxiosRequestConfig, Method } from 'axios'
|
||
import axios from 'axios'
|
||
import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus'
|
||
import { nextTick } from 'vue'
|
||
import { refreshToken } from '/@/api/common'
|
||
import { i18n } from '/@/lang/index'
|
||
import router from '/@/router/index'
|
||
import adminBaseRoute from '/@/router/static/adminBase'
|
||
import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
|
||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||
import { useConfig } from '/@/stores/config'
|
||
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
|
||
import { useUserInfo } from '/@/stores/userInfo'
|
||
import { isAdminApp } from '/@/utils/common'
|
||
|
||
window.requests = []
|
||
window.tokenRefreshing = false
|
||
const pendingMap = new Map()
|
||
const loadingInstance: LoadingInstance = {
|
||
target: null,
|
||
count: 0,
|
||
}
|
||
|
||
/** 请求是否后台 /admin/ 接口(不依赖当前路由,避免 loading 等场景误判为前台) */
|
||
function isAdminBackendRequest(config: AxiosRequestConfig): boolean {
|
||
const u = `${config.baseURL ?? ''}${config.url ?? ''}`
|
||
return /\/admin\//i.test(u)
|
||
}
|
||
|
||
/**
|
||
* 根据运行环境获取基础请求URL
|
||
*/
|
||
export const getUrl = (): string => {
|
||
const value: string = import.meta.env.VITE_AXIOS_BASE_URL as string
|
||
return value == 'getCurrentDomain' ? window.location.protocol + '//' + window.location.host : value
|
||
}
|
||
|
||
/**
|
||
* Webman Push 在 API 上的 HTTP 根地址(私有频道 POST /plugin/webman/push/auth 等)。
|
||
* 与 VITE_AXIOS_BASE_URL 一致;开发环境为空时用当前页面源,配合 Vite 对 /plugin 的代理。
|
||
*/
|
||
export function getPushHttpBase(): string {
|
||
const api = getUrl()
|
||
if (api) {
|
||
return api.replace(/\/$/, '')
|
||
}
|
||
return window.location.protocol + '//' + window.location.host
|
||
}
|
||
|
||
/**
|
||
* 加载 push.js 的绝对 URL:文件放在 web/public/plugin/webman/push/,随 dist 与后台同源发布,
|
||
* 不依赖 API 站点是否暴露 /plugin/webman/push/push.js。
|
||
*/
|
||
export function getPushScriptUrl(): string {
|
||
let base = import.meta.env.BASE_URL
|
||
if (typeof base !== 'string' || base === '') {
|
||
base = '/'
|
||
}
|
||
if (!base.endsWith('/')) {
|
||
base = base + '/'
|
||
}
|
||
return window.location.origin + base + 'plugin/webman/push/push.js'
|
||
}
|
||
|
||
/**
|
||
* 根据运行环境获取基础请求URL的端口
|
||
*/
|
||
export const getUrlPort = (): string => {
|
||
const url = getUrl()
|
||
return new URL(url).port
|
||
}
|
||
|
||
/**
|
||
* 创建`Axios`
|
||
* 默认开启`reductDataFormat(简洁响应)`,返回类型为`ApiPromise`
|
||
* 关闭`reductDataFormat`,返回类型则为`AxiosPromise`
|
||
*/
|
||
function createAxios<Data = any, T = ApiPromise<Data>>(axiosConfig: AxiosRequestConfig, options: Options = {}, loading: LoadingOptions = {}): T {
|
||
const config = useConfig()
|
||
const adminInfo = useAdminInfo()
|
||
const userInfo = useUserInfo()
|
||
|
||
const Axios = axios.create({
|
||
baseURL: getUrl(),
|
||
timeout: 1000 * 10,
|
||
headers: {
|
||
'think-lang': config.lang.defaultLang,
|
||
server: true,
|
||
},
|
||
responseType: 'json',
|
||
})
|
||
|
||
// 自定义后台入口
|
||
if (adminBaseRoute.path != '/admin' && isAdminApp() && /^\/admin\//.test(axiosConfig.url!)) {
|
||
axiosConfig.url = axiosConfig.url!.replace(/^\/admin\//, adminBaseRoute.path + '.php/')
|
||
}
|
||
|
||
// 合并默认请求选项
|
||
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
|
||
)
|
||
|
||
// 请求拦截
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 自动携带token和语言(确保每次请求使用当前语言,后端据此返回对应语言的 remark 等)
|
||
if (config.headers) {
|
||
const token = adminInfo.getToken()
|
||
if (token) (config.headers as anyObj).batoken = token
|
||
const userToken = options.anotherToken || userInfo.getToken()
|
||
if (userToken) (config.headers as anyObj)['ba-user-token'] = userToken
|
||
;(config.headers as anyObj)['think-lang'] = useConfig().lang.defaultLang
|
||
}
|
||
|
||
return config
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// 响应拦截
|
||
Axios.interceptors.response.use(
|
||
(response) => {
|
||
removePending(response.config)
|
||
options.loading && closeLoading(options) // 关闭loading
|
||
|
||
if (response.config.responseType == 'json') {
|
||
if (response.data && response.data.code !== 1) {
|
||
const needLogin = response.data.data && typeof response.data.data === 'object' && response.data.data.type === 'need login'
|
||
if (needLogin) {
|
||
const isAdminAppFlag = isAdminApp() || isAdminBackendRequest(response.config)
|
||
if (isAdminAppFlag) {
|
||
adminInfo.removeToken()
|
||
} else {
|
||
userInfo.removeToken()
|
||
}
|
||
const loginRouteName = isAdminAppFlag ? 'adminLogin' : 'userLogin'
|
||
if (router.currentRoute.value.name !== loginRouteName) {
|
||
nextTick(() => {
|
||
void router.replace({ name: loginRouteName })
|
||
})
|
||
}
|
||
return Promise.reject(response.data)
|
||
}
|
||
if (response.data.code == 409) {
|
||
if (!window.tokenRefreshing) {
|
||
window.tokenRefreshing = true
|
||
return refreshToken()
|
||
.then((res) => {
|
||
if (res.data.type == 'admin-refresh') {
|
||
adminInfo.setToken(res.data.token, 'auth')
|
||
response.headers.batoken = `${res.data.token}`
|
||
window.requests.forEach((cb) => cb(res.data.token, 'admin-refresh'))
|
||
} else if (res.data.type == 'user-refresh') {
|
||
userInfo.setToken(res.data.token, 'auth')
|
||
response.headers['ba-user-token'] = `${res.data.token}`
|
||
window.requests.forEach((cb) => cb(res.data.token, 'user-refresh'))
|
||
}
|
||
window.requests = []
|
||
return Axios(response.config)
|
||
})
|
||
.catch((err) => {
|
||
if (isAdminApp()) {
|
||
adminInfo.removeToken()
|
||
if (router.currentRoute.value.name != 'adminLogin') {
|
||
router.push({ name: 'adminLogin' })
|
||
return Promise.reject(err)
|
||
} else {
|
||
response.headers.batoken = ''
|
||
window.requests.forEach((cb) => cb('', 'admin-refresh'))
|
||
window.requests = []
|
||
return Axios(response.config)
|
||
}
|
||
} else {
|
||
userInfo.removeToken()
|
||
if (router.currentRoute.value.name != 'userLogin') {
|
||
router.push({ name: 'userLogin' })
|
||
return Promise.reject(err)
|
||
} else {
|
||
response.headers['ba-user-token'] = ''
|
||
window.requests.forEach((cb) => cb('', 'user-refresh'))
|
||
window.requests = []
|
||
return Axios(response.config)
|
||
}
|
||
}
|
||
})
|
||
.finally(() => {
|
||
window.tokenRefreshing = false
|
||
})
|
||
} else {
|
||
return new Promise((resolve) => {
|
||
// 用函数形式将 resolve 存入,等待刷新后再执行
|
||
window.requests.push((token: string, type: string) => {
|
||
if (type == 'admin-refresh') {
|
||
response.headers.batoken = `${token}`
|
||
} else {
|
||
response.headers['ba-user-token'] = `${token}`
|
||
}
|
||
resolve(Axios(response.config))
|
||
})
|
||
})
|
||
}
|
||
}
|
||
if (options.showCodeMessage) {
|
||
ElNotification({
|
||
type: 'error',
|
||
message: response.data.msg,
|
||
zIndex: SYSTEM_ZINDEX,
|
||
})
|
||
}
|
||
// 自动跳转到路由name或path
|
||
if (response.data.code == 302) {
|
||
router.push({ path: response.data.data.routePath ?? '', name: response.data.data.routeName ?? '' })
|
||
}
|
||
if (response.data.code == 303) {
|
||
const isAdminAppFlag = isAdminApp()
|
||
let routerPath = isAdminAppFlag ? adminBaseRoute.path : memberCenterBaseRoutePath
|
||
|
||
// 需要登录,清理 token,转到登录页
|
||
if (response.data.data.type == 'need login') {
|
||
if (isAdminAppFlag) {
|
||
adminInfo.removeToken()
|
||
} else {
|
||
userInfo.removeToken()
|
||
}
|
||
routerPath += '/login'
|
||
}
|
||
router.push({ path: routerPath })
|
||
}
|
||
// code不等于1, 页面then内的具体逻辑就不执行了
|
||
return Promise.reject(response.data)
|
||
} else if (options.showSuccessMessage && response.data && response.data.code == 1) {
|
||
ElNotification({
|
||
message: response.data.msg ? response.data.msg : i18n.global.t('axios.Operation successful'),
|
||
type: 'success',
|
||
zIndex: SYSTEM_ZINDEX,
|
||
})
|
||
}
|
||
}
|
||
|
||
return options.reductDataFormat ? response.data : response
|
||
},
|
||
(error) => {
|
||
error.config && removePending(error.config)
|
||
options.loading && closeLoading(options) // 关闭loading
|
||
options.showErrorMessage && httpErrorStatusHandle(error) // 处理错误状态码
|
||
return Promise.reject(error) // 错误继续返回给到具体页面
|
||
}
|
||
)
|
||
return Axios(axiosConfig) as T
|
||
}
|
||
|
||
export default createAxios
|
||
|
||
/**
|
||
* 处理异常
|
||
* @param {*} error
|
||
*/
|
||
function httpErrorStatusHandle(error: any) {
|
||
// 处理被取消的请求
|
||
if (axios.isCancel(error)) return console.error(i18n.global.t('axios.Automatic cancellation due to duplicate request:') + error.message)
|
||
let message = ''
|
||
if (error && error.response) {
|
||
switch (error.response.status) {
|
||
case 302:
|
||
message = i18n.global.t('axios.Interface redirected!')
|
||
break
|
||
case 400:
|
||
message = i18n.global.t('axios.Incorrect parameter!')
|
||
break
|
||
case 401:
|
||
message = i18n.global.t('axios.You do not have permission to operate!')
|
||
break
|
||
case 403:
|
||
message = i18n.global.t('axios.You do not have permission to operate!')
|
||
break
|
||
case 404:
|
||
message = i18n.global.t('axios.Error requesting address:') + error.response.config.url
|
||
break
|
||
case 408:
|
||
message = i18n.global.t('axios.Request timed out!')
|
||
break
|
||
case 409:
|
||
message = i18n.global.t('axios.The same data already exists in the system!')
|
||
break
|
||
case 500:
|
||
message = i18n.global.t('axios.Server internal error!')
|
||
break
|
||
case 501:
|
||
message = i18n.global.t('axios.Service not implemented!')
|
||
break
|
||
case 502:
|
||
message = i18n.global.t('axios.Gateway error!')
|
||
break
|
||
case 503:
|
||
message = i18n.global.t('axios.Service unavailable!')
|
||
break
|
||
case 504:
|
||
message = i18n.global.t('axios.The service is temporarily unavailable Please try again later!')
|
||
break
|
||
case 505:
|
||
message = i18n.global.t('axios.HTTP version is not supported!')
|
||
break
|
||
default:
|
||
message = i18n.global.t('axios.Abnormal problem, please contact the website administrator!')
|
||
break
|
||
}
|
||
}
|
||
if (error.message.includes('timeout')) message = i18n.global.t('axios.Network request timeout!')
|
||
if (error.message.includes('Network'))
|
||
message = window.navigator.onLine ? i18n.global.t('axios.Server exception!') : i18n.global.t('axios.You are disconnected!')
|
||
|
||
ElNotification({
|
||
type: 'error',
|
||
message,
|
||
zIndex: SYSTEM_ZINDEX,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 关闭Loading层实例
|
||
*/
|
||
function closeLoading(options: Options) {
|
||
if (options.loading && loadingInstance.count > 0) loadingInstance.count--
|
||
if (loadingInstance.count === 0) {
|
||
loadingInstance.target.close()
|
||
loadingInstance.target = null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 储存每个请求的唯一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).batoken ? (headers as anyObj).batoken : '',
|
||
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
|
||
// 是否开启接口错误信息展示,默认为true
|
||
showErrorMessage?: boolean
|
||
// 是否开启code不为1时的信息提示, 默认为true
|
||
showCodeMessage?: boolean
|
||
// 是否开启code为1时的信息提示, 默认为false
|
||
showSuccessMessage?: boolean
|
||
// 当前请求使用另外的用户token
|
||
anotherToken?: string
|
||
}
|
||
|
||
/*
|
||
* 感谢掘金@橙某人提供的思路和分享
|
||
* 本axios封装详细解释请参考:https://juejin.cn/post/6968630178163458084?share_token=7831c9e0-bea0-469e-8028-b587e13681a8#heading-27
|
||
*/
|