This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
|
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
|
from .utils import get_current_wechat_user
|
||||||
|
|
||||||
class ActivitySerializer(serializers.ModelSerializer):
|
class ActivitySerializer(serializers.ModelSerializer):
|
||||||
@@ -47,11 +47,10 @@ class ActivitySerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class ActivitySignupSerializer(serializers.ModelSerializer):
|
class ActivitySignupSerializer(serializers.ModelSerializer):
|
||||||
activity_info = serializers.SerializerMethodField()
|
activity_info = serializers.SerializerMethodField()
|
||||||
order = OrderSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ActivitySignup
|
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']
|
read_only_fields = ['signup_time', 'status', 'user']
|
||||||
|
|
||||||
def get_activity_info(self, obj):
|
def get_activity_info(self, obj):
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
config_name = serializers.CharField(source='config.name', read_only=True)
|
config_name = serializers.CharField(source='config.name', read_only=True)
|
||||||
course_title = serializers.CharField(source='course.title', 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()
|
config_image = serializers.SerializerMethodField()
|
||||||
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
|
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
|
||||||
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
|
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
|
||||||
@@ -216,7 +215,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
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']
|
'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']
|
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd';
|
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 { 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 LoginModal from '../components/LoginModal';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -40,6 +41,8 @@ const MyOrders = () => {
|
|||||||
const handleQueryData = async () => {
|
const handleQueryData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const { default: api } = await import('../api');
|
||||||
|
|
||||||
// Parallel fetch
|
// Parallel fetch
|
||||||
const [ordersRes, activitiesRes] = await Promise.allSettled([
|
const [ordersRes, activitiesRes] = await Promise.allSettled([
|
||||||
api.get('/orders/'),
|
api.get('/orders/'),
|
||||||
@@ -341,7 +344,7 @@ const MyOrders = () => {
|
|||||||
onLoginSuccess={(userData) => {
|
onLoginSuccess={(userData) => {
|
||||||
login(userData);
|
login(userData);
|
||||||
if (userData.phone_number) {
|
if (userData.phone_number) {
|
||||||
handleQueryData();
|
handleQueryOrders(userData.phone_number);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const ActivityDetail = () => {
|
|||||||
const [signupFormVisible, setSignupFormVisible] = useState(false);
|
const [signupFormVisible, setSignupFormVisible] = useState(false);
|
||||||
const [paymentModalVisible, setPaymentModalVisible] = useState(false);
|
const [paymentModalVisible, setPaymentModalVisible] = useState(false);
|
||||||
const [paymentInfo, setPaymentInfo] = useState(null);
|
const [paymentInfo, setPaymentInfo] = useState(null);
|
||||||
const [isPaidSuccess, setIsPaidSuccess] = useState(false);
|
const [paymentStatus, setPaymentStatus] = useState('unpaid'); // 'unpaid' | 'paying' | 'paid'
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
// Header animation: transparent to white with shadow
|
// Header animation: transparent to white with shadow
|
||||||
@@ -93,13 +93,17 @@ const ActivityDetail = () => {
|
|||||||
|
|
||||||
const signUpMutation = useMutation({
|
const signUpMutation = useMutation({
|
||||||
mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }),
|
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) {
|
if (data.payment_required) {
|
||||||
setPaymentInfo(data);
|
setPaymentInfo(data);
|
||||||
setIsPaidSuccess(false);
|
setPaymentStatus('paying');
|
||||||
// 先关闭报名表单,确保层级正确
|
|
||||||
setSignupFormVisible(false);
|
// 不关闭报名表单,直接打开支付弹窗
|
||||||
// 延迟一点点时间打开支付弹窗,避免状态更新冲突
|
// 延迟一点点时间打开支付弹窗,避免状态更新冲突
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPaymentModalVisible(true);
|
setPaymentModalVisible(true);
|
||||||
@@ -110,6 +114,7 @@ const ActivityDetail = () => {
|
|||||||
|
|
||||||
message.success('报名成功!');
|
message.success('报名成功!');
|
||||||
setSignupFormVisible(false);
|
setSignupFormVisible(false);
|
||||||
|
setPaymentStatus('paid'); // In case it was free but we track it
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 150,
|
particleCount: 150,
|
||||||
spread: 70,
|
spread: 70,
|
||||||
@@ -127,13 +132,15 @@ const ActivityDetail = () => {
|
|||||||
// Polling for payment status
|
// Polling for payment status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer;
|
let timer;
|
||||||
if (paymentModalVisible && paymentInfo?.order_id && !isPaidSuccess) {
|
if (paymentModalVisible && paymentInfo?.order_id) {
|
||||||
timer = setInterval(async () => {
|
timer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await queryOrderStatus(paymentInfo.order_id);
|
const response = await queryOrderStatus(paymentInfo.order_id);
|
||||||
if (response.data.status === 'paid') {
|
if (response.data.status === 'paid') {
|
||||||
setIsPaidSuccess(true);
|
message.success('支付成功,请点击“完成报名”!');
|
||||||
message.success('支付成功,报名已确认!');
|
setPaymentModalVisible(false);
|
||||||
|
setPaymentInfo(null);
|
||||||
|
setPaymentStatus('paid');
|
||||||
|
|
||||||
// Trigger success effects
|
// Trigger success effects
|
||||||
confetti({
|
confetti({
|
||||||
@@ -153,7 +160,7 @@ const ActivityDetail = () => {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [paymentModalVisible, paymentInfo, id, queryClient, isPaidSuccess]);
|
}, [paymentModalVisible, paymentInfo, id, queryClient]);
|
||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
@@ -180,11 +187,15 @@ const ActivityDetail = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to collect info
|
setPaymentStatus('unpaid');
|
||||||
if (activity.signup_form_config && activity.signup_form_config.length > 0) {
|
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);
|
setSignupFormVisible(true);
|
||||||
} else {
|
} else {
|
||||||
// Direct signup if no info needed
|
// Direct signup if no info needed and free
|
||||||
signUpMutation.mutate({});
|
signUpMutation.mutate({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -395,9 +406,36 @@ const ActivityDetail = () => {
|
|||||||
title="填写报名信息"
|
title="填写报名信息"
|
||||||
open={signupFormVisible}
|
open={signupFormVisible}
|
||||||
onCancel={() => setSignupFormVisible(false)}
|
onCancel={() => setSignupFormVisible(false)}
|
||||||
onOk={form.submit}
|
footer={[
|
||||||
okText={activity?.is_paid && activity?.price > 0 ? `支付 ¥${activity.price}` : '提交报名'}
|
<Button key="cancel" onClick={() => setSignupFormVisible(false)}>
|
||||||
confirmLoading={signUpMutation.isPending}
|
取消
|
||||||
|
</Button>,
|
||||||
|
(activity?.is_paid && activity?.price > 0) ? (
|
||||||
|
<React.Fragment key="paid-actions">
|
||||||
|
<Button
|
||||||
|
key="pay"
|
||||||
|
type="primary"
|
||||||
|
onClick={form.submit}
|
||||||
|
loading={signUpMutation.isPending && paymentStatus === 'unpaid'}
|
||||||
|
disabled={paymentStatus === 'paid'}
|
||||||
|
>
|
||||||
|
{paymentStatus === 'paid' ? '已支付' : `支付 ¥${activity.price}`}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
key="complete"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => setSignupFormVisible(false)}
|
||||||
|
disabled={paymentStatus !== 'paid'}
|
||||||
|
>
|
||||||
|
完成报名
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<Button key="submit" type="primary" onClick={form.submit} loading={signUpMutation.isPending}>
|
||||||
|
提交报名
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
]}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFormSubmit} layout="vertical">
|
<Form form={form} onFinish={handleFormSubmit} layout="vertical">
|
||||||
@@ -476,35 +514,16 @@ const ActivityDetail = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={isPaidSuccess ? "支付成功" : "微信支付"}
|
title="微信支付"
|
||||||
open={paymentModalVisible}
|
open={paymentModalVisible}
|
||||||
onCancel={() => {
|
onCancel={() => setPaymentModalVisible(false)}
|
||||||
setPaymentModalVisible(false);
|
footer={null}
|
||||||
if (isPaidSuccess) {
|
|
||||||
setPaymentInfo(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
footer={[
|
|
||||||
<Button
|
|
||||||
key="ok"
|
|
||||||
type="primary"
|
|
||||||
disabled={!isPaidSuccess}
|
|
||||||
onClick={() => {
|
|
||||||
setPaymentModalVisible(false);
|
|
||||||
setPaymentInfo(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPaidSuccess ? '完成' : '等待支付...'}
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={360}
|
width={360}
|
||||||
zIndex={1001} // 确保层级高于其他弹窗
|
zIndex={1001} // 确保层级高于其他弹窗
|
||||||
>
|
>
|
||||||
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||||
{paymentInfo?.code_url ? (
|
{paymentInfo?.code_url ? (
|
||||||
<>
|
|
||||||
{!isPaidSuccess ? (
|
|
||||||
<>
|
<>
|
||||||
<div style={{ background: '#fff', padding: 10, display: 'inline-block' }}>
|
<div style={{ background: '#fff', padding: 10, display: 'inline-block' }}>
|
||||||
<QRCodeSVG value={paymentInfo.code_url} size={200} />
|
<QRCodeSVG value={paymentInfo.code_url} size={200} />
|
||||||
@@ -512,18 +531,14 @@ const ActivityDetail = () => {
|
|||||||
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</p>
|
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</p>
|
||||||
<p style={{ color: '#666' }}>请使用微信扫一扫支付</p>
|
<p style={{ color: '#666' }}>请使用微信扫一扫支付</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<div style={{ padding: '20px 0' }}>
|
|
||||||
<div style={{ fontSize: 48, color: '#52c41a', marginBottom: 16 }}>
|
|
||||||
<i className="anticon anticon-check-circle"><svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path></svg></i>
|
|
||||||
</div>
|
|
||||||
<p style={{ fontSize: 16, fontWeight: 'bold' }}>支付成功</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<Spin tip="正在生成二维码..." />
|
<Spin tip="正在生成二维码..." />
|
||||||
)}
|
)}
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<Button type="primary" onClick={() => window.location.reload()}>
|
||||||
|
我已支付
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user