feat(商品列表): 实现商品列表吸顶效果和分享功能- 添加 Tabs 粘性布局组件,实现吸顶效果- 新增商品分享功能,支持分享给好友

-优化商品列表展示样式,使用瀑布流布局
- 调整商品图片展示方式和点击跳转逻辑- 添加空状态提示,改善用户体验
-修复部分样式问题,提升页面美观度- 移除旧版订单列表相关代码和依赖- 更新页面结构,提高组件复用性
- 添加系统信息获取,适配不同设备屏幕
-优化页面滚动体验,解决滑动冲突问题
This commit is contained in:
2025-10-07 20:10:04 +08:00
parent a2b0ad9bce
commit 81ec58523d
10 changed files with 679 additions and 460 deletions

View File

@@ -1,162 +1,174 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane, Swiper} from '@nutui/nutui-react-taro' import {useEffect, useState} from "react";
import {useEffect, useState, CSSProperties, useRef} from "react"; import {Image, Tabs, Empty, Sticky} from '@nutui/nutui-react-taro'
import {BszxPay} from "@/api/bszx/bszxPay/model"; import {Share} from '@nutui/icons-react-taro'
import {InfiniteLoading} from '@nutui/nutui-react-taro' import {View, Text} from '@tarojs/components';
import dayjs from "dayjs"; import Taro from "@tarojs/taro";
import {pageShopOrder} from "@/api/shop/shopOrder"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {ShopOrder} from "@/api/shop/shopOrder/model"; import {pageShopGoods} from "@/api/shop/shopGoods";
import {copyText} from "@/utils/common";
const InfiniteUlStyle: CSSProperties = { const BestSellers = (props: {onStickyChange?: (isSticky: boolean) => void}) => {
marginTop: '84px', const [tab1value, setTab1value] = useState<string | number>('0')
height: '82vh', const [list, setList] = useState<ShopGoods[]>([])
width: '100%', const [goods, setGoods] = useState<ShopGoods>()
padding: '0', const [stickyStatus, setStickyStatus] = useState<boolean>(false)
overflowY: 'auto',
overflowX: 'hidden',
}
const tabs = [
{
index: 0,
key: '全部',
title: '全部'
},
{
index: 1,
key: '已上架',
title: '已上架'
},
{
index: 2,
key: '已下架',
title: '已下架'
},
{
index: 3,
key: '已售罄',
title: '已售罄'
},
{
index: 4,
key: '警戒库存',
title: '警戒库存'
},
{
index: 5,
key: '回收站',
title: '回收站'
},
]
function GoodsList(props: any) { const reload = () => {
const [list, setList] = useState<ShopOrder[]>([]) pageShopGoods({}).then(res => {
const [page, setPage] = useState(1) setList(res?.list || []);
const [hasMore, setHasMore] = useState(true)
const swiperRef = useRef<React.ElementRef<typeof Swiper> | null>(null)
const [tabIndex, setTabIndex] = useState<string | number>(0)
console.log(props.statusBarHeight, 'ppp')
const reload = async () => {
pageShopOrder({page}).then(res => {
let newList: BszxPay[] | undefined = []
if (res?.list && res?.list.length > 0) {
newList = list?.concat(res.list)
setHasMore(true)
} else {
newList = res?.list
setHasMore(false)
}
setList(newList || []);
}) })
} }
const reloadMore = async () => { // 处理分享点击
setPage(page + 1) const handleShare = (item: ShopGoods) => {
reload().then(); setGoods(item);
console.log(goods)
// 显示分享选项菜单
Taro.showActionSheet({
itemList: ['分享给好友'],
success: (res) => {
if (res.tapIndex === 0) {
// 分享给好友 - 触发转发
Taro.showShareMenu({
withShareTicket: true,
success: () => {
// 提示用户点击右上角分享
Taro.showToast({
title: '请点击右上角分享给好友',
icon: 'none',
duration: 2000
});
}
});
}
},
fail: (err) => {
console.log('显示分享菜单失败', err);
}
});
}
// 处理粘性布局状态变化
const onStickyChange = (isSticky: boolean) => {
setStickyStatus(isSticky)
// 通知父组件粘性状态变化
props.onStickyChange?.(isSticky)
console.log('Tabs 粘性状态:', isSticky ? '已固定' : '取消固定')
}
// 获取小程序系统信息
const getSystemInfo = () => {
const systemInfo = Taro.getSystemInfoSync()
// 状态栏高度 + 导航栏高度 (一般为44px)
return (systemInfo.statusBarHeight || 0) + 44
} }
useEffect(() => { useEffect(() => {
setPage(2) reload()
reload().then()
}, []) }, [])
return ( return (
<> <>
<Tabs <View className={'py-3'} style={{paddingTop: '0'}}>
align={'left'} {/* Tabs粘性布局组件 */}
className={'fixed left-0'} <Sticky
style={{ top: '84px'}} threshold={getSystemInfo()}
value={tabIndex} onChange={onStickyChange}
onChange={(page) => { style={{
swiperRef.current?.to(page) zIndex: 999,
setTabIndex(page) backgroundColor: stickyStatus ? '#ffffff' : 'transparent',
}} boxShadow: stickyStatus ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
> transition: 'all 0.3s ease',
{ marginTop: stickyStatus ? '0' : '-12px'
tabs?.map((item, index) => {
return <TabPane key={index} title={item.title}></TabPane>
})
}
</Tabs>
<div style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}} }}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
> >
{list?.map(item => { <Tabs
value={tab1value}
className={'w-full'}
onChange={(value) => {
setTab1value(value)
}}
style={{
backgroundColor: 'transparent',
paddingTop: stickyStatus ? '0' : '8px',
paddingBottom: stickyStatus ? '8px' : '0',
}}
activeType="smile"
>
<Tabs.TabPane title="今日主推">
</Tabs.TabPane>
<Tabs.TabPane title="即将到期">
</Tabs.TabPane>
<Tabs.TabPane title="活动预告">
</Tabs.TabPane>
</Tabs>
</Sticky>
<View className={'flex flex-col justify-between items-center rounded-lg px-2 mt-2'}>
{/* 今日主推 */}
{tab1value == '0' && list?.map((item, index) => {
return ( return (
<Cell style={{padding: '16px'}}> <View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Space direction={'vertical'} className={'w-full flex flex-col'}> <Image src={item.image} mode={'aspectFit'} lazyLoad={false}
<div className={'order-no flex justify-between'}> radius="10px 10px 0 0" height="180"
<span className={'text-gray-700 font-bold text-sm'} onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
onClick={() => copyText(`${item.orderNo}`)}>{item.orderNo}</span> <View className={'flex flex-col p-2 rounded-lg'}>
<span className={'text-orange-500'}></span> <View>
</div> <View className={'car-no text-sm'}>{item.name}</View>
<div <View className={'flex justify-between text-xs py-1'}>
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div> <Text className={'text-orange-500'}>{item.comments}</Text>
<div className={'goods-info'}> <Text className={'text-gray-400'}> {item.sales}</Text>
<div className={'flex items-center'}> </View>
<div className={'flex items-center'}> <View className={'flex justify-between items-center py-2'}>
<Avatar <View className={'flex text-red-500 text-xl items-baseline'}>
src='34' <Text className={'text-xs'}></Text>
size={'45'} <Text className={'font-bold text-2xl'}>{item.price}</Text>
shape={'square'} </View>
/> <View className={'buy-btn'}>
<div className={'ml-2'}>{item.realName}</div> <View className={'cart-icon items-center hidden'}>
</div> <View
<div className={'text-gray-400 text-xs'}>{item.totalNum}</div> className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
</div> onClick={() => handleShare(item)}
</div> >
<div className={' w-full text-right'}>{item.payPrice}</div> <Share size={20}/>
<Space className={'btn flex justify-end'}> </View>
<Button size={'small'}></Button> </View>
<Button size={'small'}></Button> <Text className={'text-white pl-4 pr-5'}
</Space> onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</Space> </Text>
</Cell> </View>
</View>
</View>
</View>
</View>
) )
})} })}
</InfiniteLoading>
</div> {/* 即将到期 */}
{tab1value == '1' && (
<Empty
size={'small'}
description="暂无即将到期的商品"
style={{
background: 'transparent',
}}
/>
)}
{/* 活动预告 */}
{tab1value == '2' && (
<Empty
size={'small'}
description="暂无活动预告"
style={{
background: 'transparent',
}}
/>
)}
</View>
</View>
</> </>
) )
} }
export default BestSellers
export default GoodsList

