- 首页index.tsx中接入useVipStatus,修复热销推荐价格未显示dealerPrice的问题 - 新增getHotDisplayPrice函数,VIP时显示dealerPrice及划线原价,并添加VIP金色角标 - 后端OrderCreateRequest.OrderGoodsItem增加price字段及校验,解决后端DTO缺失price字段问题 - 重构OrderBusinessService中价格校验逻辑,确保正确优先级及SKU库存校验 - 确认价格字段约定,前端传dealerPrice给后端下单 - Maven编译通过,后端需重启服务生效
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||
import { View, Text, ScrollView, Swiper, SwiperItem, Image } from '@tarojs/components'
|
||
import Taro, { useDidShow } from '@tarojs/taro'
|
||
import { useUser } from '@/hooks/useUser'
|
||
import { useShare } from '@/hooks/useShare'
|
||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||
import { useNewOrderDetector } from '@/hooks/useNewOrderDetector'
|
||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||
import { listCmsAd } from '@/api/cms/cmsAd'
|
||
import { listCmsArticle } from '@/api/cms/cmsArticle'
|
||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||
import type { CmsAd } from '@/api/cms/cmsAd/model'
|
||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||
import Price from '@/components/common/Price'
|
||
import { getCompressedImageUrl } from '@/utils/image'
|
||
|
||
definePageConfig({
|
||
navigationBarTitleText: '首页',
|
||
enableShareAppMessage: true,
|
||
enableShareTimeline: true,
|
||
})
|
||
|
||
const IndexPage: React.FC = () => {
|
||
const { user, isLoggedIn } = useUser()
|
||
const [banners, setBanners] = useState<CmsAd[]>([])
|
||
const [announcements, setAnnouncements] = useState<CmsArticle[]>([])
|
||
const [hotProducts, setHotProducts] = useState<ShopGoods[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const scrollHeight = useScrollHeight(44)
|
||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||
const { isVip } = useVipStatus()
|
||
|
||
// 首页分享:好友 + 朋友圈 + 复制链接(右上角三个按钮全亮)
|
||
useShare({
|
||
title: '鑫龙家电 - 精选好物,实惠到家,等你来逛~',
|
||
path: '/pages/index/index',
|
||
enableTimeline: true,
|
||
enableCopyUrl: true,
|
||
})
|
||
|
||
// ─── 门店店员身份检测 ──────────────────────────────────────────
|
||
const [isClerk, setIsClerk] = useState(false)
|
||
const isClerkRef = useRef(false)
|
||
|
||
// 登录后检查是否为门店店员
|
||
useEffect(() => {
|
||
if (!isLoggedIn) {
|
||
setIsClerk(false)
|
||
isClerkRef.current = false
|
||
return
|
||
}
|
||
getMyClerk()
|
||
.then(data => {
|
||
const clerk = !!data
|
||
setIsClerk(clerk)
|
||
isClerkRef.current = clerk
|
||
})
|
||
.catch(() => {
|
||
setIsClerk(false)
|
||
isClerkRef.current = false
|
||
})
|
||
}, [isLoggedIn])
|
||
|
||
// 页面重新显示时也刷新店员身份(可能从其他页面回来时状态变了)
|
||
useDidShow(() => {
|
||
if (isLoggedIn) {
|
||
getMyClerk()
|
||
.then(data => {
|
||
const clerk = !!data
|
||
setIsClerk(clerk)
|
||
isClerkRef.current = clerk
|
||
})
|
||
.catch(() => {
|
||
isClerkRef.current = false
|
||
})
|
||
}
|
||
})
|
||
|
||
// ─── 新订单轮询检测(仅店员生效)────────────────────────────────
|
||
const { newOrderCount, clearUnread } = useNewOrderDetector({
|
||
interval: 30000, // 30 秒轮询一次
|
||
fetchLatestOrders: useCallback(() => {
|
||
// 非店员不请求,返回空数组(baseline 不会建立,无副作用)
|
||
if (!isClerkRef.current) return Promise.resolve([])
|
||
return pageShopOrder({ page: 1, limit: 5 }).then(r => r?.list || [])
|
||
}, []),
|
||
onNewOrders: useCallback((count) => {
|
||
// 非门店店员不弹新订单提醒
|
||
if (!isClerkRef.current) return
|
||
Taro.vibrateShort({ type: 'medium' })
|
||
Taro.showToast({
|
||
title: `您有 ${count} 条新订单`,
|
||
icon: 'none',
|
||
duration: 2500,
|
||
})
|
||
}, []),
|
||
})
|
||
|
||
|
||
const categories = ['全部', '球拍', '球鞋', '服装', '配件']
|
||
|
||
// 功能入口(8个)
|
||
const featureEntries = [
|
||
{ icon: '🎁', label: '全部商品', url: '/pages/shop/index' },
|
||
{ icon: '🤝', label: '收货地址', url: '/pages/user/address-list' },
|
||
{ icon: '🎫', label: '领券中心', url: '/pages/index/coupon-center/index' },
|
||
{ icon: '⭐', label: '我的收藏', url: '/pages/user/favorite-list/index' },
|
||
{ icon: '👑', label: '关于我们', url: '/pages/user/about/index' },
|
||
{ icon: '💰', label: '积分商城', url: '/pages/points/index' },
|
||
{ icon: '🏪', label: '门店信息', url: '/pages/store/list/index' },
|
||
{ icon: '❓', label: '帮助中心', url: '/pages/user/help-center/index' },
|
||
]
|
||
|
||
// 获取轮播图
|
||
useEffect(() => {
|
||
const fetchBanners = async () => {
|
||
try {
|
||
const data = await listCmsAd({ adType: 'banner', status: 0 })
|
||
setBanners(data || [])
|
||
} catch (e) {
|
||
console.error('获取轮播图失败:', e)
|
||
}
|
||
}
|
||
|
||
const fetchAnnouncements = async () => {
|
||
try {
|
||
const data = await listCmsArticle({ category: 'notice', status: 1 })
|
||
setAnnouncements(data || [])
|
||
} catch (e) {
|
||
console.error('获取公告失败:', e)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchBanners()
|
||
fetchAnnouncements()
|
||
fetchHotProducts()
|
||
}, [])
|
||
|
||
// 获取热销商品
|
||
const fetchHotProducts = async () => {
|
||
try {
|
||
const res = await pageShopGoods({ page: 1, limit: 4, status: 0, recommend: 1 })
|
||
if (res?.list) {
|
||
setHotProducts(res.list)
|
||
}
|
||
} catch (e) {
|
||
console.error('获取热销商品失败:', e)
|
||
}
|
||
}
|
||
|
||
const handleMessageClick = () => {
|
||
if (!isLoggedIn) {
|
||
Taro.navigateTo({ url: '/passport/login' })
|
||
return
|
||
}
|
||
// 店员 → 跳转门店订单管理页,有新订单时清除未读计数
|
||
if (isClerk) {
|
||
if (newOrderCount > 0) clearUnread()
|
||
Taro.navigateTo({ url: '/pages/store/orders/index' })
|
||
return
|
||
}
|
||
// 普通用户 → 跳消息通知页
|
||
Taro.navigateTo({ url: '/pages/index/notification' })
|
||
}
|
||
|
||
/**
|
||
* VIP 会员显示 dealerPrice(同时把 price 作为划线原价),非 VIP 维持原行为
|
||
*/
|
||
const getHotDisplayPrice = (item: ShopGoods): { price: string; original?: string } => {
|
||
if (isVip && item.dealerPrice) {
|
||
return { price: item.dealerPrice, original: item.price }
|
||
}
|
||
const original = item.salePrice && item.salePrice !== item.price ? item.salePrice : undefined
|
||
return { price: item.price || '0', original }
|
||
}
|
||
|
||
// tabBar 页面列表
|
||
const tabBarPages = ['/pages/index/index', '/pages/shop/index', '/pages/order/list', '/pages/user/user']
|
||
|
||
const handleFeatureClick = (url: string) => {
|
||
if (tabBarPages.includes(url)) {
|
||
Taro.switchTab({ url })
|
||
} else {
|
||
Taro.navigateTo({ url })
|
||
}
|
||
}
|
||
|
||
return (
|
||
<View className='min-h-screen bg-gray-50'>
|
||
{/* 顶部搜索栏 */}
|
||
<View className='flex items-center px-3 py-2 bg-white'>
|
||
<View className='flex-1 mx-2'>
|
||
<View
|
||
className='flex items-center bg-gray-100 rounded-full px-3 py-2'
|
||
onClick={() => Taro.navigateTo({ url: '/pages/index/search' })}
|
||
>
|
||
<Text className='text-gray-400 text-sm mr-2'>🔍</Text>
|
||
<Text className='text-gray-400 text-sm flex-1'>搜索商品</Text>
|
||
</View>
|
||
</View>
|
||
<View onClick={handleMessageClick} className='relative'>
|
||
<Text className='text-xl'>🔔</Text>
|
||
{isClerk && newOrderCount > 0 && (
|
||
<View className='absolute -top-1 -right-1 min-w-[16px] h-4 px-1 bg-red-500 rounded-full flex items-center justify-center'>
|
||
<Text className='text-white text-[10px] leading-none font-medium'>
|
||
{newOrderCount > 99 ? '99+' : newOrderCount}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
{isClerk && newOrderCount === 0 && (
|
||
<View className='absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full' />
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||
{/* 轮播图 */}
|
||
<View className='mx-3 mt-2 rounded-lg overflow-hidden'>
|
||
{banners.length > 0 ? (
|
||
<Swiper
|
||
autoplay
|
||
interval={3000}
|
||
className='rounded-lg'
|
||
style={{ height: '160px' }}
|
||
>
|
||
{banners.map(item => (
|
||
<SwiperItem key={item.adId}>
|
||
<Image
|
||
className='w-full h-full'
|
||
src={getCompressedImageUrl(item.imageList?.[0]?.url || item.image || '', { width: 750, quality: 90})}
|
||
mode='aspectFill'
|
||
onClick={() => {
|
||
if (item.path) Taro.navigateTo({ url: item.path })
|
||
}}
|
||
/>
|
||
</SwiperItem>
|
||
))}
|
||
</Swiper>
|
||
) : (
|
||
<View className='w-full bg-green-50 flex items-center justify-center' style={{ height: '160px' }}>
|
||
<Text className='text-gray-400 text-sm'>暂无轮播图</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 功能入口网格 */}
|
||
<View className='mx-3 mt-3 p-3 bg-white rounded-lg'>
|
||
<View className='grid grid-cols-4 gap-2'>
|
||
{featureEntries.map(item => (
|
||
<View
|
||
key={item.label}
|
||
className='flex flex-col items-center py-2'
|
||
onClick={() => handleFeatureClick(item.url)}
|
||
>
|
||
<Text className='text-2xl mb-1'>{item.icon}</Text>
|
||
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 公告栏 */}
|
||
{announcements.length > 0 && (
|
||
<View className='mx-3 mt-3 px-3 py-2 bg-yellow-50 rounded-lg flex items-center' style={{ display: 'none' }}>
|
||
<Text className='text-xs text-yellow-600 font-medium mr-2'>公告</Text>
|
||
<Swiper
|
||
autoplay
|
||
direction='vertical'
|
||
interval={3000}
|
||
className='flex-1'
|
||
style={{ height: '20px' }}
|
||
>
|
||
{announcements.map(item => (
|
||
<SwiperItem key={item.articleId}>
|
||
<Text
|
||
className='text-xs text-gray-600 truncate'
|
||
onClick={() => Taro.navigateTo({ url: `/pages/index/article-detail?id=${item.articleId}` })}
|
||
>
|
||
{item.title}
|
||
</Text>
|
||
</SwiperItem>
|
||
))}
|
||
</Swiper>
|
||
</View>
|
||
)}
|
||
|
||
{/* 分类导航 */}
|
||
<View className='mx-3 mt-3 p-3 bg-white rounded-lg' style={{ display: 'none' }}>
|
||
<Text className='text-base font-medium text-gray-800 mb-3 block'>商品分类</Text>
|
||
<View className='flex justify-around'>
|
||
{categories.map((item) => (
|
||
<View
|
||
key={item}
|
||
className='flex flex-col items-center'
|
||
onClick={() => Taro.navigateTo({ url: '/pages/shop/category' })}
|
||
>
|
||
<View className='w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mb-1'>
|
||
<Text className='text-xs text-gray-500'>{item.slice(0, 1)}</Text>
|
||
</View>
|
||
<Text className='text-xs text-gray-600'>{item}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 热销推荐 */}
|
||
<View className='mx-3 mt-3 mb-4'>
|
||
<View className='flex justify-between items-center mb-3'>
|
||
<Text className='text-base font-medium text-gray-800 block'>热销推荐</Text>
|
||
<Text
|
||
className='text-xs text-gray-400 block'
|
||
onClick={() => Taro.switchTab({ url: '/pages/shop/index' })}
|
||
>
|
||
查看更多 {'>'}
|
||
</Text>
|
||
</View>
|
||
<View className='grid grid-cols-2 gap-2'>
|
||
{hotProducts.length > 0 ? (
|
||
hotProducts.map((item) => (
|
||
<View
|
||
key={item.goodsId}
|
||
className='bg-white rounded-lg overflow-hidden'
|
||
onClick={() => Taro.navigateTo({ url: `/pages/shop/product-detail?id=${item.goodsId}` })}
|
||
>
|
||
<View className='w-full' style={{ paddingTop: '100%', position: 'relative' }}>
|
||
{item.image ? (
|
||
<Image
|
||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
|
||
src={getCompressedImageUrl(item.image)}
|
||
mode='aspectFit'
|
||
/>
|
||
) : (
|
||
<View style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} className='bg-gray-100 flex items-center justify-center'>
|
||
<Text className='text-xs text-gray-300'>暂无图片</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View className='p-2'>
|
||
<Text className='text-sm text-gray-700 truncate block'>{item.goodsName || item.name}</Text>
|
||
<View className='flex items-center mt-1'>
|
||
<Price
|
||
price={getHotDisplayPrice(item).price}
|
||
original={getHotDisplayPrice(item).original}
|
||
size='small'
|
||
loginMask
|
||
/>
|
||
{isVip && item.dealerPrice ? (
|
||
<Text className='text-xs text-amber-600 ml-1'>VIP</Text>
|
||
) : null}
|
||
</View>
|
||
{item.sales !== undefined && item.sales > 0 && (
|
||
<Text className='text-xs text-gray-400 mt-1'>已售 {item.sales}</Text>
|
||
)}
|
||
</View>
|
||
</View>
|
||
))
|
||
) : (
|
||
<View className='col-span-2 py-8 text-center'>
|
||
<Text className='text-sm text-gray-400'>暂无热销商品</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default IndexPage
|