useApi.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { useCallback, useEffect, useMemo } from 'react';
  2. import {
  3. AxiosInterceptorManager,
  4. AxiosRequestConfig,
  5. AxiosResponse,
  6. isAxiosError,
  7. isCancel,
  8. ValidationError,
  9. } from '@szyx-mobile/use-axios';
  10. import {
  11. RequestError,
  12. RequestOptions,
  13. useRequest,
  14. } from '@szyx-mobile/use-request';
  15. import { z } from 'zod';
  16. import { Platform } from 'react-native';
  17. import { useAuth } from '@common/contexts/useAuth.ts';
  18. import { useCommon } from '@common/contexts/useCommon.ts';
  19. import { getEnvUrl } from '@common/env/envUtils.ts';
  20. import { md5_hex } from '@common/utils/md5';
  21. import { CLIENT_ID, MD5_KEY } from '@common/constants';
  22. import { showErrorMessage } from '@common/ToastHelper.ts';
  23. import DeviceInfo from 'react-native-device-info';
  24. const SUCCESS_STATUS = /^0$/; // 请求成功的状态码
  25. type Response<T> = {
  26. status: string;
  27. message: string;
  28. data: T;
  29. };
  30. const useApi = <
  31. S extends z.ZodTypeAny,
  32. T extends z.infer<S> = unknown,
  33. D = unknown,
  34. >(
  35. url: string,
  36. method: 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD',
  37. params?: D,
  38. validationSchema?: S,
  39. options?: RequestOptions,
  40. config?: AxiosRequestConfig<D>,
  41. ): {
  42. response?: T;
  43. error?: RequestError<unknown, unknown>;
  44. loading: boolean;
  45. fetch: (config?: AxiosRequestConfig<D>) => void;
  46. fetchAsync: (config?: AxiosRequestConfig<D>) => Promise<T>;
  47. cancel: () => void;
  48. requestInterceptors: AxiosInterceptorManager<AxiosRequestConfig<unknown>>;
  49. responseInterceptors: AxiosInterceptorManager<
  50. AxiosResponse<Response<T>, unknown>
  51. >;
  52. } => {
  53. const {
  54. state: { token, userInfo },
  55. actions: { logout },
  56. } = useAuth();
  57. const {
  58. state: { info },
  59. } = useCommon();
  60. // 【这里增加公共参数】
  61. const commonParams = useMemo(() => {
  62. return {};
  63. }, []);
  64. // 【这里添加额外的 headers】
  65. const commonHeaders = useMemo(() => {
  66. try {
  67. return {
  68. clientId: CLIENT_ID, // 厂商唯一标识,这个值是为医网信app分配的
  69. deviceType: Platform.select({ android: '0', ios: '1' }),
  70. version: DeviceInfo.getVersion(),
  71. deviceId: DeviceInfo.getDeviceId(),
  72. phoneModel: DeviceInfo.getModel(),
  73. phoneBrand: DeviceInfo.getBrand(),
  74. phoneVersion: DeviceInfo.getSystemVersion(),
  75. userId: userInfo?.userId ?? '',
  76. sessionId: token ?? '',
  77. currentClientId: '',
  78. appType: '0', // 医网信是 0,冠新是 1
  79. };
  80. } catch {
  81. return {};
  82. }
  83. }, [token, userInfo?.userId]);
  84. // 【这里定义 Response 的 schema】
  85. const responseSchema = useMemo(() => {
  86. // 这里 code 和 message 字段为项目接口中相应的字段名和字段类型
  87. return z.object({
  88. status: z.string().regex(SUCCESS_STATUS),
  89. message: z.string(),
  90. data: validationSchema ?? z.unknown(),
  91. });
  92. }, [validationSchema]);
  93. const {
  94. response,
  95. error,
  96. loading,
  97. fetch,
  98. fetchAsync,
  99. cancel,
  100. requestInterceptors,
  101. responseInterceptors,
  102. } = useRequest<typeof responseSchema, Response<T>, unknown>(
  103. url,
  104. method,
  105. { ...params, ...commonParams },
  106. responseSchema,
  107. options,
  108. {
  109. // 【这里配置 baseURL】
  110. baseURL: getEnvUrl(info.env ?? 'production').url,
  111. ...config,
  112. timeout: 30000,
  113. headers: {
  114. ...commonHeaders,
  115. ...config?.headers,
  116. },
  117. },
  118. );
  119. useEffect(() => {
  120. const requestInterceptor = requestInterceptors.use(c => {
  121. // 这几个header的参数因为是实时的,所以单独拿出来
  122. const timeStamp = Date.now();
  123. const signOrigin = `timeStamp=${timeStamp}#${MD5_KEY}`;
  124. const sign = md5_hex(signOrigin);
  125. c.headers = {
  126. ...c.headers,
  127. timeStamp: timeStamp + '',
  128. sign: sign,
  129. };
  130. return c;
  131. });
  132. return () => {
  133. requestInterceptors.eject(requestInterceptor);
  134. };
  135. }, [requestInterceptors]);
  136. useEffect(() => {
  137. const responseInterceptor = responseInterceptors.use(
  138. r => {
  139. if (options?.log) {
  140. console.debug(JSON.stringify(r, null, 2));
  141. }
  142. return r;
  143. },
  144. e => {
  145. if (isCancel(e)) {
  146. return Promise.reject(
  147. new RequestError('网络请求已取消', 'Cancel', e),
  148. );
  149. }
  150. if (e instanceof ValidationError) {
  151. console.error(
  152. JSON.stringify(
  153. {
  154. name: e.name,
  155. status: e.response.status,
  156. data: e.response.data,
  157. config: e.response.config,
  158. headers: e.response.headers,
  159. issues: e.issues,
  160. url: `${e.response.config.baseURL}${e.response.config.url}`,
  161. },
  162. null,
  163. 2,
  164. ),
  165. );
  166. // 退出登录逻辑
  167. if (
  168. e.response.data?.status === '017x025' ||
  169. e.response.data?.status === '017x013'
  170. ) {
  171. showErrorMessage(
  172. e.response.data?.message ??
  173. '该账号已在其他设备登录,请重新登录!',
  174. );
  175. logout();
  176. return Promise.reject(
  177. new RequestError(
  178. e.response.data?.message ??
  179. '该账号已在其他设备登录,请重新登录!',
  180. 'ValidationError',
  181. e,
  182. ),
  183. );
  184. }
  185. if (e.issues.length <= 0) {
  186. return Promise.reject(
  187. new RequestError('数据格式校验错误', 'ValidationError', e),
  188. );
  189. }
  190. // 这里 code 为项目接口中相应的字段名
  191. if (e.issues[0].path.length > 0 && e.issues[0].path[0] === 'status') {
  192. return Promise.reject(
  193. new RequestError(
  194. e.response.data?.message ?? '', // 这里 message 为项目接口中相应的字段名
  195. 'ValidationError',
  196. e,
  197. ),
  198. );
  199. }
  200. return Promise.reject(
  201. new RequestError(e.issues[0].message, 'ValidationError', e),
  202. );
  203. }
  204. if (isAxiosError(e)) {
  205. console.error(
  206. JSON.stringify(e, null, 2),
  207. `${e.config?.baseURL}${e.config?.url}`,
  208. );
  209. return Promise.reject(
  210. new RequestError('网络请求失败', 'AxiosError', e),
  211. );
  212. }
  213. console.error(
  214. JSON.stringify(e, null, 2),
  215. `${e.config?.baseURL}${e.config?.url}`,
  216. );
  217. return Promise.reject(
  218. new RequestError('网络请求失败', 'OtherError', e),
  219. );
  220. },
  221. );
  222. return () => {
  223. responseInterceptors.eject(responseInterceptor);
  224. };
  225. }, [logout, options?.log, options?.tag, responseInterceptors]);
  226. const transformedFetchAsync = useCallback(
  227. async (c?: AxiosRequestConfig<D>) => {
  228. try {
  229. const r = await fetchAsync(c);
  230. return r.data.data;
  231. } catch (e) {
  232. return Promise.reject(e);
  233. }
  234. },
  235. [fetchAsync],
  236. );
  237. return {
  238. response: response?.data.data,
  239. error: error as RequestError<unknown, unknown>,
  240. loading,
  241. fetch,
  242. fetchAsync: transformedFetchAsync,
  243. cancel,
  244. requestInterceptors,
  245. responseInterceptors,
  246. };
  247. };
  248. export { useApi };