diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 995e413..8c2c7bf 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/views.py b/backend/shop/views.py index 26f830f..4f28df1 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -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): """ diff --git a/frontend/src/api.js b/frontend/src/api.js index f9908aa..5d074ac 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -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; diff --git a/frontend/src/pages/activity/Detail.jsx b/frontend/src/pages/activity/Detail.jsx index 1cd1a21..ac85c50 100644 --- a/frontend/src/pages/activity/Detail.jsx +++ b/frontend/src/pages/activity/Detail.jsx @@ -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 ( {
@@ -189,6 +231,10 @@ const ActivityDetail = () => { {activity.location || '线上活动'}
+
+ + {activity.current_signups || 0} / {activity.max_participants} 已报名 +
@@ -200,7 +246,33 @@ const ActivityDetail = () => {

活动详情

-
暂无详情描述

' }} /> +
+ + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ) + } + }} + > + {activity.description || activity.content || '暂无详情描述'} + +
@@ -232,6 +304,28 @@ const ActivityDetail = () => { // Auto trigger signup after login if needed, or just let user click again }} /> + + setSignupFormVisible(false)} + onOk={form.submit} + confirmLoading={signUpMutation.isPending} + destroyOnHidden + > +
+ {activity.signup_form_config && activity.signup_form_config.map(field => ( + + + + ))} +
+
); }; diff --git a/miniprogram/src/pages/order/checkout.tsx b/miniprogram/src/pages/order/checkout.tsx index 5996fdd..63bfe2a 100644 --- a/miniprogram/src/pages/order/checkout.tsx +++ b/miniprogram/src/pages/order/checkout.tsx @@ -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}` })