feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
28
src_bak/hooks/index.ts
Normal file
28
src_bak/hooks/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Context Hooks
|
||||
export { useAppContext } from './useAppContext'
|
||||
export { useUser } from './useUser'
|
||||
export { useCartContext } from '../contexts/CartContext'
|
||||
|
||||
// Theme
|
||||
export { useTheme } from './useTheme'
|
||||
|
||||
// Utility Hooks
|
||||
export { useCountDown } from './useCountDown'
|
||||
export type { CountDownResult } from './useCountDown'
|
||||
|
||||
export { useCountUp } from './useCountUp'
|
||||
export type { CountUpResult, CountUpOptions } from './useCountUp'
|
||||
|
||||
export { useRequest } from './useRequest'
|
||||
export type {
|
||||
RequestState,
|
||||
UseRequestOptions,
|
||||
UseRequestReturn,
|
||||
} from './useRequest'
|
||||
|
||||
// Feature Hooks
|
||||
export { useAddress } from './useAddress'
|
||||
export { useCoupon } from './useCoupon'
|
||||
export { usePagination } from './usePagination'
|
||||
export { usePayment } from './usePayment'
|
||||
export { useScrollHeight } from './useScrollHeight'
|
||||
98
src_bak/hooks/useAddress.ts
Normal file
98
src_bak/hooks/useAddress.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import {
|
||||
listShopUserAddress,
|
||||
getShopUserAddress,
|
||||
addShopUserAddress,
|
||||
updateShopUserAddress,
|
||||
removeShopUserAddress,
|
||||
setDefaultAddress,
|
||||
} from '@/api/shop/shopUserAddress'
|
||||
import type { ShopUserAddress } from '@/api/shop/shopUserAddress/model'
|
||||
|
||||
interface UseAddressReturn {
|
||||
addresses: ShopUserAddress[]
|
||||
loading: boolean
|
||||
defaultAddress: ShopUserAddress | null
|
||||
loadAddresses: () => Promise<void>
|
||||
getAddress: (id: number) => Promise<ShopUserAddress | null>
|
||||
addAddress: (data: Partial<ShopUserAddress>) => Promise<boolean>
|
||||
updateAddress: (data: ShopUserAddress) => Promise<boolean>
|
||||
deleteAddress: (id: number) => Promise<boolean>
|
||||
setDefault: (id: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
export function useAddress(): UseAddressReturn {
|
||||
const [addresses, setAddresses] = useState<ShopUserAddress[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const loadAddresses = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const list = await listShopUserAddress()
|
||||
// 调试日志:可看到解析后的地址数量与首项
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[useAddress] loaded addresses:', Array.isArray(list) ? list.length : 0,
|
||||
list && (list as any[]).length > 0 ? JSON.stringify(list[0])?.substring(0, 200) : '(empty)')
|
||||
setAddresses(Array.isArray(list) ? list : [])
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[useAddress] Load addresses error:', error?.message || error)
|
||||
setAddresses([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 根据 ID 获取单个地址 */
|
||||
const getAddress = useCallback(async (id: number): Promise<ShopUserAddress | null> => {
|
||||
try {
|
||||
return await getShopUserAddress(id)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const addAddress = useCallback(async (data: Partial<ShopUserAddress>): Promise<boolean> => {
|
||||
try {
|
||||
await addShopUserAddress(data as any)
|
||||
await loadAddresses()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [loadAddresses])
|
||||
|
||||
const updateAddress = useCallback(async (data: ShopUserAddress): Promise<boolean> => {
|
||||
try {
|
||||
await updateShopUserAddress(data)
|
||||
await loadAddresses()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [loadAddresses])
|
||||
|
||||
const deleteAddress = useCallback(async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
await removeShopUserAddress(id)
|
||||
setAddresses(prev => prev.filter(a => a.id !== id))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setDefault = useCallback(async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
await setDefaultAddress(id)
|
||||
await loadAddresses()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [loadAddresses])
|
||||
|
||||
const defaultAddress = addresses.find(a => a.isDefault) || addresses[0] || null
|
||||
|
||||
return { addresses, loading, defaultAddress, loadAddresses, getAddress, addAddress, updateAddress, deleteAddress, setDefault }
|
||||
}
|
||||
12
src_bak/hooks/useAppContext.ts
Normal file
12
src_bak/hooks/useAppContext.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react'
|
||||
import AppContext from '@/contexts/AppContext'
|
||||
|
||||
export const useAppContext = () => {
|
||||
const context = useContext(AppContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAppContext 必须在 AppContext.Provider 内使用')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
145
src_bak/hooks/useCountDown.ts
Normal file
145
src_bak/hooks/useCountDown.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
export interface CountDownResult {
|
||||
/** 剩余秒数 */
|
||||
seconds: number
|
||||
/** 剩余分钟 */
|
||||
minutes: number
|
||||
/** 剩余小时 */
|
||||
hours: number
|
||||
/** 剩余天数 */
|
||||
days: number
|
||||
/** 总剩余秒数 */
|
||||
totalSeconds: number
|
||||
/** 是否在运行 */
|
||||
isRunning: boolean
|
||||
/** 是否已结束 */
|
||||
isExpired: boolean
|
||||
/** 格式化后的字符串 */
|
||||
formatted: string
|
||||
/** 暂停 */
|
||||
pause: () => void
|
||||
/** 继续 */
|
||||
resume: () => void
|
||||
/** 重置 */
|
||||
reset: (newTargetDate?: number) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 倒计时 Hook
|
||||
* @param targetDate 目标时间戳(毫秒),0 表示立即结束
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function useCountDown(
|
||||
targetDate: number,
|
||||
options: {
|
||||
/** 触发间隔(毫秒),默认 1000 */
|
||||
interval?: number
|
||||
/** 结束时的回调 */
|
||||
onComplete?: () => void
|
||||
/** 是否自动开始,默认 true */
|
||||
autoStart?: boolean
|
||||
/** 格式化函数,默认返回 HH:MM:SS 或 DD天HH:MM:SS */
|
||||
format?: (result: Pick<CountDownResult, 'days' | 'hours' | 'minutes' | 'seconds'>) => string
|
||||
} = {}
|
||||
): CountDownResult {
|
||||
const { interval = 1000, onComplete, autoStart = true, format } = options
|
||||
|
||||
const targetRef = useRef(targetDate)
|
||||
const onCompleteRef = useRef(onComplete)
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const [state, setState] = useState(() => calculateTimeLeft(targetDate))
|
||||
const [isRunning, setIsRunning] = useState(autoStart)
|
||||
|
||||
// 更新回调引用
|
||||
useEffect(() => {
|
||||
onCompleteRef.current = onComplete
|
||||
}, [onComplete])
|
||||
|
||||
// 更新目标时间
|
||||
useEffect(() => {
|
||||
targetRef.current = targetDate
|
||||
setState(calculateTimeLeft(targetDate))
|
||||
}, [targetDate])
|
||||
|
||||
// 计算剩余时间
|
||||
function calculateTimeLeft(target: number) {
|
||||
const now = Date.now()
|
||||
const diff = Math.max(0, Math.floor((target - now) / 1000))
|
||||
return {
|
||||
totalSeconds: diff,
|
||||
days: Math.floor(diff / 86400),
|
||||
hours: Math.floor((diff % 86400) / 3600),
|
||||
minutes: Math.floor((diff % 3600) / 60),
|
||||
seconds: diff % 60,
|
||||
}
|
||||
}
|
||||
|
||||
// 默认格式化
|
||||
const defaultFormat = useCallback((r: Pick<CountDownResult, 'days' | 'hours' | 'minutes' | 'seconds'>) => {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
if (r.days > 0) {
|
||||
return `${r.days}天${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}`
|
||||
}
|
||||
return `${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}`
|
||||
}, [])
|
||||
|
||||
// 启动定时器
|
||||
const startInterval = useCallback(() => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
intervalRef.current = setInterval(() => {
|
||||
const left = calculateTimeLeft(targetRef.current)
|
||||
setState(left)
|
||||
if (left.totalSeconds <= 0) {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
setIsRunning(false)
|
||||
onCompleteRef.current?.()
|
||||
}
|
||||
}, interval)
|
||||
}, [interval])
|
||||
|
||||
// 控制定时器
|
||||
useEffect(() => {
|
||||
if (isRunning) {
|
||||
// 立即执行一次
|
||||
const left = calculateTimeLeft(targetRef.current)
|
||||
setState(left)
|
||||
if (left.totalSeconds <= 0) {
|
||||
setIsRunning(false)
|
||||
onCompleteRef.current?.()
|
||||
} else {
|
||||
startInterval()
|
||||
}
|
||||
} else {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
}
|
||||
}, [isRunning, startInterval])
|
||||
|
||||
const pause = useCallback(() => setIsRunning(false), [])
|
||||
const resume = useCallback(() => {
|
||||
if (state.totalSeconds > 0) setIsRunning(true)
|
||||
}, [state.totalSeconds])
|
||||
const reset = useCallback((newTarget?: number) => {
|
||||
const target = newTarget ?? targetDate
|
||||
targetRef.current = target
|
||||
setState(calculateTimeLeft(target))
|
||||
setIsRunning(true)
|
||||
}, [targetDate])
|
||||
|
||||
return {
|
||||
...state,
|
||||
isRunning,
|
||||
isExpired: state.totalSeconds <= 0,
|
||||
formatted: (format ?? defaultFormat)(state),
|
||||
pause,
|
||||
resume,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
130
src_bak/hooks/useCountUp.ts
Normal file
130
src_bak/hooks/useCountUp.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
export interface CountUpResult {
|
||||
/** 当前秒数 */
|
||||
seconds: number
|
||||
/** 当前分钟 */
|
||||
minutes: number
|
||||
/** 当前小时 */
|
||||
hours: number
|
||||
/** 总秒数 */
|
||||
totalSeconds: number
|
||||
/** 是否在运行 */
|
||||
isRunning: boolean
|
||||
/** 格式化后的字符串 */
|
||||
formatted: string
|
||||
/** 暂停 */
|
||||
pause: () => void
|
||||
/** 继续 */
|
||||
resume: () => void
|
||||
/** 重置 */
|
||||
reset: (newStartFrom?: number) => void
|
||||
}
|
||||
|
||||
export interface CountUpOptions {
|
||||
/** 起始值(秒),默认 0 */
|
||||
startFrom?: number
|
||||
/** 触发间隔(毫秒),默认 1000 */
|
||||
interval?: number
|
||||
/** 最大值,超出后停止 */
|
||||
maxValue?: number
|
||||
/** 格式化函数 */
|
||||
format?: (result: Pick<CountUpResult, 'hours' | 'minutes' | 'seconds'>) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* 正向计时 Hook
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function useCountUp(options: CountUpOptions = {}): CountUpResult {
|
||||
const { startFrom = 0, interval = 1000, maxValue, format } = options
|
||||
|
||||
const startRef = useRef(startFrom)
|
||||
const maxRef = useRef(maxValue)
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const [totalSeconds, setTotalSeconds] = useState(startFrom)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
|
||||
// 计算时分秒
|
||||
const calcTime = useCallback((total: number) => ({
|
||||
totalSeconds: total,
|
||||
hours: Math.floor(total / 3600),
|
||||
minutes: Math.floor((total % 3600) / 60),
|
||||
seconds: total % 60,
|
||||
}), [])
|
||||
|
||||
const [time, setTime] = useState(() => calcTime(startFrom))
|
||||
|
||||
// 默认格式化
|
||||
const defaultFormat = useCallback((r: Pick<CountUpResult, 'hours' | 'minutes' | 'seconds'>) => {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${pad(r.hours)}:${pad(r.minutes)}:${pad(r.seconds)}`
|
||||
}, [])
|
||||
|
||||
const startInterval = useCallback(() => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
intervalRef.current = setInterval(() => {
|
||||
setTotalSeconds(prev => {
|
||||
const next = prev + Math.floor(interval / 1000)
|
||||
const max = maxRef.current
|
||||
if (max !== undefined && next >= max) {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
setIsRunning(false)
|
||||
return max
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, interval)
|
||||
}, [interval])
|
||||
|
||||
// 更新 refs
|
||||
useEffect(() => {
|
||||
startRef.current = startFrom
|
||||
setTotalSeconds(startFrom)
|
||||
setTime(calcTime(startFrom))
|
||||
}, [startFrom, calcTime])
|
||||
|
||||
useEffect(() => {
|
||||
maxRef.current = maxValue
|
||||
}, [maxValue])
|
||||
|
||||
// 同步 totalSeconds 到 time
|
||||
useEffect(() => {
|
||||
setTime(calcTime(totalSeconds))
|
||||
}, [totalSeconds, calcTime])
|
||||
|
||||
// 控制定时器
|
||||
useEffect(() => {
|
||||
if (isRunning) {
|
||||
startInterval()
|
||||
} else {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
}
|
||||
}, [isRunning, startInterval])
|
||||
|
||||
const pause = useCallback(() => setIsRunning(false), [])
|
||||
const resume = useCallback(() => setIsRunning(true), [])
|
||||
const reset = useCallback((newStart?: number) => {
|
||||
const start = newStart ?? startFrom
|
||||
startRef.current = start
|
||||
setTotalSeconds(start)
|
||||
setTime(calcTime(start))
|
||||
setIsRunning(true)
|
||||
}, [startFrom, calcTime])
|
||||
|
||||
return {
|
||||
...time,
|
||||
isRunning,
|
||||
formatted: (format ?? defaultFormat)(time),
|
||||
pause,
|
||||
resume,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
104
src_bak/hooks/useCoupon.ts
Normal file
104
src_bak/hooks/useCoupon.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import {
|
||||
listShopUserCoupon,
|
||||
pageShopUserCoupon,
|
||||
removeShopUserCoupon,
|
||||
getMyAvailableCoupons,
|
||||
getMyUsedCoupons,
|
||||
getMyExpiredCoupons,
|
||||
} from '@/api/shop/shopUserCoupon'
|
||||
import type { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
|
||||
|
||||
export type CouponTab = 'available' | 'used' | 'expired'
|
||||
|
||||
interface UseCouponReturn {
|
||||
coupons: ShopUserCoupon[]
|
||||
loading: boolean
|
||||
tab: CouponTab
|
||||
setTab: (tab: CouponTab) => void
|
||||
loadCoupons: () => Promise<void>
|
||||
deleteCoupon: (id: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
export function useCoupon(): UseCouponReturn {
|
||||
const [coupons, setCoupons] = useState<ShopUserCoupon[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [tab, setTab] = useState<CouponTab>('available')
|
||||
|
||||
const loadCoupons = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
let list: ShopUserCoupon[] = []
|
||||
switch (tab) {
|
||||
case 'available':
|
||||
list = await getMyAvailableCoupons()
|
||||
break
|
||||
case 'used':
|
||||
list = await getMyUsedCoupons()
|
||||
break
|
||||
case 'expired':
|
||||
list = await getMyExpiredCoupons()
|
||||
break
|
||||
}
|
||||
setCoupons(list || [])
|
||||
} catch (error) {
|
||||
console.error('Load coupons error:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
const deleteCoupon = useCallback(async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
await removeShopUserCoupon(Number(id))
|
||||
setCoupons(prev => prev.filter(c => c.id !== id))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { coupons, loading, tab, setTab, loadCoupons, deleteCoupon }
|
||||
}
|
||||
|
||||
// 获取优惠券类型文本
|
||||
export function getCouponTypeText(type?: number): string {
|
||||
switch (type) {
|
||||
case 10: return '满减券'
|
||||
case 20: return '折扣券'
|
||||
case 30: return '免费券'
|
||||
case 40: return '无门槛券'
|
||||
case 50: return '场地使用券'
|
||||
default: return '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券状态文本
|
||||
export function getCouponStatusText(coupon: ShopUserCoupon): string {
|
||||
if (coupon.status === 1) return '已使用'
|
||||
if (coupon.status === 2 || coupon.isExpire === 1) return '已过期'
|
||||
return '未使用'
|
||||
}
|
||||
|
||||
// 格式化优惠券金额
|
||||
export function formatCouponValue(coupon: ShopUserCoupon): string {
|
||||
switch (coupon.type) {
|
||||
case 20:
|
||||
// 折扣券
|
||||
return `${coupon.discount || 0}折`
|
||||
case 40:
|
||||
// 无门槛券
|
||||
return `¥${coupon.reducePrice || '0'}`
|
||||
case 50:
|
||||
// 场地使用券
|
||||
return coupon.useCount && coupon.useCount > 0 ? `${coupon.useCount}次` : '不限次'
|
||||
default:
|
||||
// 满减券/免费券
|
||||
return `¥${coupon.reducePrice || '0'}`
|
||||
}
|
||||
}
|
||||
|
||||
// 检查优惠券是否即将过期(3天内)
|
||||
export function isExpiringSoon(coupon: ShopUserCoupon): boolean {
|
||||
return coupon.isExpiringSoon === true || (coupon.daysRemaining !== undefined && coupon.daysRemaining <= 3)
|
||||
}
|
||||
110
src_bak/hooks/usePagination.ts
Normal file
110
src_bak/hooks/usePagination.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import type { PageParam, PageResult } from '@/api/index'
|
||||
|
||||
interface UsePaginationOptions<T> {
|
||||
api: (params: PageParam) => Promise<PageResult<T>>
|
||||
defaultParams?: Partial<PageParam>
|
||||
pageSize?: number
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
interface UsePaginationReturn<T> {
|
||||
list: T[]
|
||||
loading: boolean
|
||||
refreshing: boolean
|
||||
loadingMore: boolean
|
||||
finished: boolean
|
||||
total: number
|
||||
params: PageParam
|
||||
run: (extraParams?: Record<string, any>) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
loadMore: () => Promise<void>
|
||||
reset: () => void
|
||||
setParams: (params: Partial<PageParam>) => void
|
||||
}
|
||||
|
||||
export function usePagination<T>(options: UsePaginationOptions<T>): UsePaginationReturn<T> {
|
||||
const { api, defaultParams = {}, pageSize = 10, immediate = false } = options
|
||||
|
||||
const [list, setList] = useState<T[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [params, setParamsState] = useState<PageParam>({
|
||||
page: 1,
|
||||
limit: pageSize,
|
||||
...defaultParams,
|
||||
})
|
||||
|
||||
const setParams = (newParams: Partial<PageParam>) => {
|
||||
setParamsState(prev => ({ ...prev, ...newParams }))
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async (page: number, extraParams?: Record<string, any>) => {
|
||||
const result = await api({ ...params, ...extraParams, page, limit: pageSize })
|
||||
setList(result?.list || [])
|
||||
setTotal(result?.count || 0)
|
||||
setFinished((result?.list || []).length < pageSize)
|
||||
return result
|
||||
}, [api, params, pageSize])
|
||||
|
||||
const run = useCallback(async (extraParams?: Record<string, any>) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await fetchData(1, extraParams)
|
||||
setParamsState(prev => ({ ...prev, page: 1 }))
|
||||
} catch (error) {
|
||||
console.error('Pagination run error:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fetchData])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (refreshing) return
|
||||
setRefreshing(true)
|
||||
setFinished(false)
|
||||
try {
|
||||
await fetchData(1)
|
||||
setParamsState(prev => ({ ...prev, page: 1 }))
|
||||
} catch (error) {
|
||||
console.error('Refresh error:', error)
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}, [fetchData, refreshing])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loadingMore || finished) return
|
||||
setLoadingMore(true)
|
||||
try {
|
||||
const nextPage = (params.page || 1) + 1
|
||||
const result = await api({ ...params, page: nextPage, limit: pageSize })
|
||||
const newList = result?.list || []
|
||||
setList(prev => [...prev, ...newList])
|
||||
setTotal(result?.count || 0)
|
||||
setFinished(newList.length < pageSize)
|
||||
setParamsState(prev => ({ ...prev, page: nextPage }))
|
||||
} catch (error) {
|
||||
console.error('LoadMore error:', error)
|
||||
} finally {
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [api, params, pageSize, loadingMore, finished])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setList([])
|
||||
setFinished(false)
|
||||
setTotal(0)
|
||||
setParamsState(prev => ({ ...prev, page: 1 }))
|
||||
}, [])
|
||||
|
||||
// 立即执行
|
||||
if (immediate && list.length === 0 && !loading && !refreshing) {
|
||||
run()
|
||||
}
|
||||
|
||||
return { list, loading, refreshing, loadingMore, finished, total, params, run, refresh, loadMore, reset, setParams }
|
||||
}
|
||||
77
src_bak/hooks/usePayment.ts
Normal file
77
src_bak/hooks/usePayment.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
interface UsePaymentOptions {
|
||||
onSuccess?: (orderNo: string) => void
|
||||
onFail?: (error: any) => void
|
||||
}
|
||||
|
||||
interface UsePaymentReturn {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
requestPayment: (payParams: {
|
||||
timeStamp: string
|
||||
nonceStr: string
|
||||
package: string
|
||||
signType: string
|
||||
paySign: string
|
||||
orderNo?: string
|
||||
}) => Promise<boolean>
|
||||
createAndPay: (createOrderFn: () => Promise<any>) => Promise<boolean>
|
||||
}
|
||||
|
||||
export function usePayment(options: UsePaymentOptions = {}): UsePaymentReturn {
|
||||
const { onSuccess, onFail } = options
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const requestPayment = useCallback(async (payParams: {
|
||||
timeStamp: string
|
||||
nonceStr: string
|
||||
package: string
|
||||
signType: string
|
||||
paySign: string
|
||||
orderNo?: string
|
||||
}): Promise<boolean> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: payParams.timeStamp,
|
||||
nonceStr: payParams.nonceStr,
|
||||
package: payParams.package,
|
||||
signType: payParams.signType as any,
|
||||
paySign: payParams.paySign,
|
||||
})
|
||||
payParams.orderNo && onSuccess?.(payParams.orderNo)
|
||||
return true
|
||||
} catch (err: any) {
|
||||
if (err.errMsg?.includes('cancel')) {
|
||||
setError('支付已取消')
|
||||
} else {
|
||||
setError(err.errMsg || '支付失败')
|
||||
onFail?.(err)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [onSuccess, onFail])
|
||||
|
||||
const createAndPay = useCallback(async (createOrderFn: () => Promise<any>): Promise<boolean> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await createOrderFn()
|
||||
return await requestPayment(result)
|
||||
} catch (err: any) {
|
||||
setError(err.message || '创建订单失败')
|
||||
onFail?.(err)
|
||||
return false
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [requestPayment, onFail])
|
||||
|
||||
return { loading, error, requestPayment, createAndPay }
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
24
src_bak/hooks/useScrollHeight.ts
Normal file
24
src_bak/hooks/useScrollHeight.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
/**
|
||||
* 获取可滚动区域高度,替代 calc(100vh - Npx)
|
||||
* 小程序不支持 vh/vw 单位,此 hook 动态计算可用高度
|
||||
* @param minus 需要减去的高度(rpx 或 px),默认 0
|
||||
*/
|
||||
export function useScrollHeight(minus: number = 0) {
|
||||
const [height, setHeight] = useState('100vh')
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
// windowHeight 已经减去了导航栏等系统 UI
|
||||
const available = systemInfo.windowHeight - minus
|
||||
setHeight(`${available}px`)
|
||||
} catch {
|
||||
setHeight('100vh')
|
||||
}
|
||||
}, [minus])
|
||||
|
||||
return height
|
||||
}
|
||||
42
src_bak/hooks/useTheme.ts
Normal file
42
src_bak/hooks/useTheme.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useContext } from 'react'
|
||||
import AppContext from '@/contexts/AppContext'
|
||||
|
||||
export interface ThemeConfig {
|
||||
/** 当前主题 */
|
||||
theme: 'light' | 'dark'
|
||||
/** 是否为深色模式 */
|
||||
isDark: boolean
|
||||
/** 是否为浅色模式 */
|
||||
isLight: boolean
|
||||
/** 切换主题 */
|
||||
toggleTheme: () => void
|
||||
/** 设置为浅色主题 */
|
||||
setLight: () => void
|
||||
/** 设置为深色主题 */
|
||||
setDark: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题 Hook
|
||||
* 从 AppContext 获取主题状态和切换函数
|
||||
*/
|
||||
export const useTheme = (): ThemeConfig => {
|
||||
const { theme, toggleTheme } = useContext(AppContext)
|
||||
|
||||
const setLight = () => {
|
||||
if (theme !== 'light') toggleTheme()
|
||||
}
|
||||
|
||||
const setDark = () => {
|
||||
if (theme !== 'dark') toggleTheme()
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
isDark: theme === 'dark',
|
||||
isLight: theme === 'light',
|
||||
toggleTheme,
|
||||
setLight,
|
||||
setDark,
|
||||
}
|
||||
}
|
||||
81
src_bak/hooks/useUser.ts
Normal file
81
src_bak/hooks/useUser.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useUserContext } from '@/contexts/UserContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { User } from '@/api/system/user/model'
|
||||
import { loginByOpenId } from '@/api/layout'
|
||||
import { TenantId } from '@/config/app'
|
||||
|
||||
/**
|
||||
* useUser - 兼容性 Hook
|
||||
* 优先使用 UserContext,在未包裹 UserProvider 时降级为本地状态
|
||||
*/
|
||||
export const useUser = () => {
|
||||
try {
|
||||
const ctx = useUserContext()
|
||||
return ctx
|
||||
} catch {
|
||||
// 未在 UserProvider 中,使用降级逻辑
|
||||
return useUserFallback()
|
||||
}
|
||||
}
|
||||
|
||||
/** 降级方案:不依赖 Context 的本地用户状态 */
|
||||
const useUserFallback = () => {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const loadFromStorage = () => {
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const userData = Taro.getStorageSync('User')
|
||||
if (token && userData) {
|
||||
const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData
|
||||
setUser(parsed as User)
|
||||
return true
|
||||
}
|
||||
return !!token
|
||||
}
|
||||
|
||||
const loginUser = (token: string, userInfo: User) => {
|
||||
Taro.setStorageSync('access_token', token)
|
||||
Taro.setStorageSync('User', userInfo)
|
||||
setUser(userInfo)
|
||||
}
|
||||
|
||||
const logoutUser = () => {
|
||||
Taro.removeStorageSync('access_token')
|
||||
Taro.removeStorageSync('User')
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
const refreshUser = async () => {
|
||||
// 降级方案不自动刷新
|
||||
return user
|
||||
}
|
||||
|
||||
const syncFromStorage = (): boolean => {
|
||||
return loadFromStorage()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const hasLocal = loadFromStorage()
|
||||
if (!hasLocal) {
|
||||
try {
|
||||
const data = await new Promise<any>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (res) => loginByOpenId({ code: res.code, tenantId: TenantId }).then(resolve).catch(reject),
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
if (data?.access_token && data?.user) {
|
||||
loginUser(data.access_token, data.user)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
init()
|
||||
}, [])
|
||||
|
||||
return { user, isLoggedIn: !!user, loading, loginUser, logoutUser, refreshUser, syncFromStorage }
|
||||
}
|
||||
Reference in New Issue
Block a user