fix(api): 替换所有接口请求的域名为 guilixu-api.websoft.top
- 将获取 STS Token 的接口地址由 paopao-api 改为 guilixu-api - 更新图片上传接口地址为新的 guilixu-api 域名 - 修改用户推广页面中邀请码链接和二维码接口的域名 - 更改注册页微信登录接口请求的域名为 guilixu-api
This commit is contained in:
28
src/pages/index/article-detail.tsx
Normal file
28
src/pages/index/article-detail.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import { View, Text, RichText, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '文章详情',
|
||||
})
|
||||
|
||||
const content = '<p style="color:#666;font-size:14px;line-height:1.8;">文章内容区域,后续对接CMS文章API后自动渲染。</p>'
|
||||
|
||||
const ArticleDetailPage: React.FC = () => {
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-white'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
<View className='p-4'>
|
||||
<Text className='text-lg font-bold text-gray-800 block mb-2'>文章标题</Text>
|
||||
<Text className='text-xs text-gray-400 block mb-4'>2026-05-11</Text>
|
||||
<RichText nodes={content} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticleDetailPage
|
||||
3
src/pages/index/article-list.config.ts
Normal file
3
src/pages/index/article-list.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '活动列表',
|
||||
}
|
||||
125
src/pages/index/article-list.tsx
Normal file
125
src/pages/index/article-list.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listCmsArticle } from '@/api/cms/cmsArticle'
|
||||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '活动列表',
|
||||
})
|
||||
|
||||
const ArticleListPage: React.FC = () => {
|
||||
const [articles, setArticles] = useState<CmsArticle[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
|
||||
const type = (Taro.getCurrentInstance().router?.params as any)?.type || 'activity'
|
||||
|
||||
useEffect(() => {
|
||||
fetchArticles()
|
||||
}, [])
|
||||
|
||||
const fetchArticles = async (loadMore = false) => {
|
||||
try {
|
||||
const currentPage = loadMore ? page + 1 : 1
|
||||
const data = await listCmsArticle({
|
||||
category: type === 'activity' ? 'activity' : 'notice',
|
||||
status: 1,
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
if (data) {
|
||||
if (loadMore) {
|
||||
setArticles(prev => [...prev, ...data])
|
||||
setPage(currentPage)
|
||||
} else {
|
||||
setArticles(data)
|
||||
}
|
||||
setHasMore(data.length >= 10)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取文章列表失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!hasMore || loading) return
|
||||
fetchArticles(true)
|
||||
}
|
||||
|
||||
const handleArticleClick = (id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/index/article-detail?id=${id}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView
|
||||
scrollY
|
||||
style={{ height: scrollHeight }}
|
||||
onScrollToLower={handleLoadMore}
|
||||
>
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : articles.length === 0 ? (
|
||||
<EmptyState text='暂无活动' />
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{articles.map(item => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg mb-3 overflow-hidden'
|
||||
onClick={() => handleArticleClick(item.id)}
|
||||
>
|
||||
{item.coverImage && (
|
||||
<Image
|
||||
className='w-full'
|
||||
src={item.coverImage}
|
||||
mode='aspectFill'
|
||||
style={{ height: '160px' }}
|
||||
/>
|
||||
)}
|
||||
<View className='p-3'>
|
||||
<Text className='text-base font-medium text-gray-800 block mb-1'>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.summary && (
|
||||
<Text className='text-sm text-gray-500 mb-2 block' numberOfLines={2}>
|
||||
{item.summary}
|
||||
</Text>
|
||||
)}
|
||||
<View className='flex justify-between items-center'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
{item.createTime}
|
||||
</Text>
|
||||
{item.isHot && (
|
||||
<View className='px-2 py-0 bg-red-500 rounded-full'>
|
||||
<Text className='text-xs text-white'>热门</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{hasMore && (
|
||||
<View className='py-3 text-center'>
|
||||
<Text className='text-gray-400 text-sm'>加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticleListPage
|
||||
3
src/pages/index/coupon-center.config.ts
Normal file
3
src/pages/index/coupon-center.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '领券中心',
|
||||
}
|
||||
125
src/pages/index/coupon-center.tsx
Normal file
125
src/pages/index/coupon-center.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { listShopCoupon } from '@/api/shop/shopCoupon'
|
||||
import type { ShopCoupon } from '@/api/shop/shopCoupon/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '领券中心',
|
||||
})
|
||||
|
||||
const CouponCenterPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [coupons, setCoupons] = useState<ShopCoupon[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
fetchCoupons()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const fetchCoupons = async () => {
|
||||
try {
|
||||
const data = await listShopCoupon({ status: 1 })
|
||||
setCoupons(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取优惠券失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClaimCoupon = (id: number) => {
|
||||
Taro.showModal({
|
||||
title: '领取优惠券',
|
||||
content: '确定要领取这张优惠券吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.showToast({ title: '领取成功', icon: 'success' })
|
||||
fetchCoupons()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getCouponTypeText = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '无门槛券',
|
||||
2: '满减券',
|
||||
3: '折扣券',
|
||||
4: '运费券',
|
||||
}
|
||||
return typeMap[type] || '优惠券'
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : coupons.length === 0 ? (
|
||||
<EmptyState text='暂无可用优惠券' />
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{coupons.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg mb-3 overflow-hidden'
|
||||
>
|
||||
<View className='flex'>
|
||||
<View
|
||||
className='p-4 text-white flex flex-col items-center justify-center'
|
||||
style={{ width: '120px', backgroundColor: '#0e932e' }}
|
||||
>
|
||||
<Text className='text-2xl font-bold'>
|
||||
{item.discountType === 3 ? `${item.discountValue}折` : `¥${item.amount}`}
|
||||
</Text>
|
||||
<Text className='text-xs mt-1'>
|
||||
{item.minAmount ? `满${item.minAmount}可用` : '无门槛'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='flex-1 p-3'>
|
||||
<Text className='text-sm font-medium text-gray-800 block mb-1'>
|
||||
{getCouponTypeText(item.couponType)}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-500 mb-1 block'>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 block'>
|
||||
有效期至:{item.expireTime}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='flex items-center pr-3'>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full'
|
||||
style={{ backgroundColor: '#0e932e' }}
|
||||
onClick={() => handleClaimCoupon(item.id!)}
|
||||
>
|
||||
<Text className='text-xs text-white'>领取</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponCenterPage
|
||||
3
src/pages/index/coupon-center/index.config.ts
Normal file
3
src/pages/index/coupon-center/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '领券中心',
|
||||
}
|
||||
164
src/pages/index/coupon-center/index.tsx
Normal file
164
src/pages/index/coupon-center/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
import { listCouponCenter } from '@/api/shop/shopCoupon'
|
||||
import { takeCoupon } from '@/api/shop/shopUserCoupon'
|
||||
import type { ShopCouponWithTake } from '@/api/shop/shopCoupon/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '领券中心',
|
||||
})
|
||||
|
||||
const CouponCenterPage: React.FC = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const [coupons, setCoupons] = useState<ShopCouponWithTake[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [claiming, setClaiming] = useState<number | null>(null)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
fetchCoupons()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const fetchCoupons = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await listCouponCenter({ status: 0, isExpire: 0 })
|
||||
setCoupons(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取优惠券失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClaimCoupon = async (id: number) => {
|
||||
if (claiming) return
|
||||
setClaiming(id)
|
||||
try {
|
||||
await takeCoupon(id)
|
||||
Taro.showToast({ title: '领取成功', icon: 'success' })
|
||||
// 更新本地状态,避免重新请求
|
||||
setCoupons(prev =>
|
||||
prev.map(c => c.id === id ? { ...c, hasTake: true, userTakeNum: (c.userTakeNum || 0) + 1 } : c)
|
||||
)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '领取失败', icon: 'none' })
|
||||
} finally {
|
||||
setClaiming(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getCouponValueText = (item: ShopCouponWithTake) => {
|
||||
switch (item.type) {
|
||||
case 20: return `${item.discount}折`
|
||||
case 40: return `¥${item.reducePrice}`
|
||||
case 50: return item.useCount && item.useCount > 0 ? `${item.useCount}次` : '不限次'
|
||||
default: return `¥${item.reducePrice}`
|
||||
}
|
||||
}
|
||||
|
||||
const getCouponConditionText = (item: ShopCouponWithTake) => {
|
||||
if (item.type === 40) return '无门槛'
|
||||
if (item.type === 50) {
|
||||
const parts: string[] = []
|
||||
if (item.venueType !== undefined && item.venueType !== null) parts.push(`场地类型${item.venueType}`)
|
||||
if (item.useDuration && item.useDuration > 0) parts.push(`${item.useDuration}分钟`)
|
||||
return parts.length > 0 ? parts.join(' | ') : '场地使用'
|
||||
}
|
||||
if (item.minPrice && Number(item.minPrice) > 0) return `满${item.minPrice}可用`
|
||||
return '无门槛'
|
||||
}
|
||||
|
||||
const getCouponExpireText = (item: ShopCouponWithTake) => {
|
||||
if (item.expireType === 10) return `领取后${item.expireDay}天内有效`
|
||||
if (item.endTime) return `有效期至:${item.endTime.slice(0, 10)}`
|
||||
return ''
|
||||
}
|
||||
|
||||
const getCouponTypeText = (type?: number) => {
|
||||
const map: Record<number, string> = { 10: '满减券', 20: '折扣券', 30: '免费券', 40: '无门槛券', 50: '场地使用券' }
|
||||
return map[type || 0] || '优惠券'
|
||||
}
|
||||
|
||||
const isClaimable = (item: ShopCouponWithTake) => {
|
||||
if (item.hasTake) return false
|
||||
if (item.limitPerUser !== -1 && (item.userTakeNum || 0) >= (item.limitPerUser || 1)) return false
|
||||
if (item.totalCount !== -1 && (item.issuedCount || 0) >= (item.totalCount || 0)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isLoggedIn) return null
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{loading ? (
|
||||
<View className='flex items-center justify-center py-10'>
|
||||
<Text className='text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
) : coupons.length === 0 ? (
|
||||
<EmptyState text='暂无可领取的优惠券' />
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{coupons.map((item) => {
|
||||
const canClaim = isClaimable(item)
|
||||
return (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg mb-3 overflow-hidden'
|
||||
style={{ opacity: canClaim ? 1 : 0.6 }}
|
||||
>
|
||||
<View className='flex'>
|
||||
<View
|
||||
className='p-4 text-white flex flex-col items-center justify-center'
|
||||
style={{ width: '120px', backgroundColor: canClaim ? (
|
||||
item.type === 40 ? '#3b82f6' :
|
||||
item.type === 50 ? '#06b6d4' :
|
||||
'#0e932e'
|
||||
) : '#999' }}
|
||||
>
|
||||
<Text className='text-2xl font-bold'>{getCouponValueText(item)}</Text>
|
||||
<Text className='text-xs mt-1'>{getCouponConditionText(item)}</Text>
|
||||
</View>
|
||||
<View className='flex-1 p-3'>
|
||||
<Text className='text-sm font-medium text-gray-800 block mb-1'>
|
||||
{getCouponTypeText(item.type)}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-500 mb-1 block'>{item.name}</Text>
|
||||
<Text className='text-xs text-gray-400 block'>{getCouponExpireText(item)}</Text>
|
||||
</View>
|
||||
<View className='flex items-center pr-3'>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full'
|
||||
style={{ backgroundColor: canClaim ? (
|
||||
item.type === 40 ? '#3b82f6' :
|
||||
item.type === 50 ? '#06b6d4' :
|
||||
'#0e932e'
|
||||
) : '#ccc' }}
|
||||
onClick={() => canClaim && handleClaimCoupon(item.id!)}
|
||||
>
|
||||
<Text className='text-xs text-white'>
|
||||
{claiming === item.id ? '领取中' : item.hasTake ? '已领取' : '领取'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponCenterPage
|
||||
6
src/pages/index/index.config.ts
Normal file
6
src/pages/index/index.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
navigationBarTitleText: '鑫龙家电',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f8f8f8'
|
||||
}
|
||||
9
src/pages/index/index.scss
Normal file
9
src/pages/index/index.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.index-page {
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.nut-button {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
267
src/pages/index/index.tsx
Normal file
267
src/pages/index/index.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Swiper, SwiperItem, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
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'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '首页',
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
|
||||
const categories = ['全部', '球拍', '球鞋', '服装', '配件']
|
||||
|
||||
// 功能入口
|
||||
const featureEntries = [
|
||||
{ icon: '🎁', label: '全部商品', url: '/pages/shop/index' },
|
||||
{ icon: '🎯', label: '限时秒杀', url: '/pages/index/seckill' },
|
||||
{ icon: '👥', label: '拼团活动', url: '/pages/index/group-buy' },
|
||||
{ icon: '🎫', label: '领券中心', url: '/pages/index/coupon-center' },
|
||||
{ icon: '⭐', label: '积分商城', url: '/pages/points/index' },
|
||||
{ icon: '👑', label: '会员中心', url: '/pages/user/member/index' },
|
||||
{ icon: '📞', label: '联系客服', url: '/pages/customer-service' },
|
||||
{ icon: '🏪', label: '门店地址', url: '/pages/index/store-list' },
|
||||
]
|
||||
|
||||
// 获取轮播图
|
||||
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 })
|
||||
if (res?.list) {
|
||||
setHotProducts(res.list)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取热销商品失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessageClick = () => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({ url: '/pages/index/notification' })
|
||||
}
|
||||
|
||||
// tabBar 页面列表
|
||||
const tabBarPages = ['/pages/index/index', '/pages/shop/index', '/pages/points/index', '/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}>
|
||||
<Text className='text-xl'>🔔</Text>
|
||||
{isLoggedIn && (
|
||||
<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={item.imageList?.[0]?.url || item.image || ''}
|
||||
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={item.image}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
) : (
|
||||
<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'>
|
||||
<Text className='text-sm font-bold text-red-500 mr-1'>¥{item.price || '0'}</Text>
|
||||
{item.salePrice && item.salePrice !== item.price && (
|
||||
<Text className='text-xs text-gray-400 line-through'>¥{item.salePrice}</Text>
|
||||
)}
|
||||
</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
|
||||
87
src/pages/index/notification.tsx
Normal file
87
src/pages/index/notification.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopMessage } from '@/api/shop/shopMessage'
|
||||
import type { ShopMessage } from '@/api/shop/shopMessage'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '消息通知',
|
||||
})
|
||||
|
||||
const NotificationPage: React.FC = () => {
|
||||
const [list, setList] = useState<ShopMessage[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
loadList(1)
|
||||
}, [])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const res = await listShopMessage({
|
||||
page: p,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
if (res?.list) {
|
||||
if (p === 1) {
|
||||
setList(res.list)
|
||||
} else {
|
||||
setList(prev => [...prev, ...res.list])
|
||||
}
|
||||
setFinished(res.list.length < 10)
|
||||
setPage(p)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载通知失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1' onScrollToLower={handleLoadMore}>
|
||||
{list.length === 0 ? (
|
||||
<EmptyState text='暂无消息' />
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{list.map(item => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg p-3 mb-2'
|
||||
style={{ opacity: item.isRead ? 0.7 : 1 }}
|
||||
>
|
||||
<View className='flex justify-between items-center mb-1'>
|
||||
<View className='flex items-center gap-2'>
|
||||
{!item.isRead && <View className='w-1 h-1 rounded-full bg-red-500' />}
|
||||
<Text className='text-sm font-medium text-gray-800'>{item.title}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-400'>{item.createdAt}</Text>
|
||||
</View>
|
||||
<Text className='text-xs text-gray-500'>{item.content}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationPage
|
||||
115
src/pages/index/search.tsx
Normal file
115
src/pages/index/search.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Input } from '@nutui/nutui-react-taro'
|
||||
import ProductCard from '@/components/common/ProductCard'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '搜索',
|
||||
})
|
||||
|
||||
const SearchPage: React.FC = () => {
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [list, setList] = useState<ShopGoods[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
const [history, setHistory] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const saved = Taro.getStorageSync('search_history')
|
||||
if (saved) setHistory(JSON.parse(saved))
|
||||
}, [])
|
||||
|
||||
const doSearch = async (kw?: string) => {
|
||||
const q = kw || keyword
|
||||
if (!q.trim()) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const params: ShopGoodsParam = { keywords: q, isShow: 1, page: 1, limit: 20 }
|
||||
const res = await pageShopGoods(params)
|
||||
setList(res?.list || [])
|
||||
// 保存搜索历史
|
||||
const newHistory = [q, ...history.filter(h => h !== q)].slice(0, 10)
|
||||
setHistory(newHistory)
|
||||
Taro.setStorageSync('search_history', JSON.stringify(newHistory))
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const clearHistory = () => {
|
||||
setHistory([])
|
||||
Taro.removeStorageSync('search_history')
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-white'>
|
||||
{/* 搜索栏 */}
|
||||
<View className='flex items-center gap-2 px-3 py-2'>
|
||||
<Input
|
||||
className='flex-1 bg-gray-100 rounded-full px-3 py-1 text-sm'
|
||||
placeholder='搜索商品'
|
||||
value={keyword}
|
||||
onChange={val => setKeyword(val)}
|
||||
onConfirm={() => doSearch()}
|
||||
/>
|
||||
<Text className='text-sm text-green-600' onClick={() => doSearch()}>搜索</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{list.length > 0 ? (
|
||||
<View className='grid grid-cols-2 gap-2 px-3 py-2'>
|
||||
{list.map(item => (
|
||||
<ProductCard key={item.goodsId} product={item} />
|
||||
))}
|
||||
</View>
|
||||
) : !loading && keyword === '' ? (
|
||||
<View className='px-3 pt-4'>
|
||||
{history.length > 0 && (
|
||||
<View className='mb-4'>
|
||||
<View className='flex justify-between items-center mb-2'>
|
||||
<Text className='text-sm font-medium text-gray-700'>搜索历史</Text>
|
||||
<Text className='text-xs text-gray-400' onClick={clearHistory}>清空</Text>
|
||||
</View>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
{history.map((h, i) => (
|
||||
<View
|
||||
key={i}
|
||||
className='px-3 py-1 bg-gray-100 rounded-full'
|
||||
onClick={() => { setKeyword(h); doSearch(h) }}
|
||||
>
|
||||
<Text className='text-xs text-gray-600'>{h}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View className='mb-2'>
|
||||
<Text className='text-sm font-medium text-gray-700 mb-2 block'>热门搜索</Text>
|
||||
<View className='flex flex-wrap gap-2'>
|
||||
{['羽毛球拍', '球鞋', '运动服', '手胶'].map((h) => (
|
||||
<View
|
||||
key={h}
|
||||
className='px-3 py-1 bg-gray-100 rounded-full'
|
||||
onClick={() => { setKeyword(h); doSearch(h) }}
|
||||
>
|
||||
<Text className='text-xs text-gray-600'>{h}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<EmptyState text='未找到相关商品' />
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchPage
|
||||
Reference in New Issue
Block a user