首页优化

This commit is contained in:
liu
2026-08-14 12:34:25 +08:00
parent b288502e0e
commit d1d8d7ab52
15 changed files with 950 additions and 260 deletions
+135 -73
View File
@@ -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>