import { useEffect, useState } from 'react'; import { View, Text, Input } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; import { formatMoney } from '../../lib/money'; type BenefitSummary = { totalBalance: number; maxRedeemAmount: number; }; const MIN_REDEEM_AMOUNT = 0.01; /** * 核销金额输入清洗: * - 只保留数字与一个小数点 * - 小数最多 2 位(再输入会被截断) * - 去掉多余前导 0 */ function sanitizeRedeemAmountInput(raw: string): string { let next = String(raw ?? '').replace(/[^\d.]/g, ''); if (!next) return ''; const firstDot = next.indexOf('.'); if (firstDot >= 0) { const intRaw = next.slice(0, firstDot).replace(/\D/g, ''); const decRaw = next .slice(firstDot + 1) .replace(/\D/g, '') .slice(0, 2); const intPart = intRaw.replace(/^0+(?=\d)/, '') || '0'; if (next.endsWith('.') && decRaw.length === 0) { return `${intPart}.`; } if (decRaw.length > 0) { return `${intPart}.${decRaw}`; } return intPart; } return next.replace(/^0+(?=\d)/, ''); } export default function RedeemPage() { const router = useRouter(); const couponId = router.params.couponId; const initialAmount = router.params.amount ?? ''; const [balance, setBalance] = useState(0); const [couponBalance, setCouponBalance] = useState(null); const [amount, setAmount] = useState(() => sanitizeRedeemAmountInput(initialAmount)); /** 原生 input 在截断小数后偶发不同步,强制 remount 对齐受控值 */ const [inputKey, setInputKey] = useState(0); const [loading, setLoading] = useState(false); const redeemableMax = couponId ? (couponBalance ?? 0) : balance; useEffect(() => { if (!isLoggedIn()) { goLogin('/pages/redeem/index'); return; } request('/benefit/summary') .then((s) => setBalance(Number(s.maxRedeemAmount ?? s.totalBalance ?? 0))) .catch(() => {}); }, []); useEffect(() => { if (!couponId) { setCouponBalance(null); return; } request>('/benefit/coupons') .then((list) => { const found = list.find((c) => String(c.id) === couponId); if (found) setCouponBalance(Number(found.balance)); }) .catch(() => {}); }, [couponId]); function fillMaxAmount() { if (redeemableMax < MIN_REDEEM_AMOUNT) return; setAmount(sanitizeRedeemAmountInput(redeemableMax.toFixed(2))); setInputKey((k) => k + 1); } function onAmountChange(raw: string) { const next = sanitizeRedeemAmountInput(raw); setAmount(next); // 用户试图输入超过两位小数 / 非法字符时,强制刷新原生框显示 if (next !== raw) { setInputKey((k) => k + 1); } } async function submit() { const value = Math.round(Number(amount) * 100) / 100; if (!Number.isFinite(value) || value < MIN_REDEEM_AMOUNT) { toast(`核销金额不能低于 ${MIN_REDEEM_AMOUNT.toFixed(2)} 元`); return; } if (value > redeemableMax) { toast(couponId ? '核销金额不能超过该权益可用余额' : '超出可用余额'); return; } setLoading(true); try { const body: { amount: number; couponId?: string } = { amount: value }; if (couponId) body.couponId = couponId; const data = await request<{ token: string; amount: number }>('/redeem/tokens', { method: 'POST', data: body, }); Taro.navigateTo({ url: `/pages/redeem-code/index?token=${encodeURIComponent(data.token)}&amount=${data.amount}`, }); } catch (e) { toast(e instanceof Error ? e.message : '生成失败'); } finally { setLoading(false); } } return ( {couponId ? '当前权益可用余额' : '可用余额'} ¥{redeemableMax > 0 ? formatMoney(redeemableMax) : '--'} onAmountChange(e.detail.value)} onBlur={(e) => onAmountChange(e.detail.value)} style={{ textAlign: 'center' }} /> 最高可核销 ¥{formatMoney(redeemableMax)} 全部核销 核销金额最低 0.01 元,小数最多两位。不超过可用权益余额。核销码有效期 3 分钟,请到店出示给收银员扫码。 {loading ? '生成中...' : '生成核销码'} ); }