diff --git a/apps/mini-user/src/pages/address-edit/index.tsx b/apps/mini-user/src/pages/address-edit/index.tsx index ef9a06a..c512ddd 100644 --- a/apps/mini-user/src/pages/address-edit/index.tsx +++ b/apps/mini-user/src/pages/address-edit/index.tsx @@ -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(() => ({ 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>>('/user/addresses').then((list) => { @@ -161,7 +197,11 @@ export default function AddressEditPage() { style={{ display: 'flex', alignItems: 'center' }} onClick={() => setPickerOpen(true)} > - {regionText || '请选择省市区'} + + {locating && !isEdit + ? '定位中…' + : regionText || '请选择省市区'} + diff --git a/apps/mini-user/src/pages/redeem/index.tsx b/apps/mini-user/src/pages/redeem/index.tsx index f9e1030..30cb143 100644 --- a/apps/mini-user/src/pages/redeem/index.tsx +++ b/apps/mini-user/src/pages/redeem/index.tsx @@ -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' }} /> @@ -115,12 +131,14 @@ export default function RedeemPage() { 全部核销 - - 直接核销:金额须大于 0 且不超过全部可用权益余额。核销码有效期 3 分钟,请到店出示。 - + + + 核销金额最低 0.01 元,且不超过可用权益余额。核销码有效期 3 分钟,请到店出示给收银员扫码。 + + {loading ? '生成中...' : '生成核销码'} diff --git a/apps/mini-user/src/styles/redeem.css b/apps/mini-user/src/styles/redeem.css index 953cae0..3995378 100644 --- a/apps/mini-user/src/styles/redeem.css +++ b/apps/mini-user/src/styles/redeem.css @@ -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 {