This commit is contained in:
2026-02-12 23:13:17 +08:00
parent d1dfc5b2e1
commit 162d7850b6
5 changed files with 122 additions and 14 deletions

View File

@@ -669,6 +669,13 @@ class OrderViewSet(viewsets.ModelViewSet):
return queryset.filter(wechat_user=user).order_by('-created_at')
return queryset.order_by('-created_at')
def perform_create(self, serializer):
"""
创建订单时自动关联当前微信用户
"""
user = get_current_wechat_user(self.request)
serializer.save(wechat_user=user)
@action(detail=True, methods=['post'])
def prepay_miniprogram(self, request, pk=None):
"""

View File

@@ -67,7 +67,7 @@ export const getMyPaidItems = () => api.get('/users/paid-items/');
export const getAnnouncements = () => api.get('/community/announcements/');
export const getActivities = () => api.get('/community/activities/');
export const getActivityDetail = (id) => api.get(`/community/activities/${id}/`);
export const signUpActivity = (id) => api.post(`/community/activities/${id}/signup/`);
export const signUpActivity = (id, data) => api.post(`/community/activities/${id}/signup/`, data);
export const getMySignups = () => api.get('/community/activities/my_signups/');
export default api;

View File

@@ -3,14 +3,19 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, useScroll, useTransform } from 'framer-motion';
import { ArrowLeftOutlined, ShareAltOutlined, CalendarOutlined, ClockCircleOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { ArrowLeftOutlined, ShareAltOutlined, CalendarOutlined, ClockCircleOutlined, EnvironmentOutlined, UserOutlined } from '@ant-design/icons';
import confetti from 'canvas-confetti';
import { message, Spin, Button, Result } from 'antd';
import { message, Spin, Button, Result, Modal, Form, Input } from 'antd';
import { getActivityDetail, signUpActivity } from '../../api';
import styles from '../../components/activity/activity.module.less';
import { pageTransition, buttonTap } from '../../animation';
import LoginModal from '../../components/LoginModal';
import { useAuth } from '../../context/AuthContext';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
const ActivityDetail = () => {
const { id } = useParams();
@@ -19,6 +24,8 @@ const ActivityDetail = () => {
const { scrollY } = useScroll();
const { login } = useAuth();
const [loginVisible, setLoginVisible] = useState(false);
const [signupFormVisible, setSignupFormVisible] = useState(false);
const [form] = Form.useForm();
// Header animation: transparent to white with shadow
const headerBg = useTransform(scrollY, [0, 60], ['rgba(255,255,255,0)', 'rgba(255,255,255,1)']);
@@ -26,7 +33,7 @@ const ActivityDetail = () => {
const headerColor = useTransform(scrollY, [0, 60], ['rgba(255,255,255,1)', 'rgba(0,0,0,0.85)']);
const titleOpacity = useTransform(scrollY, [100, 200], [0, 1]);
const { data: activity, isLoading, error } = useQuery({
const { data: activity, isLoading, error, refetch } = useQuery({
queryKey: ['activity', id],
queryFn: async () => {
try {
@@ -36,13 +43,32 @@ const ActivityDetail = () => {
throw new Error(err.response?.data?.detail || 'Failed to load activity');
}
},
staleTime: 5 * 60 * 1000,
staleTime: 0, // Ensure fresh data
refetchOnMount: 'always', // Force refetch on mount
});
// Force a refresh if needed (as requested by user)
useEffect(() => {
// 1. Force React Query refetch
refetch();
// 2. Hard refresh logic after 1 second delay
const timer = setTimeout(() => {
const hasRefreshedKey = `has_refreshed_activity_${id}`;
if (!sessionStorage.getItem(hasRefreshedKey)) {
sessionStorage.setItem(hasRefreshedKey, 'true');
window.location.reload();
}
}, 1000);
return () => clearTimeout(timer);
}, [id, refetch]);
const signUpMutation = useMutation({
mutationFn: () => signUpActivity(id),
mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }),
onSuccess: () => {
message.success('报名成功!');
setSignupFormVisible(false);
confetti({
particleCount: 150,
spread: 70,
@@ -53,7 +79,7 @@ const ActivityDetail = () => {
queryClient.invalidateQueries(['activities']);
},
onError: (err) => {
message.error(err.response?.data?.detail || '报名失败,请稍后重试');
message.error(err.response?.data?.detail || err.response?.data?.error || '报名失败,请稍后重试');
}
});
@@ -81,7 +107,18 @@ const ActivityDetail = () => {
setLoginVisible(true);
return;
}
signUpMutation.mutate();
// Check if we need to collect info
if (activity.signup_form_config && activity.signup_form_config.length > 0) {
setSignupFormVisible(true);
} else {
// Direct signup if no info needed
signUpMutation.mutate({});
}
};
const handleFormSubmit = (values) => {
signUpMutation.mutate(values);
};
if (isLoading) {
@@ -109,6 +146,11 @@ const ActivityDetail = () => {
);
}
const cleanUrl = (url) => {
if (!url) return '';
return url.replace(/[`\s]/g, '');
};
return (
<motion.div
initial="initial"
@@ -157,7 +199,7 @@ const ActivityDetail = () => {
<div className={styles.detailHeader}>
<motion.img
layoutId={`activity-card-${id}`}
src={activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://via.placeholder.com/800x600'}
src={activity.display_banner_url || cleanUrl(activity.banner_url) || activity.cover_image || 'https://via.placeholder.com/800x600'}
alt={activity.title}
className={styles.detailImage}
/>
@@ -189,6 +231,10 @@ const ActivityDetail = () => {
<EnvironmentOutlined />
<span>{activity.location || '线上活动'}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<UserOutlined />
<span>{activity.current_signups || 0} / {activity.max_participants} 已报名</span>
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
@@ -200,7 +246,33 @@ const ActivityDetail = () => {
<div className={styles.richText}>
<h3>活动详情</h3>
<div dangerouslySetInnerHTML={{ __html: activity.content || '<p>暂无详情描述</p>' }} />
<div style={{ color: '#ccc', lineHeight: '1.8' }}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{activity.description || activity.content || '暂无详情描述'}
</ReactMarkdown>
</div>
</div>
</div>
@@ -232,6 +304,28 @@ const ActivityDetail = () => {
// Auto trigger signup after login if needed, or just let user click again
}}
/>
<Modal
title="填写报名信息"
open={signupFormVisible}
onCancel={() => setSignupFormVisible(false)}
onOk={form.submit}
confirmLoading={signUpMutation.isPending}
destroyOnHidden
>
<Form form={form} onFinish={handleFormSubmit} layout="vertical">
{activity.signup_form_config && activity.signup_form_config.map(field => (
<Form.Item
key={field.name}
name={field.name}
label={field.label}
rules={[{ required: field.required, message: `请填写${field.label}` }]}
>
<Input placeholder={`请输入${field.label}`} />
</Form.Item>
))}
</Form>
</Modal>
</motion.div>
);
};

View File

@@ -101,14 +101,21 @@ export default function Checkout() {
try {
const orderPromises = items.map(item => {
const orderData = {
goodid: item.id,
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}`,
type: params.type || 'config'
ref_code: Taro.getStorageSync('ref_code') || ''
}
if (type === 'course') {
orderData.course = item.id
} else {
orderData.config = item.id
}
return createOrder(orderData)
})
@@ -123,7 +130,7 @@ export default function Checkout() {
if (results.length === 1) {
// Single order, go to payment
const orderId = results[0].order_id
const orderId = results[0].id
Taro.redirectTo({
url: `/pages/order/payment?id=${orderId}`
})