比赛
All checks were successful
Deploy to Server / deploy (push) Successful in 36s

This commit is contained in:
jeremygan2021
2026-03-10 12:35:41 +08:00
parent 417cda952d
commit 3ada996915
9 changed files with 388 additions and 11 deletions

View File

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

View File

@@ -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<any>(null)
const [enrollment, setEnrollment] = useState<any>(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 <View className='loading'>...</View>
return (
<ScrollView scrollY className='comp-detail'>
<Image
className='banner'
mode='aspectFill'
src={detail.display_cover_image || 'https://via.placeholder.com/400x200'}
/>
<View className='content'>
<View className='header'>
<Text className='title'>{detail.title}</Text>
<Text className={`status ${detail.status}`}>{getStatusText(detail.status)}</Text>
</View>
<View className='section'>
<Text className='section-title'></Text>
<RichText nodes={detail.description} />
</View>
<View className='section'>
<Text className='section-title'></Text>
<RichText nodes={detail.rule_description} />
</View>
<View className='section'>
<Text className='section-title'></Text>
<RichText nodes={detail.condition_description} />
</View>
</View>
<View className='footer-action'>
{enrollment ? (
<Button disabled className='btn enrolled'>
{enrollment.status === 'approved' ? '已报名' : '审核中'}
</Button>
) : (
<Button
className='btn enroll'
onClick={handleEnroll}
disabled={detail.status !== 'registration'}
>
{detail.status === 'registration' ? '立即报名' : '报名未开始/已结束'}
</Button>
)}
</View>
</ScrollView>
)
}

View File

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

View File

@@ -7,6 +7,7 @@ import './index.scss'
export default function CompetitionList() {
const [competitions, setCompetitions] = useState<any[]>([])
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() {
</View>
))}
{!loading && competitions.length === 0 && (
<View className='empty'></View>
<View className='empty'>
<Text></Text>
<View style={{ marginTop: 20, color: '#666', fontSize: 12, wordBreak: 'break-all', padding: 20 }}>
: {debugMsg}
</View>
</View>
)}
</ScrollView>
</View>

View File

@@ -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 },
]
},
{