request.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import type { FetchOptions, FetchContext } from 'ofetch'
  2. import type { ApiResponse } from '~/types/api'
  3. import CryptoJS from 'crypto-js'
  4. import { v4 as uuidv4 } from 'uuid'
  5. import { DEVICE_UDID_KEY, APP_LOCALE_KEY } from '~/constants'
  6. import { showToast } from 'vant'
  7. const MD5 = CryptoJS.MD5
  8. // Device and app information
  9. const DEVICE_INFO = {
  10. udid: '', // Will be initialized
  11. app: 'web',
  12. device: '',
  13. platform: 3,
  14. channel: 'official',
  15. api: 1,
  16. version: '1.0.0',
  17. network: 'wifi',
  18. }
  19. /**
  20. * Initialize device info
  21. */
  22. const initDeviceInfo = () => {
  23. if (import.meta.client) {
  24. // Generate or get UDID from localStorage
  25. let udid = localStorage.getItem(DEVICE_UDID_KEY)
  26. if (!udid) {
  27. udid = generateUUID()
  28. localStorage.setItem(DEVICE_UDID_KEY, udid)
  29. }
  30. DEVICE_INFO.udid = udid
  31. // Get device info
  32. DEVICE_INFO.device = navigator.userAgent
  33. // Detect network type
  34. interface NetworkInformation {
  35. effectiveType?: string
  36. }
  37. const nav = navigator as Navigator & {
  38. connection?: NetworkInformation
  39. mozConnection?: NetworkInformation
  40. webkitConnection?: NetworkInformation
  41. }
  42. const connection = nav.connection || nav.mozConnection || nav.webkitConnection
  43. if (connection) {
  44. DEVICE_INFO.network = connection.effectiveType || 'wifi'
  45. }
  46. }
  47. }
  48. /**
  49. * Generate UUID
  50. */
  51. const generateUUID = (): string => {
  52. return uuidv4()
  53. }
  54. /**
  55. * Generate request ID
  56. */
  57. const generateRequestId = (): string => {
  58. return uuidv4().replace(/-/g, '')
  59. }
  60. /**
  61. * Generate sign
  62. */
  63. const generateSign = (id: string, time: number, token: string, secret: string, body?: unknown): string => {
  64. const { udid, app, device, platform, channel, api, version, network } = DEVICE_INFO
  65. const bodyStr = body ? JSON.stringify(body) : ''
  66. const signStr = `${id}${udid}${app}${device}${platform}${channel}${api}${version}${network}${time}${token}${secret}${bodyStr}`
  67. return MD5(signStr).toString()
  68. }
  69. /**
  70. * Create request instance with interceptors
  71. */
  72. export const createRequest = () => {
  73. const runtimeConfig = useRuntimeConfig()
  74. const { token, logout } = useAuth()
  75. // API base configuration (read within a valid Nuxt context)
  76. const API_CONFIG = {
  77. baseURL: runtimeConfig.public.apiBase || '/api',
  78. timeout: 30000,
  79. secret: runtimeConfig.public.apiSecret || 'your-secret-key',
  80. identity: runtimeConfig.public.apiIdentity || 'web',
  81. }
  82. // Sync device/app info with runtime config
  83. DEVICE_INFO.app = API_CONFIG.identity as string
  84. // Initialize device info on client side
  85. if (import.meta.client && !DEVICE_INFO.udid) {
  86. initDeviceInfo()
  87. }
  88. /**
  89. * Request interceptor - add auth token and other headers
  90. */
  91. const onRequest = (ctx: FetchContext): void => {
  92. const { options } = ctx
  93. // Add base URL
  94. if (!options.baseURL) {
  95. options.baseURL = API_CONFIG.baseURL
  96. }
  97. // Initialize headers as a plain object
  98. const headers: Record<string, string> = {}
  99. // Copy existing headers if any
  100. if (options.headers) {
  101. const existingHeaders = options.headers as unknown as Record<string, string>
  102. Object.assign(headers, existingHeaders)
  103. }
  104. // Generate request-specific values
  105. const requestId = generateRequestId()
  106. const time = Date.now()
  107. const tokenVal = token.value || ''
  108. const language = localStorage.getItem(APP_LOCALE_KEY)
  109. if (language) {
  110. headers['Accept-Language'] = language
  111. }
  112. // Add custom API headers
  113. headers['id'] = requestId
  114. headers['udid'] = DEVICE_INFO.udid
  115. headers['app'] = DEVICE_INFO.app
  116. headers['device'] = DEVICE_INFO.device
  117. headers['platform'] = String(DEVICE_INFO.platform)
  118. headers['channel'] = DEVICE_INFO.channel
  119. headers['api'] = String(DEVICE_INFO.api)
  120. headers['version'] = DEVICE_INFO.version
  121. headers['network'] = DEVICE_INFO.network
  122. headers['time'] = String(time)
  123. // Add token if exists
  124. if (tokenVal) {
  125. headers['token'] = tokenVal
  126. }
  127. // Generate and add signature
  128. const body = options.body
  129. const sign = generateSign(requestId, time, tokenVal, API_CONFIG.secret, body)
  130. headers['sign'] = sign
  131. // Add default content type if not already set
  132. if (!headers['Content-Type']) {
  133. headers['Content-Type'] = 'application/json'
  134. }
  135. // Set headers (use type assertion to bypass strict type checking)
  136. // @ts-expect-error - Headers type mismatch between HeadersInit and Headers
  137. options.headers = headers
  138. }
  139. /**
  140. * Response interceptor - handle success response
  141. */
  142. const onResponse = (ctx: FetchContext): void => {
  143. if (!ctx.response) return
  144. const data = ctx.response._data as ApiResponse
  145. // If backend returns code field, check it
  146. if (data && typeof data === 'object' && 'code' in data) {
  147. // Success codes (you can adjust these based on your API)
  148. if (data.code === 0) {
  149. ctx.response._data = data.data
  150. return
  151. }
  152. // Handle business errors
  153. handleApiError(data.code, data.msg)
  154. throw new Error(data.msg || 'Request failed')
  155. }
  156. }
  157. /**
  158. * Response error interceptor - handle HTTP errors
  159. */
  160. const onResponseError = (ctx: FetchContext): Promise<never> => {
  161. const { error } = ctx
  162. const response = ctx.response
  163. // Handle HTTP status errors
  164. if (response) {
  165. const status = response.status
  166. const data = response._data as { msg?: string }
  167. switch (status) {
  168. case 401:
  169. handleUnauthorized()
  170. break
  171. case 403:
  172. showError('No permission to access this resource')
  173. break
  174. case 404:
  175. showError('Resource not found')
  176. break
  177. case 500:
  178. showError('Server error, please try again later')
  179. break
  180. default:
  181. showError(data?.msg || error?.message || 'Request failed')
  182. }
  183. }
  184. else {
  185. // Network error or timeout
  186. const message = error?.message || ''
  187. if (message.includes('timeout')) {
  188. showError('Request timeout, please try again')
  189. }
  190. else if (message.includes('Network Error')) {
  191. showError('Network error, please check your connection')
  192. }
  193. else {
  194. showError(message || 'Unknown error occurred')
  195. }
  196. }
  197. return Promise.reject(error)
  198. }
  199. /**
  200. * Handle API business errors
  201. */
  202. function handleApiError(code: number, message: string) {
  203. switch (code) {
  204. case 100004:
  205. handleUnauthorized()
  206. break
  207. default:
  208. if (message) {
  209. showError(message)
  210. }
  211. }
  212. }
  213. /**
  214. * Handle unauthorized access
  215. */
  216. function handleUnauthorized() {
  217. logout()
  218. // Redirect to login page
  219. if (import.meta.client) {
  220. const route = useRoute()
  221. const redirect = route.fullPath
  222. showError('Login expired, please login again')
  223. navigateTo({
  224. path: '/login',
  225. query: redirect && redirect !== '/login' ? { redirect } : undefined,
  226. })
  227. }
  228. }
  229. /**
  230. * Show error message
  231. */
  232. function showError(message: string) {
  233. if (import.meta.client) {
  234. showToast({ message: `API Error: ${message}` })
  235. }
  236. }
  237. // Create fetch instance
  238. const request = $fetch.create({
  239. baseURL: API_CONFIG.baseURL,
  240. timeout: API_CONFIG.timeout,
  241. onRequest,
  242. onResponse,
  243. onResponseError,
  244. })
  245. return request
  246. }
  247. // Lazily create and reuse a request instance within a valid Nuxt context
  248. let requestInstance: ReturnType<typeof $fetch.create> | null = null
  249. export const getRequest = () => {
  250. if (!requestInstance) {
  251. requestInstance = createRequest()
  252. }
  253. return requestInstance
  254. }
  255. /**
  256. * HTTP request methods wrapper
  257. */
  258. export const http = {
  259. /**
  260. * GET request
  261. */
  262. get<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
  263. const req = getRequest()
  264. return req(url, {
  265. ...options,
  266. method: 'GET',
  267. params: params as Record<string, unknown>,
  268. })
  269. },
  270. /**
  271. * POST request
  272. */
  273. post<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  274. const req = getRequest()
  275. return req(url, {
  276. ...options,
  277. method: 'POST',
  278. body: data as BodyInit,
  279. })
  280. },
  281. /**
  282. * PUT request
  283. */
  284. put<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  285. const req = getRequest()
  286. return req(url, {
  287. ...options,
  288. method: 'PUT',
  289. body: data as BodyInit,
  290. })
  291. },
  292. /**
  293. * DELETE request
  294. */
  295. delete<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
  296. const req = getRequest()
  297. return req(url, {
  298. ...options,
  299. method: 'DELETE',
  300. params: params as Record<string, unknown>,
  301. })
  302. },
  303. /**
  304. * PATCH request
  305. */
  306. patch<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  307. const req = getRequest()
  308. return req(url, {
  309. ...options,
  310. method: 'PATCH',
  311. body: data as BodyInit,
  312. })
  313. },
  314. }