报名表单
All checks were successful
Deploy to Server / deploy (push) Successful in 37s

This commit is contained in:
jeremygan2021
2026-02-23 15:39:47 +08:00
parent c3fab398bb
commit 0bf5f94483
5 changed files with 52 additions and 29 deletions

View File

@@ -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 from shop.serializers import WeChatUserSerializer, ESP32ConfigSerializer, ServiceSerializer, VCCourseSerializer, OrderSerializer
from .utils import get_current_wechat_user from .utils import get_current_wechat_user
class ActivitySerializer(serializers.ModelSerializer): class ActivitySerializer(serializers.ModelSerializer):
@@ -47,10 +47,11 @@ 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'] fields = ['id', 'activity', 'activity_info', 'user', 'signup_time', 'status', 'signup_info', 'order']
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):

View File

@@ -100,8 +100,8 @@ DATABASES = {
} }
# 从环境变量获取数据库配置 (Docker 环境会自动注入这些变量) # 从环境变量获取数据库配置 (Docker 环境会自动注入这些变量)
DB_HOST = os.environ.get('DB_HOST', '121.43.104.161') # DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
# DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
if DB_HOST: if DB_HOST:
DATABASES['default'] = { DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
@@ -109,8 +109,8 @@ if DB_HOST:
'USER': os.environ.get('DB_USER', 'market'), 'USER': os.environ.get('DB_USER', 'market'),
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
'HOST': DB_HOST, 'HOST': DB_HOST,
'PORT': os.environ.get('DB_PORT', '6433'), #'PORT': os.environ.get('DB_PORT', '6433'),
#'PORT': os.environ.get('DB_PORT', '5432'), 'PORT': os.environ.get('DB_PORT', '5432'),
} }

View File

@@ -207,6 +207,7 @@ 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)
@@ -215,7 +216,7 @@ class OrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no', fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'activity', 'activity_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 = {

View File

@@ -1,8 +1,7 @@
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 { queryMyOrders, getMySignups } from '../api'; import api, { 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';
@@ -41,8 +40,6 @@ 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/'),
@@ -344,7 +341,7 @@ const MyOrders = () => {
onLoginSuccess={(userData) => { onLoginSuccess={(userData) => {
login(userData); login(userData);
if (userData.phone_number) { if (userData.phone_number) {
handleQueryOrders(userData.phone_number); handleQueryData();
} }
}} }}
/> />

View File

@@ -28,6 +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 [form] = Form.useForm(); const [form] = Form.useForm();
// Header animation: transparent to white with shadow // Header animation: transparent to white with shadow
@@ -96,6 +97,7 @@ const ActivityDetail = () => {
// 检查是否需要支付 // 检查是否需要支付
if (data.payment_required) { if (data.payment_required) {
setPaymentInfo(data); setPaymentInfo(data);
setIsPaidSuccess(false);
// 先关闭报名表单,确保层级正确 // 先关闭报名表单,确保层级正确
setSignupFormVisible(false); setSignupFormVisible(false);
// 延迟一点点时间打开支付弹窗,避免状态更新冲突 // 延迟一点点时间打开支付弹窗,避免状态更新冲突
@@ -125,14 +127,13 @@ const ActivityDetail = () => {
// Polling for payment status // Polling for payment status
useEffect(() => { useEffect(() => {
let timer; let timer;
if (paymentModalVisible && paymentInfo?.order_id) { if (paymentModalVisible && paymentInfo?.order_id && !isPaidSuccess) {
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);
// Trigger success effects // Trigger success effects
confetti({ confetti({
@@ -152,7 +153,7 @@ const ActivityDetail = () => {
}, 3000); }, 3000);
} }
return () => clearInterval(timer); return () => clearInterval(timer);
}, [paymentModalVisible, paymentInfo, id, queryClient]); }, [paymentModalVisible, paymentInfo, id, queryClient, isPaidSuccess]);
const handleShare = async () => { const handleShare = async () => {
const url = window.location.href; const url = window.location.href;
@@ -475,10 +476,27 @@ const ActivityDetail = () => {
</Form> </Form>
</Modal> </Modal>
<Modal <Modal
title="微信支付" title={isPaidSuccess ? "支付成功" : "微信支付"}
open={paymentModalVisible} open={paymentModalVisible}
onCancel={() => setPaymentModalVisible(false)} onCancel={() => {
footer={null} setPaymentModalVisible(false);
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} // 确保层级高于其他弹窗
@@ -486,20 +504,26 @@ const ActivityDetail = () => {
<div style={{ textAlign: 'center', padding: '20px 0' }}> <div style={{ textAlign: 'center', padding: '20px 0' }}>
{paymentInfo?.code_url ? ( {paymentInfo?.code_url ? (
<> <>
<div style={{ background: '#fff', padding: 10, display: 'inline-block' }}> {!isPaidSuccess ? (
<QRCodeSVG value={paymentInfo.code_url} size={200} /> <>
</div> <div style={{ background: '#fff', padding: 10, display: 'inline-block' }}>
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</p> <QRCodeSVG value={paymentInfo.code_url} size={200} />
<p style={{ color: '#666' }}>请使用微信扫一扫支付</p> </div>
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</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>