From 3ada9969151e6038d860b771546f3d6d71ae6490 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Tue, 10 Mar 2026 12:35:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AF=94=E8=B5=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/competition/views.py | 21 ++++ .../competition/CompetitionCard.jsx | 12 +- .../competition/CompetitionDetail.jsx | 45 ++++++- .../competition/CompetitionList.jsx | 3 +- miniprogram/src/pages/competition/detail.scss | 102 ++++++++++++++++ miniprogram/src/pages/competition/detail.tsx | 111 ++++++++++++++++++ miniprogram/src/pages/competition/index.scss | 85 ++++++++++++++ miniprogram/src/pages/competition/index.tsx | 18 ++- miniprogram/src/pages/user/index.tsx | 2 + 9 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 miniprogram/src/pages/competition/detail.scss create mode 100644 miniprogram/src/pages/competition/detail.tsx create mode 100644 miniprogram/src/pages/competition/index.scss diff --git a/backend/competition/views.py b/backend/competition/views.py index 5a56a48..65f941b 100644 --- a/backend/competition/views.py +++ b/backend/competition/views.py @@ -10,6 +10,13 @@ from .serializers import ( ScoreSerializer, CommentSerializer, ScoreDimensionSerializer ) +from rest_framework.pagination import PageNumberPagination + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 100 + class CompetitionViewSet(viewsets.ReadOnlyModelViewSet): """ 比赛视图集 @@ -17,9 +24,23 @@ class CompetitionViewSet(viewsets.ReadOnlyModelViewSet): queryset = Competition.objects.filter(is_active=True).order_by('-created_at') serializer_class = CompetitionSerializer permission_classes = [permissions.AllowAny] + pagination_class = StandardResultsSetPagination filter_backends = [filters.SearchFilter] search_fields = ['title', 'description'] + def get_queryset(self): + """ + 获取比赛查询集,支持根据查询参数进行动态过滤 + """ + queryset = super().get_queryset() + + # 状态过滤 + status_param = self.request.query_params.get('status') + if status_param and status_param != 'all': + queryset = queryset.filter(status=status_param) + + return queryset + @action(detail=True, methods=['post'], permission_classes=[permissions.AllowAny]) def enroll(self, request, pk=None): """ diff --git a/frontend/src/components/competition/CompetitionCard.jsx b/frontend/src/components/competition/CompetitionCard.jsx index a89b7ef..65814af 100644 --- a/frontend/src/components/competition/CompetitionCard.jsx +++ b/frontend/src/components/competition/CompetitionCard.jsx @@ -4,7 +4,6 @@ import { CalendarOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; const { Title } = Typography; @@ -65,9 +64,16 @@ const CompetitionCard = ({ competition }) => { display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', - marginBottom: 0 + marginBottom: 0, + fontSize: '14px' }}> - + , + }} + > {competition.description} diff --git a/frontend/src/components/competition/CompetitionDetail.jsx b/frontend/src/components/competition/CompetitionDetail.jsx index 021d2b5..1abc7a7 100644 --- a/frontend/src/components/competition/CompetitionDetail.jsx +++ b/frontend/src/components/competition/CompetitionDetail.jsx @@ -134,33 +134,66 @@ const CompetitionDetail = () => { 比赛简介 -
+
, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, + a: (props) => , + blockquote: (props) =>
, + table: (props) => , + th: (props) =>
, + td: (props) => , + }} > {competition.description} 规则说明 -
+
, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, + a: (props) => , + blockquote: (props) =>
, + table: (props) => , + th: (props) =>
, + td: (props) => , + }} > {competition.rule_description} 参赛条件 -
+
, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, + a: (props) => , + blockquote: (props) =>
, + table: (props) => , + th: (props) => + ))} diff --git a/miniprogram/src/pages/competition/detail.scss b/miniprogram/src/pages/competition/detail.scss new file mode 100644 index 0000000..29c4d22 --- /dev/null +++ b/miniprogram/src/pages/competition/detail.scss @@ -0,0 +1,102 @@ +.comp-detail { + background-color: #000; + min-height: 100vh; + padding-bottom: 80px; + + .banner { + width: 100%; + height: 300px; + display: block; + } + + .content { + padding: 24px; + background: #111; + border-radius: 16px 16px 0 0; + margin-top: -24px; + position: relative; + z-index: 10; + + .header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 24px; + + .title { + font-size: 24px; + font-weight: bold; + color: #fff; + line-height: 1.4; + } + + .status { + font-size: 14px; + padding: 4px 8px; + border-radius: 4px; + background: #333; + color: #ccc; + margin-left: 12px; + white-space: nowrap; + + &.registration { background: #07c160; color: #fff; } + &.submission { background: #1890ff; color: #fff; } + &.judging { background: #faad14; color: #fff; } + &.ended { background: #ff4d4f; color: #fff; } + } + } + + .section { + margin-bottom: 32px; + + .section-title { + font-size: 18px; + font-weight: bold; + color: #fff; + margin-bottom: 12px; + display: block; + border-left: 4px solid #00b96b; + padding-left: 12px; + } + + rich-text { + font-size: 16px; + color: #ccc; + line-height: 1.6; + white-space: pre-wrap; + } + } + } + + .footer-action { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #1f1f1f; + padding: 16px 24px; + border-top: 1px solid #333; + z-index: 100; + + .btn { + width: 100%; + height: 48px; + line-height: 48px; + border-radius: 24px; + font-size: 18px; + font-weight: bold; + color: #fff; + background: #00b96b; + border: none; + + &.disabled { + background: #333; + color: #666; + } + + &.enrolled { + background: #1890ff; + } + } + } +} diff --git a/miniprogram/src/pages/competition/detail.tsx b/miniprogram/src/pages/competition/detail.tsx new file mode 100644 index 0000000..8c3b083 --- /dev/null +++ b/miniprogram/src/pages/competition/detail.tsx @@ -0,0 +1,111 @@ +import { View, Text, Button, Image, ScrollView, RichText } from '@tarojs/components' +import Taro, { useLoad } from '@tarojs/taro' +import { useState } from 'react' +import { getCompetitionDetail, enrollCompetition, getMyCompetitionEnrollment } from '../../api' +import './detail.scss' + +export default function CompetitionDetail() { + const [detail, setDetail] = useState(null) + const [enrollment, setEnrollment] = useState(null) + const [loading, setLoading] = useState(false) + + useLoad((options) => { + const { id } = options + if (id) { + fetchDetail(id) + fetchEnrollment(id) + } + }) + + const fetchDetail = async (id) => { + setLoading(true) + try { + const res = await getCompetitionDetail(id) + setDetail(res) + } catch (e) { + Taro.showToast({ title: '加载详情失败', icon: 'none' }) + } finally { + setLoading(false) + } + } + + const fetchEnrollment = async (id) => { + try { + const res = await getMyCompetitionEnrollment(id) + setEnrollment(res) + } catch (e) { + // 没报名则无数据,忽略 + } + } + + const handleEnroll = async () => { + if (!detail) return + try { + await enrollCompetition(detail.id, { role: 'contestant' }) + Taro.showToast({ title: '报名成功', icon: 'success' }) + fetchEnrollment(detail.id) + } catch (e) { + Taro.showToast({ title: e.message || '报名失败', icon: 'none' }) + } + } + + const getStatusText = (status) => { + const map = { + 'registration': '报名中', + 'submission': '作品提交中', + 'judging': '评审中', + 'ended': '已结束', + } + return map[status] || status + } + + if (loading || !detail) return 加载中... + + return ( + + + + + + {detail.title} + {getStatusText(detail.status)} + + + + 简介 + + + + + 规则 + + + + + 参赛条件 + + + + + + {enrollment ? ( + + ) : ( + + )} + + + ) +} diff --git a/miniprogram/src/pages/competition/index.scss b/miniprogram/src/pages/competition/index.scss new file mode 100644 index 0000000..5fb5da8 --- /dev/null +++ b/miniprogram/src/pages/competition/index.scss @@ -0,0 +1,85 @@ +.competition-page { + background-color: #000; + min-height: 100vh; + padding: 20px; + + .comp-list { + .comp-card { + background: #1f1f1f; + border-radius: 12px; + margin-bottom: 24px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + + .cover { + width: 100%; + height: 200px; + display: block; + } + + .info { + padding: 16px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + + .title { + font-size: 18px; + font-weight: bold; + color: #fff; + flex: 1; + margin-right: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .status { + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + background: #333; + color: #ccc; + + &.registration { background: #07c160; color: #fff; } + &.submission { background: #1890ff; color: #fff; } + &.judging { background: #faad14; color: #fff; } + &.ended { background: #ff4d4f; color: #fff; } + } + } + + .desc { + font-size: 14px; + color: #999; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 12px; + } + + .footer { + border-top: 1px solid #333; + padding-top: 12px; + display: flex; + justify-content: space-between; + align-items: center; + + .time { + font-size: 12px; + color: #666; + } + } + } + } + } + + .empty { + text-align: center; + color: #666; + margin-top: 40px; + } +} diff --git a/miniprogram/src/pages/competition/index.tsx b/miniprogram/src/pages/competition/index.tsx index d7d1c35..acfa3f2 100644 --- a/miniprogram/src/pages/competition/index.tsx +++ b/miniprogram/src/pages/competition/index.tsx @@ -7,6 +7,7 @@ import './index.scss' export default function CompetitionList() { const [competitions, setCompetitions] = useState([]) const [loading, setLoading] = useState(false) + const [debugMsg, setDebugMsg] = useState('') useLoad(() => { fetchCompetitions() @@ -14,12 +15,21 @@ export default function CompetitionList() { const fetchCompetitions = async () => { setLoading(true) + setDebugMsg('开始加载...') try { + console.log('Fetching competitions...') const res = await getCompetitions() + console.log('Competitions res:', res) + setDebugMsg(`请求成功: 数量 ${res?.results?.length}`) + if (res && res.results) { setCompetitions(res.results) + } else { + setDebugMsg(`数据格式异常: ${JSON.stringify(res)}`) } } catch (e) { + console.error('Fetch failed:', e) + setDebugMsg(`请求失败: ${e.errMsg || JSON.stringify(e)}`) Taro.showToast({ title: '加载失败', icon: 'none' }) } finally { setLoading(false) @@ -32,6 +42,7 @@ export default function CompetitionList() { const getStatusText = (status) => { const map = { + 'published': '即将开始', 'registration': '报名中', 'submission': '作品提交中', 'judging': '评审中', @@ -66,7 +77,12 @@ export default function CompetitionList() { ))} {!loading && competitions.length === 0 && ( - 暂无比赛 + + 暂无比赛 + + 调试信息: {debugMsg} + + )} diff --git a/miniprogram/src/pages/user/index.tsx b/miniprogram/src/pages/user/index.tsx index 20c2de6..b05d22d 100644 --- a/miniprogram/src/pages/user/index.tsx +++ b/miniprogram/src/pages/user/index.tsx @@ -34,6 +34,7 @@ export default function UserIndex() { const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' }) const goActivityList = (tab = 'all') => Taro.navigateTo({ url: `/subpackages/forum/activity/index?tab=${tab}` }) + const goCompetitionList = () => Taro.navigateTo({ url: '/pages/competition/index' }) const handleAddress = async () => { try { @@ -260,6 +261,7 @@ export default function UserIndex() { { title: '我的订单', icon: '📦', action: goOrders }, { title: '地址管理', icon: '📝', action: handleAddress }, { title: '活动管理', icon: '⌚️', action: () => goActivityList('mine') }, + { title: '赛事中心', icon: '🏆', action: goCompetitionList }, ] }, {
, + td: (props) => , + }} > {competition.condition_description} diff --git a/frontend/src/components/competition/CompetitionList.jsx b/frontend/src/components/competition/CompetitionList.jsx index 86e18e3..3fc51a0 100644 --- a/frontend/src/components/competition/CompetitionList.jsx +++ b/frontend/src/components/competition/CompetitionList.jsx @@ -56,6 +56,7 @@ const CompetitionList = () => { onChange={handleStatusChange} > + @@ -73,7 +74,7 @@ const CompetitionList = () => { {data?.data?.results?.length > 0 ? ( {data.data.results.map((comp) => ( -