2026-03-27 22:20:25 +08:00
|
|
|
|
<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>
|
2026-04-07 14:11:59 +08:00
|
|
|
|
<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>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<a-button v-else size="large" block class="action-btn-done">
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<check-circle-outlined /> 已报名
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 提交阶段 -->
|
|
|
|
|
|
<template v-else-if="currentStage === 'submit'">
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<a-button v-if="!isLoggedIn" type="primary" size="large" block class="action-btn" @click="goLogin">
|
|
|
|
|
|
登录后查看作品
|
|
|
|
|
|
</a-button>
|
2026-04-03 20:29:28 +08:00
|
|
|
|
<a-button v-else-if="!hasRegistered && isRegisterOpen" type="primary" size="large" block class="action-btn" @click="showRegisterModal = true">
|
|
|
|
|
|
立即报名
|
|
|
|
|
|
</a-button>
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<a-button v-else-if="!hasRegistered" size="large" block disabled class="action-btn-disabled">
|
2026-04-03 20:29:28 +08:00
|
|
|
|
报名已截止
|
2026-04-03 15:59:54 +08:00
|
|
|
|
</a-button>
|
2026-04-07 14:11:59 +08:00
|
|
|
|
<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>
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<a-button v-else-if="!hasSubmittedWork" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
2026-04-07 14:11:59 +08:00
|
|
|
|
<picture-outlined /> 从作品库选择
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</a-button>
|
2026-04-03 20:29:28 +08:00
|
|
|
|
<a-button v-else-if="canResubmit" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
2026-04-07 14:11:59 +08:00
|
|
|
|
<picture-outlined /> 重新提交
|
2026-04-03 20:29:28 +08:00
|
|
|
|
</a-button>
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<a-button v-else size="large" block class="action-btn-done">
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<check-circle-outlined /> 作品已提交
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 评审阶段 -->
|
|
|
|
|
|
<template v-else-if="currentStage === 'review'">
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<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">
|
|
|
|
|
|
报名已结束
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</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>
|
2026-04-03 15:59:54 +08:00
|
|
|
|
<a-button size="large" block disabled class="action-btn-disabled">
|
2026-03-27 22:20:25 +08:00
|
|
|
|
活动即将开始
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Tab 内容 -->
|
|
|
|
|
|
<div class="detail-card">
|
|
|
|
|
|
<a-tabs v-model:activeKey="activeTab">
|
|
|
|
|
|
<a-tab-pane key="info" tab="活动详情">
|
2026-04-08 16:00:59 +08:00
|
|
|
|
<div class="rich-content" v-html="activityContentHtml"></div>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<!-- 附件 -->
|
|
|
|
|
|
<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'">
|
2026-04-08 16:31:48 +08:00
|
|
|
|
<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>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</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>
|
2026-04-08 16:00:59 +08:00
|
|
|
|
<span class="notice-time">{{ formatNoticeTime(notice) }}</span>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 报名弹窗 -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="showRegisterModal"
|
|
|
|
|
|
title="活动报名"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:width="420"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="register-modal">
|
2026-04-08 16:00:59 +08:00
|
|
|
|
<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>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
2026-04-08 16:00:59 +08:00
|
|
|
|
<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>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
block
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:loading="registering"
|
|
|
|
|
|
@click="handleRegister"
|
|
|
|
|
|
class="confirm-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
确认报名
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
2026-04-07 14:11:59 +08:00
|
|
|
|
<!-- 作品选择器弹窗 -->
|
|
|
|
|
|
<WorkSelector
|
|
|
|
|
|
v-model:open="showWorkSelector"
|
|
|
|
|
|
:redirect-url="route.fullPath"
|
|
|
|
|
|
@select="handleWorkSelected"
|
|
|
|
|
|
/>
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="loading-page">
|
|
|
|
|
|
<a-spin size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-08 16:31:48 +08:00
|
|
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
2026-03-27 22:20:25 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined, CalendarOutlined, TagOutlined,
|
|
|
|
|
|
ClockCircleOutlined, PaperClipOutlined, CheckCircleOutlined,
|
2026-04-07 14:11:59 +08:00
|
|
|
|
PictureOutlined, HourglassOutlined, TrophyOutlined,
|
|
|
|
|
|
TeamOutlined, EnvironmentOutlined, CloseCircleOutlined,
|
2026-03-27 22:20:25 +08:00
|
|
|
|
} from '@ant-design/icons-vue'
|
2026-04-08 16:00:59 +08:00
|
|
|
|
import {
|
|
|
|
|
|
publicActivitiesApi,
|
|
|
|
|
|
publicChildrenApi,
|
|
|
|
|
|
type PublicActivityDetail,
|
|
|
|
|
|
type PublicActivityNotice,
|
2026-04-08 16:31:48 +08:00
|
|
|
|
type PublicActivityResultItem,
|
2026-04-08 16:00:59 +08:00
|
|
|
|
type UserWork,
|
|
|
|
|
|
} from '@/api/public'
|
2026-04-07 14:11:59 +08:00
|
|
|
|
import WorkSelector from './components/WorkSelector.vue'
|
2026-03-27 22:20:25 +08:00
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-04-08 16:00:59 +08:00
|
|
|
|
const activity = ref<PublicActivityDetail | null>(null)
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const activeTab = ref('info')
|
|
|
|
|
|
const children = ref<any[]>([])
|
|
|
|
|
|
const showRegisterModal = ref(false)
|
|
|
|
|
|
const registering = ref(false)
|
|
|
|
|
|
const hasRegistered = ref(false)
|
2026-04-07 14:11:59 +08:00
|
|
|
|
const registrationState = ref('')
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const myRegistration = ref<any>(null)
|
|
|
|
|
|
const hasSubmittedWork = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 作品提交
|
2026-04-07 14:11:59 +08:00
|
|
|
|
const showWorkSelector = ref(false)
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const submittingWork = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const participantForm = ref({
|
|
|
|
|
|
participantType: 'self',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
|
|
|
|
|
|
2026-04-08 16:31:48 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-08 16:00:59 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
|
|
|
|
|
|
|
2026-04-03 20:29:28 +08:00
|
|
|
|
// 报名是否仍在开放中
|
|
|
|
|
|
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'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
// 活动当前阶段
|
|
|
|
|
|
const currentStage = computed(() => {
|
|
|
|
|
|
if (!activity.value) return 'pending'
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const a = activity.value
|
|
|
|
|
|
if (now.isBefore(a.registerStartTime)) return 'pending'
|
2026-04-03 20:29:28 +08:00
|
|
|
|
// 提交阶段优先:如果到了提交时间且在提交截止前,优先显示提交(报名与提交可能重叠)
|
|
|
|
|
|
if (a.submitStartTime && !now.isBefore(a.submitStartTime) && a.submitEndTime && now.isBefore(a.submitEndTime)) return 'submit'
|
2026-03-27 22:20:25 +08:00
|
|
|
|
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)
|
2026-04-03 15:59:54 +08:00
|
|
|
|
// 只有当 reg 存在且有 id 时才认为已报名
|
|
|
|
|
|
if (reg && reg.id) {
|
2026-03-27 22:20:25 +08:00
|
|
|
|
hasRegistered.value = true
|
2026-04-07 14:11:59 +08:00
|
|
|
|
registrationState.value = reg.registrationState || ''
|
2026-03-27 22:20:25 +08:00
|
|
|
|
myRegistration.value = reg
|
2026-04-03 15:59:54 +08:00
|
|
|
|
// 检查是否已提交作品
|
|
|
|
|
|
hasSubmittedWork.value = reg.hasSubmittedWork || false
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
2026-04-03 15:59:54 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 未报名或查询失败,保持 hasRegistered = false
|
|
|
|
|
|
console.log('查询报名状态失败或未报名')
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 14:11:59 +08:00
|
|
|
|
// 打开作品选择器
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const openSubmitWork = () => {
|
|
|
|
|
|
if (!isLoggedIn.value) { goLogin(); return }
|
2026-04-07 14:11:59 +08:00
|
|
|
|
showWorkSelector.value = true
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 14:11:59 +08:00
|
|
|
|
// 从作品库选择作品后提交
|
|
|
|
|
|
const handleWorkSelected = async (work: UserWork) => {
|
2026-03-27 22:20:25 +08:00
|
|
|
|
if (!myRegistration.value) {
|
|
|
|
|
|
message.error('请先报名活动')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
submittingWork.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await publicActivitiesApi.submitWork(activity.value.id, {
|
|
|
|
|
|
registrationId: myRegistration.value.id,
|
2026-04-07 14:11:59 +08:00
|
|
|
|
userWorkId: work.id,
|
2026-03-27 22:20:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
message.success('作品提交成功!')
|
2026-04-07 14:11:59 +08:00
|
|
|
|
showWorkSelector.value = false
|
2026-03-27 22:20:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-08 16:00:59 +08:00
|
|
|
|
if (!activity.value) return
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
|
|
|
|
|
registering.value = true
|
|
|
|
|
|
try {
|
2026-04-08 16:00:59 +08:00
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
message.success('报名成功!')
|
|
|
|
|
|
showRegisterModal.value = false
|
|
|
|
|
|
hasRegistered.value = true
|
2026-04-07 14:11:59 +08:00
|
|
|
|
// 重新查询报名状态以获取准确的 registrationState
|
|
|
|
|
|
await checkRegistrationStatus()
|
2026-03-27 22:20:25 +08:00
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
message.error(err?.response?.data?.message || '报名失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
registering.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await fetchDetail()
|
2026-04-03 15:59:54 +08:00
|
|
|
|
await fetchChildren()
|
|
|
|
|
|
await checkRegistrationStatus()
|
2026-03-27 22:20:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
</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;
|
|
|
|
|
|
}
|
2026-04-03 15:59:54 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 16:31:48 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
|
2026-04-08 16:31:48 +08:00
|
|
|
|
.result-card__body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 16:31:48 +08:00
|
|
|
|
.result-card__title-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-card__name {
|
2026-03-27 22:20:25 +08:00
|
|
|
|
font-size: 15px;
|
2026-04-08 16:31:48 +08:00
|
|
|
|
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;
|
2026-03-27 22:20:25 +08:00
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
}
|
2026-04-08 16:31:48 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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>
|