核销成功提示

This commit is contained in:
2026-07-07 11:46:12 +08:00
parent 1dcc9aa1f4
commit e25b208545
6 changed files with 339 additions and 32 deletions
+81 -10
View File
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
const REDEEM_TOKEN_TTL_SECONDS = 300;
const POLL_INTERVAL_MS = 2500;
type BenefitSummary = {
totalBalance: number;
@@ -10,6 +11,21 @@ type BenefitSummary = {
activeCouponCount: number;
};
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 formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
@@ -37,10 +53,9 @@ export default function RedeemPage() {
const [timerSec, setTimerSec] = useState(REDEEM_TOKEN_TTL_SECONDS);
const timerRef = useRef<number | null>(null);
const presetApplied = useRef(false);
const successHandled = useRef(false);
const redeemableMax = couponId
? (couponBalance ?? 0)
: (summary?.totalBalance ?? 0);
const redeemableMax = couponId ? (couponBalance ?? 0) : (summary?.totalBalance ?? 0);
useEffect(() => {
request<BenefitSummary>('USER_H5', '/benefit/summary').then(setSummary).catch(() => {});
@@ -77,12 +92,41 @@ export default function RedeemPage() {
}
}
function closeModal() {
const closeModal = useCallback(() => {
stopTimer();
setModalOpen(false);
setToken('');
setTimerSec(REDEEM_TOKEN_TTL_SECONDS);
}
successHandled.current = false;
}, []);
const handleRedeemExpired = useCallback(() => {
if (successHandled.current) return;
successHandled.current = true;
closeModal();
sessionStorage.removeItem('redeemToken');
sessionStorage.removeItem('redeemAmount');
setMsg('核销码已失效,请重新生成');
}, [closeModal]);
const handleRedeemSuccess = useCallback(
(record: NonNullable<Extract<RedeemTokenStatus, { status: 'CONSUMED' }>['record']>) => {
if (successHandled.current) return;
successHandled.current = true;
stopTimer();
setModalOpen(false);
setToken('');
sessionStorage.setItem('lastRedeemRecordId', record.id);
sessionStorage.setItem('lastRedeemResult', JSON.stringify(record));
sessionStorage.removeItem('redeemToken');
sessionStorage.removeItem('redeemAmount');
navigate('/redeem/success', {
replace: true,
state: { record },
});
},
[navigate],
);
function startTimer() {
stopTimer();
@@ -91,10 +135,7 @@ export default function RedeemPage() {
setTimerSec((prev) => {
if (prev <= 1) {
stopTimer();
window.setTimeout(() => {
window.alert('核销码已失效,请重新生成');
closeModal();
}, 0);
window.setTimeout(() => handleRedeemExpired(), 0);
return 0;
}
return prev - 1;
@@ -102,6 +143,35 @@ export default function RedeemPage() {
}, 1000);
}
useEffect(() => {
if (!modalOpen || !token) return undefined;
let cancelled = false;
async function pollStatus() {
try {
const status = await request<RedeemTokenStatus>('USER_H5', `/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 = window.setInterval(() => void pollStatus(), POLL_INTERVAL_MS);
return () => {
cancelled = true;
window.clearInterval(pollId);
};
}, [modalOpen, token, handleRedeemSuccess, handleRedeemExpired]);
function parseAmount() {
const value = Number(amountInput);
return Number.isFinite(value) ? Math.round(value * 100) / 100 : 0;
@@ -129,6 +199,7 @@ export default function RedeemPage() {
setLoading(true);
setMsg('');
successHandled.current = false;
try {
const body: { amount: number; couponId?: string } = { amount };
if (couponId) body.couponId = couponId;