diff --git a/backend/Dockerfile b/backend/Dockerfile index f95998a..deb63b5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,7 +14,6 @@ RUN pip install --upgrade pip && pip install -r requirements.txt # 复制项目 COPY . /app/ -COPY .env /app/ # 暴露端口 EXPOSE 8876 diff --git a/backend/competition/serializers.py b/backend/competition/serializers.py index 55f0787..ead08ae 100644 --- a/backend/competition/serializers.py +++ b/backend/competition/serializers.py @@ -1,8 +1,16 @@ from rest_framework import serializers +from django.conf import settings from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment, HomePageConfig, CarouselItem from shop.serializers import WeChatUserSerializer +def _media_url(file_field): + """返回 media 文件的相对路径,避免 build_absolute_uri 生成容器内部地址""" + if file_field and file_field.name: + return settings.MEDIA_URL + file_field.name + return None + + class CarouselItemSerializer(serializers.ModelSerializer): display_image = serializers.SerializerMethodField() @@ -13,9 +21,7 @@ class CarouselItemSerializer(serializers.ModelSerializer): 'order', 'is_active'] def get_display_image(self, obj): - if obj.image: - return obj.image.url - return obj.image_url + return _media_url(obj.image) or obj.image_url class HomePageConfigSerializer(serializers.ModelSerializer): @@ -30,9 +36,7 @@ class HomePageConfigSerializer(serializers.ModelSerializer): 'organizer', 'undertaker', 'carousel1_items', 'carousel2_items'] def get_display_banner(self, obj): - if obj.banner_image: - return obj.banner_image.url - return obj.banner_image_url + 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) @@ -62,9 +66,7 @@ class CompetitionSerializer(serializers.ModelSerializer): 'score_dimensions', 'created_at'] def get_display_cover_image(self, obj): - if obj.cover_image: - return obj.cover_image.url - return obj.cover_image_url + return _media_url(obj.cover_image) or obj.cover_image_url class CompetitionEnrollmentSerializer(serializers.ModelSerializer): @@ -109,9 +111,7 @@ class ProjectSerializer(serializers.ModelSerializer): } def get_display_cover_image(self, obj): - if obj.cover_image: - return obj.cover_image.url - return obj.cover_image_url + return _media_url(obj.cover_image) or obj.cover_image_url class ScoreSerializer(serializers.ModelSerializer): diff --git a/backend/config/settings.py b/backend/config/settings.py index 9869499..13f75bd 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -173,6 +174,15 @@ STATICFILES_DIRS = [ BASE_DIR / 'static', ] +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + # 媒体文件配置 MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' diff --git a/backend/requirements.txt b/backend/requirements.txt index a07d93d..f81ec0e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,6 +23,7 @@ requests django-filter django-admin-sortable2 openpyxl +whitenoise==6.9.0 aliyun-python-sdk-core==2.16.0 aliyun-python-sdk-tingwu==1.0.7 diff --git a/frontend/src/components/competition/CompetitionDetail.jsx b/frontend/src/components/competition/CompetitionDetail.jsx index 49445e6..91200f9 100644 --- a/frontend/src/components/competition/CompetitionDetail.jsx +++ b/frontend/src/components/competition/CompetitionDetail.jsx @@ -22,11 +22,10 @@ import 'github-markdown-css/github-markdown-dark.css'; */ const getImageUrl = (url) => { if (!url) return ''; - if (url.startsWith('http') || url.startsWith('//')) return url; - const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'; - // Remove /api suffix if present to get the root URL for media files - const baseUrl = apiUrl.replace(/\/api\/?$/, ''); - return `${baseUrl}${url}`; + if (url.startsWith('http') || url.startsWith('//')) { + try { return new URL(url).pathname; } catch { return url; } + } + return url; }; const { Title, Paragraph } = Typography; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 2db6fc1..87d78c9 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -3,7 +3,7 @@ import { Card, Row, Col, Tag, Button, Spin, Typography, Carousel } from 'antd'; import { RocketOutlined, RightOutlined, LeftOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { getConfigs, getHomePageConfig, getCompetitions, getActivities } from '../api'; +import { getHomePageConfig, getCompetitions, getActivities } from '../api'; import './Home.css'; const { Title, Paragraph } = Typography; @@ -19,7 +19,7 @@ const getImageUrl = (url) => { const Home = () => { const [products, setProducts] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [typedText, setTypedText] = useState(''); const [isTypingComplete, setIsTypingComplete] = useState(false); const [currentSlide, setCurrentSlide] = useState(0); @@ -38,7 +38,6 @@ const Home = () => { const carouselRef3 = useRef(null); useEffect(() => { - fetchProducts(); fetchHomePageConfig(); fetchCompetitions(); fetchActivities(); @@ -64,17 +63,6 @@ const Home = () => { return () => clearInterval(mainTypingInterval); }, [homeConfig?.main_title]); - const fetchProducts = async () => { - try { - const response = await getConfigs(); - setProducts(response.data); - } catch (error) { - console.error('Failed to fetch products:', error); - } finally { - setLoading(false); - } - }; - const fetchHomePageConfig = async () => { try { const response = await getHomePageConfig();