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,3 @@
export default {
navigationBarTitleText: '活动列表',
}

View File

@@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { pageShopActivity, signUpActivity } from '@/api/shop/shopActivity'
import type { ShopActivity, ActivityStatus, ActivityType } from '@/api/shop/shopActivity'
import EmptyState from '@/components/common/EmptyState'
import LoadMore from '@/components/common/LoadMore'
definePageConfig({
navigationBarTitleText: '活动列表',
})
const ActivityListPage: React.FC = () => {
const [activeTab, setActiveTab] = useState(0)
const [activities, setActivities] = useState<ShopActivity[]>([])
const [loading, setLoading] = useState(false)
const [finished, setFinished] = useState(false)
const [page, setPage] = useState(1)
useEffect(() => {
setActivities([])
setPage(1)
setFinished(false)
loadList(1)
}, [activeTab])
const loadList = async (p: number) => {
if (loading) return
setLoading(true)
try {
// 根据 tab 筛选状态
let status: ActivityStatus | undefined
if (activeTab === 1) status = 'ongoing'
else if (activeTab === 2) status = 'upcoming'
else if (activeTab === 3) status = 'ended'
const res = await pageShopActivity({
page: p,
limit: 10,
status,
})
if (res?.list) {
if (p === 1) {
setActivities(res.list)
} else {
setActivities(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)
}
}
// 报名活动
const handleSignUp = async (id: number) => {
Taro.showModal({
title: '确认报名',
content: '确定报名参加该活动吗?',
confirmColor: '#0e932e',
success: async (res) => {
if (res.confirm) {
try {
await signUpActivity({ activityId: id, userId: 0 })
Taro.showToast({ title: '报名成功', icon: 'success' })
} catch (err) {
console.error('报名失败', err)
Taro.showToast({ title: '报名失败', icon: 'none' })
}
}
}
})
}
// 获取状态标签
const getStatusLabel = (status: ActivityStatus) => {
const map: Record<ActivityStatus, { label: string; color: string }> = {
'upcoming': { label: '未开始', color: 'text-blue-500' },
'ongoing': { label: '进行中', color: 'text-green-500' },
'ended': { label: '已结束', color: 'text-gray-400' },
'cancelled': { label: '已取消', color: 'text-gray-400' },
}
return map[status] || { label: '未知', color: 'text-gray-400' }
}
// 获取活动类型名称
const getTypeLabel = (type: ActivityType) => {
const map: Record<ActivityType, string> = {
'discount': '限时折扣',
'full_reduction': '满减活动',
'seckill': '秒杀活动',
'group_buy': '团购活动',
'flash_sale': '闪购活动',
}
return map[type] || '其他'
}
// 格式化时间
const formatTime = (timeStr: string) => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hour}:${minute}`
}
return (
<View className='min-h-screen bg-gray-50'>
{/* Tab 栏 */}
<View className='bg-white flex'>
{['全部', '进行中', '未开始', '已结束'].map((tab, index) => (
<View
key={index}
className={`flex-1 text-center py-3 relative ${
activeTab === index ? 'text-orange-500 font-medium' : 'text-gray-600'
}`}
onClick={() => setActiveTab(index)}
>
<Text className='text-sm'>{tab}</Text>
{activeTab === index && (
<View className='absolute bottom-0 w-8 h-px bg-orange-500 rounded' style={{ left: '50%', transform: 'translateX(-50%)' }} />
)}
</View>
))}
</View>
{/* 活动列表 */}
<ScrollView
scrollY
className='flex-1'
onScrollToLower={handleLoadMore}
lowerThreshold={100}
>
<View className='p-3'>
{activities.length === 0 && !loading ? (
<EmptyState text='暂无活动' />
) : (
activities.map(activity => (
(() => {
const statusInfo = getStatusLabel(activity.status)
return (
<View key={activity.id} className='bg-white rounded-xl p-4 mb-3 shadow-sm' onClick={() => Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })}>
{/* 活动图片 */}
<View className='w-full rounded-lg flex items-center justify-center mb-3' style={{ height: '160px', background: 'linear-gradient(to right, #fb923c, #ef4444)' }}>
<Text className='text-5xl'>🎉</Text>
</View>
{/* 活动信息 */}
<View className='mb-2'>
<View className='flex items-center gap-2 mb-1'>
<Text className='text-base font-bold text-gray-800 flex-1'>{activity.name}</Text>
<View className={`px-2 py-1 rounded ${activity.status === 'ongoing' ? 'bg-green-50' : activity.status === 'upcoming' ? 'bg-blue-50' : 'bg-gray-50'}`}>
<Text className={`text-xs ${statusInfo.color}`}>{statusInfo.label}</Text>
</View>
</View>
<Text className='text-sm text-gray-600 mb-2 block'>{activity.description}</Text>
<View className='flex items-center gap-3 text-xs text-gray-400'>
<View className='flex items-center gap-1'>
<Text>📅</Text>
<Text>{formatTime(activity.startTime)}</Text>
</View>
<View className='flex items-center gap-1'>
<Text>👥</Text>
<Text>{activity.participants}</Text>
</View>
</View>
</View>
{/* 活动类型标签 */}
<View className='flex items-center justify-between'>
<View className='bg-orange-100 px-2 py-1 rounded'>
<Text className='text-xs text-orange-500'>{getTypeLabel(activity.type)}</Text>
</View>
{activity.status === 'ongoing' && (
<View
className='bg-orange-500 px-4 py-1 rounded-full'
onClick={() => handleSignUp(activity.id)}
>
<Text className='text-xs text-white'></Text>
</View>
)}
{activity.status === 'upcoming' && (
<View
className='bg-blue-500 px-4 py-1 rounded-full'
onClick={() => Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })}
>
<Text className='text-xs text-white'></Text>
</View>
)}
</View>
</View>
)
})()
))
)}
<LoadMore loading={loading} finished={finished} />
</View>
</ScrollView>
</View>
)
}
export default ActivityListPage