request.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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. // API base configuration (read within a valid Nuxt context)
  78. const API_CONFIG = {
  79. baseURL: runtimeConfig.public.apiBase || '/api',
  80. timeout: 30000,
  81. secret: 'abc|abc|edg|9527|1234',
  82. identity: runtimeConfig.public.apiIdentity || 'web',
  83. }
  84. // Sync device/app info with runtime config
  85. DEVICE_INFO.app = API_CONFIG.identity as string
  86. // Initialize device info on client side
  87. if (import.meta.client && !DEVICE_INFO.udid) {
  88. initDeviceInfo()
  89. }
  90. /**
  91. * Request interceptor - add auth token and other headers
  92. */
  93. const onRequest = (ctx: FetchContext): void => {
  94. const { options } = ctx
  95. // Add base URL
  96. if (!options.baseURL) {
  97. options.baseURL = API_CONFIG.baseURL
  98. }
  99. // Initialize headers as a plain object
  100. const headers: Record<string, string> = {}
  101. // Copy existing headers if any
  102. if (options.headers) {
  103. const existingHeaders = options.headers as unknown as Record<string, string>
  104. Object.assign(headers, existingHeaders)
  105. }
  106. // Generate request-specific values
  107. const requestId = generateRequestId()
  108. const time = Date.now()
  109. const tokenVal = token.value || ''
  110. const language = localStorage.getItem(APP_LOCALE_KEY)
  111. if (language) {
  112. headers['Accept-Language'] = language
  113. }
  114. else {
  115. headers['Accept-Language'] = getDeviceLanguage()
  116. }
  117. // Add custom API headers
  118. headers['id'] = requestId
  119. headers['udid'] = DEVICE_INFO.udid
  120. headers['app'] = DEVICE_INFO.app
  121. headers['device'] = DEVICE_INFO.device
  122. headers['platform'] = String(DEVICE_INFO.platform)
  123. headers['channel'] = DEVICE_INFO.channel
  124. headers['api'] = String(DEVICE_INFO.api)
  125. headers['version'] = DEVICE_INFO.version
  126. headers['network'] = DEVICE_INFO.network
  127. headers['time'] = String(time)
  128. // Add token if exists
  129. if (tokenVal) {
  130. headers['token'] = tokenVal
  131. }
  132. // Generate and add signature
  133. const body = options.body
  134. const sign = generateSign(requestId, time, tokenVal, API_CONFIG.secret, body)
  135. headers['sign'] = sign
  136. // Add default content type if not already set
  137. if (!headers['Content-Type']) {
  138. headers['Content-Type'] = 'application/json'
  139. }
  140. // Set headers (use type assertion to bypass strict type checking)
  141. // @ts-expect-error - Headers type mismatch between HeadersInit and Headers
  142. options.headers = headers
  143. }
  144. /**
  145. * Response interceptor - handle success response
  146. */
  147. const onResponse = (ctx: FetchContext): void => {
  148. if (!ctx.response) return
  149. const data = ctx.response._data as ApiResponse
  150. // If backend returns code field, check it
  151. if (data && typeof data === 'object' && 'code' in data) {
  152. // Success codes (you can adjust these based on your API)
  153. if (data.code === 0) {
  154. ctx.response._data = data.data
  155. return
  156. }
  157. // Handle business errors
  158. handleApiError(data.code, data.msg)
  159. throw new Error(data.msg || 'Request failed')
  160. }
  161. }
  162. /**
  163. * Response error interceptor - handle HTTP errors
  164. */
  165. const onResponseError = (ctx: FetchContext): Promise<never> => {
  166. const { error } = ctx
  167. const response = ctx.response
  168. // Handle HTTP status errors
  169. if (response) {
  170. const status = response.status
  171. const data = response._data as { msg?: string }
  172. switch (status) {
  173. case 401:
  174. handleUnauthorized()
  175. break
  176. case 403:
  177. showError('No permission to access this resource')
  178. break
  179. case 404:
  180. showError('Resource not found')
  181. break
  182. case 500:
  183. showError('Server error, please try again later')
  184. break
  185. default:
  186. showError(data?.msg || error?.message || 'Request failed')
  187. }
  188. }
  189. else {
  190. // Network error or timeout
  191. const message = error?.message || ''
  192. if (message.includes('timeout')) {
  193. showError('Request timeout, please try again')
  194. }
  195. else if (message.includes('Network Error')) {
  196. showError('Network error, please check your connection')
  197. }
  198. else {
  199. showError(message || 'Unknown error occurred')
  200. }
  201. }
  202. return Promise.reject(error)
  203. }
  204. /**
  205. * Handle API business errors
  206. */
  207. function handleApiError(code: number, message: string) {
  208. switch (code) {
  209. case 100004:
  210. handleUnauthorized()
  211. break
  212. default:
  213. if (message) {
  214. showError(message)
  215. }
  216. }
  217. }
  218. /**
  219. * Handle unauthorized access
  220. */
  221. function handleUnauthorized() {
  222. logout()
  223. // Redirect to login page
  224. if (import.meta.client) {
  225. const route = useRoute()
  226. const redirect = route.fullPath
  227. showError('Login expired, please login again')
  228. navigateTo({
  229. path: '/login',
  230. query: redirect && redirect !== '/login' ? { redirect } : undefined,
  231. })
  232. }
  233. }
  234. /**
  235. * Show error message
  236. */
  237. function showError(message: string) {
  238. if (import.meta.client) {
  239. showToast({ message: `${message}` })
  240. }
  241. }
  242. // Create fetch instance
  243. const request = $fetch.create({
  244. baseURL: API_CONFIG.baseURL,
  245. timeout: API_CONFIG.timeout,
  246. onRequest,
  247. onResponse,
  248. onResponseError,
  249. })
  250. return request
  251. }
  252. // Lazily create and reuse a request instance within a valid Nuxt context
  253. let requestInstanceWeb: ReturnType<typeof $fetch.create> | null = null
  254. let requestInstanceNative: ReturnType<typeof $fetch.create> | null = null
  255. export const getRequest = () => {
  256. if (isNativeContext()) {
  257. if (!requestInstanceNative) {
  258. requestInstanceNative = createRequestNative()
  259. }
  260. return requestInstanceNative
  261. }
  262. if (!requestInstanceWeb) {
  263. requestInstanceWeb = createRequest()
  264. }
  265. return requestInstanceWeb
  266. }
  267. /**
  268. * HTTP request methods wrapper
  269. */
  270. export const http = {
  271. /**
  272. * GET request
  273. */
  274. get<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
  275. const req = getRequest()
  276. return req(url, {
  277. ...options,
  278. method: 'GET',
  279. params: params as Record<string, unknown>,
  280. })
  281. },
  282. /**
  283. * POST request
  284. */
  285. post<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  286. const req = getRequest()
  287. return req(url, {
  288. ...options,
  289. method: 'POST',
  290. body: data as BodyInit,
  291. })
  292. },
  293. /**
  294. * PUT request
  295. */
  296. put<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  297. const req = getRequest()
  298. return req(url, {
  299. ...options,
  300. method: 'PUT',
  301. body: data as BodyInit,
  302. })
  303. },
  304. /**
  305. * DELETE request
  306. */
  307. delete<T = unknown>(url: string, params?: unknown, options?: FetchOptions): Promise<T> {
  308. const req = getRequest()
  309. return req(url, {
  310. ...options,
  311. method: 'DELETE',
  312. params: params as Record<string, unknown>,
  313. })
  314. },
  315. /**
  316. * PATCH request
  317. */
  318. patch<T = unknown>(url: string, data?: unknown, options?: FetchOptions): Promise<T> {
  319. const req = getRequest()
  320. return req(url, {
  321. ...options,
  322. method: 'PATCH',
  323. body: data as BodyInit,
  324. })
  325. },
  326. }