n
This commit is contained in:
Binary file not shown.
@@ -669,6 +669,13 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
return queryset.filter(wechat_user=user).order_by('-created_at')
|
return queryset.filter(wechat_user=user).order_by('-created_at')
|
||||||
return queryset.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'])
|
@action(detail=True, methods=['post'])
|
||||||
def prepay_miniprogram(self, request, pk=None):
|
def prepay_miniprogram(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const getMyPaidItems = () => api.get('/users/paid-items/');
|
|||||||
export const getAnnouncements = () => api.get('/community/announcements/');
|
export const getAnnouncements = () => api.get('/community/announcements/');
|
||||||
export const getActivities = () => api.get('/community/activities/');
|
export const getActivities = () => api.get('/community/activities/');
|
||||||
export const getActivityDetail = (id) => api.get(`/community/activities/${id}/`);
|
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 const getMySignups = () => api.get('/community/activities/my_signups/');
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
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 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 { getActivityDetail, signUpActivity } from '../../api';
|
||||||
import styles from '../../components/activity/activity.module.less';
|
import styles from '../../components/activity/activity.module.less';
|
||||||
import { pageTransition, buttonTap } from '../../animation';
|
import { pageTransition, buttonTap } from '../../animation';
|
||||||
import LoginModal from '../../components/LoginModal';
|
import LoginModal from '../../components/LoginModal';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
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 ActivityDetail = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -19,6 +24,8 @@ const ActivityDetail = () => {
|
|||||||
const { scrollY } = useScroll();
|
const { scrollY } = useScroll();
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [loginVisible, setLoginVisible] = useState(false);
|
const [loginVisible, setLoginVisible] = useState(false);
|
||||||
|
const [signupFormVisible, setSignupFormVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
// Header animation: transparent to white with shadow
|
// Header animation: transparent to white with shadow
|
||||||
const headerBg = useTransform(scrollY, [0, 60], ['rgba(255,255,255,0)', 'rgba(255,255,255,1)']);
|
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 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 titleOpacity = useTransform(scrollY, [100, 200], [0, 1]);
|
||||||
|
|
||||||
const { data: activity, isLoading, error } = useQuery({
|
const { data: activity, isLoading, error, refetch } = useQuery({
|
||||||
queryKey: ['activity', id],
|
queryKey: ['activity', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,13 +43,32 @@ const ActivityDetail = () => {
|
|||||||
throw new Error(err.response?.data?.detail || 'Failed to load activity');
|
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({
|
const signUpMutation = useMutation({
|
||||||
mutationFn: () => signUpActivity(id),
|
mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
message.success('报名成功!');
|
message.success('报名成功!');
|
||||||
|
setSignupFormVisible(false);
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 150,
|
particleCount: 150,
|
||||||
spread: 70,
|
spread: 70,
|
||||||
@@ -53,7 +79,7 @@ const ActivityDetail = () => {
|
|||||||
queryClient.invalidateQueries(['activities']);
|
queryClient.invalidateQueries(['activities']);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
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);
|
setLoginVisible(true);
|
||||||
return;
|
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) {
|
if (isLoading) {
|
||||||
@@ -109,6 +146,11 @@ const ActivityDetail = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanUrl = (url) => {
|
||||||
|
if (!url) return '';
|
||||||
|
return url.replace(/[`\s]/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial="initial"
|
initial="initial"
|
||||||
@@ -157,7 +199,7 @@ const ActivityDetail = () => {
|
|||||||
<div className={styles.detailHeader}>
|
<div className={styles.detailHeader}>
|
||||||
<motion.img
|
<motion.img
|
||||||
layoutId={`activity-card-${id}`}
|
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}
|
alt={activity.title}
|
||||||
className={styles.detailImage}
|
className={styles.detailImage}
|
||||||
/>
|
/>
|
||||||
@@ -189,6 +231,10 @@ const ActivityDetail = () => {
|
|||||||
<EnvironmentOutlined />
|
<EnvironmentOutlined />
|
||||||
<span>{activity.location || '线上活动'}</span>
|
<span>{activity.location || '线上活动'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<UserOutlined />
|
||||||
|
<span>{activity.current_signups || 0} / {activity.max_participants} 已报名</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 10 }}>
|
<div style={{ display: 'flex', gap: 10 }}>
|
||||||
@@ -200,7 +246,33 @@ const ActivityDetail = () => {
|
|||||||
|
|
||||||
<div className={styles.richText}>
|
<div className={styles.richText}>
|
||||||
<h3>活动详情</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -232,6 +304,28 @@ const ActivityDetail = () => {
|
|||||||
// Auto trigger signup after login if needed, or just let user click again
|
// 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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,14 +101,21 @@ export default function Checkout() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const orderPromises = items.map(item => {
|
const orderPromises = items.map(item => {
|
||||||
const orderData = {
|
const type = params.type || 'config'
|
||||||
goodid: item.id,
|
const orderData: any = {
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
customer_name: address.userName,
|
customer_name: address.userName,
|
||||||
phone_number: address.telNumber,
|
phone_number: address.telNumber,
|
||||||
shipping_address: `${address.provinceName}${address.cityName}${address.countyName}${address.detailInfo}`,
|
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)
|
return createOrder(orderData)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -123,7 +130,7 @@ export default function Checkout() {
|
|||||||
|
|
||||||
if (results.length === 1) {
|
if (results.length === 1) {
|
||||||
// Single order, go to payment
|
// Single order, go to payment
|
||||||
const orderId = results[0].order_id
|
const orderId = results[0].id
|
||||||
Taro.redirectTo({
|
Taro.redirectTo({
|
||||||
url: `/pages/order/payment?id=${orderId}`
|
url: `/pages/order/payment?id=${orderId}`
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user