import { View, Text, Button, Image, ScrollView, PageContainer } from '@tarojs/components' import Taro, { useLoad, useDidShow } from '@tarojs/taro' import { useState, useEffect } from 'react' import { getCompetitionDetail, enrollCompetition, getMyCompetitionEnrollment, getProjects, getComments } from '../../api' import MarkdownReader from '../../components/MarkdownReader' import './detail.scss' export default function CompetitionDetail() { const [detail, setDetail] = useState(null) const [enrollment, setEnrollment] = useState(null) const [projects, setProjects] = useState([]) const [myProject, setMyProject] = useState(null) const [activeTab, setActiveTab] = useState(0) const [loading, setLoading] = useState(false) const [showComments, setShowComments] = useState(false) const [comments, setComments] = useState([]) useLoad((options) => { const { id } = options if (id) { fetchDetail(id) fetchEnrollment(id) fetchProjects(id) } }) useDidShow(() => { // 每次显示页面时刷新一下我的项目信息(比如从编辑页返回) if (detail?.id) { fetchMyProject(detail.id) } }) const fetchDetail = async (id) => { setLoading(true) try { const res = await getCompetitionDetail(id) setDetail(res) fetchMyProject(id) } catch (e) { Taro.showToast({ title: '加载详情失败', icon: 'none' }) } finally { setLoading(false) } } const fetchMyProject = async (competitionId) => { try { // 获取当前用户的所有项目,然后筛选出当前比赛的 // 或者直接调用 getProjects 并传入 contestant__user=me (如果后端支持) // 目前后端 ProjectViewSet 默认返回:所有submitted + 自己的draft/submitted // 所以我们直接调 getProjects({ competition: competitionId }) 然后在前端找自己的 // 更好的方式:后端 ProjectViewSet 应该已经过滤了,返回列表中如果有一条是自己的,那就是自己的 // 但这里我们还是显式地请求一下,或者在 fetchProjects 的结果里找 const userInfo = Taro.getStorageSync('userInfo') if (!userInfo) return const res = await getProjects({ competition: competitionId }) const list = res.results || res const myProj = list.find((p: any) => p.contestant_info?.nickname === userInfo.nickname) // 这是一个简化的判断,最好用 ID // 由于 API 返回的 contestant_info 没有 user_id,我们可能需要在 project 对象里加一个 is_mine 字段 // 或者,我们可以依赖后端返回的 contestant.user.id 与当前 user.id 比对。 // 但前端拿不到 contestant.user.id (ProjectSerializer 没返回)。 // 既然我们之前做了一个 getMyEnrollments,我们可以通过 enrollment id 来匹配 // 但这里为了简便,我们可以假设 getProjects 返回的数据里,如果 contestant_info 匹配当前用户昵称... 不太靠谱 // 让我们修改 API 或者用另一种方式: // 直接请求 getProjects,带上一个特殊参数 mine=true ? 后端 ProjectViewSet 逻辑比较复杂 // 让我们回顾一下 ProjectViewSet: // q |= Q(contestant__user=user) // 所以返回的列表里肯定包含我的项目。 // 既然我们已经有 enrollment 信息,我们可以用 enrollment.id 来匹配 project.contestant if (enrollment) { const mine = list.find((p: any) => p.contestant === enrollment.id) setMyProject(mine) } else { // 如果 enrollment 还没加载完,先不管,等 enrollment 加载完再匹配? // 或者我们再次 fetchEnrollment 后再 fetchProjects } } catch (e) { console.error(e) } } const fetchEnrollment = async (id) => { try { const res = await getMyCompetitionEnrollment(id) setEnrollment(res) // 获取到 enrollment 后,去匹配 myProject if (projects.length > 0) { const mine = projects.find((p: any) => p.contestant === res.id) setMyProject(mine) } else { // 如果 projects 还没加载,重新加载一次 projects 或者等待 fetchProjects 完成 // 其实 fetchProjects 也在运行,它完成后也会设置 projects } } catch (e) { // 没报名则无数据,忽略 } } const fetchProjects = async (id) => { try { // 注意:这里我们去掉了 status='submitted',因为我们要找自己的 draft const res = await getProjects({ competition: id }) const list = res.results || res const allProjects = Array.isArray(list) ? list : [] // 过滤出 submitted 的给列表显示 const submittedProjects = allProjects.filter(p => p.status === 'submitted') setProjects(submittedProjects) // 尝试找自己的项目 (Draft or Submitted) // 需要 enrollment 信息 // 这里暂时没法直接 setMyProject,因为 enrollment 可能还没回来 // 我们在 useEffect 里监听 enrollment 和 projects 的变化来设置 myProject } catch (e) { console.error('Fetch projects failed', e) } } // 监听变化设置 myProject useEffect(() => { if (enrollment && projects.length >= 0) { // projects could be empty // 重新获取一次所有项目以包含 draft? // 上面的 fetchProjects 已经把 submitted 过滤给 setProjects 了。 // 所以我们需要在 fetchProjects 里就把 allProjects 存下来?或者单独存 myProject // 让我们重构 fetchProjects,专门获取一次“我的项目” fetchMySpecificProject(detail?.id, enrollment.id) } }, [enrollment]) const fetchMySpecificProject = async (compId, enrollId) => { if (!compId || !enrollId) return try { const res = await getProjects({ competition: compId }) const list = res.results || res const mine = list.find((p: any) => p.contestant === enrollId) setMyProject(mine) } catch (e) {} } const fetchComments = async (projectId) => { Taro.showLoading({ title: '加载中' }) try { const res = await getComments({ project: projectId }) const list = res.results || res.data || res || [] setComments(Array.isArray(list) ? list : []) setShowComments(true) } catch (e) { Taro.showToast({ title: '获取评语失败', icon: 'none' }) } finally { Taro.hideLoading() } } 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)} {['详情', '参赛项目', '排行榜'].map((tab, index) => ( setActiveTab(index)} > {tab} ))} {activeTab === 0 && ( <> 简介 规则 参赛条件 )} {activeTab === 1 && ( {projects.map(project => ( {project.title} {project.contestant_info?.nickname || '参赛者'} {project.final_score > 0 && {project.final_score}分} ))} {projects.length === 0 && 暂无参赛项目} )} {activeTab === 2 && ( {projects .filter(p => p.final_score > 0) .sort((a, b) => b.final_score - a.final_score) .map((project, index) => ( {index + 1} {project.contestant_info?.nickname || '参赛者'} {project.title} {project.final_score} ))} {projects.filter(p => p.final_score > 0).length === 0 && 暂无排名数据} )} {enrollment ? ( myProject ? ( ) : ( enrollment.status === 'approved' ? ( ) : ( ) ) ) : ( )} setShowComments(false)} position='bottom' round> 评委评语 {comments.length > 0 ? comments.map((c: any) => ( {c.judge_name || '评委'} {c.created_at?.substring(0, 16)} {c.content} )) : 暂无评语} ) }