This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
+158
View File
@@ -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>
);
}