request.ts 9.5 KB

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