library-picturebook-activity/frontend/src/views/contests/Detail.vue
2026-04-03 15:28:15 +08:00

1322 lines
33 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="contest-detail-page">
<a-spin :spinning="loading">
<!-- 顶部海报区域 -->
<div class="hero-section">
<!-- 背景图 -->
<div
class="hero-bg"
:style="{
backgroundImage:
contest?.posterUrl || contest?.coverUrl
? `url(${contest.posterUrl || contest.coverUrl})`
: undefined,
}"
>
<div class="hero-overlay"></div>
</div>
<!-- 返回按钮 -->
<div class="hero-nav">
<button class="back-btn" @click="$router.back()">
<ArrowLeftOutlined />
<span>返回</span>
</button>
</div>
<!-- 海报内容 -->
<div class="hero-content">
<div class="hero-inner">
<!-- 状态标签 -->
<div class="status-tags">
<span class="tag tag-type">
{{
contest?.contestType === "individual" ? "个人参与" : "团队参与"
}}
</span>
<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 />
<span
>活动时间:{{ formatDate(contest?.startTime) }} ~
{{ formatDate(contest?.endTime) }}</span
>
</div>
<div class="meta-item">
<ClockCircleOutlined />
<span
>报名时间:{{ formatDate(contest?.registerStartTime) }} ~
{{ formatDate(contest?.registerEndTime) }}</span
>
</div>
</div>
<!-- 报名按钮 -->
<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 />
{{ isRegistering ? "立即报名" : "报名已截止" }}
</button>
<span v-if="isRegistering" class="countdown">
距离报名截止还有 <strong>{{ daysRemaining }}</strong> 天
</span>
</div>
</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')"
>
<FileTextOutlined />
<span>活动信息</span>
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'notices' }"
@click="handleTabChange('notices')"
>
<BellOutlined />
<span>通知公告</span>
<span v-if="notices.length > 0" class="badge">{{
notices.length
}}</span>
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'results' }"
@click="handleTabChange('results')"
>
<TrophyOutlined />
<span>活动结果</span>
</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<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 />
</div>
<h2>竞赛详情</h2>
</div>
<div class="card-body">
<div
v-if="contest.content"
class="rich-content"
v-html="contest.content"
></div>
<a-empty v-else description="暂无详情内容" />
</div>
</div>
<!-- 通知公告 Tab -->
<div v-if="activeTab === 'notices'" class="content-card">
<div class="card-header">
<div class="header-icon">
<BellOutlined />
</div>
<h2>通知公告</h2>
</div>
<div class="card-body">
<div v-if="noticesLoading" class="loading-placeholder">
<a-spin />
</div>
<div
v-else-if="notices.length === 0"
class="empty-placeholder"
>
<a-empty description="暂无公告" />
</div>
<div v-else class="notice-list">
<div
v-for="item in notices"
:key="item.id"
class="notice-item"
>
<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>
</div>
</div>
</div>
</div>
<!-- 活动结果 Tab -->
<div v-if="activeTab === 'results'" class="content-card">
<div class="card-header">
<div class="header-icon">
<TrophyOutlined />
</div>
<h2>活动结果</h2>
</div>
<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'">
<div
class="rank-badge"
:class="getRankClass(record.rank)"
>
{{ record.rank || "-" }}
</div>
</template>
<template v-else-if="column.key === 'author'">
{{
record.registration?.user?.nickname ||
record.registration?.team?.teamName ||
"-"
}}
</template>
<template v-else-if="column.key === 'award'">
<span
v-if="record.awardName"
class="award-tag"
:class="getAwardClass(record.awardName)"
>
{{ record.awardName }}
</span>
<span v-else>-</span>
</template>
</template>
</a-table>
</a-spin>
</div>
<div v-else class="empty-placeholder">
<a-empty description="结果尚未公布" />
</div>
</div>
</div>
</div>
<!-- 右侧:组织信息 -->
<div class="content-right">
<div class="sidebar-card">
<div class="sidebar-header">
<EyeOutlined />
<span>可见范围</span>
</div>
<div class="sidebar-body">
<div class="sidebar-item">
<div class="item-label">
<GlobalOutlined />
可见范围
</div>
<div class="item-value">
{{ getVisibilityText(contest?.visibility) }}
</div>
</div>
<div
v-if="contest?.visibility === 'designated' && contest?.contestTenantInfos?.length"
class="sidebar-item"
>
<div class="item-label">
<TeamOutlined />
开放机构
</div>
<div class="item-value">
<div
v-for="tenant in contest.contestTenantInfos"
:key="tenant.id"
class="org-name"
>
{{ tenant.name }}{{ tenant.code }}
</div>
</div>
</div>
</div>
</div>
<div class="sidebar-card">
<div class="sidebar-header">
<InfoCircleOutlined />
<span>基本信息</span>
</div>
<div class="sidebar-body">
<div class="sidebar-item">
<div class="item-label">
<ClockCircleOutlined />
创建时间
</div>
<div class="item-value">
{{ formatDateTime(contest?.createTime) }}
</div>
</div>
</div>
</div>
<div class="sidebar-card">
<div class="sidebar-header">
<TeamOutlined />
<span>组织信息</span>
</div>
<div class="sidebar-body">
<div class="sidebar-item">
<div class="item-label">
<BankOutlined />
主办单位
</div>
<div class="item-value">
<template v-if="contest.organizers">
<template v-if="Array.isArray(contest.organizers)">
<div
v-for="org in contest.organizers"
:key="org"
class="org-name"
>
{{ org }}
</div>
</template>
<div v-else-if="contest.organizers" class="org-name">
{{ contest.organizers }}
</div>
</template>
<span v-else class="empty-text">暂无</span>
</div>
</div>
<div class="sidebar-item">
<div class="item-label">
<ApartmentOutlined />
协办单位
</div>
<div class="item-value">
<template v-if="contest.coOrganizers">
<template v-if="Array.isArray(contest.coOrganizers)">
<div
v-for="org in contest.coOrganizers"
:key="org"
class="org-name"
>
{{ org }}
</div>
</template>
<div v-else-if="contest.coOrganizers" class="org-name">
{{ contest.coOrganizers }}
</div>
</template>
<span v-else class="empty-text">暂无</span>
</div>
</div>
<div class="sidebar-item">
<div class="item-label">
<GiftOutlined />
赞助单位
</div>
<div class="item-value">
<template v-if="contest.sponsors">
<template v-if="Array.isArray(contest.sponsors)">
<div
v-for="sp in contest.sponsors"
:key="sp"
class="org-name"
>
{{ sp }}
</div>
</template>
<div v-else-if="contest.sponsors" class="org-name">
{{ contest.sponsors }}
</div>
</template>
<span v-else class="empty-text">暂无</span>
</div>
</div>
</div>
</div>
<!-- 联系方式 -->
<div
v-if="contest.contactName || contest.contactPhone"
class="sidebar-card"
>
<div class="sidebar-header">
<PhoneOutlined />
<span>联系方式</span>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
<a-empty
v-else-if="!loading"
description="活动不存在"
style="padding: 100px 0"
/>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue"
import { useRoute, useRouter } from "vue-router"
import { message } from "ant-design-vue"
import {
ArrowLeftOutlined,
CalendarOutlined,
ClockCircleOutlined,
FormOutlined,
EyeOutlined,
FileTextOutlined,
BellOutlined,
TrophyOutlined,
TeamOutlined,
BankOutlined,
ApartmentOutlined,
GiftOutlined,
PhoneOutlined,
UserOutlined,
GlobalOutlined,
InfoCircleOutlined,
} from "@ant-design/icons-vue"
import dayjs from "dayjs"
import { useAuthStore } from "@/stores/auth"
import {
contestsApi,
noticesApi,
resultsApi,
registrationsApi,
type Contest,
type ContestNotice,
type ContestResult,
} from "@/api/contests"
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const tenantCode = route.params.tenantCode as string
const loading = ref(false)
const noticesLoading = ref(false)
const resultsLoading = ref(false)
const contest = ref<Contest | null>(null)
const notices = ref<ContestNotice[]>([])
const results = ref<ContestResult[]>([])
const activeTab = ref("info")
const hasRegistered = ref(false)
const myRegistration = ref<any>(null)
const canViewRegistration = computed(() => {
const permissions = authStore.user?.permissions || []
return (
permissions.includes("registration:read") ||
permissions.includes("registration:create")
)
})
const isTeacher = computed(() => authStore.hasRole("teacher"))
const contestId = Number(route.params.id)
const resultsPagination = ref({
current: 1,
pageSize: 20,
total: 0,
})
const resultColumns = [
{ title: "排名", key: "rank", dataIndex: "rank", width: 80 },
{ title: "作品名称", key: "title", dataIndex: "title", width: 200 },
{ title: "作者", key: "author", width: 150 },
{
title: "最终得分",
key: "finalScore",
dataIndex: "finalScore",
width: 120,
sorter: true,
},
{ title: "奖项", key: "award", width: 120 },
]
const formatDate = (dateStr?: string) => {
if (!dateStr) return "-"
return dayjs(dateStr).format("YYYY-MM-DD")
}
const formatDateTime = (dateStr?: string) => {
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)
})
const isSubmitting = computed(() => {
if (!contest.value?.submitStartTime || !contest.value?.submitEndTime)
return false
const now = dayjs()
return (
now.isAfter(dayjs(contest.value.submitStartTime)) &&
now.isBefore(dayjs(contest.value.submitEndTime))
)
})
const isReviewing = computed(() => {
if (!contest.value?.reviewStartTime || !contest.value?.reviewEndTime)
return false
const now = dayjs()
return (
now.isAfter(dayjs(contest.value.reviewStartTime)) &&
now.isBefore(dayjs(contest.value.reviewEndTime))
)
})
const daysRemaining = computed(() => {
if (!contest.value || !isRegistering.value) return 0
const diff = dayjs(contest.value.registerEndTime).diff(dayjs(), "day")
return diff > 0 ? diff : 0
})
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 ""
}
const getNoticeTypeText = (type?: string) => {
switch (type) {
case "urgent":
return "紧急"
case "system":
return "系统"
default:
return "公告"
}
}
const getVisibilityText = (visibility?: string) => {
switch (visibility) {
case "public":
return "公开(所有公众用户可见)"
case "designated":
return "指定机构"
case "internal":
return "仅内部"
default:
return "未知"
}
}
const getRankClass = (rank?: number) => {
if (rank === 1) return "rank-1"
if (rank === 2) return "rank-2"
if (rank === 3) return "rank-3"
return ""
}
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 ""
}
const fetchContestDetail = async () => {
loading.value = true
try {
contest.value = await contestsApi.getDetail(contestId)
await checkRegistration()
} catch (error: any) {
message.error(error?.response?.data?.message || "获取活动详情失败")
} finally {
loading.value = false
}
}
const checkRegistration = async () => {
if (!authStore.user) return
try {
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)
}
}
const fetchNotices = async () => {
noticesLoading.value = true
try {
notices.value = await noticesApi.getList(contestId)
} catch (error: any) {
message.error("获取公告列表失败")
} finally {
noticesLoading.value = false
}
}
const fetchResults = async () => {
if (!contest.value || contest.value.resultState !== "published") return
resultsLoading.value = true
try {
const response = await resultsApi.getResults(
contestId,
resultsPagination.value.current,
resultsPagination.value.pageSize
)
results.value = response.list || []
resultsPagination.value.total = response.total || 0
} catch (error: any) {
message.error("获取活动结果失败")
} finally {
resultsLoading.value = false
}
}
const handleResultsTableChange = (pag: any) => {
resultsPagination.value.current = pag.current || 1
resultsPagination.value.pageSize = pag.pageSize || 20
fetchResults()
}
const handleRegister = () => {
if (!authStore.user) {
message.warning("请先登录")
router.push(`/${tenantCode}/login`)
return
}
if (!contest.value) return
if (contest.value.contestType === "team") {
router.push(`/${tenantCode}/contests/${contestId}/register/team`)
} else {
router.push(`/${tenantCode}/contests/${contestId}/register/individual`)
}
}
const handleViewRegistration = () => {
if (!contest.value) return
if (contest.value.contestType === "team") {
router.push(`/${tenantCode}/contests/${contestId}/register/team`)
} else {
router.push(`/${tenantCode}/contests/${contestId}/register/individual`)
}
}
const handleTabChange = (key: string) => {
activeTab.value = key
if (key === "results" && contest.value?.resultState === "published") {
fetchResults()
}
}
onMounted(() => {
fetchContestDetail()
fetchNotices()
})
</script>
<style lang="scss" scoped>
$primary: #1890ff;
$primary-dark: #0958d9;
.contest-detail-page {
min-height: 100vh;
background: #f5f7fa;
}
// 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;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.6) 100%
);
}
.hero-nav {
position: relative;
z-index: 10;
padding: 20px 24px;
.back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
}
.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;
width: 100%;
}
}
.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 {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.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);
}
}
.hero-title {
font-size: 36px;
font-weight: 700;
color: #fff;
margin: 0 0 20px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
line-height: 1.3;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-bottom: 24px;
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
.anticon {
font-size: 16px;
}
}
}
.hero-actions {
display: flex;
align-items: center;
gap: 16px;
.action-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
border-radius: 24px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
border: none;
&.primary {
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
color: #fff;
box-shadow: 0 4px 15px rgba($primary, 0.4);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba($primary, 0.5);
}
}
&.secondary {
background: rgba(255, 255, 255, 0.9);
color: $primary;
&:hover {
background: #fff;
}
}
&.disabled {
background: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
cursor: not-allowed;
}
}
.countdown {
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
strong {
color: #ffc53d;
font-size: 18px;
}
}
}
}
// Tab 导航
.tab-section {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: sticky;
top: -20px;
z-index: 100;
.tab-container {
padding: 0 24px;
}
.custom-tabs {
display: flex;
gap: 8px;
padding: 12px 0;
.tab-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
transition: all 0.3s;
position: relative;
&:hover {
color: $primary;
background: rgba($primary, 0.06);
}
&.active {
color: #fff;
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
box-shadow: 0 4px 12px rgba($primary, 0.3);
}
.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;
}
}
}
}
// 主内容区域
.main-section {
padding: 24px 0 48px;
.main-container {
padding: 0;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
}
}
// 内容卡片
.content-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
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;
}
h2 {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin: 0;
}
}
.card-body {
padding: 24px;
}
}
// 富文本内容
.rich-content {
font-size: 14px;
color: rgba(0, 0, 0, 0.75);
line-height: 1.8;
:deep(p) {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
:deep(h1),
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
&:first-child {
margin-top: 0;
}
}
:deep(img) {
max-width: 100%;
border-radius: 8px;
margin: 16px 0;
}
:deep(ul),
:deep(ol) {
padding-left: 24px;
margin-bottom: 16px;
}
:deep(li) {
margin-bottom: 8px;
}
:deep(table) {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
th,
td {
border: 1px solid #e8e8e8;
padding: 12px;
text-align: left;
}
th {
background: #fafafa;
font-weight: 600;
}
}
:deep(blockquote) {
margin: 16px 0;
padding: 12px 20px;
background: #f9f9f9;
border-left: 4px solid $primary;
color: rgba(0, 0, 0, 0.65);
}
:deep(a) {
color: $primary;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
// 公告列表
.notice-list {
.notice-item {
padding: 16px;
background: #fafafa;
border-radius: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.notice-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.notice-title {
font-size: 15px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.notice-type {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
&.urgent {
background: #fff2f0;
color: #ff4d4f;
}
&.system {
background: #e6f7ff;
color: #1890ff;
}
}
}
.notice-content {
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.6;
margin-bottom: 10px;
}
.notice-time {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
gap: 6px;
}
}
}
// 排名徽章
.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;
color: rgba(0, 0, 0, 0.65);
&.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;
}
}
// 奖项标签
.award-tag {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
&.award-gold {
background: #fffbe6;
color: #d48806;
}
&.award-silver {
background: #f5f5f5;
color: #595959;
}
&.award-bronze {
background: #fff7e6;
color: #d46b08;
}
}
// 侧边栏卡片
.sidebar-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
background: linear-gradient(
135deg,
rgba($primary, 0.05),
rgba($primary-dark, 0.08)
);
border-bottom: 1px solid #f0f0f0;
font-size: 15px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
.anticon {
color: $primary;
font-size: 16px;
}
}
.sidebar-body {
padding: 16px 20px;
}
.sidebar-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.item-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 8px;
.anticon {
font-size: 14px;
}
}
.item-value {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
line-height: 1.6;
.org-name {
padding: 4px 0;
border-bottom: 1px dashed #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.empty-text {
color: rgba(0, 0, 0, 0.25);
}
}
}
}
// 占位状态
.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;
.hero-title {
font-size: 24px;
}
.hero-meta {
flex-direction: column;
gap: 12px;
}
.hero-actions {
flex-wrap: wrap;
}
}
.tab-section .custom-tabs {
overflow-x: auto;
padding-bottom: 8px;
.tab-item {
white-space: nowrap;
padding: 8px 16px;
}
}
}
</style>