feat(user): 新增收货地址管理及售后申请页面

- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
2026-07-01 12:11:56 +08:00
parent bf6ed504cc
commit 1fa58040f3
636 changed files with 58878 additions and 716 deletions

View 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,
}
}