Files
xin-procurement-weapp/src/pages/index/index.tsx
T
2026-09-04 14:10:33 +08:00

370 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}