首页优化
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页',
|
||||
navigationStyle: 'custom',
|
||||
})
|
||||
|
||||
+152
-19
@@ -4,17 +4,62 @@
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 顶部搜索 =====
|
||||
.home-search {
|
||||
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
|
||||
padding: 16rpx 24rpx;
|
||||
// ===== 自定义顶部导航栏 =====
|
||||
.home-header {
|
||||
background: linear-gradient(160deg, #d60410 0%, #ee0a24 55%, #ff4d4f 100%);
|
||||
// 底部留白供轮播图上移叠放
|
||||
padding-bottom: 88rpx;
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 32rpx 8rpx;
|
||||
}
|
||||
|
||||
&__brand {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #ffffff;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
&__slogan {
|
||||
margin-left: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
&__notice {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__search {
|
||||
padding: 8rpx 16rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 轮播图 =====
|
||||
.home-banner {
|
||||
margin: 20rpx 24rpx 0;
|
||||
// 上移叠在红色导航栏上,形成视觉连贯
|
||||
margin: -72rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 24rpx rgba(238, 10, 36, 0.15);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&__swiper {
|
||||
border-radius: 20rpx;
|
||||
@@ -22,52 +67,127 @@
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
height: 300rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(135deg, #ff9a9e, #ff4d4f);
|
||||
height: 300rpx;
|
||||
background: linear-gradient(135deg, #ff8a5c, #ff4d4f);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__placeholder-text {
|
||||
&__placeholder-title {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
&__placeholder-sub {
|
||||
margin-top: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 导航菜单 =====
|
||||
// ===== 宫格导航 =====
|
||||
.home-menu {
|
||||
margin: 20rpx 24rpx 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 0 8rpx;
|
||||
padding: 28rpx 0 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 24rpx;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__image {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-size: 38rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 促销推荐卡片 =====
|
||||
.home-promo {
|
||||
margin: 20rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
position: relative;
|
||||
height: 180rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, rgba(160, 4, 16, 0.82) 0%, rgba(238, 10, 36, 0.45) 55%, rgba(238, 10, 36, 0) 100%);
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #ffffff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 24rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 推荐商品 =====
|
||||
.home-recommend {
|
||||
margin: 20rpx 24rpx 0;
|
||||
margin: 28rpx 24rpx 0;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
@@ -76,6 +196,19 @@
|
||||
padding: 8rpx 8rpx 20rpx;
|
||||
}
|
||||
|
||||
&__title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title-bar {
|
||||
width: 8rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 4rpx;
|
||||
background: linear-gradient(180deg, #ee0a24, #ff6034);
|
||||
margin-right: 14rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
@@ -178,7 +311,7 @@
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background: #ee0a24;
|
||||
background: linear-gradient(135deg, #ee0a24, #ff6034);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
+135
-73
@@ -1,46 +1,68 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Grid, GridItem, Search, Swiper, SwiperItem } from '@antmjs/vantui'
|
||||
import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import useCartStore from '@/stores/cart/useCartStore'
|
||||
import { getCategoriesApi, getProductListApi } from '@/services/product'
|
||||
import { getHomeConfigApi } from '@/services/home'
|
||||
import type { HomeConfig } from '@/services/home'
|
||||
import { getProductListApi } from '@/services/product'
|
||||
import { getProductCover } from '@/types/product'
|
||||
import type { Category, Product } from '@/types/product'
|
||||
import type { Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
|
||||
const PENDING_CATEGORY_KEY = 'product_category_id'
|
||||
const PENDING_KEYWORD_KEY = 'product_keyword'
|
||||
|
||||
/** 分类菜单色块配色 */
|
||||
const MENU_COLORS = ['#ee0a24', '#ff9f43', '#07c160', '#1989fa', '#8a5cf6', '#00b8d9']
|
||||
/** tabBar 页面路径(link 跳转需改用 switchTab) */
|
||||
const TAB_PATHS = [
|
||||
'pages/index/index',
|
||||
'pages/product/index',
|
||||
'pages/cart/index',
|
||||
'pages/message/index',
|
||||
'pages/profile/index',
|
||||
]
|
||||
|
||||
/** 宫格导航无图时的兜底色块配色(生鲜红橙系) */
|
||||
const NAV_COLORS = ['#ee0a24', '#ff7a1a', '#07c160', '#1989fa', '#8a5cf6', '#ff976a', '#00b8d9', '#f56c6c']
|
||||
|
||||
/** 获取状态栏高度(H5 端为 0) */
|
||||
function getStatusBarHeight(): number {
|
||||
try {
|
||||
const info = typeof Taro.getWindowInfo === 'function' ? Taro.getWindowInfo() : Taro.getSystemInfoSync()
|
||||
return info.statusBarHeight || 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
const addItem = useCartStore(s => s.addItem)
|
||||
|
||||
/** 顶级分类(导航菜单前 6 个) */
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
/** 首页配置(轮播图 / 宫格导航 / 促销卡片) */
|
||||
const [config, setConfig] = useState<HomeConfig>({ banners: [], navs: [], promos: [] })
|
||||
/** 推荐商品 */
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
/** 搜索框输入 */
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
const loggedIn = !!token
|
||||
const statusBarHeight = useMemo(() => getStatusBarHeight(), [])
|
||||
|
||||
useDidShow(() => {
|
||||
loadCategories()
|
||||
if (loggedIn) {
|
||||
loadHomeConfig()
|
||||
loadRecommend()
|
||||
}
|
||||
})
|
||||
|
||||
/** 商品分类 */
|
||||
const loadCategories = useCallback(async () => {
|
||||
/** 首页配置聚合数据(需登录) */
|
||||
const loadHomeConfig = useCallback(async () => {
|
||||
try {
|
||||
const res = await getCategoriesApi()
|
||||
setCategories(res.data.filter(c => c.parent_id === 0))
|
||||
const res = await getHomeConfigApi()
|
||||
setConfig(res.data)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
@@ -56,17 +78,26 @@ export default function IndexPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 轮播数据:取有图商品的前 4 张图 */
|
||||
const bannerImages = useMemo(
|
||||
() => products.filter(p => getProductCover(p)).slice(0, 4),
|
||||
[products],
|
||||
)
|
||||
/**
|
||||
* 后台配置的 link 统一跳转:
|
||||
* - 空字符串不跳转
|
||||
* - tabBar 页面用 switchTab,其余用 navigateTo
|
||||
*/
|
||||
const handleLink = useCallback((link: string) => {
|
||||
if (!link) return
|
||||
const path = link.split('?')[0].replace(/^\//, '')
|
||||
if (TAB_PATHS.includes(path)) {
|
||||
Taro.switchTab({ url: `/${path}` })
|
||||
} else {
|
||||
Taro.navigateTo({ url: link })
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 跳转商品页并带上分类/关键词 */
|
||||
const goProduct = useCallback((categoryId?: number, kw?: string) => {
|
||||
/** 跳转商品页并带上关键词 */
|
||||
const goProduct = useCallback((kw?: string) => {
|
||||
try {
|
||||
if (categoryId !== undefined) Taro.setStorageSync(PENDING_CATEGORY_KEY, categoryId)
|
||||
if (kw !== undefined) Taro.setStorageSync(PENDING_KEYWORD_KEY, kw)
|
||||
Taro.removeStorageSync(PENDING_CATEGORY_KEY)
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
@@ -75,7 +106,7 @@ export default function IndexPage() {
|
||||
|
||||
/** 搜索框聚焦/提交 → 商品页搜索 */
|
||||
const handleSearchFocus = useCallback(() => {
|
||||
goProduct(undefined, keyword.trim())
|
||||
goProduct(keyword.trim())
|
||||
}, [goProduct, keyword])
|
||||
|
||||
/** 快捷加购 */
|
||||
@@ -94,82 +125,113 @@ export default function IndexPage() {
|
||||
|
||||
return (
|
||||
<View className='home-page'>
|
||||
{/* ========== 顶部搜索 ========== */}
|
||||
<View className='home-search'>
|
||||
<Search
|
||||
value={keyword}
|
||||
placeholder='搜索商品名称/规格'
|
||||
shape='round'
|
||||
background='transparent'
|
||||
onChange={e => setKeyword(String(e.detail))}
|
||||
onFocus={handleSearchFocus}
|
||||
onSearch={handleSearchFocus}
|
||||
/>
|
||||
{/* ========== 自定义顶部导航栏 ========== */}
|
||||
<View className='home-header' style={{ paddingTop: `${statusBarHeight}px` }}>
|
||||
<View className='home-header__bar'>
|
||||
<View className='home-header__brand'>
|
||||
<Text className='home-header__title'>鲜采直供</Text>
|
||||
<Text className='home-header__slogan'>产地直采 · 新鲜直达</Text>
|
||||
</View>
|
||||
<View className='home-header__notice' onClick={() => Taro.switchTab({ url: '/pages/message/index' })}>
|
||||
<Icon name='bell' size='44rpx' color='#ffffff' />
|
||||
</View>
|
||||
</View>
|
||||
<View className='home-header__search'>
|
||||
<Search
|
||||
value={keyword}
|
||||
placeholder='搜索商品名称/规格'
|
||||
shape='round'
|
||||
background='transparent'
|
||||
onChange={e => setKeyword(String(e.detail))}
|
||||
onFocus={handleSearchFocus}
|
||||
onSearch={handleSearchFocus}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 轮播图 ========== */}
|
||||
<View className='home-banner'>
|
||||
<Swiper
|
||||
className='home-banner__swiper'
|
||||
height='320rpx'
|
||||
height='300rpx'
|
||||
autoPlay={3000}
|
||||
loop
|
||||
paginationVisible
|
||||
paginationColor='#ffffff'
|
||||
>
|
||||
{bannerImages.length > 0 ? (
|
||||
bannerImages.map((product, idx) => (
|
||||
<SwiperItem key={product.id}>
|
||||
<Image
|
||||
className='home-banner__image'
|
||||
src={getProductCover(product)}
|
||||
mode='aspectFill'
|
||||
onClick={() => goProduct()}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))
|
||||
{config.banners.length > 0 ? (
|
||||
config.banners.map(banner =>
|
||||
banner.image_url ? (
|
||||
<SwiperItem key={banner.id}>
|
||||
<Image
|
||||
className='home-banner__image'
|
||||
src={banner.image_url}
|
||||
mode='aspectFill'
|
||||
onClick={() => handleLink(banner.link)}
|
||||
/>
|
||||
</SwiperItem>
|
||||
) : null,
|
||||
)
|
||||
) : (
|
||||
<SwiperItem>
|
||||
<View className='home-banner__placeholder' onClick={() => goProduct()}>
|
||||
<Text className='home-banner__placeholder-text'>订货采购</Text>
|
||||
<View className='home-banner__placeholder'>
|
||||
<Text className='home-banner__placeholder-title'>新鲜直采</Text>
|
||||
<Text className='home-banner__placeholder-sub'>每日凌晨采货 · 天亮前到店</Text>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
)}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* ========== 导航菜单 ========== */}
|
||||
<View className='home-menu'>
|
||||
<Grid columnNum={4} border={false} iconSize={52}>
|
||||
{categories.slice(0, 6).map((cat, idx) => (
|
||||
<GridItem
|
||||
key={cat.id}
|
||||
text={cat.name}
|
||||
onClick={() => goProduct(cat.id)}
|
||||
renderIcon={
|
||||
<View className='menu-icon' style={{ background: MENU_COLORS[idx % MENU_COLORS.length] }}>
|
||||
<Text className='menu-icon__text'>{cat.name.slice(0, 1)}</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{/* ========== 宫格导航(一行四个) ========== */}
|
||||
{config.navs.length > 0 && (
|
||||
<View className='home-menu'>
|
||||
<Grid columnNum={4} border={false} iconSize={88}>
|
||||
{config.navs.map((nav, idx) => (
|
||||
<GridItem
|
||||
key={nav.id}
|
||||
text={nav.name}
|
||||
onClick={() => handleLink(nav.link)}
|
||||
renderIcon={
|
||||
nav.image_url ? (
|
||||
<Image className='menu-icon__image' src={nav.image_url} mode='aspectFit' />
|
||||
) : (
|
||||
<View className='menu-icon' style={{ background: NAV_COLORS[idx % NAV_COLORS.length] }}>
|
||||
<Text className='menu-icon__text'>{nav.name.slice(0, 1)}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 促销推荐卡片 ========== */}
|
||||
{config.promos.length > 0 && (
|
||||
<View className='home-promo'>
|
||||
{config.promos.map(promo => (
|
||||
<View key={promo.id} className='promo-card' onClick={() => handleLink(promo.link)}>
|
||||
{promo.image_url && (
|
||||
<Image className='promo-card__bg' src={promo.image_url} mode='aspectFill' lazyLoad />
|
||||
)}
|
||||
{promo.title && <View className='promo-card__mask' />}
|
||||
<View className='promo-card__content'>
|
||||
<Text className='promo-card__title'>{promo.title}</Text>
|
||||
{promo.sub_title && <Text className='promo-card__sub'>{promo.sub_title}</Text>}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<GridItem
|
||||
icon='apps-o'
|
||||
text='全部商品'
|
||||
onClick={() => goProduct()}
|
||||
/>
|
||||
<GridItem
|
||||
icon='orders-o'
|
||||
text='我的订单'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/order-list/index?status=all' })}
|
||||
/>
|
||||
</Grid>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 推荐商品 ========== */}
|
||||
<View className='home-recommend'>
|
||||
<View className='home-recommend__header'>
|
||||
<Text className='home-recommend__title'>推荐商品</Text>
|
||||
<View className='home-recommend__title-wrap'>
|
||||
<View className='home-recommend__title-bar' />
|
||||
<Text className='home-recommend__title'>推荐商品</Text>
|
||||
</View>
|
||||
<Text className='home-recommend__more' onClick={() => goProduct()}>查看更多 ›</Text>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user