Files
competition-website/src/views/Home.vue
WorkBuddy e8f0195b61 chore: 前端首次基线提交
运行必须源码:src/、public/、index.html、package.json 及构建配置。
已忽略运行非必须:node_modules/、dist/、.env*。
2026-08-17 12:25:42 +08:00

965 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="home-page">
<!-- ==================== 顶部导航栏 ==================== -->
<header class="home-header">
<div class="header-inner">
<div class="header-logo">
<span class="logo-icon">&#9788;</span>
<span class="logo-text">卫星通信与导航应用技术创新赛</span>
</div>
<nav class="header-nav">
<a href="#intro" class="nav-link">赛事介绍</a>
<a href="#groups" class="nav-link">竞赛组别</a>
<a href="#news" class="nav-link">新闻公告</a>
<a href="#process" class="nav-link">参赛流程</a>
</nav>
<div class="header-actions">
<template v-if="authStore.isLoggedIn">
<el-button v-if="authStore.isAdmin" type="primary" size="default" @click="$router.push('/admin')">
管理后台
</el-button>
<el-button v-else type="primary" size="default" @click="$router.push('/dashboard')">
选手工作台
</el-button>
<el-button type="default" size="default" v-guard="() => handleLogout()">
退出登录
</el-button>
</template>
<template v-else>
<el-button type="primary" size="default" @click="$router.push('/login')">
选手登录
</el-button>
<el-button type="warning" size="default" @click="$router.push('/activate')">
激活账号
</el-button>
</template>
</div>
</div>
</header>
<!-- ==================== 轮播图区域(全部由后台数据库驱动) ==================== -->
<section class="carousel-section" v-if="carouselList.length">
<el-carousel
:interval="5000"
:height="carouselHeight"
indicator-position="outside"
arrow="always"
trigger="click"
>
<el-carousel-item v-for="item in carouselList" :key="'slide-' + item.id">
<div
class="carousel-slide"
:class="{ 'carousel-slide--gradient': !item.imageUrl }"
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : {}"
>
<div class="carousel-overlay"></div>
<div class="carousel-content">
<!-- 标题:用 v-html 以支持 <br> 等基础格式 -->
<h1 class="carousel-title" v-if="item.title" v-html="item.title"></h1>
<!-- 富文本正文(wangEditor 生成,v-html 渲染) -->
<div class="carousel-richtext" v-if="item.content" v-html="item.content"></div>
<!-- 可配置按钮:外链用 a[target=_blank],站内路由用 router-link -->
<template v-if="item.buttonText">
<a
v-if="isExternalLink(item.buttonLink)"
:href="item.buttonLink"
target="_blank"
rel="noopener noreferrer"
class="carousel-cta"
>{{ item.buttonText }}</a>
<router-link
v-else
:to="item.buttonLink || '/activate'"
class="carousel-cta"
>{{ item.buttonText }}</router-link>
</template>
</div>
</div>
</el-carousel-item>
</el-carousel>
</section>
<!-- ==================== 赛事介绍 ==================== -->
<section class="intro-section" id="intro">
<el-row justify="center">
<el-col :xs="24" :md="18" :lg="16">
<div class="intro-card">
<h2 class="section-title">
<span class="title-line"></span>
卫星通信与导航应用技术创新赛
</h2>
<el-divider />
<p class="intro-text">
卫星通信与导航应用技术竞赛为2026"一带一路"暨金砖国家技能发展与技术创新大赛全新增设核心赛项。
赛事聚焦卫星通信、导航定位领域前沿核心技术,紧扣产业发展实际需求与现代职业教育改革导向,
依托多层级、立体化竞赛考核体系,着力展示行业尖端技术成果,引领职业教育教学革新,
全面提升院校实训教学水平,锤炼参赛选手综合实操与创新能力,选拔培育卫星信息领域高素质技术技能人才。
</p>
</div>
</el-col>
</el-row>
</section>
<!-- ==================== 竞赛组别 ==================== -->
<section class="groups-section" id="groups">
<div class="section-header">
<h2 class="section-title"><span class="title-line"></span>竞赛组别</h2>
<p class="section-subtitle">选择适合您的赛道,展现您的技术实力</p>
</div>
<el-row justify="center">
<el-col :xs="24" :md="22" :lg="20">
<el-row :gutter="24" justify="center">
<el-col :xs="24" :sm="12" :md="6" v-for="grp in groups" :key="grp.key">
<el-card class="group-card" :class="`group-card--${grp.key}`" :body-style="{ padding: '0' }" shadow="hover">
<div class="group-card-inner">
<div class="group-card-header" :style="{ background: grp.color }">
<div class="group-icon"><span class="icon-placeholder">{{ grp.icon }}</span></div>
<h3 class="group-name">{{ grp.name }}</h3>
</div>
<div class="group-card-body">
<el-tag :color="grp.tagColor" class="group-tag" effect="dark">{{ grp.type }}</el-tag>
<p class="group-desc">{{ grp.desc }}</p>
<el-divider class="group-divider" />
<el-button class="group-link-btn" :style="{ color: grp.color, borderColor: grp.color }" plain round size="small" @click="$router.push('/login')">
立即报名
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
</section>
<!-- ==================== 新闻公告(带分组 Tab 筛选) ==================== -->
<section class="news-section" id="news">
<div class="section-header">
<h2 class="section-title"><span class="title-line"></span>新闻公告</h2>
<p class="section-subtitle">了解赛事最新动态与通知</p>
</div>
<el-row :gutter="20" justify="center">
<el-col :xs="24" :md="18" :lg="16">
<!-- 分组 Tab -->
<el-tabs v-model="activeNewsTab" @tab-change="onNewsTabChange" class="news-tabs">
<el-tab-pane
v-for="tab in newsTabs"
:key="tab.key"
:label="`${tab.label} (${tab.count})`"
:name="tab.key"
/>
</el-tabs>
<el-card shadow="never" class="news-card" v-loading="newsLoading">
<!-- 置顶新闻 -->
<div v-if="pinnedNews.length > 0" class="pinned-section">
<div class="pinned-header">
<el-icon color="#c9a84c"><Top /></el-icon>
<span class="pinned-title">置顶公告</span>
</div>
<div
v-for="item in pinnedNews"
:key="'pinned-' + item.id"
class="news-item pinned-item"
@click="viewNewsDetail(item.id)"
>
<el-tag type="warning" size="small" effect="dark">置顶</el-tag>
<span class="news-item-title text-ellipsis">{{ item.title }}</span>
<template v-if="item.tags">
<el-tag v-for="tag in (item.tags || '').split(',').filter(Boolean)" :key="tag" size="small" class="news-tag" round>{{ tag.trim() }}</el-tag>
</template>
<span class="news-item-time">{{ formatDate(item.publish_time) }}</span>
</div>
</div>
<!-- 普通新闻列表 -->
<div v-if="normalNews.length > 0" class="normal-section">
<div
v-for="item in normalNews"
:key="item.id"
class="news-item"
@click="viewNewsDetail(item.id)"
>
<span class="news-dot"></span>
<span class="news-item-title text-ellipsis">{{ item.title }}</span>
<template v-if="item.tags">
<el-tag v-for="tag in (item.tags || '').split(',').filter(Boolean)" :key="tag" size="small" class="news-tag" round>{{ tag.trim() }}</el-tag>
</template>
<span class="news-item-time">{{ formatDate(item.publish_time) }}</span>
</div>
</div>
<!-- 空状态 -->
<el-empty v-if="!newsLoading && pinnedNews.length === 0 && normalNews.length === 0" description="暂无新闻公告" />
<!-- 分页 -->
<div class="news-pagination" v-if="newsTotal > newsPagination.pageSize">
<el-pagination
v-model:current-page="newsPagination.page"
:page-size="newsPagination.pageSize"
:total="newsTotal"
layout="prev, pager, next"
size="small"
@current-change="onNewsPageChange"
/>
</div>
</el-card>
</el-col>
</el-row>
</section>
<!-- ==================== 参赛流程 ==================== -->
<section class="timeline-section" id="process">
<div class="section-header">
<h2 class="section-title"><span class="title-line"></span>参赛流程</h2>
<p class="section-subtitle">简单四步,开启您的竞赛之旅</p>
</div>
<el-row justify="center">
<el-col :xs="24" :md="18" :lg="16">
<div class="process-timeline">
<template v-for="(step, index) in processSteps" :key="index">
<div class="process-step">
<div class="process-step-badge">
<span class="step-number">{{ index + 1 }}</span>
</div>
<div class="process-step-content">
<h4 class="step-title">{{ step }}</h4>
</div>
</div>
<div class="process-step-arrow" v-if="index < processSteps.length - 1">
<span class="arrow-icon">&#10132;</span>
</div>
</template>
</div>
</el-col>
</el-row>
</section>
<!-- ==================== 资料下载(已移至工作台) ==================== -->
<!--
<section class="materials-section" id="materials">
<div class="section-header">
<h2 class="section-title"><span class="title-line"></span>资料下载</h2>
<p class="section-subtitle">关于中职组/高职组/本科组/教师组比赛相关的说明资料,点击按钮进入下载界面</p>
</div>
<el-row justify="center">
<el-col :xs="24" :md="18" :lg="16">
<div class="materials-card">
<el-alert
title="赛事资料请通过百度网盘下载"
type="info"
:closable="false"
show-icon
class="materials-alert"
>
<template #default>
<p class="materials-link">百度网盘链接:<el-link type="primary" href="https://pan.baidu.com" target="_blank">https://pan.baidu.com/s/xxxxxx</el-link>(提取码:2026)</p>
<p class="materials-hint">请使用微信扫描下方二维码获取资料,或登录后在工作台查看备赛资料</p>
</template>
</el-alert>
<div class="materials-qr">
<div class="qr-placeholder">
<span class="qr-icon">&#9633;</span>
<span class="qr-text">二维码</span>
</div>
<p class="qr-hint">微信扫码获取资料</p>
</div>
</div>
</el-col>
</el-row>
</section>
-->
<!-- ==================== 页脚 ==================== -->
<footer class="footer-section">
<div class="footer-content">
<h3 class="footer-contact-title">联系我们</h3>
<p class="footer-contact">赛事咨询:吴老师 13810099338</p>
<p class="footer-contact">大赛QQ群:1103585733</p>
<p class="footer-text">&copy; {{ currentYear }} 卫星通信与导航应用技术创新赛组委会 版权所有</p>
<p class="footer-subtext">技术支持 | 竞赛咨询请联系组委会官方渠道</p>
</div>
</footer>
</div>
</template>
<script setup>
/**
* Home.vue - 竞赛网站首页(公开访问)
*
* 功能:
* 1. 顶部导航栏(登录/激活按钮)
* 2. 固定两页轮播 + 管理员自定义轮播
* 3. 赛事介绍
* 4. 竞赛组别卡片(4 个组别)
* 5. 新闻公告(Tab 分组筛选)
* 6. 参赛流程时间线
* 7. 资料下载(百度网盘 + 二维码)
* 8. 页脚
*/
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Top } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
import * as carouselApi from '@/api/carousel'
import * as newsApi from '@/api/news'
import { normalizeImageUrl } from '@/utils/upload'
const router = useRouter()
const authStore = useAuthStore()
// ==================== 状态 ====================
// 轮播图(管理员自定义)
const carouselList = ref([])
const carouselHeight = ref('500px')
// 新闻
const newsList = ref([])
const newsLoading = ref(false)
const activeNewsTab = ref('all')
const newsPagination = ref({ page: 1, pageSize: 10 })
const newsTotal = ref(0)
const newsTabs = ref([
{ key: 'all', label: '全部', count: 0 },
{ key: 'zhongzhi', label: '中职组', count: 0 },
{ key: 'gaozhi', label: '高职组', count: 0 },
{ key: 'benke', label: '本科组', count: 0 },
{ key: 'jiaoshi', label: '教师组', count: 0 },
])
// 竞赛组别
const groups = [
{ key: 'zhongzhi', name: '中职组', icon: '中职', color: '#3b82f6', tagColor: '#3b82f6', type: '在线答题', desc: '重点考察卫星通信与导航领域的基础核心知识和高精度接收机的实操能力。' },
{ key: 'gaozhi', name: '高职组', icon: '高职', color: '#10b981', tagColor: '#10b981', type: '在线答题', desc: '重点考察卫星通信与导航领域的基础核心知识和卫星通信设备的实操能力。' },
{ key: 'benke', name: '本科组', icon: '本科', color: '#f59e0b', tagColor: '#f59e0b', type: '提交作品', desc: '重点考察卫星通信与导航领域的基础核心知识和卫星通信与导航的应用创新能力。' },
{ key: 'jiaoshi', name: '教师组', icon: '教师', color: '#ef4444', tagColor: '#ef4444', type: '提交作品', desc: '重点考察卫星通信与导航技术相关课程的教学方案设计能力和现场表达展现能力。' },
]
const processSteps = ['账号激活', '确认考试信息', '参加考试', '成绩获取']
const currentYear = new Date().getFullYear()
// ==================== 计算属性 ====================
const pinnedNews = computed(() => newsList.value.filter(n => n.pinned))
const normalNews = computed(() => newsList.value.filter(n => !n.pinned))
// ==================== 方法 ====================
function formatDate(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
if (isNaN(d.getTime())) return dateStr
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function viewNewsDetail(id) {
router.push(`/news/${id}`)
}
function onNewsTabChange() {
newsPagination.value.page = 1
fetchNews()
}
function onNewsPageChange() {
fetchNews()
}
/**
* 判断链接是否为外部链接(http/https/协议相对 //)
* @param {string} link
* @returns {boolean}
*/
function isExternalLink(link) {
if (!link) return false
return /^(https?:)?\/\//i.test(link)
}
async function fetchCarousel() {
try {
const data = await carouselApi.getCarouselList()
if (Array.isArray(data)) {
carouselList.value = data
.filter(item => item.enabled !== false)
.map(item => ({
id: item.id,
imageUrl: item.image_url ? normalizeImageUrl(item.image_url) : '',
title: item.title || '',
content: item.content || '',
buttonText: item.button_text || '',
buttonLink: item.button_link || '',
sortOrder: item.sort_order || 0,
}))
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
}
updateCarouselHeight()
window.addEventListener('resize', updateCarouselHeight)
} catch (err) {
console.warn('获取轮播图失败', err)
}
}
function updateCarouselHeight() {
carouselHeight.value = window.innerWidth < 768 ? '280px' : '500px'
}
async function fetchNews() {
newsLoading.value = true
try {
const params = {
page: newsPagination.value.page,
pageSize: newsPagination.value.pageSize,
}
if (activeNewsTab.value !== 'all') {
params.group = activeNewsTab.value
}
const data = await newsApi.getNewsList(params)
if (data && data.list) {
newsList.value = data.list
newsTotal.value = data.total || 0
} else if (Array.isArray(data)) {
newsList.value = data
newsTotal.value = data.length
} else {
newsList.value = []
newsTotal.value = 0
}
const counts = await newsApi.getNewsCounts()
if (counts) {
newsTabs.value.forEach(tab => {
tab.count = counts[tab.key] || 0
})
}
} catch (err) {
console.warn('获取新闻列表失败', err)
newsList.value = []
newsTotal.value = 0
} finally {
newsLoading.value = false
}
}
async function handleLogout() {
await authStore.logout()
}
onMounted(() => {
fetchCarousel()
fetchNews()
})
</script>
<style scoped>
/* ==================== CSS 变量 ==================== */
.home-page {
--primary: #1a3a5c;
--primary-light: #2a5a8c;
--primary-dark: #0f2640;
--gold: #c9a84c;
--gold-light: #e0c97a;
--bg-light: #f5f6f8;
--bg-white: #ffffff;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--border-light: #e5e7eb;
min-height: 100vh;
background: var(--bg-light);
}
/* ==================== 顶部导航栏 ==================== */
.home-header {
position: sticky;
top: 0;
z-index: 1000;
background: rgba(26, 58, 92, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 60px;
}
.header-logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
font-size: 24px;
color: var(--gold);
}
.logo-text {
font-size: 16px;
font-weight: 700;
color: #fff;
letter-spacing: 1px;
}
.header-nav {
display: flex;
gap: 28px;
}
.nav-link {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
transition: color 0.3s;
cursor: pointer;
}
.nav-link:hover {
color: var(--gold);
}
.header-actions {
display: flex;
gap: 10px;
}
@media (max-width: 768px) {
.header-nav { display: none; }
.logo-text { font-size: 13px; }
.header-inner { padding: 0 12px; }
}
/* ==================== 轮播图 ==================== */
.carousel-section {
position: relative;
background: var(--primary-dark);
}
.carousel-section .el-carousel__arrow {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
color: #fff;
font-size: 20px;
transition: all 0.3s ease;
border: none;
}
.carousel-section .el-carousel__arrow:hover {
/* 箭头图标为白色字体,hover 不可用浅/白色背景,否则图标隐形 */
background: rgba(26, 58, 92, 0.6);
}
.carousel-section .el-carousel__arrow--left {
left: 24px;
}
.carousel-section .el-carousel__arrow--right {
right: 24px;
}
.carousel-slide {
position: relative;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* 无底图时的默认渐变背景(还原原固定页深蓝渐变外观) */
.carousel-slide--gradient {
background: linear-gradient(135deg, #0f2640 0%, #1a3a5c 30%, #2a5a8c 60%, #0f2640 100%);
}
.carousel-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: radial-gradient(ellipse at 30% 50%, rgba(201, 168, 76, 0.12) 0%, transparent 60%),
radial-gradient(ellipse at 70% 50%, rgba(201, 168, 76, 0.06) 0%, transparent 60%);
pointer-events: none;
}
.carousel-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
text-align: center;
color: #fff;
max-width: 800px;
width: 100%;
padding: 0 40px;
}
.carousel-title {
font-size: 40px;
font-weight: 800;
margin-bottom: 18px;
letter-spacing: 2px;
line-height: 1.3;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.4);
animation: carouselFadeInUp 0.7s ease;
}
.carousel-desc {
font-size: 18px;
color: rgba(255, 255, 255, 0.88);
line-height: 1.8;
margin-bottom: 32px;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
animation: carouselFadeInUp 0.7s ease 0.15s both;
}
/* 富文本正文容器(wangEditor 生成的 HTML 经 v-html 渲染)
scoped CSS 不作用于 v-html 子节点,故用 :deep() 兜底常见标签的可读性 */
.carousel-richtext {
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.8;
margin-bottom: 32px;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
animation: carouselFadeInUp 0.7s ease 0.15s both;
}
.carousel-richtext :deep(p),
.carousel-richtext :deep(span),
.carousel-richtext :deep(li),
.carousel-richtext :deep(div) {
color: inherit;
line-height: 1.8;
}
.carousel-richtext :deep(a) {
color: #e0c97a;
text-decoration: underline;
}
.carousel-richtext :deep(img) {
max-width: 100%;
border-radius: 8px;
}
.carousel-cta {
display: inline-block;
padding: 14px 44px;
font-size: 17px;
font-weight: 600;
letter-spacing: 2px;
color: #1a3a5c;
background: linear-gradient(135deg, #c9a84c, #e0c97a);
border: none;
border-radius: 50px;
text-decoration: none;
cursor: pointer;
box-shadow: 0 4px 20px rgba(201, 168, 76, 0.4);
transition: all 0.3s ease;
animation: carouselFadeInUp 0.7s ease 0.3s both;
}
.carousel-cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(201, 168, 76, 0.5);
color: #1a3a5c;
}
/* 轮播页二:赛项内容卡片 */
.carousel-items {
display: flex;
justify-content: center;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 36px;
animation: carouselFadeInUp 0.7s ease 0.15s both;
}
.carousel-item-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px 24px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
min-width: 140px;
transition: all 0.3s ease;
}
.carousel-item-card:hover {
background: rgba(255, 255, 255, 0.18);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(201, 168, 76, 0.2);
}
.carousel-item-card .item-icon {
font-size: 32px;
line-height: 1;
}
.carousel-item-card span {
font-size: 15px;
font-weight: 600;
letter-spacing: 1px;
}
@keyframes carouselFadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.carousel-title {
font-size: 26px;
letter-spacing: 1px;
}
.carousel-desc {
font-size: 15px;
margin-bottom: 24px;
}
.carousel-cta {
font-size: 15px;
padding: 12px 32px;
}
.carousel-section .el-carousel__arrow {
width: 38px;
height: 38px;
}
.carousel-section .el-carousel__arrow--left {
left: 12px;
}
.carousel-section .el-carousel__arrow--right {
right: 12px;
}
.carousel-items {
gap: 12px;
}
.carousel-item-card {
min-width: 120px;
padding: 14px 16px;
}
.carousel-item-card .item-icon {
font-size: 26px;
}
.carousel-item-card span {
font-size: 13px;
}
}
/* ==================== Shared Sections ==================== */
.section-header {
text-align: center;
margin-bottom: 44px;
}
.section-title {
font-size: 30px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.title-line {
display: inline-block;
width: 4px;
height: 28px;
background: var(--gold);
border-radius: 2px;
}
.section-subtitle {
font-size: 15px;
color: var(--text-secondary);
}
/* ==================== Intro ==================== */
.intro-section {
padding: 72px 20px 56px;
background: var(--bg-white);
}
.intro-card { text-align: center; padding: 20px; }
.intro-text { font-size: 15px; color: var(--text-secondary); line-height: 1.9; margin-bottom: 16px; text-align: left; text-indent: 2em; }
/* ==================== Groups ==================== */
.groups-section {
padding: 72px 20px;
background: var(--bg-light);
}
.group-card { border-radius: 12px; overflow: hidden; border: 1px solid var(--border-light); margin-bottom: 24px; transition: transform 0.3s; }
.group-card:hover { transform: translateY(-6px); }
.group-card-inner { display: flex; flex-direction: column; height: 100%; }
.group-card-header { padding: 28px 20px 20px; text-align: center; color: #fff; }
.group-icon { width: 64px; height: 64px; border-radius: 50%; background: rgba(255, 255, 255, 0.2); display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; }
.icon-placeholder { font-size: 15px; font-weight: 700; letter-spacing: 2px; white-space: nowrap; }
.group-name { font-size: 20px; font-weight: 700; margin: 0; letter-spacing: 2px; }
.group-card-body { padding: 20px; flex: 1; display: flex; flex-direction: column; align-items: center; text-align: center; background: var(--bg-white); }
.group-tag { margin-bottom: 12px; border: none; }
.group-desc { font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin: 0; flex: 1; }
.group-divider { margin: 16px 0; }
.group-link-btn { border-width: 1.5px; }
/* ==================== News ==================== */
.news-section {
padding: 72px 20px;
background: var(--bg-white);
}
.news-tabs .el-tabs__nav-wrap::after { height: 1px; }
.news-tabs .el-tabs__item { font-size: 15px; }
.news-card { border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); }
.pinned-section { margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px dashed var(--border-light); }
.pinned-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.pinned-title { font-size: 14px; font-weight: 600; color: var(--gold); }
.news-item { display: flex; align-items: center; gap: 12px; padding: 14px 0; border-bottom: 1px solid var(--border-light); cursor: pointer; transition: all 0.2s; }
.news-item:hover { background: #fafbfc; margin: 0 -12px; padding-left: 12px; padding-right: 12px; border-radius: 8px; }
.news-item:last-child { border-bottom: none; }
.news-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary-light); flex-shrink: 0; }
.news-item-title { font-size: 15px; color: var(--text-primary); flex: 1; min-width: 0; }
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.news-tag { margin-left: 6px; font-size: 11px; }
.news-item-time { font-size: 13px; color: var(--text-secondary); white-space: nowrap; flex-shrink: 0; }
.news-pagination { margin-top: 20px; display: flex; justify-content: center; }
/* ==================== Timeline ==================== */
.timeline-section {
padding: 72px 20px;
background: var(--bg-light);
}
.process-timeline {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 0;
flex-wrap: wrap;
}
.process-step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 140px;
max-width: 180px;
position: relative;
}
.process-step-badge {
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
box-shadow: 0 4px 16px rgba(26, 58, 92, 0.25);
}
.step-number {
font-size: 24px;
font-weight: 800;
color: #fff;
}
.process-step-content {
text-align: center;
}
.step-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.process-step-arrow {
display: flex;
align-items: flex-start;
justify-content: center;
flex: 0 0 auto;
/* 垂直对齐流程点徽章中心:徽章高 64,中心在 32;箭头图标约 20 高,半高 10 → 32-10=22 */
margin: 22px 14px 0;
z-index: 1;
}
.arrow-icon {
font-size: 20px;
color: var(--primary-light);
}
/* ==================== Materials ==================== */
.materials-section {
padding: 72px 20px;
background: var(--bg-white);
}
.materials-card {
background: var(--bg-light);
border-radius: 12px;
padding: 32px;
text-align: center;
}
.materials-alert {
margin-bottom: 24px;
text-align: left;
}
.materials-link {
margin: 0 0 8px;
font-size: 14px;
}
.materials-hint {
margin: 0;
font-size: 13px;
color: var(--text-secondary);
}
.materials-qr {
margin-top: 8px;
}
.qr-placeholder {
width: 140px;
height: 140px;
border: 2px dashed var(--border-light);
border-radius: 8px;
margin: 0 auto 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff;
}
.qr-icon {
font-size: 36px;
color: var(--text-secondary);
}
.qr-text {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.qr-hint {
font-size: 13px;
color: var(--text-secondary);
}
/* ==================== Footer ==================== */
.footer-section {
background: var(--primary-dark);
color: rgba(255, 255, 255, 0.7);
padding: 48px 20px 32px;
text-align: center;
}
.footer-content {
max-width: 800px;
margin: 0 auto;
}
.footer-contact-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 12px;
}
.footer-contact {
font-size: 14px;
margin: 6px 0;
}
.footer-text {
font-size: 13px;
margin-top: 24px;
color: rgba(255, 255, 255, 0.5);
}
.footer-subtext {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
margin-top: 4px;
}
/* ==================== Responsive ==================== */
@media (max-width: 768px) {
.section-title { font-size: 24px; }
.intro-section, .groups-section, .news-section, .timeline-section { padding: 48px 16px; }
.process-step-arrow { display: none; }
.process-timeline { flex-direction: column; gap: 32px; }
.process-step { flex-direction: row; gap: 16px; min-width: auto; width: 100%; max-width: 300px; }
.process-step-badge { margin-bottom: 0; flex-shrink: 0; }
.process-step-content { text-align: left; }
}
</style>