Files
xin-procurement-weapp/src/pages/index/index.tsx
T
2026-08-27 18:08:14 +08:00

296 lines
11 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, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import useCartStore from '@/stores/cart/useCartStore'
import { getHomeConfigApi } from '@/services/home'
import type { HomeConfig } from '@/services/home'
import { getProductListApi } from '@/services/product'
import { getProductCover } from '@/types/product'
import type { Product, ProductCartPatch } from '@/types/product'
import PriceText from '@/components/PriceText'
import CartBall from '@/components/CartBall'
import CartStepper from '@/components/CartStepper'
import { formatSpec } from '@/utils/format'
import './index.less'
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
const PENDING_CATEGORY_KEY = 'product_category_id'
const PENDING_KEYWORD_KEY = 'product_keyword'
/** 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 [products, setProducts] = useState<Product[]>([])
/** 搜索框输入 */
const [keyword, setKeyword] = useState('')
const statusBarHeight = useMemo(() => getStatusBarHeight(), [])
useDidShow(() => {
loadHomeConfig()
loadRecommend()
})
/** 首页配置聚合数据(响应附带悬浮球汇总) */
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])
/** 推荐商品 */
const loadRecommend = useCallback(async () => {
try {
const res = await getProductListApi({ page: 1, pageSize: 10 })
setProducts(res.data.data)
} catch {
// 错误已由 request 层 toast
}
}, [])
/**
* 后台配置的 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) => {
setProducts(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],
)
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>
{ products.length === 0 ? (
<View className='home-recommend__empty'>
<Text className='home-recommend__empty-text'>暂无商品</Text>
</View>
) : (
<View className='product-grid'>
{products.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>
<Text className='product-card__spec'>{formatSpec(product.spec, product.unit)}</Text>
<View className='product-card__bottom'>
{product.price !== null ? (
<PriceText className='product-card__price' price={product.price} spec={product.spec} mode='block' />
) : (
<Text className='product-card__price product-card__price--none'>登陆后查看价格</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>
)}
</View>
{/* ========== 购物车悬浮球 ========== */}
<CartBall />
</View>
)
}