Browse Source
- 新增 PaymentCountdown 组件用于显示支付倒计时 - 实现 usePaymentCountdown Hook 以支持倒计时逻辑 - 添加 useOrderStats Hook 用于获取订单统计信息 - 在订单列表和详情页面集成支付倒计时功能 - 优化订单状态显示和相关操作逻辑dev
8 changed files with 833 additions and 50 deletions
@ -0,0 +1,168 @@ |
|||||
|
# PaymentCountdown 支付倒计时组件 |
||||
|
|
||||
|
基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。 |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
- ✅ **双模式支持**:静态显示(列表页)和实时更新(详情页) |
||||
|
- ✅ **智能状态判断**:自动判断紧急程度并应用不同样式 |
||||
|
- ✅ **过期自动处理**:倒计时结束后触发回调 |
||||
|
- ✅ **灵活样式**:支持徽章模式和纯文本模式 |
||||
|
- ✅ **性能优化**:避免不必要的重渲染 |
||||
|
|
||||
|
## 使用方法 |
||||
|
|
||||
|
### 基础用法 |
||||
|
|
||||
|
```tsx |
||||
|
import PaymentCountdown from '@/components/PaymentCountdown'; |
||||
|
|
||||
|
// 订单列表页 - 静态显示 |
||||
|
<PaymentCountdown |
||||
|
createTime={order.createTime} |
||||
|
payStatus={order.payStatus} |
||||
|
realTime={false} |
||||
|
mode="badge" |
||||
|
/> |
||||
|
|
||||
|
// 订单详情页 - 实时更新 |
||||
|
<PaymentCountdown |
||||
|
createTime={order.createTime} |
||||
|
payStatus={order.payStatus} |
||||
|
realTime={true} |
||||
|
showSeconds={true} |
||||
|
mode="badge" |
||||
|
onExpired={() => { |
||||
|
console.log('支付已过期'); |
||||
|
}} |
||||
|
/> |
||||
|
``` |
||||
|
|
||||
|
### 高级用法 |
||||
|
|
||||
|
```tsx |
||||
|
// 自定义超时时间(12小时) |
||||
|
<PaymentCountdown |
||||
|
createTime={order.createTime} |
||||
|
payStatus={order.payStatus} |
||||
|
realTime={true} |
||||
|
timeoutHours={12} |
||||
|
showSeconds={true} |
||||
|
mode="badge" |
||||
|
className="custom-countdown" |
||||
|
onExpired={handlePaymentExpired} |
||||
|
/> |
||||
|
|
||||
|
// 纯文本模式 |
||||
|
<PaymentCountdown |
||||
|
createTime={order.createTime} |
||||
|
payStatus={order.payStatus} |
||||
|
realTime={false} |
||||
|
mode="text" |
||||
|
/> |
||||
|
``` |
||||
|
|
||||
|
## API 参数 |
||||
|
|
||||
|
| 参数 | 类型 | 默认值 | 说明 | |
||||
|
|------|------|--------|------| |
||||
|
| createTime | string | - | 订单创建时间 | |
||||
|
| payStatus | boolean | false | 支付状态 | |
||||
|
| realTime | boolean | false | 是否实时更新 | |
||||
|
| timeoutHours | number | 24 | 超时小时数 | |
||||
|
| showSeconds | boolean | false | 是否显示秒数 | |
||||
|
| className | string | '' | 自定义样式类名 | |
||||
|
| onExpired | function | - | 过期回调函数 | |
||||
|
| mode | 'badge' \| 'text' | 'badge' | 显示模式 | |
||||
|
|
||||
|
## 样式状态 |
||||
|
|
||||
|
### 正常状态 |
||||
|
- 红色渐变背景 |
||||
|
- 白色文字 |
||||
|
- 轻微阴影效果 |
||||
|
|
||||
|
### 紧急状态(< 1小时) |
||||
|
- 更深的红色背景 |
||||
|
- 脉冲动画效果 |
||||
|
|
||||
|
### 非常紧急状态(< 10分钟) |
||||
|
- 最深的红色背景 |
||||
|
- 快速闪烁动画 |
||||
|
|
||||
|
### 过期状态 |
||||
|
- 灰色背景 |
||||
|
- 无动画效果 |
||||
|
|
||||
|
## Hook 使用 |
||||
|
|
||||
|
如果需要单独使用倒计时逻辑,可以直接使用 Hook: |
||||
|
|
||||
|
```tsx |
||||
|
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown'; |
||||
|
|
||||
|
const MyComponent = ({ order }) => { |
||||
|
const timeLeft = usePaymentCountdown( |
||||
|
order.createTime, |
||||
|
order.payStatus, |
||||
|
true, // 实时更新 |
||||
|
24 // 24小时超时 |
||||
|
); |
||||
|
|
||||
|
const countdownText = formatCountdownText(timeLeft, true); |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
剩余时间:{countdownText} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## 工具函数 |
||||
|
|
||||
|
```tsx |
||||
|
import { |
||||
|
formatCountdownText, |
||||
|
isUrgentCountdown, |
||||
|
isCriticalCountdown |
||||
|
} from '@/hooks/usePaymentCountdown'; |
||||
|
|
||||
|
// 格式化倒计时文本 |
||||
|
const text = formatCountdownText(timeLeft, true); // "2小时30分15秒" |
||||
|
|
||||
|
// 判断是否紧急 |
||||
|
const isUrgent = isUrgentCountdown(timeLeft); // < 1小时 |
||||
|
|
||||
|
// 判断是否非常紧急 |
||||
|
const isCritical = isCriticalCountdown(timeLeft); // < 10分钟 |
||||
|
``` |
||||
|
|
||||
|
## 注意事项 |
||||
|
|
||||
|
1. **性能考虑**:列表页建议使用 `realTime={false}` 避免过多定时器 |
||||
|
2. **内存泄漏**:组件会自动清理定时器,无需手动处理 |
||||
|
3. **时区问题**:确保 `createTime` 格式正确,建议使用 ISO 格式 |
||||
|
4. **过期处理**:`onExpired` 回调只在实时模式下触发 |
||||
|
|
||||
|
## 样式定制 |
||||
|
|
||||
|
可以通过 CSS 变量或覆盖样式类来自定义外观: |
||||
|
|
||||
|
```scss |
||||
|
.custom-countdown { |
||||
|
.payment-countdown-badge { |
||||
|
background: linear-gradient(135deg, #your-color-1, #your-color-2); |
||||
|
border-radius: 8px; |
||||
|
|
||||
|
&.urgent { |
||||
|
animation: customPulse 1.5s infinite; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes customPulse { |
||||
|
0%, 100% { opacity: 1; } |
||||
|
50% { opacity: 0.8; } |
||||
|
} |
||||
|
``` |
@ -0,0 +1,170 @@ |
|||||
|
/* 支付倒计时样式 */ |
||||
|
|
||||
|
/* 徽章模式样式 */ |
||||
|
.payment-countdown-badge { |
||||
|
display: inline-block; |
||||
|
background: linear-gradient(135deg, #ff4757, #ff3838); |
||||
|
color: white; |
||||
|
padding: 4px 8px; |
||||
|
border-radius: 12px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3); |
||||
|
margin-left: 8px; |
||||
|
|
||||
|
.countdown-text { |
||||
|
color: white; |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
/* 紧急状态(少于1小时) */ |
||||
|
&.urgent { |
||||
|
background: linear-gradient(135deg, #ff6b6b, #ee5a52); |
||||
|
animation: pulse 2s infinite; |
||||
|
} |
||||
|
|
||||
|
/* 非常紧急状态(少于10分钟) */ |
||||
|
&.critical { |
||||
|
background: linear-gradient(135deg, #ff4757, #c44569); |
||||
|
animation: flash 1s infinite; |
||||
|
} |
||||
|
|
||||
|
/* 过期状态 */ |
||||
|
&.expired { |
||||
|
background: linear-gradient(135deg, #95a5a6, #7f8c8d); |
||||
|
animation: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 纯文本模式样式 */ |
||||
|
.payment-countdown-text { |
||||
|
color: #ff4757; |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
|
||||
|
/* 紧急状态 */ |
||||
|
&.urgent { |
||||
|
color: #ff6b6b; |
||||
|
animation: textPulse 2s infinite; |
||||
|
} |
||||
|
|
||||
|
/* 非常紧急状态 */ |
||||
|
&.critical { |
||||
|
color: #ff4757; |
||||
|
animation: textFlash 1s infinite; |
||||
|
} |
||||
|
|
||||
|
/* 过期状态 */ |
||||
|
&.expired { |
||||
|
color: #95a5a6; |
||||
|
animation: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 动画效果 */ |
||||
|
@keyframes pulse { |
||||
|
0% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
50% { |
||||
|
opacity: 0.8; |
||||
|
transform: scale(1.02); |
||||
|
} |
||||
|
100% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes flash { |
||||
|
0% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
25% { |
||||
|
opacity: 0.7; |
||||
|
transform: scale(1.05); |
||||
|
} |
||||
|
50% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
75% { |
||||
|
opacity: 0.7; |
||||
|
transform: scale(1.05); |
||||
|
} |
||||
|
100% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes textPulse { |
||||
|
0% { opacity: 1; } |
||||
|
50% { opacity: 0.7; } |
||||
|
100% { opacity: 1; } |
||||
|
} |
||||
|
|
||||
|
@keyframes textFlash { |
||||
|
0% { opacity: 1; } |
||||
|
25% { opacity: 0.5; } |
||||
|
50% { opacity: 1; } |
||||
|
75% { opacity: 0.5; } |
||||
|
100% { opacity: 1; } |
||||
|
} |
||||
|
|
||||
|
/* 响应式调整 */ |
||||
|
@media (max-width: 375px) { |
||||
|
.payment-countdown-badge { |
||||
|
font-size: 11px; |
||||
|
padding: 3px 6px; |
||||
|
|
||||
|
.countdown-text { |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.payment-countdown-text { |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 详情页专用样式 */ |
||||
|
.order-detail-countdown { |
||||
|
.payment-countdown-badge { |
||||
|
font-size: 14px; |
||||
|
padding: 6px 12px; |
||||
|
border-radius: 16px; |
||||
|
margin: 8px 0; |
||||
|
|
||||
|
.countdown-text { |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.payment-countdown-text { |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 列表页专用样式 */ |
||||
|
.order-list-countdown { |
||||
|
.payment-countdown-badge { |
||||
|
font-size: 11px; |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 10px; |
||||
|
margin-left: 6px; |
||||
|
|
||||
|
.countdown-text { |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.payment-countdown-text { |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,89 @@ |
|||||
|
import React from 'react'; |
||||
|
import { View, Text } from '@tarojs/components'; |
||||
|
import { |
||||
|
usePaymentCountdown, |
||||
|
formatCountdownText, |
||||
|
isUrgentCountdown, |
||||
|
isCriticalCountdown |
||||
|
} from '@/hooks/usePaymentCountdown'; |
||||
|
import './PaymentCountdown.scss'; |
||||
|
|
||||
|
export interface PaymentCountdownProps { |
||||
|
/** 订单创建时间 */ |
||||
|
createTime?: string; |
||||
|
/** 支付状态 */ |
||||
|
payStatus?: boolean; |
||||
|
/** 是否实时更新(详情页用true,列表页用false) */ |
||||
|
realTime?: boolean; |
||||
|
/** 超时小时数,默认24小时 */ |
||||
|
timeoutHours?: number; |
||||
|
/** 是否显示秒数 */ |
||||
|
showSeconds?: boolean; |
||||
|
/** 自定义样式类名 */ |
||||
|
className?: string; |
||||
|
/** 过期回调 */ |
||||
|
onExpired?: () => void; |
||||
|
/** 显示模式:badge(徽章模式) | text(纯文本模式) */ |
||||
|
mode?: 'badge' | 'text'; |
||||
|
} |
||||
|
|
||||
|
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({ |
||||
|
createTime, |
||||
|
payStatus = false, |
||||
|
realTime = false, |
||||
|
timeoutHours = 24, |
||||
|
showSeconds = false, |
||||
|
className = '', |
||||
|
onExpired, |
||||
|
mode = 'badge' |
||||
|
}) => { |
||||
|
const timeLeft = usePaymentCountdown(createTime, payStatus, realTime, timeoutHours); |
||||
|
|
||||
|
// 如果已支付或没有创建时间,不显示倒计时
|
||||
|
if (payStatus || !createTime) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 如果已过期,触发回调并显示过期状态
|
||||
|
if (timeLeft.isExpired) { |
||||
|
onExpired?.(); |
||||
|
if (mode === 'text') { |
||||
|
return ( |
||||
|
<Text className={`payment-countdown-text expired ${className}`}> |
||||
|
支付已过期 |
||||
|
</Text> |
||||
|
); |
||||
|
} |
||||
|
return ( |
||||
|
<View className={`payment-countdown-badge expired ${className}`}> |
||||
|
<Text className="countdown-text">支付已过期</Text> |
||||
|
</View> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 判断紧急程度
|
||||
|
const isUrgent = isUrgentCountdown(timeLeft); |
||||
|
const isCritical = isCriticalCountdown(timeLeft); |
||||
|
|
||||
|
// 格式化倒计时文本
|
||||
|
const countdownText = formatCountdownText(timeLeft, showSeconds); |
||||
|
const fullText = `等待付款 ${countdownText}`; |
||||
|
|
||||
|
// 纯文本模式
|
||||
|
if (mode === 'text') { |
||||
|
return ( |
||||
|
<Text className={`payment-countdown-text ${isUrgent ? 'urgent' : ''} ${isCritical ? 'critical' : ''} ${className}`}> |
||||
|
{fullText} |
||||
|
</Text> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 徽章模式
|
||||
|
return ( |
||||
|
<View className={`payment-countdown-badge ${isUrgent ? 'urgent' : ''} ${isCritical ? 'critical' : ''} ${className}`}> |
||||
|
<Text className="countdown-text">{fullText}</Text> |
||||
|
</View> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default PaymentCountdown; |
@ -0,0 +1,117 @@ |
|||||
|
import { useState, useEffect, useCallback } from 'react'; |
||||
|
import { UserOrderStats } from '@/api/user'; |
||||
|
import Taro from '@tarojs/taro'; |
||||
|
import {pageShopOrder} from "@/api/shop/shopOrder"; |
||||
|
|
||||
|
/** |
||||
|
* 订单统计Hook |
||||
|
* 用于管理用户订单各状态的数量统计 |
||||
|
*/ |
||||
|
export const useOrderStats = () => { |
||||
|
const [orderStats, setOrderStats] = useState<UserOrderStats>({ |
||||
|
pending: 0, // 待付款
|
||||
|
paid: 0, // 待发货
|
||||
|
shipped: 0, // 待收货
|
||||
|
completed: 0, // 已完成
|
||||
|
refund: 0, // 退货/售后
|
||||
|
total: 0 // 总订单数
|
||||
|
}); |
||||
|
|
||||
|
const [loading, setLoading] = useState(false); |
||||
|
const [error, setError] = useState<string | null>(null); |
||||
|
|
||||
|
/** |
||||
|
* 获取订单统计数据 |
||||
|
*/ |
||||
|
const fetchOrderStats = useCallback(async (showToast = false) => { |
||||
|
try { |
||||
|
setLoading(true); |
||||
|
setError(null); |
||||
|
|
||||
|
// TODO 读取订单数量
|
||||
|
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0}) |
||||
|
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1}) |
||||
|
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3}) |
||||
|
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5}) |
||||
|
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6}) |
||||
|
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')}) |
||||
|
setOrderStats({ |
||||
|
pending: pending?.count || 0, |
||||
|
paid: paid?.count || 0, |
||||
|
shipped: shipped?.count || 0, |
||||
|
completed: completed?.count || 0, |
||||
|
refund: refund?.count || 0, |
||||
|
total: total?.count || 0 |
||||
|
}) |
||||
|
|
||||
|
if (showToast) { |
||||
|
Taro.showToast({ |
||||
|
title: '数据已更新', |
||||
|
icon: 'success', |
||||
|
duration: 1500 |
||||
|
}); |
||||
|
} |
||||
|
} catch (err: any) { |
||||
|
const errorMessage = err.message || '获取订单统计失败'; |
||||
|
setError(errorMessage); |
||||
|
|
||||
|
console.error('获取订单统计失败:', err); |
||||
|
|
||||
|
if (showToast) { |
||||
|
Taro.showToast({ |
||||
|
title: errorMessage, |
||||
|
icon: 'error', |
||||
|
duration: 2000 |
||||
|
}); |
||||
|
} |
||||
|
} finally { |
||||
|
setLoading(false); |
||||
|
} |
||||
|
}, []); |
||||
|
|
||||
|
/** |
||||
|
* 刷新订单统计数据 |
||||
|
*/ |
||||
|
const refreshOrderStats = useCallback(() => { |
||||
|
return fetchOrderStats(true); |
||||
|
}, [fetchOrderStats]); |
||||
|
|
||||
|
/** |
||||
|
* 获取指定状态的订单数量 |
||||
|
*/ |
||||
|
const getOrderCount = useCallback((status: keyof UserOrderStats) => { |
||||
|
return orderStats[status] || 0; |
||||
|
}, [orderStats]); |
||||
|
|
||||
|
/** |
||||
|
* 检查是否有待处理的订单 |
||||
|
*/ |
||||
|
const hasPendingOrders = useCallback(() => { |
||||
|
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0; |
||||
|
}, [orderStats]); |
||||
|
|
||||
|
/** |
||||
|
* 获取总的待处理订单数量 |
||||
|
*/ |
||||
|
const getTotalPendingCount = useCallback(() => { |
||||
|
return orderStats.pending + orderStats.paid + orderStats.shipped; |
||||
|
}, [orderStats]); |
||||
|
|
||||
|
// 组件挂载时自动获取数据
|
||||
|
useEffect(() => { |
||||
|
fetchOrderStats(); |
||||
|
}, [fetchOrderStats]); |
||||
|
|
||||
|
return { |
||||
|
orderStats, |
||||
|
loading, |
||||
|
error, |
||||
|
fetchOrderStats, |
||||
|
refreshOrderStats, |
||||
|
getOrderCount, |
||||
|
hasPendingOrders, |
||||
|
getTotalPendingCount |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useOrderStats; |
@ -0,0 +1,163 @@ |
|||||
|
import { useState, useEffect, useMemo } from 'react'; |
||||
|
import dayjs from 'dayjs'; |
||||
|
import duration from 'dayjs/plugin/duration'; |
||||
|
|
||||
|
// 扩展dayjs支持duration
|
||||
|
dayjs.extend(duration); |
||||
|
|
||||
|
export interface CountdownTime { |
||||
|
hours: number; |
||||
|
minutes: number; |
||||
|
seconds: number; |
||||
|
isExpired: boolean; |
||||
|
totalMinutes: number; // 总剩余分钟数
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 支付倒计时Hook |
||||
|
* @param createTime 订单创建时间 |
||||
|
* @param payStatus 支付状态 |
||||
|
* @param realTime 是否实时更新(详情页用true,列表页用false) |
||||
|
* @param timeoutHours 超时小时数,默认24小时 |
||||
|
*/ |
||||
|
export const usePaymentCountdown = ( |
||||
|
createTime?: string, |
||||
|
payStatus?: boolean, |
||||
|
realTime: boolean = false, |
||||
|
timeoutHours: number = 24 |
||||
|
): CountdownTime => { |
||||
|
const [timeLeft, setTimeLeft] = useState<CountdownTime>({ |
||||
|
hours: 0, |
||||
|
minutes: 0, |
||||
|
seconds: 0, |
||||
|
isExpired: false, |
||||
|
totalMinutes: 0 |
||||
|
}); |
||||
|
|
||||
|
// 计算剩余时间的函数
|
||||
|
const calculateTimeLeft = useMemo(() => { |
||||
|
return (): CountdownTime => { |
||||
|
if (!createTime || payStatus) { |
||||
|
return { |
||||
|
hours: 0, |
||||
|
minutes: 0, |
||||
|
seconds: 0, |
||||
|
isExpired: false, |
||||
|
totalMinutes: 0 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const createTimeObj = dayjs(createTime); |
||||
|
const expireTime = createTimeObj.add(timeoutHours, 'hour'); |
||||
|
const now = dayjs(); |
||||
|
const diff = expireTime.diff(now); |
||||
|
|
||||
|
if (diff <= 0) { |
||||
|
return { |
||||
|
hours: 0, |
||||
|
minutes: 0, |
||||
|
seconds: 0, |
||||
|
isExpired: true, |
||||
|
totalMinutes: 0 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const durationObj = dayjs.duration(diff); |
||||
|
const hours = Math.floor(durationObj.asHours()); |
||||
|
const minutes = durationObj.minutes(); |
||||
|
const seconds = durationObj.seconds(); |
||||
|
const totalMinutes = Math.floor(durationObj.asMinutes()); |
||||
|
|
||||
|
return { |
||||
|
hours, |
||||
|
minutes, |
||||
|
seconds, |
||||
|
isExpired: false, |
||||
|
totalMinutes |
||||
|
}; |
||||
|
}; |
||||
|
}, [createTime, payStatus, timeoutHours]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!createTime || payStatus) { |
||||
|
setTimeLeft({ |
||||
|
hours: 0, |
||||
|
minutes: 0, |
||||
|
seconds: 0, |
||||
|
isExpired: false, |
||||
|
totalMinutes: 0 |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 立即计算一次
|
||||
|
const initialTime = calculateTimeLeft(); |
||||
|
setTimeLeft(initialTime); |
||||
|
|
||||
|
// 如果不需要实时更新,直接返回
|
||||
|
if (!realTime) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 如果需要实时更新,设置定时器
|
||||
|
const timer = setInterval(() => { |
||||
|
const newTimeLeft = calculateTimeLeft(); |
||||
|
setTimeLeft(newTimeLeft); |
||||
|
|
||||
|
// 如果已过期,清除定时器
|
||||
|
if (newTimeLeft.isExpired) { |
||||
|
clearInterval(timer); |
||||
|
} |
||||
|
}, 1000); |
||||
|
|
||||
|
return () => clearInterval(timer); |
||||
|
}, [createTime, payStatus, realTime, calculateTimeLeft]); |
||||
|
|
||||
|
return timeLeft; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 格式化倒计时文本 |
||||
|
* @param timeLeft 倒计时时间对象 |
||||
|
* @param showSeconds 是否显示秒数 |
||||
|
*/ |
||||
|
export const formatCountdownText = ( |
||||
|
timeLeft: CountdownTime, |
||||
|
showSeconds: boolean = false |
||||
|
): string => { |
||||
|
if (timeLeft.isExpired) { |
||||
|
return '已过期'; |
||||
|
} |
||||
|
|
||||
|
if (timeLeft.hours > 0) { |
||||
|
if (showSeconds) { |
||||
|
return `${timeLeft.hours}小时${timeLeft.minutes}分${timeLeft.seconds}秒`; |
||||
|
} else { |
||||
|
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`; |
||||
|
} |
||||
|
} else if (timeLeft.minutes > 0) { |
||||
|
if (showSeconds) { |
||||
|
return `${timeLeft.minutes}分${timeLeft.seconds}秒`; |
||||
|
} else { |
||||
|
return `${timeLeft.minutes}分钟`; |
||||
|
} |
||||
|
} else { |
||||
|
return `${timeLeft.seconds}秒`; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 判断是否为紧急状态(剩余时间少于1小时) |
||||
|
*/ |
||||
|
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => { |
||||
|
return !timeLeft.isExpired && timeLeft.totalMinutes < 60; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 判断是否为非常紧急状态(剩余时间少于10分钟) |
||||
|
*/ |
||||
|
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => { |
||||
|
return !timeLeft.isExpired && timeLeft.totalMinutes < 10; |
||||
|
}; |
||||
|
|
||||
|
export default usePaymentCountdown; |
Loading…
Reference in new issue