feat(user): 新增收货地址管理及售后申请页面

- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
2026-07-01 12:11:56 +08:00
parent bf6ed504cc
commit 1fa58040f3
636 changed files with 58878 additions and 716 deletions

View File

@@ -0,0 +1,270 @@
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'
import Price from '@/components/common/Price'
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 = ['全部', '球拍', '球鞋', '服装', '配件']
// 功能入口8个
const featureEntries = [
{ icon: '🎁', label: '全部商品', url: '/pages/shop/index' },
{ icon: '🎯', label: '限时秒杀', url: '/pages/shop/seckill-list/index' },
{ icon: '🎫', label: '领券中心', url: '/pages/index/coupon-center/index' },
{ icon: '⭐', label: '我的收藏', url: '/pages/user/favorite-list/index' },
{ icon: '👑', label: '会员中心', url: '/pages/user/member/index' },
{ icon: '💰', label: '积分商城', url: '/pages/points/index' },
{ icon: '🏪', label: '门店信息', url: '/pages/store/list/index' },
{ icon: '❓', label: '帮助中心', url: '/pages/user/help-center/index' },
]
// 获取轮播图
useEffect(() => {
const fetchBanners = async () => {
try {
const data = await listCmsAd({ adType: 'banner', status: 0 })
setBanners(data || [])
} catch (e) {
console.error('获取轮播图失败:', e)
}
}
const fetchAnnouncements = async () => {
try {
const data = await listCmsArticle({ category: 'notice', status: 1 })
setAnnouncements(data || [])
} catch (e) {
console.error('获取公告失败:', e)
} finally {
setLoading(false)
}
}
fetchBanners()
fetchAnnouncements()
fetchHotProducts()
}, [])
// 获取热销商品
const fetchHotProducts = async () => {
try {
const res = await pageShopGoods({ page: 1, limit: 4, status: 0, recommend: 1 })
if (res?.list) {
setHotProducts(res.list)
}
} catch (e) {
console.error('获取热销商品失败:', e)
}
}
const handleMessageClick = () => {
if (!isLoggedIn) {
Taro.navigateTo({ url: '/passport/login' })
return
}
Taro.navigateTo({ url: '/pages/index/notification' })
}
// tabBar 页面列表
const tabBarPages = ['/pages/index/index', '/pages/shop/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='aspectFit'
/>
) : (
<View style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} className='bg-gray-100 flex items-center justify-center'>
<Text className='text-xs text-gray-300'></Text>
</View>
)}
</View>
<View className='p-2'>
<Text className='text-sm text-gray-700 truncate block'>{item.goodsName || item.name}</Text>
<View className='flex items-center mt-1'>
<Price
price={item.price || '0'}
original={item.salePrice && item.salePrice !== item.price ? item.salePrice : undefined}
size='small'
loginMask
/>
</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