商品以及商品详情
This commit is contained in:
@@ -306,11 +306,34 @@
|
||||
}
|
||||
|
||||
/* 文章不存在 */
|
||||
.empty-article {
|
||||
.empty-article-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-size: 30px;
|
||||
color: #969799;
|
||||
padding: 200px 32px 0;
|
||||
|
||||
.empty-article-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-article-text {
|
||||
font-size: 30px;
|
||||
color: #969799;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.empty-article-back-btn {
|
||||
padding: 16px 48px;
|
||||
background: #1989fa;
|
||||
border-radius: 40px;
|
||||
}
|
||||
|
||||
.empty-article-back-text {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,27 @@ export default function ArticleDetail() {
|
||||
if (!article) {
|
||||
return (
|
||||
<View className='article-page'>
|
||||
<View className='empty-article'>文章不存在</View>
|
||||
{/* ========== 自定义导航栏(带返回) ========== */}
|
||||
<View className='article-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='nav-content'>
|
||||
<View className='nav-back' onClick={handleBack}>
|
||||
<Icon name='arrow-left' size={22} color='#323233' />
|
||||
</View>
|
||||
<Text className='nav-title'>文章详情</Text>
|
||||
<View className='nav-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 占位 */}
|
||||
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||
|
||||
<View className='empty-article-wrap'>
|
||||
<Text className='empty-article-icon'>📄</Text>
|
||||
<Text className='empty-article-text'>文章不存在</Text>
|
||||
<View className='empty-article-back-btn' onClick={handleBack}>
|
||||
<Text className='empty-article-back-text'>返回</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '校园外卖',
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
/* ========================================
|
||||
校园外卖首页
|
||||
======================================== */
|
||||
|
||||
.food-delivery-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏(仅标题) ========== */
|
||||
.custom-navbar {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.navbar-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* ========== 主体滚动区域 ========== */
|
||||
.page-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ========== 主体内容区内的搜索栏 ========== */
|
||||
.inline-search-section {
|
||||
padding: 20px 24px 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ========== 顶部 5 个分类导航 ========== */
|
||||
.category-section {
|
||||
margin: 0 24px 12px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px 8px 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ========== 列表头 ========== */
|
||||
.shop-list-header {
|
||||
padding: 16px 24px 8px;
|
||||
|
||||
.shop-list-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
|
||||
.shop-list-count {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 商家列表 ========== */
|
||||
.shop-list {
|
||||
margin: 0 24px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 80px 0;
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.bottom-safe {
|
||||
height: 40px;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Search, Grid, GridItem } from '@antmjs/vantui'
|
||||
import ShopCard from '@/components/ShopCard'
|
||||
import { shops } from '@/mock/shops'
|
||||
import type { Shop, ShopCategory } from '@/types/shop'
|
||||
import './index.less'
|
||||
|
||||
/** 获取状态栏 + 导航栏高度 */
|
||||
function getNavBarHeight() {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
const navBarHeight = 44
|
||||
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
|
||||
}
|
||||
|
||||
/** 顶部 5 个分类导航 */
|
||||
const CATEGORY_NAV: { id: ShopCategory; label: string; icon: string }[] = [
|
||||
{ id: 'food', label: '美食', icon: '🍱' },
|
||||
{ id: 'dessert', label: '甜点', icon: '🍰' },
|
||||
{ id: 'supermarket', label: '超市便利', icon: '🛒' },
|
||||
{ id: 'medicine', label: '买药', icon: '💊' },
|
||||
{ id: 'fruit', label: '水果', icon: '🍎' },
|
||||
]
|
||||
|
||||
export default function FoodDelivery() {
|
||||
const [activeCategory, setActiveCategory] = useState<ShopCategory>('food')
|
||||
const navBarHeight = useMemo(() => getNavBarHeight(), [])
|
||||
|
||||
/** 按当前分类过滤商家 */
|
||||
const filteredShops = useMemo(
|
||||
() => shops.filter((s) => s.category === activeCategory),
|
||||
[activeCategory],
|
||||
)
|
||||
|
||||
/** 搜索 */
|
||||
const handleSearch = () => {
|
||||
Taro.navigateTo({ url: '/pages/search/index' })
|
||||
}
|
||||
|
||||
/** 点击商家 */
|
||||
const handleShopClick = (shop: Shop) => {
|
||||
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='food-delivery-page'>
|
||||
{/* ========== 自定义导航栏(仅标题) ========== */}
|
||||
<View
|
||||
className='custom-navbar'
|
||||
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
|
||||
>
|
||||
<View
|
||||
className='navbar-content'
|
||||
style={{ height: `${navBarHeight.navBarHeight}px` }}
|
||||
>
|
||||
<View className='navbar-title-wrap'>
|
||||
<Text className='navbar-title'>校园外卖</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 占位 */}
|
||||
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
|
||||
|
||||
{/* ========== 主体内容(搜索 + 分类 + 列表)全部可滚动 ========== */}
|
||||
<ScrollView
|
||||
className='page-scroll'
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
>
|
||||
{/* 搜索栏(主体内容区顶部) */}
|
||||
<View className='inline-search-section'>
|
||||
<Search
|
||||
placeholder='搜索商家、菜品'
|
||||
onClickInput={handleSearch}
|
||||
shape='round'
|
||||
background='#f2f3f5'
|
||||
readonly
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 5 个分类导航 */}
|
||||
<View className='category-section'>
|
||||
<Grid columnNum={5} border={false} gutter={4}>
|
||||
{CATEGORY_NAV.map((cat) => (
|
||||
<GridItem
|
||||
key={cat.id}
|
||||
icon={cat.icon}
|
||||
text={cat.label}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</View>
|
||||
|
||||
{/* 列表头 */}
|
||||
<View className='shop-list-header'>
|
||||
<Text className='shop-list-title'>
|
||||
{CATEGORY_NAV.find((c) => c.id === activeCategory)?.label || ''} ·{' '}
|
||||
<Text className='shop-list-count'>{filteredShops.length} 家</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 商家列表 */}
|
||||
<View className='shop-list'>
|
||||
{filteredShops.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} onClick={handleShopClick} />
|
||||
))}
|
||||
{filteredShops.length === 0 && (
|
||||
<View className='empty-tip'>该分类下暂无商家</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '校园论坛',
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
/* ========================================
|
||||
校园论坛首页
|
||||
======================================== */
|
||||
|
||||
.forum-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏(标题 + 发布) ========== */
|
||||
.custom-navbar {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-title-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.navbar-publish {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f7f8fa;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.publish-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 主体滚动区域 ========== */
|
||||
.page-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ========== 主体内容区内的搜索栏 ========== */
|
||||
.inline-search-section {
|
||||
padding: 20px 24px 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ========== 顶部 5 个子分类导航 ========== */
|
||||
.category-section {
|
||||
margin: 0 24px 12px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 8px 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ========== 列表头 ========== */
|
||||
.post-list-header {
|
||||
padding: 16px 24px 12px;
|
||||
|
||||
.post-list-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
|
||||
.post-list-count {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 帖子列表(独立卡片,16px 间距) ========== */
|
||||
.post-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 80px 0;
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.bottom-safe {
|
||||
height: 40px;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Search, Grid, GridItem } from '@antmjs/vantui'
|
||||
import ForumPostCard from '@/components/ForumPostCard'
|
||||
import { getForumPostsBySubCategory } from '@/mock/forum'
|
||||
import type { Article, ForumSubCategory } from '@/types/article'
|
||||
import './index.less'
|
||||
|
||||
/** 获取状态栏 + 导航栏高度 */
|
||||
function getNavBarHeight() {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
const navBarHeight = 44
|
||||
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
|
||||
}
|
||||
|
||||
/** 论坛 5 个子分类 */
|
||||
const FORUM_CATEGORIES: { id: ForumSubCategory; label: string; icon: string }[] = [
|
||||
{ id: 'life', label: '校园生活', icon: '🌸' },
|
||||
{ id: 'study', label: '学习交流', icon: '📚' },
|
||||
{ id: 'lost-found', label: '失物招领', icon: '🔍' },
|
||||
{ id: 'secondhand', label: '二手交易', icon: '🛍' },
|
||||
{ id: 'tree-hole', label: '树洞', icon: '🌳' },
|
||||
]
|
||||
|
||||
export default function Forum() {
|
||||
const [activeCategory, setActiveCategory] = useState<ForumSubCategory>('life')
|
||||
const navBarHeight = useMemo(() => getNavBarHeight(), [])
|
||||
|
||||
/** 按当前子分类过滤帖子 */
|
||||
const filteredPosts = useMemo(
|
||||
() => getForumPostsBySubCategory(activeCategory),
|
||||
[activeCategory],
|
||||
)
|
||||
|
||||
/** 搜索 */
|
||||
const handleSearch = () => {
|
||||
Taro.navigateTo({ url: '/pages/search/index' })
|
||||
}
|
||||
|
||||
/** 帖子点击 */
|
||||
const handlePostClick = (post: Article) => {
|
||||
Taro.navigateTo({ url: `/pages/article/index?id=${post.id}` })
|
||||
}
|
||||
|
||||
/** 发布帖子 */
|
||||
const handlePublish = () => {
|
||||
Taro.showToast({ title: '发布功能即将上线', icon: 'none' })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='forum-page'>
|
||||
{/* ========== 自定义导航栏(标题 + 发布) ========== */}
|
||||
<View
|
||||
className='custom-navbar'
|
||||
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
|
||||
>
|
||||
<View
|
||||
className='navbar-content'
|
||||
style={{ height: `${navBarHeight.navBarHeight}px` }}
|
||||
>
|
||||
<View className='navbar-title-wrap'>
|
||||
<Text className='navbar-title'>校园论坛</Text>
|
||||
</View>
|
||||
<View className='navbar-publish' onClick={handlePublish}>
|
||||
<Text className='publish-icon'>✏️</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 占位 */}
|
||||
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
|
||||
|
||||
{/* ========== 主体内容(搜索 + 分类 + 列表)全部可滚动 ========== */}
|
||||
<ScrollView
|
||||
className='page-scroll'
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
>
|
||||
{/* 搜索栏(主体内容区顶部) */}
|
||||
<View className='inline-search-section'>
|
||||
<Search
|
||||
placeholder='搜索帖子、用户'
|
||||
onClickInput={handleSearch}
|
||||
shape='round'
|
||||
background='#f2f3f5'
|
||||
readonly
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 5 个子分类导航 */}
|
||||
<View className='category-section'>
|
||||
<Grid columnNum={5} border={false} gutter={4}>
|
||||
{FORUM_CATEGORIES.map((cat) => (
|
||||
<GridItem
|
||||
key={cat.id}
|
||||
icon={cat.icon}
|
||||
text={cat.label}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</View>
|
||||
|
||||
{/* 列表头 */}
|
||||
<View className='post-list-header'>
|
||||
<Text className='post-list-title'>
|
||||
{FORUM_CATEGORIES.find((c) => c.id === activeCategory)?.label || ''} ·{' '}
|
||||
<Text className='post-list-count'>{filteredPosts.length} 条</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 帖子列表 */}
|
||||
<View className='post-list'>
|
||||
{filteredPosts.map((post) => (
|
||||
<ForumPostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onClick={handlePostClick}
|
||||
/>
|
||||
))}
|
||||
{filteredPosts.length === 0 && (
|
||||
<View className='empty-tip'>该板块下暂无帖子</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
+61
-78
@@ -80,8 +80,8 @@
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ========== 统一板块 ========== */
|
||||
.main-tabs-section {
|
||||
/* ========== 推荐商家 section ========== */
|
||||
.recommended-shops-section {
|
||||
margin: 0 24px 20px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
@@ -89,92 +89,72 @@
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 校园商城内容 */
|
||||
.mall-content {
|
||||
padding: 16px 20px 24px;
|
||||
}
|
||||
|
||||
/* 分类标签 */
|
||||
.category-scroll {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
.category-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 24px;
|
||||
border-radius: 32px;
|
||||
background: #f7f8fa;
|
||||
font-size: 26px;
|
||||
color: #646566;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background: #1989fa;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 商品双列网格 */
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.empty-grid {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
/* 商家入驻入口 */
|
||||
.merchant-join-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
|
||||
border: 1px dashed #1989fa;
|
||||
border-radius: 12px;
|
||||
padding: 24px 24px 12px;
|
||||
|
||||
.join-content {
|
||||
.section-title-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-emoji {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.join-icon { font-size: 48px; }
|
||||
.section-more-text {
|
||||
font-size: 24px;
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.join-text-wrap {
|
||||
.join-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.section-more-arrow {
|
||||
font-size: 28px;
|
||||
color: #1989fa;
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.join-desc {
|
||||
font-size: 24px;
|
||||
color: #969799;
|
||||
}
|
||||
.recommended-shops-list {
|
||||
/* 卡片之间用 1px 顶边线分割 */
|
||||
.shop-card {
|
||||
padding: 20px 20px;
|
||||
|
||||
.shop-image-wrap {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.join-arrow {
|
||||
font-size: 36px;
|
||||
color: #1989fa;
|
||||
font-weight: 300;
|
||||
}
|
||||
/* ========== 统一板块 ========== */
|
||||
.main-tabs-section {
|
||||
margin: 0 24px 20px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 文章列表内容 */
|
||||
@@ -224,8 +204,11 @@
|
||||
}
|
||||
|
||||
.meta-read {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.meta-comment {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-112
@@ -3,20 +3,16 @@ import Taro from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Search, Swiper, SwiperItem, Grid, GridItem, Tabs, Tab, Image as VantImage } from '@antmjs/vantui'
|
||||
import ArticleCard from '@/components/ArticleCard'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import ShopCard from '@/components/ShopCard'
|
||||
import { banners, navItems, articles } from '@/mock/articles'
|
||||
import { products } from '@/mock/products'
|
||||
import type { ArticleCategory } from '@/types/article'
|
||||
import type { ProductCategory } from '@/types/product'
|
||||
import type { Product } from '@/types/product'
|
||||
import CustomTabBar from "@/custom-tab-bar";
|
||||
import { getRecommendedShops } from '@/mock/shops'
|
||||
import type { Shop } from '@/types/shop'
|
||||
import './index.less'
|
||||
|
||||
/** 获取状态栏和导航栏高度 */
|
||||
function getNavBarHeight() {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
// 微信小程序导航栏高度一般为 44px,总高度 = 状态栏 + 导航栏
|
||||
const navBarHeight = 44
|
||||
return {
|
||||
statusBarHeight,
|
||||
@@ -25,45 +21,28 @@ function getNavBarHeight() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 统一板块标签:校园商城 + 文章分类 */
|
||||
/** 文章板块标签(校园商城 Tab 已移除) */
|
||||
const MAIN_TABS = [
|
||||
{ title: '校园商城', value: 'mall' },
|
||||
{ title: '最新', value: 'latest' },
|
||||
{ title: '推荐', value: 'recommend' },
|
||||
{ title: '热点', value: 'hot' },
|
||||
]
|
||||
|
||||
/** 商品分类标签 */
|
||||
const PRODUCT_CATEGORIES: { title: string; value: ProductCategory | 'all' }[] = [
|
||||
{ title: '全部', value: 'all' },
|
||||
{ title: '数码', value: 'digital' },
|
||||
{ title: '图书', value: 'book' },
|
||||
{ title: '生活', value: 'life' },
|
||||
{ title: '美食', value: 'food' },
|
||||
{ title: '服饰', value: 'cloth' },
|
||||
]
|
||||
|
||||
export default function Index() {
|
||||
const [activeMainTab, setActiveMainTab] = useState(0)
|
||||
const [activeCategory, setActiveCategory] = useState(0)
|
||||
const navBarHeight = useMemo(() => getNavBarHeight(), [])
|
||||
|
||||
/** 当前选中的板块 */
|
||||
const currentTab = MAIN_TABS[activeMainTab]?.value || 'mall'
|
||||
const currentTab = MAIN_TABS[activeMainTab]?.value || 'latest'
|
||||
|
||||
/** 根据文章分类筛 */
|
||||
const filteredArticles = useMemo(() => {
|
||||
const value = MAIN_TABS[activeMainTab]?.value || 'latest'
|
||||
if (value === 'mall') return articles.filter((a) => a.category === 'latest')
|
||||
return articles.filter((a) => a.category === value)
|
||||
}, [activeMainTab])
|
||||
const filteredArticles = useMemo(
|
||||
() => articles.filter((a) => a.category === currentTab),
|
||||
[currentTab],
|
||||
)
|
||||
|
||||
/** 根据当前分类筛选商品 */
|
||||
const filteredProducts = useMemo(() => {
|
||||
const catValue = PRODUCT_CATEGORIES[activeCategory]?.value || 'all'
|
||||
if (catValue === 'all') return products
|
||||
return products.filter((p) => p.category === catValue)
|
||||
}, [activeCategory])
|
||||
/** 推荐商家(首页显示前 3 个) */
|
||||
const recommendedShops = useMemo(() => getRecommendedShops(3), [])
|
||||
|
||||
/** 搜索 */
|
||||
const handleSearch = () => {
|
||||
@@ -84,19 +63,14 @@ export default function Index() {
|
||||
Taro.navigateTo({ url: `/pages/article/index?id=${id}` })
|
||||
}
|
||||
|
||||
/** 商品点击 */
|
||||
const handleProductClick = (product: Product) => {
|
||||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${product.id}` })
|
||||
/** 商家点击 */
|
||||
const handleShopClick = (shop: Shop) => {
|
||||
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
|
||||
}
|
||||
|
||||
/** 查看全部商品 */
|
||||
const handleViewAll = () => {
|
||||
Taro.showToast({ title: '商城即将上线', icon: 'none' })
|
||||
}
|
||||
|
||||
/** 商家入驻 */
|
||||
const handleMerchantJoin = () => {
|
||||
Taro.showToast({ title: '商家入驻即将上线', icon: 'none' })
|
||||
/** 查看全部商家 */
|
||||
const handleViewAllShops = () => {
|
||||
Taro.navigateTo({ url: '/pages/food-delivery/index' })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -170,7 +144,27 @@ export default function Index() {
|
||||
</Grid>
|
||||
</View>
|
||||
|
||||
{/* ========== 统一板块:校园商城 | 最新 | 推荐 | 热点 ========== */}
|
||||
{/* ========== 推荐商家 ========== */}
|
||||
<View className='recommended-shops-section'>
|
||||
<View className='section-header'>
|
||||
<View className='section-title-wrap'>
|
||||
<Text className='section-emoji'>🍱</Text>
|
||||
<Text className='section-title'>附近商家</Text>
|
||||
<Text className='section-subtitle'>校园外卖精选</Text>
|
||||
</View>
|
||||
<View className='section-more' onClick={handleViewAllShops}>
|
||||
<Text className='section-more-text'>查看全部</Text>
|
||||
<Text className='section-more-arrow'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='recommended-shops-list'>
|
||||
{recommendedShops.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} onClick={handleShopClick} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 文章板块:最新 | 推荐 | 热点 ========== */}
|
||||
<View className='main-tabs-section'>
|
||||
<Tabs
|
||||
active={activeMainTab}
|
||||
@@ -188,81 +182,26 @@ export default function Index() {
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{/* ===== 校园商城内容 ===== */}
|
||||
{currentTab === 'mall' && (
|
||||
<View className='mall-content'>
|
||||
{/* 商品分类标签 */}
|
||||
<ScrollView
|
||||
className='category-scroll'
|
||||
scrollX
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
{/* 文章列表内容 */}
|
||||
<View className='article-content'>
|
||||
{filteredArticles.map((article) => (
|
||||
<View
|
||||
key={article.id}
|
||||
className='article-item'
|
||||
onClick={() => handleArticleClick(article.id)}
|
||||
>
|
||||
<View className='category-tabs'>
|
||||
{PRODUCT_CATEGORIES.map((cat, idx) => (
|
||||
<View
|
||||
key={cat.value}
|
||||
className={`category-tab ${activeCategory === idx ? 'active' : ''}`}
|
||||
onClick={() => setActiveCategory(idx)}
|
||||
>
|
||||
<Text className='category-text'>{cat.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 商品双列网格 */}
|
||||
<View className='product-grid'>
|
||||
{filteredProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
onClick={handleProductClick}
|
||||
/>
|
||||
))}
|
||||
{filteredProducts.length === 0 && (
|
||||
<View className='empty-grid'>暂无商品</View>
|
||||
)}
|
||||
<ArticleCard article={article} />
|
||||
</View>
|
||||
|
||||
{/* 商家入驻入口 */}
|
||||
<View className='merchant-join-card' onClick={handleMerchantJoin}>
|
||||
<View className='join-content'>
|
||||
<Text className='join-icon'>🏪</Text>
|
||||
<View className='join-text-wrap'>
|
||||
<Text className='join-title'>商家入驻</Text>
|
||||
<Text className='join-desc'>拥有自己的店铺,开启校园创业之旅</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='join-arrow'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ===== 文章列表内容 ===== */}
|
||||
{currentTab !== 'mall' && (
|
||||
<View className='article-content'>
|
||||
{filteredArticles.map((article) => (
|
||||
<View
|
||||
key={article.id}
|
||||
className='article-item'
|
||||
onClick={() => handleArticleClick(article.id)}
|
||||
>
|
||||
<ArticleCard article={article} />
|
||||
</View>
|
||||
))}
|
||||
{filteredArticles.length === 0 && (
|
||||
<View className='empty-tip'>暂无文章</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
))}
|
||||
{filteredArticles.length === 0 && (
|
||||
<View className='empty-tip'>暂无文章</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部安全距离 */}
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
|
||||
{/*<CustomTabBar activeKey='index' />*/}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
/* ========================================
|
||||
商品详情页
|
||||
======================================== */
|
||||
|
||||
.product-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========== 导航栏 ========== */
|
||||
.detail-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.nav-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
|
||||
.nav-back {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 50px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 滚动区域 ========== */
|
||||
.detail-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ========== 图片轮播 ========== */
|
||||
.image-swiper {
|
||||
width: 100%;
|
||||
height: 750px;
|
||||
background: #fff;
|
||||
|
||||
.swiper-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 价格信息卡片 ========== */
|
||||
.price-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.current-price {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #ee0a24;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
font-size: 24px;
|
||||
color: #ee0a24;
|
||||
background: #fff0f0;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-name {
|
||||
display: block;
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.product-sales {
|
||||
font-size: 26px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 规格选择行 ========== */
|
||||
.sku-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.sku-label {
|
||||
font-size: 28px;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
.sku-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 商家信息 ========== */
|
||||
.merchant-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.merchant-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.merchant-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.merchant-name {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.merchant-arrow {
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 商品描述 ========== */
|
||||
.desc-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.desc-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.desc-content {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
color: #646566;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 推荐商品 ========== */
|
||||
.recommend-section {
|
||||
padding: 0 24px 16px;
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
border-left: 6px solid #1989fa;
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
.empty-recommend {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 底部安全区 ========== */
|
||||
.scroll-bottom-safe {
|
||||
height: 120px;
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import {
|
||||
Swiper,
|
||||
SwiperItem,
|
||||
Tag,
|
||||
Image as VantImage,
|
||||
Icon,
|
||||
GoodsAction,
|
||||
GoodsActionIcon,
|
||||
GoodsActionButton,
|
||||
Stepper,
|
||||
Sku,
|
||||
Popup,
|
||||
Button,
|
||||
} from '@antmjs/vantui'
|
||||
import type { ISkuItem, IGoodItem } from '@antmjs/vantui/types/sku'
|
||||
import { products } from '@/mock/products'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import type { Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
/* ==============================
|
||||
辅助函数
|
||||
============================== */
|
||||
|
||||
function getStatusBarH(): number {
|
||||
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||
catch { return 20 }
|
||||
}
|
||||
|
||||
function getSafeBottom(): number {
|
||||
try {
|
||||
const info = Taro.getSystemInfoSync()
|
||||
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||
} catch { return 0 }
|
||||
}
|
||||
|
||||
/** 根据商品生成简单 SKU 数据 */
|
||||
function buildSkuData(product: Product) {
|
||||
const goodsList: IGoodItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
skuIds: [1, 11],
|
||||
price: Math.round(product.price * 100),
|
||||
stock: 999,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
skuIds: [1, 12],
|
||||
price: Math.round(product.price * 100),
|
||||
stock: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
skuIds: [2, 11],
|
||||
price: Math.round(product.price * 100) + 1000,
|
||||
stock: 500,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
skuIds: [2, 12],
|
||||
price: Math.round(product.price * 100) + 1000,
|
||||
stock: 300,
|
||||
},
|
||||
]
|
||||
|
||||
const sku: ISkuItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '颜色',
|
||||
items: [
|
||||
{ id: 1, name: '黑色' },
|
||||
{ id: 2, name: '白色' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '版本',
|
||||
items: [
|
||||
{ id: 11, name: '标准版' },
|
||||
{ id: 12, name: '高配版' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return { goodsList, sku }
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
页面组件
|
||||
============================== */
|
||||
|
||||
export default function ProductDetail() {
|
||||
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
|
||||
|
||||
/* ---- 数据 ---- */
|
||||
const product = useMemo(() => products.find((p) => p.id === id), [id])
|
||||
|
||||
const statusBarH = useMemo(getStatusBarH, [])
|
||||
const safeBottom = useMemo(getSafeBottom, [])
|
||||
|
||||
const [isFavorited, setIsFavorited] = useState(false)
|
||||
|
||||
/* ---- SKU 弹窗 ---- */
|
||||
const [skuVisible, setSkuVisible] = useState(false)
|
||||
const [skuAction, setSkuAction] = useState<'cart' | 'buy'>('cart')
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
|
||||
const skuData = useMemo(() => {
|
||||
if (!product) return { goodsList: [], sku: [] }
|
||||
return buildSkuData(product)
|
||||
}, [product])
|
||||
|
||||
/* ---- 推荐商品 ---- */
|
||||
const recommendedProducts = useMemo(() => {
|
||||
if (!product) return []
|
||||
return products
|
||||
.filter((p) => p.category === product.category && p.id !== product.id)
|
||||
.slice(0, 4)
|
||||
}, [product])
|
||||
|
||||
/* ---- 事件 ---- */
|
||||
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
|
||||
|
||||
const handleNavigateToDetail = useCallback((targetId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${targetId}` })
|
||||
}, [])
|
||||
|
||||
/** 打开 SKU 弹窗 */
|
||||
const openSku = (action: 'cart' | 'buy') => {
|
||||
setSkuAction(action)
|
||||
setSkuVisible(true)
|
||||
}
|
||||
|
||||
const handleSkuClose = () => setSkuVisible(false)
|
||||
|
||||
/** SKU 确认 */
|
||||
const handleSkuConfirm = () => {
|
||||
setSkuVisible(false)
|
||||
if (skuAction === 'cart') {
|
||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
} else {
|
||||
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||
}
|
||||
setQuantity(1)
|
||||
}
|
||||
|
||||
/* ---- 空状态 ---- */
|
||||
if (!product) {
|
||||
return (
|
||||
<View className='product-detail-page'>
|
||||
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='nav-content'>
|
||||
<View className='nav-back' onClick={handleBack}>
|
||||
<Icon name='arrow-left' size={22} color='#323233' />
|
||||
</View>
|
||||
<Text className='nav-title'>商品详情</Text>
|
||||
<View className='nav-actions' />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||
<View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1 }}>
|
||||
<Text style={{ color: '#969799', fontSize: '30px' }}>商品不存在</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const discount = product.originalPrice
|
||||
? Math.round((product.price / product.originalPrice) * 10)
|
||||
: null
|
||||
|
||||
return (
|
||||
<View className='product-detail-page'>
|
||||
{/* ===== 导航栏 ===== */}
|
||||
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='nav-content'>
|
||||
<View className='nav-back' onClick={handleBack}>
|
||||
<Icon name='arrow-left' size={22} color='#323233' />
|
||||
</View>
|
||||
<Text className='nav-title'>商品详情</Text>
|
||||
<View className='nav-actions' />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||
|
||||
{/* ===== 滚动内容 ===== */}
|
||||
<ScrollView className='detail-scroll' scrollY enhanced showScrollbar={false}>
|
||||
{/* 图片轮播 — 使用商品图片 + 模拟多张 */}
|
||||
<Swiper
|
||||
className='image-swiper'
|
||||
autoPlay={0}
|
||||
loop
|
||||
paginationVisible
|
||||
paginationColor='#1989fa'
|
||||
height={750}
|
||||
>
|
||||
{[product.image, product.image, product.image].map((img, idx) => (
|
||||
<SwiperItem key={idx}>
|
||||
<VantImage
|
||||
className='swiper-image'
|
||||
src={img}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* 价格信息 */}
|
||||
<View className='price-card'>
|
||||
<View className='price-row'>
|
||||
<Text className='current-price'>¥{product.price.toFixed(2)}</Text>
|
||||
{product.originalPrice && (
|
||||
<Text className='original-price'>¥{product.originalPrice.toFixed(2)}</Text>
|
||||
)}
|
||||
{discount && discount < 10 && (
|
||||
<Text className='discount-badge'>{discount}折</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='product-name'>{product.title}</Text>
|
||||
{product.tags && product.tags.length > 0 && (
|
||||
<View className='product-tags'>
|
||||
{product.tags.map((t) => (
|
||||
<Tag key={t} type='danger' plain>
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
<Text className='product-sales'>已售 {product.sales}</Text>
|
||||
</View>
|
||||
|
||||
{/* 规格选择 */}
|
||||
<View className='sku-select-row' onClick={() => openSku('cart')}>
|
||||
<Text className='sku-label'>已选</Text>
|
||||
<View className='sku-value'>
|
||||
<Text>颜色: 黑色 / 版本: 标准版 ×{quantity}</Text>
|
||||
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 商家信息 */}
|
||||
<View className='merchant-card'>
|
||||
<View className='merchant-info'>
|
||||
<VantImage
|
||||
className='merchant-avatar'
|
||||
src={product.merchant.avatar}
|
||||
fit='cover'
|
||||
width={72}
|
||||
height={72}
|
||||
round
|
||||
/>
|
||||
<Text className='merchant-name'>{product.merchant.name}</Text>
|
||||
</View>
|
||||
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||
</View>
|
||||
|
||||
{/* 商品描述 */}
|
||||
<View className='desc-card'>
|
||||
<Text className='desc-title'>商品描述</Text>
|
||||
<Text className='desc-content'>{product.desc}</Text>
|
||||
</View>
|
||||
|
||||
{/* 推荐商品 */}
|
||||
<View className='recommend-section'>
|
||||
<Text className='section-title'>推荐商品</Text>
|
||||
<View className='recommend-grid'>
|
||||
{recommendedProducts.length > 0 ? (
|
||||
recommendedProducts.map((p) => (
|
||||
<ProductCard
|
||||
key={p.id}
|
||||
product={p}
|
||||
onClick={(prod) => handleNavigateToDetail(prod.id)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text className='empty-recommend'>暂无推荐</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='scroll-bottom-safe' />
|
||||
</ScrollView>
|
||||
|
||||
{/* ===== 底部操作栏 ===== */}
|
||||
<GoodsAction safeAreaInsetBottom={false}>
|
||||
<GoodsActionIcon
|
||||
icon='chat-o'
|
||||
text='客服'
|
||||
onClick={() => Taro.showToast({ title: '联系客服中...', icon: 'none' })}
|
||||
/>
|
||||
<GoodsActionIcon
|
||||
icon={isFavorited ? 'star' : 'star-o'}
|
||||
text='收藏'
|
||||
color={isFavorited ? '#ff976a' : undefined}
|
||||
onClick={() => {
|
||||
setIsFavorited(!isFavorited)
|
||||
Taro.showToast({
|
||||
title: isFavorited ? '已取消收藏' : '已收藏',
|
||||
icon: 'none',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<GoodsActionIcon
|
||||
icon='cart-o'
|
||||
text='购物车'
|
||||
info='0'
|
||||
onClick={() => Taro.showToast({ title: '购物车即将上线', icon: 'none' })}
|
||||
/>
|
||||
<GoodsActionButton
|
||||
text='加入购物车'
|
||||
type='warning'
|
||||
onClick={() => openSku('cart')}
|
||||
/>
|
||||
<GoodsActionButton
|
||||
text='立即购买'
|
||||
type='danger'
|
||||
onClick={() => openSku('buy')}
|
||||
/>
|
||||
</GoodsAction>
|
||||
|
||||
{/* ===== SKU 选择弹窗 ===== */}
|
||||
<Popup
|
||||
show={skuVisible}
|
||||
position='bottom'
|
||||
round
|
||||
rootPortal
|
||||
onClose={handleSkuClose}
|
||||
overlayStyle={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
>
|
||||
<View style={{ background: '#fff', paddingBottom: `${safeBottom}px` }}>
|
||||
{/* 弹窗头部 — 商品概要 */}
|
||||
<View style={{ display: 'flex', padding: '24px', borderBottom: '1px solid #f2f3f5' }}>
|
||||
<VantImage
|
||||
src={product.image}
|
||||
fit='cover'
|
||||
width={160}
|
||||
height={160}
|
||||
radius={8}
|
||||
/>
|
||||
<View style={{ marginLeft: '20px', flex: 1 }}>
|
||||
<Text style={{ fontSize: '40px', fontWeight: 700, color: '#ee0a24', display: 'block' }}>
|
||||
¥{product.price.toFixed(2)}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
color: '#969799',
|
||||
display: 'block',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
库存 999 件
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* SKU 规格选择 */}
|
||||
<View style={{ padding: '0 24px', maxHeight: '500px', overflowY: 'auto' }}>
|
||||
<Sku
|
||||
goodsId={1}
|
||||
goodsList={skuData.goodsList}
|
||||
sku={skuData.sku}
|
||||
onChange={(goods) => {
|
||||
if (goods) {
|
||||
// SKU 变化时更新价格
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 数量选择 */}
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '24px',
|
||||
borderTop: '1px solid #f2f3f5',
|
||||
borderBottom: '1px solid #f2f3f5',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '28px', color: '#646566' }}>数量</Text>
|
||||
<Stepper
|
||||
value={quantity}
|
||||
min={1}
|
||||
max={99}
|
||||
onChange={(e) => setQuantity(Number(e.detail))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 确认按钮 */}
|
||||
<View style={{ padding: '24px' }}>
|
||||
<Button
|
||||
type={skuAction === 'buy' ? 'danger' : 'warning'}
|
||||
round
|
||||
block
|
||||
onClick={handleSkuConfirm}
|
||||
>
|
||||
{skuAction === 'buy' ? '立即购买' : '加入购物车'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '搜索',
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '',
|
||||
})
|
||||
@@ -0,0 +1,870 @@
|
||||
/* ========================================
|
||||
店铺首页
|
||||
======================================== */
|
||||
|
||||
.shop-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 120px; /* 留出 SubmitBar 空间 */
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏 ========== */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-back {
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.back-icon {
|
||||
font-size: 56px;
|
||||
color: #1a1a1a;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.navbar-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-action {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-icon {
|
||||
font-size: 36px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 店铺头 ========== */
|
||||
.shop-header {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
|
||||
.header-bg-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(1.2);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.header-bg-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, rgba(255, 255, 255, 1) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 0 24px 20px;
|
||||
margin-top: -50px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-logo-wrap {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 4px solid #fff;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.header-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
padding-bottom: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.header-name {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-rating-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 24px;
|
||||
color: #646566;
|
||||
|
||||
.rating-value {
|
||||
color: #ff6034;
|
||||
font-weight: 600;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.header-sep {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.sales {
|
||||
color: #646566;
|
||||
}
|
||||
}
|
||||
|
||||
.header-fee-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
|
||||
.fee-sep {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
.header-announcement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin: 0 24px 20px;
|
||||
padding: 16px 20px;
|
||||
background: #fff7e6;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.announcement-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #ff976a;
|
||||
background: #fff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.announcement-text {
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
color: #646566;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 点菜 Tab ========== */
|
||||
.menu-tab {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-body {
|
||||
display: flex;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.menu-sidebar {
|
||||
width: 200px;
|
||||
background: #f7f8fa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-list-scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-category {
|
||||
padding: 20px 20px 4px;
|
||||
|
||||
& + .menu-category {
|
||||
border-top: 8px solid #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0 16px;
|
||||
|
||||
.category-name {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
|
||||
& + .menu-item {
|
||||
border-top: 1px solid #f2f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-image-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-right: 16px;
|
||||
|
||||
.menu-item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-item-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.menu-item-name {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.menu-item-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-desc {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-top: 6px;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item-sales {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.menu-item-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
|
||||
.menu-item-price {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #ee0a24;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.menu-item-original {
|
||||
font-size: 22px;
|
||||
color: #c8c9cc;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stepper-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1989fa;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.minus {
|
||||
background: #fff;
|
||||
border: 2px solid #1989fa;
|
||||
}
|
||||
|
||||
&.plus {
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
.stepper-btn-text {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
color: #1989fa;
|
||||
font-weight: 600;
|
||||
|
||||
&.plus {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-qty {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== 评价 Tab ========== */
|
||||
.reviews-tab {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.reviews-scroll {
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 600px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.reviews-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px 32px;
|
||||
margin: 16px 0;
|
||||
|
||||
.summary-rating {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
border-right: 1px solid #f2f3f5;
|
||||
|
||||
.summary-rating-value {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #ff6034;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.summary-rating-label {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-num {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reviews-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.review-card {
|
||||
padding: 24px;
|
||||
|
||||
& + .review-card {
|
||||
border-top: 1px solid #f2f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
.review-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.review-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.review-meta {
|
||||
flex: 1;
|
||||
|
||||
.review-name {
|
||||
font-size: 26px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.review-rate-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.review-time {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.review-content {
|
||||
font-size: 28px;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.review-image-wrap {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.review-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.review-dishes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
.review-dish-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 商家 Tab ========== */
|
||||
.merchant-tab {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.merchant-scroll {
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 600px;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.merchant-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.merchant-section-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.merchant-photos {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.merchant-photo-wrap {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.merchant-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.merchant-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.merchant-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.merchant-info-list {
|
||||
.merchant-info-row {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
font-size: 26px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
font-size: 26px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.merchant-announcement {
|
||||
padding: 16px;
|
||||
background: #fff7e6;
|
||||
border-radius: 8px;
|
||||
|
||||
.announcement-text {
|
||||
font-size: 26px;
|
||||
color: #646566;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 底部购物车条 ========== */
|
||||
.submit-bar-wrap {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100px;
|
||||
background: #2c2c2c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.cart-icon-btn {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #1989fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -30px;
|
||||
box-shadow: 0 4px 12px rgba(25, 137, 250, 0.4);
|
||||
|
||||
.cart-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
border: 2px solid #2c2c2c;
|
||||
|
||||
.cart-badge-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-info {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
color: #fff;
|
||||
|
||||
.cart-price {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cart-fee {
|
||||
font-size: 22px;
|
||||
color: #c8c9cc;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 80px;
|
||||
padding: 0 32px;
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.active {
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #646566;
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 购物车 Popup ========== */
|
||||
.cart-sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.cart-sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px 32px 16px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
.cart-sheet-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.cart-clear {
|
||||
padding: 8px 0;
|
||||
|
||||
.cart-clear-text {
|
||||
font-size: 24px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cart-sheet-list {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
max-height: 600px;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.cart-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.cart-row-image-wrap {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-row-info {
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
min-width: 0;
|
||||
|
||||
.cart-row-name {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cart-row-price {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-sheet-empty {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
font-size: 26px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.cart-sheet-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 32px;
|
||||
border-top: 1px solid #f2f3f5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cart-sheet-total {
|
||||
flex: 1;
|
||||
|
||||
.cart-sheet-total-label {
|
||||
font-size: 24px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.cart-sheet-total-price {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #ee0a24;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.cart-sheet-total-fee {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-sheet-submit {
|
||||
padding: 16px 40px;
|
||||
border-radius: 32px;
|
||||
background: #1989fa;
|
||||
|
||||
&.disabled {
|
||||
background: #c8c9cc;
|
||||
}
|
||||
|
||||
.cart-sheet-submit-text {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 通用 ========== */
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
font-size: 26px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.bottom-safe {
|
||||
height: 40px;
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Sidebar,
|
||||
SidebarItem,
|
||||
Image as VantImage,
|
||||
Tag,
|
||||
Rate,
|
||||
Stepper,
|
||||
Popup,
|
||||
} from '@antmjs/vantui'
|
||||
import { getShopById } from '@/mock/shops'
|
||||
import { getMenuCategories, getMenuItems } from '@/mock/menus'
|
||||
import { getShopReviews } from '@/mock/reviews'
|
||||
import type { Shop, MenuItem, MenuCategory, Review } from '@/types/shop'
|
||||
import './index.less'
|
||||
|
||||
/** 状态栏 + 导航栏高度 */
|
||||
function getNavBarHeight() {
|
||||
const systemInfo = Taro.getSystemInfoSync()
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||||
const navBarHeight = 44
|
||||
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
|
||||
}
|
||||
|
||||
export default function ShopDetail() {
|
||||
// === 路由参数 ===
|
||||
const [shop, setShop] = useState<Shop | null>(null)
|
||||
const [categories, setCategories] = useState<MenuCategory[]>([])
|
||||
const [items, setItems] = useState<MenuItem[]>([])
|
||||
const [reviews, setReviews] = useState<Review[]>([])
|
||||
const [navBarHeight, setNavBarHeight] = useState({ statusBarHeight: 20, navBarHeight: 44, totalHeight: 64 })
|
||||
|
||||
// === UI 状态 ===
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0)
|
||||
const [cart, setCart] = useState<Record<string, number>>({})
|
||||
const [cartSheetVisible, setCartSheetVisible] = useState(false)
|
||||
|
||||
/** 加载数据 */
|
||||
useEffect(() => {
|
||||
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
|
||||
if (!id) {
|
||||
Taro.showToast({ title: '参数错误', icon: 'none' })
|
||||
Taro.navigateBack()
|
||||
return
|
||||
}
|
||||
const found = getShopById(id)
|
||||
if (!found) {
|
||||
Taro.showToast({ title: '商家不存在', icon: 'none' })
|
||||
Taro.navigateBack()
|
||||
return
|
||||
}
|
||||
setShop(found)
|
||||
setCategories(getMenuCategories(id))
|
||||
setItems(getMenuItems(id))
|
||||
setReviews(getShopReviews(id))
|
||||
setNavBarHeight(getNavBarHeight())
|
||||
// 动态设置导航栏标题
|
||||
Taro.setNavigationBarTitle({ title: found.name })
|
||||
}, [])
|
||||
|
||||
// === 购物车派生数据 ===
|
||||
const cartCount = useMemo(
|
||||
() => Object.values(cart).reduce((sum, n) => sum + n, 0),
|
||||
[cart],
|
||||
)
|
||||
|
||||
const cartTotal = useMemo(() => {
|
||||
return Object.entries(cart).reduce((sum, [itemId, qty]) => {
|
||||
const item = items.find((i) => i.id === itemId)
|
||||
return sum + (item ? item.price * qty : 0)
|
||||
}, 0)
|
||||
}, [cart, items])
|
||||
|
||||
const cartItems = useMemo(
|
||||
() =>
|
||||
Object.entries(cart)
|
||||
.filter(([, qty]) => qty > 0)
|
||||
.map(([itemId, qty]) => ({
|
||||
item: items.find((i) => i.id === itemId)!,
|
||||
qty,
|
||||
}))
|
||||
.filter((c) => c.item),
|
||||
[cart, items],
|
||||
)
|
||||
|
||||
// === 菜单按分类分组(保持原始顺序) ===
|
||||
const groupedItems = useMemo(() => {
|
||||
return categories.map((cat) => ({
|
||||
category: cat,
|
||||
list: items.filter((i) => i.categoryId === cat.id),
|
||||
}))
|
||||
}, [categories, items])
|
||||
|
||||
// === 加购 / 减购 ===
|
||||
const handleAdd = (itemId: string) => {
|
||||
setCart((prev) => ({ ...prev, [itemId]: (prev[itemId] || 0) + 1 }))
|
||||
}
|
||||
|
||||
const handleSub = (itemId: string) => {
|
||||
setCart((prev) => {
|
||||
const next = { ...prev }
|
||||
if (!next[itemId] || next[itemId] <= 1) {
|
||||
delete next[itemId]
|
||||
} else {
|
||||
next[itemId] -= 1
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearCart = () => {
|
||||
setCart({})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
Taro.showToast({ title: `已提交 ${cartCount} 件,¥${cartTotal.toFixed(1)}`, icon: 'success' })
|
||||
setCart({})
|
||||
setCartSheetVisible(false)
|
||||
}
|
||||
|
||||
const handleCartIconClick = () => {
|
||||
if (cartCount === 0) {
|
||||
Taro.showToast({ title: '还没有选购商品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setCartSheetVisible(true)
|
||||
}
|
||||
|
||||
// === 渲染前等待 shop 加载 ===
|
||||
if (!shop) {
|
||||
return <View className='shop-detail-page' />
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='shop-detail-page'>
|
||||
{/* ========== 自定义导航栏 ========== */}
|
||||
<View
|
||||
className='custom-navbar'
|
||||
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
|
||||
>
|
||||
<View
|
||||
className='navbar-content'
|
||||
style={{ height: `${navBarHeight.navBarHeight}px` }}
|
||||
>
|
||||
<View className='navbar-back' onClick={() => Taro.navigateBack()}>
|
||||
<Text className='back-icon'>‹</Text>
|
||||
</View>
|
||||
<View className='navbar-title-wrap'>
|
||||
<Text className='navbar-title' numberOfLines={1}>
|
||||
{shop.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='navbar-action'>
|
||||
<Text className='action-icon'>⋯</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
|
||||
|
||||
{/* ========== 店铺头(背景图 + logo + 信息) ========== */}
|
||||
<View className='shop-header'>
|
||||
<View className='header-bg'>
|
||||
<VantImage
|
||||
className='header-bg-image'
|
||||
src={shop.image}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
/>
|
||||
<View className='header-bg-mask' />
|
||||
</View>
|
||||
|
||||
<View className='header-content'>
|
||||
<View className='header-logo-wrap'>
|
||||
<VantImage
|
||||
className='header-logo'
|
||||
src={shop.logo}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
round
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='header-info'>
|
||||
<View className='header-name-row'>
|
||||
<Text className='header-name'>{shop.name}</Text>
|
||||
{shop.tags.map((t) => (
|
||||
<Tag key={t} type='danger' plain size='medium' className='header-tag'>
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='header-rating-row'>
|
||||
<Rate
|
||||
value={shop.rating}
|
||||
readonly
|
||||
size={14}
|
||||
color='#ff6034'
|
||||
voidColor='#dcdee0'
|
||||
/>
|
||||
<Text className='rating-value'>{shop.rating.toFixed(1)}</Text>
|
||||
<Text className='header-sep'>·</Text>
|
||||
<Text className='sales'>月售 {shop.monthlySales}</Text>
|
||||
</View>
|
||||
|
||||
<View className='header-fee-row'>
|
||||
<Text className='fee-item'>起送 ¥{shop.minOrder}</Text>
|
||||
<Text className='fee-sep'>|</Text>
|
||||
<Text className='fee-item'>配送费 ¥{shop.deliveryFee}</Text>
|
||||
<Text className='fee-sep'>|</Text>
|
||||
<Text className='fee-item'>{shop.deliveryTime}分钟</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 公告 */}
|
||||
{shop.announcements && (
|
||||
<View className='header-announcement'>
|
||||
<Text className='announcement-label'>公告</Text>
|
||||
<Text className='announcement-text' numberOfLines={2}>
|
||||
{shop.announcements}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== Tabs: 点菜 / 评价 / 商家 ========== */}
|
||||
<Tabs
|
||||
active={activeTab}
|
||||
onChange={(e) => setActiveTab(e.detail.index)}
|
||||
sticky
|
||||
offsetTop={navBarHeight.totalHeight}
|
||||
color='#1989fa'
|
||||
titleActiveColor='#1989fa'
|
||||
titleInactiveColor='#646566'
|
||||
animated
|
||||
>
|
||||
<Tab title='点菜' />
|
||||
<Tab title={`评价(${reviews.length})`} />
|
||||
<Tab title='商家' />
|
||||
</Tabs>
|
||||
|
||||
{/* ===== Tab 0: 点菜 ===== */}
|
||||
{activeTab === 0 && (
|
||||
<View className='menu-tab'>
|
||||
<View className='menu-body'>
|
||||
{/* 左侧 Sidebar */}
|
||||
<Sidebar
|
||||
activeKey={activeCategoryIndex}
|
||||
onChange={(e) => setActiveCategoryIndex(e.detail ?? 0)}
|
||||
className='menu-sidebar'
|
||||
>
|
||||
{groupedItems.map((g, idx) => (
|
||||
<SidebarItem
|
||||
key={g.category.id}
|
||||
title={`${g.category.name} (${g.list.length})`}
|
||||
/>
|
||||
))}
|
||||
</Sidebar>
|
||||
|
||||
{/* 右侧菜单 */}
|
||||
<ScrollView className='menu-list-scroll' scrollY enhanced showScrollbar={false}>
|
||||
{groupedItems.map((g) => (
|
||||
<View key={g.category.id} className='menu-category'>
|
||||
<View className='category-header'>
|
||||
<Text className='category-name'>{g.category.name}</Text>
|
||||
<Text className='category-count'>共 {g.list.length} 道</Text>
|
||||
</View>
|
||||
{g.list.map((item) => {
|
||||
const qty = cart[item.id] || 0
|
||||
return (
|
||||
<View key={item.id} className='menu-item'>
|
||||
<View className='menu-item-image-wrap'>
|
||||
<VantImage
|
||||
className='menu-item-image'
|
||||
src={item.image}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
radius={8}
|
||||
/>
|
||||
</View>
|
||||
<View className='menu-item-info'>
|
||||
<View className='menu-item-name-row'>
|
||||
<Text className='menu-item-name' numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.tags?.map((t) => (
|
||||
<Tag
|
||||
key={t}
|
||||
type='danger'
|
||||
plain
|
||||
size='medium'
|
||||
className='menu-item-tag'
|
||||
>
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
<Text className='menu-item-desc' numberOfLines={1}>
|
||||
{item.description}
|
||||
</Text>
|
||||
<Text className='menu-item-sales'>月售 {item.sales}</Text>
|
||||
<View className='menu-item-bottom'>
|
||||
<View className='price-row'>
|
||||
<Text className='menu-item-price'>
|
||||
¥{item.price.toFixed(1)}
|
||||
</Text>
|
||||
{item.originalPrice && (
|
||||
<Text className='menu-item-original'>
|
||||
¥{item.originalPrice.toFixed(1)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='stepper-row'>
|
||||
{qty > 0 && (
|
||||
<View
|
||||
className='stepper-btn minus'
|
||||
onClick={() => handleSub(item.id)}
|
||||
>
|
||||
<Text className='stepper-btn-text'>−</Text>
|
||||
</View>
|
||||
)}
|
||||
{qty > 0 && (
|
||||
<Text className='stepper-qty'>{qty}</Text>
|
||||
)}
|
||||
<View
|
||||
className='stepper-btn plus'
|
||||
onClick={() => handleAdd(item.id)}
|
||||
>
|
||||
<Text className='stepper-btn-text plus'>+</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
))}
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ===== Tab 1: 评价 ===== */}
|
||||
{activeTab === 1 && (
|
||||
<View className='reviews-tab'>
|
||||
<ScrollView scrollY enhanced showScrollbar={false} className='reviews-scroll'>
|
||||
<View className='reviews-summary'>
|
||||
<View className='summary-rating'>
|
||||
<Text className='summary-rating-value'>{shop.rating.toFixed(1)}</Text>
|
||||
<Text className='summary-rating-label'>综合评分</Text>
|
||||
</View>
|
||||
<View className='summary-stats'>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{shop.monthlySales}</Text>
|
||||
<Text className='stat-label'>月售</Text>
|
||||
</View>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{shop.deliveryTime}分</Text>
|
||||
<Text className='stat-label'>配送时长</Text>
|
||||
</View>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{reviews.length}</Text>
|
||||
<Text className='stat-label'>评价数</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='reviews-list'>
|
||||
{reviews.length === 0 && (
|
||||
<View className='empty-tip'>暂无评价</View>
|
||||
)}
|
||||
{reviews.map((r) => (
|
||||
<View key={r.id} className='review-card'>
|
||||
<View className='review-header'>
|
||||
<VantImage
|
||||
className='review-avatar'
|
||||
src={r.author.avatar}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
round
|
||||
/>
|
||||
<View className='review-meta'>
|
||||
<Text className='review-name'>{r.author.name}</Text>
|
||||
<View className='review-rate-row'>
|
||||
<Rate
|
||||
value={r.rating}
|
||||
readonly
|
||||
size={12}
|
||||
color='#ff6034'
|
||||
voidColor='#dcdee0'
|
||||
/>
|
||||
<Text className='review-time'>{r.time}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='review-content'>{r.content}</Text>
|
||||
{r.images && r.images.length > 0 && (
|
||||
<View className='review-images'>
|
||||
{r.images.map((img, idx) => (
|
||||
<View key={idx} className='review-image-wrap'>
|
||||
<VantImage
|
||||
className='review-image'
|
||||
src={img}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
radius={6}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{r.dishes && r.dishes.length > 0 && (
|
||||
<View className='review-dishes'>
|
||||
{r.dishes.map((d) => (
|
||||
<Tag
|
||||
key={d}
|
||||
type='primary'
|
||||
plain
|
||||
size='medium'
|
||||
className='review-dish-tag'
|
||||
>
|
||||
{d}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ===== Tab 2: 商家 ===== */}
|
||||
{activeTab === 2 && (
|
||||
<View className='merchant-tab'>
|
||||
<ScrollView scrollY enhanced showScrollbar={false} className='merchant-scroll'>
|
||||
{/* 商家相册 */}
|
||||
{shop.photos.length > 0 && (
|
||||
<View className='merchant-section'>
|
||||
<Text className='merchant-section-title'>商家相册</Text>
|
||||
<View className='merchant-photos'>
|
||||
{shop.photos.map((p, idx) => (
|
||||
<View key={idx} className='merchant-photo-wrap'>
|
||||
<VantImage
|
||||
className='merchant-photo'
|
||||
src={p}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
radius={8}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 商家标签 */}
|
||||
{shop.tags.length > 0 && (
|
||||
<View className='merchant-section'>
|
||||
<Text className='merchant-section-title'>商家标签</Text>
|
||||
<View className='merchant-tags'>
|
||||
{shop.tags.map((t) => (
|
||||
<Tag key={t} type='primary' plain size='medium' className='merchant-tag'>
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 商家信息 */}
|
||||
<View className='merchant-section'>
|
||||
<Text className='merchant-section-title'>商家信息</Text>
|
||||
<View className='merchant-info-list'>
|
||||
<View className='merchant-info-row'>
|
||||
<Text className='info-label'>地址</Text>
|
||||
<Text className='info-value'>{shop.address}</Text>
|
||||
</View>
|
||||
<View className='merchant-info-row'>
|
||||
<Text className='info-label'>电话</Text>
|
||||
<Text className='info-value'>{shop.phone}</Text>
|
||||
</View>
|
||||
<View className='merchant-info-row'>
|
||||
<Text className='info-label'>营业时间</Text>
|
||||
<Text className='info-value'>{shop.businessHours}</Text>
|
||||
</View>
|
||||
<View className='merchant-info-row'>
|
||||
<Text className='info-label'>配送时间</Text>
|
||||
<Text className='info-value'>{shop.deliveryHours}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 服务设施 */}
|
||||
{shop.services.length > 0 && (
|
||||
<View className='merchant-section'>
|
||||
<Text className='merchant-section-title'>服务设施</Text>
|
||||
<View className='merchant-tags'>
|
||||
{shop.services.map((s) => (
|
||||
<Tag key={s} type='success' plain size='medium' className='merchant-tag'>
|
||||
{s}
|
||||
</Tag>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 公告 */}
|
||||
{shop.announcements && (
|
||||
<View className='merchant-section'>
|
||||
<Text className='merchant-section-title'>商家公告</Text>
|
||||
<View className='merchant-announcement'>
|
||||
<Text className='announcement-text'>{shop.announcements}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='bottom-safe' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 底部购物车 SubmitBar(仅点菜 Tab 显示)========== */}
|
||||
{activeTab === 0 && (
|
||||
<View className='submit-bar-wrap'>
|
||||
<View className='cart-icon-btn' onClick={handleCartIconClick}>
|
||||
<Text className='cart-icon'>🛒</Text>
|
||||
{cartCount > 0 && (
|
||||
<View className='cart-badge'>
|
||||
<Text className='cart-badge-text'>{cartCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='cart-info'>
|
||||
<Text className='cart-price'>
|
||||
¥{cartTotal.toFixed(1)}
|
||||
</Text>
|
||||
<Text className='cart-fee'>
|
||||
另需配送费 ¥{shop.deliveryFee}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`submit-btn ${cartCount > 0 && cartTotal >= shop.minOrder ? 'active' : 'disabled'}`}
|
||||
onClick={cartCount > 0 ? handleSubmit : undefined}
|
||||
>
|
||||
<Text className='submit-btn-text'>
|
||||
{cartCount === 0
|
||||
? `¥${shop.minOrder} 起送`
|
||||
: cartTotal < shop.minOrder
|
||||
? `还差 ¥${(shop.minOrder - cartTotal).toFixed(1)}`
|
||||
: '去结算'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 购物车 Popup ========== */}
|
||||
<Popup
|
||||
position='bottom'
|
||||
round
|
||||
show={cartSheetVisible}
|
||||
onClose={() => setCartSheetVisible(false)}
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
>
|
||||
<View className='cart-sheet'>
|
||||
<View className='cart-sheet-header'>
|
||||
<Text className='cart-sheet-title'>已选商品</Text>
|
||||
<View className='cart-clear' onClick={handleClearCart}>
|
||||
<Text className='cart-clear-text'>🗑 清空</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY enhanced showScrollbar={false} className='cart-sheet-list'>
|
||||
{cartItems.map(({ item, qty }) => (
|
||||
<View key={item.id} className='cart-row'>
|
||||
<View className='cart-row-image-wrap'>
|
||||
<VantImage
|
||||
className='cart-row-image'
|
||||
src={item.image}
|
||||
fit='cover'
|
||||
width='100%'
|
||||
height='100%'
|
||||
radius={6}
|
||||
/>
|
||||
</View>
|
||||
<View className='cart-row-info'>
|
||||
<Text className='cart-row-name' numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text className='cart-row-price'>¥{item.price.toFixed(1)}</Text>
|
||||
</View>
|
||||
<Stepper
|
||||
value={qty}
|
||||
min={0}
|
||||
theme='round'
|
||||
onPlus={() => handleAdd(item.id)}
|
||||
onMinus={() => handleSub(item.id)}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{cartItems.length === 0 && (
|
||||
<View className='cart-sheet-empty'>还没有选购商品</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View className='cart-sheet-footer'>
|
||||
<View className='cart-sheet-total'>
|
||||
<Text className='cart-sheet-total-label'>合计</Text>
|
||||
<Text className='cart-sheet-total-price'>¥{cartTotal.toFixed(1)}</Text>
|
||||
<Text className='cart-sheet-total-fee'>
|
||||
含配送费 ¥{shop.deliveryFee}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`cart-sheet-submit ${cartCount > 0 ? 'active' : 'disabled'}`}
|
||||
onClick={cartCount > 0 ? handleSubmit : undefined}
|
||||
>
|
||||
<Text className='cart-sheet-submit-text'>提交订单</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user