feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
325
src_bak/hooks/useRequest.ts
Normal file
325
src_bak/hooks/useRequest.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user