feat: 移除轮播图管理,系列活动改卡片布局,课程描述限10行,首页配置优化
All checks were successful
Deploy to Server / deploy (push) Successful in 2m4s
All checks were successful
Deploy to Server / deploy (push) Successful in 2m4s
This commit is contained in:
@@ -1,100 +1,102 @@
|
||||
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { CalendarOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { Card, Tag, Typography, Space, Divider } from 'antd';
|
||||
import { CalendarOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styles from './activity.module.less';
|
||||
import { hoverScale } from '../../animation';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http') || url.startsWith('//')) {
|
||||
try { return new URL(url).pathname; } catch { return url; }
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const ActivityCard = ({ activity }) => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const imgRef = useRef(null);
|
||||
|
||||
const handleCardClick = () => {
|
||||
navigate(`/activity/${activity.id}`);
|
||||
};
|
||||
const imgSrc = getImageUrl(activity.display_banner_url || activity.banner_url || activity.cover_image)
|
||||
|| 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=600&h=400&fit=crop';
|
||||
|
||||
const getStatus = (startTime) => {
|
||||
const now = new Date();
|
||||
const start = new Date(startTime);
|
||||
if (now < start) return '即将开始';
|
||||
return '报名中';
|
||||
const getStatusColor = (status) => {
|
||||
if (!status) return 'blue';
|
||||
if (status.includes('报名')) return 'green';
|
||||
if (status.includes('即将')) return 'cyan';
|
||||
if (status.includes('结束')) return 'red';
|
||||
return 'blue';
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return 'TBD';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
|
||||
return new Date(dateStr).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
};
|
||||
|
||||
const imgSrc = hasError
|
||||
? 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=600&h=400&fit=crop'
|
||||
: (activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=600&h=400&fit=crop');
|
||||
|
||||
// Check if image is already loaded (cached) to prevent flashing
|
||||
useLayoutEffect(() => {
|
||||
if (imgRef.current && imgRef.current.complete) {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
}, [imgSrc]);
|
||||
const statusText = activity.status || (
|
||||
new Date() < new Date(activity.start_time) ? '即将开始' : '报名中'
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.activityCard}
|
||||
variants={hoverScale}
|
||||
whileHover="hover"
|
||||
onClick={handleCardClick}
|
||||
layoutId={`activity-card-${activity.id}`}
|
||||
style={{ willChange: 'transform' }}
|
||||
>
|
||||
<div className={styles.imageContainer}>
|
||||
{/* Placeholder Background - Always visible behind the image */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#f5f5f5',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imgSrc}
|
||||
alt={activity.title}
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
opacity: isLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-out'
|
||||
}}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={() => {
|
||||
setHasError(true);
|
||||
setIsLoaded(true);
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className={styles.overlay} style={{ zIndex: 2 }}>
|
||||
<div className={styles.statusTag}>
|
||||
{activity.status || getStatus(activity.start_time)}
|
||||
</div>
|
||||
<h3 className={styles.title}>{activity.title}</h3>
|
||||
<div className={styles.time}>
|
||||
<CalendarOutlined />
|
||||
<span>{formatDate(activity.start_time)}</span>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<div style={{ height: 280, overflow: 'hidden', position: 'relative' }}>
|
||||
<img
|
||||
alt={activity.title}
|
||||
src={imgSrc}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => {
|
||||
e.target.src = 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=600&h=400&fit=crop';
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'absolute', top: 10, right: 10 }}>
|
||||
<Tag color={getStatusColor(statusText)} style={{ marginRight: 0, fontSize: 14, padding: '4px 12px' }}>
|
||||
{statusText}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column', fontSize: 16 }}
|
||||
styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', padding: 24 } }}
|
||||
onClick={() => navigate(`/activity/${activity.id}`)}
|
||||
>
|
||||
<Title level={3} ellipsis={{ rows: 2 }} style={{ marginBottom: 12, height: 64, fontSize: 20 }}>
|
||||
{activity.title}
|
||||
</Title>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
color: 'rgba(255,255,255,0.65)',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
fontSize: '15px',
|
||||
lineHeight: 1.6,
|
||||
}}>
|
||||
{activity.description || activity.subtitle || ''}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
{activity.start_time && (
|
||||
<Space>
|
||||
<CalendarOutlined style={{ color: '#1890ff', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 14 }}>
|
||||
{formatDate(activity.start_time)}
|
||||
{activity.end_time ? ` ~ ${formatDate(activity.end_time)}` : ''}
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
{activity.location && (
|
||||
<Space>
|
||||
<EnvironmentOutlined style={{ color: '#1890ff', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 14 }}>{activity.location}</span>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,109 +1,76 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Row, Col, Empty, Spin, Input, Select } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import { getActivities } from '../../api';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import styles from './activity.module.less';
|
||||
import { fadeInUp, staggerContainer } from '../../animation';
|
||||
|
||||
const { Search } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const ActivityList = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
const { data: activities, isLoading, error } = useQuery({
|
||||
queryKey: ['activities'],
|
||||
queryFn: async () => {
|
||||
const res = await getActivities();
|
||||
// Handle different response structures
|
||||
return Array.isArray(res.data) ? res.data : (res.data?.results || []);
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const filtered = (activities || []).filter(a => {
|
||||
const matchSearch = !search || a.title?.includes(search);
|
||||
const matchStatus = statusFilter === 'all' || a.status === statusFilter;
|
||||
return matchSearch && matchStatus;
|
||||
});
|
||||
|
||||
// Auto-play for desktop carousel
|
||||
useEffect(() => {
|
||||
if (!activities || activities.length <= 1) return;
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % activities.length);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [activities]);
|
||||
|
||||
const nextSlide = () => {
|
||||
if (!activities) return;
|
||||
setCurrentIndex((prev) => (prev + 1) % activities.length);
|
||||
};
|
||||
|
||||
const prevSlide = () => {
|
||||
if (!activities) return;
|
||||
setCurrentIndex((prev) => (prev - 1 + activities.length) % activities.length);
|
||||
};
|
||||
|
||||
if (isLoading) return <div className={styles.loading}>Loading activities...</div>;
|
||||
if (error) return null; // Or error state
|
||||
if (!activities || activities.length === 0) return null;
|
||||
if (isLoading) return (
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Spin size="large" tip="正在加载活动..." />
|
||||
</div>
|
||||
);
|
||||
if (error) return <Empty description="加载失败,请稍后重试" />;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.activitySection}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={staggerContainer}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
近期活动 / EVENTS
|
||||
</h2>
|
||||
<div className={styles.controls}>
|
||||
<button onClick={prevSlide} className={styles.navBtn}><LeftOutlined /></button>
|
||||
<button onClick={nextSlide} className={styles.navBtn}><RightOutlined /></button>
|
||||
<div>
|
||||
<div style={{ marginBottom: 32, textAlign: 'center' }}>
|
||||
<div style={{ maxWidth: 800, margin: '0 auto', display: 'flex', gap: 16 }}>
|
||||
<Search
|
||||
placeholder="搜索活动名称"
|
||||
allowClear
|
||||
enterButton="搜索"
|
||||
size="large"
|
||||
onSearch={setSearch}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Select
|
||||
defaultValue="all"
|
||||
size="large"
|
||||
style={{ width: 120 }}
|
||||
onChange={setStatusFilter}
|
||||
>
|
||||
<Option value="all">全部状态</Option>
|
||||
<Option value="报名中">报名中</Option>
|
||||
<Option value="即将开始">即将开始</Option>
|
||||
<Option value="已结束">已结束</Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Carousel (Show one prominent, but allows list structure if needed)
|
||||
User said: "Activity only shows one, and in the form of a sliding page"
|
||||
*/}
|
||||
<div className={styles.desktopCarousel}>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0, zIndex: 1 }}
|
||||
exit={{ x: '-100%', zIndex: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<ActivityCard activity={activities[currentIndex]} />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={styles.dots} style={{ position: 'absolute', bottom: '10px', width: '100%', zIndex: 10 }}>
|
||||
{activities.map((_, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`${styles.dot} ${idx === currentIndex ? styles.activeDot : ''}`}
|
||||
onClick={() => setCurrentIndex(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Vertical List/Scroll as requested "Mobile vertical scroll" */}
|
||||
<div className={styles.mobileList}>
|
||||
{activities.map((item, index) => (
|
||||
<motion.div key={item.id} variants={fadeInUp} custom={index}>
|
||||
<ActivityCard activity={item} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
{filtered.length > 0 ? (
|
||||
<Row gutter={[32, 32]} justify="center">
|
||||
{filtered.map((activity) => (
|
||||
<Col key={activity.id} xs={24} sm={12} md={8} lg={8} xl={8} xxl={8}>
|
||||
<ActivityCard activity={activity} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description="暂无相关活动" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,18 +22,15 @@ const Home = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const [isTypingComplete, setIsTypingComplete] = useState(false);
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [currentSlide2, setCurrentSlide2] = useState(0);
|
||||
const [currentSlide3, setCurrentSlide3] = useState(0);
|
||||
const [homeConfig, setHomeConfig] = useState(null);
|
||||
const [carousel1Items, setCarousel1Items] = useState([]);
|
||||
const [competitions, setCompetitions] = useState([]);
|
||||
const [activities, setActivities] = useState([]);
|
||||
const fullText = "未来已来 AI 核心驱动";
|
||||
const defaultMainTitle = '"创赢未来"云南2026创业大赛';
|
||||
const [mainTitleText, setMainTitleText] = useState(defaultMainTitle);
|
||||
const navigate = useNavigate();
|
||||
const carouselRef = useRef(null);
|
||||
const carouselRef2 = useRef(null);
|
||||
const carouselRef3 = useRef(null);
|
||||
|
||||
@@ -66,9 +63,7 @@ const Home = () => {
|
||||
const fetchHomePageConfig = async () => {
|
||||
try {
|
||||
const response = await getHomePageConfig();
|
||||
const data = response.data;
|
||||
setHomeConfig(data);
|
||||
setCarousel1Items(data.carousel1_items || []);
|
||||
setHomeConfig(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch homepage config:', error);
|
||||
}
|
||||
@@ -159,135 +154,6 @@ const Home = () => {
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 轮播图 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
style={{ marginTop: 40, maxWidth: 1200, marginLeft: 'auto', marginRight: 'auto' }}
|
||||
>
|
||||
{/* 轮播图主体 */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 32px rgba(24, 144, 255, 0.2)',
|
||||
}}>
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
autoplay
|
||||
dots={false}
|
||||
beforeChange={(_, next) => setCurrentSlide(next)}
|
||||
>
|
||||
{(carousel1Items.length > 0 ? carousel1Items : []).map((image, index) => (
|
||||
<div key={index}>
|
||||
<div style={{
|
||||
height: 450,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)',
|
||||
}}>
|
||||
<img
|
||||
src={getImageUrl(image.display_image) || image.image_url}
|
||||
alt={image.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/* 渐变遮罩 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.6) 100%)',
|
||||
}} />
|
||||
{/* 标题区域 - 图片上方 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
left: 40,
|
||||
textAlign: 'left',
|
||||
}}>
|
||||
<h2 style={{ fontSize: 36, fontWeight: 'bold', color: '#fff', margin: 0, textShadow: '2px 2px 4px rgba(0,0,0,0.3)' }}>
|
||||
{image.title}
|
||||
</h2>
|
||||
<p style={{ fontSize: 18, color: 'rgba(255,255,255,0.9)', margin: '8px 0 0 0', textShadow: '1px 1px 2px rgba(0,0,0,0.3)' }}>
|
||||
{image.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
{/* 底部信息 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: '24px 40px',
|
||||
background: 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 100%)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<Tag style={{
|
||||
background: image.status_color,
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
fontSize: 14,
|
||||
padding: '4px 16px',
|
||||
borderRadius: 20,
|
||||
}}>
|
||||
{image.status}
|
||||
</Tag>
|
||||
<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14 }}>{image.location}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<h3 style={{ color: '#fff', fontSize: 24, fontWeight: 'bold', margin: 0 }}>{image.title}</h3>
|
||||
<p style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14, margin: '8px 0 0 0' }}>
|
||||
<span style={{ marginRight: 20 }}>📅 {image.date}</span>
|
||||
<span>📍 {image.location}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 24, paddingLeft: 24, paddingRight: 24 }} onClick={() => navigate('/competitions')}>
|
||||
立即报名
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
|
||||
{/* 自定义分页指示器 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 100,
|
||||
right: 40,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
}}>
|
||||
{(carousel1Items.length > 0 ? carousel1Items : []).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => carouselRef.current?.goTo(index)}
|
||||
style={{
|
||||
width: currentSlide === index ? 32 : 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
background: currentSlide === index ? '#fff' : 'rgba(255,255,255,0.4)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 赛事中心轮播图 */}
|
||||
{competitions.length > 0 && (
|
||||
<motion.div
|
||||
@@ -360,12 +226,15 @@ const Home = () => {
|
||||
>
|
||||
{competitions.map((competition, index) => (
|
||||
<div key={competition.id}>
|
||||
<div style={{
|
||||
height: 450,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)',
|
||||
}}>
|
||||
<div
|
||||
onClick={() => navigate(`/competitions/${competition.id}`)}
|
||||
style={{
|
||||
height: 600,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)',
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
<img
|
||||
src={getImageUrl(competition.display_cover_image) || competition.cover_image}
|
||||
alt={competition.title}
|
||||
@@ -419,7 +288,7 @@ const Home = () => {
|
||||
<span style={{ marginRight: 20 }}>📅 {competition.start_time?.split('T')[0]} ~ {competition.end_time?.split('T')[0]}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 24, paddingLeft: 24, paddingRight: 24 }} onClick={() => navigate(`/competitions/${competition.id}`)}>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 24, paddingLeft: 24, paddingRight: 24 }} onClick={(e) => { e.stopPropagation(); navigate(`/competitions/${competition.id}`); }}>
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
@@ -528,12 +397,15 @@ const Home = () => {
|
||||
>
|
||||
{activities.map((activity, index) => (
|
||||
<div key={activity.id}>
|
||||
<div style={{
|
||||
height: 450,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)',
|
||||
}}>
|
||||
<div
|
||||
onClick={() => navigate(`/activity/${activity.id}`)}
|
||||
style={{
|
||||
height: 600,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)',
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
<img
|
||||
src={getImageUrl(activity.display_banner_url || activity.banner_url || activity.cover_image)}
|
||||
alt={activity.title}
|
||||
@@ -601,7 +473,7 @@ const Home = () => {
|
||||
{activity.location && <span>📍 {activity.location}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 24, paddingLeft: 24, paddingRight: 24 }} onClick={() => navigate('/activities')}>
|
||||
<Button type="primary" size="large" style={{ borderRadius: 24, paddingLeft: 24, paddingRight: 24 }} onClick={(e) => { e.stopPropagation(); navigate(`/activity/${activity.id}`); }}>
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ const VCCourses = () => {
|
||||
<span style={{ marginRight: 15 }}><ClockCircleOutlined /> {item.duration}</span>
|
||||
<span><BookOutlined /> {item.lesson_count} 课时</span>
|
||||
</div>
|
||||
<p style={{ color: '#aaa', marginBottom: 20, flex: 1 }}>{item.description}</p>
|
||||
<p style={{ color: '#aaa', marginBottom: 20, flex: 1, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 10, WebkitBoxOrient: 'vertical' }}>{item.description}</p>
|
||||
<Button type="primary" block ghost style={{ borderColor: '#00f0ff', color: '#00f0ff' }}>
|
||||
点击报名
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user