feat: 移除轮播图管理,系列活动改卡片布局,课程描述限10行,首页配置优化
All checks were successful
Deploy to Server / deploy (push) Successful in 2m4s

This commit is contained in:
爽哒哒
2026-03-22 00:10:34 +08:00
parent b24bba3753
commit 21f892fdf6
11 changed files with 188 additions and 417 deletions

View File

@@ -2,28 +2,20 @@ from django.contrib import admin
from django.utils.html import format_html
from unfold.admin import ModelAdmin
from unfold.decorators import display
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, HomePageConfig, CarouselItem
class CarouselItemInline(admin.TabularInline):
model = CarouselItem
extra = 1
tab = True
fields = ('carousel_type', 'image', 'image_url', 'title', 'subtitle', 'status', 'status_color', 'date', 'location', 'order', 'is_active')
autocomplete_fields = []
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, HomePageConfig
@admin.register(HomePageConfig)
class HomePageConfigAdmin(ModelAdmin):
list_display = ['id', 'main_title', 'carousel1_title', 'carousel2_title', 'organizer', 'undertaker', 'is_active']
list_editable = ['main_title', 'carousel1_title', 'carousel2_title', 'organizer', 'undertaker', 'is_active']
list_display = ['id', 'main_title', 'organizer', 'undertaker', 'is_active']
list_editable = ['main_title', 'organizer', 'undertaker', 'is_active']
fieldsets = (
('首页Banner', {
('封面图', {
'fields': ('banner_image', 'banner_image_url'),
'description': '首页顶部Banner图片,可上传本地图片或填写URL'
'description': '首页标题下方的封面图可上传本地图片或填写URL'
}),
('标题设置', {
'fields': ('main_title', 'carousel1_title', 'carousel2_title')
'fields': ('main_title',)
}),
('主办单位', {
'fields': ('organizer', 'undertaker')
@@ -34,37 +26,6 @@ class HomePageConfigAdmin(ModelAdmin):
)
@admin.register(CarouselItem)
class CarouselItemAdmin(ModelAdmin):
list_display = ['title', 'carousel_type', 'status', 'location', 'order', 'is_active', 'created_at']
list_filter = ['carousel_type', 'status', 'is_active']
search_fields = ['title', 'subtitle', 'location']
readonly_fields = ['image_preview']
fieldsets = (
('轮播图类型', {
'fields': ('carousel_type',)
}),
('图片设置', {
'fields': ('image', 'image_preview', 'image_url'),
'description': '优先使用本地上传的图片,上传后可预览'
}),
('内容设置', {
'fields': ('title', 'subtitle', 'status', 'status_color', 'date', 'location')
}),
('显示设置', {
'fields': ('order', 'is_active')
}),
)
@display(description='图片预览')
def image_preview(self, obj):
if obj.image:
return format_html('<img src="{}" style="max-width: 400px; max-height: 200px; border-radius: 8px;" />', obj.image.url)
elif obj.image_url:
return format_html('<img src="{}" style="max-width: 400px; max-height: 200px; border-radius: 8px;" />', obj.image_url)
return "暂无图片"
class ScoreDimensionInline(admin.TabularInline):
model = ScoreDimension
extra = 1

View File

@@ -0,0 +1,19 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('competition', '0009_alter_carouselitem_id_alter_comment_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='homepageconfig',
name='carousel1_title',
),
migrations.RemoveField(
model_name='homepageconfig',
name='carousel2_title',
),
]

View File

@@ -9,9 +9,6 @@ class HomePageConfig(models.Model):
main_title = models.CharField(max_length=200, default='"创赢未来"云南2026创业大赛', verbose_name="主标题")
carousel1_title = models.CharField(max_length=200, default='"创赢未来"云南2026创业大赛', verbose_name="轮播图1标题")
carousel2_title = models.CharField(max_length=200, default='"七彩云南创业福地"创业主题系列活动', verbose_name="轮播图2标题")
organizer = models.CharField(max_length=200, default='云南省人力资源和社会保障厅', verbose_name="主办单位")
undertaker = models.CharField(max_length=200, default='云南省就业局', verbose_name="承办单位")

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers
from django.conf import settings
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, HomePageConfig, CarouselItem
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, HomePageConfig
from shop.serializers import WeChatUserSerializer
@@ -11,41 +11,16 @@ def _media_url(file_field):
return None
class CarouselItemSerializer(serializers.ModelSerializer):
display_image = serializers.SerializerMethodField()
class Meta:
model = CarouselItem
fields = ['id', 'carousel_type', 'image', 'image_url', 'display_image',
'title', 'subtitle', 'status', 'status_color', 'date', 'location',
'order', 'is_active']
def get_display_image(self, obj):
return _media_url(obj.image) or obj.image_url
class HomePageConfigSerializer(serializers.ModelSerializer):
display_banner = serializers.SerializerMethodField()
carousel1_items = serializers.SerializerMethodField()
carousel2_items = serializers.SerializerMethodField()
class Meta:
model = HomePageConfig
fields = ['id', 'banner_image', 'banner_image_url', 'display_banner',
'main_title', 'carousel1_title', 'carousel2_title',
'organizer', 'undertaker', 'carousel1_items', 'carousel2_items']
'main_title', 'organizer', 'undertaker']
def get_display_banner(self, obj):
return _media_url(obj.banner_image) or obj.banner_image_url
def get_carousel1_items(self, obj):
items = CarouselItem.objects.filter(carousel_type='carousel1', is_active=True)
return CarouselItemSerializer(items, many=True, context=self.context).data
def get_carousel2_items(self, obj):
items = CarouselItem.objects.filter(carousel_type='carousel2', is_active=True)
return CarouselItemSerializer(items, many=True, context=self.context).data
class ScoreDimensionSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -2,7 +2,7 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
CompetitionViewSet, ProjectViewSet, ProjectFileViewSet,
ScoreViewSet, CommentViewSet, CarouselItemViewSet, get_homepage_config
ScoreViewSet, CommentViewSet, get_homepage_config
)
from . import judge_views
@@ -12,7 +12,6 @@ router.register(r'projects', ProjectViewSet, basename='project')
router.register(r'files', ProjectFileViewSet, basename='projectfile')
router.register(r'scores', ScoreViewSet, basename='score')
router.register(r'comments', CommentViewSet, basename='comment')
router.register(r'carousel-items', CarouselItemViewSet, basename='carouselitem')
urlpatterns = [
# 首页配置

View File

@@ -3,12 +3,12 @@ from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response
from django.db.models import Q
from shop.utils import get_current_wechat_user
from .models import Competition, CompetitionEnrollment, Project, ProjectFile, Score, Comment, ScoreDimension, HomePageConfig, CarouselItem
from .models import Competition, CompetitionEnrollment, Project, ProjectFile, Score, Comment, ScoreDimension, HomePageConfig
from .serializers import (
CompetitionSerializer, CompetitionEnrollmentSerializer,
ProjectSerializer, ProjectFileSerializer,
ScoreSerializer, CommentSerializer, ScoreDimensionSerializer,
HomePageConfigSerializer, CarouselItemSerializer
HomePageConfigSerializer
)
from rest_framework.pagination import PageNumberPagination
@@ -28,22 +28,6 @@ def get_homepage_config(request):
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class CarouselItemViewSet(viewsets.ModelViewSet):
"""轮播图项目管理"""
queryset = CarouselItem.objects.all()
serializer_class = CarouselItemSerializer
permission_classes = [permissions.AllowAny]
filter_backends = [filters.SearchFilter]
search_fields = ['title']
def get_queryset(self):
queryset = CarouselItem.objects.all()
carousel_type = self.request.query_params.get('carousel_type')
if carousel_type:
queryset = queryset.filter(carousel_type=carousel_type)
return queryset
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'

View File

@@ -255,11 +255,6 @@ UNFOLD = {
"icon": "home",
"link": reverse_lazy("admin:competition_homepageconfig_changelist"),
},
{
"title": "轮播图管理",
"icon": "image",
"link": reverse_lazy("admin:competition_carouselitem_changelist"),
},
],
},
{

View File

@@ -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,
}}
/>
<Card
hoverable
cover={
<div style={{ height: 280, overflow: 'hidden', position: 'relative' }}>
<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'
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';
}}
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>
<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>
);
};

View File

@@ -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>
{/* 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 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>
{/* 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>
{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>
</motion.div>
);
};

View File

@@ -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,11 +226,14 @@ const Home = () => {
>
{competitions.map((competition, index) => (
<div key={competition.id}>
<div style={{
height: 450,
<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}
@@ -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,11 +397,14 @@ const Home = () => {
>
{activities.map((activity, index) => (
<div key={activity.id}>
<div style={{
height: 450,
<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)}
@@ -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>

View File

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