RnMultibundler/src/common/api/useApi.ts
xuqm e575e9deb5 feat(app): 更新版本号并添加设备信息相关功能
- 将版本号从"1.0"修改为"1.0.0"
- 在主界面添加退出登录功能
- 使用 react-native-device-info 获取设备信息
- 更新 API 请求头中的设备信息获取方式
- 移除 User 类中冗余的 deviceInfo 字段
2025-09-01 14:38:31 +08:00

267 行
7.2 KiB
TypeScript

import { useCallback, useEffect, useMemo } from 'react';
import {
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
isAxiosError,
isCancel,
ValidationError,
} from '@szyx-mobile/use-axios';
import {
RequestError,
RequestOptions,
useRequest,
} from '@szyx-mobile/use-request';
import { z } from 'zod';
import { Platform } from 'react-native';
import { useAuth } from '@common/contexts/useAuth.ts';
import { useCommon } from '@common/contexts/useCommon.ts';
import { getEnvUrl } from '@common/env/envUtils.ts';
import { md5_hex } from '@common/utils/md5';
import { CLIENT_ID, MD5_KEY } from '@common/constants';
import { showErrorMessage } from '@common/ToastHelper.ts';
import DeviceInfo from 'react-native-device-info';
const SUCCESS_STATUS = /^0$/; // 请求成功的状态码
type Response<T> = {
status: string;
message: string;
data: T;
};
const useApi = <
S extends z.ZodTypeAny,
T extends z.infer<S> = unknown,
D = unknown,
>(
url: string,
method: 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD',
params?: D,
validationSchema?: S,
options?: RequestOptions,
config?: AxiosRequestConfig<D>,
): {
response?: T;
error?: RequestError<unknown, unknown>;
loading: boolean;
fetch: (config?: AxiosRequestConfig<D>) => void;
fetchAsync: (config?: AxiosRequestConfig<D>) => Promise<T>;
cancel: () => void;
requestInterceptors: AxiosInterceptorManager<AxiosRequestConfig<unknown>>;
responseInterceptors: AxiosInterceptorManager<
AxiosResponse<Response<T>, unknown>
>;
} => {
const {
state: { token, userInfo },
actions: { logout },
} = useAuth();
const {
state: { info },
} = useCommon();
// 【这里增加公共参数】
const commonParams = useMemo(() => {
return {};
}, []);
// 【这里添加额外的 headers】
const commonHeaders = useMemo(() => {
try {
return {
clientId: CLIENT_ID, // 厂商唯一标识,这个值是为医网信app分配的
deviceType: Platform.select({ android: '0', ios: '1' }),
version: DeviceInfo.getVersion(),
deviceId: DeviceInfo.getDeviceId(),
phoneModel: DeviceInfo.getModel(),
phoneBrand: DeviceInfo.getBrand(),
phoneVersion: DeviceInfo.getSystemVersion(),
userId: userInfo?.userId ?? '',
sessionId: token ?? '',
currentClientId: '',
appType: '0', // 医网信是 0,冠新是 1
};
} catch {
return {};
}
}, [token, userInfo?.userId]);
// 【这里定义 Response 的 schema】
const responseSchema = useMemo(() => {
// 这里 code 和 message 字段为项目接口中相应的字段名和字段类型
return z.object({
status: z.string().regex(SUCCESS_STATUS),
message: z.string(),
data: validationSchema ?? z.unknown(),
});
}, [validationSchema]);
const {
response,
error,
loading,
fetch,
fetchAsync,
cancel,
requestInterceptors,
responseInterceptors,
} = useRequest<typeof responseSchema, Response<T>, unknown>(
url,
method,
{ ...params, ...commonParams },
responseSchema,
options,
{
// 【这里配置 baseURL】
baseURL: getEnvUrl(info.env ?? 'production').url,
...config,
timeout: 30000,
headers: {
...commonHeaders,
...config?.headers,
},
},
);
useEffect(() => {
const requestInterceptor = requestInterceptors.use(c => {
// 这几个header的参数因为是实时的,所以单独拿出来
const timeStamp = Date.now();
const signOrigin = `timeStamp=${timeStamp}#${MD5_KEY}`;
const sign = md5_hex(signOrigin);
c.headers = {
...c.headers,
timeStamp: timeStamp + '',
sign: sign,
};
return c;
});
return () => {
requestInterceptors.eject(requestInterceptor);
};
}, [requestInterceptors]);
useEffect(() => {
const responseInterceptor = responseInterceptors.use(
r => {
if (options?.log) {
console.debug(JSON.stringify(r, null, 2));
}
return r;
},
e => {
if (isCancel(e)) {
return Promise.reject(
new RequestError('网络请求已取消', 'Cancel', e),
);
}
if (e instanceof ValidationError) {
console.error(
JSON.stringify(
{
name: e.name,
status: e.response.status,
data: e.response.data,
config: e.response.config,
headers: e.response.headers,
issues: e.issues,
url: `${e.response.config.baseURL}${e.response.config.url}`,
},
null,
2,
),
);
// 退出登录逻辑
if (
e.response.data?.status === '017x025' ||
e.response.data?.status === '017x013'
) {
showErrorMessage(
e.response.data?.message ??
'该账号已在其他设备登录,请重新登录!',
);
logout();
return Promise.reject(
new RequestError(
e.response.data?.message ??
'该账号已在其他设备登录,请重新登录!',
'ValidationError',
e,
),
);
}
if (e.issues.length <= 0) {
return Promise.reject(
new RequestError('数据格式校验错误', 'ValidationError', e),
);
}
// 这里 code 为项目接口中相应的字段名
if (e.issues[0].path.length > 0 && e.issues[0].path[0] === 'status') {
return Promise.reject(
new RequestError(
e.response.data?.message ?? '', // 这里 message 为项目接口中相应的字段名
'ValidationError',
e,
),
);
}
return Promise.reject(
new RequestError(e.issues[0].message, 'ValidationError', e),
);
}
if (isAxiosError(e)) {
console.error(
JSON.stringify(e, null, 2),
`${e.config?.baseURL}${e.config?.url}`,
);
return Promise.reject(
new RequestError('网络请求失败', 'AxiosError', e),
);
}
console.error(
JSON.stringify(e, null, 2),
`${e.config?.baseURL}${e.config?.url}`,
);
return Promise.reject(
new RequestError('网络请求失败', 'OtherError', e),
);
},
);
return () => {
responseInterceptors.eject(responseInterceptor);
};
}, [logout, options?.log, options?.tag, responseInterceptors]);
const transformedFetchAsync = useCallback(
async (c?: AxiosRequestConfig<D>) => {
try {
const r = await fetchAsync(c);
return r.data.data;
} catch (e) {
return Promise.reject(e);
}
},
[fetchAsync],
);
return {
response: response?.data.data,
error: error as RequestError<unknown, unknown>,
loading,
fetch,
fetchAsync: transformedFetchAsync,
cancel,
requestInterceptors,
responseInterceptors,
};
};
export { useApi };