From 6e39a01e8c62664e119550dad671f492c2777857 Mon Sep 17 00:00:00 2001 From: Therainclouds <245141853@qq.com> Date: Thu, 12 Feb 2026 21:12:55 +0800 Subject: [PATCH] new --- frontend/src/App.jsx | 44 ++-- frontend/src/api.js | 1 + .../activity/ActivityCard.stories.jsx | 67 ++++++ frontend/src/pages/Home.jsx | 5 + frontend/src/pages/activity/Detail.jsx | 212 ++++++++++++++++++ 5 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/activity/ActivityCard.stories.jsx create mode 100644 frontend/src/pages/activity/Detail.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b218974..4714660 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,7 @@ + import React from 'react' import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from './context/AuthContext'; import Layout from './components/Layout'; import Home from './pages/Home'; @@ -12,29 +14,35 @@ import VCCourseDetail from './pages/VCCourseDetail'; import MyOrders from './pages/MyOrders'; import ForumList from './pages/ForumList'; import ForumDetail from './pages/ForumDetail'; +import ActivityDetail from './pages/activity/Detail'; import 'antd/dist/reset.css'; import './App.css'; +const queryClient = new QueryClient(); + function App() { return ( - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 03ef2f8..f9908aa 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -68,5 +68,6 @@ 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 getMySignups = () => api.get('/community/activities/my_signups/'); export default api; diff --git a/frontend/src/components/activity/ActivityCard.stories.jsx b/frontend/src/components/activity/ActivityCard.stories.jsx new file mode 100644 index 0000000..afd33af --- /dev/null +++ b/frontend/src/components/activity/ActivityCard.stories.jsx @@ -0,0 +1,67 @@ + +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import ActivityCard from './ActivityCard'; +import '../../index.css'; // Global styles +import '../../App.css'; + +export default { + title: 'Components/Activity/ActivityCard', + component: ActivityCard, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], + tags: ['autodocs'], +}; + +const Template = (args) => ; + +export const NotStarted = Template.bind({}); +NotStarted.args = { + activity: { + id: 1, + title: 'Future AI Hardware Summit 2026', + start_time: '2026-12-01T09:00:00', + status: '即将开始', + cover_image: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&q=80', + }, +}; + +export const Ongoing = Template.bind({}); +Ongoing.args = { + activity: { + id: 2, + title: 'Edge Computing Hackathon', + start_time: '2025-10-20T10:00:00', + status: '报名中', + cover_image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80', + }, +}; + +export const Ended = Template.bind({}); +Ended.args = { + activity: { + id: 3, + title: 'Deep Learning Workshop', + start_time: '2023-05-15T14:00:00', + status: '已结束', + cover_image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80', + }, +}; + +export const SignedUp = Template.bind({}); +SignedUp.args = { + activity: { + id: 4, + title: 'Exclusive Developer Meetup', + start_time: '2025-11-11T18:00:00', + status: '已报名', + cover_image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&q=80', + }, +}; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 94253e8..e73f3a5 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -4,6 +4,7 @@ import { RocketOutlined, RightOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { getConfigs } from '../api'; +import ActivityList from '../components/activity/ActivityList'; import './Home.css'; const { Title, Paragraph } = Typography; @@ -107,6 +108,10 @@ const Home = () => { + +
+ +
diff --git a/frontend/src/pages/activity/Detail.jsx b/frontend/src/pages/activity/Detail.jsx new file mode 100644 index 0000000..3c3a789 --- /dev/null +++ b/frontend/src/pages/activity/Detail.jsx @@ -0,0 +1,212 @@ + +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 confetti from 'canvas-confetti'; +import { message, Spin, Button, Result } from 'antd'; +import { getActivityDetail, signUpActivity } from '../../api'; +import styles from '../../components/activity/activity.module.less'; +import { pageTransition, buttonTap } from '../../animation'; + +const ActivityDetail = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { scrollY } = useScroll(); + + // Header animation: transparent to white with shadow + const headerBg = useTransform(scrollY, [0, 60], ['rgba(255,255,255,0)', 'rgba(255,255,255,1)']); + const headerShadow = useTransform(scrollY, [0, 60], ['none', '0 2px 8px rgba(0,0,0,0.1)']); + const headerColor = useTransform(scrollY, [0, 60], ['rgba(255,255,255,1)', 'rgba(0,0,0,0.85)']); + + const { data: activity, isLoading, error } = useQuery({ + queryKey: ['activity', id], + queryFn: async () => { + try { + const res = await getActivityDetail(id); + return res.data; + } catch (err) { + throw new Error(err.response?.data?.detail || 'Failed to load activity'); + } + }, + staleTime: 5 * 60 * 1000, + }); + + const signUpMutation = useMutation({ + mutationFn: () => signUpActivity(id), + onSuccess: () => { + message.success('报名成功!'); + confetti({ + particleCount: 150, + spread: 70, + origin: { y: 0.6 }, + colors: ['#00b96b', '#1890ff', '#ffffff'] + }); + queryClient.invalidateQueries(['activity', id]); + queryClient.invalidateQueries(['activities']); + }, + onError: (err) => { + message.error(err.response?.data?.detail || '报名失败,请稍后重试'); + } + }); + + const handleShare = async () => { + const url = window.location.href; + if (navigator.share) { + try { + await navigator.share({ + title: activity?.title, + text: '来看看这个精彩活动!', + url: url + }); + } catch (err) { + console.log('Share canceled'); + } + } else { + navigator.clipboard.writeText(url); + message.success('链接已复制到剪贴板'); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ navigate(-1)}> + 返回列表 + + ]} + /> +
+ ); + } + + return ( + + {/* Sticky Header */} + + navigate(-1)} + style={{ cursor: 'pointer', color: headerColor, fontSize: 20 }} + > + + + + {activity.title} + + + + + + + {/* Hero Image with LayoutId for shared transition */} +
+ +
+
+ + {/* Content */} +
+
+

{activity.title}

+ +
+
+ + {activity.start_time ? new Date(activity.start_time).toLocaleDateString() : 'TBD'} +
+
+ + {activity.location || '线上活动'} +
+
+ +
+ + {activity.status || (new Date() < new Date(activity.start_time) ? '报名中' : '已结束')} + +
+
+ +
+

活动详情

+
暂无详情描述

' }} /> +
+
+ + {/* Fixed Footer */} +
+
+ 距离报名截止 + + {/* Simple countdown placeholder */} + 3天 12小时 + +
+ + {signUpMutation.isPending ? '提交中...' : activity.is_signed_up ? '已报名' : '立即报名'} + +
+ + ); +}; + +export default ActivityDetail;