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(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) // 动态表单数据:key -> value const [formValues, setFormValues] = useState>({}) 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 => { const map: Record = { name: '姓名', phone: '手机号', idCard: '身份证号', size: '服装尺码', } const result: Record = {} 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 ( {loading ? '加载中...' : '赛事不存在'} ) } 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 ? * : null const placeholder = field.placeholder || `请输入${field.label}${field.required === 1 ? '' : '(选填)'}` switch (field.type) { case 'select': case 'radio': { const options = field.options || [] return ( {field.label} {requiredMark} {options.map(opt => ( handleFieldChange(field.key, opt)} > {opt} ))} ) } default: { // text / number / idcard / phone const inputType = field.type === 'number' ? 'number' : 'text' const maxLength = field.type === 'phone' ? 11 : field.type === 'idcard' ? 18 : undefined return ( {field.label} {requiredMark} handleFieldChange(field.key, e.detail.value)} /> ) } } } return ( {/* 赛事信息卡 */} {event.name} {event.entryFee > 0 ? `报名费:¥${event.entryFee}` : '免费报名'} {/* 动态表单 */} {fields.length > 0 ? ( {fields.map((field, idx) => renderField(field, idx))} ) : ( 该赛事无需填写报名信息 )} {/* 提示 */} {event.entryFee > 0 && ( 💡 提交后将跳转微信支付,缴费成功后报名生效。若支付失败,可在"我的报名"中重新支付。 )} {/* 提交按钮 */} {submitting ? '提交中...' : event.entryFee > 0 ? `确认报名并支付 ¥${event.entryFee}` : '确认报名'} ) } export default EventRegisterPage