Files
xinlong-shop-taro/src_bak/pages/event/register/index.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

318 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { View, Text, Input, Picker } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { getShopEvent, registerEvent } from '@/api/shop/shopEvent'
import type { ShopEvent, EventFormField } from '@/api/shop/shopEvent'
definePageConfig({ navigationBarTitleText: '填写报名信息' })
/** 解析动态表单字段配置 */
function parseFormFields(event: ShopEvent): EventFormField[] {
// 1. 优先读取 formFields新架构
const raw = (event as any).formFields || (event as any).form_fields
if (raw) {
if (Array.isArray(raw)) return raw as EventFormField[]
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return parsed as EventFormField[]
} catch { /* ignore */ }
}
// 2. 兼容旧架构:根据 requireName/requirePhone/requireIdCard/requireSize 构建表单字段
const fields: EventFormField[] = []
let sort = 1
// 从 sizeOptions 解析尺码选项
const sizeOptions: string[] = []
if ((event as any).sizeOptions) {
const opts = String((event as any).sizeOptions).split(/[,]/).map(s => s.trim()).filter(Boolean)
sizeOptions.push(...opts)
}
// 姓名
if ((event as any).requireName === 1) {
fields.push({ key: 'name', label: '姓名', type: 'text', required: 1, placeholder: '请输入姓名', sort: sort++ })
}
// 手机号
if ((event as any).requirePhone === 1) {
fields.push({ key: 'phone', label: '手机号', type: 'phone', required: 1, placeholder: '请输入手机号', sort: sort++ })
}
// 身份证
if ((event as any).requireIdCard === 1) {
fields.push({ key: 'idCard', label: '身份证号', type: 'idcard', required: 1, placeholder: '请输入身份证号', sort: sort++ })
}
// 尺码
if ((event as any).requireSize === 1) {
fields.push({
key: 'size',
label: '服装尺码',
type: 'select',
required: 1,
placeholder: '请选择尺码',
options: sizeOptions.length > 0 ? sizeOptions : ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
sort: sort++,
})
}
return fields.sort((a, b) => a.sort - b.sort)
}
/** 根据字段类型返回校验规则 */
function getFieldValidator(field: EventFormField) {
return (value: string): string | null => {
if (field.required === 1 && !value.trim()) {
return `请填写${field.label}`
}
if (!value.trim()) return null
switch (field.type) {
case 'phone':
if (!/^1[3-9]\d{9}$/.test(value.trim())) {
return '手机号格式不正确'
}
break
case 'idcard':
if (!/^\d{17}[\dXx]$/.test(value.trim())) {
return '身份证号格式不正确'
}
break
case 'number':
if (!/^\d+$/.test(value.trim())) {
return `${field.label}必须为数字`
}
break
}
return null
}
}
const EventRegisterPage: React.FC = () => {
const { id } = Taro.getCurrentInstance().router?.params || {}
const [event, setEvent] = useState<ShopEvent | null>(null)
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
// 动态表单数据key -> value
const [formValues, setFormValues] = useState<Record<string, string>>({})
useEffect(() => {
if (id) fetchEvent(Number(id))
}, [id])
const fetchEvent = async (eventId: number) => {
try {
const data = await getShopEvent(eventId)
setEvent(data)
} catch (err: any) {
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
const handleFieldChange = (key: string, value: string) => {
setFormValues(prev => ({ ...prev, [key]: value }))
}
const validate = (): boolean => {
if (!event) return false
const fields = parseFormFields(event)
for (const field of fields) {
const validator = getFieldValidator(field)
const error = validator(formValues[field.key] || '')
if (error) {
Taro.showToast({ title: error, icon: 'none' })
return false
}
}
return true
}
/** 将英文 key 映射为中文 key与后端兼容 */
const buildFormData = (): Record<string, string> => {
const map: Record<string, string> = {
name: '姓名',
phone: '手机号',
idCard: '身份证号',
size: '服装尺码',
}
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(formValues)) {
const cnKey = map[key] || key
result[cnKey] = value
// 同时保留英文 key确保后端兼容
result[key] = value
}
return result
}
const handleSubmit = async () => {
if (!event || !validate()) return
setSubmitting(true)
try {
const result = await registerEvent({
eventId: event.id,
formData: buildFormData(),
})
if (result.free) {
Taro.showToast({ title: '报名成功!', icon: 'success' })
setTimeout(() => Taro.redirectTo({ url: '/pages/event/my/index' }), 2000)
return
}
// 后端返回已支付(回调丢失自动修复场景)
if ((result as any).paid === 'true') {
Taro.showToast({ title: '报名成功!', icon: 'success' })
setTimeout(() => Taro.redirectTo({ url: '/pages/event/my/index' }), 2000)
return
}
// 收费赛事:发起微信支付
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType as any,
paySign: result.paySign,
})
Taro.showToast({ title: '报名成功!', icon: 'success' })
// 延迟3秒跳转给微信支付异步回调充足时间
setTimeout(() => Taro.redirectTo({ url: '/pages/event/my/index' }), 3000)
} catch (err: any) {
if (err?.errMsg?.includes('cancel')) {
Taro.showModal({
title: '支付已取消',
content: '您可以在"我的报名"中重新发起支付',
showCancel: false,
confirmText: '知道了',
})
} else {
Taro.showToast({ title: err.message || '报名失败', icon: 'none' })
}
} finally {
setSubmitting(false)
}
}
if (loading || !event) {
return (
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
<Text className='text-sm text-gray-400'>{loading ? '加载中...' : '赛事不存在'}</Text>
</View>
)
}
const fields = parseFormFields(event)
/** 渲染单个表单字段 */
const renderField = (field: EventFormField, index: number) => {
const value = formValues[field.key] || ''
const isLast = index === fields.length - 1
const requiredMark = field.required === 1 ? <Text className='text-red-500'>*</Text> : null
const placeholder = field.placeholder || `请输入${field.label}${field.required === 1 ? '' : '(选填)'}`
switch (field.type) {
case 'select':
case 'radio': {
const options = field.options || []
return (
<View key={field.key} className={`bg-white rounded-xl p-4 mb-4`}>
<Text className='text-sm text-gray-600 mb-3 block'>
{field.label} {requiredMark}
</Text>
<View className='flex flex-wrap gap-2'>
{options.map(opt => (
<View
key={opt}
className={`px-4 py-2 rounded-lg border text-sm ${
value === opt
? 'bg-orange-500 border-orange-500 text-white'
: 'bg-white border-gray-200 text-gray-700'
}`}
onClick={() => handleFieldChange(field.key, opt)}
>
<Text>{opt}</Text>
</View>
))}
</View>
</View>
)
}
default: {
// text / number / idcard / phone
const inputType = field.type === 'number' ? 'number' : 'text'
const maxLength = field.type === 'phone' ? 11 : field.type === 'idcard' ? 18 : undefined
return (
<View
key={field.key}
className={`flex items-center px-4 py-3 ${!isLast ? 'border-b border-gray-100' : ''}`}
>
<Text className='text-sm text-gray-600 w-24'>
{field.label} {requiredMark}
</Text>
<Input
className='flex-1 text-sm text-gray-800'
placeholder={placeholder}
type={inputType}
maxlength={maxLength}
value={value}
onInput={e => handleFieldChange(field.key, e.detail.value)}
/>
</View>
)
}
}
}
return (
<View className='min-h-screen bg-gray-50 flex flex-col'>
<View className='flex-1 p-4'>
{/* 赛事信息卡 */}
<View className='bg-orange-50 rounded-xl p-4 mb-4'>
<Text className='text-base font-bold text-gray-800 block mb-1'>{event.name}</Text>
<Text className='text-sm text-orange-500'>
{event.entryFee > 0 ? `报名费:¥${event.entryFee}` : '免费报名'}
</Text>
</View>
{/* 动态表单 */}
{fields.length > 0 ? (
<View className='bg-white rounded-xl overflow-hidden mb-4'>
{fields.map((field, idx) => renderField(field, idx))}
</View>
) : (
<View className='bg-white rounded-xl p-4 mb-4'>
<Text className='text-sm text-gray-400 text-center'></Text>
</View>
)}
{/* 提示 */}
{event.entryFee > 0 && (
<View className='bg-yellow-50 rounded-xl p-3 mb-4'>
<Text className='text-xs text-yellow-700'>
💡 "我的报名"
</Text>
</View>
)}
</View>
{/* 提交按钮 */}
<View className='bg-white px-4 py-3 shadow-lg' style={{ paddingBottom: '20px' }}>
<View
className='bg-orange-500 text-center py-3 rounded-full'
style={{ opacity: submitting ? 0.6 : 1 }}
onClick={submitting ? undefined : handleSubmit}
>
<Text className='text-sm text-white font-bold'>
{submitting ? '提交中...' : event.entryFee > 0 ? `确认报名并支付 ¥${event.entryFee}` : '确认报名'}
</Text>
</View>
</View>
</View>
)
}
export default EventRegisterPage