diff --git a/backend/community/serializers.py b/backend/community/serializers.py index 2e6cb5c..ef3abfd 100644 --- a/backend/community/serializers.py +++ b/backend/community/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement -from shop.serializers import WeChatUserSerializer, ESP32ConfigSerializer, ServiceSerializer, VCCourseSerializer, OrderSerializer +from shop.serializers import WeChatUserSerializer, ESP32ConfigSerializer, ServiceSerializer, VCCourseSerializer from .utils import get_current_wechat_user class ActivitySerializer(serializers.ModelSerializer): @@ -47,11 +47,10 @@ class ActivitySerializer(serializers.ModelSerializer): class ActivitySignupSerializer(serializers.ModelSerializer): activity_info = serializers.SerializerMethodField() - order = OrderSerializer(read_only=True) class Meta: model = ActivitySignup - fields = ['id', 'activity', 'activity_info', 'user', 'signup_time', 'status', 'signup_info', 'order'] + fields = ['id', 'activity', 'activity_info', 'user', 'signup_time', 'status', 'signup_info'] read_only_fields = ['signup_time', 'status', 'user'] def get_activity_info(self, obj): diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 0e9d24a..7afbebb 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -207,7 +207,6 @@ class OrderSerializer(serializers.ModelSerializer): """ config_name = serializers.CharField(source='config.name', read_only=True) course_title = serializers.CharField(source='course.title', read_only=True) - activity_title = serializers.CharField(source='activity.title', read_only=True) config_image = serializers.SerializerMethodField() salesperson_name = serializers.CharField(source='salesperson.name', read_only=True) salesperson_code = serializers.CharField(source='salesperson.code', read_only=True) @@ -216,7 +215,7 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'activity', 'activity_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no', + fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no', 'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number'] read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at'] extra_kwargs = { diff --git a/frontend/src/pages/MyOrders.jsx b/frontend/src/pages/MyOrders.jsx index 8613255..90722a4 100644 --- a/frontend/src/pages/MyOrders.jsx +++ b/frontend/src/pages/MyOrders.jsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd'; import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons'; -import api, { getMySignups } from '../api'; +import { queryMyOrders, getMySignups } from '../api'; +import { motion } from 'framer-motion'; import LoginModal from '../components/LoginModal'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; @@ -40,6 +41,8 @@ const MyOrders = () => { const handleQueryData = async () => { setLoading(true); try { + const { default: api } = await import('../api'); + // Parallel fetch const [ordersRes, activitiesRes] = await Promise.allSettled([ api.get('/orders/'), @@ -341,7 +344,7 @@ const MyOrders = () => { onLoginSuccess={(userData) => { login(userData); if (userData.phone_number) { - handleQueryData(); + handleQueryOrders(userData.phone_number); } }} /> diff --git a/frontend/src/pages/activity/Detail.jsx b/frontend/src/pages/activity/Detail.jsx index 08959a1..119cb95 100644 --- a/frontend/src/pages/activity/Detail.jsx +++ b/frontend/src/pages/activity/Detail.jsx @@ -28,7 +28,7 @@ const ActivityDetail = () => { const [signupFormVisible, setSignupFormVisible] = useState(false); const [paymentModalVisible, setPaymentModalVisible] = useState(false); const [paymentInfo, setPaymentInfo] = useState(null); - const [isPaidSuccess, setIsPaidSuccess] = useState(false); + const [paymentStatus, setPaymentStatus] = useState('unpaid'); // 'unpaid' | 'paying' | 'paid' const [form] = Form.useForm(); // Header animation: transparent to white with shadow @@ -93,13 +93,17 @@ const ActivityDetail = () => { const signUpMutation = useMutation({ mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }), - onSuccess: (data) => { + onSuccess: (response) => { + // API returns axios response object, so we need to access .data + const data = response.data || response; // Fallback in case it was already unwrapped + console.log('Signup response:', data); + // 检查是否需要支付 if (data.payment_required) { setPaymentInfo(data); - setIsPaidSuccess(false); - // 先关闭报名表单,确保层级正确 - setSignupFormVisible(false); + setPaymentStatus('paying'); + + // 不关闭报名表单,直接打开支付弹窗 // 延迟一点点时间打开支付弹窗,避免状态更新冲突 setTimeout(() => { setPaymentModalVisible(true); @@ -110,6 +114,7 @@ const ActivityDetail = () => { message.success('报名成功!'); setSignupFormVisible(false); + setPaymentStatus('paid'); // In case it was free but we track it confetti({ particleCount: 150, spread: 70, @@ -127,13 +132,15 @@ const ActivityDetail = () => { // Polling for payment status useEffect(() => { let timer; - if (paymentModalVisible && paymentInfo?.order_id && !isPaidSuccess) { + if (paymentModalVisible && paymentInfo?.order_id) { timer = setInterval(async () => { try { const response = await queryOrderStatus(paymentInfo.order_id); if (response.data.status === 'paid') { - setIsPaidSuccess(true); - message.success('支付成功,报名已确认!'); + message.success('支付成功,请点击“完成报名”!'); + setPaymentModalVisible(false); + setPaymentInfo(null); + setPaymentStatus('paid'); // Trigger success effects confetti({ @@ -153,7 +160,7 @@ const ActivityDetail = () => { }, 3000); } return () => clearInterval(timer); - }, [paymentModalVisible, paymentInfo, id, queryClient, isPaidSuccess]); + }, [paymentModalVisible, paymentInfo, id, queryClient]); const handleShare = async () => { const url = window.location.href; @@ -180,11 +187,15 @@ const ActivityDetail = () => { return; } - // Check if we need to collect info - if (activity.signup_form_config && activity.signup_form_config.length > 0) { + setPaymentStatus('unpaid'); + setPaymentInfo(null); + + // Check if we need to collect info OR if it's a paid activity + // We want to use the modal for payment flow as well + if ((activity.signup_form_config && activity.signup_form_config.length > 0) || (activity.is_paid && activity.price > 0)) { setSignupFormVisible(true); } else { - // Direct signup if no info needed + // Direct signup if no info needed and free signUpMutation.mutate({}); } }; @@ -395,9 +406,36 @@ const ActivityDetail = () => { title="填写报名信息" open={signupFormVisible} onCancel={() => setSignupFormVisible(false)} - onOk={form.submit} - okText={activity?.is_paid && activity?.price > 0 ? `支付 ¥${activity.price}` : '提交报名'} - confirmLoading={signUpMutation.isPending} + footer={[ + , + (activity?.is_paid && activity?.price > 0) ? ( + + + + + ) : ( + + ) + ]} destroyOnHidden >
@@ -476,27 +514,10 @@ const ActivityDetail = () => {
{ - setPaymentModalVisible(false); - if (isPaidSuccess) { - setPaymentInfo(null); - } - }} - footer={[ - - ]} + onCancel={() => setPaymentModalVisible(false)} + footer={null} destroyOnHidden width={360} zIndex={1001} // 确保层级高于其他弹窗 @@ -504,26 +525,20 @@ const ActivityDetail = () => {
{paymentInfo?.code_url ? ( <> - {!isPaidSuccess ? ( - <> -
- -
-

¥{paymentInfo.price}

-

请使用微信扫一扫支付

- - ) : ( -
-
- -
-

支付成功

-
- )} +
+ +
+

¥{paymentInfo.price}

+

请使用微信扫一扫支付

) : ( )} +
+ +