| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- 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'
- 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
- DEVICE_INFO.device = navigator.userAgent
- // 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()
- // API base configuration (read within a valid Nuxt context)
- const API_CONFIG = {
- baseURL: runtimeConfig.public.apiBase || '/api',
- timeout: 30000,
- secret: runtimeConfig.public.apiSecret || 'your-secret-key',
- 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<string, string> = {}
- // Copy existing headers if any
- if (options.headers) {
- const existingHeaders = options.headers as unknown as Record<string, string>
- 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
- }
- // 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 || 'Request failed')
- }
- }
- /**
- * Response error interceptor - handle HTTP errors
- */
- const onResponseError = (ctx: FetchContext): Promise<never> => {
- 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('No permission to access this resource')
- break
- case 404:
- showError('Resource not found')
- break
- case 500:
- showError('Server error, please try again later')
- break
- default:
- showError(data?.msg || error?.message || 'Request failed')
- }
- }
- else {
- // Network error or timeout
- const message = error?.message || ''
- if (message.includes('timeout')) {
- showError('Request timeout, please try again')
- }
- else if (message.includes('Network Error')) {
- showError('Network error, please check your connection')
- }
- else {
- showError(message || 'Unknown error occurred')
- }
- }
- 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('Login expired, please login again')
- navigateTo({
- path: '/login',
- query: redirect && redirect !== '/login' ? { redirect } : undefined,
- })
- }
- }
- /**
- * Show error message
- */
- function showError(message: string) {
- if (import.meta.client) {
- showToast({ message: `API Error: ${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 requestInstance: ReturnType<typeof $fetch.create> | null = null
- export const getRequest = () => {
- if (!requestInstance) {
- requestInstance = createRequest()
- }
- return requestInstance
- }
- /**
- * HTTP request methods wrapper
- */
- export const http = {
- /**
- * GET request
- */
- get<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
- const req = getRequest()
- return req(url, {
- ...options,
- method: 'GET',
- params: params as Record<string, unknown>,
- })
- },
- /**
- * POST request
- */
- post<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
- const req = getRequest()
- return req(url, {
- ...options,
- method: 'POST',
- body: data as BodyInit,
- })
- },
- /**
- * PUT request
- */
- put<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
- const req = getRequest()
- return req(url, {
- ...options,
- method: 'PUT',
- body: data as BodyInit,
- })
- },
- /**
- * DELETE request
- */
- delete<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
- const req = getRequest()
- return req(url, {
- ...options,
- method: 'DELETE',
- params: params as Record<string, unknown>,
- })
- },
- /**
- * PATCH request
- */
- patch<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
- const req = getRequest()
- return req(url, {
- ...options,
- method: 'PATCH',
- body: data as BodyInit,
- })
- },
- }
|