feat(pages): 实现商品分类页面及优化轮播图功能
- 新增商品分类页面,包含左侧导航和右侧商品列表 - 实现分类切换和商品展示功能 - 添加骨架屏加载效果和空状态处理 - 优化首页轮播图组件,支持自动播放和触摸滑动 - 调整轮播图高度默认值为300px- 移除旧的热卖商品逻辑,改为获取推荐文章 - 修复医生申请页面用户类型选择功能 - 更新页面标题文本内容 - 添加网站配置获取hook
This commit is contained in:
3
src/pages/category/category.config.ts
Normal file
3
src/pages/category/category.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分类'
|
||||
})
|
||||
425
src/pages/category/category.scss
Normal file
425
src/pages/category/category.scss
Normal file
@@ -0,0 +1,425 @@
|
||||
.category-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 1rpx);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 左侧分类导航 */
|
||||
.category-left {
|
||||
width: 200rpx;
|
||||
background-color: #fff;
|
||||
border-right: 2rpx solid #f0f0f0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.category-scroll {
|
||||
height: calc(100vh - 1rpx);
|
||||
}
|
||||
|
||||
.category-item {
|
||||
padding: 30rpx 20rpx;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
color: #ff6b35;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8rpx;
|
||||
height: 50rpx;
|
||||
background: linear-gradient(180deg, #ff6b35 0%, #ff8f6b 100%);
|
||||
border-radius: 0 8rpx 8rpx 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2rpx;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.active .category-name {
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.active .category-count {
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧商品列表 */
|
||||
.category-right {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
|
||||
.goods-scroll {
|
||||
height: calc(100vh - 1rpx);
|
||||
}
|
||||
|
||||
.goods-section {
|
||||
.section-title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: linear-gradient(135deg, #f8f8f8 0%, #f0f0f0 100%);
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
z-index: 10;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #fafafa;
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.goods-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 12rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.goods-price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.goods-price {
|
||||
font-size: 36rpx;
|
||||
color: #ff6b35;
|
||||
font-weight: 700;
|
||||
margin-right: 16rpx;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 24rpx;
|
||||
position: relative;
|
||||
top: -2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-stock {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
background-color: #f8f8f8;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 4rpx;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
padding: 120rpx 30rpx;
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '📋';
|
||||
display: block;
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.category-skeleton {
|
||||
display: flex;
|
||||
height: calc(100vh - 1rpx);
|
||||
|
||||
.category-left {
|
||||
width: 200rpx;
|
||||
background-color: #fff;
|
||||
border-right: 2rpx solid #f0f0f0;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.skeleton-category-item {
|
||||
padding: 30rpx 20rpx;
|
||||
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-right {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
|
||||
.skeleton-goods-item {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
|
||||
.skeleton-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 24rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-info {
|
||||
flex: 1;
|
||||
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 750rpx) {
|
||||
.category-left {
|
||||
width: 160rpx;
|
||||
|
||||
.category-item {
|
||||
padding: 24rpx 16rpx;
|
||||
|
||||
.category-name {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-right {
|
||||
.goods-section {
|
||||
.goods-list {
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.goods-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
.goods-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.goods-price-row {
|
||||
.goods-price {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-container {
|
||||
height: calc(100vh - 1rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
padding: 60rpx 40rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f6b 100%);
|
||||
color: #fff;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 50rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
src/pages/category/category.tsx
Normal file
270
src/pages/category/category.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useShareAppMessage } from "@tarojs/taro"
|
||||
import { useEffect, useState, useRef, useCallback } from "react"
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Image } from '@nutui/nutui-react-taro'
|
||||
import { listCmsNavigation } from "@/api/cms/cmsNavigation"
|
||||
import { CmsNavigation } from "@/api/cms/cmsNavigation/model"
|
||||
import { pageShopGoods } from "@/api/shop/shopGoods"
|
||||
import { ShopGoods } from "@/api/shop/shopGoods/model"
|
||||
import './category.scss'
|
||||
|
||||
function Category() {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [categories, setCategories] = useState<CmsNavigation[]>([])
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number>(0)
|
||||
const [goods, setGoods] = useState<{ [key: number]: ShopGoods[] }>({})
|
||||
const [allGoods, setAllGoods] = useState<ShopGoods[]>([])
|
||||
const rightScrollRef = useRef<any>(null)
|
||||
const [scrollIntoView, setScrollIntoView] = useState('')
|
||||
const [isScrollingByClick, setIsScrollingByClick] = useState(false)
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 获取商品分类
|
||||
const categoryList = await listCmsNavigation({ model: 'goods' })
|
||||
|
||||
if (!categoryList || categoryList.length === 0) {
|
||||
Taro.showToast({
|
||||
title: '暂无商品分类',
|
||||
icon: 'none'
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setCategories(categoryList)
|
||||
const firstCategory = categoryList[0]
|
||||
setSelectedCategoryId(firstCategory.navigationId!)
|
||||
|
||||
// 并行获取所有分类的商品数据
|
||||
const goodsPromises = categoryList.map((category: CmsNavigation) =>
|
||||
pageShopGoods({ categoryId: category.navigationId }).catch(err => {
|
||||
console.error(`分类 ${category.title} 商品加载失败:`, err)
|
||||
return { list: [] }
|
||||
})
|
||||
)
|
||||
|
||||
const goodsResults = await Promise.all(goodsPromises)
|
||||
|
||||
// 组织商品数据
|
||||
const goodsByCategory: { [key: number]: ShopGoods[] } = {}
|
||||
categoryList.forEach((category: CmsNavigation, index: number) => {
|
||||
goodsByCategory[category.navigationId!] = goodsResults[index]?.list || []
|
||||
})
|
||||
|
||||
setGoods(goodsByCategory)
|
||||
|
||||
// 获取所有商品用于搜索等功能
|
||||
try {
|
||||
const allGoodsRes = await pageShopGoods({})
|
||||
setAllGoods(allGoodsRes?.list || [])
|
||||
} catch (err) {
|
||||
console.error('获取所有商品失败:', err)
|
||||
}
|
||||
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '商品分类'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('分类数据加载失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initData().then()
|
||||
console.log(allGoods,'allGoods')
|
||||
}, [])
|
||||
|
||||
// 点击左侧分类
|
||||
const handleCategoryClick = (categoryId: number) => {
|
||||
setIsScrollingByClick(true)
|
||||
setSelectedCategoryId(categoryId)
|
||||
setScrollIntoView(`category-${categoryId}`)
|
||||
|
||||
// 延迟重置滚动标志
|
||||
setTimeout(() => {
|
||||
setIsScrollingByClick(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 右侧滚动时处理分类切换
|
||||
const handleRightScroll = useCallback((e: any) => {
|
||||
console.log(e,'右侧滚动时处理分类切换')
|
||||
if (isScrollingByClick) return
|
||||
|
||||
// 这里可以添加逻辑来检测当前滚动到哪个分类
|
||||
// 由于小程序限制,暂时简化处理
|
||||
}, [isScrollingByClick])
|
||||
|
||||
// 跳转商品详情
|
||||
const goToGoodsDetail = (goodsId: number) => {
|
||||
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${goodsId}` })
|
||||
}
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '商品分类',
|
||||
path: '/pages/category/category',
|
||||
success: function () {
|
||||
console.log('分享成功')
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 骨架屏组件
|
||||
const CategorySkeleton = () => (
|
||||
<View className="category-skeleton" style={{
|
||||
marginTop: '1rpx',
|
||||
}}>
|
||||
<View className="category-left">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<View key={i} className="skeleton-category-item">
|
||||
<View className="skeleton-text" />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className="category-right">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<View key={i} className="skeleton-goods-item">
|
||||
<View className="skeleton-image" />
|
||||
<View className="skeleton-info">
|
||||
<View className="skeleton-text" style={{ width: '80%', marginBottom: '8px' }} />
|
||||
<View className="skeleton-text" style={{ width: '60%' }} />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <CategorySkeleton />
|
||||
}
|
||||
|
||||
// 空状态处理
|
||||
if (!categories || categories.length === 0) {
|
||||
return (
|
||||
<View className="empty-container">
|
||||
<View className="empty-content">
|
||||
<Text className="empty-icon">📋</Text>
|
||||
<Text className="empty-title">暂无商品分类</Text>
|
||||
<Text className="empty-desc">请稍后再试或联系客服</Text>
|
||||
<View className="empty-action" onClick={initData}>
|
||||
<Text>重新加载</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="category-container" style={{
|
||||
marginTop: '1rpx',
|
||||
}}>
|
||||
{/* 左侧分类导航 */}
|
||||
<View className="category-left">
|
||||
<ScrollView
|
||||
className="category-scroll"
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
> {categories.map((category) => (
|
||||
<View
|
||||
key={category.navigationId}
|
||||
className={`category-item ${
|
||||
selectedCategoryId === category.navigationId ? 'active' : ''
|
||||
}`}
|
||||
onClick={() => handleCategoryClick(category.navigationId!)}
|
||||
>
|
||||
<Text className="category-name">{category.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 右侧商品列表 */}
|
||||
<View className="category-right">
|
||||
<ScrollView
|
||||
ref={rightScrollRef}
|
||||
className="goods-scroll"
|
||||
scrollY
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
scrollIntoView={scrollIntoView}
|
||||
onScroll={handleRightScroll}
|
||||
>
|
||||
{categories.map((category) => {
|
||||
const categoryGoods = goods[category.navigationId!] || []
|
||||
return (
|
||||
<View
|
||||
key={category.navigationId}
|
||||
id={`category-${category.navigationId}`}
|
||||
className="goods-section"
|
||||
>
|
||||
<View className="section-title">
|
||||
<Text>{category.title}</Text>
|
||||
</View>
|
||||
|
||||
{categoryGoods.length > 0 ? (
|
||||
<View className="goods-list">
|
||||
{categoryGoods.map((item) => (
|
||||
<View
|
||||
key={item.goodsId}
|
||||
className="goods-item"
|
||||
onClick={() => goToGoodsDetail(item.goodsId!)}
|
||||
>
|
||||
<Image
|
||||
className="goods-image"
|
||||
src={item.image || ''}
|
||||
mode="aspectFill"
|
||||
lazyLoad
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<View className="goods-info">
|
||||
<Text className="goods-name">{item.name}</Text>
|
||||
{item.comments && (
|
||||
<Text className="goods-desc">{item.comments}</Text>
|
||||
)}
|
||||
<View className="goods-price-row">
|
||||
<Text className="goods-price">¥{item.price}</Text>
|
||||
{item.salePrice && Number(item.salePrice) !== Number(item.price) && (
|
||||
<Text className="goods-original-price">¥{item.salePrice}</Text>
|
||||
)}
|
||||
</View>
|
||||
{item.stock !== undefined && (
|
||||
<Text className="goods-stock">
|
||||
库存: {item.stock > 0 ? item.stock : '缺货'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View className="empty-section">
|
||||
<Text>该分类暂无商品</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Category
|
||||
35
src/pages/category/components/ArticleList.tsx
Normal file
35
src/pages/category/components/ArticleList.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {Image, Cell} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const ArticleList = (props: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'px-3'}>
|
||||
{props.data.map((item: any, index: number) => {
|
||||
return (
|
||||
<Cell
|
||||
title={
|
||||
<View>
|
||||
<View className="text-base font-medium mb-1">{item.title}</View>
|
||||
{item.comments && (
|
||||
<Text className="text-sm text-gray-500 leading-relaxed">
|
||||
{item.comments}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
extra={
|
||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
|
||||
}
|
||||
key={index}
|
||||
onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ArticleList
|
||||
59
src/pages/category/components/ArticleTabs.tsx
Normal file
59
src/pages/category/components/ArticleTabs.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Tabs, Loading} from '@nutui/nutui-react-taro'
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import ArticleList from "./ArticleList";
|
||||
|
||||
const ArticleTabs = (props: any) => {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = async (value) => {
|
||||
const {data} = props
|
||||
pageCmsArticle({
|
||||
categoryId: data[value].navigationId,
|
||||
page: 1,
|
||||
status: 0,
|
||||
limit: 10
|
||||
}).then((res) => {
|
||||
res && setList(res?.list || [])
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setTab1value(value)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload(0).then()
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
value={tab1value}
|
||||
onChange={(value) => {
|
||||
reload(value).then()
|
||||
}}
|
||||
>
|
||||
{props.data?.map((item, index) => {
|
||||
return (
|
||||
<Tabs.TabPane title={item.categoryName} key={index}/>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<ArticleList data={list}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ArticleTabs
|
||||
31
src/pages/category/components/Banner.tsx
Normal file
31
src/pages/category/components/Banner.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAd} from "@/api/cms/cmsAd";
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(439).then(data => {
|
||||
setItem(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
|
||||
{item?.imageList?.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MyPage
|
||||
@@ -5,55 +5,123 @@ import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {listShopGoods} from "@/api/shop/shopGoods";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
|
||||
const MyPage = () => {
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
// const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
// const [groupBuy, setGroupBuy] = useState<CmsAd>()
|
||||
const [hotGoods, setHotGoods] = useState<ShopGoods[]>([])
|
||||
const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||
|
||||
// 用于记录触摸开始位置
|
||||
// const touchStartRef = useRef({x: 0, y: 0})
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
// 轮播图
|
||||
getCmsAdByCode('flash').then(data => {
|
||||
setCarouselData(data)
|
||||
})
|
||||
// 今日热卖素材(上层图片)
|
||||
// getCmsAd(444).then(data => {
|
||||
// setHotToday(data)
|
||||
// })
|
||||
// 社区拼团素材(下层图片)
|
||||
// getCmsAd(445).then(data => {
|
||||
// setGroupBuy(data)
|
||||
// })
|
||||
// 今日热卖
|
||||
listShopGoods({categoryId: 4424, limit: 2}).then(data => {
|
||||
setHotGoods(data)
|
||||
})
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 轮播图
|
||||
const flash = await getCmsAdByCode('flash')
|
||||
// 今日热卖
|
||||
const hotToday = await getCmsAdByCode('hot_today')
|
||||
// 时里动态
|
||||
const news = await pageCmsArticle({limit:1,recommend:1})
|
||||
// 赋值
|
||||
if(flash){
|
||||
setCarouselData(flash)
|
||||
}
|
||||
if(hotToday){
|
||||
setHotToday(hotToday)
|
||||
}
|
||||
if(news && news.list.length > 0){
|
||||
setItem(news.list[0])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Banner数据加载失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认200px
|
||||
const carouselHeight = carouselData?.height || 200;
|
||||
// 轮播图高度,默认300px
|
||||
const carouselHeight = carouselData?.height || 300;
|
||||
|
||||
// 骨架屏组件
|
||||
const BannerSkeleton = () => (
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图骨架屏 */}
|
||||
<View style={{width: '50%', height: '100%'}}>
|
||||
<View
|
||||
className="bg-gray-200 rounded animate-pulse"
|
||||
style={{height: `${carouselHeight}px`}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 右侧骨架屏 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层骨架屏 */}
|
||||
<View className="ml-2 bg-white rounded-lg">
|
||||
<View className="px-3 my-2">
|
||||
<View className="bg-gray-200 h-4 w-16 rounded animate-pulse"/>
|
||||
</View>
|
||||
<View className="px-3 flex" style={{height: '110px'}}>
|
||||
{[1, 2].map(i => (
|
||||
<View key={i} className="item flex flex-col mr-4">
|
||||
<View className="bg-gray-200 rounded animate-pulse" style={{width: 70, height: 70}}/>
|
||||
<View className="bg-gray-200 h-3 w-16 rounded mt-2 animate-pulse"/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下层骨架屏 */}
|
||||
<View className="ml-2 bg-white rounded-lg mt-3">
|
||||
<View className="px-3 my-2">
|
||||
<View className="bg-gray-200 h-4 w-20 rounded animate-pulse"/>
|
||||
</View>
|
||||
<View className="rounded-lg px-3 pb-3">
|
||||
<View className="bg-gray-200 rounded animate-pulse" style={{width: '100%', height: 106}}/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
// 如果正在加载,显示骨架屏
|
||||
if (loading) {
|
||||
return <BannerSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View style={{width: '50%', height: '100%'}}>
|
||||
<View
|
||||
style={{width: '50%', height: '100%'}}
|
||||
className="banner-swiper-container"
|
||||
>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
indicator
|
||||
style={{height: `${carouselHeight}px`}}
|
||||
autoPlay
|
||||
duration={3000}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
||||
}}
|
||||
disableTouch={false}
|
||||
direction="horizontal"
|
||||
className="custom-swiper"
|
||||
>
|
||||
{carouselData?.imageList?.map((img, index) => (
|
||||
<Swiper.Item key={index}>
|
||||
{carouselData && carouselData?.imageList?.map((img, index) => (
|
||||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||||
<Image
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -61,7 +129,11 @@ const MyPage = () => {
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => navTo(`${img.path}`)}
|
||||
lazyLoad={false}
|
||||
style={{height: `${carouselHeight}px`, borderRadius: '4px'}}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
borderRadius: '4px',
|
||||
touchAction: 'manipulation' // 关键修改:优化触摸操作
|
||||
}}
|
||||
/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
@@ -71,26 +143,26 @@ const MyPage = () => {
|
||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层图片 - 使用今日热卖素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg'}>
|
||||
<View className={'ml-2 bg-white rounded-lg shadow-sm'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
||||
<View className={'px-3 flex'} style={{
|
||||
<View className={'px-3 flex justify-between'} style={{
|
||||
height: '110px'
|
||||
}}>
|
||||
{
|
||||
hotGoods.map(item => (
|
||||
<View className={'item flex flex-col mr-4'}>
|
||||
hotToday?.imageList?.map(item => (
|
||||
<View className={'item flex flex-col mr-1'} key={item.url}>
|
||||
<Image
|
||||
width={70}
|
||||
height={70}
|
||||
src={item.image}
|
||||
src={item.url}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('/shop/category/index?id=4424')}
|
||||
onClick={() => navTo(item.path)}
|
||||
/>
|
||||
<View className={'text-xs py-2'}>到手价¥{item.price}</View>
|
||||
<View className={'text-xs py-2 text-orange-600 whitespace-nowrap text-center'}>{item.title || '到手价¥9.9'}</View>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
@@ -98,19 +170,19 @@ const MyPage = () => {
|
||||
</View>
|
||||
|
||||
{/* 下层图片 - 使用社区拼团素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg mt-3'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>走进社区</View>
|
||||
<View className={'ml-2 bg-white rounded-lg mt-3 shadow-sm'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>{item?.overview || item?.categoryName || '推荐文章'}</View>
|
||||
<View className={'rounded-lg px-3 pb-3'}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={100}
|
||||
src={'https://oss.wsdns.cn/20250919/941c99899e694a7798cab3bb28f1f238.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90'}
|
||||
height={94}
|
||||
src={item?.image}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=10109')}
|
||||
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -120,4 +192,3 @@ const MyPage = () => {
|
||||
}
|
||||
|
||||
export default MyPage
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// 订单页面样式
|
||||
.order-page {
|
||||
// 订单相关样式
|
||||
}
|
||||
@@ -3,18 +3,28 @@ import navTo from "@/utils/common";
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro'
|
||||
import {useUser} from '@/hooks/useUser'
|
||||
import {useEffect} from "react";
|
||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model"; // 使用新的自定义Hook
|
||||
|
||||
const IsDealer = () => {
|
||||
const themeStyles = useThemeStyles();
|
||||
const { config } = useConfig(); // 使用新的Hook
|
||||
const {isSuperAdmin} = useUser();
|
||||
const {dealerUser} = useDealerUser()
|
||||
const [register, setRegister] = useState<CmsAd>()
|
||||
|
||||
const reload = async () => {
|
||||
const item = await getCmsAdByCode('register')
|
||||
setRegister(item)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [])
|
||||
reload().then()
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 管理中心
|
||||
@@ -54,7 +64,7 @@ const IsDealer = () => {
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}}
|
||||
className={'pl-3 text-orange-100 font-medium'}>VIP申请</Text>
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '入驻申请'}</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -78,12 +88,12 @@ const IsDealer = () => {
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通VIP</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{register?.name || '注册会员'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}></Text>
|
||||
</View>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/doctor/apply/add', true)}
|
||||
onClick={() => navTo(`${register?.path}`, true)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user