商品以及商品详情

This commit is contained in:
liu
2026-07-10 20:14:43 +08:00
parent c25cf0e8b3
commit 3022990ea1
93 changed files with 4576 additions and 1008 deletions
+4
View File
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '搜索',
})
+194
View File
@@ -0,0 +1,194 @@
/* ========================================
搜索页
======================================== */
.search-page {
min-height: 100vh;
background: #f7f8fa;
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.navbar-content {
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
}
.navbar-back {
width: 60px;
height: 100%;
display: flex;
align-items: center;
flex-shrink: 0;
.back-icon {
font-size: 56px;
color: #1a1a1a;
line-height: 1;
}
}
.navbar-search {
flex: 1;
min-width: 0;
}
.navbar-search-btn {
flex-shrink: 0;
padding: 8px 8px;
.search-btn-text {
font-size: 28px;
color: #1989fa;
font-weight: 500;
}
}
/* ========== 滚动区域 ========== */
.search-scroll {
height: calc(100vh - 100px);
}
/* ========== section 通用 ========== */
.section {
margin: 20px 24px;
background: #fff;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.section-title {
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
}
.clear-btn {
padding: 4px;
.clear-icon {
font-size: 28px;
color: #969799;
}
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tag-item {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
background: #f7f8fa;
border-radius: 32px;
transition: opacity 0.2s;
&:active {
opacity: 0.6;
}
&.hot {
background: #fff7e6;
}
.rank {
font-size: 24px;
font-weight: 700;
color: #ff6034;
}
.tag-text {
font-size: 26px;
color: #646566;
}
}
/* ========== 搜索结果 ========== */
.results-section {
padding: 0 24px;
}
.results-header {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 20px 0 16px;
.results-title {
font-size: 28px;
color: #646566;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.results-keyword {
color: #1989fa;
font-weight: 600;
}
.results-count {
font-size: 24px;
color: #969799;
flex-shrink: 0;
margin-left: 12px;
}
}
.results-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.result-post-wrap {
/* ForumPostCard 自带 padding 和 shadow */
}
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 0;
.empty-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.4;
}
.empty-text {
font-size: 28px;
color: #969799;
}
}
.bottom-safe {
height: 40px;
}
+227
View File
@@ -0,0 +1,227 @@
import { useState, useMemo, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Search, Tag } from '@antmjs/vantui'
import { shops } from '@/mock/shops'
import { forumPosts } from '@/mock/forum'
import ShopCard from '@/components/ShopCard'
import ForumPostCard from '@/components/ForumPostCard'
import type { Article } from '@/types/article'
import type { Shop } from '@/types/shop'
import './index.less'
/** 状态栏 + 导航栏高度 */
function getNavBarHeight() {
try {
const sys = Taro.getSystemInfoSync()
return { statusBarHeight: sys.statusBarHeight || 20, navBarHeight: 44 }
} catch {
return { statusBarHeight: 20, navBarHeight: 44 }
}
}
/** 搜索结果项的联合类型 */
type SearchResult =
| { kind: 'shop'; data: Shop }
| { kind: 'post'; data: Article }
export default function SearchPage() {
const [keyword, setKeyword] = useState('')
const [submittedKeyword, setSubmittedKeyword] = useState('')
const [history, setHistory] = useState<string[]>(['麻辣烫', '二手 iPad', '考研资料'])
const [navBarHeight, setNavBarHeight] = useState({ statusBarHeight: 20, navBarHeight: 44 })
useEffect(() => {
setNavBarHeight(getNavBarHeight())
}, [])
/** 热门搜索 */
const hotKeywords = ['麻辣烫', '奶茶', '二手 iPad', '考研资料', '黄焖鸡', '失物招领', '六级', '外卖红包']
/** 执行搜索(综合匹配商家和帖子) */
const results = useMemo<SearchResult[]>(() => {
if (!submittedKeyword.trim()) return []
const kw = submittedKeyword.toLowerCase().trim()
const matchedShops: Shop[] = shops.filter(
(s) =>
s.name.toLowerCase().includes(kw) ||
s.description.toLowerCase().includes(kw) ||
s.tags.some((t) => t.toLowerCase().includes(kw)),
)
const matchedPosts: Article[] = forumPosts.filter(
(p) =>
p.title.toLowerCase().includes(kw) ||
p.excerpt.toLowerCase().includes(kw) ||
p.source.toLowerCase().includes(kw),
)
return [
...matchedShops.map((s) => ({ kind: 'shop' as const, data: s })),
...matchedPosts.map((p) => ({ kind: 'post' as const, data: p })),
]
}, [submittedKeyword])
/** 返回 */
const handleBack = () => {
Taro.navigateBack()
}
/** 点击搜索 */
const handleSearch = () => {
const kw = keyword.trim()
if (!kw) {
Taro.showToast({ title: '请输入搜索关键词', icon: 'none' })
return
}
setSubmittedKeyword(kw)
// 保存到历史(去重,最多 8 条)
setHistory((prev) => {
const next = [kw, ...prev.filter((h) => h !== kw)]
return next.slice(0, 8)
})
}
/** 点击历史/热门 */
const handleQuickKeyword = (kw: string) => {
setKeyword(kw)
setSubmittedKeyword(kw)
setHistory((prev) => {
const next = [kw, ...prev.filter((h) => h !== kw)]
return next.slice(0, 8)
})
}
/** 清空历史 */
const handleClearHistory = () => {
setHistory([])
}
/** 商家点击 */
const handleShopClick = (shop: Shop) => {
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
}
/** 帖子点击 */
const handlePostClick = (post: Article) => {
Taro.navigateTo({ url: `/pages/article/index?id=${post.id}` })
}
return (
<View className='search-page'>
{/* ========== 自定义导航栏(带返回 + 搜索框 + 搜索按钮) ========== */}
<View
className='custom-navbar'
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
>
<View
className='navbar-content'
style={{ height: `${navBarHeight.navBarHeight}px` }}
>
<View className='navbar-back' onClick={handleBack}>
<Text className='back-icon'></Text>
</View>
<View className='navbar-search'>
<Search
value={keyword}
placeholder='搜索商家、菜品、帖子'
shape='round'
background='#f2f3f5'
onChange={(e) => setKeyword(e.detail.value)}
onSearch={handleSearch}
/>
</View>
<View className='navbar-search-btn' onClick={handleSearch}>
<Text className='search-btn-text'></Text>
</View>
</View>
</View>
{/* 占位 */}
<View style={{ height: `${navBarHeight.statusBarHeight + navBarHeight.navBarHeight}px` }} />
<ScrollView className='search-scroll' scrollY enhanced showScrollbar={false}>
{/* ========== 搜索结果(已提交关键词时显示) ========== */}
{submittedKeyword ? (
<View className='results-section'>
<View className='results-header'>
<Text className='results-title'>
"<Text className='results-keyword'>{submittedKeyword}</Text>"
</Text>
<Text className='results-count'> {results.length} </Text>
</View>
{results.length > 0 ? (
<View className='results-list'>
{results.map((r) =>
r.kind === 'shop' ? (
<ShopCard
key={`shop-${r.data.id}`}
shop={r.data}
onClick={handleShopClick}
/>
) : (
<View key={`post-${r.data.id}`} className='result-post-wrap'>
<ForumPostCard post={r.data} onClick={handlePostClick} />
</View>
),
)}
</View>
) : (
<View className='empty-tip'>
<Text className='empty-icon'>🔍</Text>
<Text className='empty-text'></Text>
</View>
)}
</View>
) : (
<>
{/* ========== 搜索历史 ========== */}
{history.length > 0 && (
<View className='section'>
<View className='section-header'>
<Text className='section-title'></Text>
<View className='clear-btn' onClick={handleClearHistory}>
<Text className='clear-icon'>🗑</Text>
</View>
</View>
<View className='tag-list'>
{history.map((h) => (
<View
key={h}
className='tag-item'
onClick={() => handleQuickKeyword(h)}
>
<Text className='tag-text'>{h}</Text>
</View>
))}
</View>
</View>
)}
{/* ========== 热门搜索 ========== */}
<View className='section'>
<View className='section-header'>
<Text className='section-title'></Text>
</View>
<View className='tag-list'>
{hotKeywords.map((kw, idx) => (
<View
key={kw}
className={`tag-item ${idx < 3 ? 'hot' : ''}`}
onClick={() => handleQuickKeyword(kw)}
>
{idx < 3 && <Text className='rank'>{idx + 1}</Text>}
<Text className='tag-text'>{kw}</Text>
</View>
))}
</View>
</View>
</>
)}
<View className='bottom-safe' />
</ScrollView>
</View>
)
}