|
|
@@ -0,0 +1,340 @@
|
|
|
+import type { FetchOptions, FetchContext } from 'ofetch'
|
|
|
+import type { ApiResponse } from '~/types/api'
|
|
|
+import { MD5 } from 'crypto-js'
|
|
|
+import { v4 as uuidv4 } from 'uuid'
|
|
|
+
|
|
|
+const runtimeConfig = useRuntimeConfig()
|
|
|
+
|
|
|
+// API base configuration
|
|
|
+const API_CONFIG = {
|
|
|
+ baseURL: runtimeConfig.public.apiBase || '/api',
|
|
|
+ timeout: 30000,
|
|
|
+ secret: runtimeConfig.public.apiSecret || 'your-secret-key',
|
|
|
+ identity: runtimeConfig.public.apiIdentity || 'web',
|
|
|
+}
|
|
|
+
|
|
|
+// Device and app information
|
|
|
+const DEVICE_INFO = {
|
|
|
+ udid: '', // Will be initialized
|
|
|
+ app: API_CONFIG.identity as string,
|
|
|
+ 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')
|
|
|
+ if (!udid) {
|
|
|
+ udid = generateUUID()
|
|
|
+ localStorage.setItem('device_udid', 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()}${uuidv4()}`.replace('-', '')
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Generate request ID
|
|
|
+ */
|
|
|
+const generateRequestId = (): string => {
|
|
|
+ return uuidv4().replace('-', '')
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Generate sign
|
|
|
+ */
|
|
|
+const generateSign = (id: string, time: number, token: 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}${API_CONFIG.secret}${bodyStr}`
|
|
|
+
|
|
|
+ return MD5(signStr).toString()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Create request instance with interceptors
|
|
|
+ */
|
|
|
+export const createRequest = () => {
|
|
|
+ const { getToken } = useAuth()
|
|
|
+
|
|
|
+ // 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 token = getToken() || ''
|
|
|
+
|
|
|
+ // 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 (token) {
|
|
|
+ headers['token'] = token
|
|
|
+ }
|
|
|
+
|
|
|
+ // Generate and add signature
|
|
|
+ const body = options.body
|
|
|
+ const sign = generateSign(requestId, time, token, 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 || data.code === 200) {
|
|
|
+ ctx.response._data = data.data
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle business errors
|
|
|
+ handleApiError(data.code, data.message)
|
|
|
+ throw new Error(data.message || '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 { message?: 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?.message || 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create fetch instance
|
|
|
+ const request = $fetch.create({
|
|
|
+ baseURL: API_CONFIG.baseURL,
|
|
|
+ timeout: API_CONFIG.timeout,
|
|
|
+ onRequest,
|
|
|
+ onResponse,
|
|
|
+ onResponseError,
|
|
|
+ })
|
|
|
+
|
|
|
+ return request
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Handle API business errors
|
|
|
+ */
|
|
|
+function handleApiError(code: number, message: string) {
|
|
|
+ // You can handle specific error codes here
|
|
|
+ switch (code) {
|
|
|
+ case 401:
|
|
|
+ handleUnauthorized()
|
|
|
+ break
|
|
|
+ case 403:
|
|
|
+ showError('No permission')
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ if (message) {
|
|
|
+ showError(message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Handle unauthorized access
|
|
|
+ */
|
|
|
+function handleUnauthorized() {
|
|
|
+ const { logout } = useAuth()
|
|
|
+ logout()
|
|
|
+
|
|
|
+ // Redirect to login page
|
|
|
+ if (import.meta.client) {
|
|
|
+ showError('Login expired, please login again')
|
|
|
+ navigateTo('/login')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Show error message
|
|
|
+ */
|
|
|
+function showError(message: string) {
|
|
|
+ if (import.meta.client) {
|
|
|
+ // You can replace this with your preferred notification library
|
|
|
+ // For example, using Vant's showToast
|
|
|
+ console.error(message)
|
|
|
+ // showToast({ message, type: 'fail' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Export default request instance
|
|
|
+export const request = createRequest()
|
|
|
+
|
|
|
+/**
|
|
|
+ * HTTP request methods wrapper
|
|
|
+ */
|
|
|
+export const http = {
|
|
|
+ /**
|
|
|
+ * GET request
|
|
|
+ */
|
|
|
+ get<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
|
|
|
+ return request(url, {
|
|
|
+ ...options,
|
|
|
+ method: 'GET',
|
|
|
+ params: params as Record<string, unknown>,
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * POST request
|
|
|
+ */
|
|
|
+ post<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
|
|
|
+ return request(url, {
|
|
|
+ ...options,
|
|
|
+ method: 'POST',
|
|
|
+ body: data as BodyInit,
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PUT request
|
|
|
+ */
|
|
|
+ put<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
|
|
|
+ return request(url, {
|
|
|
+ ...options,
|
|
|
+ method: 'PUT',
|
|
|
+ body: data as BodyInit,
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DELETE request
|
|
|
+ */
|
|
|
+ delete<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
|
|
|
+ return request(url, {
|
|
|
+ ...options,
|
|
|
+ method: 'DELETE',
|
|
|
+ params: params as Record<string, unknown>,
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PATCH request
|
|
|
+ */
|
|
|
+ patch<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
|
|
|
+ return request(url, {
|
|
|
+ ...options,
|
|
|
+ method: 'PATCH',
|
|
|
+ body: data as BodyInit,
|
|
|
+ })
|
|
|
+ },
|
|
|
+}
|