import { useCallback, useEffect, useRef, useState } from 'react'; import { View, Text } from '@tarojs/components'; import '../../styles/redeem.css'; import Taro, { useRouter } from '@tarojs/taro'; import { REDEEM_TOKEN_TTL_SECONDS } from '@dukang/shared-types'; import PageShell from '../../components/PageShell'; import RedeemQrCode from '../../components/RedeemQrCode'; import SubPageHeader from '../../components/SubPageHeader'; import { request, toast } from '../../lib/api'; import { formatMoney } from '../../lib/money'; import BenefitFigure from '../../components/BenefitFigure'; const POLL_INTERVAL_MS = 2500; const LAST_REDEEM_RECORD_KEY = 'lastRedeemRecordId'; const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult'; type RedeemTokenStatus = | { status: 'PENDING'; expireInSeconds: number; amount: number } | { status: 'CONSUMED'; record: { id: string; redeemNo: string; amount: number; storeId: string; storeName: string; createdAt: string; }; } | { status: 'EXPIRED' }; function formatTimer(seconds: number) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; } export default function RedeemCodePage() { const router = useRouter(); const token = decodeURIComponent(router.params.token ?? ''); const amount = Number(router.params.amount ?? 0); const [timerSec, setTimerSec] = useState(REDEEM_TOKEN_TTL_SECONDS); const timerRef = useRef | null>(null); const successHandled = useRef(false); const lastTokenTapAt = useRef(0); function onTokenTap() { if (!token) return; const now = Date.now(); if (now - lastTokenTapAt.current < 350) { lastTokenTapAt.current = 0; void Taro.setClipboardData({ data: token }) .then(() => toast('核销码编号已复制', 'success')) .catch(() => toast('复制失败')); return; } lastTokenTapAt.current = now; } const stopTimer = useCallback(() => { if (timerRef.current != null) { clearInterval(timerRef.current); timerRef.current = null; } }, []); const handleRedeemExpired = useCallback(() => { if (successHandled.current) return; successHandled.current = true; stopTimer(); toast('核销码已失效,请重新生成'); setTimeout(() => { Taro.navigateBack(); }, 1500); }, [stopTimer]); const handleRedeemSuccess = useCallback( (record: NonNullable['record']>) => { if (successHandled.current) return; successHandled.current = true; stopTimer(); Taro.setStorageSync(LAST_REDEEM_RECORD_KEY, record.id); Taro.setStorageSync(LAST_REDEEM_RESULT_KEY, JSON.stringify(record)); Taro.redirectTo({ url: `/pages/redeem-success/index?amount=${record.amount}`, }); }, [stopTimer], ); useEffect(() => { if (!token) { toast('核销码无效,请重新生成'); Taro.navigateBack(); return undefined; } successHandled.current = false; setTimerSec(REDEEM_TOKEN_TTL_SECONDS); timerRef.current = setInterval(() => { setTimerSec((prev) => { if (prev <= 1) { stopTimer(); setTimeout(() => handleRedeemExpired(), 0); return 0; } return prev - 1; }); }, 1000); return () => stopTimer(); }, [token, stopTimer, handleRedeemExpired]); useEffect(() => { if (!token) return undefined; let cancelled = false; async function pollStatus() { try { const status = await request(`/redeem/tokens/${token}/status`); if (cancelled || successHandled.current) return; if (status.status === 'CONSUMED' && status.record) { handleRedeemSuccess(status.record); } else if (status.status === 'EXPIRED') { handleRedeemExpired(); } else if (status.status === 'PENDING' && status.expireInSeconds > 0) { setTimerSec((prev) => Math.min(prev, status.expireInSeconds)); } } catch { /* 轮询失败忽略,下次重试 */ } } void pollStatus(); const pollId = setInterval(() => void pollStatus(), POLL_INTERVAL_MS); return () => { cancelled = true; clearInterval(pollId); }; }, [token, handleRedeemSuccess, handleRedeemExpired]); return ( 请向收银员出示此码 0 && timerSec < REDEEM_TOKEN_TTL_SECONDS ? ' redeem-timer--active' : ''}`}> {formatTimer(timerSec)} 失效倒计时 待核销金额 {token ? ( 核销码编号(供追查) {token} 双击复制 ) : null} Taro.navigateBack()}> 取消核销 ); }