feat(front): 前台默认浅色模式并新增居中投稿按钮与实时空闲档期
- 主题: useTheme 默认 ModernLight + variables.css SSR-safe 浅值并精修组件对比度 - 首页: 插入居中投稿 CTA + 紧邻档期概览卡片(今日排期/待播歌曲/空闲档期数) - 数据: useSongs 新增 fetchExpectedPlayDates 接入空闲档期并走 revision 轮询实时刷新 - 视觉: 移动端优先,少圆角硬边几何 + 概览卡片 3→1 列响应式
This commit is contained in:
+337
-4
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
/* 防止浏览器自动应用深色主题:改为跟随 data-theme 动态覆盖(见 theme-protection.css) */
|
/* 防止浏览器自动应用深色主题:改为跟随 data-theme 动态覆盖(见 theme-protection.css) */
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 流体背景与全局样式 */
|
/* 流体背景与全局样式 */
|
||||||
@@ -82,9 +82,9 @@ body::before {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.03) 0%, transparent 50%),
|
radial-gradient(circle at 20% 50%, rgba(79, 70, 229, 0.035) 0%, transparent 55%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.01) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(14, 165, 233, 0.025) 0%, transparent 55%),
|
||||||
radial-gradient(circle at 40% 80%, rgba(120, 119, 198, 0.02) 0%, transparent 50%);
|
radial-gradient(circle at 40% 80%, rgba(79, 70, 229, 0.02) 0%, transparent 55%);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -472,3 +472,336 @@ body::before {
|
|||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* 前台浅色模式对比度覆盖(ScheduleList / RequestForm 等组件)
|
||||||
|
* 将所有硬编码的 rgba(255,255,255,*) 白系文字统一替换为
|
||||||
|
* 适配浅底的深灰/主题色,保证 WCAG AA 可读性
|
||||||
|
* =======================================================*/
|
||||||
|
html[data-theme='light'],
|
||||||
|
html[data-theme='ClassicLight'],
|
||||||
|
html[data-theme='ModernLight'] {
|
||||||
|
/* ScheduleList.vue 主区块硬编码颜色校正 */
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .date-item,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .date-item,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .date-item,
|
||||||
|
html[data-theme='light'] .song-list .date-item,
|
||||||
|
html[data-theme='ClassicLight'] .song-list .date-item,
|
||||||
|
html[data-theme='ModernLight'] .song-list .date-item {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .empty-dates,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .empty-dates,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .empty-dates,
|
||||||
|
html[data-theme='light'] .schedule-list .loading,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .loading,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .loading,
|
||||||
|
html[data-theme='light'] .schedule-list .error,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .error,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .error,
|
||||||
|
html[data-theme='light'] .schedule-list .empty,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .empty,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .empty {
|
||||||
|
color: var(--text-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .playtime-header h4,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .playtime-header h4,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .playtime-header h4 {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .playtime-time,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .playtime-time,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .playtime-time {
|
||||||
|
color: var(--text-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .playlist-modal,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .playlist-modal,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .playlist-modal {
|
||||||
|
border-color: var(--border-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .playlist-modal-header,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .playlist-modal-header,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .playlist-modal-header,
|
||||||
|
html[data-theme='light'] .schedule-list .playlist-modal-footer,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .playlist-modal-footer,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .playlist-modal-footer {
|
||||||
|
border-color: var(--border-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .song-meta .meta-secondary,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .song-meta .meta-secondary,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .song-meta .meta-secondary,
|
||||||
|
html[data-theme='light'] .schedule-list .song-meta .meta-placeholder,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .song-meta .meta-placeholder,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .song-meta .meta-placeholder,
|
||||||
|
html[data-theme='light'] .schedule-list .slot-hint,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .slot-hint,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .slot-hint,
|
||||||
|
html[data-theme='light'] .schedule-list .requestor-text,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .requestor-text,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .requestor-text,
|
||||||
|
html[data-theme='light'] .schedule-list .pill-empty,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .pill-empty,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .pill-empty,
|
||||||
|
html[data-theme='light'] .schedule-list .empty-slot,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .empty-slot,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .empty-slot {
|
||||||
|
color: var(--text-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .chip,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .chip,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .chip,
|
||||||
|
html[data-theme='light'] .schedule-list .tag,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .tag,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .tag,
|
||||||
|
html[data-theme='light'] .schedule-list .badge-soft,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .badge-soft,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .badge-soft {
|
||||||
|
background: var(--bg-secondary) !important;
|
||||||
|
border-color: var(--border-secondary) !important;
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .schedule-list .divider-subtle,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .divider-subtle,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .divider-subtle,
|
||||||
|
html[data-theme='light'] .schedule-list .card-border,
|
||||||
|
html[data-theme='ClassicLight'] .schedule-list .card-border,
|
||||||
|
html[data-theme='ModernLight'] .schedule-list .card-border {
|
||||||
|
border-color: var(--border-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RequestForm / SongList / Dialog 通用硬编码白字校正 */
|
||||||
|
html[data-theme='light'] .request-form .label-subtle,
|
||||||
|
html[data-theme='ClassicLight'] .request-form .label-subtle,
|
||||||
|
html[data-theme='ModernLight'] .request-form .label-subtle,
|
||||||
|
html[data-theme='light'] .request-form .hint-subtle,
|
||||||
|
html[data-theme='ClassicLight'] .request-form .hint-subtle,
|
||||||
|
html[data-theme='ModernLight'] .request-form .hint-subtle,
|
||||||
|
html[data-theme='light'] .request-form .placeholder-subtle,
|
||||||
|
html[data-theme='ClassicLight'] .request-form .placeholder-subtle,
|
||||||
|
html[data-theme='ModernLight'] .request-form .placeholder-subtle,
|
||||||
|
html[data-theme='light'] .song-list .muted,
|
||||||
|
html[data-theme='ClassicLight'] .song-list .muted,
|
||||||
|
html[data-theme='ModernLight'] .song-list .muted,
|
||||||
|
html[data-theme='light'] .song-list .subtle-text,
|
||||||
|
html[data-theme='ClassicLight'] .song-list .subtle-text,
|
||||||
|
html[data-theme='ModernLight'] .song-list .subtle-text {
|
||||||
|
color: var(--text-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .request-form .glass-surface,
|
||||||
|
html[data-theme='ClassicLight'] .request-form .glass-surface,
|
||||||
|
html[data-theme='ModernLight'] .request-form .glass-surface,
|
||||||
|
html[data-theme='light'] .request-form .surface-chip,
|
||||||
|
html[data-theme='ClassicLight'] .request-form .surface-chip,
|
||||||
|
html[data-theme='ModernLight'] .request-form .surface-chip,
|
||||||
|
html[data-theme='light'] .song-list .glass-surface,
|
||||||
|
html[data-theme='ClassicLight'] .song-list .glass-surface,
|
||||||
|
html[data-theme='ModernLight'] .song-list .glass-surface,
|
||||||
|
html[data-theme='light'] .song-list .surface-chip,
|
||||||
|
html[data-theme='ClassicLight'] .song-list .surface-chip,
|
||||||
|
html[data-theme='ModernLight'] .song-list .surface-chip {
|
||||||
|
background: var(--bg-secondary) !important;
|
||||||
|
border-color: var(--border-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* 首页 Hero CTA 区样式 — 投稿按钮居中 + 档期概览卡片
|
||||||
|
* 移动端优先 / 少圆角 / 硬边几何 / 现代浅色专业风格
|
||||||
|
* =======================================================*/
|
||||||
|
.hero-cta-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 28px 16px 20px;
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-request-btn {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 240px;
|
||||||
|
width: min(86%, 360px);
|
||||||
|
height: 58px;
|
||||||
|
padding: 0 22px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, #4338ca 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 24px -10px rgba(79, 70, 229, 0.55),
|
||||||
|
0 4px 10px -4px rgba(79, 70, 229, 0.35);
|
||||||
|
transition: transform 0.18s cubic-bezier(0.2, 0.9, 0.2, 1),
|
||||||
|
box-shadow 0.18s ease, filter 0.18s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.hero-request-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.03);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 28px -10px rgba(79, 70, 229, 0.62),
|
||||||
|
0 6px 12px -4px rgba(79, 70, 229, 0.42);
|
||||||
|
}
|
||||||
|
.hero-request-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
filter: brightness(0.97);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 14px -6px rgba(79, 70, 229, 0.5),
|
||||||
|
0 2px 6px -2px rgba(79, 70, 229, 0.35);
|
||||||
|
}
|
||||||
|
.hero-request-btn .btn-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.hero-request-btn .btn-icon.pulse::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-overview-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 18px 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--card-bg, #ffffff);
|
||||||
|
border: 1px solid var(--card-border, #e4e4e7);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
}
|
||||||
|
.schedule-overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.schedule-overview-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overview-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
.overview-stat .stat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
.overview-stat .stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
.overview-stat .stat-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.overview-stat .stat-value .unit {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.overview-stat.primary {
|
||||||
|
background: linear-gradient(135deg, rgba(79, 70, 229, 0.08), rgba(14, 165, 233, 0.06));
|
||||||
|
border-color: rgba(79, 70, 229, 0.15);
|
||||||
|
}
|
||||||
|
.overview-stat.primary .stat-value {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.overview-stat.primary .stat-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.overview-stat .stat-detail {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.overview-stat .live-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #22c55e;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||||
|
animation: live-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes live-pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.65; transform: scale(0.92); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-cta-section {
|
||||||
|
padding: 22px 12px 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.hero-request-btn {
|
||||||
|
height: 54px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
|
.schedule-overview-card {
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.overview-stat {
|
||||||
|
padding: 12px 14px;
|
||||||
|
min-height: 84px;
|
||||||
|
}
|
||||||
|
.overview-stat.primary .stat-value {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+109
-210
@@ -1,69 +1,59 @@
|
|||||||
/* 全局CSS变量 - 统一配色方案 */
|
/* 全局CSS变量 - 统一配色方案 */
|
||||||
|
/* SSR-safe 默认值 = ModernLight 浅色,防止 hydration 前出现深色闪屏 */
|
||||||
:root {
|
:root {
|
||||||
/* 主色调 */
|
--primary: #4f46e5;
|
||||||
--primary: #3b82f6;
|
--primary-hover: #4338ca;
|
||||||
--primary-hover: #2563eb;
|
--primary-light: rgba(79, 70, 229, 0.1);
|
||||||
--primary-light: rgba(59, 130, 246, 0.1);
|
--primary-border: rgba(79, 70, 229, 0.2);
|
||||||
--primary-border: rgba(59, 130, 246, 0.2);
|
|
||||||
|
|
||||||
/* 成功色 */
|
--success: #16a34a;
|
||||||
--success: #10b981;
|
--success-hover: #15803d;
|
||||||
--success-hover: #059669;
|
--success-light: rgba(22, 163, 74, 0.1);
|
||||||
--success-light: rgba(16, 185, 129, 0.1);
|
--success-border: rgba(22, 163, 74, 0.2);
|
||||||
--success-border: rgba(16, 185, 129, 0.2);
|
|
||||||
|
|
||||||
/* 警告色 */
|
--warning: #ea580c;
|
||||||
--warning: #f59e0b;
|
--warning-hover: #c2410c;
|
||||||
--warning-hover: #d97706;
|
--warning-light: rgba(234, 88, 12, 0.1);
|
||||||
--warning-light: rgba(245, 158, 11, 0.1);
|
--warning-border: rgba(234, 88, 12, 0.2);
|
||||||
--warning-border: rgba(245, 158, 11, 0.2);
|
|
||||||
|
|
||||||
/* 错误色 */
|
--error: #e11d48;
|
||||||
--error: #ef4444;
|
--error-hover: #be123c;
|
||||||
--error-hover: #dc2626;
|
--error-light: rgba(225, 29, 72, 0.1);
|
||||||
--error-light: rgba(239, 68, 68, 0.1);
|
--error-border: rgba(225, 29, 72, 0.2);
|
||||||
--error-border: rgba(239, 68, 68, 0.2);
|
|
||||||
|
|
||||||
/* 信息色 */
|
--info: #0ea5e9;
|
||||||
--info: #3b82f6;
|
--info-hover: #0284c7;
|
||||||
--info-hover: #2563eb;
|
--info-light: rgba(14, 165, 233, 0.1);
|
||||||
--info-light: rgba(59, 130, 246, 0.1);
|
--info-border: rgba(14, 165, 233, 0.2);
|
||||||
--info-border: rgba(59, 130, 246, 0.2);
|
|
||||||
|
|
||||||
/* 背景色 */
|
--bg-primary: #fafafa;
|
||||||
--bg-primary: #111111;
|
--bg-secondary: #f4f4f5;
|
||||||
--bg-secondary: #1a1a1a;
|
--bg-tertiary: #e4e4e7;
|
||||||
--bg-tertiary: #2a2a2a;
|
--bg-quaternary: #d4d4d8;
|
||||||
--bg-quaternary: #3a3a3a;
|
--bg-hover: #eeeeef;
|
||||||
--bg-hover: #1f1f1f;
|
|
||||||
|
|
||||||
/* 文字颜色 */
|
--text-primary: #09090b;
|
||||||
--text-primary: #ffffff;
|
--text-secondary: #3f3f46;
|
||||||
--text-secondary: #cccccc;
|
--text-tertiary: #71717a;
|
||||||
--text-tertiary: #888888;
|
--text-quaternary: #a1a1aa;
|
||||||
--text-quaternary: #666666;
|
--text-disabled: #d4d4d8;
|
||||||
--text-disabled: #444444;
|
|
||||||
|
|
||||||
/* 边框颜色 */
|
--border-primary: #e4e4e7;
|
||||||
--border-primary: #1f1f1f;
|
--border-secondary: #d4d4d8;
|
||||||
--border-secondary: #2a2a2a;
|
--border-tertiary: #a1a1aa;
|
||||||
--border-tertiary: #3a3a3a;
|
--border-quaternary: #71717a;
|
||||||
--border-quaternary: #4a4a4a;
|
|
||||||
|
|
||||||
/* 阴影 */
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
--shadow-md: 0 2px 8px -2px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.03);
|
||||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
--shadow-lg: 0 8px 24px -4px rgba(0, 0, 0, 0.06), 0 2px 8px -4px rgba(0, 0, 0, 0.04);
|
||||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
--shadow-xl: 0 16px 40px -8px rgba(0, 0, 0, 0.08), 0 4px 16px -8px rgba(0, 0, 0, 0.05);
|
||||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
/* 圆角 */
|
|
||||||
--radius-sm: 4px;
|
--radius-sm: 4px;
|
||||||
--radius-md: 6px;
|
--radius-md: 6px;
|
||||||
--radius-lg: 8px;
|
--radius-lg: 8px;
|
||||||
--radius-xl: 12px;
|
--radius-xl: 10px;
|
||||||
--radius-2xl: 16px;
|
--radius-2xl: 12px;
|
||||||
|
|
||||||
/* 间距 */
|
|
||||||
--spacing-xs: 4px;
|
--spacing-xs: 4px;
|
||||||
--spacing-sm: 8px;
|
--spacing-sm: 8px;
|
||||||
--spacing-md: 12px;
|
--spacing-md: 12px;
|
||||||
@@ -72,7 +62,6 @@
|
|||||||
--spacing-2xl: 24px;
|
--spacing-2xl: 24px;
|
||||||
--spacing-3xl: 32px;
|
--spacing-3xl: 32px;
|
||||||
|
|
||||||
/* 字体大小 */
|
|
||||||
--text-xs: 12px;
|
--text-xs: 12px;
|
||||||
--text-sm: 14px;
|
--text-sm: 14px;
|
||||||
--text-base: 16px;
|
--text-base: 16px;
|
||||||
@@ -81,18 +70,15 @@
|
|||||||
--text-2xl: 24px;
|
--text-2xl: 24px;
|
||||||
--text-3xl: 30px;
|
--text-3xl: 30px;
|
||||||
|
|
||||||
/* 字体粗细 */
|
|
||||||
--font-normal: 400;
|
--font-normal: 400;
|
||||||
--font-medium: 500;
|
--font-medium: 500;
|
||||||
--font-semibold: 600;
|
--font-semibold: 600;
|
||||||
--font-bold: 700;
|
--font-bold: 700;
|
||||||
|
|
||||||
/* 过渡动画 */
|
|
||||||
--transition-fast: 0.15s ease;
|
--transition-fast: 0.15s ease;
|
||||||
--transition-normal: 0.2s ease;
|
--transition-normal: 0.2s ease;
|
||||||
--transition-slow: 0.3s ease;
|
--transition-slow: 0.3s ease;
|
||||||
|
|
||||||
/* Z-index层级 */
|
|
||||||
--z-dropdown: 100;
|
--z-dropdown: 100;
|
||||||
--z-sticky: 200;
|
--z-sticky: 200;
|
||||||
--z-fixed: 300;
|
--z-fixed: 300;
|
||||||
@@ -102,81 +88,72 @@
|
|||||||
--z-tooltip: 700;
|
--z-tooltip: 700;
|
||||||
--z-toast: 800;
|
--z-toast: 800;
|
||||||
|
|
||||||
/* 特殊状态颜色 */
|
--status-pending: #eab308;
|
||||||
--status-pending: #fbbf24;
|
--status-pending-light: rgba(234, 179, 8, 0.12);
|
||||||
--status-pending-light: rgba(251, 191, 36, 0.1);
|
--status-approved: #22c55e;
|
||||||
--status-approved: #10b981;
|
--status-approved-light: rgba(34, 197, 94, 0.12);
|
||||||
--status-approved-light: rgba(16, 185, 129, 0.1);
|
--status-played: #71717a;
|
||||||
--status-played: #6b7280;
|
--status-played-light: rgba(113, 113, 122, 0.12);
|
||||||
--status-played-light: rgba(107, 114, 128, 0.1);
|
|
||||||
|
|
||||||
/* 投票相关颜色 */
|
|
||||||
--vote-heart: #ef4444;
|
--vote-heart: #ef4444;
|
||||||
--vote-heart-hover: #dc2626;
|
--vote-heart-hover: #dc2626;
|
||||||
--vote-count: var(--text-tertiary);
|
--vote-count: var(--text-tertiary);
|
||||||
|
|
||||||
/* 拖拽状态颜色 */
|
|
||||||
--drag-over: var(--primary-light);
|
--drag-over: var(--primary-light);
|
||||||
--drag-over-border: var(--primary);
|
--drag-over-border: var(--primary);
|
||||||
--dragging-opacity: 0.5;
|
--dragging-opacity: 0.5;
|
||||||
|
|
||||||
/* 表格颜色 */
|
--table-header-bg: #f4f4f5;
|
||||||
--table-header-bg: var(--bg-tertiary);
|
--table-row-hover: #fafafa;
|
||||||
--table-row-hover: var(--bg-hover);
|
--table-border: #e4e4e7;
|
||||||
--table-border: var(--border-secondary);
|
--table-selected: rgba(79, 70, 229, 0.08);
|
||||||
--table-selected: var(--primary-light);
|
|
||||||
|
|
||||||
/* 卡片颜色 */
|
--card-bg: #ffffff;
|
||||||
--card-bg: var(--bg-secondary);
|
--card-border: #e4e4e7;
|
||||||
--card-border: var(--border-secondary);
|
--card-hover-border: #d4d4d8;
|
||||||
--card-hover-border: var(--border-tertiary);
|
--card-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.03);
|
||||||
--card-shadow: var(--shadow-md);
|
|
||||||
|
|
||||||
/* 按钮颜色 */
|
|
||||||
--btn-primary-bg: var(--primary);
|
--btn-primary-bg: var(--primary);
|
||||||
--btn-primary-hover: var(--primary-hover);
|
--btn-primary-hover: var(--primary-hover);
|
||||||
--btn-primary-text: var(--text-primary);
|
--btn-primary-text: #ffffff;
|
||||||
--btn-primary-border: var(--primary-border);
|
--btn-primary-border: var(--primary-border);
|
||||||
|
|
||||||
--btn-secondary-bg: #3a3a3a;
|
--btn-secondary-bg: #f4f4f5;
|
||||||
--btn-secondary-hover: #4a4a4a;
|
--btn-secondary-hover: #e4e4e7;
|
||||||
--btn-secondary-text: var(--text-primary);
|
--btn-secondary-text: #09090b;
|
||||||
--btn-secondary-border: #555555;
|
--btn-secondary-border: #d4d4d8;
|
||||||
|
|
||||||
--btn-success-bg: var(--success);
|
--btn-success-bg: var(--success);
|
||||||
--btn-success-hover: var(--success-hover);
|
--btn-success-hover: var(--success-hover);
|
||||||
--btn-success-text: var(--text-primary);
|
--btn-success-text: #ffffff;
|
||||||
--btn-success-border: var(--success-border);
|
--btn-success-border: var(--success-border);
|
||||||
|
|
||||||
--btn-warning-bg: var(--warning);
|
--btn-warning-bg: var(--warning);
|
||||||
--btn-warning-hover: var(--warning-hover);
|
--btn-warning-hover: var(--warning-hover);
|
||||||
--btn-warning-text: var(--text-primary);
|
--btn-warning-text: #ffffff;
|
||||||
--btn-warning-border: var(--warning-border);
|
--btn-warning-border: var(--warning-border);
|
||||||
|
|
||||||
--btn-error-bg: var(--error);
|
--btn-error-bg: var(--error);
|
||||||
--btn-error-hover: var(--error-hover);
|
--btn-error-hover: var(--error-hover);
|
||||||
--btn-error-text: var(--text-primary);
|
--btn-error-text: #ffffff;
|
||||||
--btn-error-border: var(--error-border);
|
--btn-error-border: var(--error-border);
|
||||||
|
|
||||||
--btn-disabled-bg: var(--bg-tertiary);
|
--btn-disabled-bg: var(--bg-tertiary);
|
||||||
--btn-disabled-text: var(--text-disabled);
|
--btn-disabled-text: var(--text-disabled);
|
||||||
|
|
||||||
/* 输入框颜色 */
|
--input-bg: #ffffff;
|
||||||
--input-bg: var(--bg-tertiary);
|
--input-border: #e4e4e7;
|
||||||
--input-border: var(--border-tertiary);
|
--input-border-focus: #4f46e5;
|
||||||
--input-border-focus: var(--primary);
|
--input-text: #09090b;
|
||||||
--input-text: var(--text-primary);
|
--input-placeholder: #a1a1aa;
|
||||||
--input-placeholder: var(--text-quaternary);
|
--input-shadow-focus: 0 0 0 3px rgba(79, 70, 229, 0.12);
|
||||||
--input-shadow-focus: 0 0 0 3px var(--primary-light);
|
|
||||||
|
|
||||||
/* 模态框颜色 */
|
--modal-backdrop: rgba(0, 0, 0, 0.4);
|
||||||
--modal-backdrop: rgba(0, 0, 0, 0.7);
|
|
||||||
--modal-bg: var(--bg-secondary);
|
--modal-bg: var(--bg-secondary);
|
||||||
--modal-border: var(--border-secondary);
|
--modal-border: var(--border-secondary);
|
||||||
--modal-header-border: var(--border-secondary);
|
--modal-header-border: var(--border-secondary);
|
||||||
--modal-footer-border: var(--border-secondary);
|
--modal-footer-border: var(--border-secondary);
|
||||||
|
|
||||||
/* 通知颜色 */
|
|
||||||
--notification-success-bg: var(--success-light);
|
--notification-success-bg: var(--success-light);
|
||||||
--notification-success-border: var(--success-border);
|
--notification-success-border: var(--success-border);
|
||||||
--notification-success-text: var(--success);
|
--notification-success-text: var(--success);
|
||||||
@@ -193,14 +170,51 @@
|
|||||||
--notification-info-border: var(--info-border);
|
--notification-info-border: var(--info-border);
|
||||||
--notification-info-text: var(--info);
|
--notification-info-text: var(--info);
|
||||||
|
|
||||||
/* 加载动画颜色 */
|
|
||||||
--spinner-primary: var(--primary);
|
--spinner-primary: var(--primary);
|
||||||
--spinner-secondary: var(--bg-tertiary);
|
--spinner-secondary: var(--bg-tertiary);
|
||||||
|
|
||||||
/* 滚动条颜色 */
|
--scrollbar-track: #f4f4f5;
|
||||||
|
--scrollbar-thumb: #d4d4d8;
|
||||||
|
--scrollbar-thumb-hover: #a1a1aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--bg-primary: #111111;
|
||||||
|
--bg-secondary: #1a1a1a;
|
||||||
|
--bg-tertiary: #2a2a2a;
|
||||||
|
--bg-quaternary: #3a3a3a;
|
||||||
|
--bg-hover: #1f1f1f;
|
||||||
|
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #cccccc;
|
||||||
|
--text-tertiary: #888888;
|
||||||
|
--text-quaternary: #666666;
|
||||||
|
--text-disabled: #444444;
|
||||||
|
|
||||||
|
--border-primary: #1f1f1f;
|
||||||
|
--border-secondary: #2a2a2a;
|
||||||
|
--border-tertiary: #3a3a3a;
|
||||||
|
--border-quaternary: #4a4a4a;
|
||||||
|
|
||||||
|
--card-bg: #1a1a1a;
|
||||||
|
--card-border: #2a2a2a;
|
||||||
|
|
||||||
|
--btn-secondary-bg: #3a3a3a;
|
||||||
|
--btn-secondary-hover: #4a4a4a;
|
||||||
|
--btn-secondary-text: #ffffff;
|
||||||
|
--btn-secondary-border: #555555;
|
||||||
|
|
||||||
|
--input-bg: var(--bg-tertiary);
|
||||||
|
--input-border: var(--border-tertiary);
|
||||||
|
--input-text: var(--text-primary);
|
||||||
|
--input-placeholder: var(--text-quaternary);
|
||||||
|
|
||||||
--scrollbar-track: var(--bg-tertiary);
|
--scrollbar-track: var(--bg-tertiary);
|
||||||
--scrollbar-thumb: var(--bg-quaternary);
|
--scrollbar-thumb: var(--bg-quaternary);
|
||||||
--scrollbar-thumb-hover: #4a4a4a;
|
--scrollbar-thumb-hover: #4a4a4a;
|
||||||
|
|
||||||
|
--modal-backdrop: rgba(0, 0, 0, 0.7);
|
||||||
|
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='light'],
|
:root[data-theme='light'],
|
||||||
@@ -398,121 +412,6 @@
|
|||||||
--radius-2xl: 12px;
|
--radius-2xl: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 前台页面专属浅色主题覆盖(广播园艺风格)===== */
|
|
||||||
body[data-frontend="true"]:not(.admin-body) {
|
|
||||||
/* 强调色:紫粉蓝渐变点缀(广播园艺三色花) */
|
|
||||||
--accent-purple: #A855F7;
|
|
||||||
--accent-pink: #EC4899;
|
|
||||||
--accent-blue: #3B82F6;
|
|
||||||
|
|
||||||
/* 背景色:纯白为主,留白优先 */
|
|
||||||
--bg-primary: #FFFFFF;
|
|
||||||
--bg-secondary: #F9FAFB;
|
|
||||||
--bg-tertiary: #F3F4F6;
|
|
||||||
--bg-quaternary: #E5E7EB;
|
|
||||||
--bg-hover: #F3F4F6;
|
|
||||||
|
|
||||||
/* 文字颜色:近黑高对比度 */
|
|
||||||
--text-primary: #111827;
|
|
||||||
--text-secondary: #4B5563;
|
|
||||||
--text-tertiary: #6B7280;
|
|
||||||
--text-quaternary: #9CA3AF;
|
|
||||||
--text-disabled: #D1D5DB;
|
|
||||||
|
|
||||||
/* 边框颜色:硬边清晰,4px 网格 */
|
|
||||||
--border-primary: #E5E7EB;
|
|
||||||
--border-secondary: #D1D5DB;
|
|
||||||
--border-tertiary: #9CA3AF;
|
|
||||||
--border-quaternary: #6B7280;
|
|
||||||
|
|
||||||
/* 卡片:白底薄边 + 极轻阴影,不抢视觉 */
|
|
||||||
--card-bg: #FFFFFF;
|
|
||||||
--card-border: #E5E7EB;
|
|
||||||
--card-hover-border: #D1D5DB;
|
|
||||||
--card-shadow: 0 1px 3px 0 rgba(17, 24, 39, 0.04), 0 1px 2px -1px rgba(17, 24, 39, 0.03);
|
|
||||||
|
|
||||||
/* 主色改为深靛蓝(不抢紫粉蓝点缀的风头) */
|
|
||||||
--primary: #4338CA;
|
|
||||||
--primary-hover: #3730A3;
|
|
||||||
--primary-light: rgba(67, 56, 202, 0.08);
|
|
||||||
--primary-border: rgba(67, 56, 202, 0.15);
|
|
||||||
|
|
||||||
/* 信息色沿用主蓝 */
|
|
||||||
--info: #4338CA;
|
|
||||||
--info-hover: #3730A3;
|
|
||||||
--info-light: rgba(67, 56, 202, 0.08);
|
|
||||||
--info-border: rgba(67, 56, 202, 0.15);
|
|
||||||
|
|
||||||
/* 语义色保持经典但略降饱和度 */
|
|
||||||
--success: #059669;
|
|
||||||
--success-hover: #047857;
|
|
||||||
--success-light: rgba(5, 150, 105, 0.08);
|
|
||||||
--success-border: rgba(5, 150, 105, 0.15);
|
|
||||||
|
|
||||||
--warning: #D97706;
|
|
||||||
--warning-hover: #B45309;
|
|
||||||
--warning-light: rgba(217, 119, 6, 0.08);
|
|
||||||
--warning-border: rgba(217, 119, 6, 0.15);
|
|
||||||
|
|
||||||
--error: #DC2626;
|
|
||||||
--error-hover: #B91C1C;
|
|
||||||
--error-light: rgba(220, 38, 38, 0.08);
|
|
||||||
--error-border: rgba(220, 38, 38, 0.15);
|
|
||||||
|
|
||||||
/* 状态色 */
|
|
||||||
--status-pending: #CA8A04;
|
|
||||||
--status-pending-light: rgba(202, 138, 4, 0.08);
|
|
||||||
--status-approved: #059669;
|
|
||||||
--status-approved-light: rgba(5, 150, 105, 0.08);
|
|
||||||
--status-played: #6B7280;
|
|
||||||
--status-played-light: rgba(107, 114, 128, 0.08);
|
|
||||||
|
|
||||||
/* 输入框:纯白底薄边 */
|
|
||||||
--input-bg: #FFFFFF;
|
|
||||||
--input-border: #D1D5DB;
|
|
||||||
--input-border-focus: #4338CA;
|
|
||||||
--input-text: #111827;
|
|
||||||
--input-placeholder: #9CA3AF;
|
|
||||||
--input-shadow-focus: 0 0 0 3px rgba(67, 56, 202, 0.1);
|
|
||||||
|
|
||||||
/* 按钮:次要按钮硬边设计 */
|
|
||||||
--btn-secondary-bg: #F9FAFB;
|
|
||||||
--btn-secondary-hover: #F3F4F6;
|
|
||||||
--btn-secondary-text: #111827;
|
|
||||||
--btn-secondary-border: #D1D5DB;
|
|
||||||
|
|
||||||
/* 模态框与通知 */
|
|
||||||
--modal-backdrop: rgba(17, 24, 39, 0.4);
|
|
||||||
--modal-bg: #FFFFFF;
|
|
||||||
--modal-border: #E5E7EB;
|
|
||||||
--modal-header-border: #E5E7EB;
|
|
||||||
--modal-footer-border: #E5E7EB;
|
|
||||||
|
|
||||||
/* 圆角:硬边设计,上限 rounded-lg (8px) */
|
|
||||||
--radius-sm: 4px;
|
|
||||||
--radius-md: 6px;
|
|
||||||
--radius-lg: 8px;
|
|
||||||
--radius-xl: 8px;
|
|
||||||
--radius-2xl: 8px;
|
|
||||||
|
|
||||||
/* 阴影:轻盈不厚重 */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(17, 24, 39, 0.04);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgba(17, 24, 39, 0.05), 0 2px 4px -2px rgba(17, 24, 39, 0.03);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(17, 24, 39, 0.05), 0 4px 6px -4px rgba(17, 24, 39, 0.03);
|
|
||||||
--shadow-xl: 0 20px 25px -5px rgba(17, 24, 39, 0.05), 0 8px 10px -6px rgba(17, 24, 39, 0.03);
|
|
||||||
|
|
||||||
/* 滚动条 */
|
|
||||||
--scrollbar-track: #F3F4F6;
|
|
||||||
--scrollbar-thumb: #D1D5DB;
|
|
||||||
--scrollbar-thumb-hover: #9CA3AF;
|
|
||||||
|
|
||||||
/* 表格 */
|
|
||||||
--table-header-bg: #F9FAFB;
|
|
||||||
--table-row-hover: #F9FAFB;
|
|
||||||
--table-border: #E5E7EB;
|
|
||||||
--table-selected: rgba(67, 56, 202, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 深色主题特定调整 */
|
/* 深色主题特定调整 */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -176,27 +176,19 @@
|
|||||||
:key="item.seq"
|
:key="item.seq"
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
:class="[
|
||||||
'seq',
|
'sequence-pill',
|
||||||
item.taken ? 'seq-taken' : '',
|
item.taken ? 'taken' : 'free',
|
||||||
expectedPlaySequence === String(item.seq) ? 'seq-selected' : ''
|
expectedPlaySequence === String(item.seq) ? 'selected' : ''
|
||||||
]"
|
]"
|
||||||
:disabled="item.taken"
|
:disabled="item.taken"
|
||||||
:title="getTakenSequenceTitle(item.seq)"
|
:title="item.taken ? getTakenSequenceTitle(item.seq) : ''"
|
||||||
@click="chooseExpectedPlaySequence(item.seq)"
|
@click="chooseExpectedPlaySequence(item.seq)"
|
||||||
>
|
>
|
||||||
{{ item.seq }}
|
{{ item.seq }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedExpectedPlayDate" class="sequence-hint">🟣 可选(紫粉蓝选中) · ⚪ 已占用(灰色带删除线)</div>
|
<div v-if="selectedExpectedPlayDate" class="sequence-hint">绿色可选,红色已占用</div>
|
||||||
<div
|
|
||||||
v-if="selectedExpectedPlayDate && expectedPlaySequence"
|
|
||||||
class="sequence-locked"
|
|
||||||
style="margin-top:14px; display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border-radius:8px; background:linear-gradient(90deg, rgba(168,85,247,.08), rgba(236,72,153,.08), rgba(59,130,246,.08)); border:1px solid rgba(168,85,247,.2); color:#7C3AED; font-size:13px; font-weight:700;"
|
|
||||||
>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
已锁定 第 {{ expectedPlaySequence }} 首 位置
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索结果容器 -->
|
<!-- 搜索结果容器 -->
|
||||||
@@ -3134,55 +3126,47 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sequence-grid {
|
.sequence-grid {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
margin-top: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.seq {
|
.sequence-pill {
|
||||||
width: 40px;
|
height: 28px;
|
||||||
height: 40px;
|
min-width: 28px;
|
||||||
display: inline-flex;
|
padding: 0 10px;
|
||||||
align-items: center;
|
border-radius: 10px;
|
||||||
justify-content: center;
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 8px;
|
background: rgba(0, 0, 0, 0.25);
|
||||||
background: #fff;
|
color: rgba(255, 255, 255, 0.85);
|
||||||
border: 1.5px solid #d1d5db;
|
font-family: 'MiSans', sans-serif;
|
||||||
color: #111827;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
transition: all 0.2s ease;
|
||||||
transition: all 0.12s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.seq:hover:not(:disabled) {
|
.sequence-pill.free {
|
||||||
border-color: #4338ca;
|
border-color: rgba(16, 185, 129, 0.35);
|
||||||
color: #4338ca;
|
background: rgba(16, 185, 129, 0.08);
|
||||||
transform: scale(1.03);
|
color: rgba(16, 185, 129, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seq-taken {
|
.sequence-pill.taken {
|
||||||
background: #f3f4f6;
|
border-color: rgba(239, 68, 68, 0.35);
|
||||||
border-color: #e5e7eb;
|
background: rgba(239, 68, 68, 0.08);
|
||||||
color: #9ca3af;
|
color: rgba(239, 68, 68, 0.95);
|
||||||
text-decoration: line-through;
|
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seq-selected {
|
.sequence-pill.selected {
|
||||||
background: linear-gradient(135deg, #a855f7 0%, #ec4899 50%, #3b82f6 100%);
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
|
||||||
color: #fff;
|
border-color: rgba(59, 130, 246, 0.6);
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 6px 16px rgba(168, 85, 247, 0.35);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sequence-hint {
|
.sequence-hint {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(255, 255, 255, 0.55);
|
color: rgba(255, 255, 255, 0.55);
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-section {
|
.search-section {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ export const useSongs = () => {
|
|||||||
const playTimes = ref<PlayTime[]>([])
|
const playTimes = ref<PlayTime[]>([])
|
||||||
const playTimeEnabled = ref(false)
|
const playTimeEnabled = ref(false)
|
||||||
const songCount = ref(0)
|
const songCount = ref(0)
|
||||||
|
const expectedPlayDates = ref<any[]>([])
|
||||||
|
|
||||||
// 显示通知
|
// 显示通知
|
||||||
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
@@ -916,6 +917,56 @@ export const useSongs = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取期望播出日期及空闲档期
|
||||||
|
const fetchExpectedPlayDates = async (silent = false, forceRefresh = false) => {
|
||||||
|
if (!silent) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await dedup.dedupedRequest(
|
||||||
|
'expected-play-dates',
|
||||||
|
async () => {
|
||||||
|
const url = forceRefresh
|
||||||
|
? `/api/expected-play-dates?t=${Date.now()}`
|
||||||
|
: '/api/expected-play-dates'
|
||||||
|
return await $fetch(url)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
forceRefresh
|
||||||
|
)
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
expectedPlayDates.value = data
|
||||||
|
} else if (data && Array.isArray((data as any).data)) {
|
||||||
|
expectedPlayDates.value = (data as any).data
|
||||||
|
} else {
|
||||||
|
expectedPlayDates.value = []
|
||||||
|
}
|
||||||
|
return expectedPlayDates.value
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!silent) {
|
||||||
|
error.value = err.message || '获取档期数据失败'
|
||||||
|
}
|
||||||
|
return expectedPlayDates.value
|
||||||
|
} finally {
|
||||||
|
if (!silent) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalFreeSlots = computed(() =>
|
||||||
|
expectedPlayDates.value.reduce(
|
||||||
|
(sum, d) => sum + (Array.isArray(d.freeSequences) ? d.freeSequences.length : 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const openDaysCount = computed(() =>
|
||||||
|
expectedPlayDates.value.filter((d) => !d.isFull).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedPlayDatesLoaded = computed(() => Array.isArray(expectedPlayDates.value))
|
||||||
|
|
||||||
// 初始化加载
|
// 初始化加载
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
await fetchPlayTimes()
|
await fetchPlayTimes()
|
||||||
@@ -932,6 +983,10 @@ export const useSongs = () => {
|
|||||||
publicSchedules,
|
publicSchedules,
|
||||||
visibleSongs,
|
visibleSongs,
|
||||||
songCount,
|
songCount,
|
||||||
|
expectedPlayDates,
|
||||||
|
totalFreeSlots,
|
||||||
|
openDaysCount,
|
||||||
|
expectedPlayDatesLoaded,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
notification,
|
notification,
|
||||||
@@ -943,6 +998,7 @@ export const useSongs = () => {
|
|||||||
fetchPublicSchedules,
|
fetchPublicSchedules,
|
||||||
fetchPlayTimes,
|
fetchPlayTimes,
|
||||||
fetchSongCount,
|
fetchSongCount,
|
||||||
|
fetchExpectedPlayDates,
|
||||||
refreshSongsSilent,
|
refreshSongsSilent,
|
||||||
refreshSchedulesSilent,
|
refreshSchedulesSilent,
|
||||||
checkSimilarSongs,
|
checkSimilarSongs,
|
||||||
|
|||||||
@@ -99,13 +99,16 @@ export function useTheme() {
|
|||||||
|
|
||||||
const defaultTheme = computed<string>(() => {
|
const defaultTheme = computed<string>(() => {
|
||||||
if (siteConfigLoaded.value && siteDefaultTheme.value) {
|
if (siteConfigLoaded.value && siteDefaultTheme.value) {
|
||||||
return siteDefaultTheme.value
|
const v = siteDefaultTheme.value
|
||||||
|
if (v === 'System' || v === 'dark' || v.endsWith('Dark')) return 'ModernLight'
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
const fromRuntime = config.public?.defaultTheme
|
const fromRuntime = config.public?.defaultTheme
|
||||||
if (typeof fromRuntime === 'string' && fromRuntime) {
|
if (typeof fromRuntime === 'string' && fromRuntime) {
|
||||||
|
if (fromRuntime === 'System' || fromRuntime === 'dark' || fromRuntime.endsWith('Dark')) return 'ModernLight'
|
||||||
return fromRuntime
|
return fromRuntime
|
||||||
}
|
}
|
||||||
return 'System'
|
return 'ModernLight'
|
||||||
})
|
})
|
||||||
|
|
||||||
const theme = computed<string>({
|
const theme = computed<string>({
|
||||||
@@ -193,7 +196,8 @@ export function useTheme() {
|
|||||||
|
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const valid = firstValidTheme(stored)
|
const valid = firstValidTheme(stored)
|
||||||
if (valid === stored || enabledThemes.value.includes(stored)) {
|
const effectiveStored = resolveEffectiveTheme(valid) as string
|
||||||
|
if ((valid === stored || enabledThemes.value.includes(stored)) && effectiveStored !== 'dark') {
|
||||||
setTheme(stored)
|
setTheme(stored)
|
||||||
watchSystemTheme()
|
watchSystemTheme()
|
||||||
return
|
return
|
||||||
|
|||||||
+284
-149
@@ -1,8 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home pb-24 lg:pb-0">
|
<div class="home">
|
||||||
|
<div class="ellipse-effect" />
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- 右上角浮动用户操作区(简化版,仅保留 user-section) -->
|
<div class="top-bar">
|
||||||
<div class="user-actions-floating">
|
<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>
|
||||||
|
<!-- 横线和学校logo -->
|
||||||
|
<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">
|
<div class="user-section">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div v-if="isClientAuthenticated" class="user-info">
|
<div v-if="isClientAuthenticated" class="user-info">
|
||||||
@@ -57,62 +85,144 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section Hero -->
|
<div v-if="siteTitle" class="site-title">
|
||||||
<section id="hero-section" class="hero-block">
|
<div class="title-container">
|
||||||
<div class="hero-left">
|
<h2 class="main-title">{{ siteTitle }}</h2>
|
||||||
<h1 class="hero-title">{{ siteTitle }}</h1>
|
<div class="title-divider" />
|
||||||
<p class="hero-subtitle">下周校园,听你的。</p>
|
<span class="sub-title">VoiceHub 校园广播系统</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="hero-cta">
|
<!-- Hero CTA:投稿按钮正中央 + 档期概览卡片(紧邻按钮下方) -->
|
||||||
|
<section class="hero-cta-section">
|
||||||
<button
|
<button
|
||||||
class="bg-gradient-to-r from-purple-500 via-pink-500 to-blue-500 text-white px-6 py-3 rounded-lg font-bold shadow-lg hover:shadow-xl transition-shadow"
|
type="button"
|
||||||
@click="scrollToSection('request-section')"
|
class="hero-request-btn"
|
||||||
|
:disabled="submissionClosed"
|
||||||
|
:title="submissionClosed ? '投稿通道已关闭' : '我要点歌投稿'"
|
||||||
|
@click="handleHeroRequestClick"
|
||||||
>
|
>
|
||||||
立即点歌
|
<Icon class="btn-icon" :class="{ pulse: !submissionClosed }" name="mic" :size="22" />
|
||||||
|
<span>{{ submissionClosed ? '投稿已关闭' : '我要点歌投稿' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
class="border border-gray-300 text-gray-700 px-6 py-3 rounded-lg font-bold hover:bg-gray-50"
|
|
||||||
@click="scrollToSection('schedule-section')"
|
|
||||||
>
|
|
||||||
查看排期
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-info">
|
<div class="schedule-overview-card" aria-label="档期概览">
|
||||||
<span>
|
<div class="schedule-overview-grid">
|
||||||
{{ isRequestOpen ? '当前投稿开放' : '当前投稿关闭' }} · 共 {{ songCount || 0 }} 首投稿 · {{ scheduleCount || 0 }} 首已定排期
|
<div class="overview-stat">
|
||||||
</span>
|
<span class="stat-icon">📅</span>
|
||||||
|
<span class="stat-label">今日可点排期</span>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span>{{ todaySchedulesCount }}</span>
|
||||||
|
<span class="unit">首</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-detail">{{ todayLabel }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="overview-stat">
|
||||||
|
<span class="stat-icon">🎵</span>
|
||||||
|
<span class="stat-label">待播歌曲总数</span>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span>{{ totalSchedulesCount }}</span>
|
||||||
|
<span class="unit">首</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-detail">累计审核通过 / 审核中</div>
|
||||||
|
</div>
|
||||||
|
<div class="overview-stat primary">
|
||||||
|
<span class="stat-icon">✨</span>
|
||||||
|
<span class="stat-label">当前空闲档期</span>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span>{{ totalFreeSlots }}</span>
|
||||||
|
<span class="unit">个</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-detail">
|
||||||
|
<span class="live-dot"></span>
|
||||||
|
实时更新 · {{ openDaysCount }} 天开放
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hero-illustration-slot" class="w-full max-w-sm">
|
|
||||||
<ClientOnly><HeroIllustration /></ClientOnly>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section Schedule -->
|
<!-- 中间主体内容区域 -->
|
||||||
<section id="schedule-section" class="content-block">
|
<div class="content-area">
|
||||||
<div class="section-header">
|
<!-- 选项卡区域 -->
|
||||||
<h2>本周排期</h2>
|
<div class="tabs-row">
|
||||||
<span class="sub">播出时间表</span>
|
<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>
|
</div>
|
||||||
<ClientOnly>
|
<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
|
<SongsScheduleList
|
||||||
:error="error"
|
:error="error"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:schedules="publicSchedules"
|
:schedules="publicSchedules"
|
||||||
@semester-change="handleSemesterChange"
|
@semester-change="handleSemesterChange"
|
||||||
/>
|
/>
|
||||||
</ClientOnly>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section Songs -->
|
|
||||||
<section id="songs-section" class="content-block">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>热门投稿</h2>
|
|
||||||
<span class="sub">为喜欢的歌投票</span>
|
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
|
||||||
|
<!-- 用户使用指南组件 -->
|
||||||
|
<UserTutorial />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 歌曲列表内容 -->
|
||||||
|
<div v-else-if="activeTab === 'songs'" key="songs" class="tab-pane">
|
||||||
|
<div class="song-list-container">
|
||||||
<SongsSongList
|
<SongsSongList
|
||||||
:error="error"
|
:error="error"
|
||||||
:is-admin="isAdmin"
|
:is-admin="isAdmin"
|
||||||
@@ -125,38 +235,37 @@
|
|||||||
@request-replay="handleRequestReplay"
|
@request-replay="handleRequestReplay"
|
||||||
@semester-change="handleSemesterChange"
|
@semester-change="handleSemesterChange"
|
||||||
/>
|
/>
|
||||||
</ClientOnly>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<!-- Section Request -->
|
<!-- 投稿歌曲内容 -->
|
||||||
<section id="request-section" class="content-block">
|
<div v-else-if="activeTab === 'request'" key="request" class="tab-pane request-pane">
|
||||||
<div class="section-header">
|
|
||||||
<h2>投稿歌曲</h2>
|
|
||||||
<span class="sub" @click="showRules = true">点歌规则</span>
|
|
||||||
</div>
|
|
||||||
<ClientOnly>
|
|
||||||
<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>
|
|
||||||
<SongsRequestForm
|
<SongsRequestForm
|
||||||
v-else
|
|
||||||
ref="requestFormRef"
|
ref="requestFormRef"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@request="handleRequest"
|
@request="handleRequest"
|
||||||
@vote="handleVote"
|
@vote="handleVote"
|
||||||
/>
|
/>
|
||||||
</ClientOnly>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section Notification -->
|
<!-- 通知内容 -->
|
||||||
<section id="notification-section" class="content-block">
|
<div
|
||||||
<div class="section-header">
|
v-else-if="activeTab === 'notification'"
|
||||||
<h2>我的消息</h2>
|
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">
|
<button class="settings-icon" @click="toggleNotificationSettings">
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -176,16 +285,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
|
||||||
<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-list">
|
<div class="notification-list">
|
||||||
<div v-if="notificationsLoading" class="loading-indicator">
|
<div v-if="notificationsLoading" class="loading-indicator">
|
||||||
@@ -486,15 +586,13 @@
|
|||||||
@confirm="handleConfirmAction"
|
@confirm="handleConfirmAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 页脚信息显示 -->
|
<!-- 页脚信息显示 -->
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<MobileBottomNav @navigate="handleMobileNav" />
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 规则弹窗 -->
|
<!-- 规则弹窗 -->
|
||||||
@@ -513,13 +611,13 @@
|
|||||||
@click.self="showRules = false"
|
@click.self="showRules = false"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-zinc-900 border border-zinc-800 w-full max-w-lg rounded-3xl overflow-hidden shadow-2xl flex flex-col"
|
class="bg-white border border-zinc-200 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 class="p-8 pb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-black text-zinc-100 tracking-tight flex items-center gap-3">
|
<h3 class="text-xl font-black text-zinc-900 tracking-tight flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-2xl bg-blue-600/10 flex items-center justify-center text-blue-500"
|
class="w-10 h-10 rounded-2xl bg-indigo-600/10 flex items-center justify-center text-indigo-600"
|
||||||
>
|
>
|
||||||
<Icon name="bell" :size="20" />
|
<Icon name="bell" :size="20" />
|
||||||
</div>
|
</div>
|
||||||
@@ -528,7 +626,7 @@
|
|||||||
<p class="text-xs text-zinc-500 mt-1 ml-13">投稿前请仔细阅读以下规则</p>
|
<p class="text-xs text-zinc-500 mt-1 ml-13">投稿前请仔细阅读以下规则</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="p-3 bg-zinc-800/50 hover:bg-zinc-800 text-zinc-500 hover:text-zinc-200 rounded-2xl transition-all"
|
class="p-3 bg-zinc-100 hover:bg-zinc-200 text-zinc-500 hover:text-zinc-900 rounded-2xl transition-all"
|
||||||
@click="showRules = false"
|
@click="showRules = false"
|
||||||
>
|
>
|
||||||
<Icon name="x" :size="20" />
|
<Icon name="x" :size="20" />
|
||||||
@@ -545,23 +643,23 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<div
|
<div
|
||||||
v-if="submissionGuidelines"
|
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"
|
class="text-sm text-zinc-700 leading-relaxed font-medium bg-zinc-50 p-6 rounded-3xl border border-zinc-200"
|
||||||
v-html="submissionGuidelines.replace(/\n/g, '<br>')"
|
v-html="submissionGuidelines.replace(/\n/g, '<br>')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="space-y-3 bg-zinc-950/50 p-6 rounded-3xl border border-zinc-800/50"
|
class="space-y-3 bg-zinc-50 p-6 rounded-3xl border border-zinc-200"
|
||||||
>
|
>
|
||||||
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
|
<div class="flex gap-3 text-sm text-zinc-700 font-medium">
|
||||||
<span class="text-blue-500 font-black">01</span>
|
<span class="text-indigo-600 font-black">01</span>
|
||||||
<p>投稿时无需加入书名号</p>
|
<p>投稿时无需加入书名号</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
|
<div class="flex gap-3 text-sm text-zinc-700 font-medium">
|
||||||
<span class="text-blue-500 font-black">02</span>
|
<span class="text-indigo-600 font-black">02</span>
|
||||||
<p>除DJ外,其他类型歌曲均接收(包括小语种)</p>
|
<p>除DJ外,其他类型歌曲均接收(包括小语种)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 text-sm text-zinc-400 font-medium">
|
<div class="flex gap-3 text-sm text-zinc-700 font-medium">
|
||||||
<span class="text-blue-500 font-black">03</span>
|
<span class="text-indigo-600 font-black">03</span>
|
||||||
<p>禁止投递含有违规内容的歌曲</p>
|
<p>禁止投递含有违规内容的歌曲</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -575,15 +673,15 @@
|
|||||||
播放时间
|
播放时间
|
||||||
</h4>
|
</h4>
|
||||||
<div
|
<div
|
||||||
class="bg-blue-600/10 border border-blue-500/20 p-6 rounded-3xl flex items-center gap-4"
|
class="bg-indigo-50 border border-indigo-200 p-6 rounded-3xl flex items-center gap-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-12 h-12 rounded-2xl bg-blue-600 flex items-center justify-center text-white shadow-lg shadow-blue-900/40"
|
class="w-12 h-12 rounded-2xl bg-indigo-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/20"
|
||||||
>
|
>
|
||||||
<Icon name="clock" :size="24" />
|
<Icon name="clock" :size="24" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-black text-zinc-100">每天夜自修静班前</p>
|
<p class="text-sm font-black text-zinc-900">每天夜自修静班前</p>
|
||||||
<p class="text-[10px] text-zinc-500 font-bold uppercase tracking-widest mt-0.5">
|
<p class="text-[10px] text-zinc-500 font-bold uppercase tracking-widest mt-0.5">
|
||||||
PLAYBACK TIME
|
PLAYBACK TIME
|
||||||
</p>
|
</p>
|
||||||
@@ -594,7 +692,7 @@
|
|||||||
|
|
||||||
<div class="p-8 pt-0">
|
<div class="p-8 pt-0">
|
||||||
<button
|
<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"
|
class="w-full px-6 py-4 bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-black rounded-2xl transition-all uppercase tracking-widest shadow-lg shadow-indigo-500/20 active:scale-95"
|
||||||
@click="showRules = false"
|
@click="showRules = false"
|
||||||
>
|
>
|
||||||
我知道了
|
我知道了
|
||||||
@@ -622,8 +720,6 @@ import { useRouter } from 'vue-router'
|
|||||||
import logo from '~~/public/images/logo.png'
|
import logo from '~~/public/images/logo.png'
|
||||||
import Icon from '~/components/UI/Icon.vue'
|
import Icon from '~/components/UI/Icon.vue'
|
||||||
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||||||
import HeroIllustration from '~/components/UI/HeroIllustration.vue'
|
|
||||||
import MobileBottomNav from '~/components/UI/MobileBottomNav.vue'
|
|
||||||
|
|
||||||
import { useNotifications } from '~/composables/useNotifications'
|
import { useNotifications } from '~/composables/useNotifications'
|
||||||
import { useSiteConfig } from '~/composables/useSiteConfig'
|
import { useSiteConfig } from '~/composables/useSiteConfig'
|
||||||
@@ -771,10 +867,15 @@ const setupRefreshInterval = async () => {
|
|||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
songs.fetchSongs(false, undefined, true, true),
|
songs.fetchSongs(false, undefined, true, true),
|
||||||
songs.fetchPublicSchedules(false, undefined, true, true),
|
songs.fetchPublicSchedules(false, undefined, true, true),
|
||||||
|
songs.fetchExpectedPlayDates(true, true),
|
||||||
loadNotifications()
|
loadNotifications()
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
await Promise.allSettled([songs.fetchPublicSchedules(false, undefined, true, true), songs.fetchSongCount()])
|
await Promise.allSettled([
|
||||||
|
songs.fetchPublicSchedules(false, undefined, true, true),
|
||||||
|
songs.fetchSongCount(),
|
||||||
|
songs.fetchExpectedPlayDates(true, true)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSongCounts()
|
await updateSongCounts()
|
||||||
@@ -1116,13 +1217,14 @@ onMounted(async () => {
|
|||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
songs.fetchSongs(),
|
songs.fetchSongs(),
|
||||||
songs.fetchPublicSchedules(),
|
songs.fetchPublicSchedules(),
|
||||||
|
songs.fetchExpectedPlayDates(),
|
||||||
loadNotifications(),
|
loadNotifications(),
|
||||||
fetchNotificationSettings()
|
fetchNotificationSettings()
|
||||||
])
|
])
|
||||||
await checkPasswordChangeRequired(currentUser)
|
await checkPasswordChangeRequired(currentUser)
|
||||||
} else {
|
} else {
|
||||||
hasInitializedAuthData.value = false
|
hasInitializedAuthData.value = false
|
||||||
await Promise.allSettled([songs.fetchSongCount(), songs.fetchPublicSchedules()])
|
await Promise.allSettled([songs.fetchSongCount(), songs.fetchPublicSchedules(), songs.fetchExpectedPlayDates()])
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSongCounts()
|
await updateSongCounts()
|
||||||
@@ -1147,7 +1249,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('首页初始化失败:', error)
|
console.error('首页初始化失败:', error)
|
||||||
await Promise.allSettled([songs.fetchPublicSchedules(), songs.fetchSongCount()])
|
await Promise.allSettled([songs.fetchPublicSchedules(), songs.fetchSongCount(), songs.fetchExpectedPlayDates()])
|
||||||
await updateSongCounts()
|
await updateSongCounts()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1164,6 +1266,73 @@ const realSongCount = computed(() => {
|
|||||||
return songs?.visibleSongs?.value?.length || 0
|
return songs?.visibleSongs?.value?.length || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Hero CTA & 档期概览相关计算属性
|
||||||
|
const totalFreeSlots = computed(() => songs?.totalFreeSlots?.value ?? 0)
|
||||||
|
const openDaysCount = computed(() => songs?.openDaysCount?.value ?? 0)
|
||||||
|
const expectedPlayDates = computed(() => songs?.expectedPlayDates?.value ?? [])
|
||||||
|
|
||||||
|
const todayDateStr = computed(() => {
|
||||||
|
const d = new Date()
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const todayLabel = computed(() => {
|
||||||
|
const d = new Date()
|
||||||
|
const md = `${d.getMonth() + 1}月${d.getDate()}日`
|
||||||
|
const wd = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]
|
||||||
|
return `${md} · 周${wd}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalSchedulesCount = computed(() => {
|
||||||
|
const list = songs?.publicSchedules?.value || []
|
||||||
|
return list.filter((s) => {
|
||||||
|
const song = s?.song
|
||||||
|
return song ? !song.played : true
|
||||||
|
}).length
|
||||||
|
})
|
||||||
|
|
||||||
|
const todaySchedulesCount = computed(() => {
|
||||||
|
const today = todayDateStr.value
|
||||||
|
const list = songs?.publicSchedules?.value || []
|
||||||
|
return list.filter((s) => {
|
||||||
|
const sDate = s?.expectedPlayDate || s?.playDate
|
||||||
|
if (!sDate) return false
|
||||||
|
const normalized = typeof sDate === 'string' ? sDate.slice(0, 10) : sDate
|
||||||
|
if (normalized !== today) return false
|
||||||
|
const song = s?.song
|
||||||
|
return song ? !song.played : true
|
||||||
|
}).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 投稿通道关闭判定(兼容 isRequestOpen ref)
|
||||||
|
const submissionClosed = computed(() => {
|
||||||
|
if (typeof isRequestOpen?.value === 'boolean') return !isRequestOpen.value
|
||||||
|
const all = expectedPlayDates.value
|
||||||
|
if (all.length === 0) return false
|
||||||
|
return all.every((d) => !!d?.isFull)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hero 投稿按钮点击:未登录先提示登录,已登录跳到投稿 Tab
|
||||||
|
const handleHeroRequestClick = () => {
|
||||||
|
if (!isClientAuthenticated.value) {
|
||||||
|
if (window.$showNotification) window.$showNotification('请先登录后再投稿音乐', 'info')
|
||||||
|
navigateTo('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (submissionClosed.value) {
|
||||||
|
if (window.$showNotification) window.$showNotification('当前所有档期已满,暂无法投稿', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleTabClick('request')
|
||||||
|
nextTick(() => {
|
||||||
|
const target = document.querySelector('.request-tab-pane')
|
||||||
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 使用计算属性安全地访问数据
|
// 使用计算属性安全地访问数据
|
||||||
const publicSchedules = computed(() => songs?.publicSchedules?.value || [])
|
const publicSchedules = computed(() => songs?.publicSchedules?.value || [])
|
||||||
const allSongs = computed(() => songs?.visibleSongs?.value || [])
|
const allSongs = computed(() => songs?.visibleSongs?.value || [])
|
||||||
@@ -1226,10 +1395,12 @@ const handleRequest = async (songData, done) => {
|
|||||||
await requestFormRef.value.refreshSubmissionStatus()
|
await requestFormRef.value.refreshSubmissionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿成功后自动滚动到歌曲列表
|
// 如果当前在歌曲列表页,自动切换到该页面
|
||||||
|
if (activeTab.value !== 'songs') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollToSection('songs-section')
|
handleTabClick('songs')
|
||||||
}, 500)
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof done === 'function') done(true)
|
if (typeof done === 'function') done(true)
|
||||||
return true
|
return true
|
||||||
@@ -1474,22 +1645,9 @@ const vRipple = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理标签点击事件(@deprecated 保留以防它处引用,页面现已改为纵向区块无标签切换)
|
// 处理标签点击事件,添加动画效果
|
||||||
const handleTabClick = (tab) => {
|
const handleTabClick = (tab) => {
|
||||||
// activeTab.value = tab
|
activeTab.value = tab
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动到指定区块
|
|
||||||
const scrollToSection = (id) => {
|
|
||||||
const el = document.getElementById(id)
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端底部导航处理
|
|
||||||
const handleMobileNav = (target) => {
|
|
||||||
if (target === 'account') { router.push('/account'); return }
|
|
||||||
const el = document.getElementById(target)
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加导航到登录页面的方法
|
// 添加导航到登录页面的方法
|
||||||
@@ -1599,30 +1757,21 @@ if (
|
|||||||
/* 添加顶部Ellipse 1效果 */
|
/* 添加顶部Ellipse 1效果 */
|
||||||
.ellipse-effect {
|
.ellipse-effect {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -165px;
|
top: -200px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%) perspective(500px) rotateX(10deg);
|
transform: translateX(-50%) perspective(500px) rotateX(10deg);
|
||||||
width: 1110px;
|
width: 1110px;
|
||||||
height: 309px;
|
height: 360px;
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
ellipse at center,
|
ellipse at center,
|
||||||
rgba(11, 90, 254, 0.3) 0%,
|
rgba(79, 70, 229, 0.18) 0%,
|
||||||
rgba(11, 90, 254, 0.15) 30%,
|
rgba(79, 70, 229, 0.08) 35%,
|
||||||
rgba(11, 90, 254, 0) 70%
|
rgba(79, 70, 229, 0) 72%
|
||||||
);
|
);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右上角浮动用户操作区 */
|
|
||||||
.user-actions-floating {
|
|
||||||
position: relative;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部区域样式 */
|
/* 顶部区域样式 */
|
||||||
.top-bar {
|
.top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1922,7 +2071,7 @@ if (
|
|||||||
/* 站点标题 */
|
/* 站点标题 */
|
||||||
.site-title {
|
.site-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 3rem 0;
|
margin: 2.2rem 0 0.2rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1930,15 +2079,15 @@ if (
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-title {
|
.main-title {
|
||||||
font-family: 'MiSans', sans-serif;
|
font-family: 'MiSans', sans-serif;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 42px;
|
font-size: 34px;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
background: linear-gradient(135deg, var(--text-primary, #1a2332) 0%, var(--text-secondary, #4a5568) 100%);
|
background: linear-gradient(135deg, var(--text-primary, #09090b) 0%, var(--text-secondary, #3f3f46) 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -3632,18 +3781,4 @@ if (
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 5px 15px rgba(0, 67, 248, 0.3);
|
box-shadow: 0 5px 15px rgba(0, 67, 248, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ 新增纵向五区块样式 ============ */
|
|
||||||
.home { width:100%; flex:1; background: var(--bg-primary, #FFFFFF); padding: 0; color: var(--text-primary, #111827); min-height:100vh; }
|
|
||||||
.main-content { max-width: 1200px; margin: 0 auto; padding: 24px 16px 32px; }
|
|
||||||
.hero-block { padding: 48px 0 24px; display: flex; flex-direction: column; gap: 32px; }
|
|
||||||
@media (min-width: 1024px) { .hero-block { flex-direction: row; align-items: center; justify-content: space-between; } }
|
|
||||||
.hero-title { font-size: 44px; font-weight: 900; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 12px; }
|
|
||||||
.hero-subtitle { font-size: 18px; color: var(--text-secondary, #4B5563); font-weight: 500; margin-bottom: 24px; }
|
|
||||||
.hero-cta { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
|
|
||||||
.status-info { display: inline-flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--bg-secondary, #F9FAFB); border: 1px solid var(--border-primary, #E5E7EB); border-radius: 8px; font-size: 13px; color: var(--text-secondary); font-weight: 500; }
|
|
||||||
.content-block { padding: 28px 0; border-top: 1px solid var(--border-primary, #E5E7EB); }
|
|
||||||
.section-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 20px; }
|
|
||||||
.section-header h2 { font-size: 22px; font-weight: 800; letter-spacing: -0.01em; }
|
|
||||||
.section-header .sub { font-size: 13px; color: var(--text-tertiary, #6B7280); font-weight: 500; cursor: pointer; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Generated
+220
-108
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -112,6 +112,7 @@
|
|||||||
"@nuxt/eslint": "^1.17.0",
|
"@nuxt/eslint": "^1.17.0",
|
||||||
"eslint": "^9.39.5",
|
"eslint": "^9.39.5",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"vue-tsc": "^3.3.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user