From 68a655877686be8e9630d6b41c130b1b4cd14e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E9=87=8E?= Date: Sun, 10 Aug 2025 23:02:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.config.ts | 1 + src/components/DynamicForm/DynamicForm.scss | 102 +++++ src/components/DynamicForm/DynamicForm.tsx | 326 ++++++++++++++ src/components/DynamicForm/FieldRenderer.scss | 304 +++++++++++++ src/components/DynamicForm/FieldRenderer.tsx | 279 ++++++++++++ src/components/DynamicForm/index.ts | 9 + src/config/formSchema/bulishBallFormSchema.ts | 173 ++++++++ src/pages/publishBall/index.config.ts | 3 + src/pages/publishBall/index.scss | 402 ++++++++++++++++++ src/pages/publishBall/index.tsx | 62 +++ 10 files changed, 1661 insertions(+) create mode 100644 src/components/DynamicForm/DynamicForm.scss create mode 100644 src/components/DynamicForm/DynamicForm.tsx create mode 100644 src/components/DynamicForm/FieldRenderer.scss create mode 100644 src/components/DynamicForm/FieldRenderer.tsx create mode 100644 src/components/DynamicForm/index.ts create mode 100644 src/config/formSchema/bulishBallFormSchema.ts create mode 100644 src/pages/publishBall/index.config.ts create mode 100644 src/pages/publishBall/index.scss create mode 100644 src/pages/publishBall/index.tsx diff --git a/src/app.config.ts b/src/app.config.ts index 15c683b..60330e6 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,5 +1,6 @@ export default defineAppConfig({ pages: [ + 'pages/publishBall/index', 'pages/index/index' ], window: { diff --git a/src/components/DynamicForm/DynamicForm.scss b/src/components/DynamicForm/DynamicForm.scss new file mode 100644 index 0000000..adac9f0 --- /dev/null +++ b/src/components/DynamicForm/DynamicForm.scss @@ -0,0 +1,102 @@ +.dynamic-form { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 200px; + + .form-reminder { + background: #fff8dc; + padding: 16px; + margin: 16px; + border-radius: 8px; + font-size: 14px; + color: #666; + line-height: 1.4; + } + + .form-container { + background: #fff; + margin: 16px; + border-radius: 12px; + padding: 20px; + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + .delete-form-btn { + position: absolute; + top: 15px; + right: 15px; + width: 28px; + height: 28px; + background: #ff4757; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; + font-weight: bold; + z-index: 10; + } + } + + .form-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + padding: 20px 16px 30px; + border-top: 1px solid #e0e0e0; + + .add-form-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 44px; + border: 1px solid #ddd; + border-radius: 8px; + background: #fff; + margin-bottom: 12px; + font-size: 14px; + color: #666; + + .plus-icon { + font-size: 20px; + color: #999; + } + } + + .submit-btn { + width: 100%; + height: 48px; + background: #000; + color: #fff; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + transition: all 0.3s ease; + + &.submitting { + background: #999; + opacity: 0.8; + } + + &:disabled { + background: #ccc; + color: #999; + cursor: not-allowed; + } + } + + .disclaimer { + font-size: 12px; + color: #999; + text-align: center; + line-height: 1.4; + display: block; + } + } +} \ No newline at end of file diff --git a/src/components/DynamicForm/DynamicForm.tsx b/src/components/DynamicForm/DynamicForm.tsx new file mode 100644 index 0000000..85a8b35 --- /dev/null +++ b/src/components/DynamicForm/DynamicForm.tsx @@ -0,0 +1,326 @@ +import React, { useState, useEffect } from 'react' +import { View, Text, Button } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { Dialog } from '@nutui/nutui-react-taro' +import { FormConfig, FormFieldConfig } from '../../config/formConfig' +import FieldRenderer from './FieldRenderer' +import commonApi from '../../services/commonApi' +import './DynamicForm.scss' + +interface DynamicFormProps { + config: FormConfig + formType?: string // 表单类型,用于API提交 + onSubmit?: (formData: any[]) => void | Promise + onSubmitSuccess?: (response: any) => void + onSubmitError?: (error: any) => void + onAddForm?: () => void + onDeleteForm?: (index: number) => void + enableApiSubmit?: boolean // 是否启用API提交 +} + +interface FormData { + id: string + data: Record +} + +const DynamicForm: React.FC = ({ + config, + formType = 'default', + onSubmit, + onSubmitSuccess, + onSubmitError, + onAddForm, + onDeleteForm, + enableApiSubmit = true +}) => { + const [forms, setForms] = useState([]) + const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [deleteIndex, setDeleteIndex] = useState(-1) + const [isSubmitting, setIsSubmitting] = useState(false) + + // 初始化表单数据 + useEffect(() => { + const initialData = createInitialFormData() + setForms([{ + id: Date.now().toString(), + data: initialData + }]) + }, [config]) + + // 根据配置创建初始数据 + const createInitialFormData = () => { + const data: Record = {} + config.fields.forEach(field => { + data[field.key] = field.defaultValue + }) + return data + } + + // 添加新表单 + const handleAddForm = () => { + const newForm: FormData = { + id: Date.now().toString(), + data: createInitialFormData() + } + setForms([...forms, newForm]) + onAddForm?.() + } + + // 删除表单 + const handleDeleteForm = (index: number) => { + if (forms.length <= 1) return + + setDeleteIndex(index) + setShowDeleteDialog(true) + } + + const confirmDelete = () => { + if (deleteIndex >= 0 && forms.length > 1) { + const newForms = forms.filter((_, i) => i !== deleteIndex) + setForms(newForms) + onDeleteForm?.(deleteIndex) + } + setShowDeleteDialog(false) + setDeleteIndex(-1) + } + + // 更新字段值 + const updateFieldValue = (formIndex: number, fieldKey: string, value: any) => { + const newForms = [...forms] + newForms[formIndex].data[fieldKey] = value + setForms(newForms) + } + + // 表单验证 + const validateForm = (formData: Record) => { + const errors: string[] = [] + + config.fields.forEach(field => { + if (field.required) { + const value = formData[field.key] + if (value === undefined || value === null || value === '' || + (Array.isArray(value) && value.length === 0)) { + errors.push(`${field.title}为必填项`) + } + } + + // 数字验证 + if (field.validation && formData[field.key] !== '') { + const value = Number(formData[field.key]) + if (field.validation.min !== undefined && value < field.validation.min) { + errors.push(field.validation.message || `${field.title}不能小于${field.validation.min}`) + } + if (field.validation.max !== undefined && value > field.validation.max) { + errors.push(field.validation.message || `${field.title}不能大于${field.validation.max}`) + } + } + }) + + return errors + } + + // 处理图片上传 + const handleImageUpload = async (formData: Record): Promise> => { + const processedData = { ...formData } + + for (const [key, value] of Object.entries(formData)) { + // 检查是否为图片字段 + const fieldConfig = config.fields.find(field => field.key === key) + if (fieldConfig?.type === 'image-upload' && Array.isArray(value) && value.length > 0) { + try { + // 过滤出本地图片路径(临时文件) + const localImages = value.filter(url => url.startsWith('wxfile://') || url.startsWith('http://tmp/')) + const remoteImages = value.filter(url => !url.startsWith('wxfile://') && !url.startsWith('http://tmp/')) + + if (localImages.length > 0) { + // 上传本地图片 + const uploadResult = await commonApi.uploadImages(localImages) + const uploadedUrls = uploadResult.data.map(item => item.url) + + // 合并远程图片和新上传的图片 + processedData[key] = [...remoteImages, ...uploadedUrls] + } + } catch (error) { + console.error('图片上传失败:', error) + throw new Error(`${fieldConfig.title}上传失败`) + } + } + } + + return processedData + } + + // 提交表单 + const handleSubmit = async () => { + if (isSubmitting) return + + setIsSubmitting(true) + + try { + // 表单验证 + const allErrors: string[] = [] + const formDataList = forms.map((form, index) => { + const errors = validateForm(form.data) + if (errors.length > 0) { + allErrors.push(`第${index + 1}${config.title || '表单'}: ${errors.join(', ')}`) + } + return form.data + }) + + if (allErrors.length > 0) { + Taro.showModal({ + title: '表单验证失败', + content: allErrors.join('\n'), + showCancel: false + }) + return + } + + // 处理图片上传 + const processedFormDataList = await Promise.all( + formDataList.map(formData => handleImageUpload(formData)) + ) + + // 执行自定义提交逻辑 + if (onSubmit) { + await onSubmit(processedFormDataList) + } + + // API提交 + if (enableApiSubmit) { + try { + let response + + // 根据表单类型选择不同的API + switch (formType) { + case 'publishBall': + if (processedFormDataList.length === 1) { + response = await commonApi.publishMatch(processedFormDataList[0] as any) + } else { + response = await commonApi.publishMultipleMatches(processedFormDataList as any) + } + break + case 'userProfile': + response = await commonApi.updateUserProfile(processedFormDataList[0] as any) + break + case 'feedback': + response = await commonApi.submitFeedback(processedFormDataList[0] as any) + break + default: + response = await commonApi.submitForm(formType, processedFormDataList) + } + + // 提交成功回调 + onSubmitSuccess?.(response) + + Taro.showToast({ + title: '提交成功!', + icon: 'success' + }) + } catch (apiError) { + console.error('API提交失败:', apiError) + onSubmitError?.(apiError) + throw apiError + } + } else { + // 不使用API提交时,显示成功提示 + Taro.showToast({ + title: '提交成功!', + icon: 'success' + }) + } + } catch (error) { + console.error('表单提交失败:', error) + + // 错误回调 + onSubmitError?.(error) + + const errorMessage = error instanceof Error ? error.message : '提交失败,请重试' + Taro.showToast({ + title: errorMessage, + icon: 'none', + duration: 2000 + }) + } finally { + setIsSubmitting(false) + } + } + + return ( + + {/* 提醒文本 */} + {config.reminder && ( + + {config.reminder} + + )} + + {/* 表单列表 */} + {forms.map((form, formIndex) => ( + + {/* 删除按钮 */} + {forms.length > 1 && ( + handleDeleteForm(formIndex)} + > + × + + )} + + {/* 渲染所有字段 */} + {config.fields.map((fieldConfig: FormFieldConfig) => ( + updateFieldValue(formIndex, fieldConfig.key, value)} + matchId={form.id} + /> + ))} + + ))} + + {/* 底部操作区 */} + + {config.actions?.addText && ( + + + + {config.actions.addText} + + )} + + {config.actions?.submitText && ( + + )} + + {config.actions?.disclaimer && ( + + {config.actions.disclaimer} + + )} + + + {/* 删除确认弹窗 */} + { + setShowDeleteDialog(false) + setDeleteIndex(-1) + }} + > + 确定要删除这场约球吗? + + + ) +} + +export default DynamicForm \ No newline at end of file diff --git a/src/components/DynamicForm/FieldRenderer.scss b/src/components/DynamicForm/FieldRenderer.scss new file mode 100644 index 0000000..ce2691d --- /dev/null +++ b/src/components/DynamicForm/FieldRenderer.scss @@ -0,0 +1,304 @@ +.field-renderer { + margin-bottom: 24px; + + .field-header { + margin-bottom: 12px; + + .field-title { + font-size: 16px; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 4px; + } + + .field-hint { + font-size: 12px; + color: #999; + display: block; + } + } + + .field-content { + // 图片上传 + .upload-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .upload-btn { + width: 80px; + height: 80px; + border: 2px dashed #ddd; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: #fafafa; + + .plus-icon { + font-size: 24px; + color: #999; + } + } + + .image-item { + position: relative; + width: 80px; + height: 80px; + + .cover-image { + width: 100%; + height: 100%; + border-radius: 8px; + } + + .remove-btn { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + background: #ff4757; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + } + } + } + + // 文本输入 + .text-input, .number-input { + width: 100%; + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0 16px; + font-size: 14px; + background: #fff; + } + + // 时间显示 + .time-display { + background: #f8f8f8; + border-radius: 8px; + padding: 16px; + + .time-row { + display: flex; + align-items: center; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + .time-label { + width: 40px; + font-size: 14px; + color: #666; + } + + .time-value { + flex: 1; + font-size: 14px; + color: #333; + margin-left: 16px; + + &:last-child { + text-align: right; + } + } + } + } + + // 场地输入 + .venue-input-container { + display: flex; + align-items: center; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding-right: 16px; + + .venue-input { + flex: 1; + height: 44px; + padding: 0 16px; + font-size: 14px; + border: none; + } + + .arrow { + font-size: 18px; + color: #999; + } + } + + // 多选按钮 + .multi-select-container { + display: flex; + flex-wrap: wrap; + gap: 12px; + + .select-btn { + padding: 8px 20px; + border: 1px solid #e0e0e0; + border-radius: 20px; + background: #fff; + font-size: 14px; + color: #666; + + &.selected { + background: #000; + color: #fff; + border-color: #000; + } + } + } + + // 计数器 + .counter-container { + display: flex; + align-items: center; + justify-content: space-between; + + .count-group { + display: flex; + align-items: center; + gap: 8px; + + .count-label { + font-size: 14px; + color: #666; + } + + .count-controls { + display: flex; + align-items: center; + gap: 8px; + + .count-btn { + width: 32px; + height: 32px; + border: 1px solid #ddd; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + font-size: 18px; + color: #333; + } + + .count-number { + font-size: 16px; + color: #333; + min-width: 24px; + text-align: center; + } + } + } + + .separator { + font-size: 14px; + color: #666; + } + } + + // 滑动条 + .slider-container { + display: flex; + align-items: center; + gap: 12px; + + .slider-label { + font-size: 12px; + color: #666; + white-space: nowrap; + } + + .slider-wrapper { + flex: 1; + position: relative; + + .slider-track { + height: 4px; + background: #e0e0e0; + border-radius: 2px; + position: relative; + + .slider-fill { + position: absolute; + height: 100%; + background: #333; + border-radius: 2px; + left: 20%; + right: 20%; + } + + .slider-thumb { + position: absolute; + width: 20px; + height: 20px; + background: #333; + border-radius: 50%; + top: -8px; + transform: translateX(-50%); + } + } + } + } + + // 单选按钮组 + .radio-group-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .radio-btn { + padding: 8px 16px; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #fff; + font-size: 14px; + color: #666; + + &.selected { + background: #000; + color: #fff; + border-color: #000; + } + } + } + + // 复选框 + .checkbox-container { + display: flex; + justify-content: flex-end; + + .checkbox { + width: 20px; + height: 20px; + border: 1px solid #ddd; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + + &.checked { + background: #000; + border-color: #000; + + .checkmark { + color: #fff; + font-size: 12px; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/components/DynamicForm/FieldRenderer.tsx b/src/components/DynamicForm/FieldRenderer.tsx new file mode 100644 index 0000000..fd0f82d --- /dev/null +++ b/src/components/DynamicForm/FieldRenderer.tsx @@ -0,0 +1,279 @@ +import React from 'react' +import { View, Text, Input, Image, Button } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { FormFieldConfig } from '../../config/formConfig' +import './FieldRenderer.scss' + +interface FieldRendererProps { + config: FormFieldConfig + value: any + onChange: (value: any) => void + matchId: string +} + +const FieldRenderer: React.FC = ({ config, value, onChange, matchId }) => { + const { key, type, title, placeholder, hint, options, config: fieldConfig } = config + + // 图片上传组件 + const renderImageUpload = () => { + const handleImageUpload = () => { + Taro.chooseImage({ + count: (fieldConfig?.maxImages || 9) - (value?.length || 0), + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const newImages = [...(value || []), ...res.tempFilePaths].slice(0, fieldConfig?.maxImages || 9) + onChange(newImages) + } + }) + } + + const removeImage = (index: number) => { + const newImages = (value || []).filter((_: any, i: number) => i !== index) + onChange(newImages) + } + + return ( + + + + + + {(value || []).map((url: string, i: number) => ( + + + removeImage(i)}> + × + + + ))} + + ) + } + + // 文本输入组件 + const renderTextInput = () => ( + onChange(e.detail.value)} + /> + ) + + // 数字输入组件 + const renderNumberInput = () => ( + onChange(e.detail.value)} + /> + ) + + // 时间显示组件 + const renderTimeDisplay = () => ( + + + 开始 + 11/23/2025 + 8:00 AM + + + 结束 + 11/23/2025 + 10:00 AM + + + ) + + // 场地输入组件 + const renderVenueInput = () => ( + + onChange(e.detail.value)} + /> + {fieldConfig?.showArrow && } + + ) + + // 多选按钮组件 + const renderMultiSelect = () => ( + + {options?.map((option) => ( + { + const currentValues = value || [] + const newValues = currentValues.includes(option.value) + ? currentValues.filter((v: any) => v !== option.value) + : [...currentValues, option.value] + onChange(newValues) + }} + > + {option.label} + + ))} + + ) + + // 计数器组件 + const renderCounter = () => { + const minValue = fieldConfig?.minValue || 1 + const maxValue = fieldConfig?.maxValue || 50 + const currentValue = value || { min: 1, max: 4 } + + return ( + + + 最少 + + onChange({ + ...currentValue, + min: Math.max(minValue, currentValue.min - 1) + })} + > + - + + {currentValue.min} + onChange({ + ...currentValue, + min: Math.min(maxValue, currentValue.min + 1) + })} + > + + + + + + + + + onChange({ + ...currentValue, + max: Math.max(currentValue.min, currentValue.max - 1) + })} + > + - + + {currentValue.max} + onChange({ + ...currentValue, + max: Math.min(maxValue, currentValue.max + 1) + })} + > + + + + + 最多 + + + ) + } + + // 滑动条组件 + const renderSlider = () => { + const range = fieldConfig?.range || [1, 7] + const currentValue = value || [2.0, 4.0] + + return ( + + 2.0及以下 + + + + + + + + 4.0及以上 + + ) + } + + // 单选组件 + const renderRadioGroup = () => ( + + {options?.map((option) => ( + onChange(option.value)} + > + {option.label} + + ))} + + ) + + // 复选框组件 + const renderCheckbox = () => ( + + onChange(!value)} + > + {value && } + + + ) + + // 根据类型渲染不同组件 + const renderField = () => { + switch (type) { + case 'image-upload': + return renderImageUpload() + case 'text-input': + return renderTextInput() + case 'number-input': + return renderNumberInput() + case 'time-display': + return renderTimeDisplay() + case 'venue-input': + return renderVenueInput() + case 'multi-select': + return renderMultiSelect() + case 'counter': + return renderCounter() + case 'slider': + return renderSlider() + case 'radio-group': + return renderRadioGroup() + case 'checkbox': + return renderCheckbox() + default: + return 未知字段类型: {type} + } + } + + return ( + + + {title} + {hint && {hint}} + + + {renderField()} + + + ) +} + +export default FieldRenderer \ No newline at end of file diff --git a/src/components/DynamicForm/index.ts b/src/components/DynamicForm/index.ts new file mode 100644 index 0000000..f4671c8 --- /dev/null +++ b/src/components/DynamicForm/index.ts @@ -0,0 +1,9 @@ +// DynamicForm组件统一导出 +export { default as DynamicForm } from './DynamicForm' +export { default as FieldRenderer } from './FieldRenderer' + +// 导出类型 +export type { FormConfig, FormFieldConfig, FieldType } from '../../config/formSchema/bulishBallFormSchema' + +// 默认导出DynamicForm组件 +export { default } from './DynamicForm' \ No newline at end of file diff --git a/src/config/formSchema/bulishBallFormSchema.ts b/src/config/formSchema/bulishBallFormSchema.ts new file mode 100644 index 0000000..66be8b7 --- /dev/null +++ b/src/config/formSchema/bulishBallFormSchema.ts @@ -0,0 +1,173 @@ +// 表单字段类型定义 +export type FieldType = + | 'image-upload' + | 'text-input' + | 'number-input' + | 'time-display' + | 'multi-select' + | 'counter' + | 'slider' + | 'radio-group' + | 'checkbox' + | 'venue-input' + +// 表单字段配置接口 +export interface FormFieldConfig { + key: string // 字段名 + type: FieldType // 字段类型 + title: string // 显示标题 + required?: boolean // 是否必填 + placeholder?: string // 占位符 + hint?: string // 提示文字 + defaultValue?: any // 默认值 + validation?: { // 验证规则 + min?: number + max?: number + pattern?: string + message?: string + } + options?: Array<{ // 选项(用于选择类型) + label: string + value: any + }> + config?: { // 特殊配置 + maxImages?: number // 图片上传最大数量 + minValue?: number // 计数器最小值 + maxValue?: number // 计数器最大值 + range?: [number, number] // 滑动条范围 + unit?: string // 单位 + showArrow?: boolean // 是否显示箭头 + } +} + +// 完整的表单配置 +export interface FormConfig { + title: string // 表单标题 + reminder?: string // 提醒文本 + fields: FormFieldConfig[] // 字段列表 + actions?: { // 底部操作 + addText?: string + submitText?: string + disclaimer?: string + } +} + +// 发布球的表单配置 +export const publishBallFormConfig: FormConfig = { + title: '发布个人约球', + reminder: '提醒: 活动开始前x小时未达到最低招募人数,活动自动取消;活动结束2天后,报名费自动转入[钱包—余额],可提现到微信。', + fields: [ + { + key: 'cover', + type: 'image-upload', + title: '活动封面', + required: false, + defaultValue: [], + config: { + maxImages: 9 + } + }, + { + key: 'theme', + type: 'text-input', + title: '活动主题 (选填)', + required: false, + placeholder: '好的主题更吸引人哦', + defaultValue: '' + }, + { + key: 'time', + type: 'time-display', + title: '活动时间', + required: true, + defaultValue: { + startTime: '2025-11-23 08:00', + endTime: '2025-11-23 10:00' + } + }, + { + key: 'venue', + type: 'venue-input', + title: '活动场地', + required: true, + placeholder: '选择活动地点', + defaultValue: '', + config: { + showArrow: true + } + }, + { + key: 'price', + type: 'number-input', + title: '人均价格/元', + required: true, + placeholder: '请填写每个人多少钱', + defaultValue: '', + validation: { + min: 0, + message: '价格不能为负数' + } + }, + { + key: 'playStyle', + type: 'multi-select', + title: '活动玩法', + required: true, + defaultValue: [], + options: [ + { label: '单打', value: '单打' }, + { label: '双打', value: '双打' }, + { label: '娱乐拉球', value: '娱乐拉球' }, + { label: '到了再说', value: '到了再说' } + ] + }, + { + key: 'playerCount', + type: 'counter', + title: '招募人数', + required: true, + defaultValue: { min: 1, max: 4 }, + config: { + minValue: 1, + maxValue: 50 + } + }, + { + key: 'ntrpRange', + type: 'slider', + title: 'NTRP水平区间', + required: true, + defaultValue: [2.0, 4.0], + config: { + range: [1.0, 7.0], + unit: '' + } + }, + { + key: 'genderPreference', + type: 'radio-group', + title: '补充要求 (选填)', + hint: '补充性别偏好、特殊要求和注意事项等信息', + required: false, + defaultValue: 'unlimited', + options: [ + { label: '选择填入', value: 'select' }, + { label: '仅限男生', value: 'male' }, + { label: '仅限女生', value: 'female' }, + { label: '性别不限', value: 'unlimited' } + ] + }, + { + key: 'autoStandby', + type: 'checkbox', + title: '开启自动候补逻辑', + required: false, + defaultValue: true + } + ], + actions: { + addText: '再添加一场', + submitText: '完成', + disclaimer: '点击确定发布按钮,即表示已阅读并同意《约球规则》' + } +} \ No newline at end of file diff --git a/src/pages/publishBall/index.config.ts b/src/pages/publishBall/index.config.ts new file mode 100644 index 0000000..b60b329 --- /dev/null +++ b/src/pages/publishBall/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '发布个人约球' +}) \ No newline at end of file diff --git a/src/pages/publishBall/index.scss b/src/pages/publishBall/index.scss new file mode 100644 index 0000000..825e47e --- /dev/null +++ b/src/pages/publishBall/index.scss @@ -0,0 +1,402 @@ +.publish-ball-page { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 200px; + + .reminder { + background: #fff8dc; + padding: 16px; + margin: 16px; + border-radius: 8px; + font-size: 14px; + color: #666; + line-height: 1.4; + } + + .match-form { + background: #fff; + margin: 16px; + border-radius: 12px; + padding: 20px; + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + .delete-btn { + position: absolute; + top: 15px; + right: 15px; + width: 28px; + height: 28px; + background: #ff4757; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; + font-weight: bold; + } + + .section { + margin-bottom: 24px; + + .section-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + display: block; + } + + // 封面上传 + .cover-upload { + .upload-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .upload-btn { + width: 80px; + height: 80px; + border: 2px dashed #ddd; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: #fafafa; + + .plus-icon { + font-size: 24px; + color: #999; + } + } + + .image-item { + position: relative; + width: 80px; + height: 80px; + + .cover-image { + width: 100%; + height: 100%; + border-radius: 8px; + } + + .remove-btn { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + background: #ff4757; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + } + } + } + } + + // 主题输入 + .theme-input { + width: 100%; + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0 16px; + font-size: 14px; + background: #fff; + } + + // 时间显示 + .time-display { + background: #f8f8f8; + border-radius: 8px; + padding: 16px; + + .time-row { + display: flex; + align-items: center; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + .time-label { + width: 40px; + font-size: 14px; + color: #666; + } + + .time-value { + flex: 1; + font-size: 14px; + color: #333; + margin-left: 16px; + + &:last-child { + text-align: right; + } + } + } + } + + // 场地输入 + .venue-input-container { + display: flex; + align-items: center; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding-right: 16px; + + .venue-input { + flex: 1; + height: 44px; + padding: 0 16px; + font-size: 14px; + border: none; + } + + .arrow { + font-size: 18px; + color: #999; + } + } + + // 价格输入 + .price-input { + width: 100%; + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0 16px; + font-size: 14px; + background: #fff; + } + + // 玩法选择 + .play-style-container { + display: flex; + flex-wrap: wrap; + gap: 12px; + + .play-style-btn { + padding: 8px 20px; + border: 1px solid #e0e0e0; + border-radius: 20px; + background: #fff; + font-size: 14px; + color: #666; + + &.selected { + background: #000; + color: #fff; + border-color: #000; + } + } + } + + // 人数控制 + .player-count-container { + display: flex; + align-items: center; + justify-content: space-between; + + .count-group { + display: flex; + align-items: center; + gap: 8px; + + .count-label { + font-size: 14px; + color: #666; + } + + .count-controls { + display: flex; + align-items: center; + gap: 8px; + + .count-btn { + width: 32px; + height: 32px; + border: 1px solid #ddd; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + font-size: 18px; + color: #333; + } + + .count-number { + font-size: 16px; + color: #333; + min-width: 24px; + text-align: center; + } + } + } + + .separator { + font-size: 14px; + color: #666; + } + } + + // NTRP滑动条 + .ntrp-container { + display: flex; + align-items: center; + gap: 12px; + + .ntrp-label { + font-size: 12px; + color: #666; + white-space: nowrap; + } + + .slider-container { + flex: 1; + position: relative; + + .slider-track { + height: 4px; + background: #e0e0e0; + border-radius: 2px; + position: relative; + + .slider-fill { + position: absolute; + height: 100%; + background: #333; + border-radius: 2px; + left: 20%; + right: 20%; + } + + .slider-thumb { + position: absolute; + width: 20px; + height: 20px; + background: #333; + border-radius: 50%; + top: -8px; + transform: translateX(-50%); + } + } + } + } + + // 性别选择 + .requirements-hint { + font-size: 12px; + color: #999; + margin-bottom: 12px; + } + + .gender-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .gender-btn { + padding: 8px 16px; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #fff; + font-size: 14px; + color: #666; + + &.selected { + background: #000; + color: #fff; + border-color: #000; + } + } + } + + // 自动候补 + .standby-container { + display: flex; + align-items: center; + justify-content: space-between; + + .checkbox { + width: 20px; + height: 20px; + border: 1px solid #ddd; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + + &.checked { + background: #000; + border-color: #000; + + .checkmark { + color: #fff; + font-size: 12px; + } + } + } + } + } + } + + // 底部操作区 + .bottom-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + padding: 20px 16px 30px; + border-top: 1px solid #e0e0e0; + + .add-match-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 44px; + border: 1px solid #ddd; + border-radius: 8px; + background: #fff; + margin-bottom: 12px; + font-size: 14px; + color: #666; + + .plus-icon { + font-size: 20px; + color: #999; + } + } + + .complete-btn { + width: 100%; + height: 48px; + background: #000; + color: #fff; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + } + + .disclaimer { + font-size: 12px; + color: #999; + text-align: center; + line-height: 1.4; + } + } +} \ No newline at end of file diff --git a/src/pages/publishBall/index.tsx b/src/pages/publishBall/index.tsx new file mode 100644 index 0000000..1ef99c2 --- /dev/null +++ b/src/pages/publishBall/index.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { View } from '@tarojs/components' +import Taro from '@tarojs/taro' +import DynamicForm from '../../components/DynamicForm/DynamicForm' +import { publishBallFormConfig } from '../../config/formSchema/bulishBallFormSchema' +import './index.scss' + +const PublishBallPage: React.FC = () => { + // 提交成功回调 + const handleSubmitSuccess = (response: any) => { + console.log('发布成功:', response) + + Taro.showModal({ + title: '发布成功', + content: response.data.length > 1 ? + `成功发布${response.data.length}场约球活动!` : + '约球活动发布成功!', + showCancel: false, + success: () => { + // 可以跳转到活动列表页面 + // Taro.navigateTo({ url: '/pages/matchList/matchList' }) + } + }) + } + + // 提交失败回调 + const handleSubmitError = (error: any) => { + console.error('发布失败:', error) + + Taro.showModal({ + title: '发布失败', + content: error.message || '网络错误,请稍后重试', + showCancel: false + }) + } + + // 添加表单 + const handleAddForm = () => { + console.log('添加新表单') + } + + // 删除表单 + const handleDeleteForm = (index: number) => { + console.log('删除表单:', index) + } + + return ( + + + + ) +} + +export default PublishBallPage \ No newline at end of file