View File

@@ -152,8 +152,8 @@ const MyPage = () => {
hotToday?.imageList?.map(item => ( hotToday?.imageList?.map(item => (
<View className={'item flex flex-col mr-1'} key={item.url}> <View className={'item flex flex-col mr-1'} key={item.url}>
<Image <Image
width={70} width={60}
height={70} height={60}
src={item.url} src={item.url}
mode={'scaleToFill'} mode={'scaleToFill'}
lazyLoad={false} lazyLoad={false}

View File

@@ -1,70 +1,39 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Image, Swiper, SwiperItem, Empty} from '@nutui/nutui-react-taro' import {Image, Tabs, Empty, Sticky} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro' import {Share} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'; import {View, Text} from '@tarojs/components';
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import {Tabs} from '@nutui/nutui-react-taro'
import {ShopGoods} from "@/api/shop/shopGoods/model"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {pageShopGoods} from "@/api/shop/shopGoods"; import {pageShopGoods} from "@/api/shop/shopGoods";
const BestSellers = (props: {onStickyChange?: (isSticky: boolean) => void}) => {
const BestSellers = () => {
const [tab1value, setTab1value] = useState<string | number>('0') const [tab1value, setTab1value] = useState<string | number>('0')
const [list, setList] = useState<ShopGoods[]>([]) const [list, setList] = useState<ShopGoods[]>([])
const [goods, setGoods] = useState<ShopGoods | null>(null) const [goods, setGoods] = useState<ShopGoods>()
// 轮播图固定高度,可根据需求调整 const [stickyStatus, setStickyStatus] = useState<boolean>(false)
const SWIPER_HEIGHT = 180;
const reload = () => { const reload = () => {
pageShopGoods({}).then(res => { pageShopGoods({}).then(res => {
const processGoodsItem = (item: ShopGoods) => { setList(res?.list || []);
const pics: string[] = []; })
// 添加主图
if (item.image) {
pics.push(item.image);
}
// 处理附加图片
if (item.files) {
try {
// 解析文件字符串为对象
const files = typeof item.files === "string"
? JSON.parse(item.files)
: item.files;
// 收集所有图片URL
Object.values(files).forEach(file => {
if (file?.url) {
pics.push(file.url);
}
});
} catch (error) {
console.error('解析文件失败:', error);
}
}
// 返回新对象,避免直接修改原对象
return {...item, pics};
};
// 处理商品列表
const goods = (res?.list || []).map(processGoodsItem);
setList(goods);
}).catch(err => {
console.error('获取商品列表失败:', err);
});
} }
// 处理分享点击 // 处理分享点击
const handleShare = (item: ShopGoods) => { const handleShare = (item: ShopGoods) => {
setGoods(item); setGoods(item);
console.log(goods)
// 显示分享选项菜单 // 显示分享选项菜单
Taro.showActionSheet({ Taro.showActionSheet({
itemList: ['分享给好友'], itemList: ['分享给好友'],
success: (res) => { success: (res) => {
if (res.tapIndex === 0) { if (res.tapIndex === 0) {
// 分享给好友 - 触发转发
Taro.showShareMenu({ Taro.showShareMenu({
withShareTicket: true, withShareTicket: true,
success: () => { success: () => {
// 提示用户点击右上角分享
Taro.showToast({ Taro.showToast({
title: '请点击右上角分享给好友', title: '请点击右上角分享给好友',
icon: 'none', icon: 'none',
@@ -80,136 +49,126 @@ const BestSellers = () => {
}); });
} }
useEffect(() => { // 处理粘性布局状态变化
reload(); const onStickyChange = (isSticky: boolean) => {
}, []); setStickyStatus(isSticky)
// 通知父组件粘性状态变化
props.onStickyChange?.(isSticky)
console.log('Tabs 粘性状态:', isSticky ? '已固定' : '取消固定')
}
// 配置分享内容 // 获取小程序系统信息
Taro.useShareAppMessage(() => { const getSystemInfo = () => {
if (goods) { const systemInfo = Taro.getSystemInfoSync()
return { // 状态栏高度 + 导航栏高度 (一般为44px)
title: goods.name, return (systemInfo.statusBarHeight || 0) + 44
path: `/shop/goodsDetail/index?id=${goods.goodsId}`, }
imageUrl: goods.image || ''
}; useEffect(() => {
} reload()
return { }, [])
title: '热销商品',
path: '/pages/index/index'
};
});
return ( return (
<View className={'py-3'}> <>
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}> <View className={'py-3'} style={{paddingTop: '0'}}>
<Tabs {/* Tabs粘性布局组件 */}
value={tab1value} <Sticky
className={'w-full'} threshold={getSystemInfo()}
onChange={(value) => { onChange={onStickyChange}
setTab1value(value)
}}
style={{ style={{
backgroundColor: '#fff', zIndex: 999,
backgroundColor: stickyStatus ? '#ffffff' : 'transparent',
boxShadow: stickyStatus ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
transition: 'all 0.3s ease',
marginTop: stickyStatus ? '0' : '-12px'
}} }}
activeType="smile"
> >
<Tabs.TabPane title="今日主推"> <Tabs
</Tabs.TabPane> value={tab1value}
<Tabs.TabPane title="即将到期"> className={'w-full'}
</Tabs.TabPane> onChange={(value) => {
<Tabs.TabPane title="活动预告"> setTab1value(value)
</Tabs.TabPane> }}
</Tabs> style={{
backgroundColor: 'transparent',
{tab1value == '0' && list?.map((item) => ( paddingTop: stickyStatus ? '0' : '8px',
<View paddingBottom: stickyStatus ? '8px' : '0',
key={item.goodsId || item.id} // 使用商品唯一ID作为key }}
className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'} activeType="smile"
> >
{/* 轮播图组件 */} <Tabs.TabPane title="今日主推">
{item.pics && item.pics.length > 0 ? ( </Tabs.TabPane>
<Swiper <Tabs.TabPane title="即将到期">
defaultValue={0} </Tabs.TabPane>
height={SWIPER_HEIGHT} <Tabs.TabPane title="活动预告">
indicator </Tabs.TabPane>
className="swiper-container" </Tabs>
autoPlay </Sticky>
interval={3000}
>
{item.pics.map((pic, picIndex) => (
<SwiperItem key={picIndex}>
<Image
radius="12px 12px 0 0"
height={SWIPER_HEIGHT}
src={pic}
mode={'aspectFill'} // 使用aspectFill保持比例并填充容器
lazyLoad
onClick={() => Taro.navigateTo({
url: `/shop/goodsDetail/index?id=${item.goodsId}`
})}
className="swiper-image"
/>
</SwiperItem>
))}
</Swiper>
) : (
// 没有图片时显示占位图
<View className="no-image-placeholder" style={{height: `${SWIPER_HEIGHT}px`}}>
<Text className="placeholder-text"></Text>
</View>
)}
<View className={'flex flex-col p-2 rounded-lg'}> <View className={'flex flex-col justify-between items-center rounded-lg px-2 mt-2'}>
<View> {/* 今日主推 */}
<View className={'car-no text-sm'}>{item.name}</View> {tab1value == '0' && list?.map((item, index) => {
<View className={'flex justify-between text-xs py-1'}> return (
<Text className={'text-orange-500'}>{item.comments}</Text> <View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Text className={'text-gray-400'}> {item.sales}</Text> <Image src={item.image} mode={'aspectFit'} lazyLoad={false}
</View> radius="10px 10px 0 0" height="180"
<View className={'flex justify-between items-center py-2'}> onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
<View className={'flex text-red-500 text-xl items-baseline'}> <View className={'flex flex-col p-2 rounded-lg'}>
<Text className={'text-xs'}></Text> <View>
<Text className={'font-bold text-2xl'}>{item.price}</Text> <View className={'car-no text-sm'}>{item.name}</View>
</View> <View className={'flex justify-between text-xs py-1'}>
<View className={'buy-btn'}> <Text className={'text-orange-500'}>{item.comments}</Text>
<View className={'cart-icon flex items-center hidden'}> <Text className={'text-gray-400'}> {item.sales}</Text>
<View </View>
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'} <View className={'flex justify-between items-center py-2'}>
onClick={() => handleShare(item)} <View className={'flex text-red-500 text-xl items-baseline'}>
> <Text className={'text-xs'}></Text>
<Share size={20}/> <Text className={'font-bold text-2xl'}>{item.price}</Text>
</View>
<View className={'buy-btn'}>
<View className={'cart-icon items-center hidden'}>
<View
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
onClick={() => handleShare(item)}
>
<Share size={20}/>
</View>
</View>
<Text className={'text-white pl-4 pr-5'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</Text>
</View> </View>
</View> </View>
<Text
className={'text-white pl-5 pr-5'}
onClick={() => Taro.navigateTo({
url: `/shop/goodsDetail/index?id=${item.goodsId}`
})}
>
</Text>
</View> </View>
</View> </View>
</View> </View>
</View> )
</View> })}
))}
{ {/* 即将到期 */}
tab1value == '1' && <Empty description="暂无相关商品" style={{ {tab1value == '1' && (
background: 'transparent', <Empty
}}/> size={'small'}
} description="暂无即将到期的商品"
style={{
{ background: 'transparent',
tab1value == '2' && <Empty description="暂无相关商品" style={{ }}
background: 'transparent', />
}}/> )}
}
{/* 活动预告 */}
{tab1value == '2' && (
<Empty
size={'small'}
description="暂无活动预告"
style={{
background: 'transparent',
}}
/>
)}
</View>
</View> </View>
</View> </>
) )
} }
export default BestSellers export default BestSellers

View File

@@ -1,14 +1,15 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Image} from '@nutui/nutui-react-taro' import {Image, Tabs, Empty, Sticky} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro' import {View, Text} from '@tarojs/components';
import Taro from '@tarojs/taro' import Taro from "@tarojs/taro";
import {ShopGoods} from "@/api/shop/shopGoods/model"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {pageShopGoods} from "@/api/shop/shopGoods"; import {pageShopGoods} from "@/api/shop/shopGoods";
import './GoodsList.scss' import './GoodsList.scss';
const GoodsList = (props: {onStickyChange?: (isSticky: boolean) => void}) => {
const BestSellers = () => { const [tab1value, setTab1value] = useState<string | number>('0')
const [list, setList] = useState<ShopGoods[]>([]) const [list, setList] = useState<ShopGoods[]>([])
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
const reload = () => { const reload = () => {
pageShopGoods({}).then(res => { pageShopGoods({}).then(res => {
@@ -16,52 +17,127 @@ const BestSellers = () => {
}) })
} }
// 处理粘性布局状态变化
const onStickyChange = (isSticky: boolean) => {
setStickyStatus(isSticky)
// 通知父组件粘性状态变化
props.onStickyChange?.(isSticky)
console.log('Tabs 粘性状态:', isSticky ? '已固定' : '取消固定')
}
// 获取小程序系统信息
const getSystemInfo = () => {
const systemInfo = Taro.getSystemInfoSync()
// 状态栏高度 + 导航栏高度 (一般为44px)
return (systemInfo.statusBarHeight || 0) + 44
}
useEffect(() => { useEffect(() => {
reload() reload()
}, []) }, [])
return ( return (
<> <>
<div className={'py-3'}> <View className={'py-3'} style={{paddingTop: '0'}}>
<div className={'flex flex-wrap justify-between items-start rounded-lg px-2'}> {/* Tabs粘性布局组件 */}
{list?.map((item, index) => { <Sticky
return ( threshold={getSystemInfo()}
<div key={index} className={'flex flex-col rounded-lg bg-white shadow-sm mb-5'} style={{ onChange={onStickyChange}
width: '48%' style={{
}}> zIndex: 999,
<Image src={item.image} mode={'scaleToFill'} lazyLoad={false} backgroundColor: stickyStatus ? '#ffffff' : 'transparent',
radius="10px 10px 0 0" height="180" boxShadow: stickyStatus ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> transition: 'all 0.3s ease',
<div className={'flex flex-col p-2 rounded-lg'}> marginTop: stickyStatus ? '0' : '-12px'
<div> }}
<div className={'car-no text-sm'}>{item.name}</div> >
<div className={'flex justify-between text-xs py-1'}> <Tabs
<span className={'text-orange-500'}>{item.comments}</span> value={tab1value}
<span className={'text-gray-400'}> {item.sales}</span> className={'w-full'}
</div> onChange={(value) => {
<div className={'flex justify-between items-center py-2'}> setTab1value(value)
<div className={'flex text-red-500 text-xl items-baseline'}> }}
<span className={'text-xs'}></span> style={{
<span className={'font-bold text-2xl'}>{item.price}</span> backgroundColor: 'transparent',
</div> paddingTop: stickyStatus ? '0' : '8px',
<div className={'buy-btn'}> paddingBottom: stickyStatus ? '8px' : '0',
<div className={'cart-icon'}> }}
<Share size={20} className={'mx-4 mt-2'} activeType="smile"
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> >
</div> <Tabs.TabPane title="今日主推">
<div className={'text-white pl-4 pr-5'} </Tabs.TabPane>
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}> <Tabs.TabPane title="即将到期">
</div> </Tabs.TabPane>
</div> <Tabs.TabPane title="活动预告">
</div> </Tabs.TabPane>
</div> </Tabs>
</div> </Sticky>
</div>
) <View className={'bg-gray-50'}>
})} {/* 今日主推 - 瀑布流布局 */}
</div> {tab1value == '0' && (
</div> <View className={'grid grid-cols-2 gap-2 pb-2 p-2 '}>
{list?.map((item, index) => {
return (
<View key={index} className={'goods-waterfall-item bg-white'} style={{
borderRadius: '0 0 6px 6px'
}}>
<View className={'goods-card'}>
<Image
src={item.image}
lazyLoad={false}
style={{
borderRadius: '6px 6px 0 0',
height: '180px'
}}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}
/>
<View className={'goods-info p-2 flex flex-col'}>
<View className={'goods-title text-sm font-bold'}>{item.name}</View>
<View className={'goods-meta'}>
<Text className={'goods-comments text-gray-400 text-xs'}>{item.comments}</Text>
</View>
<View className={'goods-price-section flex justify-between'}>
<View className={'goods-price'}>
<Text className={'price-unit text-orange-600 font-bold text-lg'}></Text>
<Text className={'price-number text-orange-600 font-bold text-lg'}>{item.price}</Text>
</View>
<View className={'goods-actions'}>
<Text className={'goods-sales text-gray-400 text-xs'}> {item.sales}</Text>
</View>
</View>
</View>
</View>
</View>
)
})}
</View>
)}
{/* 即将到期 */}
{tab1value == '1' && (
<Empty
size={'small'}
description="暂无即将到期的商品"
style={{
background: 'transparent',
}}
/>
)}
{/* 活动预告 */}
{tab1value == '2' && (
<Empty
size={'small'}
description="暂无活动预告"
style={{
background: 'transparent',
}}
/>
)}
</View>
</View>
</> </>
) )
} }
export default BestSellers export default GoodsList

View File

@@ -14,3 +14,10 @@
position: absolute; position: absolute;
z-index: 0; z-index: 0;
} }
/* 吸顶状态下的样式 */
.nutui-sticky--fixed {
.header-bg {
height: 100%;
}
}

View File

@@ -1,6 +1,6 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {Button, Space} from '@nutui/nutui-react-taro' import {Button, Space, Sticky} from '@nutui/nutui-react-taro'
import {TriangleDown} from '@nutui/icons-react-taro' import {TriangleDown} from '@nutui/icons-react-taro'
import {Avatar, NavBar} from '@nutui/nutui-react-taro' import {Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from "@/api/layout"; import {getUserInfo, getWxOpenId} from "@/api/layout";
@@ -13,7 +13,7 @@ import {View,Text} from '@tarojs/components'
import MySearch from "./MySearch"; import MySearch from "./MySearch";
import './Header.scss'; import './Header.scss';
const Header = (props: any) => { const Header = (_: any) => {
// 使用新的useShopInfo Hook // 使用新的useShopInfo Hook
const { const {
getWebsiteName, getWebsiteName,
@@ -22,6 +22,7 @@ const Header = (props: any) => {
const [IsLogin, setIsLogin] = useState<boolean>(true) const [IsLogin, setIsLogin] = useState<boolean>(true)
const [statusBarHeight, setStatusBarHeight] = useState<number>() const [statusBarHeight, setStatusBarHeight] = useState<number>()
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
const reload = async () => { const reload = async () => {
Taro.getSystemInfo({ Taro.getSystemInfo({
@@ -166,49 +167,76 @@ const Header = (props: any) => {
}) })
} }
// 处理粘性布局状态变化
const onStickyChange = (isSticky: boolean) => {
setStickyStatus(isSticky)
console.log('Header 粘性状态:', isSticky ? '已固定' : '取消固定')
}
// 获取小程序系统信息
// const getSystemInfo = () => {
// const systemInfo = Taro.getSystemInfoSync()
// // 状态栏高度 + 导航栏高度 (一般为44px)
// return (systemInfo.statusBarHeight || 0) + 44
// }
useEffect(() => { useEffect(() => {
reload().then() reload().then()
}, []) }, [])
return ( return (
<> <>
<View className={'fixed top-0 header-bg'} style={{ <Sticky
height: !props.stickyStatus ? '180px' : 'auto', threshold={0}
paddingBottom: '12px' onChange={onStickyChange}
}}> style={{
<MySearch statusBarHeight={statusBarHeight} /> zIndex: 1000,
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/} backgroundColor: stickyStatus ? '#03605c' : 'transparent',
</View> transition: 'background-color 0.3s ease',
<NavBar
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
onBackClick={() => {
}} }}
left={ >
!IsLogin ? ( <View className={'header-bg'} style={{
<View style={{display: 'flex', alignItems: 'center'}}> height: !stickyStatus ? '180px' : `${(statusBarHeight || 0) + 44}px`,
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}> paddingBottom: !stickyStatus ? '12px' : '0px'
<Space> }}>
<Avatar {/* 只在非吸顶状态下显示搜索框 */}
size="22" {!stickyStatus && <MySearch statusBarHeight={statusBarHeight} />}
src={getWebsiteLogo()} </View>
/> <NavBar
<Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text> style={{
<TriangleDown size={9} className={'text-white'}/> marginTop: `${statusBarHeight}px`,
</Space> marginBottom: '0px',
</Button> backgroundColor: 'transparent'
</View> }}
) : ( onBackClick={() => {
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}> }}
<Avatar left={
size="22" !IsLogin ? (
src={getWebsiteLogo()} <View style={{display: 'flex', alignItems: 'center'}}>
/> <Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Text className={'text-white'}>{getWebsiteName()}</Text> <Space>
<TriangleDown className={'text-white'} size={9}/> <Avatar
</View> size="22"
)}> src={getWebsiteLogo()}
{/*<QRLoginButton />*/} />
</NavBar> <Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
<TriangleDown size={9} className={'text-white'}/>
</Space>
</Button>
</View>
) : (
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<Text className={'text-white'}>{getWebsiteName()}</Text>
<TriangleDown className={'text-white'} size={9}/>
</View>
)}>
{/*<QRLoginButton />*/}
</NavBar>
</Sticky>
</> </>
) )
} }

View File

@@ -18,3 +18,59 @@ page {
height: 70px; height: 70px;
} }
} }
/* 轮播图容器样式,确保支持两种滑动操作 */
.banner-swiper-container {
touch-action: pan-y !important; /* 允许垂直滑动 */
.nut-swiper {
touch-action: pan-y !important; /* 允许垂直滑动 */
.nut-swiper-item {
touch-action: pan-x pan-y !important; /* 允许水平和垂直滑动 */
image {
pointer-events: auto; /* 确保图片点击事件正常 */
touch-action: manipulation; /* 优化触摸操作 */
}
}
}
/* 为Swiper容器添加特殊处理 */
.nut-swiper--horizontal {
touch-action: pan-y !important; /* 允许垂直滑动 */
}
}
/* 吸顶状态下的样式 */
.nutui-sticky--fixed {
.header-bg {
height: 100%;
}
}
/* 为Swiper添加更精确的触摸控制 */
.nut-swiper {
touch-action: pan-y !important;
.nut-swiper-inner {
touch-action: pan-x pan-y !important;
}
}
/* 自定义Swiper样式 */
.custom-swiper {
touch-action: pan-y !important;
.nut-swiper-item {
touch-action: pan-x pan-y !important;
}
}
/* 确保Swiper内部元素不会阻止页面滚动 */
.banner-swiper-container,
.custom-swiper,
.nut-swiper,
.nut-swiper-item {
-webkit-overflow-scrolling: touch; /* iOS平台启用硬件加速滚动 */
}

View File

@@ -1,27 +1,26 @@
import Header from './Header'; import Header from './Header';
import BestSellers from './BestSellers';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {useShareAppMessage} from "@tarojs/taro" import {useShareAppMessage} from "@tarojs/taro"
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getShopInfo} from "@/api/layout"; import {getShopInfo} from "@/api/layout";
import {Sticky} from '@nutui/nutui-react-taro'
import Menu from "./Menu"; import Menu from "./Menu";
import Banner from "./Banner"; import Banner from "./Banner";
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite"; import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
import './index.scss' import './index.scss'
import GoodsList from './GoodsList';
// import GoodsList from "./GoodsList";
function Home() { function Home() {
// 吸顶状态 // 吸顶状态
const [stickyStatus, setStickyStatus] = useState<boolean>(false) // const [stickyStatus, setStickyStatus] = useState<boolean>(false)
// Tabs粘性状态
const [_, setTabsStickyStatus] = useState<boolean>(false)
useShareAppMessage(() => { useShareAppMessage(() => {
// 获取当前用户ID用于生成邀请链接 // 获取当前用户ID用于生成邀请链接
const userId = Taro.getStorageSync('UserId'); const userId = Taro.getStorageSync('UserId');
return { return {
title: '网宿小店 - 网宿软件', title: '🏠 首页 🏠',
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`, path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
success: function () { success: function () {
console.log('首页分享成功'); console.log('首页分享成功');
@@ -79,10 +78,15 @@ function Home() {
}); });
}; };
const onSticky = (item: IArguments) => { // const onSticky = (item: IArguments) => {
if(item){ // if(item){
setStickyStatus(!stickyStatus) // setStickyStatus(!stickyStatus)
} // }
// }
// 处理Tabs粘性状态变化
const handleTabsStickyChange = (isSticky: boolean) => {
setTabsStickyStatus(isSticky)
} }
const reload = () => { const reload = () => {
@@ -150,14 +154,13 @@ function Home() {
return ( return (
<> <>
<Sticky threshold={0} onChange={() => onSticky(arguments)}> {/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
<Header stickyStatus={stickyStatus}/> <Header />
</Sticky>
<div className={'flex flex-col mt-12'}> <div className={'flex flex-col mt-12'}>
<Menu/> <Menu/>
<Banner/> <Banner/>
<BestSellers/> <GoodsList onStickyChange={handleTabsStickyChange}/>
{/*<GoodsList/>*/}
</div> </div>
</> </>
) )

View File

@@ -15,3 +15,11 @@ rich-text img {
.no-margin { .no-margin {
margin: 0 !important; /* 使用 !important 来确保覆盖默认样式 */ margin: 0 !important; /* 使用 !important 来确保覆盖默认样式 */
} }
/* 文本截断样式 */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}

View File

@@ -1,8 +1,8 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Image, Divider, Badge} from "@nutui/nutui-react-taro"; import {Image, Badge, Popup, CellGroup, Cell} from "@nutui/nutui-react-taro";
import {ArrowLeft, Headphones, Share, Cart} from "@nutui/icons-react-taro"; import {ArrowLeft, Headphones, Share, Cart, ArrowRight} from "@nutui/icons-react-taro";
import Taro, {useShareAppMessage} from "@tarojs/taro"; import Taro, {useShareAppMessage} from "@tarojs/taro";
import {RichText, View} from '@tarojs/components' import {RichText, View, Text} from '@tarojs/components'
import {ShopGoods} from "@/api/shop/shopGoods/model"; import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods"; import {getShopGoods} from "@/api/shop/shopGoods";
import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec"; import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec";
@@ -14,21 +14,30 @@ import navTo, {wxParse} from "@/utils/common";
import SpecSelector from "@/components/SpecSelector"; import SpecSelector from "@/components/SpecSelector";
import "./index.scss"; import "./index.scss";
import {useCart} from "@/hooks/useCart"; import {useCart} from "@/hooks/useCart";
import {useConfig} from "@/hooks/useConfig";
const GoodsDetail = () => { const GoodsDetail = () => {
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
const [windowWidth, setWindowWidth] = useState<number>(390)
const [goods, setGoods] = useState<ShopGoods | null>(null); const [goods, setGoods] = useState<ShopGoods | null>(null);
const [files, setFiles] = useState<any[]>([]); const [files, setFiles] = useState<any[]>([]);
const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]); const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]);
const [skus, setSkus] = useState<ShopGoodsSku[]>([]); const [skus, setSkus] = useState<ShopGoodsSku[]>([]);
const [showSpecSelector, setShowSpecSelector] = useState(false); const [showSpecSelector, setShowSpecSelector] = useState(false);
const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart'); const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart');
const [showBottom, setShowBottom] = useState(false)
const [bottomItem, setBottomItem] = useState<any>({
title: '',
content: ''
})
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null); // const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router; const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id; const goodsId = router?.params?.id;
// 使用购物车Hook // 使用购物车Hook
const {cartCount, addToCart} = useCart(); const {cartCount, addToCart} = useCart()
const {config} = useConfig()
// 处理加入购物车 // 处理加入购物车
const handleAddToCart = () => { const handleAddToCart = () => {
@@ -117,7 +126,21 @@ const GoodsDetail = () => {
} }
}; };
const openBottom = (title: string, content: string) => {
setBottomItem({
title,
content
})
setShowBottom(true)
}
useEffect(() => { useEffect(() => {
Taro.getSystemInfo({
success: (res) => {
setWindowWidth(res.windowWidth)
setStatusBarHeight(Number(res.statusBarHeight) + 5)
},
});
if (goodsId) { if (goodsId) {
setLoading(true); setLoading(true);
@@ -187,12 +210,12 @@ const GoodsDetail = () => {
}); });
if (!goods || loading) { if (!goods || loading) {
return <div>...</div>; return <View>...</View>;
} }
return ( return (
<div className={"py-0"}> <View className={"py-0"}>
<div <View
className={ className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70" "fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70"
} }
@@ -200,36 +223,36 @@ const GoodsDetail = () => {
borderRadius: "100%", borderRadius: "100%",
width: "32px", width: "32px",
height: "32px", height: "32px",
top: "50px", top: statusBarHeight + 'px',
left: "10px", left: "10px",
}} }}
onClick={() => Taro.navigateBack()} onClick={() => Taro.navigateBack()}
> >
<ArrowLeft size={14}/> <ArrowLeft size={14}/>
</div> </View>
<div className={ <View className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90" "fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90"
} }
style={{ style={{
borderRadius: "100%", borderRadius: "100%",
width: "32px", width: "32px",
height: "32px", height: "32px",
top: "50px", top: statusBarHeight + 'px',
right: "110px", right: "110px",
}} }}
onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}> onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}>
<Badge value={cartCount} top="-2" right="2"> <Badge value={cartCount} top="-2" right="2">
<div style={{display: 'flex', alignItems: 'center'}}> <View style={{display: 'flex', alignItems: 'center'}}>
<Cart size={16}/> <Cart size={16}/>
</div> </View>
</Badge> </Badge>
</div> </View>
{ {
files.length > 0 && ( files.length > 0 && (
<Swiper defaultValue={0} indicator height={'350px'}> <Swiper defaultValue={0} indicator height={windowWidth}>
{files.map((item) => ( {files.map((item) => (
<Swiper.Item key={item}> <Swiper.Item key={item}>
<Image width="100%" height={'100%'} src={item.url} mode={'scaleToFill'} lazyLoad={false}/> <Image width={windowWidth} height={windowWidth} src={item.url} mode={'scaleToFill'} lazyLoad={false}/>
</Swiper.Item> </Swiper.Item>
))} ))}
</Swiper> </Swiper>
@@ -245,69 +268,116 @@ const GoodsDetail = () => {
/> />
) )
} }
<div <View
className={"flex flex-col justify-between items-center rounded-lg px-2"} className={"flex flex-col justify-between items-center"}
> >
<div <View
className={ className={
"flex flex-col rounded-lg bg-white shadow-sm w-full mt-2" "flex flex-col bg-white w-full"
} }
> >
<div className={"flex flex-col p-2 rounded-lg"}> <View className={"flex flex-col p-3 rounded-lg"}>
<> <>
<div className={'flex justify-between'}> <View className={'flex justify-between'}>
<div className={'flex text-red-500 text-xl items-baseline'}> <View className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span> <span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{goods.price}</span> <span className={'font-bold text-2xl'}>{goods.price}</span>
</div> </View>
<span className={"text-gray-400 text-xs"}> {goods.sales}</span> <span className={"text-gray-400 text-xs"}> {goods.sales}</span>
</div> </View>
<div className={'flex justify-between items-center'}> <View className={'flex justify-between items-center'}>
<div className={'goods-info'}> <View className={'goods-info'}>
<div className={"car-no text-lg"}> <View className={"car-no text-lg"}>
{goods.name} {goods.name}
</div> </View>
<div className={"flex justify-between text-xs py-1"}> <View className={"flex justify-between text-xs py-1"}>
<span className={"text-orange-500"}> <span className={"text-orange-500"}>
{goods.comments} {goods.comments}
</span> </span>
</div> </View>
</div> </View>
<View> <View>
<button <button
className={'flex flex-col justify-center items-center text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'} className={'flex flex-col justify-center items-center bg-white text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'}
open-type="share"><Share open-type="share">
size={20}/> <Share size={20}/>
<span className={'text-xs'}></span> <span className={'text-xs'}></span>
</button> </button>
</View> </View>
</div> </View>
</> </>
</div> </View>
</div> </View>
<div className={"mt-2 py-2"}> <View className={'w-full'}>
<Divider></Divider> {
config?.deliveryText && (
<CellGroup>
<Cell title={'配送'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.deliveryText || '14:30下单明天配送'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('配送', `${config?.deliveryText}`)}/>
<Cell title={'保障'} extra={
<View className="flex items-center">
<Text className={'truncate max-w-56 inline-block'}>{config?.guaranteeText || '支持7天无理由退货'}</Text>
<ArrowRight color="#cccccc" size={15}/>
</View>
} onClick={() => openBottom('保障', `${config?.guaranteeText}`)}/>
</CellGroup>
)
}
{config?.openComments == '1' && (
<CellGroup>
<Cell title={'用户评价 (0)'} extra={
<>
<Text></Text>
<ArrowRight color="#cccccc" size={15}/>
</>
} onClick={() => navTo(`/shop/comments/index`)}/>
<Cell className={'flex h-32 bg-white p-4'}>
</Cell>
</CellGroup>
)}
</View>
<View className={'w-full'}>
<RichText nodes={goods.content || '内容详情'}/> <RichText nodes={goods.content || '内容详情'}/>
</div> <View className={'h-24'}></View>
</div> </View>
</View>
{/*底部弹窗*/}
<Popup
visible={showBottom}
position="bottom"
onClose={() => {
setShowBottom(false)
}}
lockScroll
>
<View className={'flex flex-col p-4'}>
<Text className={'font-bold text-sm'}>{bottomItem.title}</Text>
<Text className={'text-gray-500 my-2'}>{bottomItem.content}</Text>
</View>
</Popup>
{/*底部购买按钮*/} {/*底部购买按钮*/}
<div className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-10'}> <View className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-8'}>
<View className={'btn-bar flex justify-between items-center'}> <View className={'btn-bar flex justify-between items-center'}>
<div className={'flex justify-center items-center mx-4'}> <View className={'flex justify-center items-center mx-4'}>
<button open-type="contact" className={'flex items-center'}> <button open-type="contact" className={'flex items-center'}>
<Headphones size={18} style={{marginRight: '4px'}}/> <Headphones size={18} style={{marginRight: '4px'}}/>
</button> </button>
</div> </View>
<div className={'buy-btn mx-4'}> <View className={'buy-btn mx-4'}>
<div className={'cart-add px-4 text-sm'} <View className={'cart-add px-4 text-sm'}
onClick={() => handleAddToCart()}> onClick={() => handleAddToCart()}>
</div> </View>
<div className={'cart-buy pl-4 pr-5 text-sm'} <View className={'cart-buy pl-4 pr-5 text-sm'}
onClick={() => handleBuyNow()}> onClick={() => handleBuyNow()}>
</div> </View>
</div> </View>
</View> </View>
</div> </View>
{/* 规格选择器 */} {/* 规格选择器 */}
{showSpecSelector && ( {showSpecSelector && (
@@ -320,7 +390,7 @@ const GoodsDetail = () => {
onClose={() => setShowSpecSelector(false)} onClose={() => setShowSpecSelector(false)}
/> />
)} )}
</div> </View>
); );
}; };