938 lines
26 KiB
Vue
938 lines
26 KiB
Vue
<template>
|
||
<div class="detail-page" v-if="activity">
|
||
<!-- 活动头部 -->
|
||
<div class="detail-hero">
|
||
<img v-if="activity.coverUrl" :src="activity.coverUrl" class="hero-cover" />
|
||
<div v-else class="hero-placeholder">
|
||
<span>{{ activity.contestName?.charAt(0) }}</span>
|
||
</div>
|
||
<div class="hero-overlay">
|
||
<a-button shape="round" size="small" @click="$router.back()" class="back-btn">
|
||
<arrow-left-outlined /> 返回
|
||
</a-button>
|
||
<div class="hero-badge">{{ stageLabel }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 活动信息 -->
|
||
<div class="detail-card">
|
||
<h1 class="detail-title">{{ activity.contestName }}</h1>
|
||
<div class="detail-meta">
|
||
<div class="meta-row">
|
||
<calendar-outlined />
|
||
<span>{{ formatDate(activity.startTime) }} - {{ formatDate(activity.endTime) }}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<tag-outlined />
|
||
<span>{{ activity.contestType === 'individual' ? '个人参与' : '团队参与' }}</span>
|
||
</div>
|
||
<div class="meta-row" v-if="activity.registerStartTime">
|
||
<clock-circle-outlined />
|
||
<span>报名:{{ formatDate(activity.registerStartTime) }} - {{ formatDate(activity.registerEndTime) }}</span>
|
||
</div>
|
||
<div class="meta-row" v-if="activity.ageMin || activity.ageMax">
|
||
<team-outlined />
|
||
<span>年龄要求:{{ activity.ageMin || 0 }} - {{ activity.ageMax || '不限' }} 岁</span>
|
||
</div>
|
||
<div class="meta-row" v-if="activity.targetCities?.length">
|
||
<environment-outlined />
|
||
<span>面向城市:{{ (activity.targetCities as string[]).join('、') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮(根据阶段动态展示) -->
|
||
<div class="action-area">
|
||
<!-- 报名阶段 -->
|
||
<template v-if="currentStage === 'register'">
|
||
<a-button v-if="!isLoggedIn" type="primary" size="large" block class="action-btn" @click="goLogin">
|
||
登录后报名
|
||
</a-button>
|
||
<a-button v-else-if="!hasRegistered" type="primary" size="large" block class="action-btn" @click="showRegisterModal = true">
|
||
立即报名
|
||
</a-button>
|
||
<a-button v-else-if="registrationState === 'pending'" size="large" block class="action-btn-info">
|
||
<hourglass-outlined /> 报名审核中
|
||
</a-button>
|
||
<a-button v-else-if="registrationState === 'rejected'" size="large" block class="action-btn-disabled">
|
||
<close-circle-outlined /> 报名未通过
|
||
</a-button>
|
||
<a-button v-else size="large" block class="action-btn-done">
|
||
<check-circle-outlined /> 已报名
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 提交阶段 -->
|
||
<template v-else-if="currentStage === 'submit'">
|
||
<a-button v-if="!isLoggedIn" type="primary" size="large" block class="action-btn" @click="goLogin">
|
||
登录后查看作品
|
||
</a-button>
|
||
<a-button v-else-if="!hasRegistered && isRegisterOpen" type="primary" size="large" block class="action-btn" @click="showRegisterModal = true">
|
||
立即报名
|
||
</a-button>
|
||
<a-button v-else-if="!hasRegistered" size="large" block disabled class="action-btn-disabled">
|
||
报名已截止
|
||
</a-button>
|
||
<a-button v-else-if="registrationState === 'pending'" size="large" block class="action-btn-info">
|
||
<hourglass-outlined /> 报名审核中,通过后可提交作品
|
||
</a-button>
|
||
<a-button v-else-if="registrationState === 'rejected'" size="large" block disabled class="action-btn-disabled">
|
||
<close-circle-outlined /> 报名未通过,无法提交作品
|
||
</a-button>
|
||
<a-button v-else-if="!hasSubmittedWork" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
||
<picture-outlined /> 从作品库选择
|
||
</a-button>
|
||
<a-button v-else-if="canResubmit" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
||
<picture-outlined /> 重新提交
|
||
</a-button>
|
||
<a-button v-else size="large" block class="action-btn-done">
|
||
<check-circle-outlined /> 作品已提交
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 评审阶段 -->
|
||
<template v-else-if="currentStage === 'review'">
|
||
<a-button v-if="hasRegistered" size="large" block class="action-btn-info">
|
||
<hourglass-outlined /> 您已报名,评审中请耐心等待
|
||
</a-button>
|
||
<a-button v-else size="large" block disabled class="action-btn-disabled">
|
||
报名已结束
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 已结束 -->
|
||
<template v-else-if="currentStage === 'finished'">
|
||
<a-button type="primary" size="large" block class="action-btn" @click="activeTab = 'results'">
|
||
查看成果
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 未开始 -->
|
||
<template v-else>
|
||
<a-button size="large" block disabled class="action-btn-disabled">
|
||
活动即将开始
|
||
</a-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab 内容 -->
|
||
<div class="detail-card">
|
||
<a-tabs v-model:activeKey="activeTab">
|
||
<a-tab-pane key="info" tab="活动详情">
|
||
<div class="rich-content" v-html="activityContentHtml"></div>
|
||
<!-- 附件 -->
|
||
<div v-if="activity.attachments?.length" class="attachments">
|
||
<h4>相关附件</h4>
|
||
<div v-for="att in activity.attachments" :key="att.id" class="att-item">
|
||
<a :href="att.fileUrl" target="_blank">
|
||
<paper-clip-outlined /> {{ att.fileName }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
<a-tab-pane key="results" tab="活动成果" v-if="activity.resultState === 'published'">
|
||
<div class="results-panel">
|
||
<div class="results-hero">
|
||
<trophy-outlined class="results-icon" />
|
||
<p class="results-hero-text">活动成果已发布</p>
|
||
</div>
|
||
<p v-if="activity.resultPublishTime" class="results-publish-time">
|
||
发布时间:{{ formatDateTime(activity.resultPublishTime) }}
|
||
</p>
|
||
<p class="results-hint-line">以下排名与得分以主办方发布为准。</p>
|
||
<a-spin :spinning="resultsLoading">
|
||
<template v-if="resultsList.length > 0 || resultsLoading">
|
||
<div class="results-cards">
|
||
<div
|
||
v-for="record in resultsList"
|
||
:key="record.id"
|
||
class="result-card"
|
||
:class="{
|
||
'result-card--top1': record.rank === 1,
|
||
'result-card--top2': record.rank === 2,
|
||
'result-card--top3': record.rank === 3,
|
||
}"
|
||
>
|
||
<div class="result-card__rank">
|
||
<span v-if="record.rank != null" class="rank-pill">{{ record.rank }}</span>
|
||
<span v-else class="rank-pill rank-pill--muted">-</span>
|
||
</div>
|
||
<div class="result-card__body">
|
||
<div class="result-card__title-row">
|
||
<span class="result-card__name">{{ record.participantName || '-' }}</span>
|
||
<a-tag v-if="record.awardName" color="gold" class="result-card__award">
|
||
{{ record.awardName }}
|
||
</a-tag>
|
||
</div>
|
||
<div class="result-card__meta">
|
||
<span class="result-card__meta-item">
|
||
<span class="meta-label">作品编号</span>
|
||
{{ record.workNo || '-' }}
|
||
</span>
|
||
<span class="result-card__meta-item">
|
||
<span class="meta-label">得分</span>
|
||
<span v-if="record.finalScore != null" class="score-text">
|
||
{{ Number(record.finalScore).toFixed(2) }}
|
||
</span>
|
||
<span v-else class="text-muted">-</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="resultsTotal > resultsPageSize" class="results-pagination-wrap">
|
||
<a-pagination
|
||
:current="resultsPage"
|
||
:total="resultsTotal"
|
||
:page-size="resultsPageSize"
|
||
:show-size-changer="false"
|
||
show-less-items
|
||
:show-total="(t: number) => `共 ${t} 条`"
|
||
@change="onResultsPageChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<div v-else class="empty-tab">
|
||
<a-empty description="暂无公示信息" />
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</a-tab-pane>
|
||
<a-tab-pane key="notices" tab="活动公告">
|
||
<div v-if="!activity.notices?.length" class="empty-tab">
|
||
<a-empty description="暂无公告" />
|
||
</div>
|
||
<div v-else>
|
||
<div v-for="notice in activity.notices" :key="notice.id" class="notice-item">
|
||
<h4>{{ notice.title }}</h4>
|
||
<div class="notice-content" v-html="notice.content"></div>
|
||
<span class="notice-time">{{ formatNoticeTime(notice) }}</span>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
|
||
<!-- 报名弹窗 -->
|
||
<a-modal
|
||
v-model:open="showRegisterModal"
|
||
title="活动报名"
|
||
:footer="null"
|
||
:width="420"
|
||
>
|
||
<div class="register-modal">
|
||
<template v-if="isChildUser">
|
||
<p class="modal-desc">将使用当前账号报名,确认参加本活动吗?</p>
|
||
</template>
|
||
<template v-else>
|
||
<p class="modal-desc">请选择参与者:</p>
|
||
<a-radio-group v-model:value="participantForm.participantType" class="participant-options">
|
||
<div class="participant-option">
|
||
<a-radio value="self">我自己</a-radio>
|
||
</div>
|
||
<template v-if="children.length">
|
||
<div v-for="child in children" :key="child.id" class="participant-option">
|
||
<a-radio :value="'child_' + child.id">
|
||
子女:{{ child.name }}
|
||
<span class="child-detail" v-if="child.grade">({{ child.grade }})</span>
|
||
</a-radio>
|
||
</div>
|
||
</template>
|
||
</a-radio-group>
|
||
<a-button type="link" @click="$router.push('/p/mine/children')" class="add-child-link">
|
||
+ 添加新的子女
|
||
</a-button>
|
||
</template>
|
||
<a-button
|
||
type="primary"
|
||
block
|
||
size="large"
|
||
:loading="registering"
|
||
@click="handleRegister"
|
||
class="confirm-btn"
|
||
>
|
||
确认报名
|
||
</a-button>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 作品选择器弹窗 -->
|
||
<WorkSelector
|
||
v-model:open="showWorkSelector"
|
||
:redirect-url="route.fullPath"
|
||
@select="handleWorkSelected"
|
||
/>
|
||
</div>
|
||
|
||
<div v-else class="loading-page">
|
||
<a-spin size="large" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { message } from 'ant-design-vue'
|
||
import {
|
||
ArrowLeftOutlined, CalendarOutlined, TagOutlined,
|
||
ClockCircleOutlined, PaperClipOutlined, CheckCircleOutlined,
|
||
PictureOutlined, HourglassOutlined, TrophyOutlined,
|
||
TeamOutlined, EnvironmentOutlined, CloseCircleOutlined,
|
||
} from '@ant-design/icons-vue'
|
||
import {
|
||
publicActivitiesApi,
|
||
publicChildrenApi,
|
||
type PublicActivityDetail,
|
||
type PublicActivityNotice,
|
||
type PublicActivityResultItem,
|
||
type UserWork,
|
||
} from '@/api/public'
|
||
import WorkSelector from './components/WorkSelector.vue'
|
||
import dayjs from 'dayjs'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const activity = ref<PublicActivityDetail | null>(null)
|
||
const activeTab = ref('info')
|
||
const children = ref<any[]>([])
|
||
const showRegisterModal = ref(false)
|
||
const registering = ref(false)
|
||
const hasRegistered = ref(false)
|
||
const registrationState = ref('')
|
||
const myRegistration = ref<any>(null)
|
||
const hasSubmittedWork = ref(false)
|
||
|
||
// 作品提交
|
||
const showWorkSelector = ref(false)
|
||
const submittingWork = ref(false)
|
||
|
||
const participantForm = ref({
|
||
participantType: 'self',
|
||
})
|
||
|
||
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
||
|
||
const formatDateTime = (d: string) => dayjs(d).format('YYYY-MM-DD HH:mm')
|
||
|
||
/** 公示成果列表 */
|
||
const resultsLoading = ref(false)
|
||
const resultsList = ref<PublicActivityResultItem[]>([])
|
||
const resultsTotal = ref(0)
|
||
const resultsPage = ref(1)
|
||
const resultsPageSize = ref(10)
|
||
|
||
|
||
const loadPublicResults = async (page = 1) => {
|
||
if (!activity.value?.id) return
|
||
resultsLoading.value = true
|
||
try {
|
||
const res = await publicActivitiesApi.getPublishedResults(activity.value.id, {
|
||
page,
|
||
pageSize: resultsPageSize.value,
|
||
})
|
||
resultsList.value = res.list ?? []
|
||
resultsTotal.value = Number(res.total ?? 0)
|
||
resultsPage.value = Number(res.page ?? page)
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '加载成果列表失败')
|
||
resultsList.value = []
|
||
resultsTotal.value = 0
|
||
} finally {
|
||
resultsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const onResultsPageChange = (page: number) => {
|
||
void loadPublicResults(page)
|
||
}
|
||
|
||
watch(activeTab, (k) => {
|
||
if (k === 'results' && activity.value?.resultState === 'published') {
|
||
void loadPublicResults(1)
|
||
}
|
||
})
|
||
|
||
const formatNoticeTime = (n: PublicActivityNotice) =>
|
||
formatDate(n.publishTime || n.createTime || '')
|
||
|
||
/** 活动详情富文本:后端字段为 content */
|
||
const activityContentHtml = computed(() => {
|
||
const a = activity.value
|
||
if (!a) return ''
|
||
return a.content || a.description || ''
|
||
})
|
||
|
||
/** 子女账号:直接报名,不选参与者、不添加子女 */
|
||
const isChildUser = computed(() => {
|
||
const raw = localStorage.getItem('public_user')
|
||
if (!raw) return false
|
||
try {
|
||
return JSON.parse(raw).userType === 'child'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
|
||
|
||
// 报名是否仍在开放中
|
||
const isRegisterOpen = computed(() => {
|
||
if (!activity.value) return false
|
||
const now = dayjs()
|
||
return !now.isBefore(activity.value.registerStartTime) && now.isBefore(activity.value.registerEndTime)
|
||
})
|
||
|
||
// 是否允许重新提交(submitRule === 'resubmit' 且已提交过作品)
|
||
const canResubmit = computed(() => {
|
||
return hasSubmittedWork.value && activity.value?.submitRule === 'resubmit'
|
||
})
|
||
|
||
// 活动当前阶段
|
||
const currentStage = computed(() => {
|
||
if (!activity.value) return 'pending'
|
||
const now = dayjs()
|
||
const a = activity.value
|
||
if (now.isBefore(a.registerStartTime)) return 'pending'
|
||
// 提交阶段优先:如果到了提交时间且在提交截止前,优先显示提交(报名与提交可能重叠)
|
||
if (a.submitStartTime && !now.isBefore(a.submitStartTime) && a.submitEndTime && now.isBefore(a.submitEndTime)) return 'submit'
|
||
if (now.isBefore(a.registerEndTime)) return 'register'
|
||
if (a.reviewStartTime && now.isBefore(a.reviewEndTime)) return 'review'
|
||
if (a.status === 'finished' || a.resultState === 'published') return 'finished'
|
||
return 'review'
|
||
})
|
||
|
||
const stageLabel = computed(() => {
|
||
const map: Record<string, string> = {
|
||
pending: '即将开始', register: '报名中', submit: '提交中',
|
||
review: '评审中', finished: '已结束',
|
||
}
|
||
return map[currentStage.value] || '进行中'
|
||
})
|
||
|
||
const goLogin = () => router.push({ path: '/p/login', query: { redirect: route.fullPath } })
|
||
|
||
const fetchDetail = async () => {
|
||
const id = Number(route.params.id)
|
||
try {
|
||
activity.value = await publicActivitiesApi.detail(id)
|
||
} catch {
|
||
message.error('活动不存在')
|
||
router.push('/p/activities')
|
||
}
|
||
}
|
||
|
||
const fetchChildren = async () => {
|
||
if (!isLoggedIn.value) return
|
||
try {
|
||
children.value = await publicChildrenApi.list()
|
||
} catch { /* ignore */ }
|
||
}
|
||
|
||
// 检查报名状态和作品提交状态
|
||
const checkRegistrationStatus = async () => {
|
||
if (!isLoggedIn.value || !activity.value) return
|
||
try {
|
||
const reg = await publicActivitiesApi.getMyRegistration(activity.value.id)
|
||
// 只有当 reg 存在且有 id 时才认为已报名
|
||
if (reg && reg.id) {
|
||
hasRegistered.value = true
|
||
registrationState.value = reg.registrationState || ''
|
||
myRegistration.value = reg
|
||
// 检查是否已提交作品
|
||
hasSubmittedWork.value = reg.hasSubmittedWork || false
|
||
}
|
||
} catch (e) {
|
||
// 未报名或查询失败,保持 hasRegistered = false
|
||
console.log('查询报名状态失败或未报名')
|
||
}
|
||
}
|
||
|
||
// 打开作品选择器
|
||
const openSubmitWork = () => {
|
||
if (!isLoggedIn.value) { goLogin(); return }
|
||
showWorkSelector.value = true
|
||
}
|
||
|
||
// 从作品库选择作品后提交
|
||
const handleWorkSelected = async (work: UserWork) => {
|
||
if (!myRegistration.value) {
|
||
message.error('请先报名活动')
|
||
return
|
||
}
|
||
submittingWork.value = true
|
||
try {
|
||
await publicActivitiesApi.submitWork(activity.value.id, {
|
||
registrationId: myRegistration.value.id,
|
||
userWorkId: work.id,
|
||
})
|
||
message.success('作品提交成功!')
|
||
showWorkSelector.value = false
|
||
hasSubmittedWork.value = true
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '提交失败')
|
||
} finally {
|
||
submittingWork.value = false
|
||
}
|
||
}
|
||
|
||
const handleRegister = async () => {
|
||
if (!isLoggedIn.value) {
|
||
router.push({ path: '/p/login', query: { redirect: route.fullPath } })
|
||
return
|
||
}
|
||
if (!activity.value) return
|
||
|
||
registering.value = true
|
||
try {
|
||
if (isChildUser.value) {
|
||
await publicActivitiesApi.register(activity.value.id, { participantType: 'self' })
|
||
} else {
|
||
const val = participantForm.value.participantType
|
||
const isChild = val.startsWith('child_')
|
||
await publicActivitiesApi.register(activity.value.id, {
|
||
participantType: isChild ? 'child' : 'self',
|
||
childId: isChild ? parseInt(val.replace('child_', '')) : undefined,
|
||
})
|
||
}
|
||
message.success('报名成功!')
|
||
showRegisterModal.value = false
|
||
hasRegistered.value = true
|
||
// 重新查询报名状态以获取准确的 registrationState
|
||
await checkRegistrationStatus()
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '报名失败')
|
||
} finally {
|
||
registering.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await fetchDetail()
|
||
await fetchChildren()
|
||
await checkRegistrationStatus()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
$primary: #6366f1;
|
||
|
||
.loading-page {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 100px 0;
|
||
}
|
||
|
||
.detail-hero {
|
||
position: relative;
|
||
height: 220px;
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
margin-bottom: 16px;
|
||
|
||
.hero-cover {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.hero-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #c7d2fe, #fbcfe8, #a7f3d0);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
span {
|
||
font-size: 60px;
|
||
font-weight: 800;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
}
|
||
|
||
.hero-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.3) 100%);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
padding: 16px;
|
||
}
|
||
|
||
.back-btn {
|
||
background: rgba(255,255,255,0.2);
|
||
backdrop-filter: blur(8px);
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
color: #fff;
|
||
}
|
||
|
||
.hero-badge {
|
||
padding: 4px 16px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, $primary, #ec4899);
|
||
}
|
||
}
|
||
|
||
.detail-card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid rgba($primary, 0.06);
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: #1e1b4b;
|
||
margin: 0 0 16px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.detail-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
|
||
.meta-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
}
|
||
}
|
||
|
||
.action-area {
|
||
.action-btn {
|
||
height: 48px !important;
|
||
border-radius: 14px !important;
|
||
font-size: 16px !important;
|
||
font-weight: 700 !important;
|
||
background: linear-gradient(135deg, $primary, #ec4899) !important;
|
||
border: none !important;
|
||
box-shadow: 0 4px 16px rgba($primary, 0.3);
|
||
}
|
||
|
||
.action-btn-done {
|
||
height: 48px !important;
|
||
border-radius: 14px !important;
|
||
font-size: 15px !important;
|
||
font-weight: 600 !important;
|
||
background: #ecfdf5 !important;
|
||
color: #059669 !important;
|
||
border: 1px solid #a7f3d0 !important;
|
||
}
|
||
|
||
.action-btn-info {
|
||
height: 48px !important;
|
||
border-radius: 14px !important;
|
||
font-size: 15px !important;
|
||
font-weight: 600 !important;
|
||
background: #eef2ff !important;
|
||
color: $primary !important;
|
||
border: 1px solid #c7d2fe !important;
|
||
}
|
||
|
||
.action-btn-disabled {
|
||
height: 48px !important;
|
||
border-radius: 14px !important;
|
||
font-size: 15px !important;
|
||
font-weight: 600 !important;
|
||
background: #f3f4f6 !important;
|
||
color: #9ca3af !important;
|
||
border: 1px solid #e5e7eb !important;
|
||
}
|
||
}
|
||
|
||
.results-panel {
|
||
.results-hero {
|
||
text-align: center;
|
||
padding: 8px 0 16px;
|
||
|
||
.results-icon {
|
||
font-size: 40px;
|
||
color: #f59e0b;
|
||
margin-bottom: 8px;
|
||
display: block;
|
||
}
|
||
|
||
.results-hero-text {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.results-publish-time {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
margin: 0 0 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.results-hint-line {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin: 0 0 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.results-cards {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.result-card {
|
||
display: flex;
|
||
gap: 14px;
|
||
align-items: stretch;
|
||
padding: 14px 16px;
|
||
background: #faf9fe;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba($primary, 0.12);
|
||
transition: box-shadow 0.2s, border-color 0.2s;
|
||
|
||
&:hover {
|
||
border-color: rgba($primary, 0.22);
|
||
box-shadow: 0 4px 14px rgba($primary, 0.08);
|
||
}
|
||
|
||
&--top1 {
|
||
border-color: rgba(234, 179, 8, 0.45);
|
||
background: linear-gradient(135deg, #fffbeb 0%, #faf9fe 100%);
|
||
}
|
||
|
||
&--top2 {
|
||
border-color: rgba(148, 163, 184, 0.5);
|
||
background: linear-gradient(135deg, #f8fafc 0%, #faf9fe 100%);
|
||
}
|
||
|
||
&--top3 {
|
||
border-color: rgba(217, 119, 6, 0.35);
|
||
background: linear-gradient(135deg, #fff7ed 0%, #faf9fe 100%);
|
||
}
|
||
}
|
||
|
||
.result-card__rank {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.result-card__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.result-card__title-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.result-card__name {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #1e1b4b;
|
||
line-height: 1.4;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.result-card__award {
|
||
flex-shrink: 0;
|
||
margin: 0 !important;
|
||
}
|
||
|
||
.result-card__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px 20px;
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.result-card__meta-item {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
}
|
||
|
||
.meta-label {
|
||
color: #9ca3af;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.results-pagination-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
padding-top: 4px;
|
||
}
|
||
|
||
.rank-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 28px;
|
||
padding: 2px 8px;
|
||
border-radius: 8px;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
background: linear-gradient(135deg, #eef2ff, #fce7f3);
|
||
color: #4f46e5;
|
||
|
||
&--muted {
|
||
background: #f3f4f6;
|
||
color: #9ca3af;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.score-text {
|
||
font-variant-numeric: tabular-nums;
|
||
font-weight: 600;
|
||
color: #1e1b4b;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
|
||
.rich-content {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #374151;
|
||
|
||
:deep(img) {
|
||
max-width: 100%;
|
||
border-radius: 8px;
|
||
}
|
||
}
|
||
|
||
.attachments {
|
||
margin-top: 20px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0ecf9;
|
||
|
||
h4 {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
.att-item a {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: $primary;
|
||
padding: 6px 0;
|
||
}
|
||
}
|
||
|
||
.notice-item {
|
||
padding: 16px;
|
||
background: #faf9fe;
|
||
border-radius: 12px;
|
||
margin-bottom: 12px;
|
||
|
||
h4 {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1e1b4b;
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
.notice-content {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.notice-time {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin-top: 8px;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.empty-tab {
|
||
padding: 40px 0;
|
||
}
|
||
|
||
// 报名弹窗
|
||
.register-modal {
|
||
.modal-desc {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
margin: 0 0 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.participant-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.participant-option {
|
||
padding: 12px 16px;
|
||
background: #faf9fe;
|
||
border-radius: 12px;
|
||
transition: background 0.2s;
|
||
|
||
&:hover {
|
||
background: #eef2ff;
|
||
}
|
||
|
||
.child-detail {
|
||
color: #9ca3af;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.add-child-link {
|
||
margin: 12px 0;
|
||
padding: 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.confirm-btn {
|
||
margin-top: 8px;
|
||
height: 44px !important;
|
||
border-radius: 12px !important;
|
||
font-weight: 600 !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.detail-hero {
|
||
height: 180px;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.detail-card {
|
||
padding: 18px;
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
</style>
|