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,170 @@
import React, { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import { getMyRegistrations, payEventRegistration, getPayStatusLabel } from '@/api/shop/shopEvent'
import type { EventRegistration, EventFormField } from '@/api/shop/shopEvent'
definePageConfig({ navigationBarTitleText: '我的报名' })
const MyRegistrationsPage: React.FC = () => {
const [list, setList] = useState<EventRegistration[]>([])
const [loading, setLoading] = useState(true)
const [paying, setPaying] = useState<number | null>(null)
useDidShow(() => {
fetchList()
})
const fetchList = async () => {
try {
const data = await getMyRegistrations()
setList(data || [])
} catch (err: any) {
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
const handleRepay = async (reg: EventRegistration) => {
setPaying(reg.id)
try {
const wxParams = await payEventRegistration(reg.id)
// 后端返回已支付(回调丢失自动修复场景)
if ((wxParams as any).paid === 'true') {
Taro.showToast({ title: '支付成功', icon: 'success' })
setList(prev => prev.map(r => r.id === reg.id ? { ...r, payStatus: 1, paidAt: new Date().toISOString() } : r))
fetchList()
return
}
await Taro.requestPayment({
timeStamp: wxParams.timeStamp,
nonceStr: wxParams.nonceStr,
package: wxParams.package,
signType: wxParams.signType as any,
paySign: wxParams.paySign,
})
// 支付成功后立即更新本地状态,避免后端回调延迟导致仍显示"待缴费"
setList(prev => prev.map(r => r.id === reg.id ? { ...r, payStatus: 1, paidAt: new Date().toISOString() } : r))
Taro.showToast({ title: '支付成功', icon: 'success' })
fetchList()
} catch (err: any) {
if (!err?.errMsg?.includes('cancel')) {
Taro.showToast({ title: err.message || '支付失败', icon: 'none' })
}
} finally {
setPaying(null)
}
}
const formatDate = (dateStr: string) => {
const d = new Date(dateStr)
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
}
const fieldLabelMap: Record<string, string> = {
name: '姓名',
phone: '手机号',
idCard: '身份证号',
size: '服装尺码',
email: '邮箱',
gender: '性别',
age: '年龄',
address: '地址',
company: '单位/公司',
job: '职业',
remark: '备注',
emergencyContact: '紧急联系人',
emergencyPhone: '紧急联系人电话',
wechat: '微信号',
team: '团队/队伍',
}
if (loading) {
return (
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
<Text className='text-sm text-gray-400'>...</Text>
</View>
)
}
return (
<View className='min-h-screen bg-gray-50'>
<ScrollView scrollY className='flex-1'>
<View className='p-3'>
{list.length === 0 ? (
<View className='flex flex-col items-center justify-center py-20'>
<Text className='text-4xl mb-3'>🏆</Text>
<Text className='text-sm text-gray-400'></Text>
<View
className='mt-4 bg-orange-500 px-6 py-2 rounded-full'
onClick={() => Taro.navigateTo({ url: '/pages/event/list/index' })}
>
<Text className='text-sm text-white'></Text>
</View>
</View>
) : (
list.map(reg => {
// 后端支付回调可能延迟paidAt 有值即视为已缴费
const effectivePayStatus = reg.paidAt ? 1 : reg.payStatus
const payInfo = getPayStatusLabel(effectivePayStatus)
return (
<View key={reg.id} className='bg-white rounded-xl p-4 mb-3 shadow-sm'>
<View className='flex items-start justify-between mb-3'>
<Text className='text-base font-bold text-gray-800 flex-1 mr-2'>{reg.eventName || '赛事'}</Text>
<View className={`px-2 py-1 rounded ${effectivePayStatus === 1 ? 'bg-green-50' : effectivePayStatus === 0 ? 'bg-orange-50' : 'bg-gray-50'}`}>
<Text className={`text-xs ${payInfo.color}`}>{payInfo.label}</Text>
</View>
</View>
<View className='text-xs text-gray-500 space-y-1 mb-3'>
{/* 动态表单数据展示 */}
{(() => {
let formData: Record<string, string> = {}
if (reg.formData) {
if (typeof reg.formData === 'string') {
try { formData = JSON.parse(reg.formData) } catch { formData = {} }
} else {
formData = reg.formData as Record<string, string>
}
}
// 去重:过滤掉与英文 key 重复的中文 label key
const labelToKey: Record<string, string> = {}
Object.entries(fieldLabelMap).forEach(([k, v]) => { labelToKey[v] = k })
return Object.entries(formData).filter(([key]) => {
if (fieldLabelMap[key]) return true // 英文 key 保留
if (labelToKey[key]) return false // 中文 label key 跳过(会由英文 key 渲染)
return true // 自定义字段保留
}).map(([key, value]) => (
<Text key={key} className='block'>{fieldLabelMap[key] || key}{value}</Text>
))
})()}
<Text className='block'>
{reg.entryFee > 0 ? `¥${reg.entryFee}` : '免费'}
</Text>
<Text className='block'>{formatDate(reg.createTime)}</Text>
{reg.paidAt && <Text className='block'>{formatDate(reg.paidAt)}</Text>}
</View>
{effectivePayStatus === 0 && (
<View
className='bg-orange-500 text-center py-2 rounded-full'
style={{ opacity: paying === reg.id ? 0.6 : 1 }}
onClick={paying === reg.id ? undefined : () => handleRepay(reg)}
>
<Text className='text-sm text-white font-medium'>
{paying === reg.id ? '支付中...' : `重新支付 ¥${reg.entryFee}`}
</Text>
</View>
)}
</View>
)
})
)}
</View>
</ScrollView>
</View>
)
}
export default MyRegistrationsPage