2025-12-09 11:10:36 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="contest-detail-page">
|
|
|
|
|
|
<a-spin :spinning="loading">
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<!-- 顶部海报区域 -->
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="hero-section">
|
|
|
|
|
|
<!-- 背景图 -->
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<div
|
2026-01-16 14:18:32 +08:00
|
|
|
|
class="hero-bg"
|
2026-01-08 09:17:46 +08:00
|
|
|
|
:style="{
|
|
|
|
|
|
backgroundImage:
|
|
|
|
|
|
contest?.posterUrl || contest?.coverUrl
|
|
|
|
|
|
? `url(${contest.posterUrl || contest.coverUrl})`
|
2026-01-16 14:18:32 +08:00
|
|
|
|
: undefined,
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="hero-overlay"></div>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 返回按钮 -->
|
|
|
|
|
|
<div class="hero-nav">
|
|
|
|
|
|
<button class="back-btn" @click="$router.back()">
|
|
|
|
|
|
<ArrowLeftOutlined />
|
|
|
|
|
|
<span>返回</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 海报内容 -->
|
|
|
|
|
|
<div class="hero-content">
|
|
|
|
|
|
<div class="hero-inner">
|
|
|
|
|
|
<!-- 状态标签 -->
|
|
|
|
|
|
<div class="status-tags">
|
|
|
|
|
|
<span class="tag tag-type">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
{{
|
|
|
|
|
|
contest?.contestType === "individual" ? "个人赛" : "团队赛"
|
|
|
|
|
|
}}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</span>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<span v-if="getStageText()" class="tag" :class="getStageClass()">
|
|
|
|
|
|
{{ getStageText() }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 标题 -->
|
|
|
|
|
|
<h1 class="hero-title">{{ contest?.contestName }}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 时间信息 -->
|
|
|
|
|
|
<div class="hero-meta">
|
|
|
|
|
|
<div class="meta-item">
|
|
|
|
|
|
<CalendarOutlined />
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<span
|
|
|
|
|
|
>比赛时间:{{ formatDate(contest?.startTime) }} ~
|
|
|
|
|
|
{{ formatDate(contest?.endTime) }}</span
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meta-item">
|
|
|
|
|
|
<ClockCircleOutlined />
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<span
|
|
|
|
|
|
>报名时间:{{ formatDate(contest?.registerStartTime) }} ~
|
|
|
|
|
|
{{ formatDate(contest?.registerEndTime) }}</span
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 报名按钮 -->
|
|
|
|
|
|
<div class="hero-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="isTeacher && isRegistering && !hasRegistered"
|
|
|
|
|
|
class="action-btn primary"
|
|
|
|
|
|
@click="handleRegister"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormOutlined />
|
|
|
|
|
|
立即报名
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="isTeacher && hasRegistered && canViewRegistration"
|
|
|
|
|
|
class="action-btn secondary"
|
|
|
|
|
|
@click="handleViewRegistration"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EyeOutlined />
|
|
|
|
|
|
查看报名
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="isTeacher"
|
|
|
|
|
|
class="action-btn disabled"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormOutlined />
|
2026-01-16 14:48:14 +08:00
|
|
|
|
{{ isRegistering ? "立即报名" : "报名已截止" }}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<span v-if="isRegistering" class="countdown">
|
|
|
|
|
|
距离报名截止还有 <strong>{{ daysRemaining }}</strong> 天
|
|
|
|
|
|
</span>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Tab 导航 -->
|
|
|
|
|
|
<div class="tab-section">
|
|
|
|
|
|
<div class="tab-container">
|
|
|
|
|
|
<div class="custom-tabs">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'info' }"
|
|
|
|
|
|
@click="handleTabChange('info')"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
|
<span>赛事信息</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'notices' }"
|
|
|
|
|
|
@click="handleTabChange('notices')"
|
2026-01-08 09:17:46 +08:00
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<BellOutlined />
|
|
|
|
|
|
<span>通知公告</span>
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<span v-if="notices.length > 0" class="badge">{{
|
|
|
|
|
|
notices.length
|
|
|
|
|
|
}}</span>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'results' }"
|
|
|
|
|
|
@click="handleTabChange('results')"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<TrophyOutlined />
|
|
|
|
|
|
<span>赛事结果</span>
|
|
|
|
|
|
</div>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 主内容区域 -->
|
|
|
|
|
|
<div v-if="contest" class="main-section">
|
|
|
|
|
|
<div class="main-container">
|
|
|
|
|
|
<div class="content-grid">
|
|
|
|
|
|
<!-- 左侧:主要内容 -->
|
|
|
|
|
|
<div class="content-left">
|
|
|
|
|
|
<!-- 赛事信息 Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'info'" class="content-card">
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-icon">
|
|
|
|
|
|
<FileTextOutlined />
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<h2>竞赛详情</h2>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="card-body">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="contest.content"
|
|
|
|
|
|
class="rich-content"
|
|
|
|
|
|
v-html="contest.content"
|
|
|
|
|
|
></div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<a-empty v-else description="暂无详情内容" />
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 通知公告 Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'notices'" class="content-card">
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-icon">
|
|
|
|
|
|
<BellOutlined />
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<h2>通知公告</h2>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div v-if="noticesLoading" class="loading-placeholder">
|
|
|
|
|
|
<a-spin />
|
|
|
|
|
|
</div>
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="notices.length === 0"
|
|
|
|
|
|
class="empty-placeholder"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<a-empty description="暂无公告" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="notice-list">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in notices"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
class="notice-item"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="notice-header">
|
|
|
|
|
|
<span class="notice-title">{{ item.title }}</span>
|
|
|
|
|
|
<span class="notice-type" :class="item.noticeType">
|
|
|
|
|
|
{{ getNoticeTypeText(item.noticeType) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="notice-content">{{ item.content }}</div>
|
|
|
|
|
|
<div class="notice-time">
|
|
|
|
|
|
<ClockCircleOutlined />
|
|
|
|
|
|
{{ formatDateTime(item.publishTime) }}
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 赛事结果 Tab -->
|
|
|
|
|
|
<div v-if="activeTab === 'results'" class="content-card">
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-icon">
|
|
|
|
|
|
<TrophyOutlined />
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<h2>赛事结果</h2>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div v-if="contest.resultState === 'published'">
|
|
|
|
|
|
<a-spin :spinning="resultsLoading">
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="resultColumns"
|
|
|
|
|
|
:data-source="results"
|
|
|
|
|
|
:pagination="resultsPagination"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
@change="handleResultsTableChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'rank'">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="rank-badge"
|
|
|
|
|
|
:class="getRankClass(record.rank)"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
{{ record.rank || "-" }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'author'">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
{{
|
|
|
|
|
|
record.registration?.user?.nickname ||
|
|
|
|
|
|
record.registration?.team?.teamName ||
|
|
|
|
|
|
"-"
|
|
|
|
|
|
}}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'award'">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<span
|
|
|
|
|
|
v-if="record.awardName"
|
|
|
|
|
|
class="award-tag"
|
|
|
|
|
|
:class="getAwardClass(record.awardName)"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
{{ record.awardName }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="empty-placeholder">
|
|
|
|
|
|
<a-empty description="结果尚未公布" />
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 右侧:组织信息 -->
|
|
|
|
|
|
<div class="content-right">
|
|
|
|
|
|
<div class="sidebar-card">
|
|
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
|
<TeamOutlined />
|
|
|
|
|
|
<span>组织信息</span>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="sidebar-body">
|
|
|
|
|
|
<div class="sidebar-item">
|
|
|
|
|
|
<div class="item-label">
|
|
|
|
|
|
<BankOutlined />
|
|
|
|
|
|
主办单位
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-value">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<template
|
|
|
|
|
|
v-if="contest.organizers && contest.organizers.length"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="org in contest.organizers"
|
|
|
|
|
|
:key="org"
|
|
|
|
|
|
class="org-name"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ org }}
|
|
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<span v-else class="empty-text">暂无</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="sidebar-item">
|
|
|
|
|
|
<div class="item-label">
|
|
|
|
|
|
<ApartmentOutlined />
|
|
|
|
|
|
协办单位
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="item-value">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<template
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
contest.coOrganizers && contest.coOrganizers.length
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="org in contest.coOrganizers"
|
|
|
|
|
|
:key="org"
|
|
|
|
|
|
class="org-name"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ org }}
|
|
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<span v-else class="empty-text">暂无</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="sidebar-item">
|
|
|
|
|
|
<div class="item-label">
|
|
|
|
|
|
<GiftOutlined />
|
|
|
|
|
|
赞助单位
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-value">
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<template
|
|
|
|
|
|
v-if="contest.sponsors && contest.sponsors.length"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="sp in contest.sponsors"
|
|
|
|
|
|
:key="sp"
|
|
|
|
|
|
class="org-name"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ sp }}
|
|
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<span v-else class="empty-text">暂无</span>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<!-- 联系方式 -->
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="contest.contactName || contest.contactPhone"
|
|
|
|
|
|
class="sidebar-card"
|
|
|
|
|
|
>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
|
<PhoneOutlined />
|
|
|
|
|
|
<span>联系方式</span>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
<div class="sidebar-body">
|
|
|
|
|
|
<div v-if="contest.contactName" class="sidebar-item">
|
|
|
|
|
|
<div class="item-label">
|
|
|
|
|
|
<UserOutlined />
|
|
|
|
|
|
联系人
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-value">{{ contest.contactName }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="contest.contactPhone" class="sidebar-item">
|
|
|
|
|
|
<div class="item-label">
|
|
|
|
|
|
<PhoneOutlined />
|
|
|
|
|
|
电话
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-value">{{ contest.contactPhone }}</div>
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-08 09:17:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
<a-empty
|
|
|
|
|
|
v-else-if="!loading"
|
|
|
|
|
|
description="比赛不存在"
|
|
|
|
|
|
style="padding: 100px 0"
|
|
|
|
|
|
/>
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-01-08 09:17:46 +08:00
|
|
|
|
import { ref, onMounted, computed } from "vue"
|
|
|
|
|
|
import { useRoute, useRouter } from "vue-router"
|
|
|
|
|
|
import { message } from "ant-design-vue"
|
2026-01-16 14:18:32 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
CalendarOutlined,
|
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
|
FormOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
BellOutlined,
|
|
|
|
|
|
TrophyOutlined,
|
|
|
|
|
|
TeamOutlined,
|
|
|
|
|
|
BankOutlined,
|
|
|
|
|
|
ApartmentOutlined,
|
|
|
|
|
|
GiftOutlined,
|
|
|
|
|
|
PhoneOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
} from "@ant-design/icons-vue"
|
2026-01-08 09:17:46 +08:00
|
|
|
|
import dayjs from "dayjs"
|
|
|
|
|
|
import { useAuthStore } from "@/stores/auth"
|
|
|
|
|
|
import {
|
|
|
|
|
|
contestsApi,
|
|
|
|
|
|
noticesApi,
|
|
|
|
|
|
resultsApi,
|
|
|
|
|
|
registrationsApi,
|
|
|
|
|
|
type Contest,
|
|
|
|
|
|
type ContestNotice,
|
|
|
|
|
|
type ContestResult,
|
|
|
|
|
|
} from "@/api/contests"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
const tenantCode = route.params.tenantCode as string
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const noticesLoading = ref(false)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const resultsLoading = ref(false)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
const contest = ref<Contest | null>(null)
|
|
|
|
|
|
const notices = ref<ContestNotice[]>([])
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const results = ref<ContestResult[]>([])
|
|
|
|
|
|
const activeTab = ref("info")
|
|
|
|
|
|
const hasRegistered = ref(false)
|
|
|
|
|
|
const myRegistration = ref<any>(null)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
|
2026-01-12 20:04:11 +08:00
|
|
|
|
const canViewRegistration = computed(() => {
|
|
|
|
|
|
const permissions = authStore.user?.permissions || []
|
2026-01-16 14:48:14 +08:00
|
|
|
|
return (
|
|
|
|
|
|
permissions.includes("registration:read") ||
|
|
|
|
|
|
permissions.includes("registration:create")
|
|
|
|
|
|
)
|
2026-01-15 16:35:00 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const isTeacher = computed(() => authStore.hasRole("teacher"))
|
2026-01-12 20:04:11 +08:00
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
const contestId = Number(route.params.id)
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const resultsPagination = ref({
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const resultColumns = [
|
2026-01-16 14:18:32 +08:00
|
|
|
|
{ title: "排名", key: "rank", dataIndex: "rank", width: 80 },
|
|
|
|
|
|
{ title: "作品名称", key: "title", dataIndex: "title", width: 200 },
|
|
|
|
|
|
{ title: "作者", key: "author", width: 150 },
|
2026-01-16 14:48:14 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "最终得分",
|
|
|
|
|
|
key: "finalScore",
|
|
|
|
|
|
dataIndex: "finalScore",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
sorter: true,
|
|
|
|
|
|
},
|
2026-01-16 14:18:32 +08:00
|
|
|
|
{ title: "奖项", key: "award", width: 120 },
|
2026-01-08 09:17:46 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateStr?: string) => {
|
|
|
|
|
|
if (!dateStr) return "-"
|
|
|
|
|
|
return dayjs(dateStr).format("YYYY-MM-DD")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
const formatDateTime = (dateStr?: string) => {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
if (!dateStr) return "-"
|
|
|
|
|
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isRegistering = computed(() => {
|
|
|
|
|
|
if (!contest.value) return false
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const start = dayjs(contest.value.registerStartTime)
|
|
|
|
|
|
const end = dayjs(contest.value.registerEndTime)
|
|
|
|
|
|
return now.isAfter(start) && now.isBefore(end)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const isSubmitting = computed(() => {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
if (!contest.value?.submitStartTime || !contest.value?.submitEndTime)
|
|
|
|
|
|
return false
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const now = dayjs()
|
2026-01-16 14:48:14 +08:00
|
|
|
|
return (
|
|
|
|
|
|
now.isAfter(dayjs(contest.value.submitStartTime)) &&
|
|
|
|
|
|
now.isBefore(dayjs(contest.value.submitEndTime))
|
|
|
|
|
|
)
|
2026-01-16 14:18:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const isReviewing = computed(() => {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
if (!contest.value?.reviewStartTime || !contest.value?.reviewEndTime)
|
|
|
|
|
|
return false
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const now = dayjs()
|
2026-01-16 14:48:14 +08:00
|
|
|
|
return (
|
|
|
|
|
|
now.isAfter(dayjs(contest.value.reviewStartTime)) &&
|
|
|
|
|
|
now.isBefore(dayjs(contest.value.reviewEndTime))
|
|
|
|
|
|
)
|
2026-01-16 14:18:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const daysRemaining = computed(() => {
|
|
|
|
|
|
if (!contest.value || !isRegistering.value) return 0
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const diff = dayjs(contest.value.registerEndTime).diff(dayjs(), "day")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return diff > 0 ? diff : 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const getStageText = () => {
|
|
|
|
|
|
if (isRegistering.value) return "报名中"
|
|
|
|
|
|
if (isSubmitting.value) return "征稿中"
|
|
|
|
|
|
if (isReviewing.value) return "评审中"
|
|
|
|
|
|
if (contest.value?.status === "finished") return "已结束"
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStageClass = () => {
|
|
|
|
|
|
if (isRegistering.value) return "tag-registering"
|
|
|
|
|
|
if (isSubmitting.value) return "tag-submitting"
|
|
|
|
|
|
if (isReviewing.value) return "tag-reviewing"
|
|
|
|
|
|
if (contest.value?.status === "finished") return "tag-finished"
|
|
|
|
|
|
return ""
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getNoticeTypeText = (type?: string) => {
|
|
|
|
|
|
switch (type) {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
case "urgent":
|
|
|
|
|
|
return "紧急"
|
|
|
|
|
|
case "system":
|
|
|
|
|
|
return "系统"
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "公告"
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const getRankClass = (rank?: number) => {
|
|
|
|
|
|
if (rank === 1) return "rank-1"
|
|
|
|
|
|
if (rank === 2) return "rank-2"
|
|
|
|
|
|
if (rank === 3) return "rank-3"
|
|
|
|
|
|
return ""
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
const getAwardClass = (award?: string) => {
|
|
|
|
|
|
if (!award) return ""
|
|
|
|
|
|
if (award.includes("一等奖") || award.includes("金奖")) return "award-gold"
|
|
|
|
|
|
if (award.includes("二等奖") || award.includes("银奖")) return "award-silver"
|
|
|
|
|
|
if (award.includes("三等奖") || award.includes("铜奖")) return "award-bronze"
|
|
|
|
|
|
return ""
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
const fetchContestDetail = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
contest.value = await contestsApi.getDetail(contestId)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
await checkRegistration()
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error(error?.response?.data?.message || "获取比赛详情失败")
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const checkRegistration = async () => {
|
|
|
|
|
|
if (!authStore.user) return
|
2025-12-09 11:10:36 +08:00
|
|
|
|
try {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const response = await registrationsApi.getList({
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
userId: authStore.user.id,
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (response.list && response.list.length > 0) {
|
|
|
|
|
|
hasRegistered.value = true
|
|
|
|
|
|
myRegistration.value = response.list[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("检查报名状态失败:", error)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetchNotices = async () => {
|
|
|
|
|
|
noticesLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
notices.value = await noticesApi.getList(contestId)
|
|
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error("获取公告列表失败")
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
noticesLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const fetchResults = async () => {
|
|
|
|
|
|
if (!contest.value || contest.value.resultState !== "published") return
|
|
|
|
|
|
resultsLoading.value = true
|
2025-12-09 11:10:36 +08:00
|
|
|
|
try {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
const response = await resultsApi.getResults(
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
resultsPagination.value.current,
|
|
|
|
|
|
resultsPagination.value.pageSize
|
|
|
|
|
|
)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
results.value = response.list || []
|
|
|
|
|
|
resultsPagination.value.total = response.total || 0
|
2025-12-09 11:10:36 +08:00
|
|
|
|
} catch (error: any) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.error("获取赛事结果失败")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
resultsLoading.value = false
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const handleResultsTableChange = (pag: any) => {
|
|
|
|
|
|
resultsPagination.value.current = pag.current || 1
|
|
|
|
|
|
resultsPagination.value.pageSize = pag.pageSize || 20
|
|
|
|
|
|
fetchResults()
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const handleRegister = () => {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
if (!authStore.user) {
|
|
|
|
|
|
message.warning("请先登录")
|
|
|
|
|
|
router.push(`/${tenantCode}/login`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
if (!contest.value) return
|
|
|
|
|
|
if (contest.value.contestType === "team") {
|
|
|
|
|
|
router.push(`/${tenantCode}/contests/${contestId}/register/team`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.push(`/${tenantCode}/contests/${contestId}/register/individual`)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const handleViewRegistration = () => {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
if (!contest.value) return
|
|
|
|
|
|
if (contest.value.contestType === "team") {
|
|
|
|
|
|
router.push(`/${tenantCode}/contests/${contestId}/register/team`)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
router.push(`/${tenantCode}/contests/${contestId}/register/individual`)
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const handleTabChange = (key: string) => {
|
|
|
|
|
|
activeTab.value = key
|
|
|
|
|
|
if (key === "results" && contest.value?.resultState === "published") {
|
|
|
|
|
|
fetchResults()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchContestDetail()
|
|
|
|
|
|
fetchNotices()
|
|
|
|
|
|
})
|
2025-12-09 11:10:36 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
<style lang="scss" scoped>
|
2026-01-16 14:18:32 +08:00
|
|
|
|
$primary: #1890ff;
|
|
|
|
|
|
$primary-dark: #0958d9;
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
.contest-detail-page {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
min-height: 100vh;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// Hero 区域
|
|
|
|
|
|
.hero-section {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 420px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.hero-bg {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
background-position: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
180deg,
|
|
|
|
|
|
rgba(0, 0, 0, 0.3) 0%,
|
|
|
|
|
|
rgba(0, 0, 0, 0.6) 100%
|
|
|
|
|
|
);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-nav {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
position: relative;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
z-index: 10;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 8px 16px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
backdrop-filter: blur(10px);
|
2026-01-16 14:48:14 +08:00
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
border-radius: 20px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
color: #fff;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.25);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.hero-content {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
height: calc(100% - 60px);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0 24px;
|
|
|
|
|
|
|
|
|
|
|
|
.hero-inner {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
width: 100%;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-type {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.tag-registering {
|
|
|
|
|
|
background: linear-gradient(135deg, #52c41a, #73d13d);
|
|
|
|
|
|
}
|
|
|
|
|
|
.tag-submitting {
|
|
|
|
|
|
background: linear-gradient(135deg, #1890ff, #40a9ff);
|
|
|
|
|
|
}
|
|
|
|
|
|
.tag-reviewing {
|
|
|
|
|
|
background: linear-gradient(135deg, #faad14, #ffc53d);
|
|
|
|
|
|
}
|
|
|
|
|
|
.tag-finished {
|
|
|
|
|
|
background: linear-gradient(135deg, #8c8c8c, #bfbfbf);
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-title {
|
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
margin: 0 0 20px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
|
|
|
|
|
|
.meta-item {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 14px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.anticon {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.hero-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
display: inline-flex;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
align-items: center;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 12px 28px;
|
|
|
|
|
|
border-radius: 24px;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
border: none;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&.primary {
|
|
|
|
|
|
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
box-shadow: 0 4px 15px rgba($primary, 0.4);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 6px 20px rgba($primary, 0.5);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&.secondary {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
color: $primary;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #fff;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&.disabled {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.6);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.countdown {
|
|
|
|
|
|
font-size: 14px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(255, 255, 255, 0.85);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
strong {
|
|
|
|
|
|
color: #ffc53d;
|
|
|
|
|
|
font-size: 18px;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// Tab 导航
|
|
|
|
|
|
.tab-section {
|
|
|
|
|
|
background: #fff;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
position: sticky;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
top: -20px;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
z-index: 100;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.tab-container {
|
|
|
|
|
|
padding: 0 24px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.custom-tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 12px 0;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.tab-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.65);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
position: relative;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
background: rgba($primary, 0.06);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
&.active {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba($primary, 0.3);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 4px;
|
|
|
|
|
|
right: 4px;
|
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// 主内容区域
|
|
|
|
|
|
.main-section {
|
|
|
|
|
|
padding: 24px 0 48px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.main-container {
|
2026-01-16 14:48:14 +08:00
|
|
|
|
padding: 0;
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.content-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 320px;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// 内容卡片
|
|
|
|
|
|
.content-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
|
|
.header-icon {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
h2 {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
margin: 0;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
2025-12-09 11:10:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// 富文本内容
|
|
|
|
|
|
.rich-content {
|
|
|
|
|
|
font-size: 14px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.75);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(p) {
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
:deep(h1),
|
|
|
|
|
|
:deep(h2),
|
|
|
|
|
|
:deep(h3),
|
|
|
|
|
|
:deep(h4),
|
|
|
|
|
|
:deep(h5),
|
|
|
|
|
|
:deep(h6) {
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:first-child {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(img) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
:deep(ul),
|
|
|
|
|
|
:deep(ol) {
|
2026-01-16 14:18:32 +08:00
|
|
|
|
padding-left: 24px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(li) {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(table) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
th,
|
|
|
|
|
|
td {
|
2026-01-16 14:18:32 +08:00
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: left;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
th {
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(blockquote) {
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: #f9f9f9;
|
|
|
|
|
|
border-left: 4px solid $primary;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.65);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(a) {
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
text-decoration: none;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// 公告列表
|
|
|
|
|
|
.notice-list {
|
|
|
|
|
|
.notice-item {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
.notice-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
.notice-title {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.notice-type {
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&.urgent {
|
|
|
|
|
|
background: #fff2f0;
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.system {
|
|
|
|
|
|
background: #e6f7ff;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.notice-content {
|
|
|
|
|
|
font-size: 14px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.65);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notice-time {
|
|
|
|
|
|
font-size: 12px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
// 排名徽章
|
|
|
|
|
|
.rank-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
background: #f0f0f0;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.65);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&.rank-1 {
|
|
|
|
|
|
background: linear-gradient(135deg, #ffd700, #ffb800);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.rank-2 {
|
|
|
|
|
|
background: linear-gradient(135deg, #c0c0c0, #a0a0a0);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.rank-3 {
|
|
|
|
|
|
background: linear-gradient(135deg, #cd7f32, #b8860b);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 奖项标签
|
|
|
|
|
|
.award-tag {
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&.award-gold {
|
|
|
|
|
|
background: #fffbe6;
|
|
|
|
|
|
color: #d48806;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.award-silver {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
color: #595959;
|
|
|
|
|
|
}
|
|
|
|
|
|
&.award-bronze {
|
|
|
|
|
|
background: #fff7e6;
|
|
|
|
|
|
color: #d46b08;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 侧边栏卡片
|
|
|
|
|
|
.sidebar-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
.sidebar-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 16px 20px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
rgba($primary, 0.05),
|
|
|
|
|
|
rgba($primary-dark, 0.08)
|
|
|
|
|
|
);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.anticon {
|
|
|
|
|
|
color: $primary;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-body {
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-item {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
|
|
|
|
|
.item-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 13px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.anticon {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:18:32 +08:00
|
|
|
|
.item-value {
|
|
|
|
|
|
font-size: 14px;
|
2026-01-16 14:48:14 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-16 14:18:32 +08:00
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
|
|
|
|
|
|
.org-name {
|
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
|
border-bottom: 1px dashed #f0f0f0;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.empty-text {
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.25);
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 占位状态
|
|
|
|
|
|
.loading-placeholder,
|
|
|
|
|
|
.empty-placeholder {
|
|
|
|
|
|
padding: 48px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式
|
|
|
|
|
|
@media (max-width: 992px) {
|
|
|
|
|
|
.main-section .content-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-right {
|
|
|
|
|
|
order: -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.hero-section {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
min-height: 360px;
|
|
|
|
|
|
|
2026-01-16 14:48:14 +08:00
|
|
|
|
.hero-title {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.hero-meta {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.hero-actions {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
2026-01-16 14:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-section .custom-tabs {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
padding: 8px 16px;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|