feat(front): 前台默认浅色模式并新增居中投稿按钮与实时空闲档期

- 主题: useTheme 默认 ModernLight + variables.css SSR-safe 浅值并精修组件对比度
- 首页: 插入居中投稿 CTA + 紧邻档期概览卡片(今日排期/待播歌曲/空闲档期数)
- 数据: useSongs 新增 fetchExpectedPlayDates 接入空闲档期并走 revision 轮询实时刷新
- 视觉: 移动端优先,少圆角硬边几何 + 概览卡片 3→1 列响应式
This commit is contained in:
2026-09-13 19:32:27 +08:00
parent f80abe659f
commit 92704d6637
9 changed files with 1836 additions and 1463 deletions
+337 -4
View File
@@ -7,7 +7,7 @@
/* 防止浏览器自动应用深色主题:改为跟随 data-theme 动态覆盖(见 theme-protection.css) */
:root {
color-scheme: light dark;
color-scheme: light;
}
/* 流体背景与全局样式 */
@@ -82,9 +82,9 @@ body::before {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.01) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(120, 119, 198, 0.02) 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(14, 165, 233, 0.025) 0%, transparent 55%),
radial-gradient(circle at 40% 80%, rgba(79, 70, 229, 0.02) 0%, transparent 55%);
z-index: -1;
pointer-events: none;
}
@@ -472,3 +472,336 @@ body::before {
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
View File
@@ -1,69 +1,59 @@
/* 全局CSS变量 - 统一配色方案 */
/* SSR-safe 默认值 = ModernLight 浅色,防止 hydration 前出现深色闪屏 */
:root {
/* 主色调 */
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.1);
--primary-border: rgba(59, 130, 246, 0.2);
--primary: #4f46e5;
--primary-hover: #4338ca;
--primary-light: rgba(79, 70, 229, 0.1);
--primary-border: rgba(79, 70, 229, 0.2);
/* 成功色 */
--success: #10b981;
--success-hover: #059669;
--success-light: rgba(16, 185, 129, 0.1);
--success-border: rgba(16, 185, 129, 0.2);
--success: #16a34a;
--success-hover: #15803d;
--success-light: rgba(22, 163, 74, 0.1);
--success-border: rgba(22, 163, 74, 0.2);
/* 警告色 */
--warning: #f59e0b;
--warning-hover: #d97706;
--warning-light: rgba(245, 158, 11, 0.1);
--warning-border: rgba(245, 158, 11, 0.2);
--warning: #ea580c;
--warning-hover: #c2410c;
--warning-light: rgba(234, 88, 12, 0.1);
--warning-border: rgba(234, 88, 12, 0.2);
/* 错误色 */
--error: #ef4444;
--error-hover: #dc2626;
--error-light: rgba(239, 68, 68, 0.1);
--error-border: rgba(239, 68, 68, 0.2);
--error: #e11d48;
--error-hover: #be123c;
--error-light: rgba(225, 29, 72, 0.1);
--error-border: rgba(225, 29, 72, 0.2);
/* 信息色 */
--info: #3b82f6;
--info-hover: #2563eb;
--info-light: rgba(59, 130, 246, 0.1);
--info-border: rgba(59, 130, 246, 0.2);
--info: #0ea5e9;
--info-hover: #0284c7;
--info-light: rgba(14, 165, 233, 0.1);
--info-border: rgba(14, 165, 233, 0.2);
/* 背景色 */
--bg-primary: #111111;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
--bg-quaternary: #3a3a3a;
--bg-hover: #1f1f1f;
--bg-primary: #fafafa;
--bg-secondary: #f4f4f5;
--bg-tertiary: #e4e4e7;
--bg-quaternary: #d4d4d8;
--bg-hover: #eeeeef;
/* 文字颜色 */
--text-primary: #ffffff;
--text-secondary: #cccccc;
--text-tertiary: #888888;
--text-quaternary: #666666;
--text-disabled: #444444;
--text-primary: #09090b;
--text-secondary: #3f3f46;
--text-tertiary: #71717a;
--text-quaternary: #a1a1aa;
--text-disabled: #d4d4d8;
/* 边框颜色 */
--border-primary: #1f1f1f;
--border-secondary: #2a2a2a;
--border-tertiary: #3a3a3a;
--border-quaternary: #4a4a4a;
--border-primary: #e4e4e7;
--border-secondary: #d4d4d8;
--border-tertiary: #a1a1aa;
--border-quaternary: #71717a;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
--shadow-md: 0 2px 8px -2px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.03);
--shadow-lg: 0 8px 24px -4px rgba(0, 0, 0, 0.06), 0 2px 8px -4px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 16px 40px -8px rgba(0, 0, 0, 0.08), 0 4px 16px -8px rgba(0, 0, 0, 0.05);
/* 圆角 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-xl: 10px;
--radius-2xl: 12px;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
@@ -72,7 +62,6 @@
--spacing-2xl: 24px;
--spacing-3xl: 32px;
/* 字体大小 */
--text-xs: 12px;
--text-sm: 14px;
--text-base: 16px;
@@ -81,18 +70,15 @@
--text-2xl: 24px;
--text-3xl: 30px;
/* 字体粗细 */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* 过渡动画 */
--transition-fast: 0.15s ease;
--transition-normal: 0.2s ease;
--transition-slow: 0.3s ease;
/* Z-index层级 */
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
@@ -102,81 +88,72 @@
--z-tooltip: 700;
--z-toast: 800;
/* 特殊状态颜色 */
--status-pending: #fbbf24;
--status-pending-light: rgba(251, 191, 36, 0.1);
--status-approved: #10b981;
--status-approved-light: rgba(16, 185, 129, 0.1);
--status-played: #6b7280;
--status-played-light: rgba(107, 114, 128, 0.1);
--status-pending: #eab308;
--status-pending-light: rgba(234, 179, 8, 0.12);
--status-approved: #22c55e;
--status-approved-light: rgba(34, 197, 94, 0.12);
--status-played: #71717a;
--status-played-light: rgba(113, 113, 122, 0.12);
/* 投票相关颜色 */
--vote-heart: #ef4444;
--vote-heart-hover: #dc2626;
--vote-count: var(--text-tertiary);
/* 拖拽状态颜色 */
--drag-over: var(--primary-light);
--drag-over-border: var(--primary);
--dragging-opacity: 0.5;
/* 表格颜色 */
--table-header-bg: var(--bg-tertiary);
--table-row-hover: var(--bg-hover);
--table-border: var(--border-secondary);
--table-selected: var(--primary-light);
--table-header-bg: #f4f4f5;
--table-row-hover: #fafafa;
--table-border: #e4e4e7;
--table-selected: rgba(79, 70, 229, 0.08);
/* 卡片颜色 */
--card-bg: var(--bg-secondary);
--card-border: var(--border-secondary);
--card-hover-border: var(--border-tertiary);
--card-shadow: var(--shadow-md);
--card-bg: #ffffff;
--card-border: #e4e4e7;
--card-hover-border: #d4d4d8;
--card-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.03);
/* 按钮颜色 */
--btn-primary-bg: var(--primary);
--btn-primary-hover: var(--primary-hover);
--btn-primary-text: var(--text-primary);
--btn-primary-text: #ffffff;
--btn-primary-border: var(--primary-border);
--btn-secondary-bg: #3a3a3a;
--btn-secondary-hover: #4a4a4a;
--btn-secondary-text: var(--text-primary);
--btn-secondary-border: #555555;
--btn-secondary-bg: #f4f4f5;
--btn-secondary-hover: #e4e4e7;
--btn-secondary-text: #09090b;
--btn-secondary-border: #d4d4d8;
--btn-success-bg: var(--success);
--btn-success-hover: var(--success-hover);
--btn-success-text: var(--text-primary);
--btn-success-text: #ffffff;
--btn-success-border: var(--success-border);
--btn-warning-bg: var(--warning);
--btn-warning-hover: var(--warning-hover);
--btn-warning-text: var(--text-primary);
--btn-warning-text: #ffffff;
--btn-warning-border: var(--warning-border);
--btn-error-bg: var(--error);
--btn-error-hover: var(--error-hover);
--btn-error-text: var(--text-primary);
--btn-error-text: #ffffff;
--btn-error-border: var(--error-border);
--btn-disabled-bg: var(--bg-tertiary);
--btn-disabled-text: var(--text-disabled);
/* 输入框颜色 */
--input-bg: var(--bg-tertiary);
--input-border: var(--border-tertiary);
--input-border-focus: var(--primary);
--input-text: var(--text-primary);
--input-placeholder: var(--text-quaternary);
--input-shadow-focus: 0 0 0 3px var(--primary-light);
--input-bg: #ffffff;
--input-border: #e4e4e7;
--input-border-focus: #4f46e5;
--input-text: #09090b;
--input-placeholder: #a1a1aa;
--input-shadow-focus: 0 0 0 3px rgba(79, 70, 229, 0.12);
/* 模态框颜色 */
--modal-backdrop: rgba(0, 0, 0, 0.7);
--modal-backdrop: rgba(0, 0, 0, 0.4);
--modal-bg: var(--bg-secondary);
--modal-border: var(--border-secondary);
--modal-header-border: var(--border-secondary);
--modal-footer-border: var(--border-secondary);
/* 通知颜色 */
--notification-success-bg: var(--success-light);
--notification-success-border: var(--success-border);
--notification-success-text: var(--success);
@@ -193,14 +170,51 @@
--notification-info-border: var(--info-border);
--notification-info-text: var(--info);
/* 加载动画颜色 */
--spinner-primary: var(--primary);
--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-thumb: var(--bg-quaternary);
--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'],
@@ -398,121 +412,6 @@
--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) {
:root {
+31 -47
View File
@@ -176,27 +176,19 @@
:key="item.seq"
type="button"
:class="[
'seq',
item.taken ? 'seq-taken' : '',
expectedPlaySequence === String(item.seq) ? 'seq-selected' : ''
'sequence-pill',
item.taken ? 'taken' : 'free',
expectedPlaySequence === String(item.seq) ? 'selected' : ''
]"
:disabled="item.taken"
:title="getTakenSequenceTitle(item.seq)"
:title="item.taken ? getTakenSequenceTitle(item.seq) : ''"
@click="chooseExpectedPlaySequence(item.seq)"
>
{{ item.seq }}
</button>
</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 v-if="selectedExpectedPlayDate" class="sequence-hint">绿色可选,红色已占用</div>
</div>
<!-- 搜索结果容器 -->
@@ -3134,55 +3126,47 @@ defineExpose({
}
.sequence-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
gap: 8px;
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.seq {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #fff;
border: 1.5px solid #d1d5db;
color: #111827;
font-size: 16px;
font-weight: 800;
transition: all 0.12s ease;
cursor: pointer;
.sequence-pill {
height: 28px;
min-width: 28px;
padding: 0 10px;
border-radius: 10px;
border: 1px solid var(--border-primary);
background: rgba(0, 0, 0, 0.25);
color: rgba(255, 255, 255, 0.85);
font-family: 'MiSans', sans-serif;
font-weight: 700;
font-size: 12px;
transition: all 0.2s ease;
}
.seq:hover:not(:disabled) {
border-color: #4338ca;
color: #4338ca;
transform: scale(1.03);
.sequence-pill.free {
border-color: rgba(16, 185, 129, 0.35);
background: rgba(16, 185, 129, 0.08);
color: rgba(16, 185, 129, 0.95);
}
.seq-taken {
background: #f3f4f6;
border-color: #e5e7eb;
color: #9ca3af;
text-decoration: line-through;
.sequence-pill.taken {
border-color: rgba(239, 68, 68, 0.35);
background: rgba(239, 68, 68, 0.08);
color: rgba(239, 68, 68, 0.95);
cursor: not-allowed;
}
.seq-selected {
background: linear-gradient(135deg, #a855f7 0%, #ec4899 50%, #3b82f6 100%);
color: #fff;
border-color: transparent;
box-shadow: 0 6px 16px rgba(168, 85, 247, 0.35);
transform: scale(1.05);
.sequence-pill.selected {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
border-color: rgba(59, 130, 246, 0.6);
}
.sequence-hint {
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.55);
margin-top: 8px;
}
.search-section {
File diff suppressed because it is too large Load Diff
+56
View File
@@ -17,6 +17,7 @@ export const useSongs = () => {
const playTimes = ref<PlayTime[]>([])
const playTimeEnabled = ref(false)
const songCount = ref(0)
const expectedPlayDates = ref<any[]>([])
// 显示通知
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 () => {
await fetchPlayTimes()
@@ -932,6 +983,10 @@ export const useSongs = () => {
publicSchedules,
visibleSongs,
songCount,
expectedPlayDates,
totalFreeSlots,
openDaysCount,
expectedPlayDatesLoaded,
loading,
error,
notification,
@@ -943,6 +998,7 @@ export const useSongs = () => {
fetchPublicSchedules,
fetchPlayTimes,
fetchSongCount,
fetchExpectedPlayDates,
refreshSongsSilent,
refreshSchedulesSilent,
checkSimilarSongs,
+7 -3
View File
@@ -99,13 +99,16 @@ export function useTheme() {
const defaultTheme = computed<string>(() => {
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
if (typeof fromRuntime === 'string' && fromRuntime) {
if (fromRuntime === 'System' || fromRuntime === 'dark' || fromRuntime.endsWith('Dark')) return 'ModernLight'
return fromRuntime
}
return 'System'
return 'ModernLight'
})
const theme = computed<string>({
@@ -193,7 +196,8 @@ export function useTheme() {
if (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)
watchSystemTheme()
return
+597 -462
View File
File diff suppressed because it is too large Load Diff