Files
xinlong-shop-taro/src_bak/hooks/useRequest.ts
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

326 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react'
/** 简易 AbortSignal 替代(兼容小程序环境) */
class SimpleAbortSignal {
private _aborted = false
get aborted() { return this._aborted }
abort() { this._aborted = true }
}
class SimpleAbortController {
signal = new SimpleAbortSignal()
abort() { this.signal.abort() }
}
export interface RequestState<T = any> {
/** 请求结果数据 */
data: T | null
/** 是否加载中 */
loading: boolean
/** 错误信息 */
error: Error | null
/** 是否首次加载完成(不管成功或失败) */
initialized: boolean
}
export interface UseRequestOptions<T = any, P extends any[] = any[]> {
/** 手动触发模式下需要调用 run 执行 */
manual?: boolean
/** 初始数据 */
defaultData?: T
/** 请求函数 */
service?: (...args: P) => Promise<T>
/** 成功回调 */
onSuccess?: (data: T, params: P) => void
/** 失败回调 */
onError?: (error: Error, params: P) => void
/** 轮询间隔(毫秒),设置后自动轮询 */
pollingInterval?: number
/** 轮询在页面隐藏时暂停,默认 true */
pollingWhenHidden?: boolean
/** 防抖延迟(毫秒) */
debounceInterval?: number
/** 依赖变化时自动重新请求 */
refreshDeps?: unknown[]
/** 请求节流间隔(毫秒) */
throttleInterval?: number
/** 缓存 key相同 key 的请求结果会被缓存 */
cacheKey?: string
/** 缓存有效期(毫秒) */
cacheTime?: number
/** 重新请求尝试次数 */
retryCount?: number
/** 重试延迟(毫秒),默认 1000 */
retryInterval?: number
}
export interface UseRequestReturn<T = any, P extends any[] = any[]> {
/** 当前状态 */
state: RequestState<T>
/** 请求结果数据state.data 的快捷访问) */
data: T | null
/** 是否加载中state.loading 的快捷访问) */
loading: boolean
/** 错误信息state.error 的快捷访问) */
error: Error | null
/** 是否首次加载完成state.initialized 的快捷访问) */
initialized: boolean
/** 触发请求manual 模式下) */
run: (...params: P) => Promise<T | undefined>
/** 带 loading 的运行 */
runLoading: (...params: P) => Promise<T | undefined>
/** 取消请求 */
cancel: () => void
/** 刷新(使用上次参数) */
refresh: () => void
/** 修改 data不发起请求 */
mutate: (data: T | ((prev: T | null) => T)) => void
/** 当前参数 */
params: P
}
// 简单缓存
const cache = new Map<string, { data: unknown; expireAt: number }>()
function getCache<T>(key: string): T | null {
const item = cache.get(key)
if (!item) return null
if (Date.now() > item.expireAt) {
cache.delete(key)
return null
}
return item.data as T
}
function setCache<T>(key: string, data: T, cacheTime: number) {
cache.set(key, { data, expireAt: Date.now() + cacheTime })
}
/**
* 请求封装 Hook
* @param service 请求函数
* @param options 配置选项
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useRequest<T = any, P extends any[] = any[]>(
service: (...args: P) => Promise<T>,
options: UseRequestOptions<T, P> = {}
): UseRequestReturn<T, P>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useRequest<T = any>(
service: undefined,
options: UseRequestOptions<T, unknown[]> = {}
): UseRequestReturn<T, unknown[]>
export function useRequest<T = any, P extends any[] = any[]>(
service: ((...args: P) => Promise<T>) | undefined,
options: UseRequestOptions<T, P> = {}
): UseRequestReturn<T, P> {
const {
manual = false,
defaultData,
onSuccess,
onError,
pollingInterval,
pollingWhenHidden = true,
debounceInterval,
refreshDeps = [],
throttleInterval,
cacheKey,
cacheTime = 5 * 60 * 1000,
retryCount = 0,
retryInterval = 1000,
} = options
const [state, setState] = useState<RequestState<T>>({
data: defaultData ?? null,
loading: false,
error: null,
initialized: !!defaultData,
})
const [params, setParams] = useState<P>([] as unknown as P)
const serviceRef = useRef(service)
const onSuccessRef = useRef(onSuccess)
const onErrorRef = useRef(onError)
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const abortControllerRef = useRef<SimpleAbortController | null>(null)
const isMountedRef = useRef(true)
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const throttleLastRunRef = useRef(0)
// 更新 refs
useEffect(() => {
serviceRef.current = service
}, [service])
useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])
useEffect(() => { onErrorRef.current = onError }, [onError])
// 清理
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
if (pollingTimerRef.current) clearInterval(pollingTimerRef.current)
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
abortControllerRef.current?.abort()
}
}, [])
// 页面可见性处理(使用 Taro 生命周期,兼容小程序环境)
useEffect(() => {
if (!pollingInterval || !pollingWhenHidden) return
// 在小程序中Taro.useDidShow / useDidHide 仅可在页面组件中使用
// useRequest 作为通用 hook 无法直接使用这些,因此跳过
// 如需轮询暂停/恢复能力,可在页面组件中手动调用 cancel / refresh
}, [pollingInterval, pollingWhenHidden])
// 依赖刷新 - 使用 useRef 避免闭包过期
const refreshDepsRef = useRef(refreshDeps)
refreshDepsRef.current = refreshDeps
useEffect(() => {
if (!manual && refreshDepsRef.current.length > 0) {
refresh()
}
}, [...refreshDeps]) // eslint-disable-line
const run = useCallback(async (...p: P): Promise<T | undefined> => {
// 防抖
if (debounceInterval) {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
return new Promise((resolve) => {
debounceTimerRef.current = setTimeout(async () => {
const result = await doRequest(...p)
resolve(result)
}, debounceInterval)
})
}
// 节流
if (throttleInterval) {
const now = Date.now()
if (now - throttleLastRunRef.current < throttleInterval) {
return
}
throttleLastRunRef.current = now
}
return doRequest(...p)
}, [debounceInterval, throttleInterval]) // eslint-disable-line
const doRequest = useCallback(async (...p: P): Promise<T | undefined> => {
if (!serviceRef.current) return
setParams(p)
abortControllerRef.current?.abort()
abortControllerRef.current = new SimpleAbortController()
// 检查缓存
if (cacheKey) {
const cached = getCache<T>(cacheKey)
if (cached !== null) {
setState({ data: cached, loading: false, error: null, initialized: true })
return cached
}
}
setState(prev => ({ ...prev, loading: true, error: null }))
try {
let result!: T
let attempt = 0
const maxAttempts = retryCount + 1
while (attempt < maxAttempts) {
try {
result = await serviceRef.current(...p)
break
} catch (err: unknown) {
attempt++
const isAbortError = err instanceof Error && err.message.includes('abort')
if (attempt >= maxAttempts || isAbortError) {
throw err
}
await new Promise(r => setTimeout(r, retryInterval * attempt))
}
}
if (!isMountedRef.current) return
// 缓存
if (cacheKey) {
setCache(cacheKey, result, cacheTime)
}
setState({ data: result, loading: false, error: null, initialized: true })
onSuccessRef.current?.(result, p)
// 轮询
if (pollingInterval && !pollingTimerRef.current) {
pollingTimerRef.current = setInterval(() => {
doRequest(...p)
}, pollingInterval)
}
return result
} catch (err: unknown) {
if (!isMountedRef.current) return
const error = err instanceof Error ? err : new Error(String(err))
setState({ data: null, loading: false, error, initialized: true })
onErrorRef.current?.(error, p)
return undefined
}
}, [cacheKey, cacheTime, pollingInterval, pollingWhenHidden, retryCount, retryInterval]) // eslint-disable-line
const runLoading = useCallback(async (...p: P) => {
if (!serviceRef.current) return
setParams(p)
return doRequest(...p)
}, [doRequest])
const cancel = useCallback(() => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current)
pollingTimerRef.current = null
}
abortControllerRef.current?.abort()
setState(prev => ({ ...prev, loading: false }))
}, [])
const refresh = useCallback(() => {
if (params.length > 0) {
run(...params)
}
}, [params, run])
const mutate = useCallback((data: T | ((prev: T | null) => T)) => {
setState(prev => ({
...prev,
data: typeof data === 'function' ? (data as (prev: T | null) => T)(prev.data) : data,
}))
}, [])
// 自动执行
useEffect(() => {
if (!manual && service) {
run(...([] as unknown as P))
}
}, []) // eslint-disable-line
return {
state,
data: state.data,
loading: state.loading,
error: state.error,
initialized: state.initialized,
run,
runLoading,
cancel,
refresh,
mutate,
params,
}
}