Files
BYW-VoiceHub/old-index.vue
T
Henry_Du 9fc5e3e607
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Canceled after 0s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Canceled after 0s
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Canceled after 1m27s
fix(ui): 批量替换所有组件中残留的硬编码深色值以彻底支持浅色主题
2026-09-12 15:56:10 +08:00

3662 lines
98 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">
<div class="ellipse-effect" />
<div class="main-content">
<div class="top-bar">
<div class="logo-section">
<NuxtLink class="logo-link" to="/">
<img
alt="VoiceHub Logo"
class="logo-image"
:src="logo"
width="90"
height="90"
decoding="async"
fetchpriority="high"
>
</NuxtLink>
<!-- 妯嚎鍜屽鏍ogo -->
<div v-if="schoolLogoHomeUrl && schoolLogoHomeUrl.trim()" class="logo-divider-container">
<div class="logo-divider" />
<img
:src="proxiedSchoolLogoUrl"
alt="瀛︽牎Logo"
class="school-logo"
width="200"
height="80"
decoding="async"
fetchpriority="high"
>
</div>
</div>
<!-- 鐢ㄦ埛淇℃伅鍖哄煙 -->
<div class="user-section">
<ClientOnly>
<!-- 涓婚鍒囨崲鎸夐挳 -->
<button
class="utility-btn theme-toggle-btn"
type="button"
aria-label="涓婚"
title="鍒囨崲涓婚"
@click="handleToggleTheme"
>
<Icon :name="isDark ? 'sun' : 'moon'" :size="18" />
<span class="utility-btn-label">涓婚</span>
</button>
<!-- 璇█鍒囨崲鎸夐挳锛堝崰浣嶆寜閽紝褰撳墠鍗曡绉嶏級 -->
<button
class="utility-btn lang-toggle-btn"
type="button"
aria-label="璇█"
title="璇█"
@click="handleLanguageClick"
>
<Icon name="globe" :size="18" />
<span class="utility-btn-label">璇█</span>
</button>
<div v-if="isClientAuthenticated" class="user-info">
<div class="user-details-desktop">
<span class="user-name">{{ user?.name || '鐢ㄦ埛' }}</span>
<span v-if="isAdmin" class="user-badge admin">{{ roleName }}</span>
<span v-else class="user-badge">{{ userClassInfo }}</span>
</div>
<NuxtLink v-if="isAdmin" class="enter-admin-btn" to="/dashboard">
<Icon name="settings" :size="16" />
<span>杩涘叆鍚庡彴</span>
</NuxtLink>
<div class="user-avatar-wrapper" @click="toggleUserActions">
<img
v-if="user?.avatar && !avatarError"
:src="user.avatar"
class="user-avatar"
@error="avatarError = true"
>
<div v-else class="user-avatar-placeholder">
{{ user?.name?.[0] || 'U' }}
</div>
</div>
<Transition name="dropdown-fade">
<div v-if="showUserActions" class="user-actions-dropdown">
<NuxtLink class="action-item" to="/account">
<Icon name="user" :size="16" />
<span>璐﹀彿绠$悊</span>
</NuxtLink>
<NuxtLink v-if="isAdmin" class="action-item" to="/dashboard">
<Icon name="settings" :size="16" />
<span>绠$悊鍚庡彴</span>
</NuxtLink>
<button class="action-item logout" @click="handleLogout">
<Icon name="logout" :size="16" />
<span>閫€鍑虹櫥褰?/span>
</button>
</div>
</Transition>
</div>
<div v-else class="login-options">
<NuxtLink class="login-btn" to="/login">
<Icon name="user" :size="16" />
<span>鐧诲綍/娉ㄥ唽</span>
</NuxtLink>
</div>
</ClientOnly>
</div>
</div>
<div v-if="siteTitle" class="site-title">
<div class="title-container">
<h2 class="main-title">{{ siteTitle }}</h2>
<div class="title-divider" />
<span class="sub-title">VoiceHub 鏍″洯骞挎挱绯荤粺</span>
</div>
</div>
<!-- 涓棿涓讳綋鍐呭鍖哄煙 -->
<div class="content-area">
<!-- 閫夐」鍗″尯鍩?-->
<div class="tabs-row">
<div
:class="{ active: activeTab === 'schedule' }"
class="section-tab schedule-tab"
@click="handleTabClick('schedule')"
>
<Icon class="tab-icon" name="calendar" :size="20" />
<span class="tab-text">鎾嚭鎺掓湡</span>
</div>
<div
:class="{ active: activeTab === 'songs' }"
class="section-tab songs-tab"
@click="handleTabClick('songs')"
>
<Icon class="tab-icon" name="music" :size="20" />
<span class="tab-text">姝屾洸鍒楄〃</span>
</div>
<div
:class="{ active: activeTab === 'request' }"
class="section-tab request-tab"
@click="handleTabClick('request')"
>
<Icon class="tab-icon" name="search" :size="20" />
<span class="tab-text">鎶曠姝屾洸</span>
</div>
<ClientOnly>
<div
ref="notificationTabRef"
:class="{ active: activeTab === 'notification', disabled: !isClientAuthenticated }"
class="section-tab"
data-tab="notification"
@click="isClientAuthenticated ? handleTabClick('notification') : showLoginNotice()"
>
<div class="icon-wrapper">
<Icon class="tab-icon" name="message-circle" :size="20" />
<span
v-if="isClientAuthenticated && hasUnreadNotifications"
class="notification-badge-tab"
/>
</div>
<span class="tab-text">
娑堟伅
<span
v-if="isClientAuthenticated && hasUnreadNotifications"
class="notification-badge-desktop"
/>
</span>
</div>
<template #fallback>
<div class="section-tab disabled" data-tab="notification">
<Icon class="tab-icon" name="message-circle" :size="20" />
<span class="tab-text">娑堟伅</span>
</div>
</template>
</ClientOnly>
</div>
<!-- 鍐呭鍖哄煙 -->
<div class="tab-content-container">
<ClientOnly>
<!-- 鎾嚭鎺掓湡鍐呭 -->
<div v-if="activeTab === 'schedule'" key="schedule" class="tab-pane schedule-tab-pane">
<div class="full-width">
<SongsScheduleList
:error="error"
:loading="loading"
:schedules="publicSchedules"
@semester-change="handleSemesterChange"
/>
</div>
<!-- 鐢ㄦ埛浣跨敤鎸囧崡缁勪欢 -->
<UserTutorial />
</div>
<!-- 姝屾洸鍒楄〃鍐呭 -->
<div v-else-if="activeTab === 'songs'" key="songs" class="tab-pane">
<div class="song-list-container">
<SongsSongList
:error="error"
:is-admin="isAdmin"
:loading="loading"
:songs="filteredSongs"
@refresh="refreshSongs"
@vote="handleVote"
@withdraw="handleWithdraw"
@cancel-replay="handleCancelReplay"
@request-replay="handleRequestReplay"
@semester-change="handleSemesterChange"
/>
</div>
</div>
<!-- 鎶曠姝屾洸鍐呭 -->
<div v-else-if="activeTab === 'request'" key="request" class="tab-pane request-pane">
<SongsRequestForm
ref="requestFormRef"
:loading="loading"
@request="handleRequest"
@vote="handleVote"
/>
</div>
<!-- 閫氱煡鍐呭 -->
<div
v-else-if="activeTab === 'notification'"
key="notification"
class="tab-pane notification-pane"
>
<div v-if="!isClientAuthenticated" class="login-required-container">
<div class="login-required-content">
<Lock class="login-icon" :size="48" />
<h3>闇€瑕佺櫥褰?/h3>
<p>鎮ㄩ渶瑕佺櫥褰曟墠鑳芥煡鐪嬮€氱煡</p>
<button class="login-button" @click="navigateToLogin">绔嬪嵆鐧诲綍</button>
</div>
</div>
<div v-else class="notification-container">
<!-- 鏍囬鍜岃缃寜閽?-->
<div class="notification-header">
<h2 class="notification-title">閫氱煡涓績</h2>
<button class="settings-icon" @click="toggleNotificationSettings">
<svg
fill="none"
height="20"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
</button>
</div>
<!-- 閫氱煡鍒楄〃 -->
<div class="notification-list">
<div v-if="notificationsLoading" class="loading-indicator">
<div class="loading-spinner" />
<span>鍔犺浇涓?..</span>
</div>
<div v-else-if="userNotifications.length === 0" class="empty-notification">
<div class="empty-icon">
<Icon :size="48" color="#6b7280" name="bell" />
</div>
<p>鏆傛棤閫氱煡</p>
</div>
<Transition mode="out-in" name="notification-list-fade">
<div
v-if="userNotifications.length > 0"
:key="notificationsService.currentPage.value"
class="notification-items"
>
<div
v-for="(notification, index) in userNotifications"
:key="notification.id"
:class="{ unread: !notification.read }"
:style="{ '--animation-delay': index * 0.1 + 's' }"
class="notification-card"
@click="viewNotification(notification)"
>
<div class="notification-card-header">
<div class="notification-icon-type">
<Icon
v-if="notification.type === 'SONG_SELECTED'"
:size="20"
color="#4f46e5"
name="check"
/>
<Icon
v-else-if="notification.type === 'SONG_PLAYED'"
:size="20"
color="#10b981"
name="play"
/>
<Icon
v-else-if="notification.type === 'SONG_VOTED'"
:size="20"
color="#f59e0b"
name="thumbs-up"
/>
<Icon
v-else-if="notification.type === 'SONG_REJECTED'"
:size="20"
color="#ef4444"
name="x-circle"
/>
<Icon
v-else-if="notification.type === 'COLLABORATION_INVITE'"
:size="20"
color="#0B5AFE"
name="users"
/>
<Icon
v-else-if="notification.type === 'COLLABORATION_RESPONSE'"
:size="20"
color="#8b5cf6"
name="message-circle"
/>
<Icon v-else :size="20" color="#6b7280" name="bell" />
</div>
<div class="notification-title-row">
<div class="notification-title">
<span v-if="notification.type === 'SONG_SELECTED'">姝屾洸宸查€変腑</span>
<span v-else-if="notification.type === 'SONG_PLAYED'"
>姝屾洸宸叉挱鏀?/span
>
<span v-else-if="notification.type === 'SONG_VOTED'">鏀跺埌鏂版姇绁?/span>
<span v-else-if="notification.type === 'SONG_REJECTED'"
>姝屾洸琚┏鍥?/span
>
<span v-else-if="notification.type === 'COLLABORATION_INVITE'">
鑱斿悎鎶曠閭€璇? <span
v-if="notification.handled"
:class="[
'status-tag',
notification.status === 'ACCEPTED'
? 'accepted'
: notification.status === 'INVALID'
? 'invalid'
: 'rejected'
]"
>
{{
notification.status === 'ACCEPTED'
? '- 宸叉帴鍙?
: notification.status === 'INVALID'
? '- 宸插け鏁?
: '- 宸叉嫆缁?
}}
</span>
</span>
<span v-else-if="notification.type === 'COLLABORATION_RESPONSE'"
>鑱斿悎鎶曠鍥炲</span
>
<span v-else>绯荤粺閫氱煡</span>
<span v-if="!notification.read" class="unread-indicator" />
</div>
<div class="notification-time">
{{ formatNotificationTime(notification.createdAt) }}
</div>
</div>
</div>
<div class="notification-card-body">
<div class="notification-text">{{ notification.message }}</div>
<!-- 鑱斿悎鎶曠閭€璇锋搷浣滄寜閽?-->
<div
v-if="
notification.type === 'COLLABORATION_INVITE' && !notification.handled
"
class="invite-actions"
>
<button
:disabled="notification.processing"
class="action-button accept-btn"
@click.stop="handleCollaborationReply(notification, true)"
>
{{ notification.processing ? '澶勭悊涓?..' : '鎺ュ彈閭€璇? }}
</button>
<button
:disabled="notification.processing"
class="action-button reject-btn"
@click.stop="handleCollaborationReply(notification, false)"
>
鎷掔粷
</button>
</div>
</div>
<div class="notification-card-actions">
<button
class="action-button delete"
title="鍒犻櫎"
@click.stop="deleteNotification(notification.id)"
>
<svg
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="3 6 5 6 21 6" />
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/>
</svg>
<span>鍒犻櫎</span>
</button>
</div>
</div>
</div>
</Transition>
</div>
<!-- 鍒嗛〉鎺т欢 -->
<div
v-if="notificationsService.totalCount.value > 0"
class="notification-pagination"
>
<div class="pagination-info">
<span class="pagination-text">
鍏?{{ notificationsService.totalCount.value }} 鏉¢€氱煡锛?绗? {{ notificationsService.currentPage.value }} /
{{ notificationsService.totalPages.value }} 椤? </span>
</div>
<div class="pagination-controls">
<!-- 姣忛〉鏄剧ず鏁伴噺閫夋嫨鍣?-->
<div class="page-size-selector">
<label for="pageSize">姣忛〉鏄剧ず锛?/label>
<CustomSelect
id="pageSize"
:model-value="notificationsService.pageSize.value"
:options="pageSizeOptions"
class="page-size-custom-select"
@update:model-value="handlePageSizeChange"
/>
</div>
<!-- 椤电爜瀵艰埅 -->
<div class="page-navigation">
<button
:disabled="
!notificationsService.hasPrevPage.value ||
notificationsService.isPaginationLoading.value
"
class="page-nav-button"
title="涓婁竴椤?
@click="notificationsService.prevPage()"
>
<svg
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<!-- 椤电爜鎸夐挳 -->
<div class="page-numbers">
<template v-for="page in getVisiblePages()" :key="page">
<button
v-if="page !== '...'"
:class="[
'page-number-button',
{ active: page === notificationsService.currentPage.value }
]"
:disabled="notificationsService.isPaginationLoading.value"
@click="notificationsService.goToPage(page)"
>
{{ page }}
</button>
<span v-else class="page-ellipsis">...</span>
</template>
</div>
<button
:disabled="
!notificationsService.hasNextPage.value ||
notificationsService.isPaginationLoading.value
"
class="page-nav-button"
title="涓嬩竴椤?
@click="notificationsService.nextPage()"
>
<svg
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
</div>
</div>
<!-- 鍒嗛〉鍔犺浇鐘舵€?-->
<div
v-if="notificationsService.isPaginationLoading.value"
class="pagination-loading"
>
<div class="loading-spinner" />
<span>鍔犺浇涓?..</span>
</div>
</div>
<!-- 搴曢儴鎿嶄綔鎸夐挳 -->
<div v-if="userNotifications.length > 0" class="notification-actions-bar">
<button
:class="{ disabled: !hasUnreadNotifications }"
:disabled="!hasUnreadNotifications"
class="action-button-large"
@click="markAllNotificationsAsRead"
>
鍏ㄩ儴鏍囪涓哄凡璇? </button>
<button class="action-button-large danger" @click="clearAllNotifications">
娓呯┖鎵€鏈夋秷鎭? </button>
</div>
<!-- 纭瀵硅瘽妗?-->
<ConfirmDialog
v-model:show="showConfirmDialog"
:cancel-text="confirmDialogConfig.cancelText"
:confirm-text="confirmDialogConfig.confirmText"
:message="confirmDialogConfig.message"
:title="confirmDialogConfig.title"
:type="confirmDialogConfig.type"
@cancel="handleCancelAction"
@confirm="handleConfirmAction"
/>
</div>
</div>
</ClientOnly>
</div>
</div>
<!-- 椤佃剼淇℃伅鏄剧ず -->
<SiteFooter />
</div>
<!-- 瑙勫垯寮圭獥 -->
<Teleport to="body">
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="showRules"
class="fixed inset-0 z-[70] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
@click.self="showRules = false"
>
<div
class="bg-zinc-900 border border-zinc-800 w-full max-w-lg rounded-3xl overflow-hidden shadow-2xl flex flex-col"
>
<div class="p-8 pb-4 flex items-center justify-between">
<div>
<h3 class="text-xl font-black text-zinc-100 tracking-tight flex items-center gap-3">
<div
class="w-10 h-10 rounded-2xl bg-blue-600/10 flex items-center justify-center text-blue-500"
>
<Icon name="bell" :size="20" />
</div>
鐐规瓕瑙勫垯
</h3>
<p class="text-xs text-zinc-500 mt-1 ml-13">鎶曠鍓嶈浠旂粏闃呰浠ヤ笅瑙勫垯</p>
</div>
<button
class="p-3 bg-zinc-800/50 hover:bg-zinc-800 text-zinc-500 hover:text-zinc-200 rounded-2xl transition-all"
@click="showRules = false"
>
<Icon name="x" :size="20" />
</button>
</div>
<div class="p-8 pt-4 space-y-8">
<div class="rules-group space-y-4">
<h4
class="text-[10px] font-black text-zinc-500 uppercase tracking-widest flex items-center gap-2"
>
<Icon name="message-circle" :size="12" />
鎶曠椤荤煡
</h4>
<div
v-if="submissionGuidelines"
class="text-sm text-zinc-400 leading-relaxed font-medium bg-zinc-950/50 p-6 rounded-3xl border border-zinc-800/50"
v-html="submissionGuidelines.replace(/\n/g, '<br>')"
/>
<div
v-else
class="space-y-3 bg-zinc-950/50 p-6 rounded-3xl border border-zinc-800/50"
>
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
<span class="text-blue-500 font-black">01</span>
<p>鎶曠鏃舵棤闇€鍔犲叆涔﹀悕鍙?/p>
</div>
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
<span class="text-blue-500 font-black">02</span>
<p>闄J澶栵紝鍏朵粬绫诲瀷姝屾洸鍧囨帴鏀讹紙鍖呮嫭灏忚绉嶏級</p>
</div>
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
<span class="text-blue-500 font-black">03</span>
<p>绂佹鎶曢€掑惈鏈夎繚瑙勫唴瀹圭殑姝屾洸</p>
</div>
</div>
</div>
<div class="rules-group space-y-4">
<h4
class="text-[10px] font-black text-zinc-500 uppercase tracking-widest flex items-center gap-2"
>
<Icon name="calendar" :size="12" />
鎾斁鏃堕棿
</h4>
<div
class="bg-blue-600/10 border border-blue-500/20 p-6 rounded-3xl flex items-center gap-4"
>
<div
class="w-12 h-12 rounded-2xl bg-blue-600 flex items-center justify-center text-white shadow-lg shadow-blue-900/40"
>
<Icon name="clock" :size="24" />
</div>
<div>
<p class="text-sm font-black text-zinc-100">姣忓ぉ澶滆嚜淇潤鐝墠</p>
<p class="text-[10px] text-zinc-500 font-bold uppercase tracking-widest mt-0.5">
PLAYBACK TIME
</p>
</div>
</div>
</div>
</div>
<div class="p-8 pt-0">
<button
class="w-full px-6 py-4 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-black rounded-2xl transition-all uppercase tracking-widest shadow-lg active:scale-95"
@click="showRules = false"
>
鎴戠煡閬撲簡
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
<div
v-if="statisticsCodeEnabled && statisticsCode"
id="site-statistics-code"
class="hidden"
v-html="statisticsCode"
/>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { Lock } from '@lucide/vue'
import { useRouter } from 'vue-router'
import logo from '~~/public/images/logo.png'
import Icon from '~/components/UI/Icon.vue'
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
import { useNotifications } from '~/composables/useNotifications'
import { useSiteConfig } from '~/composables/useSiteConfig'
import { useTheme } from '~/composables/useTheme'
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
// 鑾峰彇杩愯鏃堕厤缃?const config = useRuntimeConfig()
const router = useRouter()
// 涓婚
const { isDark, toggleTheme } = useTheme()
const handleToggleTheme = () => {
toggleTheme()
}
const handleLanguageClick = () => {
if (typeof window !== 'undefined' && window.$showNotification) {
window.$showNotification('褰撳墠鐗堟湰鏆備笉鏀寔澶氳瑷€鍒囨崲', 'info')
}
}
// 绔欑偣閰嶇疆
const {
siteTitle,
description: siteDescription,
guidelines: submissionGuidelines,
icp: icpNumber,
schoolLogoHomeUrl,
statisticsCode,
statisticsCodeEnabled,
initSiteConfig
} = useSiteConfig()
const auth = useAuth()
const isClientAuthenticated = computed(() => auth?.isAuthenticated?.value || false)
const isAdmin = computed(() => auth?.isAdmin?.value || false)
const user = computed(() => auth?.user?.value || null)
const roleName = computed(() => {
const role = user.value?.role
const map = {
ADMIN: '閮ㄩ暱',
SUPER_ADMIN: '鎶€鏈繍缁?,
SONG_ADMIN: '宸ヤ綔浜哄憳',
VICE_MINISTER: '鍓儴',
RESIDENT: '甯搁┗',
TEACHER: '鏁欏笀',
USER: '瀛︾敓锛堟櫘閫氱敤鎴凤級'
}
return map[role] || '绠$悊鍛?
})
const userClassInfo = computed(() => {
if (user.value?.grade && user.value?.class) {
return `${user.value.grade} ${user.value.class}`
}
return '鍚屽'
})
const songs = useSongs()
// 绔嬪嵆鍒濆鍖栭€氱煡鏈嶅姟锛岄伩鍏嶆椂搴忛棶棰?const notificationsService = useNotifications()
const unreadNotificationCount = ref(0)
// 妯℃嫙鏁版嵁鍒濆鍊?const songCount = ref(0)
const scheduleCount = ref(0)
const isRequestOpen = ref(true)
// 寮圭獥鐘舵€?const showRequestModal = ref(false)
const showRules = ref(false)
const showUserActions = ref(false)
const avatarError = ref(false)
const toggleUserActions = (event) => {
event.stopPropagation()
showUserActions.value = !showUserActions.value
}
// 鐩戝惉鐢ㄦ埛澶村儚鍙樺寲锛岄噸缃敊璇姸鎬?watch(
() => user.value?.avatar,
() => {
avatarError.value = false
}
)
// 鐐瑰嚮澶栭儴鍏抽棴涓嬫媺鑿滃崟
const handleClickOutside = (event) => {
if (showUserActions.value) {
const dropdown = document.querySelector('.user-actions-dropdown')
const avatar = document.querySelector('.user-avatar-wrapper')
if (dropdown && !dropdown.contains(event.target) && !avatar.contains(event.target)) {
showUserActions.value = false
}
}
}
onMounted(() => {
window.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
window.removeEventListener('click', handleClickOutside)
})
// 鏍囩椤电姸鎬?const activeTab = ref('schedule') // 榛樿鏄剧ず鎾嚭鎺掓湡
const tabOrder = ['schedule', 'songs', 'request', 'notification']
const activeIndex = computed(() => {
const index = tabOrder.indexOf(activeTab.value)
return index === -1 ? 0 : index
})
// 閫氱煡鎸夐挳寮哄埗鏇存柊鐩稿叧
const notificationTabRef = ref(null)
const hasInitializedAuthData = ref(isClientAuthenticated.value)
let refreshInterval = null
let lastDataRevision = ''
const fetchDataRevision = async () => {
try {
const url = isClientAuthenticated.value ? '/api/songs/revision' : '/api/songs/public-revision'
const authConfig = isClientAuthenticated.value ? auth.getAuthConfig() : {}
const result = await $fetch(url, {
...authConfig,
query: { t: Date.now() }
})
return result?.revision || ''
} catch (e) {
console.error('鑾峰彇鏁版嵁淇鍙峰け璐?', e)
return ''
}
}
const setupRefreshInterval = async () => {
if (refreshInterval) {
clearInterval(refreshInterval)
}
const intervalSeconds = Math.max(5, Math.min(30, notificationSettings.value.refreshInterval || 10))
const intervalMs = intervalSeconds * 1000
lastDataRevision = await fetchDataRevision()
refreshInterval = setInterval(async () => {
try {
const nextRevision = await fetchDataRevision()
if (!nextRevision || nextRevision === lastDataRevision) return
lastDataRevision = nextRevision
if (isClientAuthenticated.value) {
await Promise.allSettled([
songs.fetchSongs(false, undefined, true, true),
songs.fetchPublicSchedules(false, undefined, true, true),
loadNotifications()
])
} else {
await Promise.allSettled([songs.fetchPublicSchedules(false, undefined, true, true), songs.fetchSongCount()])
}
await updateSongCounts()
} catch (error) {
console.error('瀹氭湡鍒锋柊澶辫触:', error)
}
}, intervalMs)
}
// 娣诲姞閫氱煡鐩稿叧鍙橀噺
const userNotifications = computed(() => notificationsService?.notifications?.value || [])
const notificationsLoading = computed(() => notificationsService?.loading?.value || false)
const hasUnreadNotifications = computed(() => {
// 纭繚notificationsService宸插垵濮嬪寲涓旀湁unreadCount
if (!notificationsService || !notificationsService.unreadCount) {
return false
}
const unreadCount = notificationsService.unreadCount.value
return unreadCount > 0
})
const showNotificationSettings = ref(false)
const pageSizeOptions = [
{ label: '5鏉?, value: 5 },
{ label: '10鏉?, value: 10 },
{ label: '20鏉?, value: 20 },
{ label: '50鏉?, value: 50 }
]
const notificationSettings = ref({
songSelectedNotify: true,
songPlayedNotify: true,
songVotedNotify: true,
songVotedThreshold: 1,
systemNotify: true,
refreshInterval: 60
})
// 璺宠浆鍒伴€氱煡璁剧疆椤甸潰
const toggleNotificationSettings = () => {
navigateTo('/notification-settings')
}
// 鑾峰彇閫氱煡璁剧疆
const fetchNotificationSettings = async () => {
if (notificationsService) {
await notificationsService.fetchNotificationSettings()
if (notificationsService.settings.value) {
notificationSettings.value = {
songSelectedNotify: notificationsService.settings.value.songSelectedNotify,
songPlayedNotify: notificationsService.settings.value.songPlayedNotify,
songVotedNotify: notificationsService.settings.value.songVotedNotify,
songVotedThreshold: notificationsService.settings.value.songVotedThreshold || 1,
systemNotify: notificationsService.settings.value.systemNotify,
refreshInterval: notificationsService.settings.value.refreshInterval || 60
}
}
}
}
// 淇濆瓨閫氱煡璁剧疆
const saveNotificationSettings = async () => {
if (notificationsService) {
await notificationsService.updateNotificationSettings(notificationSettings.value)
// 濡傛灉鍦ㄩ椤碉紝鏇存柊鍒锋柊闂撮殧
if (typeof setupRefreshInterval === 'function') {
setupRefreshInterval()
}
}
}
// 鍔犺浇閫氱煡
const loadNotifications = async () => {
if (isClientAuthenticated.value && notificationsService) {
try {
await notificationsService.fetchNotifications()
} catch (error) {
console.error('[閫氱煡鑾峰彇] 鍔犺浇閫氱煡澶辫触:', error)
}
}
}
// 鏍囪閫氱煡涓哄凡璇?const markNotificationAsRead = async (id) => {
if (notificationsService) {
await notificationsService.markAsRead(id)
}
}
// 鏍囪鎵€鏈夐€氱煡涓哄凡璇?const markAllNotificationsAsRead = async () => {
try {
if (notificationsService) {
await notificationsService.markAllAsRead()
}
} catch (error) {
console.error('[閫氱煡鎿嶄綔] 鏍囪鎵€鏈夐€氱煡涓哄凡璇诲け璐?', error)
}
}
// 鍒犻櫎閫氱煡
const deleteNotification = async (id) => {
pendingAction.value = 'delete'
pendingId.value = id
confirmDialogConfig.value = {
title: '鍒犻櫎娑堟伅',
message: '纭畾瑕佸垹闄ゆ娑堟伅鍚楋紵',
type: 'warning',
confirmText: '鍒犻櫎',
cancelText: '鍙栨秷'
}
showConfirmDialog.value = true
}
// 娓呯┖鎵€鏈夐€氱煡
const clearAllNotifications = async () => {
pendingAction.value = 'clearAll'
confirmDialogConfig.value = {
title: '娓呯┖鎵€鏈夋秷鎭?,
message: '纭畾瑕佹竻绌烘墍鏈夋秷鎭悧锛熸鎿嶄綔涓嶅彲鎾ら攢銆?,
type: 'danger',
confirmText: '娓呯┖',
cancelText: '鍙栨秷'
}
showConfirmDialog.value = true
}
// 纭瀵硅瘽妗嗙浉鍏崇姸鎬?const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
type: 'warning',
confirmText: '纭畾',
cancelText: '鍙栨秷'
})
const pendingAction = ref('')
const pendingId = ref(null)
// 澶勭悊纭鎿嶄綔
const handleConfirmAction = async () => {
if (notificationsService) {
if (pendingAction.value === 'delete') {
await notificationsService.deleteNotification(pendingId.value)
pendingId.value = null
} else if (pendingAction.value === 'clearAll') {
await notificationsService.clearAllNotifications()
}
}
showConfirmDialog.value = false
pendingAction.value = ''
}
// 澶勭悊鍙栨秷鎿嶄綔
const handleCancelAction = () => {
showConfirmDialog.value = false
pendingAction.value = ''
pendingId.value = null
}
// 鍒嗛〉鐩稿叧鏂规硶
const handlePageSizeChange = async (newSize) => {
const size = parseInt(newSize)
if (notificationsService) {
await notificationsService.changePageSize(size)
}
}
// 鑾峰彇鍙鐨勯〉鐮佸垪琛?const getVisiblePages = () => {
if (!notificationsService) return []
const currentPage = notificationsService.currentPage.value
const totalPages = notificationsService.totalPages.value
const pages = []
if (totalPages <= 7) {
// 鎬婚〉鏁板皯浜庣瓑浜?椤碉紝鏄剧ず鎵€鏈夐〉鐮? for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
} else {
// 鎬婚〉鏁板ぇ浜?椤碉紝鏄剧ず鐪佺暐鍙? if (currentPage <= 4) {
// 褰撳墠椤靛湪鍓嶉潰
for (let i = 1; i <= 5; i++) {
pages.push(i)
}
pages.push('...')
pages.push(totalPages)
} else if (currentPage >= totalPages - 3) {
// 褰撳墠椤靛湪鍚庨潰
pages.push(1)
pages.push('...')
for (let i = totalPages - 4; i <= totalPages; i++) {
pages.push(i)
}
} else {
// 褰撳墠椤靛湪涓棿
pages.push(1)
pages.push('...')
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
pages.push(i)
}
pages.push('...')
pages.push(totalPages)
}
}
return pages
}
// 鏍煎紡鍖栭€氱煡鏃堕棿
const formatNotificationTime = (timeString) => {
const date = new Date(timeString)
const now = new Date()
const diff = now.getTime() - date.getTime()
// 灏忎簬1鍒嗛挓
if (diff < 60000) {
return '鍒氬垰'
}
// 灏忎簬1灏忔椂
if (diff < 3600000) {
return `${Math.floor(diff / 60000)}鍒嗛挓鍓峘
}
// 灏忎簬24灏忔椂
if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}灏忔椂鍓峘
}
// 灏忎簬30澶? if (diff < 2592000000) {
return `${Math.floor(diff / 86400000)}澶╁墠`
}
// 澶т簬30澶╋紝鏄剧ず鍏蜂綋鏃ユ湡
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
// 鐩戝惉鏍囩椤靛垏鎹紝濡傛灉鍒囨崲鍒伴€氱煡鏍囩椤碉紝鍔犺浇閫氱煡
watch(activeTab, (newTab) => {
if (newTab === 'notification') {
loadNotifications()
}
})
watch(
() => auth?.isAuthenticated?.value,
async (newAuthState, oldAuthState) => {
if (newAuthState && !oldAuthState) {
hasInitializedAuthData.value = true
await Promise.allSettled([loadNotifications(), fetchNotificationSettings(), songs.fetchSongs()])
await updateSongCounts()
return
}
if (!newAuthState && oldAuthState) {
hasInitializedAuthData.value = false
await Promise.allSettled([songs.fetchSongCount(), songs.fetchPublicSchedules()])
unreadNotificationCount.value = 0
await updateSongCounts()
}
},
{ flush: 'post' }
)
// 鍒濆鍖栨椂濡傛灉宸茬粡鍦ㄩ€氱煡鏍囩椤碉紝鍒欏姞杞介€氱煡
onMounted(() => {
if (activeTab.value === 'notification') {
loadNotifications()
}
})
// 鑾峰彇褰撳墠鏃ユ湡鍜屾槦鏈?const getCurrentDate = () => {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const date = now.getDate()
const weekDays = ['鏃?, '涓€', '浜?, '涓?, '鍥?, '浜?, '鍏?]
const weekDay = weekDays[now.getDay()]
return `${year}骞?{month}鏈?{date}鏃?鍛?{weekDay}`
}
// RequestForm缁勪欢寮曠敤
const requestFormRef = ref(null)
// 鏃х殑showNotification鍑芥暟宸茬Щ闄わ紝浣跨敤鍏ㄥ眬閫氱煡绯荤粺
// 鏇存柊姝屾洸鏁伴噺缁熻锛堜紭鍖栫増鏈紝閬垮厤閲嶅璇锋眰锛?const updateSongCounts = async (semester = null) => {
try {
// 鏇存柊鎺掓湡姝屾洸鏁伴噺
const schedules = songs?.publicSchedules?.value || []
scheduleCount.value = schedules.length
// 鏇存柊鎬绘瓕鏇叉暟閲? if (isClientAuthenticated.value && songs?.songs?.value) {
// 宸茬櫥褰曠敤鎴凤細浣跨敤瀹屾暣姝屾洸鍒楄〃
songCount.value = songs.songs.value.length
} else {
// 鏈櫥褰曠敤鎴凤細浣跨敤缂撳瓨鐨勬瓕鏇叉€绘暟
songCount.value = songs?.songCount?.value || 0
}
} catch (e) {
console.error('鏇存柊姝屾洸缁熻澶辫触', e)
}
}
// 鐩戝惉siteTitle鍙樺寲锛岀‘淇濋椤祎itle姝g‘璁剧疆
watch(
siteTitle,
(newSiteTitle) => {
if (typeof document !== 'undefined' && newSiteTitle) {
document.title = `棣栭〉 | ${newSiteTitle}`
}
},
{ immediate: true }
)
// 鍦ㄧ粍浠舵寕杞藉悗鍒濆鍖栬璇佸拰姝屾洸锛堝彧浼氬湪瀹㈡埛绔墽琛岋級
onMounted(async () => {
try {
await initSiteConfig()
const currentUser = await auth.initAuth()
if (typeof document !== 'undefined' && siteTitle.value) {
document.title = `棣栭〉 | ${siteTitle.value}`
}
if (isClientAuthenticated.value) {
hasInitializedAuthData.value = true
await Promise.allSettled([
songs.fetchSongs(),
songs.fetchPublicSchedules(),
loadNotifications(),
fetchNotificationSettings()
])
await checkPasswordChangeRequired(currentUser)
} else {
hasInitializedAuthData.value = false
await Promise.allSettled([songs.fetchSongCount(), songs.fetchPublicSchedules()])
}
await updateSongCounts()
await setupRefreshInterval()
if (songs.notification) {
watch(songs.notification, (newVal) => {
if (newVal.show) {
showNotification(newVal.message, newVal.type)
}
})
}
// 娉ㄥ叆绔欑偣缁熻浠g爜锛堝厹搴曟柟妗堬細鍔ㄦ€佹彃鍏?script 鍒?body 鏈熬锛? if (statisticsCodeEnabled.value && statisticsCode.value) {
try {
injectStatisticsCode(statisticsCode.value)
} catch (e) {
console.warn('缁熻浠g爜娉ㄥ叆澶辫触:', e)
}
}
} catch (error) {
console.error('棣栭〉鍒濆鍖栧け璐?', error)
await Promise.allSettled([songs.fetchPublicSchedules(), songs.fetchSongCount()])
await updateSongCounts()
}
})
// 缁勪欢鍗歌浇鏃舵竻闄ゅ畾鏃跺櫒
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval)
}
})
// 瀹炴椂璁$畻姝屾洸鎬绘暟
const realSongCount = computed(() => {
return songs?.visibleSongs?.value?.length || 0
})
// 浣跨敤璁$畻灞炴€у畨鍏ㄥ湴璁块棶鏁版嵁
const publicSchedules = computed(() => songs?.publicSchedules?.value || [])
const allSongs = computed(() => songs?.visibleSongs?.value || [])
const filteredSongs = computed(() => {
// 杩斿洖鎵€鏈夋瓕鏇诧紝浣嗗皢宸叉挱鏀剧殑姝屾洸鎺掑湪鏈€鍚? if (allSongs.value && allSongs.value.length > 0) {
const unplayedSongs = allSongs.value.filter((song) => !song.played)
const playedSongs = allSongs.value.filter((song) => song.played)
return [...unplayedSongs, ...playedSongs]
}
return []
})
const loading = computed(() => songs?.loading?.value || false)
const error = computed(() => songs?.error?.value || '')
// 澶勭悊瀛︽牎logo鐨凥TTP/HTTPS浠g悊
const proxiedSchoolLogoUrl = computed(() => {
if (!schoolLogoHomeUrl.value || !schoolLogoHomeUrl.value.trim()) {
return ''
}
const logoUrl = schoolLogoHomeUrl.value.trim()
// 濡傛灉鏄疕TTP閾炬帴锛岄€氳繃浠g悊璁块棶
if (logoUrl.startsWith('http://')) {
return `/api/proxy/image?url=${encodeURIComponent(logoUrl)}`
}
// HTTPS閾炬帴鎴栫浉瀵硅矾寰勭洿鎺ヨ繑鍥? return logoUrl
})
// 澶勭悊鎶曠璇锋眰
const handleRequest = async (songData, done) => {
if (!auth || !isClientAuthenticated.value) {
if (window.$showNotification) {
window.$showNotification('闇€瑕佺櫥褰曟墠鑳芥姇绋挎瓕鏇?, 'error')
}
showRequestModal.value = false
if (typeof done === 'function') done(false)
return false
}
try {
console.log('澶勭悊姝屾洸璇锋眰:', songData.title)
// 鐩存帴浼犻€掓暣涓猻ongData瀵硅薄锛岀‘淇滼SON鏍煎紡姝g‘
const result = await songs.requestSong(songData)
if (result) {
// 鏄剧ず鎶曠鎴愬姛閫氱煡
if (window.$showNotification) {
window.$showNotification(`銆?{songData.title} - ${songData.artist}銆嬫姇绋挎垚鍔燂紒`, 'success')
}
// 寮哄埗鍒锋柊姝屾洸鍒楄〃
console.log('鎶曠鎴愬姛锛屽埛鏂版瓕鏇插垪琛?)
await refreshSongs()
// 鍒锋柊鎶曠鐘舵€? if (requestFormRef.value && requestFormRef.value.refreshSubmissionStatus) {
await requestFormRef.value.refreshSubmissionStatus()
}
// 濡傛灉褰撳墠鍦ㄦ瓕鏇插垪琛ㄩ〉锛岃嚜鍔ㄥ垏鎹㈠埌璇ラ〉闈? if (activeTab.value !== 'songs') {
setTimeout(() => {
handleTabClick('songs')
}, 500)
}
if (typeof done === 'function') done(true)
return true
}
if (typeof done === 'function') done(false)
return false
} catch (err) {
if (window.$showNotification) {
window.$showNotification(err.message || '鐐规瓕澶辫触', 'error')
}
if (typeof done === 'function') done(false)
return false
}
}
// 澶勭悊鎶曠エ
const handleVote = async (song) => {
if (!isClientAuthenticated.value) {
showNotification('璇峰厛鐧诲綍鍚庡啀鎶曠エ', 'error')
return
}
try {
if (!songs) return
// 璋冪敤鎶曠エAPI - 閫氱煡宸插湪composable涓鐞? // 妫€鏌ユ槸鍚︽槸鍙栨秷鎶曠エ璇锋眰
if (song.unvote) {
// 浼犻€掑畬鏁村璞′互鏀寔鎾ら攢鎶曠エ鍔熻兘
await songs.voteSong(song)
} else {
// 淇濇寔鍚戝悗鍏煎锛屼紶閫扞D
await songs.voteSong(song.id)
}
// 闈欓粯鍒锋柊姝屾洸鍒楄〃浠ヨ幏鍙栨渶鏂扮姸鎬侊紝浣嗕笉褰卞搷褰撳墠瑙嗗浘
setTimeout(() => {
songs.refreshSongsSilent().catch((err) => {
console.error('鍒锋柊姝屾洸鍒楄〃澶辫触', err)
})
}, 500)
} catch (err) {
// 涓嶅仛浠讳綍澶勭悊锛屽洜涓簎seSongs涓凡缁忓鐞嗕簡閿欒鎻愮ず
console.log('API閿欒宸插湪useSongs涓鐞?)
}
}
const handleCancelReplay = async (song) => {
if (!isClientAuthenticated.value) {
showNotification('璇峰厛鐧诲綍鎵嶈兘鍙栨秷閲嶆挱鐢宠', 'error')
return
}
try {
if (!songs) return
await songs.withdrawReplay(song.id)
updateSongCounts()
} catch (err) {
console.log('API閿欒宸插湪useSongs涓鐞?)
}
}
const handleRequestReplay = async (song) => {
if (!isClientAuthenticated.value) {
showNotification('璇峰厛鐧诲綍鎵嶈兘鐢宠閲嶆挱', 'error')
return
}
try {
if (!songs) return
await songs.requestReplay(song.id)
updateSongCounts()
} catch (err) {
console.log('API閿欒宸插湪useSongs涓鐞?)
}
}
// 澶勭悊鎾ゅ洖鎶曠
const handleWithdraw = async (song) => {
if (!isClientAuthenticated.value) {
showNotification('璇峰厛鐧诲綍鎵嶈兘鎾ゅ洖鎶曠', 'error')
return
}
try {
if (!songs) return
// 璋冪敤鎾ゅ洖API - 閫氱煡宸插湪composable涓鐞? await songs.withdrawSong(song.id)
// 鏇存柊璁℃暟
updateSongCounts()
} catch (err) {
// 涓嶅仛浠讳綍澶勭悊锛屽洜涓簎seSongs涓凡缁忓鐞嗕簡閿欒鎻愮ず
console.log('API閿欒宸插湪useSongs涓鐞?)
}
}
// 鍒锋柊姝屾洸鍒楄〃锛堜紭鍖栫増鏈級
const refreshSongs = async () => {
try {
if (isClientAuthenticated.value) {
await songs.fetchSongs(false, undefined, true) // forceRefresh=true
} else {
await songs.fetchPublicSchedules(false, undefined, true) // forceRefresh=true
}
updateSongCounts()
} catch (err) {
console.error('鍒锋柊姝屾洸鍒楄〃澶辫触', err)
}
}
// 澶勭悊瀛︽湡鍙樺寲锛堝墠绔繃婊ょ増鏈級
const handleSemesterChange = async (semester) => {
try {
// 閫氳繃浜嬩欢鎬荤嚎閫氱煡SongList缁勪欢杩涜鍓嶇杩囨护
// 浣跨敤nextTick纭繚浜嬩欢鍦―OM鏇存柊鍚庤Е鍙? await nextTick()
// 瑙﹀彂鑷畾涔変簨浠讹紝閫氱煡鎵€鏈夌洃鍚殑缁勪欢
const event = new CustomEvent('semester-filter-change', {
detail: { semester }
})
window.dispatchEvent(event)
console.log('瀛︽湡鍒囨崲浜嬩欢宸插彂閫?', semester)
// 鏇存柊姝屾洸璁℃暟锛堝熀浜庡綋鍓嶅凡鏈夋暟鎹級
await updateSongCounts(semester)
} catch (err) {
console.error('鍒囨崲瀛︽湡澶辫触', err)
}
}
// 鏇存柊閫氱煡鏁伴噺 - 鍙互淇濈暀杩欎釜鍑芥暟浣嗕笉鍐嶈皟鐢?const updateNotificationCount = async () => {
// 鍑芥暟淇濈暀浣嗕笉鍐嶄娇鐢?}
// 澶勭悊鐧诲嚭
const handleLogout = () => {
if (auth) {
auth.logout()
}
}
// 澶勭悊杩涘叆鍚庡彴鐨勭偣鍑诲姩鐢?const handleDashboardClick = (event) => {
const button = event.currentTarget
button.classList.add('clicking')
// 娣诲姞娑熸吉鏁堟灉
const ripple = document.createElement('span')
ripple.classList.add('ripple')
button.appendChild(ripple)
setTimeout(() => {
button.classList.remove('clicking')
ripple.remove()
}, 300)
}
// 娣诲姞鏌ョ湅閫氱煡骞舵爣璁颁负宸茶
const viewNotification = async (notification) => {
if (!notification.read) {
await notificationsService.markAsRead(notification.id)
}
}
// 澶勭悊鑱斿悎鎶曠鍥炲
const handleCollaborationReply = async (notification, accept) => {
if (notification.processing) return
notification.processing = true
try {
await $fetch('/api/songs/collaborators/reply', {
method: 'POST',
body: {
songId: notification.songId,
accept
}
})
// 鏍囪涓哄凡澶勭悊
notification.handled = true
notification.status = accept ? 'ACCEPTED' : 'REJECTED'
notification.repliedAt = new Date()
// notification.message += accept ? ' (宸叉帴鍙?' : ' (宸叉嫆缁?'
if (window.$showNotification) {
window.$showNotification(accept ? '宸叉帴鍙楄仈鍚堟姇绋块個璇? : '宸叉嫆缁濊仈鍚堟姇绋块個璇?, 'success')
}
// 鏍囪閫氱煡涓哄凡璇? await markNotificationAsRead(notification.id)
// 鍒锋柊姝屾洸鍒楄〃
refreshSongs()
// 鍒锋柊閫氱煡鍒楄〃
await loadNotifications()
} catch (error) {
console.error('澶勭悊鑱斿悎鎶曠閭€璇峰け璐?', error)
if (window.$showNotification) {
window.$showNotification(error.statusMessage || '鎿嶄綔澶辫触', 'error')
}
} finally {
notification.processing = false
}
}
// 鏍煎紡鍖栧埛鏂伴棿闅?const formatRefreshInterval = (seconds) => {
if (seconds < 60) {
return `${seconds}绉抈
} else {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return remainingSeconds > 0 ? `${minutes}鍒?{remainingSeconds}绉抈 : `${minutes}鍒嗛挓`
}
}
// 娉㈢汗鏁堟灉鎸囦护
const vRipple = {
mounted(el) {
el.addEventListener('click', (e) => {
const rect = el.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const ripple = document.createElement('span')
ripple.className = 'ripple-effect'
ripple.style.left = `${x}px`
ripple.style.top = `${y}px`
el.appendChild(ripple)
setTimeout(() => {
ripple.remove()
}, 600) // 涓嶤SS鍔ㄧ敾鏃堕棿涓€鑷? })
}
}
// 澶勭悊鏍囩鐐瑰嚮浜嬩欢锛屾坊鍔犲姩鐢绘晥鏋?const handleTabClick = (tab) => {
activeTab.value = tab
}
// 娣诲姞瀵艰埅鍒扮櫥褰曢〉闈㈢殑鏂规硶
const navigateToLogin = () => {
router.push('/login')
}
// 鏄剧ず鐧诲綍鎻愮ず
const showLoginNotice = () => {
if (window.$showNotification) {
window.$showNotification('闇€瑕佺櫥褰曟墠鑳芥煡鐪嬮€氱煡', 'info')
}
}
// 娉ㄥ叆绔欑偣缁熻浠g爜锛堝厹搴曟柟妗堬細鍔ㄦ€佽В鏋愬苟鎻掑叆 script 鍏冪礌鍒?body 鏈熬锛?const injectStatisticsCode = (code) => {
if (typeof document === 'undefined' || !code) return
// 鍏堝皾璇曠敤涓存椂瀹瑰櫒瑙f瀽 HTML锛屾彁鍙栧叾涓殑 script 鏍囩
const tempDiv = document.createElement('div')
tempDiv.innerHTML = code
// 灏嗛潪 script 鐨勫厓绱犲厛闄勫姞鍒板鍣ㄤ腑
const container = document.getElementById('site-statistics-code')
if (container) {
Array.from(tempDiv.childNodes).forEach((node) => {
if (node.nodeType === 1 && node.tagName && node.tagName.toLowerCase() !== 'script') {
container.appendChild(node.cloneNode(true))
}
})
}
// 鍗曠嫭澶勭悊 script 鏍囩锛氬姩鎬佸垱寤哄苟鎻掑叆鍒?body 鏈熬锛岀‘淇濊兘姝g‘鎵ц
const scripts = tempDiv.querySelectorAll('script')
scripts.forEach((oldScript) => {
const newScript = document.createElement('script')
Array.from(oldScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value)
})
if (oldScript.textContent) {
newScript.textContent = oldScript.textContent
}
document.body.appendChild(newScript)
})
}
// 妫€鏌ョ敤鎴锋槸鍚﹂渶瑕佷慨鏀瑰瘑鐮?const checkPasswordChangeRequired = async (user = null) => {
try {
// 浣跨敤浼犲叆鐨勭敤鎴蜂俊鎭垨褰撳墠璁よ瘉鐘舵€佷腑鐨勭敤鎴蜂俊鎭? const currentUser = user || auth?.user?.value
if (currentUser && currentUser.requirePasswordChange) {
// 寤惰繜1绉掓樉绀洪€氱煡锛岀‘淇濋〉闈㈠姞杞藉畬鎴? setTimeout(() => {
if (window.$showNotification) {
window.$showNotification(
'涓轰簡鎮ㄧ殑璐︽埛瀹夊叏锛屽缓璁偍淇敼瀵嗙爜銆傛偍鍙互鐐瑰嚮鍙充笂瑙掔殑"淇敼瀵嗙爜"鎸夐挳杩涜淇敼銆?,
'info',
true,
8000 // 鏄剧ず8绉? )
}
}, 1000)
}
} catch (error) {
console.error('妫€鏌ュ瘑鐮佷慨鏀圭姸鎬佸け璐?', error)
}
}
// 鏃х殑showToast鍑芥暟宸茬Щ闄わ紝浣跨敤鍏ㄥ眬閫氱煡绯荤粺
// 娣诲姞鏈閫氱煡璁℃暟
// 涔嬪墠宸插0鏄庝簡unreadNotificationCount锛岃繖閲屽鍏惰繘琛屽寮?if (
notificationsService &&
notificationsService.unreadCount &&
notificationsService.unreadCount.value
) {
const count = notificationsService.unreadCount.value
unreadNotificationCount.value = count
}
</script>
<style scoped>
.home {
width: 100%;
flex: 1;
background-color: #121318;
padding: 1.5rem;
color: #ffffff;
display: flex;
flex-direction: column;
min-height: 100vh; /* 纭繚鑷冲皯鍗犳弧瑙嗗彛 */
}
.main-content {
width: 100%;
max-width: 1440px;
margin: 0 auto;
position: relative;
display: flex;
flex-direction: column;
height: 100%; /* 鏀逛负鍗犳弧鐖跺鍣ㄩ珮搴﹁€屼笉鏄鍙i珮搴?*/
}
/* 娣诲姞椤堕儴Ellipse 1鏁堟灉 */
.ellipse-effect {
position: absolute;
top: -165px;
left: 50%;
transform: translateX(-50%) perspective(500px) rotateX(10deg);
width: 1110px;
height: 309px;
background: radial-gradient(
ellipse at center,
rgba(11, 90, 254, 0.3) 0%,
rgba(11, 90, 254, 0.15) 30%,
rgba(11, 90, 254, 0) 70%
);
z-index: 0;
pointer-events: none;
}
/* 椤堕儴鍖哄煙鏍峰紡 */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
margin-top: -2rem;
}
.logo-section {
display: flex;
align-items: center;
gap: 20px;
min-height: 160px;
}
.logo-link {
display: block;
text-decoration: none;
}
.logo-image {
width: 80px;
height: auto;
object-fit: contain;
}
/* 妯嚎鍜屽鏍ogo瀹瑰櫒 */
.logo-divider-container {
display: flex;
align-items: center;
gap: 30px;
}
/* 妯嚎鏍峰紡 */
.logo-divider {
width: 2px;
height: 100px;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.3),
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.3)
);
border-radius: 1px;
}
/* 瀛︽牎logo鏍峰紡 */
.school-logo {
max-width: 200px;
max-height: 80px;
width: auto;
height: auto;
object-fit: contain;
}
.user-section {
position: relative;
z-index: 100;
display: flex;
align-items: center;
gap: 10px;
}
/* 閫氱敤宸ュ叿鎸夐挳锛堜富棰?璇█锛?*/
.utility-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
background: var(--utility-btn-bg, rgba(255, 255, 255, 0.06));
color: var(--utility-btn-color, rgba(255, 255, 255, 0.78));
border: 1px solid var(--utility-btn-border, rgba(255, 255, 255, 0.12));
border-radius: 10px;
font-size: 13px;
font-weight: 500;
line-height: 1;
cursor: pointer;
transition:
background 0.2s ease,
color 0.2s ease,
border-color 0.2s ease,
transform 0.15s ease;
backdrop-filter: blur(6px);
}
.utility-btn:hover {
background: var(--utility-btn-hover-bg, rgba(255, 255, 255, 0.12));
color: var(--utility-btn-hover-color, #ffffff);
border-color: var(--utility-btn-hover-border, rgba(255, 255, 255, 0.22));
}
.utility-btn:active {
transform: scale(0.97);
}
.utility-btn-label {
white-space: nowrap;
}
@media (max-width: 768px) {
.utility-btn-label {
display: none;
}
.utility-btn {
padding: 7px 8px;
border-radius: 9px;
}
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.login-options {
display: flex;
align-items: center;
gap: 12px;
}
.enter-admin-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.2s ease;
cursor: pointer;
text-decoration: none;
margin-right: 8px;
}
.enter-admin-btn:hover {
background: rgba(59, 130, 246, 0.25);
transform: translateY(-1px);
}
.user-details-desktop {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.user-badge {
font-size: 10px;
padding: 1px 6px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.5);
border-radius: 4px;
margin-top: 2px;
}
.user-badge.admin {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.user-avatar-wrapper {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.user-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-avatar-placeholder {
font-size: 16px;
font-weight: 700;
color: var(--primary);
}
.user-actions-dropdown {
position: absolute;
top: calc(100% + 12px);
right: 0;
background: #1a1a1f;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 8px;
min-width: 160px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
gap: 4px;
}
.action-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
background: transparent;
width: 100%;
text-align: left;
}
.action-item:hover {
background: rgba(255, 255, 255, 0.05);
color: white;
}
.action-item.logout:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
min-width: 18px;
height: 18px;
border-radius: 9px;
background-color: #f44336;
color: white;
font-size: 10px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
/* 鐧诲綍鎸夐挳 - 妗岄潰绔?*/
.login-options .login-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 24px;
border-radius: 14px;
font-size: 14px;
font-weight: 600;
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
text-decoration: none;
position: relative;
overflow: hidden;
}
.login-options .login-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2);
color: white;
}
.login-options .login-btn :deep(.icon),
.login-options .login-btn i {
color: #3b82f6;
transition: all 0.3s ease;
}
.login-options .login-btn:hover :deep(.icon),
.login-options .login-btn:hover i {
transform: scale(1.1);
filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5));
}
.login-options .login-btn:active {
transform: translateY(0) scale(0.96);
transition: all 0.1s;
}
/* 绔欑偣鏍囬 */
.site-title {
text-align: center;
margin: 3rem 0;
padding: 0 1rem;
}
.title-container {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.main-title {
font-family: 'MiSans', sans-serif;
font-weight: 800;
font-size: 42px;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.7) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
margin: 0;
line-height: 1.2;
}
.title-divider {
width: 40px;
height: 4px;
background: #0b5afe;
border-radius: 2px;
box-shadow: 0 0 15px rgba(11, 90, 254, 0.6);
}
.sub-title {
font-family: 'MiSans', sans-serif;
font-weight: 500;
font-size: 14px;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.2em;
text-transform: uppercase;
}
/* 鍐呭鍖哄煙缁撴瀯 */
.content-area {
display: flex;
flex-direction: column;
gap: 0;
flex: 1; /* 鍗犳嵁鍓╀綑绌洪棿 */
min-height: 0; /* 鍏佽 flex 瀛愬厓绱犳敹缂?*/
width: 100%; /* 纭繚瀹藉害鍗犳弧 */
}
/* 閫夐」鍗℃牱寮?- 妗岄潰绔?*/
.tabs-row {
display: flex;
gap: 5px;
margin-bottom: 0;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
-ms-overflow-style: none;
padding-bottom: 2px;
}
.tabs-row::-webkit-scrollbar {
display: none;
}
.section-tab {
background: #1a1b24;
border-radius: 15px 15px 0 0;
padding: 15px 24px;
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
border: 2px solid #282830;
border-bottom: none;
cursor: pointer;
flex: 0 0 auto;
}
.section-tab.active {
background: #21242d;
color: #ffffff;
position: relative;
z-index: 1;
}
@keyframes tab-pane-enter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 閫氱煡鍒楄〃杩囨浮鍔ㄧ敾 */
.notification-list-fade-enter-active,
.notification-list-fade-leave-active {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.notification-list-fade-enter-from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
.notification-list-fade-leave-to {
opacity: 0;
transform: translateY(-20px) scale(1.02);
}
/* 閫氱煡椤逛氦閿欒繘鍏ュ姩鐢?*/
.notification-card {
animation: notification-item-enter 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
animation-delay: var(--animation-delay, 0s);
opacity: 0;
transform: translateY(20px);
will-change: transform, opacity;
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s ease;
}
.notification-card:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
@keyframes notification-item-enter {
0% {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 閫夐」鍗″垏鎹㈠姩鐢?*/
.section-tab {
position: relative;
transition: all 0.3s ease;
overflow: hidden;
padding: 0.75rem 1.5rem;
z-index: 10; /* 纭繚鍦ㄥ唴瀹逛箣涓?*/
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.section-tab .tab-icon {
display: none; /* PC绔粯璁ら殣钘忓浘鏍?*/
}
.section-tab .icon-wrapper {
display: none; /* PC绔粯璁ら殣钘忓浘鏍囧鍣紝閬垮厤闂磋窛闂 */
}
.section-tab .tab-text {
display: inline;
position: relative;
}
/* PC绔€氱煡灏忓渾鐐?*/
.notification-badge-desktop {
position: absolute;
top: -2px;
right: -8px;
width: 6px;
height: 6px;
background: #0b5afe;
border-radius: 50%;
box-shadow: 0 0 5px rgba(11, 90, 254, 0.5);
}
.section-tab::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: #0b5afe;
transition: all 0.3s ease;
transform: translateX(-50%);
}
.section-tab:hover::after {
width: 50%;
}
.section-tab.active::after {
width: 100%;
}
/* 绉婚櫎涓婃诞鏁堟灉 */
.section-tab:hover {
transform: none; /* 绉婚櫎涓婃诞鏁堟灉 */
background-color: transparent; /* 绉婚櫎鑳屾櫙鑹?*/
box-shadow: none; /* 绉婚櫎鍐呴槾褰?*/
color: rgba(255, 255, 255, 0.9);
}
.section-tab.active:hover {
background-color: transparent;
box-shadow: none;
color: #ffffff;
}
/* 鍐呭瀹瑰櫒 */
.tab-content-container {
background: #1a1b24;
border: 2px solid #282830;
border-radius: 0 15px 15px 15px;
padding: 1.5rem;
margin-top: -2px; /* 浣垮唴瀹瑰鍣ㄤ笌鏍囩杩炴帴 */
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
flex: 1; /* 鍗犳嵁鍓╀綑绌洪棿 */
min-height: 0; /* 鍏佽 flex 瀛愬厓绱犳敹缂?*/
overflow: hidden; /* 闃叉鍐呭婧㈠嚭 */
}
@media (max-width: 768px) {
.tab-content-container {
padding: 1rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
}
.tab-pane {
width: 100%;
box-sizing: border-box;
animation: tab-pane-enter 0.45s ease;
}
/* 閽堝鎺掓湡鏍囩椤电殑鐗规畩鏍峰紡 */
.schedule-tab-pane {
width: 100%;
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
flex: 1; /* 鍗犳弧鐖跺鍣ㄩ珮搴?*/
min-height: 0; /* 鍏佽 flex 瀛愬厓绱犳敹缂?*/
overflow: hidden; /* 闃叉鍐呭婧㈠嚭 */
}
.schedule-tab-pane .full-width {
flex: 1; /* 鍗犳嵁鍓╀綑绌洪棿 */
display: flex;
flex-direction: column;
min-height: 0; /* 鍏佽 flex 瀛愬厓绱犳敹缂?*/
overflow: hidden; /* 闃叉鍐呭婧㈠嚭 */
}
@media (max-width: 768px) {
.tab-pane {
padding: 0.5rem;
}
.schedule-tab-pane {
padding: 0;
}
/* 绉诲姩绔垎椤垫帶浠舵牱寮?*/
.pagination-controls {
flex-direction: column;
gap: 10px;
}
.page-size-selector {
justify-content: center;
}
.page-navigation {
justify-content: center;
flex-wrap: wrap;
}
.page-numbers {
flex-wrap: wrap;
justify-content: center;
}
}
.song-list-container {
width: 100%;
padding: 1rem 0;
}
.date-info p {
font-family: 'MiSans', sans-serif;
font-weight: 400;
font-size: 16px;
letter-spacing: 4%;
}
/* 姝屾洸鏃舵 */
.time-label {
font-family: 'MiSans', sans-serif;
font-weight: 400;
font-size: 16px;
letter-spacing: 4%;
color: rgba(255, 255, 255, 0.6);
margin: 1.5rem 0 1rem;
}
.song-cards {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* 璇锋眰琛ㄥ崟甯冨眬 */
.request-pane {
display: flex;
gap: 2rem;
}
/* 绌虹姸鎬?*/
.empty-state {
text-align: center;
padding: 2rem;
color: rgba(255, 255, 255, 0.6);
}
/* 涓嬫媺鑿滃崟鍔ㄧ敾 */
.dropdown-fade-enter-active,
.dropdown-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.dropdown-fade-enter-from,
.dropdown-fade-leave-to {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
/* 閫氱煡闈㈡澘 */
.notification-pane {
height: 100%;
display: flex;
flex-direction: column;
}
.notification-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
}
/* 閫氱煡澶撮儴 */
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.notification-title {
font-size: 1.25rem;
font-weight: 700;
color: #ffffff;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.settings-icon {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.settings-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
transform: rotate(30deg);
}
/* 閫氱煡鍒楄〃 */
.notification-list {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 0.5rem;
margin-bottom: 1.5rem;
min-height: 400px;
}
.loading-indicator,
.empty-notification {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
color: rgba(255, 255, 255, 0.4);
gap: 1.25rem;
}
.empty-icon {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.03);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.5rem;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.notification-items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.notification-card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20px;
padding: 1.25rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
cursor: pointer;
}
.notification-card:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
transform: translateY(-2px);
}
.notification-card.unread {
background: rgba(59, 130, 246, 0.05);
border-color: rgba(59, 130, 246, 0.2);
}
.notification-card-header {
display: flex;
align-items: flex-start;
margin-bottom: 1rem;
}
.notification-icon-type {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border-radius: 12px;
margin-right: 1rem;
flex-shrink: 0;
font-size: 1.25rem;
}
.notification-title-row {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 0.95rem;
font-weight: 600;
color: #ffffff;
margin-bottom: 0.25rem;
}
.notification-time {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
}
.notification-card-body {
padding: 0 0 0 3.5rem;
}
.notification-text {
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
line-height: 1.6;
}
.notification-card-actions {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
padding-left: 3.5rem;
}
.action-button.delete {
background: rgba(239, 68, 68, 0.05);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.1);
padding: 0.4rem 0.75rem;
border-radius: 8px;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s;
}
.action-button.delete:hover {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.2);
}
.notification-actions-bar {
display: flex;
gap: 1rem;
padding: 1rem 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.action-button-large {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
padding: 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.action-button-large:hover:not(.disabled) {
background: rgba(255, 255, 255, 0.1);
}
.action-button-large.danger {
color: #ef4444;
}
.action-button-large.danger:hover {
background: rgba(239, 68, 68, 0.1);
}
.action-button-large.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 鍒嗛〉鎺т欢鏍峰紡 */
.notification-pagination {
padding: 15px 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.pagination-info {
text-align: center;
margin-bottom: 15px;
}
.pagination-text {
color: rgba(255, 255, 255, 0.7);
font-size: 0.85rem;
}
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
}
.page-size-selector label {
color: rgba(255, 255, 255, 0.7);
font-size: 0.85rem;
white-space: nowrap;
}
.page-size-custom-select {
width: 90px;
}
.page-navigation {
display: flex;
align-items: center;
gap: 8px;
}
.page-nav-button {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--light);
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, background-color;
}
.page-nav-button:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.page-nav-button:active:not(:disabled) {
transform: translateY(0) scale(0.95);
transition: all 0.1s ease;
}
.page-nav-button:disabled {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
}
.page-numbers {
display: flex;
align-items: center;
gap: 4px;
}
.page-number-button {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--light);
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, background-color;
position: relative;
overflow: hidden;
}
.page-number-button:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.page-number-button:active:not(:disabled) {
transform: translateY(0) scale(0.95);
transition: all 0.1s ease;
}
.page-number-button.active {
background-color: var(--primary);
border-color: var(--primary);
color: white;
}
.page-number-button.active:hover {
background-color: #0952e8;
border-color: #0952e8;
}
.page-number-button:disabled {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
}
.page-ellipsis {
color: rgba(255, 255, 255, 0.5);
padding: 0 4px;
font-size: 0.85rem;
}
/* 鍒嗛〉鍔犺浇鐘舵€?*/
.pagination-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 0;
color: rgba(255, 255, 255, 0.7);
font-size: 0.85rem;
animation: fade-in 0.3s ease;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top: 2px solid var(--primary);
border-radius: 50%;
animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
will-change: transform;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fade-in {
0% {
opacity: 0;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 搴曢儴鎿嶄綔鏍?*/
.notification-actions-bar {
display: flex;
justify-content: center;
gap: 10px;
padding: 15px 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.action-button-large {
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: var(--light);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.action-button-large:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.action-button-large.danger {
color: #ef4444;
}
.action-button-large.danger:hover {
background-color: rgba(239, 68, 68, 0.2);
}
.action-button-large.disabled {
background-color: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
opacity: 0.5;
}
.action-button-large.disabled:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* ==================== 绉诲姩绔璁?==================== */
/* 鍩虹绉诲姩绔€傞厤 */
@media (max-width: 768px) {
.home {
padding: 0;
background-color: #0a0a0f;
}
.main-content {
padding: 0;
}
/* 闅愯棌鍘熸湁鐨別llipse鏁堟灉锛屼娇鐢ㄦ洿寰鐨勮儗鏅?*/
.ellipse-effect {
display: none;
}
/* 椤堕儴鍖哄煙 */
.top-bar {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin: 0;
background: linear-gradient(180deg, rgba(11, 90, 254, 0.08) 0%, transparent 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.logo-section {
min-height: auto;
gap: 8px;
}
.logo-image {
width: 76px;
height: auto;
}
.logo-divider-container {
gap: 8px;
}
.logo-divider {
height: 28px;
width: 1px;
background: rgba(255, 255, 255, 0.15);
}
/* 绉诲姩绔殑涓婚〉闈㈤噷涓嶉渶瑕佸啓瀛︽牎鍚嶏紝淇濇寔绠€娲?*/
.site-title {
display: none;
}
.school-logo {
max-width: 120px;
max-height: 36px;
}
/* 鐢ㄦ埛鍖哄煙绠€鍖?*/
.user-section {
width: auto;
}
.user-details-desktop {
display: none; /* 绉诲姩绔粎鏄剧ず澶村儚 */
}
.user-avatar-wrapper {
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
.user-avatar-placeholder {
font-size: 14px;
}
.user-actions-dropdown {
top: calc(100% + 10px);
min-width: 140px;
padding: 6px;
}
.action-item {
padding: 8px 10px;
font-size: 13px;
}
/* 鐧诲綍鎸夐挳 */
.login-options .login-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 18px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.login-options .login-btn :deep(.icon) {
color: #3b82f6;
}
.login-options .login-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.15);
border-color: rgba(59, 130, 246, 0.4);
}
/* Footer 闂磋窛浼樺寲 */
:deep(.site-footer) {
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
}
/* 鍐呭鍖哄煙 - 鍏ㄥ鏃犺竟妗?*/
.content-area {
min-height: auto;
overflow-x: hidden; /* 闃叉妯悜婧㈠嚭 */
max-width: 100vw; /* 纭繚涓嶈秴杩囪鍙e搴?*/
box-sizing: border-box; /* 纭繚padding璁″叆鎬诲搴?*/
}
.tabs-row {
position: fixed;
bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
left: 1rem;
right: 1rem;
margin: 0 auto;
max-width: 500px;
display: flex;
justify-content: space-around;
align-items: stretch; /* 淇敼涓?stretch 浠ラ厤鍚堝瓙鍏冪礌 height: 100% */
gap: 0;
padding: 0 0.5rem;
height: 64px;
background: rgba(28, 28, 30, 0.9);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 9999px;
z-index: 1000;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.section-tab {
flex: 1;
height: 100%; /* 纭繚濉弧瀹瑰櫒楂樺害 */
background: transparent;
border: none;
border-radius: 0;
padding: 0; /* 绉婚櫎鍥哄畾鍐呰竟璺濓紝鏀圭敤 flex 灞呬腑 */
font-size: 10px;
font-weight: 500;
color: #71717a; /* text-zinc-500 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* 鍨傜洿灞呬腑鍐呭 */
gap: 4px;
position: relative;
transition: all 0.2s ease;
}
.section-tab .tab-icon {
display: block;
margin-bottom: 2px;
transition: all 0.2s ease;
color: currentColor;
}
.section-tab .tab-text {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.02em;
transition: all 0.2s ease;
}
.section-tab.active {
color: #3b82f6 !important; /* text-blue-500 - Force blue */
background: transparent !important;
transform: none !important;
text-shadow: 0 0 12px rgba(59, 130, 246, 0.6); /* Text Glow */
}
/* Prevent hover from turning it white on mobile */
.section-tab.active:hover {
color: #3b82f6 !important;
background: transparent !important;
box-shadow: none !important;
}
.section-tab.active .tab-icon {
opacity: 1;
color: currentColor;
transform: none;
filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.5)); /* Icon Glow */
}
.section-tab.active .tab-text {
font-weight: 700;
}
/* 绉婚櫎鍘熸湁鐨勪吉鍏冪礌鍥炬爣 */
.section-tab::before {
display: none;
}
/* 绉婚櫎搴曢儴鎸囩ず鍣紙妯潯锛? 褰诲簳闅愯棌 */
.section-tab::after {
display: none !important;
}
@keyframes dot-pop-in {
0% {
transform: scale(0);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.icon-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 0 auto;
}
.section-tab .icon-wrapper {
display: flex; /* 绉诲姩绔樉绀哄浘鏍囧鍣?*/
}
/* 閫氱煡寰界珷 - 鍥炲綊钃濊壊椋庢牸 */
.notification-badge-tab {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 8px;
background: #0b5afe;
border-radius: 50%;
border: 1.5px solid #0a0a0f;
box-shadow: 0 0 5px rgba(11, 90, 254, 0.4);
z-index: 2;
}
.notification-badge-desktop {
display: none; /* 绉诲姩绔殣钘忔闈㈢増寰界珷 */
}
@keyframes badge-pulse {
/* 绉婚櫎瀵艰嚧浣嶇Щ鐨勫姩鐢?*/
}
.section-tab.disabled {
opacity: 0.3;
filter: grayscale(1);
}
/* 鍐呭瀹瑰櫒 */
.tab-content-container {
background: transparent;
border: none;
border-radius: 0;
padding: 0 6px calc(80px + env(safe-area-inset-bottom, 0px));
margin: 0;
min-height: calc(100vh - 120px);
}
.tab-pane {
padding: 0;
overflow-x: hidden; /* 闃叉妯悜婧㈠嚭 */
max-width: 100%; /* 纭繚涓嶈秴杩囪鍙e搴?*/
}
/* 鎺掓湡鏍囩椤典紭鍖?*/
.schedule-tab-pane {
padding: 0;
min-height: auto;
}
/* 璇锋眰琛ㄥ崟鍖哄煙 */
.request-pane {
flex-direction: column;
gap: 16px;
overflow-x: hidden; /* 闃叉妯悜婧㈠嚭 */
max-width: 100%; /* 纭繚涓嶈秴杩囪鍙e搴?*/
padding: 0 0.5rem; /* 娣诲姞宸﹀彸鍐呰竟璺濓紝闃叉鍐呭璐磋竟 */
box-sizing: border-box; /* 纭繚padding璁″叆鎬诲搴?*/
}
/* 鐧诲綍閫夐」 */
.login-options {
display: flex;
align-items: center;
}
.login-options .btn-outline {
padding: 6px 14px;
font-size: 12px;
border-radius: 6px;
background: rgba(11, 90, 254, 0.15);
border: 1px solid rgba(11, 90, 254, 0.3);
}
}
/* 灏忓睆骞曡澶囬澶栦紭鍖?*/
@media (max-width: 480px) {
.top-bar {
padding: 8px 10px;
}
.logo-image {
width: 90px;
}
.school-logo {
max-width: 100px;
max-height: 32px;
}
.action-button {
padding: 5px 8px;
font-size: 10px;
}
.site-title {
margin: 6px 0 10px;
padding: 0 12px;
}
.site-title h2 {
font-size: 16px;
}
.tab-content-container {
padding: 0 4px calc(80px + env(safe-area-inset-bottom, 0px));
}
}
/* 瓒呭皬灞忓箷璁惧 */
@media (max-width: 360px) {
.logo-image {
width: 60px;
}
.school-logo {
max-width: 80px;
max-height: 28px;
}
.action-button {
padding: 4px 6px;
font-size: 9px;
}
.section-tab {
font-size: 9px;
}
}
/* 寮圭獥閬僵灞?*/
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: overlay-fade-in 0.4s ease-out;
}
@keyframes overlay-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 寮圭獥鍐呭 */
.modal-content {
background: linear-gradient(135deg, #1a1b24 0%, #121318 100%);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
max-width: 420px;
width: 90%;
overflow: hidden;
animation: modal-slide-up 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes modal-slide-up {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 寮圭獥澶撮儴 */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 28px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.modal-header h2 {
font-family:
'MiSans',
system-ui,
-apple-system,
sans-serif;
font-size: 20px;
font-weight: 700;
color: #ffffff;
margin: 0;
letter-spacing: 0.02em;
}
/* 鍏抽棴鎸夐挳 */
.close-button {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: none;
color: rgba(255, 255, 255, 0.6);
font-size: 24px;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
transform: rotate(90deg);
}
/* 寮圭獥涓讳綋 */
.modal-body {
padding: 24px 28px 28px;
}
.rules-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.rules-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rules-subtitle {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 16px;
font-weight: 700;
color: #ffffff;
margin: 0;
}
.rules-icon {
color: #3b82f6;
}
.rules-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
margin: 0;
}
.guidelines-content {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
}
.default-rules {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rule-item {
display: flex;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.5;
}
.rule-item span {
margin-right: 0.5rem;
color: rgba(255, 255, 255, 0.3);
font-weight: 600;
}
/* 骞村害鎶ュ憡寮圭獥 */
.year-review-overlay {
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.4);
}
.year-review-card {
position: relative;
width: 90%;
max-width: 400px;
background: #12121a;
border-radius: 32px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: card-appear 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.card-glow {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.15) 0%, transparent 70%);
pointer-events: none;
}
.card-pattern {
position: absolute;
inset: 0;
background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.5;
}
.card-close {
position: absolute;
top: 20px;
right: 20px;
width: 36px;
height: 36px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.2s ease;
}
.card-close:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
transform: translateY(2px);
}
.card-content {
position: relative;
padding: 40px 32px;
text-align: center;
z-index: 5;
}
.brand-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 99px;
color: #a78bfa;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.1em;
margin-bottom: 32px;
}
.visual-container {
position: relative;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.main-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.5);
z-index: 2;
transform: rotate(-5deg);
}
.music-bars {
position: absolute;
display: flex;
align-items: flex-end;
gap: 4px;
height: 40px;
opacity: 0.3;
}
.bar {
width: 4px;
background: #8b5cf6;
border-radius: 2px;
animation: bar-dance 1.2s ease-in-out infinite;
}
.bar:nth-child(1) {
height: 20px;
animation-delay: 0.1s;
}
.bar:nth-child(2) {
height: 35px;
animation-delay: 0.3s;
}
.bar:nth-child(3) {
height: 25px;
animation-delay: 0.2s;
}
.bar:nth-child(4) {
height: 40px;
animation-delay: 0.4s;
}
.bar:nth-child(5) {
height: 30px;
animation-delay: 0.2s;
}
@keyframes bar-dance {
0%,
100% {
transform: scaleY(1);
}
50% {
transform: scaleY(0.6);
}
}
.card-title {
font-size: 24px;
font-weight: 800;
color: #fff;
margin-bottom: 12px;
letter-spacing: -0.01em;
}
.card-description {
font-size: 15px;
color: rgba(255, 255, 255, 0.5);
line-height: 1.6;
margin-bottom: 32px;
}
.card-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn-primary {
width: 100%;
padding: 16px;
background: #fff;
border: none;
border-radius: 16px;
color: #000;
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(255, 255, 255, 0.1);
background: #f4f4f4;
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
width: 100%;
padding: 14px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
border-color: rgba(255, 255, 255, 0.2);
}
@keyframes card-appear {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 瑕嗙洊鏃у姩鐢?*/
.modal-animation-enter-active,
.modal-animation-leave-active {
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.modal-animation-enter-from,
.modal-animation-leave-to {
opacity: 0;
backdrop-filter: blur(0);
}
.modal-animation-enter-from .year-review-card,
.modal-animation-leave-to .year-review-card {
transform: translateY(40px) scale(0.9);
}
/* 娉㈢汗鏁堟灉 */
.section-tab {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
}
.ripple-effect {
position: absolute;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
width: 100px;
height: 100px;
margin-left: -50px; /* 灞呬腑瀹氫綅 */
margin-top: -50px; /* 灞呬腑瀹氫綅 */
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* 纭繚鍏ㄥ鏄剧ず */
.full-width,
.full-width > div {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
padding: 0 !important;
margin: 0 !important;
display: block !important;
}
/* 閫氱煡鏍囩涓婄殑鏈寰界珷 */
.notification-badge-tab {
position: absolute; /* 淇敼涓虹粷瀵瑰畾浣嶏紝闃叉鎸ゅ帇鍥炬爣 */
top: 0;
right: 0;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #0b5afe;
display: inline-block;
z-index: 2;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.4);
}
70% {
transform: scale(1.05);
box-shadow: 0 0 0 5px rgba(0, 122, 255, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(0, 122, 255, 0);
}
}
/* 绂佺敤鐨勬爣绛鹃〉鏍峰紡 */
.section-tab.disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 鏈櫥褰曟彁绀烘牱寮?*/
.login-required-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 40px 0;
}
.login-required-content {
text-align: center;
max-width: 400px;
padding: 30px;
background-color: rgba(30, 41, 59, 0.5);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
.login-icon {
font-size: 3rem;
margin-bottom: 15px;
}
.login-required-content h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: var(--light);
}
.login-required-content p {
margin-bottom: 20px;
color: rgba(255, 255, 255, 0.7);
}
.login-button {
background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
border: none;
color: white;
padding: 10px 20px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.login-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 67, 248, 0.3);
}
</style>