diff --git a/backend/competition/admin.py b/backend/competition/admin.py index c34079a..31373f0 100644 --- a/backend/competition/admin.py +++ b/backend/competition/admin.py @@ -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('', obj.image.url) - elif obj.image_url: - return format_html('', obj.image_url) - return "暂无图片" - - class ScoreDimensionInline(admin.TabularInline): model = ScoreDimension extra = 1 diff --git a/backend/competition/migrations/0010_remove_carousel_titles.py b/backend/competition/migrations/0010_remove_carousel_titles.py new file mode 100644 index 0000000..57b9556 --- /dev/null +++ b/backend/competition/migrations/0010_remove_carousel_titles.py @@ -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', + ), + ] diff --git a/backend/competition/models.py b/backend/competition/models.py index c3f89bc..9a5b9b5 100644 --- a/backend/competition/models.py +++ b/backend/competition/models.py @@ -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="承办单位") diff --git a/backend/competition/serializers.py b/backend/competition/serializers.py index ead08ae..bf2391e 100644 --- a/backend/competition/serializers.py +++ b/backend/competition/serializers.py @@ -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: diff --git a/backend/competition/urls.py b/backend/competition/urls.py index 60bab79..2553db1 100644 --- a/backend/competition/urls.py +++ b/backend/competition/urls.py @@ -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 = [ # 首页配置 diff --git a/backend/competition/views.py b/backend/competition/views.py index 2d07c6b..8abf7da 100644 --- a/backend/competition/views.py +++ b/backend/competition/views.py @@ -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' diff --git a/backend/config/settings.py b/backend/config/settings.py index afa75a4..a19e6e1 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -255,11 +255,6 @@ UNFOLD = { "icon": "home", "link": reverse_lazy("admin:competition_homepageconfig_changelist"), }, - { - "title": "轮播图管理", - "icon": "image", - "link": reverse_lazy("admin:competition_carouselitem_changelist"), - }, ], }, { diff --git a/frontend/src/components/activity/ActivityCard.jsx b/frontend/src/components/activity/ActivityCard.jsx index 67f1876..0b04e00 100644 --- a/frontend/src/components/activity/ActivityCard.jsx +++ b/frontend/src/components/activity/ActivityCard.jsx @@ -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 ( - -
- {/* Placeholder Background - Always visible behind the image */} -
- - {activity.title} setIsLoaded(true)} - onError={() => { - setHasError(true); - setIsLoaded(true); - }} - loading="lazy" - /> -
-
- {activity.status || getStatus(activity.start_time)} -
-

{activity.title}

-
- - {formatDate(activity.start_time)} + + {activity.title} { + e.target.src = 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=600&h=400&fit=crop'; + }} + /> +
+ + {statusText} +
+ } + style={{ height: '100%', display: 'flex', flexDirection: 'column', fontSize: 16 }} + styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', padding: 24 } }} + onClick={() => navigate(`/activity/${activity.id}`)} + > + + {activity.title} + + +
+ {activity.description || activity.subtitle || ''}
- + + + + + {activity.start_time && ( + + + + {formatDate(activity.start_time)} + {activity.end_time ? ` ~ ${formatDate(activity.end_time)}` : ''} + + + )} + {activity.location && ( + + + {activity.location} + + )} + + ); }; diff --git a/frontend/src/components/activity/ActivityList.jsx b/frontend/src/components/activity/ActivityList.jsx index 3badb43..dd174bc 100644 --- a/frontend/src/components/activity/ActivityList.jsx +++ b/frontend/src/components/activity/ActivityList.jsx @@ -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
Loading activities...
; - if (error) return null; // Or error state - if (!activities || activities.length === 0) return null; + if (isLoading) return ( +
+ +
+ ); + if (error) return ; return ( - -
-

- 近期活动 / EVENTS -

-
- - +
+
+
+ +
- {/* 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" - */} -
- - - - - - -
- {activities.map((_, idx) => ( - setCurrentIndex(idx)} - /> - ))} -
-
- - {/* Mobile: Vertical List/Scroll as requested "Mobile vertical scroll" */} -
- {activities.map((item, index) => ( - - - - ))} -
- + {filtered.length > 0 ? ( + + {filtered.map((activity) => ( + + + + ))} + + ) : ( + + )} +
); }; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 87d78c9..1dac6a0 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -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 = () => { /> - {/* 轮播图 */} - - {/* 轮播图主体 */} -
- setCurrentSlide(next)} - > - {(carousel1Items.length > 0 ? carousel1Items : []).map((image, index) => ( -
-
- {image.title} { - e.target.style.display = 'none'; - }} - /> - {/* 渐变遮罩 */} -
- {/* 标题区域 - 图片上方 */} -
-

- {image.title} -

-

- {image.subtitle} -

-
- {/* 底部信息 */} -
-
- - {image.status} - - {image.location} -
-
-
-

{image.title}

-

- 📅 {image.date} - 📍 {image.location} -

-
- -
-
-
-
- ))} - - - {/* 自定义分页指示器 */} -
- {(carousel1Items.length > 0 ? carousel1Items : []).map((_, index) => ( -
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', - }} - /> - ))} -
-
- - {/* 赛事中心轮播图 */} {competitions.length > 0 && ( { > {competitions.map((competition, index) => (
-
+
navigate(`/competitions/${competition.id}`)} + style={{ + height: 600, + position: 'relative', + overflow: 'hidden', + background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)', + cursor: 'pointer', + }}> {competition.title} { 📅 {competition.start_time?.split('T')[0]} ~ {competition.end_time?.split('T')[0]}

-
@@ -528,12 +397,15 @@ const Home = () => { > {activities.map((activity, index) => (
-
+
navigate(`/activity/${activity.id}`)} + style={{ + height: 600, + position: 'relative', + overflow: 'hidden', + background: 'linear-gradient(135deg, #1890ff 0%, #69c0ff 100%)', + cursor: 'pointer', + }}> {activity.title} { {activity.location && 📍 {activity.location}}

-
diff --git a/frontend/src/pages/VCCourses.jsx b/frontend/src/pages/VCCourses.jsx index 52d1753..04cd206 100644 --- a/frontend/src/pages/VCCourses.jsx +++ b/frontend/src/pages/VCCourses.jsx @@ -78,7 +78,7 @@ const VCCourses = () => { {item.duration} {item.lesson_count} 课时
-

{item.description}

+

{item.description}