小程序端核销权益输入框限制
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-23 00:32:09 +08:00
parent 047ffd7b16
commit cd6b82abd8
2 changed files with 48 additions and 19 deletions
+25 -10
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useState } from 'react';
import { View, Text } from '@tarojs/components'; import { View, Text } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
@@ -35,17 +35,18 @@ function usagePercent(coupon: CouponItem) {
export default function BenefitPage() { export default function BenefitPage() {
const metrics = useNavBarMetrics(); const metrics = useNavBarMetrics();
const loggedIn = isLoggedIn(); const [loggedIn, setLoggedIn] = useState(() => isLoggedIn());
const [summary, setSummary] = useState<BenefitSummary | null>(null); const [summary, setSummary] = useState<BenefitSummary | null>(null);
const [coupons, setCoupons] = useState<CouponItem[]>([]); const [coupons, setCoupons] = useState<CouponItem[]>([]);
const [tab, setTab] = useState<'available' | 'history'>('available'); const [tab, setTab] = useState<'available' | 'history'>('available');
useDidShow(() => { const resetGuestState = useCallback(() => {
syncTabBarSelected(2); setSummary(null);
}); setCoupons([]);
}, []);
const loadBenefit = useCallback(() => { const loadBenefit = useCallback(() => {
if (!loggedIn) return Promise.resolve(); if (!isLoggedIn()) return Promise.resolve();
return Promise.all([ return Promise.all([
request<BenefitSummary>('/benefit/summary'), request<BenefitSummary>('/benefit/summary'),
request<CouponItem[]>('/benefit/coupons'), request<CouponItem[]>('/benefit/coupons'),
@@ -55,13 +56,27 @@ export default function BenefitPage() {
setCoupons(Array.isArray(list) ? list : []); setCoupons(Array.isArray(list) ? list : []);
}) })
.catch((e) => toast(e instanceof Error ? e.message : '加载失败')); .catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [loggedIn]); }, []);
useEffect(() => { useDidShow(() => {
void loadBenefit(); syncTabBarSelected(2);
}, [loadBenefit]); const loggedInNow = isLoggedIn();
setLoggedIn(loggedInNow);
if (loggedInNow) {
void loadBenefit();
} else {
resetGuestState();
}
});
usePullDownRefresh(() => { usePullDownRefresh(() => {
const loggedInNow = isLoggedIn();
setLoggedIn(loggedInNow);
if (!loggedInNow) {
resetGuestState();
Taro.stopPullDownRefresh();
return;
}
void loadBenefit().finally(() => Taro.stopPullDownRefresh()); void loadBenefit().finally(() => Taro.stopPullDownRefresh());
}); });
+23 -9
View File
@@ -17,7 +17,12 @@ function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
} }
/** 核销金额输入:最多两位小数;去掉前导 0;禁止非法字符 */ /**
* 核销金额输入清洗:
* - 只保留数字与一个小数点
* - 小数最多 2 位(再输入会被截断)
* - 去掉多余前导 0
*/
function sanitizeRedeemAmountInput(raw: string): string { function sanitizeRedeemAmountInput(raw: string): string {
let next = String(raw ?? '').replace(/[^\d.]/g, ''); let next = String(raw ?? '').replace(/[^\d.]/g, '');
if (!next) return ''; if (!next) return '';
@@ -28,19 +33,18 @@ function sanitizeRedeemAmountInput(raw: string): string {
const decRaw = next const decRaw = next
.slice(firstDot + 1) .slice(firstDot + 1)
.replace(/\D/g, '') .replace(/\D/g, '')
.replace(/\./g, '')
.slice(0, 2); .slice(0, 2);
const intPart = intRaw.replace(/^0+(?=\d)/, '') || '0'; const intPart = intRaw.replace(/^0+(?=\d)/, '') || '0';
// 正在输入小数点或小数位时保留点 if (next.endsWith('.') && decRaw.length === 0) {
if (decRaw.length > 0 || next.endsWith('.')) { return `${intPart}.`;
}
if (decRaw.length > 0) {
return `${intPart}.${decRaw}`; return `${intPart}.${decRaw}`;
} }
return intPart; return intPart;
} }
// 纯整数:忽略前导 0(保留单个 0) return next.replace(/^0+(?=\d)/, '');
next = next.replace(/^0+(?=\d)/, '');
return next;
} }
export default function RedeemPage() { export default function RedeemPage() {
@@ -50,6 +54,8 @@ export default function RedeemPage() {
const [balance, setBalance] = useState(0); const [balance, setBalance] = useState(0);
const [couponBalance, setCouponBalance] = useState<number | null>(null); const [couponBalance, setCouponBalance] = useState<number | null>(null);
const [amount, setAmount] = useState(() => sanitizeRedeemAmountInput(initialAmount)); const [amount, setAmount] = useState(() => sanitizeRedeemAmountInput(initialAmount));
/** 原生 input 在截断小数后偶发不同步,强制 remount 对齐受控值 */
const [inputKey, setInputKey] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const redeemableMax = couponId ? (couponBalance ?? 0) : balance; const redeemableMax = couponId ? (couponBalance ?? 0) : balance;
@@ -80,10 +86,16 @@ export default function RedeemPage() {
function fillMaxAmount() { function fillMaxAmount() {
if (redeemableMax < MIN_REDEEM_AMOUNT) return; if (redeemableMax < MIN_REDEEM_AMOUNT) return;
setAmount(sanitizeRedeemAmountInput(redeemableMax.toFixed(2))); setAmount(sanitizeRedeemAmountInput(redeemableMax.toFixed(2)));
setInputKey((k) => k + 1);
} }
function onAmountChange(raw: string) { function onAmountChange(raw: string) {
setAmount(sanitizeRedeemAmountInput(raw)); const next = sanitizeRedeemAmountInput(raw);
setAmount(next);
// 用户试图输入超过两位小数 / 非法字符时,强制刷新原生框显示
if (next !== raw) {
setInputKey((k) => k + 1);
}
} }
async function submit() { async function submit() {
@@ -130,11 +142,13 @@ export default function RedeemPage() {
</View> </View>
<View className="redeem-input-wrap"> <View className="redeem-input-wrap">
<Input <Input
key={inputKey}
className="redeem-input" className="redeem-input"
type="digit" type="digit"
placeholder="输入核销金额" placeholder="输入核销金额"
placeholderClass="redeem-input-placeholder" placeholderClass="redeem-input-placeholder"
value={amount} value={amount}
maxlength={12}
onInput={(e) => onAmountChange(e.detail.value)} onInput={(e) => onAmountChange(e.detail.value)}
onBlur={(e) => onAmountChange(e.detail.value)} onBlur={(e) => onAmountChange(e.detail.value)}
style={{ textAlign: 'center' }} style={{ textAlign: 'center' }}
@@ -150,7 +164,7 @@ export default function RedeemPage() {
</View> </View>
<View className="redeem-tips"> <View className="redeem-tips">
<Text className="redeem-tips-text"> <Text className="redeem-tips-text">
0.01 3 0.01 3
</Text> </Text>
</View> </View>
<View <View