init
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import AppImage from '@dukang/shared-ui/AppImage';
|
||||
import { request } from '../lib/api';
|
||||
import TabMainHeader from '../components/TabMainHeader';
|
||||
|
||||
type StoreItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
district: string;
|
||||
address: string;
|
||||
coverUrl?: string | null;
|
||||
status: string;
|
||||
openTime?: string | null;
|
||||
closeTime?: string | null;
|
||||
category?: { name: string } | null;
|
||||
};
|
||||
|
||||
const CATEGORY_TABS = ['全部', '火锅', '地方菜', '高端餐饮', '烧烤烤肉', '西餐'] as const;
|
||||
const MOCK_DISTANCES = ['800m', '1.2km', '3.5km', '1.5km', '2.0km'];
|
||||
|
||||
function storeCover(store: StoreItem, index: number) {
|
||||
if (store.coverUrl) return String(store.coverUrl);
|
||||
const imgs = ['/images/1.png', '/images/2.png', '/images/3.png'];
|
||||
return imgs[index % imgs.length];
|
||||
}
|
||||
|
||||
function formatHours(store: StoreItem) {
|
||||
if (store.openTime && store.closeTime) {
|
||||
return `营业时间: ${store.openTime}-${store.closeTime}`;
|
||||
}
|
||||
return '营业时间: 10:00-22:00';
|
||||
}
|
||||
|
||||
export default function StoreListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [stores, setStores] = useState<StoreItem[]>([]);
|
||||
const [categoryTab, setCategoryTab] = useState<string>('全部');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
request<StoreItem[]>('USER_H5', '/stores?cityCode=410100').then(setStores);
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let list = stores;
|
||||
if (categoryTab !== '全部') {
|
||||
list = list.filter((s) => s.category?.name === categoryTab);
|
||||
}
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (q) {
|
||||
list = list.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) ||
|
||||
s.address.toLowerCase().includes(q) ||
|
||||
s.district.toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [stores, categoryTab, keyword]);
|
||||
|
||||
return (
|
||||
<div className="page store-page">
|
||||
<header className="store-header">
|
||||
<TabMainHeader title="门店" />
|
||||
|
||||
<div className="store-toolbar">
|
||||
<button type="button" className="store-location">
|
||||
<span className="material-symbols-outlined">location_on</span>
|
||||
<span>郑州市 · 金水区</span>
|
||||
<span className="material-symbols-outlined store-location-arrow">expand_more</span>
|
||||
</button>
|
||||
<div className="store-search">
|
||||
<span className="material-symbols-outlined store-search-icon">search</span>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="搜索门店名称或地址"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="store-category-bar">
|
||||
<div className="store-category-tabs">
|
||||
{CATEGORY_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
className={`store-category-tab${categoryTab === tab ? ' active' : ''}`}
|
||||
onClick={() => setCategoryTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="store-list">
|
||||
{filtered.map((s, index) => (
|
||||
<article
|
||||
key={s.id}
|
||||
className="store-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => navigate(`/stores/${s.id}`)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
navigate(`/stores/${s.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="store-card-cover">
|
||||
<AppImage src={storeCover(s, index)} alt={s.name} wrapperClassName="app-image--fill" />
|
||||
</div>
|
||||
<div className="store-card-body">
|
||||
<div className="store-card-top">
|
||||
<div className="store-card-title-row">
|
||||
<h3 className="store-card-name">{s.name}</h3>
|
||||
<span className="store-card-distance">
|
||||
{MOCK_DISTANCES[index % MOCK_DISTANCES.length]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="store-card-meta">
|
||||
<span className="status-open">营业中</span>
|
||||
<span className="store-card-hours">{formatHours(s)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="store-card-bottom">
|
||||
<p className="store-card-address">
|
||||
{s.district}
|
||||
{s.address}
|
||||
</p>
|
||||
<Link
|
||||
to="/redeem"
|
||||
className="store-redeem-btn"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
去核销
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="store-empty">{stores.length === 0 ? '暂无门店' : '未找到匹配门店'}</div>
|
||||
)}
|
||||
|
||||
{filtered.length > 0 && (
|
||||
<p className="store-list-end">没有更多门店了</p>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user