fix(common): 对齐 @szyx-mobile useRequest 行为并修复 POST body 丢失

- 修复 useRequest POST/PUT/UPLOAD/POSTFORM 请求忽略 fetchAsync data 的问题
- RequestOptions 增加 tag/log 字段,与旧项目兼容
- 导出 ValidationError,供宿主二次封装使用 instanceof 判断
- usePageApi 支持 list 与 records 两种分页字段
- 升级 devDependencies 版本(axios 1.18.0、zod 3.25.76 等)

Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-20 18:26:45 +08:00
父节点 1f72b415dc
当前提交 1143224194
共有 6 个文件被更改,包括 97 次插入42 次删除

查看文件

@ -26,11 +26,11 @@
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/react": "^19.0.0",
"@react-native-async-storage/async-storage": "^2.1.2",
"axios": "^1.7.0",
"react": "^19.0.0",
"react-native": "^0.85.0",
"zod": "^3.23.0"
"@types/react": "^19.2.14",
"@react-native-async-storage/async-storage": "^3.1.1",
"axios": "^1.18.0",
"react": "^19.2.0",
"react-native": "^0.85.3",
"zod": "^3.25.76"
}
}

查看文件

@ -1,4 +1,4 @@
export { useRequest } from './useRequest'
export { useRequest, ValidationError } from './useRequest'
export { useApi } from './useApi'
export { usePageApi } from './usePageApi'
export { RequestError } from './errors'

查看文件

@ -8,7 +8,7 @@ import {
} from 'axios'
import { z } from 'zod'
import { RequestError } from './errors'
import { type RequestOptions, useRequest } from './useRequest'
import { type RequestOptions, useRequest, ValidationError } from './useRequest'
const SUCCESS_STATUS = /^0$/
@ -20,21 +20,6 @@ type ApiResponse<T> = {
export type ApiMethod = 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD'
// ValidationError shape (duck-typed to avoid circular dependency)
interface HasIssues {
issues: Array<{ path: (string | number)[]; message: string }>
response?: { data?: { message?: string } }
}
function isValidationError(e: unknown): e is HasIssues {
return (
e != null &&
typeof e === 'object' &&
'issues' in e &&
Array.isArray((e as { issues: unknown }).issues)
)
}
export const useApi = <
S extends z.ZodTypeAny,
T extends z.infer<S> = z.infer<S>,
@ -97,7 +82,12 @@ export const useApi = <
useEffect(() => {
const responseInterceptor = responseInterceptors.use(
nextResponse => nextResponse,
r => {
if (options?.log) {
console.debug(JSON.stringify(r, null, 2))
}
return r
},
(thrownError: unknown) => {
if (isCancel(thrownError)) {
return Promise.reject(
@ -105,45 +95,68 @@ export const useApi = <
)
}
if (isValidationError(thrownError)) {
if (thrownError.issues.length <= 0) {
if (thrownError instanceof ValidationError) {
const e = thrownError
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.issues.length <= 0) {
return Promise.reject(
new RequestError('数据格式校验错误', 'ValidationError', thrownError),
new RequestError('数据格式校验错误', 'ValidationError', e),
)
}
const firstIssue = thrownError.issues[0]
const firstIssue = e.issues[0]
if (firstIssue.path.length > 0 && firstIssue.path[0] === 'status') {
return Promise.reject(
new RequestError(
thrownError.response?.data?.message ?? '接口返回失败',
e.response.data?.message ?? '接口返回失败',
'ValidationError',
thrownError,
e,
),
)
}
return Promise.reject(
new RequestError(firstIssue.message, 'ValidationError', thrownError),
new RequestError(firstIssue.message, 'ValidationError', e),
)
}
if (isAxiosError(thrownError)) {
console.error(
JSON.stringify(thrownError, null, 2),
`${thrownError.config?.baseURL}${thrownError.config?.url}`,
)
return Promise.reject(
new RequestError('网络请求失败', 'AxiosError', thrownError),
)
}
console.error(
JSON.stringify(thrownError, null, 2),
)
return Promise.reject(
new RequestError('网络请求失败', 'OtherError', thrownError),
)
},
)
return () => {
responseInterceptors.eject(responseInterceptor)
}
}, [responseInterceptors])
}, [options?.log, responseInterceptors])
const transformedFetchAsync = useCallback(
async (nextConfig?: AxiosRequestConfig<D>) => {

查看文件

@ -7,7 +7,8 @@ import { z } from 'zod'
import { useApi } from './useApi'
type List<T> = {
records: T[]
records?: T[]
list?: T[]
}
type Root<T> = T[]
@ -41,7 +42,8 @@ export const usePageApi = <
const [status, setStatus] = useState<'loaded' | 'reload' | 'loadmore'>('loaded')
const listSchema: z.ZodSchema<List<T>> = z.object({
records: z.array(validationSchema ?? z.any()),
records: z.array(validationSchema ?? z.any()).optional(),
list: z.array(validationSchema ?? z.any()).optional(),
})
const rootSchema: z.ZodSchema<Root<T>> = z.array(validationSchema ?? z.any())
@ -58,7 +60,12 @@ export const usePageApi = <
status === 'loadmore'
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
: 1,
pageNum:
status === 'loadmore'
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
: 1,
size: options?.pageSize ?? 10,
pageSize: options?.pageSize ?? 10,
}
: {
...params,
@ -90,9 +97,11 @@ export const usePageApi = <
case 'root':
setData(result as Root<T>)
break
default:
setData((result as List<T>).records)
default: {
const listResult = result as List<T>
setData(listResult.records ?? listResult.list)
break
}
}
setStatus('loaded')
return
@ -102,12 +111,14 @@ export const usePageApi = <
case 'root':
setData(previous => [...(previous ?? []), ...(result as Root<T>)])
break
default:
default: {
const listResult = result as List<T>
setData(previous => [
...(previous ?? []),
...(result as List<T>).records,
...(listResult.records ?? listResult.list ?? []),
])
break
}
}
setStatus('loaded')
})

