import type { SimpleAlovaOptions } from './types'; import { useAppConfig } from '@vben/hooks'; import { useAccessStore } from '@vben/stores'; import { createServerTokenAuthentication } from 'alova/client'; import JsonBigint from 'json-bigint'; import { createAlovaClient } from './alova'; /** 超出 JS 安全整数范围的 id 等字段在 JSON 解析时保留为字符串,避免精度丢失 */ const parseJsonBody = JsonBigint({ storeAsString: true }).parse; const { requestBaseURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const BINARY_DOWNLOAD_CONTENT_TYPES = new Set([ 'application/octet-stream', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]); function decodeContentDispositionFileName(value: string): string { try { return decodeURIComponent(value.trim()); } catch { return value.trim(); } } function parseContentDispositionFileName( disposition?: string | null, fallbackUrl?: string, ): string { if (disposition) { const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i); if (utf8Match?.[1]) { return decodeContentDispositionFileName(utf8Match[1]); } const fileNameMatch = disposition.match(/filename=["']?([^"';]+)["']?/i); if (fileNameMatch?.[1]) { return decodeContentDispositionFileName(fileNameMatch[1]); } } const urlFileName = fallbackUrl?.match(/[^/?#]+(?=$|[?#])/i)?.[0]; return urlFileName ? decodeContentDispositionFileName(urlFileName) : 'download'; } function isBinaryDownloadContentType(contentType?: string): boolean { if (!contentType) return false; return ( BINARY_DOWNLOAD_CONTENT_TYPES.has(contentType) || contentType.startsWith('application/vnd.openxmlformats-officedocument.') ); } export default function createRequestClient(options?: SimpleAlovaOptions) { const store = options?.tokenStore ?? useAccessStore(); const transform = options?.transform ?? ((body) => body); const interceptor = options?.interceptor ?? { async onSuccess(response, method) { if (response.status > 400) { const message = response.statusText; // eslint-disable-next-line no-throw-literal throw { message }; } /* prettier-ignore */ if (method.meta?.notParseResponse) return response.clone(); /* prettier-ignore */ const [contentType] = response.headers.get('content-type')?.split(';') ?? []; if (contentType === 'application/json') { const text = await response.text(); const body = transform(text ? parseJsonBody(text) : {}, method); if (method.meta?.notParseResponseBody) return body; if (body.code === 0) return body.data; throw body; } if (isBinaryDownloadContentType(contentType)) { return { fileName: parseContentDispositionFileName( response.headers.get('content-disposition'), method.url, ), source: await response.blob(), }; } return response; }, }; const instance = createAlovaClient({ id: options?.id, baseURL: options?.baseURL ?? requestBaseURL[options?.id?.toString().toLocaleLowerCase() ?? 'url'] ?? requestBaseURL.url, tokenAuthentication: createServerTokenAuthentication({ refreshTokenOnSuccess: { async isExpired(response) { return response.status === 401; }, async handler(response) { if (store.refreshToken) { console.log(response); } }, metaMatches: { authRole: 'refreshToken', refreshToken: true, }, }, login: { async handler(response, method) { const data = await interceptor.onSuccess?.(response.clone(), method); store.loginExpired = false; store.accessToken = data.accessToken; store.refreshToken = data.refreshToken; }, metaMatches: { authRole: 'login', login: true, }, }, logout: { async handler() { store.loginExpired = true; store.accessToken = null; store.refreshToken = null; }, metaMatches: { authRole: 'logout', logout: true, }, }, assignToken(method) { method.config.headers.Authorization ??= store.accessToken; }, visitorMeta: { authRole: 'none', visitor: true, }, }), cacheLogger: import.meta.env.DEV, }); for (const [type, _interceptor] of Object.entries(interceptor)) { instance.interceptor(type.slice(2).toLowerCase(), _interceptor); } return instance; } export * from './alova'; export * from './composables'; export * from './types'; export * from 'alova/client';