用户端调整

2.首页商品详情增加点击区域
3.浓香型 酱香型 点击增加提示:暂未开放
5.首页餐券2字更换为“好客权益”
6.门店页面地址筛选功能 (三级菜单,省市区)
7.好客权益:去使用点击交互调整为核销页面,单张好客权益点击核销页面带参数和金额
7.核销金额限制为可用余额最大数,不是500
9.收货地址电话号码增加校验,长度限制,地址或者未填提示,地区调整为省市区三级菜单
10.微信授权位置调整在手机验证码下侧
This commit is contained in:
2026-07-01 00:19:08 +08:00
parent 6e047dc0a5
commit c43defca3f
18 changed files with 772 additions and 190 deletions
+57 -97
View File
@@ -1,8 +1,11 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import SubPageHeader from '../components/SubPageHeader';
import RegionPicker from '../components/RegionPicker';
import { request } from '../lib/api';
import { buildAddressListUrl, readCheckoutContext } from '../lib/navigation';
import { DEFAULT_REGION, formatRegion } from '../lib/region-data';
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
type AddressForm = {
receiverName: string;
@@ -14,35 +17,20 @@ type AddressForm = {
isDefault: boolean;
};
const REGION_OPTIONS = [
{ province: '河南省', city: '郑州市', district: '金水区', label: '河南省 郑州市 金水区' },
{ province: '河南省', city: '郑州市', district: '二七区', label: '河南省 郑州市 二七区' },
{ province: '河南省', city: '洛阳市', district: '涧西区', label: '河南省 洛阳市 涧西区' },
{ province: '河南省', city: '洛阳市', district: '洛龙区', label: '河南省 洛阳市 洛龙区' },
{ province: '北京市', city: '北京市', district: '东城区', label: '北京市 东城区' },
{ province: '上海市', city: '上海市', district: '黄浦区', label: '上海市 黄浦区' },
{ province: '陕西省', city: '西安市', district: '雁塔区', label: '陕西省 西安市 雁塔区' },
] as const;
function regionLabel(form: Pick<AddressForm, 'province' | 'city' | 'district'>) {
if (!form.province || !form.city || !form.district) return '';
return `${form.province} ${form.city} ${form.district}`;
}
export default function AddressEditPage() {
const { id } = useParams();
const [params] = useSearchParams();
const isEdit = Boolean(id);
const navigate = useNavigate();
const [pickerOpen, setPickerOpen] = useState(false);
const [pickerDraft, setPickerDraft] = useState<typeof REGION_OPTIONS[number] | null>(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [form, setForm] = useState<AddressForm>({
receiverName: '',
phone: '13800000001',
province: '河南省',
city: params.get('city') || '郑州市',
district: '金水区',
phone: '',
province: DEFAULT_REGION.province,
city: params.get('city') || DEFAULT_REGION.city,
district: DEFAULT_REGION.district,
detail: '',
isDefault: true,
});
@@ -71,35 +59,26 @@ export default function AddressEditPage() {
});
}, [id]);
const regionText = regionLabel(form);
const regionText = formatRegion(form.province, form.city, form.district);
function openPicker() {
const current =
REGION_OPTIONS.find(
(r) => r.province === form.province && r.city === form.city && r.district === form.district,
) ?? null;
setPickerDraft(current);
setPickerOpen(true);
}
function confirmRegion() {
if (pickerDraft) {
setForm((f) => ({
...f,
province: pickerDraft.province,
city: pickerDraft.city,
district: pickerDraft.district,
}));
}
setPickerOpen(false);
function validateForm(): string | null {
if (!form.receiverName.trim()) return '请输入收货人姓名';
const phoneCheck = validateMobilePhone(form.phone);
if (!phoneCheck.ok) return phoneCheck.message ?? '请输入正确的手机号码';
if (!form.province || !form.city || !form.district) return '请选择所在地区';
if (!form.detail.trim()) return '请输入详细地址';
return null;
}
async function save() {
if (!form.receiverName.trim()) return;
if (!form.phone.trim()) return;
if (!regionText) return;
if (!form.detail.trim()) return;
const validationError = validateForm();
if (validationError) {
setError(validationError);
return;
}
setSaving(true);
setError('');
try {
if (isEdit && id) {
await request('USER_H5', `/user/addresses/${id}`, {
@@ -135,7 +114,10 @@ export default function AddressEditPage() {
className="address-edit-input"
placeholder="请输入姓名"
value={form.receiverName}
onChange={(e) => setForm({ ...form, receiverName: e.target.value })}
onChange={(e) => {
setForm({ ...form, receiverName: e.target.value });
setError('');
}}
/>
<span className="material-symbols-outlined address-edit-field-icon">person</span>
</div>
@@ -149,20 +131,23 @@ export default function AddressEditPage() {
type="tel"
className="address-edit-input"
placeholder="请输入手机号"
maxLength={11}
inputMode="numeric"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
onChange={(e) => {
setForm({ ...form, phone: normalizePhoneInput(e.target.value) });
setError('');
}}
/>
<span className="material-symbols-outlined address-edit-field-icon">smartphone</span>
</div>
</div>
<button type="button" className="address-edit-field address-edit-region" onClick={openPicker}>
<label className="address-edit-label"></label>
<div className="address-edit-line address-edit-line--picker">
<button type="button" className="address-edit-field address-edit-region" onClick={() => setPickerOpen(true)}>
<div className="address-edit-line address-edit-line--picker address-edit-line--region">
<span className={regionText ? 'address-edit-region-value' : 'address-edit-region-placeholder'}>
{regionText || '请选择省/市/区'}
{regionText || '省份、城市、区县'}
</span>
<span className="material-symbols-outlined address-edit-field-icon">chevron_right</span>
</div>
</button>
@@ -174,7 +159,10 @@ export default function AddressEditPage() {
placeholder="街道、门牌号、小区名称等"
rows={3}
value={form.detail}
onChange={(e) => setForm({ ...form, detail: e.target.value })}
onChange={(e) => {
setForm({ ...form, detail: e.target.value });
setError('');
}}
/>
</div>
</div>
@@ -201,6 +189,8 @@ export default function AddressEditPage() {
</label>
</section>
{error && <p className="address-edit-error">{error}</p>}
<div className="address-edit-security">
<span className="material-symbols-outlined">location_on</span>
<span></span>
@@ -223,51 +213,21 @@ export default function AddressEditPage() {
</button>
</nav>
{pickerOpen && (
<div
className="address-edit-picker-overlay"
role="presentation"
onClick={() => setPickerOpen(false)}
>
<div
className="address-edit-picker-sheet"
role="dialog"
aria-label="选择地区"
onClick={(e) => e.stopPropagation()}
>
<div className="address-edit-picker-head">
<h4></h4>
<button type="button" aria-label="关闭" onClick={() => setPickerOpen(false)}>
<span className="material-symbols-outlined">close</span>
</button>
</div>
<div className="address-edit-picker-list">
{REGION_OPTIONS.map((r) => {
const selected =
pickerDraft?.province === r.province &&
pickerDraft?.city === r.city &&
pickerDraft?.district === r.district;
return (
<button
key={r.label}
type="button"
className={`address-edit-picker-item${selected ? ' selected' : ''}`}
onClick={() => setPickerDraft(r)}
>
<span>{r.label}</span>
{selected && (
<span className="material-symbols-outlined">check_circle</span>
)}
</button>
);
})}
</div>
<button type="button" className="address-edit-picker-confirm" onClick={confirmRegion}>
</button>
</div>
</div>
)}
<RegionPicker
open={pickerOpen}
value={{ province: form.province, city: form.city, district: form.district }}
onClose={() => setPickerOpen(false)}
onConfirm={(region) => {
setForm((f) => ({
...f,
province: region.province,
city: region.city,
district: region.district,
}));
setPickerOpen(false);
setError('');
}}
/>
</div>
);
}
+47 -11
View File
@@ -34,6 +34,15 @@ function usagePercent(coupon: CouponItem) {
return Math.min(100, Math.round((Number(coupon.usedAmount) / total) * 100));
}
function buildRedeemUrl(coupon?: CouponItem) {
if (!coupon) return '/redeem';
const params = new URLSearchParams({
couponId: coupon.id,
amount: String(coupon.balance),
});
return `/redeem?${params.toString()}`;
}
export default function BenefitPage() {
const navigate = useNavigate();
const listRef = useRef<HTMLElement>(null);
@@ -71,6 +80,10 @@ export default function BenefitPage() {
listRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function goRedeem(coupon?: CouponItem) {
navigate(buildRedeemUrl(coupon));
}
return (
<div className="benefit-page">
<header className="benefit-header">
@@ -110,8 +123,8 @@ export default function BenefitPage() {
</section>
<section className="benefit-action">
<button type="button" className="benefit-use-btn" onClick={() => navigate('/stores')}>
<span className="material-symbols-outlined filled">storefront</span>
<button type="button" className="benefit-use-btn" onClick={() => goRedeem()}>
<span className="material-symbols-outlined filled">qr_code_2</span>
使
</button>
</section>
@@ -136,7 +149,21 @@ export default function BenefitPage() {
{visible.length > 0 ? (
<div className="benefit-list">
{visible.map((c) => (
<article key={c.id} className="benefit-card">
<article
key={c.id}
className={`benefit-card${tab === 'available' && c.balance > 0 ? ' benefit-card--clickable' : ''}`}
role={tab === 'available' && c.balance > 0 ? 'button' : undefined}
tabIndex={tab === 'available' && c.balance > 0 ? 0 : undefined}
onClick={() => {
if (tab === 'available' && c.balance > 0) goRedeem(c);
}}
onKeyDown={(e) => {
if (tab === 'available' && c.balance > 0 && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
goRedeem(c);
}
}}
>
<div className="benefit-card-inner">
<div className="benefit-card-value">
<span className="benefit-card-value-label"></span>
@@ -167,16 +194,25 @@ export default function BenefitPage() {
</div>
</div>
<div className="benefit-card-foot">
<Link to={`/benefit/${c.id}`} className="benefit-card-no">
<Link
to={`/benefit/${c.id}`}
className="benefit-card-no"
onClick={(e) => e.stopPropagation()}
>
{formatCouponNo(c.couponNo)}
</Link>
<button
type="button"
className="benefit-card-redeem"
onClick={() => navigate('/redeem')}
>
</button>
{tab === 'available' && c.balance > 0 && (
<button
type="button"
className="benefit-card-redeem"
onClick={(e) => {
e.stopPropagation();
goRedeem(c);
}}
>
</button>
)}
</div>
</div>
</div>
+38 -18
View File
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { request } from '../lib/api';
import ProductCarousel from '../components/ProductCarousel';
import TabMainHeader from '../components/TabMainHeader';
import AppToast from '../components/AppToast';
import { getProductImages } from '../lib/product-images';
import CouponBadge from '@dukang/shared-ui/CouponBadge';
@@ -19,19 +20,33 @@ type Product = {
};
const AROMA_TABS = [
{ key: 'QINGXIANG', label: '清香型', disabled: false },
{ key: 'JIANGXIANG', label: '酱香型', disabled: true },
{ key: 'NONGXIANG', label: '浓香型', disabled: true },
{ key: 'QINGXIANG', label: '清香型', open: true },
{ key: 'JIANGXIANG', label: '酱香型', open: false },
{ key: 'NONGXIANG', label: '浓香型', open: false },
];
export default function HomePage() {
const [tab, setTab] = useState('QINGXIANG');
const [products, setProducts] = useState<Product[]>([]);
const [toast, setToast] = useState('');
useEffect(() => {
request<Product[]>('USER_H5', '/catalog/products').then(setProducts);
}, []);
function showToast(message: string) {
setToast(message);
window.setTimeout(() => setToast(''), 2200);
}
function onAromaTabClick(key: string, open: boolean) {
if (!open) {
showToast('暂未开放');
return;
}
setTab(key);
}
const filtered = products.filter((p) => p.aromaType === tab);
const onSale = tab === 'QINGXIANG';
@@ -52,9 +67,8 @@ export default function HomePage() {
<button
key={t.key}
type="button"
disabled={t.disabled}
className={`home-aroma-tab${tab === t.key ? ' active' : ''}${t.disabled ? ' disabled' : ''}`}
onClick={() => !t.disabled && setTab(t.key)}
className={`home-aroma-tab${tab === t.key ? ' active' : ''}${!t.open ? ' muted' : ''}`}
onClick={() => onAromaTabClick(t.key, t.open)}
>
{t.label}
</button>
@@ -66,23 +80,29 @@ export default function HomePage() {
{onSale &&
filtered.map((p, index) => (
<article key={p.id} className="home-product-card">
<ProductCarousel images={getProductImages(index)} alt={p.name} />
<div className="home-product-body">
<div className="home-product-row">
<h3 className="home-product-name">{p.name}</h3>
<span className="home-product-price">¥{p.price}</span>
</div>
<p className="home-product-sub">{p.subtitle}</p>
<div className="home-product-footer">
<CouponBadge amount={p.benefitDisplay} />
<Link to={`/product/${p.id}`} className="home-buy-btn">
</Link>
<Link to={`/product/${p.id}`} className="home-product-link">
<ProductCarousel images={getProductImages(index)} alt={p.name} />
<div className="home-product-body">
<div className="home-product-row">
<h3 className="home-product-name">{p.name}</h3>
<span className="home-product-price">¥{p.price}</span>
</div>
<p className="home-product-sub">{p.subtitle}</p>
<div className="home-product-footer">
<CouponBadge amount={p.benefitDisplay} label="好客权益" />
</div>
</div>
</Link>
<div className="home-product-actions">
<Link to={`/product/${p.id}`} className="home-buy-btn">
</Link>
</div>
</article>
))}
</section>
<AppToast message={toast} />
</div>
);
}
+32 -12
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { request, saveAuth } from '../lib/api';
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
export default function LoginPage() {
const navigate = useNavigate();
@@ -22,6 +23,11 @@ export default function LoginPage() {
async function sendCode() {
if (!ensureAgreed()) return;
const phoneCheck = validateMobilePhone(phone);
if (!phoneCheck.ok) {
setMsg(phoneCheck.message ?? '请输入正确的手机号码');
return;
}
setMsg('');
await request('USER_H5', '/auth/sms/send', {
method: 'POST',
@@ -42,6 +48,15 @@ export default function LoginPage() {
async function login() {
if (!ensureAgreed()) return;
const phoneCheck = validateMobilePhone(phone);
if (!phoneCheck.ok) {
setMsg(phoneCheck.message ?? '请输入正确的手机号码');
return;
}
if (!code.trim()) {
setMsg('请输入验证码');
return;
}
setLoading(true);
setMsg('');
try {
@@ -77,17 +92,6 @@ export default function LoginPage() {
</header>
<main className="login-main">
<button type="button" className="login-wechat-btn" onClick={wechatLogin}>
<span className="material-symbols-outlined login-wechat-icon">chat</span>
<span></span>
</button>
<div className="login-divider">
<span className="login-divider-line" />
<span className="login-divider-text"></span>
<span className="login-divider-line" />
</div>
<div className="login-card">
<h3 className="login-card-title"></h3>
<div className="login-field">
@@ -96,8 +100,13 @@ export default function LoginPage() {
type="tel"
className="login-field-input"
placeholder="请输入手机号"
maxLength={11}
inputMode="numeric"
value={phone}
onChange={(e) => setPhone(e.target.value)}
onChange={(e) => {
setPhone(normalizePhoneInput(e.target.value));
setMsg('');
}}
/>
</div>
<div className="login-field">
@@ -128,6 +137,17 @@ export default function LoginPage() {
{loading ? '登录中...' : '登录'}
</button>
</div>
<div className="login-divider">
<span className="login-divider-line" />
<span className="login-divider-text"></span>
<span className="login-divider-line" />
</div>
<button type="button" className="login-wechat-btn" onClick={wechatLogin}>
<span className="material-symbols-outlined login-wechat-icon">chat</span>
<span></span>
</button>
</main>
<footer className="login-footer">
+1 -1
View File
@@ -129,7 +129,7 @@ export default function MinePage() {
<span className="mine-asset-value">{formatMoney(benefitBalance)}</span>
</div>
</div>
<Link to="/benefit" className="mine-asset-cta">
<Link to="/redeem" className="mine-asset-cta">
使
</Link>
</div>
+51 -17
View File
@@ -1,8 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
const REDEEM_MAX_AMOUNT = 500;
const REDEEM_TOKEN_TTL_SECONDS = 300;
type BenefitSummary = {
@@ -23,7 +22,12 @@ function formatTimer(seconds: number) {
export default function RedeemPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const couponId = searchParams.get('couponId') ?? undefined;
const presetAmount = searchParams.get('amount');
const [summary, setSummary] = useState<BenefitSummary | null>(null);
const [couponBalance, setCouponBalance] = useState<number | null>(null);
const [amountInput, setAmountInput] = useState('');
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState(false);
@@ -32,12 +36,40 @@ export default function RedeemPage() {
const [confirmAmount, setConfirmAmount] = useState(0);
const [timerSec, setTimerSec] = useState(REDEEM_TOKEN_TTL_SECONDS);
const timerRef = useRef<number | null>(null);
const presetApplied = useRef(false);
const redeemableMax = couponId
? (couponBalance ?? 0)
: (summary?.totalBalance ?? 0);
useEffect(() => {
request<BenefitSummary>('USER_H5', '/benefit/summary').then(setSummary).catch(() => {});
return () => stopTimer();
}, []);
useEffect(() => {
if (!couponId) {
setCouponBalance(null);
return;
}
request<Array<Record<string, unknown>>>('USER_H5', '/benefit/coupons')
.then((list) => {
const found = list.find((c) => String(c.id) === couponId);
if (found) {
setCouponBalance(Number(found.balance));
}
})
.catch(() => {});
}, [couponId]);
useEffect(() => {
if (presetApplied.current) return;
if (presetAmount && Number(presetAmount) > 0) {
setAmountInput(formatMoney(Number(presetAmount)));
presetApplied.current = true;
}
}, [presetAmount]);
function stopTimer() {
if (timerRef.current != null) {
window.clearInterval(timerRef.current);
@@ -76,33 +108,34 @@ export default function RedeemPage() {
}
function fillMaxAmount() {
if (!summary) return;
setAmountInput(formatMoney(summary.maxRedeemAmount));
if (redeemableMax <= 0) return;
setAmountInput(formatMoney(redeemableMax));
setMsg('');
}
async function submit() {
const amount = parseAmount();
if (!summary) return;
const maxAllowed = couponId ? (couponBalance ?? 0) : summary.totalBalance;
if (amount <= 0) {
setMsg('请输入核销金额');
return;
}
if (amount > summary.totalBalance) {
setMsg('核销金额不能超过可用余额');
return;
}
if (amount > REDEEM_MAX_AMOUNT) {
setMsg(`单次核销不能超过 ¥${REDEEM_MAX_AMOUNT}`);
if (amount > maxAllowed) {
setMsg(couponId ? '核销金额不能超过该权益可用余额' : '核销金额不能超过可用余额');
return;
}
setLoading(true);
setMsg('');
try {
const body: { amount: number; couponId?: string } = { amount };
if (couponId) body.couponId = couponId;
const data = await request<{ token: string; amount: number }>('USER_H5', '/redeem/tokens', {
method: 'POST',
body: JSON.stringify({ amount }),
body: JSON.stringify(body),
});
setToken(data.token);
setConfirmAmount(data.amount);
@@ -117,7 +150,6 @@ export default function RedeemPage() {
}
}
const maxHint = summary ? Math.min(REDEEM_MAX_AMOUNT, summary.totalBalance) : REDEEM_MAX_AMOUNT;
const qrUrl = token
? `https://api.qrserver.com/v1/create-qr-code/?size=192x192&data=${encodeURIComponent(token)}`
: '';
@@ -133,7 +165,7 @@ export default function RedeemPage() {
type="button"
className="redeem-header-btn"
aria-label="帮助"
onClick={() => window.alert('单次核销上限 ¥500,核销码 5 分钟内有效。')}
onClick={() => window.alert('核销金额不超过可用余额,核销码 5 分钟内有效。')}
>
<span className="material-symbols-outlined">help_outline</span>
</button>
@@ -143,11 +175,13 @@ export default function RedeemPage() {
<section className="redeem-balance-card">
<div className="redeem-balance-pattern" aria-hidden />
<div className="redeem-balance-inner">
<span className="redeem-balance-label"></span>
<span className="redeem-balance-label">
{couponId ? '当前权益可用余额' : '当前好客权益可用余额'}
</span>
<div className="redeem-balance-amount">
<span className="redeem-balance-symbol">¥</span>
<span className="redeem-balance-value">
{summary ? formatMoney(summary.totalBalance) : '--'}
{redeemableMax > 0 ? formatMoney(redeemableMax) : '--'}
</span>
</div>
</div>
@@ -175,7 +209,7 @@ export default function RedeemPage() {
/>
</div>
<div className="redeem-amount-foot">
<span> ¥{formatMoney(maxHint)}</span>
<span> ¥{formatMoney(redeemableMax)}</span>
<button type="button" className="redeem-fill-max" onClick={fillMaxAmount}>
</button>
@@ -187,7 +221,7 @@ export default function RedeemPage() {
<button
type="button"
className="redeem-submit-btn"
disabled={loading || !summary || summary.totalBalance <= 0}
disabled={loading || redeemableMax <= 0}
onClick={submit}
>
<span className="material-symbols-outlined">qr_code_2</span>
+38 -3
View File
@@ -3,10 +3,19 @@ import { Link, useNavigate } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { request } from '../lib/api';
import TabMainHeader from '../components/TabMainHeader';
import RegionPicker from '../components/RegionPicker';
import {
DEFAULT_REGION,
formatRegion,
REGION_ALL,
type RegionSelection,
} from '../lib/region-data';
type StoreItem = {
id: string;
name: string;
province?: string;
cityName?: string;
district: string;
address: string;
coverUrl?: string | null;
@@ -37,6 +46,8 @@ export default function StoreListPage() {
const [stores, setStores] = useState<StoreItem[]>([]);
const [categoryTab, setCategoryTab] = useState<string>('全部');
const [keyword, setKeyword] = useState('');
const [region, setRegion] = useState<RegionSelection>(DEFAULT_REGION);
const [regionPickerOpen, setRegionPickerOpen] = useState(false);
useEffect(() => {
request<StoreItem[]>('USER_H5', '/stores?cityCode=410100').then(setStores);
@@ -47,6 +58,14 @@ export default function StoreListPage() {
if (categoryTab !== '全部') {
list = list.filter((s) => s.category?.name === categoryTab);
}
list = list.filter((s) => {
const province = s.province ?? '河南省';
const city = s.cityName ?? '郑州市';
if (region.province !== REGION_ALL && province !== region.province) return false;
if (region.city !== REGION_ALL && city !== region.city) return false;
if (region.district !== REGION_ALL && s.district !== region.district) return false;
return true;
});
const q = keyword.trim().toLowerCase();
if (q) {
list = list.filter(
@@ -57,7 +76,9 @@ export default function StoreListPage() {
);
}
return list;
}, [stores, categoryTab, keyword]);
}, [stores, categoryTab, keyword, region]);
const regionLabel = formatRegion(region.province, region.city, region.district);
return (
<div className="page store-page">
@@ -65,9 +86,13 @@ export default function StoreListPage() {
<TabMainHeader title="门店" />
<div className="store-toolbar">
<button type="button" className="store-location">
<button
type="button"
className="store-location"
onClick={() => setRegionPickerOpen(true)}
>
<span className="material-symbols-outlined">location_on</span>
<span> · </span>
<span>{regionLabel}</span>
<span className="material-symbols-outlined store-location-arrow">expand_more</span>
</button>
<div className="store-search">
@@ -153,6 +178,16 @@ export default function StoreListPage() {
<p className="store-list-end"></p>
)}
</main>
<RegionPicker
open={regionPickerOpen}
value={region}
onClose={() => setRegionPickerOpen(false)}
onConfirm={(next) => {
setRegion(next);
setRegionPickerOpen(false);
}}
/>
</div>
);
}