This commit is contained in:
2026-02-12 21:12:55 +08:00
parent a94815399f
commit 6e39a01e8c
5 changed files with 311 additions and 18 deletions

View File

@@ -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 (
<AuthProvider>
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/services" element={<AIServices />} />
<Route path="/services/:id" element={<ServiceDetail />} />
<Route path="/courses" element={<VCCourses />} />
<Route path="/courses/:id" element={<VCCourseDetail />} />
<Route path="/forum" element={<ForumList />} />
<Route path="/forum/:id" element={<ForumDetail />} />
<Route path="/my-orders" element={<MyOrders />} />
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/payment/:orderId" element={<Payment />} />
</Routes>
</Layout>
</BrowserRouter>
</AuthProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/services" element={<AIServices />} />
<Route path="/services/:id" element={<ServiceDetail />} />
<Route path="/courses" element={<VCCourses />} />
<Route path="/courses/:id" element={<VCCourseDetail />} />
<Route path="/forum" element={<ForumList />} />
<Route path="/forum/:id" element={<ForumDetail />} />
<Route path="/activity/:id" element={<ActivityDetail />} />
<Route path="/my-orders" element={<MyOrders />} />
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/payment/:orderId" element={<Payment />} />
</Routes>
</Layout>
</BrowserRouter>
</AuthProvider>
</QueryClientProvider>
)
}

View File

@@ -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;

View File

@@ -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) => (
<MemoryRouter>
<div style={{ maxWidth: '400px', padding: '20px' }}>
<Story />
</div>
</MemoryRouter>
),
],
tags: ['autodocs'],
};
const Template = (args) => <ActivityCard {...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',
},
};

View File

@@ -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 = () => {
</Paragraph>
</motion.div>
</div>
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '0 24px' }}>
<ActivityList />
</div>
<div className="product-scroll-container">
<Row gutter={[24, 24]} wrap={false}>

View File

@@ -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 (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', background: '#1f1f1f' }}>
<Spin size="large" tip="加载活动详情..." />
</div>
);
}
if (error) {
return (
<div style={{ padding: 40, background: '#1f1f1f', minHeight: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Result
status="error"
title="加载失败"
subTitle={error.message}
extra={[
<Button type="primary" key="back" onClick={() => navigate(-1)}>
返回列表
</Button>
]}
/>
</div>
);
}
return (
<motion.div
initial="initial"
animate="animate"
exit="exit"
variants={pageTransition}
style={{ background: '#1f1f1f', minHeight: '100vh', color: '#fff' }}
>
{/* Sticky Header */}
<motion.div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 60,
zIndex: 100,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 20px',
background: headerBg,
boxShadow: headerShadow,
}}
>
<motion.div
onClick={() => navigate(-1)}
style={{ cursor: 'pointer', color: headerColor, fontSize: 20 }}
>
<ArrowLeftOutlined />
</motion.div>
<motion.div
style={{ color: headerColor, fontWeight: 600, opacity: useTransform(scrollY, [100, 200], [0, 1]) }}
>
{activity.title}
</motion.div>
<motion.div
onClick={handleShare}
style={{ cursor: 'pointer', color: headerColor, fontSize: 20 }}
>
<ShareAltOutlined />
</motion.div>
</motion.div>
{/* Hero Image with LayoutId for shared transition */}
<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'}
alt={activity.title}
className={styles.detailImage}
/>
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: '50%',
background: 'linear-gradient(to top, #1f1f1f, transparent)'
}} />
</div>
{/* Content */}
<div className={styles.detailContent}>
<div className={styles.infoCard}>
<h1 style={{ fontSize: 28, marginBottom: 16, color: '#fff' }}>{activity.title}</h1>
<div style={{ display: 'flex', gap: 20, marginBottom: 16, color: 'rgba(255,255,255,0.7)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<CalendarOutlined />
<span>{activity.start_time ? new Date(activity.start_time).toLocaleDateString() : 'TBD'}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<EnvironmentOutlined />
<span>{activity.location || '线上活动'}</span>
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<span className={styles.statusTag}>
{activity.status || (new Date() < new Date(activity.start_time) ? '报名中' : '已结束')}
</span>
</div>
</div>
<div className={styles.richText}>
<h3>活动详情</h3>
<div dangerouslySetInnerHTML={{ __html: activity.content || '<p>暂无详情描述</p>' }} />
</div>
</div>
{/* Fixed Footer */}
<div className={styles.fixedFooter}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.5)' }}>距离报名截止</span>
<span style={{ color: '#00b96b', fontWeight: 'bold' }}>
{/* Simple countdown placeholder */}
3 12小时
</span>
</div>
<motion.button
className={styles.actionBtn}
variants={buttonTap}
whileTap="tap"
onClick={handleSignUp}
disabled={signUpMutation.isPending || activity.is_signed_up}
>
{signUpMutation.isPending ? '提交中...' : activity.is_signed_up ? '已报名' : '立即报名'}
</motion.button>
</div>
</motion.div>
);
};
export default ActivityDetail;