This commit is contained in:
@@ -185,6 +185,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-section {
|
||||
.schedule-box {
|
||||
background: #111;
|
||||
padding: 30px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 240, 255, 0.2);
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
font-size: 28px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #888;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #00f0ff;
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
|
||||
@@ -56,6 +56,17 @@ export default function CourseDetail() {
|
||||
if (loading) return <View className='page-container'><Text style={{color:'#fff'}}>Loading...</Text></View>
|
||||
if (!detail) return <View className='page-container'><Text style={{color:'#fff'}}>Not Found</Text></View>
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
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 `${year}/${month}/${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<ScrollView scrollY className='scroll-content'>
|
||||
@@ -109,6 +120,27 @@ export default function CourseDetail() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 开课时间 */}
|
||||
{detail.is_fixed_schedule && (detail.start_time || detail.end_time) && (
|
||||
<View className='section schedule-section'>
|
||||
<Text className='section-title'>开课时间</Text>
|
||||
<View className='schedule-box'>
|
||||
{detail.start_time && (
|
||||
<View className='time-row'>
|
||||
<Text className='label'>开始时间:</Text>
|
||||
<Text className='value'>{formatDateTime(detail.start_time)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{detail.end_time && (
|
||||
<View className='time-row'>
|
||||
<Text className='label'>结束时间:</Text>
|
||||
<Text className='value'>{formatDateTime(detail.end_time)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 课程简介 */}
|
||||
<View className='section'>
|
||||
<Text className='section-title'>课程简介</Text>
|
||||
|
||||
@@ -92,9 +92,31 @@ export default function Checkout() {
|
||||
}, [items])
|
||||
|
||||
const submitOrder = async () => {
|
||||
if (!address) {
|
||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||
return
|
||||
// 免费课程不需要地址
|
||||
const isFreeCourse = params.type === 'course' && items.length > 0 && Number(items[0].price) === 0
|
||||
|
||||
if (!address && !isFreeCourse) {
|
||||
// 尝试调用 chooseAddress
|
||||
try {
|
||||
await chooseAddress()
|
||||
if (!address) {
|
||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是免费课程且没有地址,使用默认值
|
||||
const orderAddress = address || {
|
||||
userName: '免费课程学员',
|
||||
telNumber: '13800000000',
|
||||
provinceName: '',
|
||||
cityName: '',
|
||||
countyName: '',
|
||||
detailInfo: '线上课程'
|
||||
}
|
||||
|
||||
Taro.showLoading({ title: '提交中...' })
|
||||
@@ -102,11 +124,13 @@ export default function Checkout() {
|
||||
try {
|
||||
const orderPromises = items.map(item => {
|
||||
const type = params.type || 'config'
|
||||
|
||||
// 构造订单数据
|
||||
const orderData: any = {
|
||||
quantity: item.quantity,
|
||||
customer_name: address.userName,
|
||||
phone_number: address.telNumber,
|
||||
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`,
|
||||
customer_name: orderAddress.userName,
|
||||
phone_number: orderAddress.telNumber,
|
||||
shipping_address: `${orderAddress.provinceName}${orderAddress.cityName}${orderAddress.countyName}${orderAddress.detailInfo}`,
|
||||
ref_code: Taro.getStorageSync('ref_code') || ''
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,19 @@ export default function Payment() {
|
||||
|
||||
const handlePay = async () => {
|
||||
if (!order) return
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// 如果是免费订单,直接显示成功并跳转
|
||||
if (parseFloat(order.total_price) <= 0) {
|
||||
Taro.showToast({ title: '报名成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({ url: '/pages/order/list' })
|
||||
}, 1500)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const params = await prepayMiniprogram(order.id)
|
||||
|
||||
@@ -81,7 +93,9 @@ export default function Payment() {
|
||||
</View>
|
||||
|
||||
<View className='btn-area safe-area-bottom'>
|
||||
<Button className='btn-pay' onClick={handlePay} loading={loading}>微信支付</Button>
|
||||
<Button className='btn-pay' onClick={handlePay} loading={loading}>
|
||||
{parseFloat(order.total_price) <= 0 ? '确认报名' : '微信支付'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -373,3 +373,49 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Signup Form Styles */
|
||||
.signup-form {
|
||||
.form-field-wrapper {
|
||||
|
||||
&.custom-field {
|
||||
padding: 12px 24px;
|
||||
position: relative;
|
||||
background-color: #fff; /* Ensure white background inside modal */
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 24px;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.required {
|
||||
color: #ff4949;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.at-radio::before, .at-radio::after,
|
||||
.at-checkbox::before, .at-checkbox::after,
|
||||
.at-textarea::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.at-textarea {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
|
||||
import { View, Text, Image, Button, RichText } from '@tarojs/components'
|
||||
import { AtIcon, AtProgress, AtModal, AtModalHeader, AtModalContent, AtModalAction, AtInput } from 'taro-ui'
|
||||
import { View, Text, Image, Button, RichText, Picker } from '@tarojs/components'
|
||||
import { AtIcon, AtProgress, AtModal, AtModalHeader, AtModalContent, AtModalAction, AtInput, AtTextarea, AtRadio, AtCheckbox } from 'taro-ui'
|
||||
import { getActivityDetail, signupActivity } from '../../../api'
|
||||
import { marked } from 'marked'
|
||||
import './detail.scss'
|
||||
@@ -79,6 +79,12 @@ const ActivityDetail = () => {
|
||||
setShowSignupModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already unpaid (resume payment)
|
||||
if (activity.my_signup_status === 'unpaid' && activity.my_order_id) {
|
||||
Taro.navigateTo({ url: `/pages/order/payment?id=${activity.my_order_id}` })
|
||||
return
|
||||
}
|
||||
|
||||
// Direct signup if no config
|
||||
submitSignup({})
|
||||
@@ -87,7 +93,18 @@ const ActivityDetail = () => {
|
||||
const submitSignup = async (data: any) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await signupActivity(Number(id), { signup_info: data })
|
||||
const res = await signupActivity(Number(id), { signup_info: data })
|
||||
|
||||
// Handle payment if order_id is returned
|
||||
if (res.order_id) {
|
||||
Taro.showToast({ title: '即将跳转支付', icon: 'none' })
|
||||
setShowSignupModal(false)
|
||||
setTimeout(() => {
|
||||
Taro.navigateTo({ url: `/pages/order/payment?id=${res.order_id}` })
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '报名成功', icon: 'success' })
|
||||
setShowSignupModal(false)
|
||||
fetchDetail() // Refresh status
|
||||
@@ -224,7 +241,9 @@ const ActivityDetail = () => {
|
||||
{/* Footer Action Bar */}
|
||||
<View className='footer-action-bar'>
|
||||
<View className='left-info'>
|
||||
<Text className='price'>免费</Text>
|
||||
<Text className='price'>
|
||||
{Number(activity.price) > 0 ? `¥${activity.price}` : '免费'}
|
||||
</Text>
|
||||
<Text className='desc'>限时活动</Text>
|
||||
</View>
|
||||
<Button
|
||||
@@ -251,18 +270,100 @@ const ActivityDetail = () => {
|
||||
<AtModalHeader>填写报名信息</AtModalHeader>
|
||||
<AtModalContent>
|
||||
<View className='signup-form'>
|
||||
{activity.signup_form_config && activity.signup_form_config.map((field, idx) => (
|
||||
<AtInput
|
||||
key={idx}
|
||||
name={field.name}
|
||||
title={field.label}
|
||||
type={field.type || 'text'}
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={formData[field.name]}
|
||||
onChange={(val) => handleFormChange(field.name, val)}
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
{activity.signup_form_config && activity.signup_form_config.map((field, idx) => {
|
||||
if (field.type === 'select') {
|
||||
const currentOption = field.options?.find(opt => opt.value === formData[field.name])
|
||||
return (
|
||||
<View key={idx} className='form-field-wrapper'>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={field.options || []}
|
||||
rangeKey='label'
|
||||
onChange={(e) => {
|
||||
const index = e.detail.value
|
||||
const selected = field.options?.[index]
|
||||
if (selected) {
|
||||
handleFormChange(field.name, selected.value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AtInput
|
||||
name={field.name}
|
||||
title={field.label}
|
||||
type='text'
|
||||
placeholder={field.placeholder || `请选择${field.label}`}
|
||||
value={currentOption ? currentOption.label : ''}
|
||||
editable={false}
|
||||
required={field.required}
|
||||
/>
|
||||
</Picker>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'radio') {
|
||||
return (
|
||||
<View key={idx} className='form-field-wrapper custom-field'>
|
||||
<View className='field-label'>
|
||||
{field.required && <Text className='required'>*</Text>}
|
||||
{field.label}
|
||||
</View>
|
||||
<AtRadio
|
||||
options={field.options || []}
|
||||
value={formData[field.name]}
|
||||
onClick={(val) => handleFormChange(field.name, val)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
return (
|
||||
<View key={idx} className='form-field-wrapper custom-field'>
|
||||
<View className='field-label'>
|
||||
{field.required && <Text className='required'>*</Text>}
|
||||
{field.label}
|
||||
</View>
|
||||
<AtCheckbox
|
||||
options={field.options || []}
|
||||
selectedList={formData[field.name] || []}
|
||||
onChange={(val) => handleFormChange(field.name, val)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'textarea') {
|
||||
return (
|
||||
<View key={idx} className='form-field-wrapper custom-field'>
|
||||
<View className='field-label'>
|
||||
{field.required && <Text className='required'>*</Text>}
|
||||
{field.label}
|
||||
</View>
|
||||
<AtTextarea
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(val) => handleFormChange(field.name, val)}
|
||||
placeholder={field.placeholder || `请输入${field.label}`}
|
||||
maxLength={500}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={idx} className='form-field-wrapper'>
|
||||
<AtInput
|
||||
name={field.name}
|
||||
title={field.label}
|
||||
type={field.type === 'tel' ? 'phone' : (field.type === 'number' ? 'number' : 'text')}
|
||||
placeholder={field.placeholder || `请输入${field.label}`}
|
||||
value={formData[field.name]}
|
||||
onChange={(val) => handleFormChange(field.name, val)}
|
||||
required={field.required}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</AtModalContent>
|
||||
<AtModalAction>
|
||||
|
||||
Reference in New Issue
Block a user