核销金额:最低 0.01 元;输入最多两位小数;低于该值会 toast 提示,无法生成核销码。

底部提示:左右留白 + 内边距,浅底圆角块,文案改为最低 0.01、余额上限、3 分钟到店出示。
新增地址:进入页会尝试定位,自动选中当前省市区(失败则保留默认郑州);编辑地址不重定位。
This commit is contained in:
2026-07-22 23:11:34 +08:00
parent fb530e2ff5
commit f2303f7fb8
3 changed files with 79 additions and 15 deletions
@@ -5,9 +5,15 @@ import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import RegionPicker from '../../components/RegionPicker';
import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav';
import { DEFAULT_REGION, formatRegion, type RegionSelection } from '../../lib/region-data';
import {
DEFAULT_REGION,
REGION_ALL,
formatRegion,
type RegionSelection,
} from '../../lib/region-data';
import { normalizePhoneInput, validateMobilePhone } from '../../lib/phone';
import { getStoredUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
import { resolveUserCity } from '../../lib/user-location';
import { request, toast, type UserProfile } from '../../lib/api';
type AddressForm = {
@@ -27,6 +33,7 @@ export default function AddressEditPage() {
const checkoutCtx = readCheckoutContext(router.params);
const [pickerOpen, setPickerOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [locating, setLocating] = useState(false);
const [error, setError] = useState('');
const [form, setForm] = useState<AddressForm>(() => ({
receiverName: '',
@@ -49,6 +56,35 @@ export default function AddressEditPage() {
.catch(() => {});
}, [id]);
useEffect(() => {
if (id) return;
let cancelled = false;
setLocating(true);
void resolveUserCity(true)
.then((resolved) => {
if (cancelled) return;
const district =
resolved.region.district && resolved.region.district !== REGION_ALL
? resolved.region.district
: resolved.district && resolved.district !== REGION_ALL
? resolved.district
: DEFAULT_REGION.district;
setForm((prev) => ({
...prev,
province: resolved.region.province || prev.province,
city: resolved.region.city || prev.city,
district: district || prev.district,
}));
})
.catch(() => {})
.finally(() => {
if (!cancelled) setLocating(false);
});
return () => {
cancelled = true;
};
}, [id]);
useEffect(() => {
if (!id) return;
request<Array<Record<string, unknown>>>('/user/addresses').then((list) => {
@@ -161,7 +197,11 @@ export default function AddressEditPage() {
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => setPickerOpen(true)}
>
<Text>{regionText || '请选择省市区'}</Text>
<Text>
{locating && !isEdit
? '定位中…'
: regionText || '请选择省市区'}
</Text>
</View>
</View>
<View className="address-form-field">
+27 -9
View File
@@ -11,10 +11,26 @@ type BenefitSummary = {
maxRedeemAmount: number;
};
const MIN_REDEEM_AMOUNT = 0.01;
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/** 核销金额输入:最多两位小数,禁止非法字符 */
function sanitizeRedeemAmountInput(raw: string): string {
let next = raw.replace(/[^\d.]/g, '');
const firstDot = next.indexOf('.');
if (firstDot >= 0) {
next =
next.slice(0, firstDot + 1) + next.slice(firstDot + 1).replace(/\./g, '');
const [intPart, decPart = ''] = next.split('.');
next = `${intPart}.${decPart.slice(0, 2)}`;
}
if (next.startsWith('.')) next = `0${next}`;
return next;
}
export default function RedeemPage() {
const router = useRouter();
const couponId = router.params.couponId;
@@ -50,14 +66,14 @@ export default function RedeemPage() {
}, [couponId]);
function fillMaxAmount() {
if (redeemableMax <= 0) return;
if (redeemableMax < MIN_REDEEM_AMOUNT) return;
setAmount(String(redeemableMax));
}
async function submit() {
const value = Math.round(Number(amount) * 100) / 100;
if (!(value > 0)) {
toast('请输入核销金额');
if (!Number.isFinite(value) || value < MIN_REDEEM_AMOUNT) {
toast(`核销金额不能低于 ${MIN_REDEEM_AMOUNT.toFixed(2)}`);
return;
}
if (value > redeemableMax) {
@@ -103,7 +119,7 @@ export default function RedeemPage() {
placeholder="输入核销金额"
placeholderClass="redeem-input-placeholder"
value={amount}
onInput={(e) => setAmount(e.detail.value)}
onInput={(e) => setAmount(sanitizeRedeemAmountInput(e.detail.value))}
style={{ textAlign: 'center' }}
/>
</View>
@@ -115,12 +131,14 @@ export default function RedeemPage() {
</Text>
</View>
<Text className="redeem-tips">
0 3
</Text>
<View className="redeem-tips">
<Text className="redeem-tips-text">
0.01 3
</Text>
</View>
<View
className={`redeem-submit${loading || redeemableMax <= 0 ? ' redeem-submit--disabled' : ''}`}
onClick={loading || redeemableMax <= 0 ? undefined : submit}
className={`redeem-submit${loading || redeemableMax < MIN_REDEEM_AMOUNT ? ' redeem-submit--disabled' : ''}`}
onClick={loading || redeemableMax < MIN_REDEEM_AMOUNT ? undefined : submit}
>
<Text>{loading ? '生成中...' : '生成核销码'}</Text>
</View>
+10 -4
View File
@@ -77,12 +77,18 @@
.redeem-tips {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0 var(--space-page);
margin: 8px var(--space-page) 0;
padding: 12px 16px;
border-radius: var(--radius-md);
background: rgba(0, 0, 0, 0.03);
}
.redeem-tips-text {
display: block;
font-size: 12px;
color: var(--color-subtle-gray);
line-height: 1.6;
line-height: 1.7;
word-break: break-word;
}
.redeem-amount-hint {