import type { FetchOptions, FetchContext } from 'ofetch' import type { ApiResponse } from '~/types/api' import CryptoJS from 'crypto-js' import { v4 as uuidv4 } from 'uuid' import { DEVICE_UDID_KEY, APP_LOCALE_KEY } from '~/constants' import { showToast } from 'vant' import { getDeviceLanguage, isNativeContext } from '~/utils/helpers' import { createRequestNative } from '~/utils/requestNative' const MD5 = CryptoJS.MD5 // Device and app information const DEVICE_INFO = { udid: '', // Will be initialized app: 'web', device: '', platform: 3, channel: 'official', api: 1, version: '1.0.0', network: 'wifi', } /** * Initialize device info */ const initDeviceInfo = () => { if (import.meta.client) { // Generate or get UDID from localStorage let udid = localStorage.getItem(DEVICE_UDID_KEY) if (!udid) { udid = generateUUID() localStorage.setItem(DEVICE_UDID_KEY, udid) } DEVICE_INFO.udid = udid // Get device info (limit userAgent length to 256 chars) DEVICE_INFO.device = (navigator.userAgent || 'unknown').slice(0, 256) // Detect network type interface NetworkInformation { effectiveType?: string } const nav = navigator as Navigator & { connection?: NetworkInformation mozConnection?: NetworkInformation webkitConnection?: NetworkInformation } const connection = nav.connection || nav.mozConnection || nav.webkitConnection if (connection) { DEVICE_INFO.network = connection.effectiveType || 'wifi' } } } /** * Generate UUID */ const generateUUID = (): string => { return uuidv4() } /** * Generate request ID */ const generateRequestId = (): string => { return uuidv4().replace(/-/g, '') } /** * Generate sign */ const generateSign = (id: string, time: number, token: string, secret: string, body?: unknown): string => { const { udid, app, device, platform, channel, api, version, network } = DEVICE_INFO const bodyStr = body ? JSON.stringify(body) : '' const signStr = `${id}${udid}${app}${device}${platform}${channel}${api}${version}${network}${time}${token}${secret}${bodyStr}` return MD5(signStr).toString() } /** * Create request instance with interceptors */ export const createRequest = () => { const runtimeConfig = useRuntimeConfig() const { token, logout } = useAuth() const nuxtApp = useNuxtApp() // eslint-disable-next-line @typescript-eslint/no-explicit-any const t = ((nuxtApp as any)?.$i18n?.t?.bind((nuxtApp as any)?.$i18n) as ((key: string) => string) | undefined) || ((key: string) => key) // API base configuration (read within a valid Nuxt context) const API_CONFIG = { baseURL: runtimeConfig.public.apiBase || '/api', timeout: 30000, secret: 'abc|abc|edg|9527|1234', identity: runtimeConfig.public.apiIdentity || 'web', } // Sync device/app info with runtime config DEVICE_INFO.app = API_CONFIG.identity as string // Initialize device info on client side if (import.meta.client && !DEVICE_INFO.udid) { initDeviceInfo() } /** * Request interceptor - add auth token and other headers */ const onRequest = (ctx: FetchContext): void => { const { options } = ctx // Add base URL if (!options.baseURL) { options.baseURL = API_CONFIG.baseURL } // Initialize headers as a plain object const headers: Record = {} // Copy existing headers if any if (options.headers) { const existingHeaders = options.headers as unknown as Record Object.assign(headers, existingHeaders) } // Generate request-specific values const requestId = generateRequestId() const time = Date.now() const tokenVal = token.value || '' const language = localStorage.getItem(APP_LOCALE_KEY) if (language) { headers['Accept-Language'] = language } else { headers['Accept-Language'] = getDeviceLanguage() } // Add custom API headers headers['id'] = requestId headers['udid'] = DEVICE_INFO.udid headers['app'] = DEVICE_INFO.app headers['device'] = DEVICE_INFO.device headers['platform'] = String(DEVICE_INFO.platform) headers['channel'] = DEVICE_INFO.channel headers['api'] = String(DEVICE_INFO.api) headers['version'] = DEVICE_INFO.version headers['network'] = DEVICE_INFO.network headers['time'] = String(time) // Add token if exists if (tokenVal) { headers['token'] = tokenVal } // Generate and add signature const body = options.body const sign = generateSign(requestId, time, tokenVal, API_CONFIG.secret, body) headers['sign'] = sign // Add default content type if not already set if (!headers['Content-Type']) { headers['Content-Type'] = 'application/json' } // Set headers (use type assertion to bypass strict type checking) // @ts-expect-error - Headers type mismatch between HeadersInit and Headers options.headers = headers } /** * Response interceptor - handle success response */ const onResponse = (ctx: FetchContext): void => { if (!ctx.response) return const data = ctx.response._data as ApiResponse // If backend returns code field, check it if (data && typeof data === 'object' && 'code' in data) { // Success codes (you can adjust these based on your API) if (data.code === 0) { ctx.response._data = data.data return } // Handle business errors handleApiError(data.code, data.msg) throw new Error(data.msg || t('common.errors.requestFailed')) } } /** * Response error interceptor - handle HTTP errors */ const onResponseError = (ctx: FetchContext): Promise => { const { error } = ctx const response = ctx.response // Handle HTTP status errors if (response) { const status = response.status const data = response._data as { msg?: string } switch (status) { case 401: handleUnauthorized() break case 403: showError(t('common.errors.noPermission')) break case 404: showError(t('common.errors.notFound')) break case 500: showError(t('common.errors.serverError')) break default: showError(data?.msg || error?.message || t('common.errors.requestFailed')) } } else { // Network error or timeout const message = error?.message || '' if (message.includes('timeout')) { showError(t('common.errors.timeout')) } else if (message.includes('Network Error')) { showError(t('common.errors.network')) } else { showError(message || t('common.errors.unknownError')) } } return Promise.reject(error) } /** * Handle API business errors */ function handleApiError(code: number, message: string) { switch (code) { case 100004: handleUnauthorized() break default: if (message) { showError(message) } } } /** * Handle unauthorized access */ function handleUnauthorized() { logout() // Redirect to login page if (import.meta.client) { const route = useRoute() const redirect = route.fullPath showError(t('common.errors.loginExpired')) navigateTo({ path: '/login', query: redirect && redirect !== '/login' ? { redirect } : undefined, }) } } /** * Show error message */ function showError(message: string) { if (import.meta.client) { showToast({ message: `${message}` }) } } // Create fetch instance const request = $fetch.create({ baseURL: API_CONFIG.baseURL, timeout: API_CONFIG.timeout, onRequest, onResponse, onResponseError, }) return request } // Lazily create and reuse a request instance within a valid Nuxt context let requestInstanceWeb: ReturnType | null = null let requestInstanceNative: ReturnType | null = null export const getRequest = () => { if (isNativeContext()) { if (!requestInstanceNative) { requestInstanceNative = createRequestNative() } return requestInstanceNative } if (!requestInstanceWeb) { requestInstanceWeb = createRequest() } return requestInstanceWeb } /** * HTTP request methods wrapper */ export const http = { /** * GET request */ get(url: string, params?: unknown, options?: FetchOptions): Promise { const req = getRequest() return req(url, { ...options, method: 'GET', params: params as Record, }) }, /** * POST request */ post(url: string, data?: unknown, options?: FetchOptions): Promise { const req = getRequest() return req(url, { ...options, method: 'POST', body: data as BodyInit, }) }, /** * PUT request */ put(url: string, data?: unknown, options?: FetchOptions): Promise { const req = getRequest() return req(url, { ...options, method: 'PUT', body: data as BodyInit, }) }, /** * DELETE request */ delete(url: string, params?: unknown, options?: FetchOptions): Promise { const req = getRequest() return req(url, { ...options, method: 'DELETE', params: params as Record, }) }, /** * PATCH request */ patch(url: string, data?: unknown, options?: FetchOptions): Promise { const req = getRequest() return req(url, { ...options, method: 'PATCH', body: data as BodyInit, }) }, }