You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
5.5 KiB
292 lines
5.5 KiB
|
1 month ago
|
/**
|
||
|
|
* 请求 Hook
|
||
|
|
* 提供统一的请求状态管理
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||
|
|
import { AxiosError } from 'axios';
|
||
|
1 month ago
|
import type { RequestConfig } from '@/utils/network/api';
|
||
|
1 month ago
|
|
||
|
|
/**
|
||
|
|
* 请求状态
|
||
|
|
*/
|
||
|
|
export interface RequestState<T> {
|
||
|
|
data: T | null;
|
||
|
|
loading: boolean;
|
||
|
|
error: Error | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 请求选项
|
||
|
|
*/
|
||
|
|
export interface UseRequestOptions<T> extends RequestConfig {
|
||
|
|
/** 是否立即执行 */
|
||
|
|
immediate?: boolean;
|
||
|
|
/** 成功回调 */
|
||
|
|
onSuccess?: (data: T) => void;
|
||
|
|
/** 失败回调 */
|
||
|
|
onError?: (error: Error) => void;
|
||
|
|
/** 完成回调(无论成功失败) */
|
||
|
|
onFinally?: () => void;
|
||
|
|
/** 默认数据 */
|
||
|
|
defaultData?: T;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 请求 Hook
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```tsx
|
||
|
|
* const { data, loading, error, run, refresh } = useRequest(
|
||
|
|
* () => request.get('/api/users'),
|
||
|
|
* { immediate: true }
|
||
|
|
* );
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function useRequest<T = any>(
|
||
|
|
requestFn: () => Promise<T>,
|
||
|
|
options: UseRequestOptions<T> = {}
|
||
|
|
) {
|
||
|
|
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options;
|
||
|
|
|
||
|
|
const [state, setState] = useState<RequestState<T>>({
|
||
|
|
data: defaultData,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
const requestRef = useRef(requestFn);
|
||
|
|
requestRef.current = requestFn;
|
||
|
|
|
||
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 执行请求
|
||
|
|
*/
|
||
|
|
const run = useCallback(
|
||
|
|
async (...args: any[]) => {
|
||
|
|
// 取消之前的请求
|
||
|
|
if (abortControllerRef.current) {
|
||
|
|
abortControllerRef.current.abort();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 创建新的 AbortController
|
||
|
|
abortControllerRef.current = new AbortController();
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
loading: true,
|
||
|
|
error: null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const data = await requestRef.current();
|
||
|
|
|
||
|
|
setState({
|
||
|
|
data,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
onSuccess?.(data);
|
||
|
|
|
||
|
|
return data;
|
||
|
|
} catch (error) {
|
||
|
|
const err = error as Error;
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
loading: false,
|
||
|
|
error: err,
|
||
|
|
}));
|
||
|
|
|
||
|
|
onError?.(err);
|
||
|
|
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
onFinally?.();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[onSuccess, onError, onFinally]
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 刷新(重新执行请求)
|
||
|
|
*/
|
||
|
|
const refresh = useCallback(() => {
|
||
|
|
return run();
|
||
|
|
}, [run]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重置状态
|
||
|
|
*/
|
||
|
|
const reset = useCallback(() => {
|
||
|
|
setState({
|
||
|
|
data: defaultData,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
}, [defaultData]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 取消请求
|
||
|
|
*/
|
||
|
|
const cancel = useCallback(() => {
|
||
|
|
if (abortControllerRef.current) {
|
||
|
|
abortControllerRef.current.abort();
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 立即执行
|
||
|
|
useEffect(() => {
|
||
|
|
if (immediate) {
|
||
|
|
run();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 组件卸载时取消请求
|
||
|
|
return () => {
|
||
|
|
cancel();
|
||
|
|
};
|
||
|
|
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
|
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
run,
|
||
|
|
refresh,
|
||
|
|
reset,
|
||
|
|
cancel,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 分页请求 Hook
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```tsx
|
||
|
|
* const { data, loading, loadMore, refresh, hasMore } = usePagination(
|
||
|
|
* (page, pageSize) => request.get('/api/users', { params: { page, pageSize } })
|
||
|
|
* );
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function usePagination<T = any>(
|
||
|
|
requestFn: (
|
||
|
|
page: number,
|
||
|
|
pageSize: number
|
||
|
|
) => Promise<{
|
||
|
|
list: T[];
|
||
|
|
total: number;
|
||
|
|
hasMore: boolean;
|
||
|
|
}>,
|
||
|
|
options: {
|
||
|
|
pageSize?: number;
|
||
|
|
immediate?: boolean;
|
||
|
|
onSuccess?: (data: T[]) => void;
|
||
|
|
onError?: (error: Error) => void;
|
||
|
|
} = {}
|
||
|
|
) {
|
||
|
|
const { pageSize = 20, immediate = false, onSuccess, onError } = options;
|
||
|
|
|
||
|
|
const [state, setState] = useState({
|
||
|
|
data: [] as T[],
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
error: null as Error | null,
|
||
|
|
page: 1,
|
||
|
|
total: 0,
|
||
|
|
hasMore: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 加载数据
|
||
|
|
*/
|
||
|
|
const load = useCallback(
|
||
|
|
async (page: number, append = false) => {
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
loading: !append,
|
||
|
|
loadingMore: append,
|
||
|
|
error: null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await requestFn(page, pageSize);
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
data: append ? [...prev.data, ...result.list] : result.list,
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
page,
|
||
|
|
total: result.total,
|
||
|
|
hasMore: result.hasMore,
|
||
|
|
}));
|
||
|
|
|
||
|
|
onSuccess?.(result.list);
|
||
|
|
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
const err = error as Error;
|
||
|
|
|
||
|
|
setState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
error: err,
|
||
|
|
}));
|
||
|
|
|
||
|
|
onError?.(err);
|
||
|
|
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[requestFn, pageSize, onSuccess, onError]
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 加载更多
|
||
|
|
*/
|
||
|
|
const loadMore = useCallback(async () => {
|
||
|
|
if (state.loadingMore || !state.hasMore) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
return load(state.page + 1, true);
|
||
|
|
}, [state.loadingMore, state.hasMore, state.page, load]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 刷新(重新加载第一页)
|
||
|
|
*/
|
||
|
|
const refresh = useCallback(async () => {
|
||
|
|
return load(1, false);
|
||
|
|
}, [load]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重置
|
||
|
|
*/
|
||
|
|
const reset = useCallback(() => {
|
||
|
|
setState({
|
||
|
|
data: [],
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
error: null,
|
||
|
|
page: 1,
|
||
|
|
total: 0,
|
||
|
|
hasMore: true,
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 立即执行
|
||
|
|
useEffect(() => {
|
||
|
|
if (immediate) {
|
||
|
|
load(1, false);
|
||
|
|
}
|
||
|
|
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
|
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
loadMore,
|
||
|
|
refresh,
|
||
|
|
reset,
|
||
|
|
};
|
||
|
|
}
|