小程序页面优化(门店)
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, Image, Input } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
import RegionPicker from '../../components/RegionPicker';
|
||||
import CategoryPicker, {
|
||||
EMPTY_CATEGORY,
|
||||
formatCategoryLabel,
|
||||
type CategorySelection,
|
||||
type StoreCategoryNode,
|
||||
} from '../../components/CategoryPicker';
|
||||
import {
|
||||
DEFAULT_REGION,
|
||||
formatRegionLabel,
|
||||
@@ -26,20 +32,36 @@ type Store = {
|
||||
openTime?: string | null;
|
||||
closeTime?: string | null;
|
||||
status?: string;
|
||||
categoryId?: string | null;
|
||||
category?: { id?: string; name?: string; parentId?: string | null } | null;
|
||||
};
|
||||
|
||||
const CATEGORY_TABS = ['全部', '火锅', '地方菜', '高端餐饮', '烧烤烤肉', '西餐'] as const;
|
||||
const MOCK_DISTANCES = ['800m', '1.2km', '3.5km', '1.5km', '2.0km'];
|
||||
|
||||
export default function StoresPage() {
|
||||
const [stores, setStores] = useState<Store[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [categoryTab, setCategoryTab] = useState<string>('全部');
|
||||
const [keywordInput, setKeywordInput] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [region, setRegion] = useState<RegionSelection>(DEFAULT_REGION);
|
||||
const [regionOpen, setRegionOpen] = useState(false);
|
||||
const [category, setCategory] = useState<CategorySelection>(EMPTY_CATEGORY);
|
||||
const [categoryOpen, setCategoryOpen] = useState(false);
|
||||
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
|
||||
const [cityCode, setCityCode] = useState<string>(FALLBACK_CITY_CODE);
|
||||
const regionLabel = formatRegionLabel(region);
|
||||
const categoryLabel = formatCategoryLabel(category);
|
||||
|
||||
const childIdsByParent = useMemo(() => {
|
||||
const map = new Map<string, string[]>();
|
||||
for (const root of categoryTree) {
|
||||
map.set(
|
||||
root.id,
|
||||
(root.children ?? []).map((c) => c.id),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}, [categoryTree]);
|
||||
|
||||
useDidShow(() => {
|
||||
syncTabBarSelected(1);
|
||||
@@ -49,6 +71,12 @@ export default function StoresPage() {
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void request<StoreCategoryNode[]>('/store-categories')
|
||||
.then((tree) => setCategoryTree(Array.isArray(tree) ? tree : []))
|
||||
.catch(() => setCategoryTree([]));
|
||||
}, []);
|
||||
|
||||
const loadStores = useCallback(() => {
|
||||
setLoading(true);
|
||||
const path = cityCode ? `/stores?cityCode=${encodeURIComponent(cityCode)}` : '/stores';
|
||||
@@ -82,13 +110,37 @@ export default function StoresPage() {
|
||||
})();
|
||||
});
|
||||
|
||||
function matchesCategory(store: Store): boolean {
|
||||
if (!category.parentId) return true;
|
||||
const storeCatId = String(store.categoryId || store.category?.id || '');
|
||||
const storeParentId = String(store.category?.parentId || '');
|
||||
if (category.childId) {
|
||||
return storeCatId === category.childId;
|
||||
}
|
||||
if (storeParentId && storeParentId === category.parentId) return true;
|
||||
const siblings = childIdsByParent.get(category.parentId) ?? [];
|
||||
return siblings.includes(storeCatId);
|
||||
}
|
||||
|
||||
const filtered = stores.filter((s) => {
|
||||
if (!matchesRegionFilter(s, region)) return false;
|
||||
if (!matchesCategory(s)) return false;
|
||||
if (!keyword.trim()) return true;
|
||||
const q = keyword.trim();
|
||||
return s.name.includes(q) || (s.address ?? '').includes(q) || (s.district ?? '').includes(q);
|
||||
});
|
||||
|
||||
function applySearch() {
|
||||
setKeyword(keywordInput.trim());
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
setKeywordInput('');
|
||||
setKeyword('');
|
||||
setCategory(EMPTY_CATEGORY);
|
||||
setRegion(DEFAULT_REGION);
|
||||
}
|
||||
|
||||
function formatHours(store: Store) {
|
||||
if (store.openTime && store.closeTime) {
|
||||
return `营业时间: ${store.openTime}-${store.closeTime}`;
|
||||
@@ -99,29 +151,35 @@ export default function StoresPage() {
|
||||
return (
|
||||
<PageShell variant="tab" className="store-page no-tab-header">
|
||||
<TabMainHeader title="门店" />
|
||||
<View className="store-toolbar">
|
||||
<View className="store-location" onClick={() => setRegionOpen(true)}>
|
||||
<View className="store-location-pin" />
|
||||
<Text className="store-location-text">{regionLabel} ▾</Text>
|
||||
</View>
|
||||
<Input
|
||||
className="store-search"
|
||||
placeholder="搜索门店"
|
||||
value={keyword}
|
||||
onInput={(e) => setKeyword(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="store-category-tabs">
|
||||
{CATEGORY_TABS.map((tab) => (
|
||||
<Text
|
||||
key={tab}
|
||||
className={`store-category-tab${categoryTab === tab ? ' store-category-tab--active' : ''}`}
|
||||
onClick={() => setCategoryTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
<View className="store-filter">
|
||||
<View className="store-search-row">
|
||||
<Input
|
||||
className="store-search-input"
|
||||
placeholder="搜索门店名称/地址"
|
||||
value={keywordInput}
|
||||
confirmType="search"
|
||||
onInput={(e) => setKeywordInput(e.detail.value)}
|
||||
onConfirm={applySearch}
|
||||
/>
|
||||
<View className="store-search-btn" onClick={applySearch} aria-label="搜索">
|
||||
<View className="store-search-icon" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="store-filter-row">
|
||||
<View className="store-filter-chip" onClick={() => setRegionOpen(true)}>
|
||||
<Text className="store-filter-chip-text">{regionLabel}</Text>
|
||||
<Text className="store-filter-chip-arrow">▾</Text>
|
||||
</View>
|
||||
<View className="store-filter-chip" onClick={() => setCategoryOpen(true)}>
|
||||
<Text className="store-filter-chip-text">{categoryLabel}</Text>
|
||||
<Text className="store-filter-chip-arrow">▾</Text>
|
||||
</View>
|
||||
<Text className="store-filter-reset" onClick={resetFilters}>
|
||||
重置
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="store-list">
|
||||
@@ -171,6 +229,13 @@ export default function StoresPage() {
|
||||
onClose={() => setRegionOpen(false)}
|
||||
onConfirm={(next) => setRegion(next)}
|
||||
/>
|
||||
<CategoryPicker
|
||||
open={categoryOpen}
|
||||
tree={categoryTree}
|
||||
value={category}
|
||||
onClose={() => setCategoryOpen(false)}
|
||||
onConfirm={(next) => setCategory(next)}
|
||||
/>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user