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>
这个提交包含在:
父节点
1f72b415dc
当前提交
1143224194
@ -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,10 +97,12 @@ 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,13 +111,15 @@ 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')
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@ -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'
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户