首页优化

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
+1
View File
@@ -1,3 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '首页',
navigationStyle: 'custom',
})
+152 -19
View File
@@ -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
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>