370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
import { useCallback, useMemo, useRef, useState } from 'react'
|
||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||
import { View, Text, Image } from '@tarojs/components'
|
||
import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui'
|
||
import useCartStore from '@/stores/cart/useCartStore'
|
||
import { getHomeConfigApi } from '@/services/home'
|
||
import type { HomeConfig } from '@/services/home'
|
||
import { getSpecialListApi, normalizeSpecialCart } from '@/services/special'
|
||
import { getProductCover } from '@/types/product'
|
||
import type { Product, ProductCartPatch } from '@/types/product'
|
||
import { getToken } from '@/utils/request'
|
||
import CartBall from '@/components/CartBall'
|
||
import CartStepper from '@/components/CartStepper'
|
||
import {formatRetailPrice, formatSpec} from '@/utils/format'
|
||
import './index.less'
|
||
import CustomTabBar from "@/components/CustomTabBar";
|
||
|
||
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
|
||
const PENDING_CATEGORY_KEY = 'product_category_id'
|
||
const PENDING_KEYWORD_KEY = 'product_keyword'
|
||
|
||
/** 特价推荐每页条数 */
|
||
const SPECIAL_PAGE_SIZE = 10
|
||
|
||
/** 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 addItem = useCartStore(s => s.addItem)
|
||
const setSummary = useCartStore(s => s.setSummary)
|
||
|
||
/** 首页配置(轮播图 / 宫格导航 / 促销卡片) */
|
||
const [config, setConfig] = useState<HomeConfig>({ banners: [], navs: [], promos: [] })
|
||
/** 特价推荐商品(后台「客户端配置 → 特价推荐」标记,价格为登录门店的等级价) */
|
||
const [specials, setSpecials] = useState<Product[]>([])
|
||
/** 特价推荐分页 */
|
||
const [specialPage, setSpecialPage] = useState(1)
|
||
const [specialHasMore, setSpecialHasMore] = useState(false)
|
||
/** 加载更多中(首屏重置不展示,避免已渲染列表下方闪烁) */
|
||
const [specialLoading, setSpecialLoading] = useState(false)
|
||
/** 特价推荐请求序号(返回 tab 重置与上拉加载并发时,仅采用最后一次响应) */
|
||
const specialSeqRef = useRef(0)
|
||
/** 是否有「加载更多」请求进行中 */
|
||
const specialLoadingRef = useRef(false)
|
||
/** 搜索框输入 */
|
||
const [keyword, setKeyword] = useState('')
|
||
|
||
const statusBarHeight = useMemo(() => getStatusBarHeight(), [])
|
||
|
||
useDidShow(() => {
|
||
loadHomeConfig()
|
||
loadSpecials(1, true)
|
||
})
|
||
|
||
/** 上拉加载更多特价推荐 */
|
||
useReachBottom(() => {
|
||
if (!specialHasMore) return
|
||
loadSpecials(specialPage + 1, false)
|
||
})
|
||
|
||
/** 首页配置聚合数据(响应附带悬浮球汇总) */
|
||
const loadHomeConfig = useCallback(async () => {
|
||
try {
|
||
const res = await getHomeConfigApi()
|
||
setConfig(res.data)
|
||
// 旧版本后端可能未返回 cart 块
|
||
if (res.data.cart) setSummary(res.data.cart)
|
||
} catch {
|
||
// 错误已由 request 层 toast
|
||
}
|
||
}, [setSummary])
|
||
|
||
/**
|
||
* 特价推荐商品(reset 时回到第一页整体替换)。
|
||
* 行结构与 /mini/product/list 一致;响应附带悬浮球汇总
|
||
*/
|
||
const loadSpecials = useCallback(
|
||
async (pageNum: number, reset: boolean) => {
|
||
if (!reset && specialLoadingRef.current) return
|
||
const seq = ++specialSeqRef.current
|
||
specialLoadingRef.current = true
|
||
if (!reset) setSpecialLoading(true)
|
||
try {
|
||
const res = await getSpecialListApi({ page: pageNum, pageSize: SPECIAL_PAGE_SIZE })
|
||
if (seq !== specialSeqRef.current) return // 已有更新的请求,丢弃本次响应
|
||
const { data, total, cart } = res.data
|
||
setSpecials(prev => (reset ? data : [...prev, ...data]))
|
||
setSpecialPage(pageNum)
|
||
setSpecialHasMore(pageNum * SPECIAL_PAGE_SIZE < total)
|
||
// 列表响应附带悬浮球汇总(旧版本后端可能未返回)
|
||
const summary = normalizeSpecialCart(cart)
|
||
if (summary) setSummary(summary)
|
||
} catch {
|
||
// 错误已由 request 层 toast
|
||
} finally {
|
||
if (seq === specialSeqRef.current) {
|
||
specialLoadingRef.current = false
|
||
setSpecialLoading(false)
|
||
}
|
||
}
|
||
},
|
||
[setSummary],
|
||
)
|
||
|
||
/**
|
||
* 后台配置的 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((kw?: string) => {
|
||
try {
|
||
if (kw !== undefined) Taro.setStorageSync(PENDING_KEYWORD_KEY, kw)
|
||
Taro.removeStorageSync(PENDING_CATEGORY_KEY)
|
||
} catch {
|
||
// noop
|
||
}
|
||
Taro.switchTab({ url: '/pages/product/index' })
|
||
}, [])
|
||
|
||
/** 跳转商品详情 */
|
||
const goDetail = useCallback((id: number) => {
|
||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` })
|
||
}, [])
|
||
|
||
/** 搜索框聚焦/提交 → 商品页搜索 */
|
||
const handleSearchFocus = useCallback(() => {
|
||
goProduct(keyword.trim())
|
||
}, [goProduct, keyword])
|
||
|
||
/** 行内加减购确认后回写特价商品项的购物车字段 */
|
||
const handleRowSync = useCallback((productId: number, patch: ProductCartPatch) => {
|
||
setSpecials(prev => prev.map(p => (p.id === productId ? { ...p, ...patch } : p)))
|
||
}, [])
|
||
|
||
/** 快捷加购(用返回的购物车行回写,卡片随即展示加减器) */
|
||
const handleQuickAdd = useCallback(
|
||
async (product: Product, e: any) => {
|
||
e.stopPropagation()
|
||
try {
|
||
const res = await addItem(product.id, 1)
|
||
handleRowSync(product.id, { cart_id: res.id, cart_quantity: res.quantity })
|
||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||
} catch {
|
||
// 错误(未设等级价等)已由 request 层 toast
|
||
}
|
||
},
|
||
[addItem, handleRowSync],
|
||
)
|
||
|
||
/** 无价格时点击:未登录引导登录,已登录但未设等级价提示原因 */
|
||
const handlePriceGuide = useCallback((e: any) => {
|
||
e.stopPropagation()
|
||
if (getToken()) {
|
||
Taro.showToast({ title: '该商品暂未设置等级价', icon: 'none' })
|
||
} else {
|
||
Taro.navigateTo({ url: '/pages/login/index' })
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<View className='home-page'>
|
||
{/* ========== 自定义顶部导航栏 ========== */}
|
||
<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='300rpx'
|
||
autoPlay={3000}
|
||
loop
|
||
paginationVisible
|
||
paginationColor='#ffffff'
|
||
>
|
||
{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'>
|
||
<Text className='home-banner__placeholder-title'>新鲜直采</Text>
|
||
<Text className='home-banner__placeholder-sub'>每日凌晨采货 · 天亮前到店</Text>
|
||
</View>
|
||
</SwiperItem>
|
||
)}
|
||
</Swiper>
|
||
</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>
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{/* ========== 特价推荐 ========== */}
|
||
<View className='home-recommend'>
|
||
<View className='home-recommend__header'>
|
||
<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>
|
||
|
||
{ specials.length === 0 ? (
|
||
<View className='home-recommend__empty'>
|
||
<Text className='home-recommend__empty-text'>暂无特价商品</Text>
|
||
</View>
|
||
) : (
|
||
<>
|
||
<View className='product-grid'>
|
||
{specials.map(product => (
|
||
<View key={product.id} className='product-card' onClick={() => goDetail(product.id)}>
|
||
<Image
|
||
className='product-card__image'
|
||
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
|
||
mode='aspectFill'
|
||
lazyLoad
|
||
/>
|
||
<View className='product-card__info'>
|
||
<Text className='product-card__name'>{product.name}</Text>
|
||
<View className='product-card__spec'>
|
||
{formatSpec(product.spec, product.unit)}{' '}
|
||
{product.price !== null && <>
|
||
单价:{formatRetailPrice(product.price, product.spec)} {product.price_unit}
|
||
</>}
|
||
</View>
|
||
<View className='product-card__bottom'>
|
||
{product.price !== null ? (
|
||
<Text className='product-card__price'>¥{product.price}</Text>
|
||
) : (
|
||
<Text
|
||
className='product-card__price product-card__price--none'
|
||
onClick={handlePriceGuide}
|
||
>
|
||
登录后查看价格
|
||
</Text>
|
||
)}
|
||
{/* 已加购展示行内加减器,否则展示快捷加购按钮 */}
|
||
{Number(product.cart_quantity ?? 0) > 0 ? (
|
||
<CartStepper product={product} onSync={handleRowSync} />
|
||
) : (
|
||
<View className='product-card__add' onClick={e => handleQuickAdd(product, e)}>
|
||
<Text className='product-card__add-icon'>+</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
{/* 加载更多状态 */}
|
||
{specialLoading && (
|
||
<View className='home-recommend__loading'>
|
||
<Text className='home-recommend__loading-text'>加载中…</Text>
|
||
</View>
|
||
)}
|
||
{!specialHasMore && specialPage > 1 && (
|
||
<View className='home-recommend__loading'>
|
||
<Text className='home-recommend__loading-text'>已加载全部特价商品</Text>
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
|
||
{/* ========== 购物车悬浮球 ========== */}
|
||
<CartBall />
|
||
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
|
||
</View>
|
||
)
|
||
}
|