查看文件

@ -18,11 +18,15 @@ export interface RequestOptions {
manual?: boolean
/** 延迟多少毫秒后才将 loading 置为 true默认 0。*/
loadingDelay?: number
/** 请求标签,用于日志或错误追踪。*/
tag?: string
/** true = 在响应拦截器中打印完整响应。*/
log?: boolean
}
// ─── 内部 ValidationError对齐 @szyx-mobile/use-axios 的 ValidationError────
class ValidationError<T = unknown, D = unknown> extends z.ZodError {
export class ValidationError<T = unknown, D = unknown> extends z.ZodError {
constructor(
issues: z.ZodIssue[],
public readonly response: AxiosResponse<T, D>,
@ -127,6 +131,28 @@ export function useRequest<
}
}
function mergeRequestData(base: unknown, override: unknown): unknown {
if (override === undefined || override === null) {
return base
}
if (base === undefined || base === null) {
return override
}
if (
typeof base === 'object' &&
typeof override === 'object' &&
!(override instanceof FormData) &&
!(override instanceof ArrayBuffer) &&
!Array.isArray(override)
) {
return {...base as Record<string, unknown>, ...override as Record<string, unknown>}
}
return override
}
const fetchAsync = useCallback(
async (overrideConfig?: AxiosRequestConfig<D>): Promise<R> => {
const loadingDelay = optionsRef.current?.loadingDelay ?? 0
@ -152,8 +178,12 @@ export function useRequest<
...overrideConfig,
url,
method: axiosMethod,
params: isGet ? params : (overrideConfig?.params ?? config?.params),
data: isGet ? (overrideConfig?.data ?? config?.data) : params,
params: isGet
? (overrideConfig?.params ?? config?.params ?? params)
: (overrideConfig?.params ?? config?.params),
data: isGet
? (overrideConfig?.data ?? config?.data)
: mergeRequestData(config?.data, overrideConfig?.data ?? params),
headers: {
...methodHeaders,
...(config?.headers as Record<string, string> | undefined),

查看文件

@ -47,6 +47,7 @@ export {
useRequest,
useApi,
usePageApi,
ValidationError,
} from './api'
export type { RequestOptions as UseRequestOptions, ApiMethod } from './api'
export type { PageOptions } from './api'