From 34f067911733276c26f6b18523f3b1064746cb86 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Sat, 12 Sep 2026 18:20:43 +0800 Subject: [PATCH] feat: align with upstream 1.5.9.9 --- .dockerignore | 17 + .env.example | 93 + .github/ISSUE_TEMPLATE/bug_report.yml | 133 + .github/ISSUE_TEMPLATE/config.yml | 14 + .github/ISSUE_TEMPLATE/feature_request.yml | 139 + .github/ISSUE_TEMPLATE/question.yml | 112 + .github/ISSUE_TEMPLATE/security_report.yml | 149 + .github/workflows/build-fpk.yml | 125 + .github/workflows/docker-build.yml | 222 + .github/workflows/docker-postgres.yml | 79 + .github/workflows/nix.yml | 32 + .github/workflows/update-nix-pnpm-hash.yml | 110 + .gitignore | 49 + .prettierignore | 19 + .prettierrc | 7 + .vercelignore | 8 + AGENTS.md | 110 + CODE_OF_CONDUCT.md | 76 + CODE_OF_CONDUCT.zh-CN.md | 76 + Dockerfile | 79 + Dockerfile-postgres | 10 + LICENSE | 674 + README.md | 2428 ++ UPGRADE.md | 225 + app/app.vue | 259 + app/assets/css/components.css | 410 + app/assets/css/lyric-player.module.css | 197 + app/assets/css/main.css | 492 + app/assets/css/markdown.css | 145 + app/assets/css/mobile-admin.css | 455 + app/assets/css/print-fix.css | 278 + app/assets/css/sf-pro-icons.css | 38 + app/assets/css/theme-protection.css | 79 + app/assets/css/themes/ClassicDark.css | 841 + app/assets/css/themes/ClassicLight.css | 841 + app/assets/css/themes/ModernLight.css | 841 + app/assets/css/transitions.css | 265 + app/assets/css/variables.css | 156 + app/assets/css/year-review.css | 177 + app/components/AMLL/LyricPlayer.vue | 390 + app/components/Account/SocialBindings.vue | 627 + app/components/Admin/ApiKeyManager.vue | 1165 + app/components/Admin/BackupAutoSettings.vue | 586 + app/components/Admin/BackupManager.vue | 1357 + app/components/Admin/BatchUpdateModal.vue | 1861 ++ app/components/Admin/BlacklistManager.vue | 673 + app/components/Admin/CardCodesManager.vue | 822 + app/components/Admin/DataAnalysisPanel.vue | 1215 + app/components/Admin/DatabaseManager.vue | 920 + app/components/Admin/EmailTemplateManager.vue | 365 + app/components/Admin/GradeClassManager.vue | 451 + .../Admin/MusicSourceController.vue | 361 + app/components/Admin/NotificationHistory.vue | 1006 + app/components/Admin/NotificationSender.vue | 891 + app/components/Admin/OAuthBindingsModal.vue | 231 + app/components/Admin/OAuthConfigManager.vue | 575 + app/components/Admin/OverviewDashboard.vue | 461 + app/components/Admin/PlayTimeManager.vue | 712 + .../Admin/ProviderConfigSection.vue | 114 + app/components/Admin/RequestTimeManager.vue | 1111 + app/components/Admin/ScheduleForm.vue | 182 + app/components/Admin/ScheduleItemPrint.vue | 437 + app/components/Admin/ScheduleManager.vue | 5719 ++++ .../Admin/SchedulePlaylistFilterModal.vue | 492 + app/components/Admin/SchedulePrinter.vue | 2127 ++ app/components/Admin/ScheduleTablePrint.vue | 376 + app/components/Admin/SemesterManager.vue | 711 + app/components/Admin/Sidebar.vue | 265 + app/components/Admin/SiteConfigManager.vue | 1263 + app/components/Admin/SmtpManager.vue | 418 + app/components/Admin/SongDownloadDialog.vue | 2127 ++ app/components/Admin/SongManagement.vue | 2550 ++ .../Admin/SubmissionRemarkDialog.vue | 138 + app/components/Admin/UserApprovalModal.vue | 293 + app/components/Admin/UserManager.vue | 3071 ++ app/components/Admin/UserSongsModal.vue | 489 + app/components/Admin/VotersModal.vue | 293 + app/components/Auth/CaptchaInput.vue | 109 + app/components/Auth/ChangePasswordForm.vue | 328 + app/components/Auth/LoginForm.vue | 1586 + .../Auth/OAuthBindReminderModal.vue | 334 + app/components/Auth/OAuthBindingCard.vue | 561 + app/components/Auth/OAuthButtons.vue | 117 + app/components/Auth/OAuthQuickLogin.vue | 159 + .../Auth/Providers/Casdoor/Icon.vue | 3 + app/components/Auth/Providers/GitHub/Icon.vue | 13 + app/components/Auth/Providers/Google/Icon.vue | 22 + app/components/Auth/TurnstileWidget.vue | 109 + app/components/Auth/TwoFactorSetup.vue | 240 + app/components/Auth/TwoFactorVerify.vue | 243 + app/components/Common/UserSearchModal.vue | 301 + .../Notifications/NotificationSettings.vue | 236 + app/components/Player/PlayerLyric/AMLyric.vue | 219 + .../Player/PlayerLyric/DefaultLyric.vue | 832 + app/components/SiteFooter.vue | 201 + app/components/Songs/AlbumDetailsModal.vue | 666 + .../Songs/BilibiliEpisodesModal.vue | 402 + app/components/Songs/DuplicateSongModal.vue | 166 + app/components/Songs/ImportSongsModal.vue | 487 + app/components/Songs/NeteaseLoginModal.vue | 269 + app/components/Songs/NeteaseUploadDialog.vue | 633 + .../Songs/PlaylistSelectionModal.vue | 637 + app/components/Songs/PodcastEpisodesModal.vue | 481 + app/components/Songs/QQMusicLoginModal.vue | 371 + app/components/Songs/RecentSongsModal.vue | 448 + app/components/Songs/RequestForm.vue | 7266 +++++ app/components/Songs/ScheduleList.vue | 3798 +++ app/components/Songs/SongList.vue | 3279 +++ app/components/UI/AppLoadingScreen.vue | 293 + app/components/UI/AppleMusicLyrics.vue | 268 + app/components/UI/AudioPlayer.vue | 2920 ++ .../UI/AudioPlayer/AudioElement.vue | 86 + .../UI/AudioPlayer/PlayerControls.vue | 317 + app/components/UI/AudioPlayer/PlayerInfo.vue | 190 + .../UI/AudioPlayer/VolumeControl.vue | 340 + app/components/UI/BilibiliIframeModal.vue | 136 + app/components/UI/Common/AppSpinner.vue | 76 + .../UI/Common/CollapsibleSection.vue | 25 + app/components/UI/Common/CustomSelect.vue | 273 + app/components/UI/Common/DataTable.vue | 661 + app/components/UI/Common/ErrorBoundary.vue | 291 + app/components/UI/Common/InputField.vue | 23 + app/components/UI/Common/LoadingState.vue | 180 + app/components/UI/Common/MethodCard.vue | 49 + app/components/UI/Common/Pagination.vue | 167 + app/components/UI/Common/PasswordField.vue | 37 + app/components/UI/Common/Popover.vue | 155 + app/components/UI/Common/SearchFilter.vue | 473 + app/components/UI/Common/StatCard.vue | 190 + app/components/UI/ConfirmDialog.vue | 206 + app/components/UI/Icon.vue | 619 + .../UI/ImportantNotificationModal.vue | 261 + app/components/UI/LyricsModal.vue | 2589 ++ app/components/UI/MarqueeText.vue | 140 + app/components/UI/Notification.vue | 255 + app/components/UI/NotificationContainer.vue | 283 + app/components/UI/PageTransition.vue | 133 + app/components/UI/ProgressBar.vue | 92 + app/components/UI/SongComments.vue | 1327 + app/components/UI/WarpCanvas.vue | 307 + app/components/year-review/ArtistSlide.vue | 143 + app/components/year-review/FirstSongSlide.vue | 222 + app/components/year-review/IntroSlide.vue | 116 + app/components/year-review/MiscSlide.vue | 146 + app/components/year-review/OutroSlide.vue | 213 + app/components/year-review/ShareCard.vue | 219 + app/components/year-review/StatsSlide.vue | 162 + app/composables/useAdmin.ts | 462 + app/composables/useAudioPlayer.ts | 465 + app/composables/useAudioPlayerControl.ts | 932 + app/composables/useAudioPlayerEnhanced.ts | 375 + app/composables/useAudioPlayerSync.ts | 711 + app/composables/useAudioQuality.ts | 242 + app/composables/useAudioVisualizer.ts | 160 + app/composables/useAuth.ts | 243 + app/composables/useBackgroundRenderer.ts | 264 + app/composables/useBilibiliPreview.ts | 32 + app/composables/useChkszSource.ts | 213 + app/composables/useErrorHandler.ts | 121 + app/composables/useImportantNotification.ts | 159 + app/composables/useLocaleText.ts | 180 + app/composables/useLyricManager.ts | 319 + app/composables/useLyricPlayer.ts | 356 + app/composables/useLyricSettings.ts | 156 + app/composables/useLyrics.ts | 431 + app/composables/useMediaSession.ts | 258 + app/composables/useMusicSources.ts | 2530 ++ app/composables/useMusicWebSocket.ts | 247 + app/composables/useNotifications.ts | 433 + app/composables/useOAuthBindReminder.ts | 34 + app/composables/usePasswordStrength.ts | 53 + app/composables/usePermissions.ts | 128 + app/composables/usePlatformConfig.ts | 110 + app/composables/useProgress.ts | 114 + app/composables/useProgressEvents.ts | 156 + app/composables/useRequestDedup.ts | 120 + app/composables/useSafeLocale.ts | 31 + app/composables/useScrollMemory.ts | 118 + app/composables/useSemesters.ts | 369 + app/composables/useSiteConfig.js | 298 + app/composables/useSongPlayer.ts | 94 + app/composables/useSongs.ts | 817 + app/composables/useSyncedTime.ts | 65 + app/composables/useTheme.ts | 156 + app/composables/useThemeImage.ts | 37 + app/composables/useToast.ts | 212 + app/composables/useUserFilters.ts | 159 + app/drizzle/db.ts | 198 + .../migrations/0000_friendly_garia.sql | 132 + ...50102000001_add_schedule_draft_support.sql | 14 + ...50113000001_add_user_status_management.sql | 55 + .../20250827125530_little_james_howlett.sql | 1 + ...0250828000001_add_api_framework_tables.sql | 68 + .../20250828081010_careful_shinobi_shaw.sql | 2 + .../20250911000001_add_smtp_email_fields.sql | 20 + .../20250913141519_clumsy_maria_hill.sql | 10 + .../20250914030901_majestic_black_widow.sql | 10 + .../20251002000001_add_song_play_url.sql | 2 + .../20251002085227_deep_silver_sable.sql | 3 + ...400_add_collaboration_and_request_time.sql | 42 + .../20260110000001_add_replay_requests.sql | 10 + ...260130094335_add_replay_request_status.sql | 2 + ...260212113037_add_user_identities_table.sql | 11 + ...227102030_add_monthly_submission_limit.sql | 1 + .../20260307132345_add_gongan_filing.sql | 2 + .../20260307133318_remove_gongan_link.sql | 1 + ...54_add_collaborative_submission_toggle.sql | 1 + .../20260319133000_add_submission_remarks.sql | 3 + ...260328120000_add_custom_oauth_provider.sql | 14 + ...1000_add_oauth_system_settings_columns.sql | 15 + .../20260402133552_remarkable_star_brand.sql | 1 + .../20260404135543_add_show_beian_icon.sql | 1 + ...405124654_add_allow_oauth_registration.sql | 1 + .../20260509073125_add_captcha_settings.sql | 2 + .../20260509091403_add_turnstile_support.sql | 3 + ...9_add_instance_id_and_telemetry_toggle.sql | 3 + ...614000100_add_card_code_request_system.sql | 97 + ...260626000100_add_card_code_auto_delete.sql | 18 + ...3521_add_card_code_foreign_key_indexes.sql | 2 + ...60710133348_add_card_code_limit_bypass.sql | 1 + ...718000100_add_aggregate_oauth_provider.sql | 6 + ..._minimize-redis-add-song-query-indexes.sql | 22 + ...20260727000100_replay_request_seamless.sql | 38 + .../20260728114908_add_password_security.sql | 27 + ...260731100048_notification-announcement.sql | 10 + .../20260801000000_add_auto_backup.sql | 14 + ...20260809000000_add_platform_management.sql | 5 + .../20260816000000_add_schedule_song_pool.sql | 21 + .../20260823000000_theme-settings.sql | 2 + .../20260823230554_add_auth_sessions.sql | 25 + ...24160946_add_schedule_visibility_range.sql | 4 + .../20260825102755_add-oauth-avatar.sql | 3 + ..._drop_aggregate_oauth_endpoint_default.sql | 1 + ...ion_review_grade_class_and_note_review.sql | 22 + ...0821_add_register_requires_grade_class.sql | 1 + ...20260828013425_add_song_type_blacklist.sql | 2 + .../20260828073721_add_statistics_code.sql | 2 + .../migrations/meta/0000_snapshot.json | 791 + .../meta/20250827125530_snapshot.json | 783 + .../meta/20250828081010_snapshot.json | 767 + .../meta/20250913141519_snapshot.json | 1144 + .../meta/20250914030901_snapshot.json | 1207 + .../meta/20251002085227_snapshot.json | 1226 + .../meta/20260107092400_snapshot.json | 1445 + .../meta/20260110000001_snapshot.json | 1506 + .../meta/20260130094335_snapshot.json | 1523 + .../meta/20260212113037_snapshot.json | 1596 + .../meta/20260227102030_snapshot.json | 1602 + .../meta/20260307132345_snapshot.json | 1614 ++ .../meta/20260307133318_snapshot.json | 1608 + .../meta/20260319122554_snapshot.json | 1615 ++ .../meta/20260402133552_snapshot.json | 1636 ++ .../meta/20260404135543_snapshot.json | 1643 ++ .../meta/20260405124636_snapshot.json | 1810 ++ .../meta/20260405124654_snapshot.json | 1817 ++ .../meta/20260509073125_snapshot.json | 1831 ++ .../meta/20260509091403_snapshot.json | 1850 ++ .../meta/20260524062329_snapshot.json | 1863 ++ .../meta/20260614000100_snapshot.json | 2123 ++ .../meta/20260626000100_snapshot.json | 2123 ++ .../meta/20260627023521_snapshot.json | 2155 ++ .../meta/20260710133348_snapshot.json | 2162 ++ .../meta/20260720070237_snapshot.json | 2382 ++ .../meta/20260727000100_snapshot.json | 2420 ++ .../meta/20260728114908_snapshot.json | 2571 ++ .../meta/20260731100048_snapshot.json | 2677 ++ .../meta/20260801000000_snapshot.json | 2665 ++ .../meta/20260809000000_snapshot.json | 2679 ++ .../meta/20260816000000_snapshot.json | 2890 ++ .../meta/20260823000000_snapshot.json | 2904 ++ .../meta/20260823230554_snapshot.json | 3055 ++ .../meta/20260824160946_snapshot.json | 3083 ++ .../meta/20260825102755_snapshot.json | 3101 ++ .../meta/20260825105359_snapshot.json | 3100 ++ .../meta/20260826033254_snapshot.json | 3237 +++ .../meta/20260827100821_snapshot.json | 3244 +++ .../meta/20260828013425_snapshot.json | 3246 +++ .../meta/20260828073721_snapshot.json | 3259 +++ app/drizzle/migrations/meta/_journal.json | 349 + app/drizzle/migrations/relations.ts | 13 + app/drizzle/migrations/schema.ts | 415 + app/drizzle/schema.ts | 762 + app/layouts/default.vue | 28 + app/middleware/auth.global.ts | 38 + app/pages/account/index.vue | 940 + app/pages/auth/error.vue | 105 + app/pages/change-password.vue | 375 + app/pages/dashboard.vue | 456 + app/pages/forgot-password.vue | 453 + app/pages/index.vue | 4446 +++ app/pages/login.vue | 334 + app/pages/notification-settings.vue | 282 + app/pages/reset-password.vue | 523 + app/pages/year-review.vue | 400 + app/plugins/auth.client.ts | 162 + app/plugins/auth.server.ts | 8 + app/plugins/locale.ts | 59 + app/plugins/sentry.client.ts | 153 + app/plugins/theme.client.ts | 39 + app/plugins/time-sync.client.ts | 14 + app/utils/autoSchedule.ts | 482 + app/utils/bilibiliSource.ts | 72 + app/utils/blacklist.ts | 39 + app/utils/core/security.ts | 10 + app/utils/data/cjkT2sMap.ts | 546 + app/utils/debounce.ts | 17 + app/utils/embedded-browser.ts | 14 + app/utils/grade-class-input.ts | 32 + app/utils/gradeClassWeights.js | 26 + app/utils/locale/en-US.ts | 4442 +++ app/utils/locale/index.ts | 283 + app/utils/locale/zh-CN.ts | 4451 +++ app/utils/lyric/exclude.ts | 309 + app/utils/lyric/lyricFormat.ts | 230 + app/utils/lyric/lyricLanguage.ts | 76 + app/utils/lyric/lyricMatchQuality.ts | 298 + app/utils/lyric/lyricParser.ts | 532 + app/utils/lyric/lyricStripper.ts | 411 + app/utils/lyric/parseLrc.ts | 69 + app/utils/lyric/qrc-parser.ts | 64 + app/utils/lyricAdapter.ts | 300 + app/utils/markdown.js | 83 + app/utils/musicSources.ts | 253 + app/utils/musicUrl.ts | 612 + app/utils/neteaseApi.ts | 92 + app/utils/oauth-register.ts | 36 + app/utils/oauth.ts | 107 + app/utils/password-policy.ts | 101 + app/utils/platforms.ts | 44 + app/utils/qqUserLibrary.ts | 80 + app/utils/sentryUpstreamMusicErrors.ts | 90 + app/utils/song-name-normalize.ts | 26 + app/utils/timeUtils.ts | 188 + app/utils/url.ts | 181 + app/utils/user-archive.ts | 28 + app/utils/webauthn.js | 227 + app/workers/audioEncoderWorker.js | 369 + custom_changes.patch | 23802 +++++++++++++++ docker-compose.yml | 36 + docker-compose/README.md | 27 + .../docker-compose-prebuild-github-redis.yml | 46 + .../docker-compose-prebuild-github.yml | 32 + .../docker-compose-prebuild-nju-redis.yml | 44 + .../docker-compose-prebuild-nju.yml | 32 + docker-compose/docker-compose-redis.yml | 48 + docker-compose/docker-compose.yml | 36 + drizzle.config.ts | 20 + edgeone.json | 5 + eslint.config.mjs | 48 + flake.lock | 61 + flake.nix | 360 + fnos/app/ui/config | 13 + fnos/cmd/main | 16 + fnos/manifest | 14 + netlify.toml | 32 + nuxt.config.ts | 930 + package-lock.json | 24170 ++++++++++++++++ package.json | 120 + ...-deploy-Docker-compose-Gitea-Actions.patch | 458 + .../0002-ci-Gitea-Actions-Runner-Deploy.patch | 811 + ...i-Gitea-1.27-Runner-Token-RGT-RGO-RG.patch | 157 + ...04-fix-Gitea-1.27-Registration-Token.patch | 61 + ...ea-Runner-404-v3-gitea-runner-assets.patch | 186 + ...-setup-deploy-user.sh-useradd-disabl.patch | 42 + ...ix-Docker-Compose-v2-cpus-ParseFloat.patch | 53 + ...ose-name-.env.example-REDIS_PASSWORD.patch | 46 + ...ehub-cpus-mem_limit-deploy.resources.patch | 39 + ...l-GITEA_HOST-DEPLOY_PATH-data-opt-by.patch | 42 + ...-docker-node-22-bookworm-slim-setup-.patch | 139 + ...-Gitea-container-image-runs-on-ubunt.patch | 59 + patches/0013-style-ci-emoji.patch | 367 + ....json-qq-music-api-npm-ci-legacy-pee.patch | 1186 + ...rt-ref-template-ScheduleList-Request.patch | 191 + ...6-refactor-ci-uses-action-run-GitHub.patch | 481 + ...an-Ubuntu-GitHub-v4.gh-proxy.org-Bui.patch | 167 + ...-Preflight-Job-ca-certificates-HTTPS.patch | 133 + ...-PlaylistSelection-NeteaseUpload-Bil.patch | 178 + ...x-create-env-invalid-driver-option-e.patch | 33 + ...uilder-moby-buildkit-registry-mirror.patch | 77 + ...-ci-docker-buildx-build-context-flag.patch | 40 + ...gistry-owner-repo-tag-repository-nam.patch | 91 + ....toml-Gitea-Basic-Auth-OAuth2-token-.patch | 96 + ...ild-load-docker-push-buildkitd-OAuth.patch | 92 + ...b-docker-compose.override.yml-heredo.patch | 80 + ...mpose.override.yml-heredoc-printf-ba.patch | 49 + patches/0028-feat-songs.patch | 13320 +++++++++ ...ent-add-lang-ts-to-script-setup-to-f.patch | 26 + ...ngs-DB-SYSTEM_SETTINGS_DEFAULTS-Redi.patch | 723 + ...-Set-up-job-docker-pull-Node-22-daem.patch | 205 + ...l-YAML-line-177-could-not-find-expec.patch | 80 + ...tions-setup-node-use-npmmirror-gh-pr.patch | 176 + ...package-lock.json-add-parcel-watcher.patch | 1089 + ...ngs-resolve-index.post.ts-L844-synta.patch | 27 + ...ngs-correctly-balance-fillDefaults-a.patch | 27 + ...hon-scripts-as-base64-to-avoid-YAML-.patch | 76 + ...defaultTheme-ClassicLight-enabledThe.patch | 106 + ...light-node-heap-from-2G-to-4G-resolv.patch | 26 + ...-deploy-env-vars-via-remote-tmp-file.patch | 101 + ...eredoc-based-ENV_SH-with-printf-q-pi.patch | 69 + ...x-schedule-feat-player-5-localStorag.patch | 738 + patches/0043-feat-admin.patch | 1409 + patches/0044-feat-14.patch | 1054 + ...lacklistManager-ScheduleManager-lang.patch | 45 + ...-permissions-user-avatar-server-util.patch | 140 + patches/0047-feat-site-UI.patch | 569 + patches/0048-fix-ui.patch | 514 + ...-Run-53-Preflight-Nitro-OOM-exit-137.patch | 49 + patches/0050-fix-ui-index.vue.patch | 869 + ...1-fix-ci-Run-55-Nitro-OOM-10G-minify.patch | 58 + ...DE_OPTIONS-gc-interval-Run-56-exit-9.patch | 41 + patches/0053-fix-ui-index-dashboard.patch | 441 + patches/0054-fix-ui.patch | 447 + patches/0055-fix-ui.patch | 8396 ++++++ .../0056-chore-remove-temporary-files.patch | 5840 ++++ patches/0057-fix-ui-Tailwind-CSS.patch | 913 + patches/0058-chore-remove-script.patch | 121 + pnpm-lock.yaml | 17120 +++++++++++ pnpm-workspace.yaml | 9 + public/assets/logo-144.png | Bin 0 -> 4817 bytes public/assets/logo.png | Bin 0 -> 19327 bytes public/audio-match/afp.js | 1627 ++ public/audio-match/afp.wasm.js | 5 + public/audio-match/rec.js | 48 + public/favicon.ico | Bin 0 -> 4286 bytes public/images/beian.png | Bin 0 -> 10305 bytes public/robots.txt | 2 + public/themes/ClassicDark/logo.svg | 7 + public/themes/ClassicDark/search.svg | 19 + public/themes/ClassicDark/thumbs-up.svg | 4 + public/themes/ClassicLight/logo.svg | 7 + public/themes/ClassicLight/search.svg | 19 + public/themes/ClassicLight/thumbs-up.svg | 4 + public/themes/ModernLight/logo.svg | 7 + public/themes/ModernLight/search.svg | 19 + public/themes/ModernLight/thumbs-up.svg | 4 + scripts/build.js | 450 + scripts/check-deploy.js | 245 + scripts/clear-database.js | 203 + scripts/create-admin.js | 99 + scripts/db-sync.js | 496 + scripts/deploy.js | 279 + scripts/drizzle/migrations/meta/_journal.json | 1 + scripts/netlify-build.js | 196 + scripts/package.json | 23 + scripts/postinstall.js | 23 + scripts/redis-scan-legacy.js | 83 + scripts/reset-database.js | 181 + scripts/safe-migrate.js | 241 + server/api/admin/activities.get.ts | 94 + server/api/admin/api-keys/[id].delete.ts | 79 + server/api/admin/api-keys/[id].get.ts | 92 + server/api/admin/api-keys/[id].put.ts | 164 + server/api/admin/api-keys/index.get.ts | 199 + server/api/admin/api-keys/index.post.ts | 161 + server/api/admin/api-keys/logs.get.ts | 71 + server/api/admin/api-keys/permissions.ts | 12 + server/api/admin/backup/auto-config.get.ts | 44 + server/api/admin/backup/auto-config.put.ts | 114 + server/api/admin/backup/clear.post.ts | 178 + .../admin/backup/delete/[filename].delete.ts | 71 + server/api/admin/backup/download.get.ts | 84 + .../admin/backup/download/[filename].get.ts | 76 + server/api/admin/backup/export.post.ts | 618 + server/api/admin/backup/history-clear.post.ts | 14 + server/api/admin/backup/history.get.ts | 14 + server/api/admin/backup/list.get.ts | 158 + server/api/admin/backup/restore-chunk.post.ts | 1389 + server/api/admin/backup/restore.post.ts | 2672 ++ server/api/admin/backup/test-email.post.ts | 52 + server/api/admin/backup/test-s3.post.ts | 58 + server/api/admin/backup/test-telegram.post.ts | 46 + server/api/admin/backup/test-webdav.post.ts | 80 + server/api/admin/backup/upload.post.ts | 133 + server/api/admin/blacklist/[id].delete.ts | 57 + server/api/admin/blacklist/[id].patch.ts | 72 + server/api/admin/blacklist/index.get.ts | 63 + server/api/admin/blacklist/index.post.ts | 95 + server/api/admin/card-codes/[id].put.ts | 96 + server/api/admin/card-codes/create.post.ts | 117 + server/api/admin/card-codes/delete.post.ts | 48 + server/api/admin/card-codes/export.get.ts | 85 + server/api/admin/card-codes/index.get.ts | 74 + .../api/admin/card-codes/redeem-logs.get.ts | 126 + server/api/admin/card-codes/update.post.ts | 109 + server/api/admin/database/cleanup.post.ts | 35 + server/api/admin/database/performance.get.ts | 34 + server/api/admin/database/pool-status.get.ts | 34 + server/api/admin/database/reset.post.ts | 282 + server/api/admin/database/status.get.ts | 57 + server/api/admin/db-status.get.ts | 69 + .../api/admin/email-templates/index.delete.ts | 21 + server/api/admin/email-templates/index.get.ts | 51 + .../api/admin/email-templates/index.post.ts | 37 + .../api/admin/email-templates/preview.post.ts | 22 + server/api/admin/fix-sequence.post.ts | 213 + server/api/admin/grade-class/[id].delete.ts | 36 + .../api/admin/grade-class/by-grade.delete.ts | 25 + server/api/admin/grade-class/index.get.ts | 50 + server/api/admin/grade-class/index.post.ts | 85 + .../api/admin/grade-class/initialize.post.ts | 64 + server/api/admin/notifications/history.get.ts | 173 + .../notifications/history/[batchId].delete.ts | 64 + .../notifications/history/[batchId].get.ts | 157 + .../notifications/history/[batchId].put.ts | 181 + server/api/admin/notifications/send.post.ts | 220 + server/api/admin/play-times/[id].ts | 240 + server/api/admin/play-times/index.post.ts | 101 + server/api/admin/play-times/index.ts | 35 + server/api/admin/replay-requests/index.get.ts | 186 + .../api/admin/replay-requests/reject.post.ts | 54 + server/api/admin/request-times/[id].ts | 188 + server/api/admin/request-times/index.post.ts | 99 + server/api/admin/request-times/index.ts | 40 + server/api/admin/schedule.post.ts | 224 + .../api/admin/schedule/bulk-publish.post.ts | 324 + server/api/admin/schedule/copy.post.ts | 158 + server/api/admin/schedule/draft.post.ts | 220 + server/api/admin/schedule/full.get.ts | 459 + server/api/admin/schedule/move-date.post.ts | 128 + server/api/admin/schedule/publish.post.ts | 194 + .../admin/schedule/remove-all-date.post.ts | 45 + server/api/admin/schedule/remove.post.ts | 139 + server/api/admin/schedule/sequence.post.ts | 67 + server/api/admin/schedule/song-pool.delete.ts | 32 + server/api/admin/schedule/song-pool.get.ts | 82 + server/api/admin/schedule/song-pool.post.ts | 102 + server/api/admin/semesters/[id].delete.ts | 63 + server/api/admin/semesters/[id].put.ts | 93 + server/api/admin/semesters/index.get.ts | 27 + server/api/admin/semesters/index.post.ts | 72 + server/api/admin/semesters/set-active.post.ts | 56 + server/api/admin/smtp/reload.post.ts | 37 + server/api/admin/smtp/test-connection.post.ts | 110 + server/api/admin/smtp/test-email.post.ts | 121 + server/api/admin/songs/delete.post.ts | 100 + server/api/admin/songs/duration.post.ts | 291 + server/api/admin/songs/mark-played.post.ts | 136 + server/api/admin/songs/reject.post.ts | 147 + server/api/admin/stats.get.ts | 366 + server/api/admin/stats/active-users.get.ts | 92 + server/api/admin/stats/realtime.get.ts | 230 + .../admin/stats/semester-comparison.get.ts | 69 + server/api/admin/stats/top-songs.get.ts | 80 + server/api/admin/stats/trends.get.ts | 57 + server/api/admin/stats/user-engagement.get.ts | 70 + .../clear-aggregate-bindings.post.ts | 121 + .../system-settings/env-oauth-import.post.ts | 118 + .../admin/system-settings/env-oauth.get.ts | 34 + .../api/admin/system-settings/index.post.ts | 1127 + server/api/admin/system-settings/index.ts | 75 + .../api/admin/system-settings/secretMask.ts | 42 + server/api/admin/users/[id].delete.ts | 79 + server/api/admin/users/[id].get.ts | 73 + server/api/admin/users/[id].put.ts | 262 + server/api/admin/users/[id]/approval.post.ts | 197 + .../admin/users/[id]/reset-password.post.ts | 112 + server/api/admin/users/[id]/songs.get.ts | 273 + .../api/admin/users/[id]/status-logs.get.ts | 123 + server/api/admin/users/[id]/status.put.ts | 132 + .../admin/users/batch-grade-update.post.ts | 181 + server/api/admin/users/batch-status.put.ts | 222 + server/api/admin/users/batch-update.post.ts | 243 + server/api/admin/users/batch.post.ts | 210 + server/api/admin/users/index.get.ts | 193 + server/api/admin/users/index.post.ts | 134 + server/api/admin/users/index.ts | 40 + server/api/admin/users/options.get.ts | 103 + server/api/admin/users/status-logs.get.ts | 199 + server/api/api-enhanced/netease/[...path].ts | 113 + server/api/auth/2fa/send-email.post.ts | 139 + server/api/auth/2fa/verify.post.ts | 292 + server/api/auth/[provider]/callback.get.ts | 456 + server/api/auth/[provider]/index.get.ts | 150 + server/api/auth/bind.post.ts | 252 + server/api/auth/captcha.get.ts | 24 + server/api/auth/change-password.post.ts | 145 + server/api/auth/email-code.post.ts | 79 + server/api/auth/forgot-password.post.ts | 125 + server/api/auth/grade-class-options.get.ts | 11 + server/api/auth/identities.get.ts | 68 + server/api/auth/login.post.ts | 316 + server/api/auth/logout.post.ts | 50 + server/api/auth/oauth-register-options.get.ts | 31 + server/api/auth/oauth-register.post.ts | 217 + server/api/auth/register.post.ts | 235 + server/api/auth/reset-password.post.ts | 106 + server/api/auth/set-initial-password.post.ts | 121 + server/api/auth/unbind.post.ts | 98 + server/api/auth/verify.get.ts | 77 + .../api/auth/webauthn/login/options.post.ts | 21 + server/api/auth/webauthn/login/verify.post.ts | 120 + .../api/auth/webauthn/register/options.get.ts | 65 + .../api/auth/webauthn/register/verify.post.ts | 91 + server/api/auth/webauthn/rename.post.ts | 52 + server/api/bilibili/playurl.get.ts | 114 + server/api/bilibili/search.get.ts | 158 + server/api/blacklist/check.post.ts | 110 + server/api/card-codes/validate.post.ts | 106 + server/api/meow/bind.post.ts | 235 + server/api/meow/unbind.post.ts | 64 + server/api/music/resolve-url.post.ts | 150 + server/api/music/state.post.ts | 110 + server/api/music/websocket.ts | 238 + server/api/native-api/comment/tx.get.ts | 171 + server/api/native-api/lyric/mg.get.ts | 257 + server/api/native-api/lyric/tx.get.ts | 123 + server/api/native-api/migu/playurl.get.ts | 151 + server/api/native-api/qq/avatar.get.ts | 32 + server/api/native-api/qq/check-cookie.post.ts | 41 + server/api/native-api/qq/check-login.post.ts | 77 + .../api/native-api/qq/check-wx-login.post.ts | 66 + server/api/native-api/qq/login-qr-wx.get.ts | 16 + server/api/native-api/qq/login-qr.get.ts | 15 + .../api/native-api/qq/playlist-songs.post.ts | 26 + server/api/native-api/qq/playlists.post.ts | 22 + server/api/native-api/search/mg.get.ts | 162 + server/api/native-api/search/tx.get.ts | 181 + server/api/native-api/search/wy.get.ts | 92 + server/api/notifications/[id].delete.ts | 80 + server/api/notifications/[id]/read.post.ts | 73 + server/api/notifications/clear-all.delete.ts | 58 + server/api/notifications/important.get.ts | 61 + server/api/notifications/index.ts | 160 + .../meow/send-verification.post.ts | 69 + server/api/notifications/meow/test.post.ts | 90 + server/api/notifications/read-all.post.ts | 39 + server/api/notifications/settings.post.ts | 113 + server/api/notifications/settings.ts | 86 + server/api/open/backup/auto.post.ts | 27 + server/api/open/backup/status/[id].get.ts | 72 + server/api/open/card-codes.delete.ts | 4 + server/api/open/card-codes.get.ts | 98 + server/api/open/card-codes.patch.ts | 129 + server/api/open/card-codes.post.ts | 151 + server/api/open/card-codes/delete.post.ts | 4 + server/api/open/schedules.get.ts | 243 + server/api/open/songs.get.ts | 282 + server/api/open/songs/mark-played.post.ts | 118 + server/api/open/songs/request.post.ts | 58 + server/api/platform-config/index.get.ts | 45 + server/api/play-times/index.ts | 32 + server/api/progress/events.ts | 145 + server/api/progress/id.ts | 16 + server/api/proxy/image.get.ts | 255 + server/api/request-times/index.ts | 57 + server/api/semesters/current.get.ts | 31 + server/api/semesters/options.get.ts | 28 + server/api/site-config.get.ts | 40 + server/api/songs/[id]/update.put.ts | 359 + server/api/songs/[id]/voters.get.ts | 98 + server/api/songs/add.post.ts | 151 + server/api/songs/check-restriction.post.ts | 168 + server/api/songs/collaborators/reply.post.ts | 77 + server/api/songs/count.get.ts | 24 + server/api/songs/import.post.ts | 198 + server/api/songs/index.get.ts | 511 + server/api/songs/public.get.ts | 468 + server/api/songs/replay.delete.ts | 40 + server/api/songs/replay.post.ts | 139 + server/api/songs/request.post.ts | 14 + server/api/songs/submission-status.get.ts | 134 + server/api/songs/vote.post.ts | 172 + server/api/songs/withdraw.post.ts | 186 + server/api/sys/time.get.ts | 9 + server/api/system/instance.get.ts | 17 + server/api/system/location.get.ts | 12 + server/api/system/reconnect.post.ts | 40 + server/api/system/status.get.ts | 66 + server/api/user/2fa/disable.post.ts | 37 + server/api/user/2fa/enable.post.ts | 44 + server/api/user/2fa/generate.post.ts | 29 + server/api/user/api-keys/[id].delete.ts | 83 + server/api/user/api-keys/[id]/logs.get.ts | 122 + server/api/user/api-keys/index.get.ts | 59 + server/api/user/api-keys/index.post.ts | 115 + server/api/user/avatar.post.ts | 41 + server/api/user/email/bind.post.ts | 70 + .../user/email/resend-verification.post.ts | 55 + server/api/user/email/send-code.post.ts | 91 + server/api/user/email/unbind.post.ts | 39 + server/api/user/email/verify-code.post.ts | 86 + server/api/user/sessions.delete.ts | 33 + server/api/user/sessions.get.ts | 47 + server/api/user/year-review.get.ts | 163 + server/api/users/search.get.ts | 78 + server/api/users/social-accounts.get.ts | 61 + .../api/users/social-accounts/meow.delete.ts | 55 + server/api/users/social-accounts/meow.post.ts | 88 + server/card-codes/statuses.ts | 3 + server/config/constants.ts | 494 + server/error.ts | 62 + server/middleware/00.request-id.ts | 11 + server/middleware/api-auth.ts | 381 + server/middleware/api-cors.ts | 135 + server/middleware/auth.ts | 360 + server/plugins/00.sentry.ts | 265 + server/plugins/01.pre-warm-ssr.ts | 10 + server/plugins/error-handler.ts | 92 + server/plugins/redis-lifecycle.ts | 15 + server/plugins/statistics-code.ts | 67 + server/services/apiLogService.ts | 407 + server/services/autoBackupService.ts | 510 + server/services/cardCodeDeleteService.ts | 39 + server/services/cardCodeLifecycleService.ts | 168 + server/services/durationValidationService.ts | 74 + server/services/meowNotificationService.ts | 221 + server/services/notificationService.ts | 906 + server/services/oauthConfigService.ts | 130 + server/services/passwordSecurityService.ts | 115 + server/services/securityService.ts | 808 + server/services/smtpService.ts | 813 + server/services/songRequestService.ts | 645 + server/services/userService.ts | 89 + server/tsconfig.json | 3 + server/utils/admin-password-policy.ts | 40 + server/utils/apiError.ts | 33 + server/utils/apiKeyUtils.ts | 62 + server/utils/auth-route-policy.ts | 101 + server/utils/auth-session.ts | 161 + server/utils/auth.ts | 272 + server/utils/bilibiliWbi.ts | 54 + server/utils/captcha.ts | 50 + server/utils/captchaStore.ts | 201 + server/utils/card-code-delete-handler.ts | 68 + server/utils/database-health.ts | 151 + server/utils/database-manager.ts | 241 + server/utils/email-verification.ts | 181 + server/utils/geo.ts | 61 + server/utils/grade-class-core.ts | 39 + server/utils/grade-class-options.ts | 49 + server/utils/important-notification-policy.ts | 72 + server/utils/initial-password-policy.ts | 33 + server/utils/instance-id.ts | 114 + server/utils/ip-utils.ts | 149 + server/utils/jwt-enhanced.ts | 210 + server/utils/log-manager.ts | 395 + server/utils/native_common.ts | 48 + server/utils/native_tx.ts | 267 + server/utils/native_wy.ts | 59 + server/utils/notification-history-policy.ts | 108 + server/utils/oauth-identity.ts | 86 + server/utils/oauth-providers.ts | 127 + server/utils/oauth-strategies.ts | 616 + server/utils/oauth-token.ts | 23 + server/utils/oauth.ts | 307 + server/utils/permissions.js | 114 + server/utils/qqComment.ts | 269 + server/utils/qq_music_sdk.ts | 1470 + server/utils/rateLimiter.ts | 148 + server/utils/redis.ts | 105 + server/utils/register-validation.ts | 35 + server/utils/registration-notify.ts | 44 + server/utils/request-utils.ts | 99 + server/utils/requireSongAdmin.ts | 16 + server/utils/restoreScheduleSongPool.ts | 71 + server/utils/s3Client.ts | 125 + server/utils/scheduleReplayBinding.ts | 129 + server/utils/scheduleSongPool.ts | 13 + server/utils/sequence-sync.ts | 30 + server/utils/serverTime.ts | 18 + server/utils/siteUtils.ts | 24 + server/utils/song-duration-policy.ts | 39 + server/utils/song-name-normalize.ts | 2 + server/utils/song-type-resolver.ts | 221 + server/utils/songDurationFetcher.ts | 81 + server/utils/studentMask.ts | 104 + server/utils/submission-restriction-policy.ts | 48 + server/utils/submissionLimit.ts | 110 + server/utils/system-settings-defaults.ts | 161 + server/utils/system-settings-helper.ts | 40 + server/utils/telemetry.ts | 52 + server/utils/theme-config.ts | 39 + server/utils/user-archive.ts | 3 + server/utils/user-avatar.ts | 72 + server/utils/user.ts | 15 + server/utils/webauthn-config.ts | 40 + server/utils/webauthn-token.ts | 46 + sh/main.sh | 362 + sh/next/deploy.sh | 572 + sh/next/main.sh | 570 + sh/next/update.sh | 308 + temp_update/1.5.9.9.zip | Bin 0 -> 2592128 bytes tests/server/auth-route-policy.test.ts | 71 + .../clear-aggregate-bindings-policy.test.ts | 108 + tests/server/cors-origin-policy.test.ts | 63 + tests/server/grade-class-input.test.ts | 32 + tests/server/grade-class-options.test.ts | 45 + .../important-notification-policy.test.ts | 93 + tests/server/initial-password-policy.test.ts | 86 + .../notification-history-policy.test.ts | 90 + tests/server/oauth-state-cookie.test.ts | 50 + tests/server/password-policy.test.ts | 48 + tests/server/qq-comment-normalize.test.ts | 162 + tests/server/register-validation.test.ts | 29 + tests/server/song-duration-policy.test.ts | 43 + tests/server/song-name-normalize.test.ts | 46 + tests/server/song-type-blacklist.test.ts | 164 + .../submission-restriction-policy.test.ts | 83 + tests/server/token-version-policy.test.ts | 14 + tests/server/user-archive.test.ts | 41 + tests/server/user-avatar.test.ts | 135 + tsconfig.json | 4 + types/global.d.ts | 5 + types/index.ts | 182 + vercel.json | 15 + 805 files changed, 376275 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/ISSUE_TEMPLATE/security_report.yml create mode 100644 .github/workflows/build-fpk.yml create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/docker-postgres.yml create mode 100644 .github/workflows/nix.yml create mode 100644 .github/workflows/update-nix-pnpm-hash.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vercelignore create mode 100644 AGENTS.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CODE_OF_CONDUCT.zh-CN.md create mode 100644 Dockerfile create mode 100644 Dockerfile-postgres create mode 100644 LICENSE create mode 100644 README.md create mode 100644 UPGRADE.md create mode 100644 app/app.vue create mode 100644 app/assets/css/components.css create mode 100644 app/assets/css/lyric-player.module.css create mode 100644 app/assets/css/main.css create mode 100644 app/assets/css/markdown.css create mode 100644 app/assets/css/mobile-admin.css create mode 100644 app/assets/css/print-fix.css create mode 100644 app/assets/css/sf-pro-icons.css create mode 100644 app/assets/css/theme-protection.css create mode 100644 app/assets/css/themes/ClassicDark.css create mode 100644 app/assets/css/themes/ClassicLight.css create mode 100644 app/assets/css/themes/ModernLight.css create mode 100644 app/assets/css/transitions.css create mode 100644 app/assets/css/variables.css create mode 100644 app/assets/css/year-review.css create mode 100644 app/components/AMLL/LyricPlayer.vue create mode 100644 app/components/Account/SocialBindings.vue create mode 100644 app/components/Admin/ApiKeyManager.vue create mode 100644 app/components/Admin/BackupAutoSettings.vue create mode 100644 app/components/Admin/BackupManager.vue create mode 100644 app/components/Admin/BatchUpdateModal.vue create mode 100644 app/components/Admin/BlacklistManager.vue create mode 100644 app/components/Admin/CardCodesManager.vue create mode 100644 app/components/Admin/DataAnalysisPanel.vue create mode 100644 app/components/Admin/DatabaseManager.vue create mode 100644 app/components/Admin/EmailTemplateManager.vue create mode 100644 app/components/Admin/GradeClassManager.vue create mode 100644 app/components/Admin/MusicSourceController.vue create mode 100644 app/components/Admin/NotificationHistory.vue create mode 100644 app/components/Admin/NotificationSender.vue create mode 100644 app/components/Admin/OAuthBindingsModal.vue create mode 100644 app/components/Admin/OAuthConfigManager.vue create mode 100644 app/components/Admin/OverviewDashboard.vue create mode 100644 app/components/Admin/PlayTimeManager.vue create mode 100644 app/components/Admin/ProviderConfigSection.vue create mode 100644 app/components/Admin/RequestTimeManager.vue create mode 100644 app/components/Admin/ScheduleForm.vue create mode 100644 app/components/Admin/ScheduleItemPrint.vue create mode 100644 app/components/Admin/ScheduleManager.vue create mode 100644 app/components/Admin/SchedulePlaylistFilterModal.vue create mode 100644 app/components/Admin/SchedulePrinter.vue create mode 100644 app/components/Admin/ScheduleTablePrint.vue create mode 100644 app/components/Admin/SemesterManager.vue create mode 100644 app/components/Admin/Sidebar.vue create mode 100644 app/components/Admin/SiteConfigManager.vue create mode 100644 app/components/Admin/SmtpManager.vue create mode 100644 app/components/Admin/SongDownloadDialog.vue create mode 100644 app/components/Admin/SongManagement.vue create mode 100644 app/components/Admin/SubmissionRemarkDialog.vue create mode 100644 app/components/Admin/UserApprovalModal.vue create mode 100644 app/components/Admin/UserManager.vue create mode 100644 app/components/Admin/UserSongsModal.vue create mode 100644 app/components/Admin/VotersModal.vue create mode 100644 app/components/Auth/CaptchaInput.vue create mode 100644 app/components/Auth/ChangePasswordForm.vue create mode 100644 app/components/Auth/LoginForm.vue create mode 100644 app/components/Auth/OAuthBindReminderModal.vue create mode 100644 app/components/Auth/OAuthBindingCard.vue create mode 100644 app/components/Auth/OAuthButtons.vue create mode 100644 app/components/Auth/OAuthQuickLogin.vue create mode 100644 app/components/Auth/Providers/Casdoor/Icon.vue create mode 100644 app/components/Auth/Providers/GitHub/Icon.vue create mode 100644 app/components/Auth/Providers/Google/Icon.vue create mode 100644 app/components/Auth/TurnstileWidget.vue create mode 100644 app/components/Auth/TwoFactorSetup.vue create mode 100644 app/components/Auth/TwoFactorVerify.vue create mode 100644 app/components/Common/UserSearchModal.vue create mode 100644 app/components/Notifications/NotificationSettings.vue create mode 100644 app/components/Player/PlayerLyric/AMLyric.vue create mode 100644 app/components/Player/PlayerLyric/DefaultLyric.vue create mode 100644 app/components/SiteFooter.vue create mode 100644 app/components/Songs/AlbumDetailsModal.vue create mode 100644 app/components/Songs/BilibiliEpisodesModal.vue create mode 100644 app/components/Songs/DuplicateSongModal.vue create mode 100644 app/components/Songs/ImportSongsModal.vue create mode 100644 app/components/Songs/NeteaseLoginModal.vue create mode 100644 app/components/Songs/NeteaseUploadDialog.vue create mode 100644 app/components/Songs/PlaylistSelectionModal.vue create mode 100644 app/components/Songs/PodcastEpisodesModal.vue create mode 100644 app/components/Songs/QQMusicLoginModal.vue create mode 100644 app/components/Songs/RecentSongsModal.vue create mode 100644 app/components/Songs/RequestForm.vue create mode 100644 app/components/Songs/ScheduleList.vue create mode 100644 app/components/Songs/SongList.vue create mode 100644 app/components/UI/AppLoadingScreen.vue create mode 100644 app/components/UI/AppleMusicLyrics.vue create mode 100644 app/components/UI/AudioPlayer.vue create mode 100644 app/components/UI/AudioPlayer/AudioElement.vue create mode 100644 app/components/UI/AudioPlayer/PlayerControls.vue create mode 100644 app/components/UI/AudioPlayer/PlayerInfo.vue create mode 100644 app/components/UI/AudioPlayer/VolumeControl.vue create mode 100644 app/components/UI/BilibiliIframeModal.vue create mode 100644 app/components/UI/Common/AppSpinner.vue create mode 100644 app/components/UI/Common/CollapsibleSection.vue create mode 100644 app/components/UI/Common/CustomSelect.vue create mode 100644 app/components/UI/Common/DataTable.vue create mode 100644 app/components/UI/Common/ErrorBoundary.vue create mode 100644 app/components/UI/Common/InputField.vue create mode 100644 app/components/UI/Common/LoadingState.vue create mode 100644 app/components/UI/Common/MethodCard.vue create mode 100644 app/components/UI/Common/Pagination.vue create mode 100644 app/components/UI/Common/PasswordField.vue create mode 100644 app/components/UI/Common/Popover.vue create mode 100644 app/components/UI/Common/SearchFilter.vue create mode 100644 app/components/UI/Common/StatCard.vue create mode 100644 app/components/UI/ConfirmDialog.vue create mode 100644 app/components/UI/Icon.vue create mode 100644 app/components/UI/ImportantNotificationModal.vue create mode 100644 app/components/UI/LyricsModal.vue create mode 100644 app/components/UI/MarqueeText.vue create mode 100644 app/components/UI/Notification.vue create mode 100644 app/components/UI/NotificationContainer.vue create mode 100644 app/components/UI/PageTransition.vue create mode 100644 app/components/UI/ProgressBar.vue create mode 100644 app/components/UI/SongComments.vue create mode 100644 app/components/UI/WarpCanvas.vue create mode 100644 app/components/year-review/ArtistSlide.vue create mode 100644 app/components/year-review/FirstSongSlide.vue create mode 100644 app/components/year-review/IntroSlide.vue create mode 100644 app/components/year-review/MiscSlide.vue create mode 100644 app/components/year-review/OutroSlide.vue create mode 100644 app/components/year-review/ShareCard.vue create mode 100644 app/components/year-review/StatsSlide.vue create mode 100644 app/composables/useAdmin.ts create mode 100644 app/composables/useAudioPlayer.ts create mode 100644 app/composables/useAudioPlayerControl.ts create mode 100644 app/composables/useAudioPlayerEnhanced.ts create mode 100644 app/composables/useAudioPlayerSync.ts create mode 100644 app/composables/useAudioQuality.ts create mode 100644 app/composables/useAudioVisualizer.ts create mode 100644 app/composables/useAuth.ts create mode 100644 app/composables/useBackgroundRenderer.ts create mode 100644 app/composables/useBilibiliPreview.ts create mode 100644 app/composables/useChkszSource.ts create mode 100644 app/composables/useErrorHandler.ts create mode 100644 app/composables/useImportantNotification.ts create mode 100644 app/composables/useLocaleText.ts create mode 100644 app/composables/useLyricManager.ts create mode 100644 app/composables/useLyricPlayer.ts create mode 100644 app/composables/useLyricSettings.ts create mode 100644 app/composables/useLyrics.ts create mode 100644 app/composables/useMediaSession.ts create mode 100644 app/composables/useMusicSources.ts create mode 100644 app/composables/useMusicWebSocket.ts create mode 100644 app/composables/useNotifications.ts create mode 100644 app/composables/useOAuthBindReminder.ts create mode 100644 app/composables/usePasswordStrength.ts create mode 100644 app/composables/usePermissions.ts create mode 100644 app/composables/usePlatformConfig.ts create mode 100644 app/composables/useProgress.ts create mode 100644 app/composables/useProgressEvents.ts create mode 100644 app/composables/useRequestDedup.ts create mode 100644 app/composables/useSafeLocale.ts create mode 100644 app/composables/useScrollMemory.ts create mode 100644 app/composables/useSemesters.ts create mode 100644 app/composables/useSiteConfig.js create mode 100644 app/composables/useSongPlayer.ts create mode 100644 app/composables/useSongs.ts create mode 100644 app/composables/useSyncedTime.ts create mode 100644 app/composables/useTheme.ts create mode 100644 app/composables/useThemeImage.ts create mode 100644 app/composables/useToast.ts create mode 100644 app/composables/useUserFilters.ts create mode 100644 app/drizzle/db.ts create mode 100644 app/drizzle/migrations/0000_friendly_garia.sql create mode 100644 app/drizzle/migrations/20250102000001_add_schedule_draft_support.sql create mode 100644 app/drizzle/migrations/20250113000001_add_user_status_management.sql create mode 100644 app/drizzle/migrations/20250827125530_little_james_howlett.sql create mode 100644 app/drizzle/migrations/20250828000001_add_api_framework_tables.sql create mode 100644 app/drizzle/migrations/20250828081010_careful_shinobi_shaw.sql create mode 100644 app/drizzle/migrations/20250911000001_add_smtp_email_fields.sql create mode 100644 app/drizzle/migrations/20250913141519_clumsy_maria_hill.sql create mode 100644 app/drizzle/migrations/20250914030901_majestic_black_widow.sql create mode 100644 app/drizzle/migrations/20251002000001_add_song_play_url.sql create mode 100644 app/drizzle/migrations/20251002085227_deep_silver_sable.sql create mode 100644 app/drizzle/migrations/20260107092400_add_collaboration_and_request_time.sql create mode 100644 app/drizzle/migrations/20260110000001_add_replay_requests.sql create mode 100644 app/drizzle/migrations/20260130094335_add_replay_request_status.sql create mode 100644 app/drizzle/migrations/20260212113037_add_user_identities_table.sql create mode 100644 app/drizzle/migrations/20260227102030_add_monthly_submission_limit.sql create mode 100644 app/drizzle/migrations/20260307132345_add_gongan_filing.sql create mode 100644 app/drizzle/migrations/20260307133318_remove_gongan_link.sql create mode 100644 app/drizzle/migrations/20260319122554_add_collaborative_submission_toggle.sql create mode 100644 app/drizzle/migrations/20260319133000_add_submission_remarks.sql create mode 100644 app/drizzle/migrations/20260328120000_add_custom_oauth_provider.sql create mode 100644 app/drizzle/migrations/20260328121000_add_oauth_system_settings_columns.sql create mode 100644 app/drizzle/migrations/20260402133552_remarkable_star_brand.sql create mode 100644 app/drizzle/migrations/20260404135543_add_show_beian_icon.sql create mode 100644 app/drizzle/migrations/20260405124654_add_allow_oauth_registration.sql create mode 100644 app/drizzle/migrations/20260509073125_add_captcha_settings.sql create mode 100644 app/drizzle/migrations/20260509091403_add_turnstile_support.sql create mode 100644 app/drizzle/migrations/20260524062329_add_instance_id_and_telemetry_toggle.sql create mode 100644 app/drizzle/migrations/20260614000100_add_card_code_request_system.sql create mode 100644 app/drizzle/migrations/20260626000100_add_card_code_auto_delete.sql create mode 100644 app/drizzle/migrations/20260627023521_add_card_code_foreign_key_indexes.sql create mode 100644 app/drizzle/migrations/20260710133348_add_card_code_limit_bypass.sql create mode 100644 app/drizzle/migrations/20260718000100_add_aggregate_oauth_provider.sql create mode 100644 app/drizzle/migrations/20260720070237_minimize-redis-add-song-query-indexes.sql create mode 100644 app/drizzle/migrations/20260727000100_replay_request_seamless.sql create mode 100644 app/drizzle/migrations/20260728114908_add_password_security.sql create mode 100644 app/drizzle/migrations/20260731100048_notification-announcement.sql create mode 100644 app/drizzle/migrations/20260801000000_add_auto_backup.sql create mode 100644 app/drizzle/migrations/20260809000000_add_platform_management.sql create mode 100644 app/drizzle/migrations/20260816000000_add_schedule_song_pool.sql create mode 100644 app/drizzle/migrations/20260823000000_theme-settings.sql create mode 100644 app/drizzle/migrations/20260823230554_add_auth_sessions.sql create mode 100644 app/drizzle/migrations/20260824160946_add_schedule_visibility_range.sql create mode 100644 app/drizzle/migrations/20260825102755_add-oauth-avatar.sql create mode 100644 app/drizzle/migrations/20260825105359_drop_aggregate_oauth_endpoint_default.sql create mode 100644 app/drizzle/migrations/20260826033254_add_registration_review_grade_class_and_note_review.sql create mode 100644 app/drizzle/migrations/20260827100821_add_register_requires_grade_class.sql create mode 100644 app/drizzle/migrations/20260828013425_add_song_type_blacklist.sql create mode 100644 app/drizzle/migrations/20260828073721_add_statistics_code.sql create mode 100644 app/drizzle/migrations/meta/0000_snapshot.json create mode 100644 app/drizzle/migrations/meta/20250827125530_snapshot.json create mode 100644 app/drizzle/migrations/meta/20250828081010_snapshot.json create mode 100644 app/drizzle/migrations/meta/20250913141519_snapshot.json create mode 100644 app/drizzle/migrations/meta/20250914030901_snapshot.json create mode 100644 app/drizzle/migrations/meta/20251002085227_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260107092400_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260110000001_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260130094335_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260212113037_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260227102030_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260307132345_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260307133318_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260319122554_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260402133552_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260404135543_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260405124636_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260405124654_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260509073125_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260509091403_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260524062329_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260614000100_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260626000100_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260627023521_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260710133348_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260720070237_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260727000100_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260728114908_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260731100048_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260801000000_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260809000000_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260816000000_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260823000000_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260823230554_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260824160946_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260825102755_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260825105359_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260826033254_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260827100821_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260828013425_snapshot.json create mode 100644 app/drizzle/migrations/meta/20260828073721_snapshot.json create mode 100644 app/drizzle/migrations/meta/_journal.json create mode 100644 app/drizzle/migrations/relations.ts create mode 100644 app/drizzle/migrations/schema.ts create mode 100644 app/drizzle/schema.ts create mode 100644 app/layouts/default.vue create mode 100644 app/middleware/auth.global.ts create mode 100644 app/pages/account/index.vue create mode 100644 app/pages/auth/error.vue create mode 100644 app/pages/change-password.vue create mode 100644 app/pages/dashboard.vue create mode 100644 app/pages/forgot-password.vue create mode 100644 app/pages/index.vue create mode 100644 app/pages/login.vue create mode 100644 app/pages/notification-settings.vue create mode 100644 app/pages/reset-password.vue create mode 100644 app/pages/year-review.vue create mode 100644 app/plugins/auth.client.ts create mode 100644 app/plugins/auth.server.ts create mode 100644 app/plugins/locale.ts create mode 100644 app/plugins/sentry.client.ts create mode 100644 app/plugins/theme.client.ts create mode 100644 app/plugins/time-sync.client.ts create mode 100644 app/utils/autoSchedule.ts create mode 100644 app/utils/bilibiliSource.ts create mode 100644 app/utils/blacklist.ts create mode 100644 app/utils/core/security.ts create mode 100644 app/utils/data/cjkT2sMap.ts create mode 100644 app/utils/debounce.ts create mode 100644 app/utils/embedded-browser.ts create mode 100644 app/utils/grade-class-input.ts create mode 100644 app/utils/gradeClassWeights.js create mode 100644 app/utils/locale/en-US.ts create mode 100644 app/utils/locale/index.ts create mode 100644 app/utils/locale/zh-CN.ts create mode 100644 app/utils/lyric/exclude.ts create mode 100644 app/utils/lyric/lyricFormat.ts create mode 100644 app/utils/lyric/lyricLanguage.ts create mode 100644 app/utils/lyric/lyricMatchQuality.ts create mode 100644 app/utils/lyric/lyricParser.ts create mode 100644 app/utils/lyric/lyricStripper.ts create mode 100644 app/utils/lyric/parseLrc.ts create mode 100644 app/utils/lyric/qrc-parser.ts create mode 100644 app/utils/lyricAdapter.ts create mode 100644 app/utils/markdown.js create mode 100644 app/utils/musicSources.ts create mode 100644 app/utils/musicUrl.ts create mode 100644 app/utils/neteaseApi.ts create mode 100644 app/utils/oauth-register.ts create mode 100644 app/utils/oauth.ts create mode 100644 app/utils/password-policy.ts create mode 100644 app/utils/platforms.ts create mode 100644 app/utils/qqUserLibrary.ts create mode 100644 app/utils/sentryUpstreamMusicErrors.ts create mode 100644 app/utils/song-name-normalize.ts create mode 100644 app/utils/timeUtils.ts create mode 100644 app/utils/url.ts create mode 100644 app/utils/user-archive.ts create mode 100644 app/utils/webauthn.js create mode 100644 app/workers/audioEncoderWorker.js create mode 100644 custom_changes.patch create mode 100644 docker-compose.yml create mode 100644 docker-compose/README.md create mode 100644 docker-compose/docker-compose-prebuild-github-redis.yml create mode 100644 docker-compose/docker-compose-prebuild-github.yml create mode 100644 docker-compose/docker-compose-prebuild-nju-redis.yml create mode 100644 docker-compose/docker-compose-prebuild-nju.yml create mode 100644 docker-compose/docker-compose-redis.yml create mode 100644 docker-compose/docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 edgeone.json create mode 100644 eslint.config.mjs create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 fnos/app/ui/config create mode 100644 fnos/cmd/main create mode 100644 fnos/manifest create mode 100644 netlify.toml create mode 100644 nuxt.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 patches/0001-chore-deploy-Docker-compose-Gitea-Actions.patch create mode 100644 patches/0002-ci-Gitea-Actions-Runner-Deploy.patch create mode 100644 patches/0003-ci-Gitea-1.27-Runner-Token-RGT-RGO-RG.patch create mode 100644 patches/0004-fix-Gitea-1.27-Registration-Token.patch create mode 100644 patches/0005-fix-Gitea-Runner-404-v3-gitea-runner-assets.patch create mode 100644 patches/0006-fix-Ubuntu-24.04-setup-deploy-user.sh-useradd-disabl.patch create mode 100644 patches/0007-fix-Docker-Compose-v2-cpus-ParseFloat.patch create mode 100644 patches/0008-fix-docker-compose-name-.env.example-REDIS_PASSWORD.patch create mode 100644 patches/0009-fix-voicehub-cpus-mem_limit-deploy.resources.patch create mode 100644 patches/0010-fix-ci-deploy.yml-GITEA_HOST-DEPLOY_PATH-data-opt-by.patch create mode 100644 patches/0011-fix-ci-Preflight-docker-node-22-bookworm-slim-setup-.patch create mode 100644 patches/0012-fix-ci-Preflight-Gitea-container-image-runs-on-ubunt.patch create mode 100644 patches/0013-style-ci-emoji.patch create mode 100644 patches/0014-fix-package-lock.json-qq-music-api-npm-ci-legacy-pee.patch create mode 100644 patches/0015-fix-4-Modal-import-ref-template-ScheduleList-Request.patch create mode 100644 patches/0016-refactor-ci-uses-action-run-GitHub.patch create mode 100644 patches/0017-perf-ci-Job-Debian-Ubuntu-GitHub-v4.gh-proxy.org-Bui.patch create mode 100644 patches/0018-fix-ci-Preflight-Job-ca-certificates-HTTPS.patch create mode 100644 patches/0019-refactor-cleanup-PlaylistSelection-NeteaseUpload-Bil.patch create mode 100644 patches/0020-fix-ci-Job-buildx-create-env-invalid-driver-option-e.patch create mode 100644 patches/0021-perf-ci-buildx-builder-moby-buildkit-registry-mirror.patch create mode 100644 patches/0022-fix-ci-docker-buildx-build-context-flag.patch create mode 100644 patches/0023-fix-ci-Docker-Registry-owner-repo-tag-repository-nam.patch create mode 100644 patches/0024-fix-ci-buildkitd.toml-Gitea-Basic-Auth-OAuth2-token-.patch create mode 100644 patches/0025-fix-ci-buildx-build-load-docker-push-buildkitd-OAuth.patch create mode 100644 patches/0026-fix-ci-deploy-Job-docker-compose.override.yml-heredo.patch create mode 100644 patches/0027-fix-ci-docker-compose.override.yml-heredoc-printf-ba.patch create mode 100644 patches/0028-feat-songs.patch create mode 100644 patches/0029-fix-song-management-add-lang-ts-to-script-setup-to-f.patch create mode 100644 patches/0030-fix-system-settings-DB-SYSTEM_SETTINGS_DEFAULTS-Redi.patch create mode 100644 patches/0031-fix-ci-preflight-Set-up-job-docker-pull-Node-22-daem.patch create mode 100644 patches/0032-fix-ci-deploy.yml-YAML-line-177-could-not-find-expec.patch create mode 100644 patches/0033-fix-ci-remove-actions-setup-node-use-npmmirror-gh-pr.patch create mode 100644 patches/0034-chore-deps-sync-package-lock.json-add-parcel-watcher.patch create mode 100644 patches/0035-fix-system-settings-resolve-index.post.ts-L844-synta.patch create mode 100644 patches/0036-fix-system-settings-correctly-balance-fillDefaults-a.patch create mode 100644 patches/0037-fix-ci-embed-python-scripts-as-base64-to-avoid-YAML-.patch create mode 100644 patches/0038-feat-theme-sync-defaultTheme-ClassicLight-enabledThe.patch create mode 100644 patches/0039-fix-ci-bump-preflight-node-heap-from-2G-to-4G-resolv.patch create mode 100644 patches/0040-fix-ci-propagate-deploy-env-vars-via-remote-tmp-file.patch create mode 100644 patches/0041-fix-ci-replace-heredoc-based-ENV_SH-with-printf-q-pi.patch create mode 100644 patches/0042-feat-schedule-fix-schedule-feat-player-5-localStorag.patch create mode 100644 patches/0043-feat-admin.patch create mode 100644 patches/0044-feat-14.patch create mode 100644 patches/0045-fix-vue-parser-BlacklistManager-ScheduleManager-lang.patch create mode 100644 patches/0046-fix-nitro-import-permissions-user-avatar-server-util.patch create mode 100644 patches/0047-feat-site-UI.patch create mode 100644 patches/0048-fix-ui.patch create mode 100644 patches/0049-fix-ci-Run-53-Preflight-Nitro-OOM-exit-137.patch create mode 100644 patches/0050-fix-ui-index.vue.patch create mode 100644 patches/0051-fix-ci-Run-55-Nitro-OOM-10G-minify.patch create mode 100644 patches/0052-fix-ci-NODE_OPTIONS-gc-interval-Run-56-exit-9.patch create mode 100644 patches/0053-fix-ui-index-dashboard.patch create mode 100644 patches/0054-fix-ui.patch create mode 100644 patches/0055-fix-ui.patch create mode 100644 patches/0056-chore-remove-temporary-files.patch create mode 100644 patches/0057-fix-ui-Tailwind-CSS.patch create mode 100644 patches/0058-chore-remove-script.patch create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 public/assets/logo-144.png create mode 100644 public/assets/logo.png create mode 100644 public/audio-match/afp.js create mode 100644 public/audio-match/afp.wasm.js create mode 100644 public/audio-match/rec.js create mode 100644 public/favicon.ico create mode 100644 public/images/beian.png create mode 100644 public/robots.txt create mode 100644 public/themes/ClassicDark/logo.svg create mode 100644 public/themes/ClassicDark/search.svg create mode 100644 public/themes/ClassicDark/thumbs-up.svg create mode 100644 public/themes/ClassicLight/logo.svg create mode 100644 public/themes/ClassicLight/search.svg create mode 100644 public/themes/ClassicLight/thumbs-up.svg create mode 100644 public/themes/ModernLight/logo.svg create mode 100644 public/themes/ModernLight/search.svg create mode 100644 public/themes/ModernLight/thumbs-up.svg create mode 100644 scripts/build.js create mode 100644 scripts/check-deploy.js create mode 100644 scripts/clear-database.js create mode 100644 scripts/create-admin.js create mode 100644 scripts/db-sync.js create mode 100644 scripts/deploy.js create mode 100644 scripts/drizzle/migrations/meta/_journal.json create mode 100644 scripts/netlify-build.js create mode 100644 scripts/package.json create mode 100644 scripts/postinstall.js create mode 100644 scripts/redis-scan-legacy.js create mode 100644 scripts/reset-database.js create mode 100644 scripts/safe-migrate.js create mode 100644 server/api/admin/activities.get.ts create mode 100644 server/api/admin/api-keys/[id].delete.ts create mode 100644 server/api/admin/api-keys/[id].get.ts create mode 100644 server/api/admin/api-keys/[id].put.ts create mode 100644 server/api/admin/api-keys/index.get.ts create mode 100644 server/api/admin/api-keys/index.post.ts create mode 100644 server/api/admin/api-keys/logs.get.ts create mode 100644 server/api/admin/api-keys/permissions.ts create mode 100644 server/api/admin/backup/auto-config.get.ts create mode 100644 server/api/admin/backup/auto-config.put.ts create mode 100644 server/api/admin/backup/clear.post.ts create mode 100644 server/api/admin/backup/delete/[filename].delete.ts create mode 100644 server/api/admin/backup/download.get.ts create mode 100644 server/api/admin/backup/download/[filename].get.ts create mode 100644 server/api/admin/backup/export.post.ts create mode 100644 server/api/admin/backup/history-clear.post.ts create mode 100644 server/api/admin/backup/history.get.ts create mode 100644 server/api/admin/backup/list.get.ts create mode 100644 server/api/admin/backup/restore-chunk.post.ts create mode 100644 server/api/admin/backup/restore.post.ts create mode 100644 server/api/admin/backup/test-email.post.ts create mode 100644 server/api/admin/backup/test-s3.post.ts create mode 100644 server/api/admin/backup/test-telegram.post.ts create mode 100644 server/api/admin/backup/test-webdav.post.ts create mode 100644 server/api/admin/backup/upload.post.ts create mode 100644 server/api/admin/blacklist/[id].delete.ts create mode 100644 server/api/admin/blacklist/[id].patch.ts create mode 100644 server/api/admin/blacklist/index.get.ts create mode 100644 server/api/admin/blacklist/index.post.ts create mode 100644 server/api/admin/card-codes/[id].put.ts create mode 100644 server/api/admin/card-codes/create.post.ts create mode 100644 server/api/admin/card-codes/delete.post.ts create mode 100644 server/api/admin/card-codes/export.get.ts create mode 100644 server/api/admin/card-codes/index.get.ts create mode 100644 server/api/admin/card-codes/redeem-logs.get.ts create mode 100644 server/api/admin/card-codes/update.post.ts create mode 100644 server/api/admin/database/cleanup.post.ts create mode 100644 server/api/admin/database/performance.get.ts create mode 100644 server/api/admin/database/pool-status.get.ts create mode 100644 server/api/admin/database/reset.post.ts create mode 100644 server/api/admin/database/status.get.ts create mode 100644 server/api/admin/db-status.get.ts create mode 100644 server/api/admin/email-templates/index.delete.ts create mode 100644 server/api/admin/email-templates/index.get.ts create mode 100644 server/api/admin/email-templates/index.post.ts create mode 100644 server/api/admin/email-templates/preview.post.ts create mode 100644 server/api/admin/fix-sequence.post.ts create mode 100644 server/api/admin/grade-class/[id].delete.ts create mode 100644 server/api/admin/grade-class/by-grade.delete.ts create mode 100644 server/api/admin/grade-class/index.get.ts create mode 100644 server/api/admin/grade-class/index.post.ts create mode 100644 server/api/admin/grade-class/initialize.post.ts create mode 100644 server/api/admin/notifications/history.get.ts create mode 100644 server/api/admin/notifications/history/[batchId].delete.ts create mode 100644 server/api/admin/notifications/history/[batchId].get.ts create mode 100644 server/api/admin/notifications/history/[batchId].put.ts create mode 100644 server/api/admin/notifications/send.post.ts create mode 100644 server/api/admin/play-times/[id].ts create mode 100644 server/api/admin/play-times/index.post.ts create mode 100644 server/api/admin/play-times/index.ts create mode 100644 server/api/admin/replay-requests/index.get.ts create mode 100644 server/api/admin/replay-requests/reject.post.ts create mode 100644 server/api/admin/request-times/[id].ts create mode 100644 server/api/admin/request-times/index.post.ts create mode 100644 server/api/admin/request-times/index.ts create mode 100644 server/api/admin/schedule.post.ts create mode 100644 server/api/admin/schedule/bulk-publish.post.ts create mode 100644 server/api/admin/schedule/copy.post.ts create mode 100644 server/api/admin/schedule/draft.post.ts create mode 100644 server/api/admin/schedule/full.get.ts create mode 100644 server/api/admin/schedule/move-date.post.ts create mode 100644 server/api/admin/schedule/publish.post.ts create mode 100644 server/api/admin/schedule/remove-all-date.post.ts create mode 100644 server/api/admin/schedule/remove.post.ts create mode 100644 server/api/admin/schedule/sequence.post.ts create mode 100644 server/api/admin/schedule/song-pool.delete.ts create mode 100644 server/api/admin/schedule/song-pool.get.ts create mode 100644 server/api/admin/schedule/song-pool.post.ts create mode 100644 server/api/admin/semesters/[id].delete.ts create mode 100644 server/api/admin/semesters/[id].put.ts create mode 100644 server/api/admin/semesters/index.get.ts create mode 100644 server/api/admin/semesters/index.post.ts create mode 100644 server/api/admin/semesters/set-active.post.ts create mode 100644 server/api/admin/smtp/reload.post.ts create mode 100644 server/api/admin/smtp/test-connection.post.ts create mode 100644 server/api/admin/smtp/test-email.post.ts create mode 100644 server/api/admin/songs/delete.post.ts create mode 100644 server/api/admin/songs/duration.post.ts create mode 100644 server/api/admin/songs/mark-played.post.ts create mode 100644 server/api/admin/songs/reject.post.ts create mode 100644 server/api/admin/stats.get.ts create mode 100644 server/api/admin/stats/active-users.get.ts create mode 100644 server/api/admin/stats/realtime.get.ts create mode 100644 server/api/admin/stats/semester-comparison.get.ts create mode 100644 server/api/admin/stats/top-songs.get.ts create mode 100644 server/api/admin/stats/trends.get.ts create mode 100644 server/api/admin/stats/user-engagement.get.ts create mode 100644 server/api/admin/system-settings/clear-aggregate-bindings.post.ts create mode 100644 server/api/admin/system-settings/env-oauth-import.post.ts create mode 100644 server/api/admin/system-settings/env-oauth.get.ts create mode 100644 server/api/admin/system-settings/index.post.ts create mode 100644 server/api/admin/system-settings/index.ts create mode 100644 server/api/admin/system-settings/secretMask.ts create mode 100644 server/api/admin/users/[id].delete.ts create mode 100644 server/api/admin/users/[id].get.ts create mode 100644 server/api/admin/users/[id].put.ts create mode 100644 server/api/admin/users/[id]/approval.post.ts create mode 100644 server/api/admin/users/[id]/reset-password.post.ts create mode 100644 server/api/admin/users/[id]/songs.get.ts create mode 100644 server/api/admin/users/[id]/status-logs.get.ts create mode 100644 server/api/admin/users/[id]/status.put.ts create mode 100644 server/api/admin/users/batch-grade-update.post.ts create mode 100644 server/api/admin/users/batch-status.put.ts create mode 100644 server/api/admin/users/batch-update.post.ts create mode 100644 server/api/admin/users/batch.post.ts create mode 100644 server/api/admin/users/index.get.ts create mode 100644 server/api/admin/users/index.post.ts create mode 100644 server/api/admin/users/index.ts create mode 100644 server/api/admin/users/options.get.ts create mode 100644 server/api/admin/users/status-logs.get.ts create mode 100644 server/api/api-enhanced/netease/[...path].ts create mode 100644 server/api/auth/2fa/send-email.post.ts create mode 100644 server/api/auth/2fa/verify.post.ts create mode 100644 server/api/auth/[provider]/callback.get.ts create mode 100644 server/api/auth/[provider]/index.get.ts create mode 100644 server/api/auth/bind.post.ts create mode 100644 server/api/auth/captcha.get.ts create mode 100644 server/api/auth/change-password.post.ts create mode 100644 server/api/auth/email-code.post.ts create mode 100644 server/api/auth/forgot-password.post.ts create mode 100644 server/api/auth/grade-class-options.get.ts create mode 100644 server/api/auth/identities.get.ts create mode 100644 server/api/auth/login.post.ts create mode 100644 server/api/auth/logout.post.ts create mode 100644 server/api/auth/oauth-register-options.get.ts create mode 100644 server/api/auth/oauth-register.post.ts create mode 100644 server/api/auth/register.post.ts create mode 100644 server/api/auth/reset-password.post.ts create mode 100644 server/api/auth/set-initial-password.post.ts create mode 100644 server/api/auth/unbind.post.ts create mode 100644 server/api/auth/verify.get.ts create mode 100644 server/api/auth/webauthn/login/options.post.ts create mode 100644 server/api/auth/webauthn/login/verify.post.ts create mode 100644 server/api/auth/webauthn/register/options.get.ts create mode 100644 server/api/auth/webauthn/register/verify.post.ts create mode 100644 server/api/auth/webauthn/rename.post.ts create mode 100644 server/api/bilibili/playurl.get.ts create mode 100644 server/api/bilibili/search.get.ts create mode 100644 server/api/blacklist/check.post.ts create mode 100644 server/api/card-codes/validate.post.ts create mode 100644 server/api/meow/bind.post.ts create mode 100644 server/api/meow/unbind.post.ts create mode 100644 server/api/music/resolve-url.post.ts create mode 100644 server/api/music/state.post.ts create mode 100644 server/api/music/websocket.ts create mode 100644 server/api/native-api/comment/tx.get.ts create mode 100644 server/api/native-api/lyric/mg.get.ts create mode 100644 server/api/native-api/lyric/tx.get.ts create mode 100644 server/api/native-api/migu/playurl.get.ts create mode 100644 server/api/native-api/qq/avatar.get.ts create mode 100644 server/api/native-api/qq/check-cookie.post.ts create mode 100644 server/api/native-api/qq/check-login.post.ts create mode 100644 server/api/native-api/qq/check-wx-login.post.ts create mode 100644 server/api/native-api/qq/login-qr-wx.get.ts create mode 100644 server/api/native-api/qq/login-qr.get.ts create mode 100644 server/api/native-api/qq/playlist-songs.post.ts create mode 100644 server/api/native-api/qq/playlists.post.ts create mode 100644 server/api/native-api/search/mg.get.ts create mode 100644 server/api/native-api/search/tx.get.ts create mode 100644 server/api/native-api/search/wy.get.ts create mode 100644 server/api/notifications/[id].delete.ts create mode 100644 server/api/notifications/[id]/read.post.ts create mode 100644 server/api/notifications/clear-all.delete.ts create mode 100644 server/api/notifications/important.get.ts create mode 100644 server/api/notifications/index.ts create mode 100644 server/api/notifications/meow/send-verification.post.ts create mode 100644 server/api/notifications/meow/test.post.ts create mode 100644 server/api/notifications/read-all.post.ts create mode 100644 server/api/notifications/settings.post.ts create mode 100644 server/api/notifications/settings.ts create mode 100644 server/api/open/backup/auto.post.ts create mode 100644 server/api/open/backup/status/[id].get.ts create mode 100644 server/api/open/card-codes.delete.ts create mode 100644 server/api/open/card-codes.get.ts create mode 100644 server/api/open/card-codes.patch.ts create mode 100644 server/api/open/card-codes.post.ts create mode 100644 server/api/open/card-codes/delete.post.ts create mode 100644 server/api/open/schedules.get.ts create mode 100644 server/api/open/songs.get.ts create mode 100644 server/api/open/songs/mark-played.post.ts create mode 100644 server/api/open/songs/request.post.ts create mode 100644 server/api/platform-config/index.get.ts create mode 100644 server/api/play-times/index.ts create mode 100644 server/api/progress/events.ts create mode 100644 server/api/progress/id.ts create mode 100644 server/api/proxy/image.get.ts create mode 100644 server/api/request-times/index.ts create mode 100644 server/api/semesters/current.get.ts create mode 100644 server/api/semesters/options.get.ts create mode 100644 server/api/site-config.get.ts create mode 100644 server/api/songs/[id]/update.put.ts create mode 100644 server/api/songs/[id]/voters.get.ts create mode 100644 server/api/songs/add.post.ts create mode 100644 server/api/songs/check-restriction.post.ts create mode 100644 server/api/songs/collaborators/reply.post.ts create mode 100644 server/api/songs/count.get.ts create mode 100644 server/api/songs/import.post.ts create mode 100644 server/api/songs/index.get.ts create mode 100644 server/api/songs/public.get.ts create mode 100644 server/api/songs/replay.delete.ts create mode 100644 server/api/songs/replay.post.ts create mode 100644 server/api/songs/request.post.ts create mode 100644 server/api/songs/submission-status.get.ts create mode 100644 server/api/songs/vote.post.ts create mode 100644 server/api/songs/withdraw.post.ts create mode 100644 server/api/sys/time.get.ts create mode 100644 server/api/system/instance.get.ts create mode 100644 server/api/system/location.get.ts create mode 100644 server/api/system/reconnect.post.ts create mode 100644 server/api/system/status.get.ts create mode 100644 server/api/user/2fa/disable.post.ts create mode 100644 server/api/user/2fa/enable.post.ts create mode 100644 server/api/user/2fa/generate.post.ts create mode 100644 server/api/user/api-keys/[id].delete.ts create mode 100644 server/api/user/api-keys/[id]/logs.get.ts create mode 100644 server/api/user/api-keys/index.get.ts create mode 100644 server/api/user/api-keys/index.post.ts create mode 100644 server/api/user/avatar.post.ts create mode 100644 server/api/user/email/bind.post.ts create mode 100644 server/api/user/email/resend-verification.post.ts create mode 100644 server/api/user/email/send-code.post.ts create mode 100644 server/api/user/email/unbind.post.ts create mode 100644 server/api/user/email/verify-code.post.ts create mode 100644 server/api/user/sessions.delete.ts create mode 100644 server/api/user/sessions.get.ts create mode 100644 server/api/user/year-review.get.ts create mode 100644 server/api/users/search.get.ts create mode 100644 server/api/users/social-accounts.get.ts create mode 100644 server/api/users/social-accounts/meow.delete.ts create mode 100644 server/api/users/social-accounts/meow.post.ts create mode 100644 server/card-codes/statuses.ts create mode 100644 server/config/constants.ts create mode 100644 server/error.ts create mode 100644 server/middleware/00.request-id.ts create mode 100644 server/middleware/api-auth.ts create mode 100644 server/middleware/api-cors.ts create mode 100644 server/middleware/auth.ts create mode 100644 server/plugins/00.sentry.ts create mode 100644 server/plugins/01.pre-warm-ssr.ts create mode 100644 server/plugins/error-handler.ts create mode 100644 server/plugins/redis-lifecycle.ts create mode 100644 server/plugins/statistics-code.ts create mode 100644 server/services/apiLogService.ts create mode 100644 server/services/autoBackupService.ts create mode 100644 server/services/cardCodeDeleteService.ts create mode 100644 server/services/cardCodeLifecycleService.ts create mode 100644 server/services/durationValidationService.ts create mode 100644 server/services/meowNotificationService.ts create mode 100644 server/services/notificationService.ts create mode 100644 server/services/oauthConfigService.ts create mode 100644 server/services/passwordSecurityService.ts create mode 100644 server/services/securityService.ts create mode 100644 server/services/smtpService.ts create mode 100644 server/services/songRequestService.ts create mode 100644 server/services/userService.ts create mode 100644 server/tsconfig.json create mode 100644 server/utils/admin-password-policy.ts create mode 100644 server/utils/apiError.ts create mode 100644 server/utils/apiKeyUtils.ts create mode 100644 server/utils/auth-route-policy.ts create mode 100644 server/utils/auth-session.ts create mode 100644 server/utils/auth.ts create mode 100644 server/utils/bilibiliWbi.ts create mode 100644 server/utils/captcha.ts create mode 100644 server/utils/captchaStore.ts create mode 100644 server/utils/card-code-delete-handler.ts create mode 100644 server/utils/database-health.ts create mode 100644 server/utils/database-manager.ts create mode 100644 server/utils/email-verification.ts create mode 100644 server/utils/geo.ts create mode 100644 server/utils/grade-class-core.ts create mode 100644 server/utils/grade-class-options.ts create mode 100644 server/utils/important-notification-policy.ts create mode 100644 server/utils/initial-password-policy.ts create mode 100644 server/utils/instance-id.ts create mode 100644 server/utils/ip-utils.ts create mode 100644 server/utils/jwt-enhanced.ts create mode 100644 server/utils/log-manager.ts create mode 100644 server/utils/native_common.ts create mode 100644 server/utils/native_tx.ts create mode 100644 server/utils/native_wy.ts create mode 100644 server/utils/notification-history-policy.ts create mode 100644 server/utils/oauth-identity.ts create mode 100644 server/utils/oauth-providers.ts create mode 100644 server/utils/oauth-strategies.ts create mode 100644 server/utils/oauth-token.ts create mode 100644 server/utils/oauth.ts create mode 100644 server/utils/permissions.js create mode 100644 server/utils/qqComment.ts create mode 100644 server/utils/qq_music_sdk.ts create mode 100644 server/utils/rateLimiter.ts create mode 100644 server/utils/redis.ts create mode 100644 server/utils/register-validation.ts create mode 100644 server/utils/registration-notify.ts create mode 100644 server/utils/request-utils.ts create mode 100644 server/utils/requireSongAdmin.ts create mode 100644 server/utils/restoreScheduleSongPool.ts create mode 100644 server/utils/s3Client.ts create mode 100644 server/utils/scheduleReplayBinding.ts create mode 100644 server/utils/scheduleSongPool.ts create mode 100644 server/utils/sequence-sync.ts create mode 100644 server/utils/serverTime.ts create mode 100644 server/utils/siteUtils.ts create mode 100644 server/utils/song-duration-policy.ts create mode 100644 server/utils/song-name-normalize.ts create mode 100644 server/utils/song-type-resolver.ts create mode 100644 server/utils/songDurationFetcher.ts create mode 100644 server/utils/studentMask.ts create mode 100644 server/utils/submission-restriction-policy.ts create mode 100644 server/utils/submissionLimit.ts create mode 100644 server/utils/system-settings-defaults.ts create mode 100644 server/utils/system-settings-helper.ts create mode 100644 server/utils/telemetry.ts create mode 100644 server/utils/theme-config.ts create mode 100644 server/utils/user-archive.ts create mode 100644 server/utils/user-avatar.ts create mode 100644 server/utils/user.ts create mode 100644 server/utils/webauthn-config.ts create mode 100644 server/utils/webauthn-token.ts create mode 100644 sh/main.sh create mode 100644 sh/next/deploy.sh create mode 100644 sh/next/main.sh create mode 100644 sh/next/update.sh create mode 100644 temp_update/1.5.9.9.zip create mode 100644 tests/server/auth-route-policy.test.ts create mode 100644 tests/server/clear-aggregate-bindings-policy.test.ts create mode 100644 tests/server/cors-origin-policy.test.ts create mode 100644 tests/server/grade-class-input.test.ts create mode 100644 tests/server/grade-class-options.test.ts create mode 100644 tests/server/important-notification-policy.test.ts create mode 100644 tests/server/initial-password-policy.test.ts create mode 100644 tests/server/notification-history-policy.test.ts create mode 100644 tests/server/oauth-state-cookie.test.ts create mode 100644 tests/server/password-policy.test.ts create mode 100644 tests/server/qq-comment-normalize.test.ts create mode 100644 tests/server/register-validation.test.ts create mode 100644 tests/server/song-duration-policy.test.ts create mode 100644 tests/server/song-name-normalize.test.ts create mode 100644 tests/server/song-type-blacklist.test.ts create mode 100644 tests/server/submission-restriction-policy.test.ts create mode 100644 tests/server/token-version-policy.test.ts create mode 100644 tests/server/user-archive.test.ts create mode 100644 tests/server/user-avatar.test.ts create mode 100644 tsconfig.json create mode 100644 types/global.d.ts create mode 100644 types/index.ts create mode 100644 vercel.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f317af0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +node_modules +.nuxt +.output +.git +.gitignore +.env +.env.local +.env.production +.env.development +dist +coverage +.vscode +.idea +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fafc089 --- /dev/null +++ b/.env.example @@ -0,0 +1,93 @@ +# 数据库连接地址(必填) +# 格式:postgresql://用户名:密码@主机:端口/数据库名?sslmode=require +# 示例:postgresql://myuser:mypassword@localhost:5432/voicehub?sslmode=require +DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require" + +# JWT 认证密钥(必填) +# 请使用强随机字符串,建议至少 32 位 +JWT_SECRET="your-very-secure-jwt-secret-key" + +# 受信的 CDN/反向代理真实 IP 头(可选) +# 多个头用逗号分隔;未配置时只使用 TCP 连接地址,避免客户端伪造转发头。 +# 示例:EdgeOne 使用 eo-connecting-ip,Cloudflare 使用 cf-connecting-ip +# TRUSTED_CLIENT_IP_HEADERS=eo-connecting-ip + +# WebAuthn / Passkey 配置(可选) +# 依赖方 ID (RP ID),必须是你域名的一个有效域名 +# 规则:必须是请求域名的精确匹配或父域名 +# +# 示例: +# - 请求域名为 voicehub.example.com,RP ID 可设为 voicehub.example.com 或 example.com +# - 不配置时,默认使用当前请求的 hostname(适合开发/预览环境) +# - 生产环境建议配置为你的主域名,如:voicehub.example.com +WEBAUTHN_RP_ID= + +# 允许的 Origin 列表(可选) +# WebAuthn 要求精确匹配 Origin,此配置用于支持多域名部署 +# 多个域名用逗号分隔,必须包含 https:// 前缀 +# +# 示例: +# - 单域名:https://voicehub.example.com +# - 多域名(Vercel 预览 + 生产):https://voicehub.example.com,https://voicehub-xxx.vercel.app +# - 二级域名:https://voicehub.example.com,https://sub.voicehub.example.com +# +# 不配置时:默认信任请求的 Origin(适合开发环境) +# 生产环境:强烈建议配置,以防止恶意站点伪造 Origin 攻击 +WEBAUTHN_ORIGIN= + +# 应用运行环境(可选) +# development: 开发环境 +# production: 生产环境 +NODE_ENV=development + +# Sentry 配置(可选) +# 是否启用错误追踪与遥测请在管理员后台的站点配置中控制。 +# 默认仅在后台开启后发送错误事件和实例在线事件;性能追踪默认 10%,会话回放默认关闭。 +# 上报的硬性前提:NODE_ENV=production 且非平台预览部署(Vercel preview / Netlify deploy-preview 与 branch-deploy 自动关闭)。 +# 如需强制覆盖(例如自托管测试环境或临时调试),可设置 SENTRY_ENABLED=true/false,设置后以该值为准。 +# 如需覆盖采样率,可取消注释并设置为 [0,1] 范围内的数字。 +# SENTRY_ENABLED=false +# SENTRY_TRACES_SAMPLE_RATE=0.1 +# NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0.1 +# NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0 +# NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0 + +# Redis 分布式短期状态服务(可选) +# 单实例可不配置;多实例/Serverless 建议配置,用于验证码、限流和临时锁定。 +# 格式:redis://用户名:密码@主机:端口 +# REDIS_URL="redis://default:password@host:port" +# Redis 键命名空间,多个 VoiceHub 环境共用 Redis 时应分别设置 +# REDIS_KEY_PREFIX="voicehub:v2:" + +# OAuth 配置(可选) +# 启用相应的 OAuth 提供商需要填写对应的 Client ID 和 Client Secret +# OAuth 运行时配置已经迁移到管理员后台;以下变量仅用于兼容旧部署和后台一键导入。 +# 导入完成后,系统以数据库中保存的配置为准,不会在每次启动时直接读取这些变量。 + +# OAuth 回调地址(必填,如果启用 OAuth) +# 注意:[provider] 是占位符,实际使用时需要根据具体情况填写 +OAUTH_REDIRECT_URI="http://localhost:3000/api/auth/[provider]/callback" + +# 用于加密 OAuth state 的密钥(必填,如果启用 OAuth) +OAUTH_STATE_SECRET=your-secure-random-string-for-oauth-state + +# GitHub OAuth +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Casdoor OAuth +CASDOOR_CLIENT_ID= +CASDOOR_CLIENT_SECRET= +CASDOOR_ENDPOINT= + +# Google OAuth +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# 聚合登陆 OAuth +AGGREGATE_OAUTH_APP_ID= +AGGREGATE_OAUTH_APP_KEY= +# 支持单值、逗号分隔或 JSON 数组,例如:qq,wx,sina,baidu +# 可用值:qq,wx,alipay,sina,baidu,douyin,huawei,xiaomi,gitee,gitea,bilibili,kuaishou +AGGREGATE_OAUTH_LOGIN_TYPE=qq +AGGREGATE_OAUTH_ENDPOINT=https://example.com/connect.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2c3427e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,133 @@ +name: 🐛 问题反馈 +description: 报告VoiceHub系统中的问题或错误 +title: '[Bug]: ' +labels: ['bug', '需要确认'] +assignees: [] +body: + - type: markdown + attributes: + value: | + 感谢您报告问题!请详细描述遇到的问题,这将帮助我们更快地定位和解决问题。 + + - type: dropdown + id: bug-type + attributes: + label: 问题类型 + description: 请选择最符合的问题类型 + options: + - 音乐播放相关 + - 用户登录/注册 + - 点歌功能 + - 通知系统 + - 管理员功能 + - 界面显示 + - 性能问题 + - 其他 + validations: + required: true + + - type: textarea + id: bug-description + attributes: + label: 问题描述 + description: 请详细描述遇到的问题 + placeholder: 请清楚地描述问题的具体表现... + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: 复现步骤 + description: 请提供详细的复现步骤 + placeholder: | + 1. 进入... + 2. 点击... + 3. 看到错误... + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: 期望行为 + description: 描述您期望发生的正确行为 + placeholder: 应该显示/执行... + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: 实际行为 + description: 描述实际发生的错误行为 + placeholder: 实际显示/执行了... + validations: + required: true + + - type: dropdown + id: browser + attributes: + label: 浏览器 + description: 使用的浏览器类型 + options: + - Chrome + - Firefox + - Safari + - Edge + - 其他 + validations: + required: false + + - type: input + id: browser-version + attributes: + label: 浏览器版本 + description: 浏览器的具体版本号 + placeholder: 例如:Chrome 120.0.0.0 + validations: + required: false + + - type: dropdown + id: device-type + attributes: + label: 设备类型 + description: 使用的设备类型 + options: + - 桌面电脑 + - 笔记本电脑 + - 平板电脑 + - 手机 + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: 附加信息 + description: 任何其他有助于理解问题的信息 + placeholder: 截图、错误日志、相关配置等... + validations: + required: false + + - type: input + id: instance-id + attributes: + label: 实例 ID(可选) + description: 如果方便,可以提供系统实例 ID(可在管理员后台「系统状态」或浏览器控制台中查看),这能帮助开发者关联到具体的部署环境 + placeholder: 例如:a807234c-cc23-45ea-ab3b-dcb604e1b7d9 + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: 确认事项 + description: 请确认以下事项 + options: + - label: 我已经搜索了现有的Issues,确认这不是重复问题 + required: true + - label: 我已经尝试刷新页面或重新登录 + required: false + - label: 问题可以稳定复现 + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..7309184 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: + - name: 📚 使用文档 + url: https://docs.voicehub.lao-shui.top + about: 查看VoiceHub的使用文档和常见问题解答 + - name: 💬 讨论区 + url: https://github.com/laoshuikaixue/VoiceHub/discussions + about: 在讨论区提问、分享经验或参与社区讨论 + - name: 🔒 安全问题私密报告 + url: mailto:contact@lao-shui.top + about: 对于严重的安全漏洞,请通过邮件私密报告 + - name: 📧 联系我们 + url: mailto:contact@lao-shui.top + about: 如果您需要技术支持或有其他问题,可以直接联系我们 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..e4e277b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,139 @@ +name: ✨ 功能建议 +description: 为VoiceHub系统提出新功能或改进建议 +title: '[Feature]: ' +labels: ['enhancement', '功能建议'] +assignees: [] +body: + - type: markdown + attributes: + value: | + 感谢您为VoiceHub提出功能建议!您的想法将帮助我们改进系统,为用户提供更好的体验。 + + - type: dropdown + id: feature-category + attributes: + label: 功能类别 + description: 请选择建议功能所属的类别 + options: + - 音乐播放功能 + - 点歌系统 + - 用户管理 + - 通知系统 + - 管理员工具 + - 用户界面 + - 数据统计 + - 安全功能 + - API接口 + - 其他 + validations: + required: true + + - type: textarea + id: feature-summary + attributes: + label: 功能概述 + description: 简要描述您建议的功能 + placeholder: 用一两句话概括您的功能建议... + validations: + required: true + + - type: textarea + id: problem-statement + attributes: + label: 问题描述 + description: 描述当前存在的问题或不便之处 + placeholder: 目前在使用VoiceHub时遇到了什么问题或不便? + validations: + required: false + + - type: textarea + id: proposed-solution + attributes: + label: 建议方案 + description: 详细描述您建议的解决方案 + placeholder: | + 请详细描述您的功能建议: + - 功能如何工作 + - 用户如何使用 + - 预期效果 + validations: + required: false + + - type: textarea + id: user-stories + attributes: + label: 用户场景 + description: 描述用户在什么情况下会使用这个功能 + placeholder: | + 作为一个[用户角色],我希望[功能描述],这样我就可以[预期收益]。 + + 例如: + - 作为学生,我希望能够预约点歌时间,这样我就可以在特定时间播放我的歌曲 + - 作为管理员,我希望能够批量管理歌曲,这样我就可以更高效地维护歌曲库 + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: 替代方案 + description: 是否考虑过其他解决方案? + placeholder: 描述您考虑过的其他可能的解决方案... + validations: + required: false + + - type: dropdown + id: priority + attributes: + label: 优先级 + description: 您认为这个功能的重要程度如何? + options: + - 高 - 非常重要,急需实现 + - 中 - 重要,希望尽快实现 + - 低 - 有了更好,不急 + validations: + required: true + + - type: dropdown + id: user-type + attributes: + label: 目标用户 + description: 这个功能主要面向哪类用户? + options: + - 所有用户 + - 普通学生用户 + - 管理员 + - 特定用户群体 + validations: + required: true + + - type: textarea + id: technical-considerations + attributes: + label: 技术考虑 + description: 如果您有技术背景,可以提供一些技术实现的想法 + placeholder: 实现思路、技术栈建议、性能考虑等... + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: 附加信息 + description: 任何其他有助于理解功能需求的信息 + placeholder: 参考资料、类似功能的例子、设计草图等... + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: 确认事项 + description: 请确认以下事项 + options: + - label: 我已经搜索了现有的Issues,确认这不是重复建议 + required: true + - label: 我愿意参与功能的讨论和测试 + required: false + - label: 如果有能力,我愿意协助开发这个功能 + required: false diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..cf9ed22 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,112 @@ +name: '❓ 提问 / Question' +description: '遇到问题需要帮助?在这里提问,我们会尽快为您解答!' +title: '[Question]: ' +labels: ['question', '需要帮助'] +assignees: [] +body: + - type: markdown + attributes: + value: | + 感谢您使用 VoiceHub 校园广播点歌系统!如果您在使用过程中遇到任何问题,请填写以下信息,我们会尽快为您提供帮助。 + + 💡 **提示**:提问前建议先查看已有的 Issues。 + + - type: dropdown + id: question-type + attributes: + label: '问题类型' + description: '请选择您遇到的问题类型' + options: + - '使用问题 - 不知道如何操作某个功能' + - '技术问题 - 功能无法正常工作' + - '配置问题 - 系统设置相关' + - '账户问题 - 登录、权限等' + - '音频问题 - 播放、上传相关' + - '界面问题 - 显示异常或操作困难' + - '其他问题' + validations: + required: true + + - type: textarea + id: question-description + attributes: + label: '问题描述' + description: '请详细描述您遇到的问题,包括具体的操作步骤和现象' + placeholder: | + 例如: + 1. 我在尝试点歌时... + 2. 点击"添加到播放列表"按钮后... + 3. 期望的结果是...,但实际发生的是... + validations: + required: true + + - type: dropdown + id: user-role + attributes: + label: '用户角色' + description: '您在系统中的角色是什么?' + options: + - '学生用户' + - '教师用户' + - '管理员' + - '超级管理员' + - '访客' + validations: + required: true + + - type: input + id: environment + attributes: + label: '使用环境' + description: '请提供您的使用环境信息' + placeholder: '例如:Chrome 120, Windows 11, 移动端Safari等' + validations: + required: false + + - type: textarea + id: attempted-solutions + attributes: + label: '已尝试的解决方法' + description: '请描述您已经尝试过的解决方法(如果有的话)' + placeholder: | + 例如: + - 刷新了页面 + - 清除了浏览器缓存 + - 尝试了不同的浏览器 + - 重新登录了账户 + validations: + required: false + + - type: dropdown + id: urgency + attributes: + label: '紧急程度' + description: '这个问题对您的使用影响有多大?' + options: + - '低 - 不影响正常使用,有时间时解决即可' + - '中 - 影响部分功能,希望尽快解决' + - '高 - 严重影响使用,需要紧急处理' + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: '补充信息' + description: '任何其他可能有帮助的信息,如截图、错误信息等' + placeholder: '您可以在这里添加截图、错误截图或其他相关信息' + validations: + required: false + + - type: checkboxes + id: confirmation + attributes: + label: '确认事项' + description: '请确认以下事项' + options: + - label: '我已经搜索了现有的 Issues,确认这不是重复问题' + required: true + # - label: "我已经查看了项目文档和常见问题" + # required: false + - label: '我愿意提供更多信息来帮助解决这个问题' + required: false diff --git a/.github/ISSUE_TEMPLATE/security_report.yml b/.github/ISSUE_TEMPLATE/security_report.yml new file mode 100644 index 0000000..266b9f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_report.yml @@ -0,0 +1,149 @@ +name: 🔒 安全问题报告 +description: 报告VoiceHub系统中的安全漏洞或安全相关问题 +title: '[Security]: ' +labels: ['security', '高优先级'] +assignees: [] +body: + - type: markdown + attributes: + value: | + ⚠️ **重要提醒**:如果您发现了严重的安全漏洞,请不要在公开的Issue中详细描述。 + 请发送邮件至 `contact@lao-shui.top` 。 + + 感谢您帮助我们保护VoiceHub系统的安全! + + - type: dropdown + id: security-type + attributes: + label: 安全问题类型 + description: 请选择最符合的安全问题类型 + options: + - 身份认证问题 + - 权限控制问题 + - 数据泄露风险 + - 跨站脚本攻击(XSS) + - SQL注入 + - 跨站请求伪造(CSRF) + - 文件上传安全 + - 会话管理问题 + - 密码安全 + - API安全 + - 其他安全问题 + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: 严重程度 + description: 请评估安全问题的严重程度 + options: + - 严重 - 可能导致系统完全被攻破 + - 高 - 可能导致重要数据泄露 + - 中 - 可能影响部分用户或功能 + - 低 - 轻微的安全改进建议 + validations: + required: true + + - type: textarea + id: security-summary + attributes: + label: 安全问题概述 + description: 简要描述发现的安全问题(避免详细的攻击步骤) + placeholder: 简要说明发现的安全问题类型和影响范围... + validations: + required: true + + - type: textarea + id: affected-components + attributes: + label: 受影响的组件 + description: 列出可能受到影响的系统组件或功能模块 + placeholder: | + 例如: + - 用户登录模块 + - 文件上传功能 + - 管理员面板 + - API接口 + validations: + required: true + + - type: textarea + id: potential-impact + attributes: + label: 潜在影响 + description: 描述这个安全问题可能造成的影响 + placeholder: | + 可能的影响包括: + - 用户数据泄露 + - 未授权访问 + - 系统功能被滥用 + - 其他安全风险 + validations: + required: true + + - type: textarea + id: reproduction-info + attributes: + label: 复现信息 + description: 提供必要的复现信息(请避免详细的攻击步骤) + placeholder: | + 请提供: + - 发现问题的环境 + - 触发条件 + - 观察到的异常行为 + + 注意:请不要提供完整的攻击代码或详细攻击步骤 + validations: + required: false + + - type: textarea + id: mitigation-suggestions + attributes: + label: 缓解建议 + description: 如果您有安全方面的建议,请提供 + placeholder: | + 可能的解决方案: + - 输入验证改进 + - 权限检查加强 + - 安全配置调整 + - 其他安全措施 + validations: + required: false + + - type: dropdown + id: disclosure-timeline + attributes: + label: 披露时间线 + description: 您希望的问题处理时间线 + options: + - 立即处理 - 严重安全问题 + - 1周内 - 重要安全问题 + - 2周内 - 一般安全问题 + - 1个月内 - 安全改进建议 + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: 附加信息 + description: 其他有助于理解和解决安全问题的信息 + placeholder: 相关文档、参考资料、类似案例等... + validations: + required: false + + - type: checkboxes + id: security-terms + attributes: + label: 安全报告确认 + description: 请确认以下事项 + options: + - label: 我理解这是一个安全相关的问题报告 + required: true + - label: 我已经避免在公开Issue中透露详细的攻击方法 + required: true + - label: 我愿意配合安全团队进行问题验证和修复 + required: true + - label: 我同意遵循负责任的安全披露原则 + required: true diff --git a/.github/workflows/build-fpk.yml b/.github/workflows/build-fpk.yml new file mode 100644 index 0000000..5f481ea --- /dev/null +++ b/.github/workflows/build-fpk.yml @@ -0,0 +1,125 @@ +# .github/workflows/build-fpk.yml +# VoiceHub 飞牛 fnOS FPK 安装包自动构建 +# 手动触发,版本号自动从 package.json 读取 + +name: Build VoiceHub FPK + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-fpk: + if: github.repository == 'laoshuikaixue/VoiceHub' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: 读取版本号 & 安装 fnpack + run: | + # 读取原始版本号 + ORIGINAL_VER=$(node -p "require('./package.json').version") + echo "📦 原始版本: $ORIGINAL_VER" + + # 转换为三段式版本号 (x.y.zw 格式,将第四段拼接到第三段) + FNOS_VER=$(echo "$ORIGINAL_VER" | awk -F. '{ + if (NF >= 4) { + # 四段式版本号,拼接最后两段 + printf "%s.%s.%s%s", $1, $2, $3, $4 + } else if (NF == 3) { + # 已经是三段式,直接使用 + printf "%s.%s.%s", $1, $2, $3 + } else { + # 其他情况,保持原样 + print $0 + } + }') + + echo "APP_VER=$FNOS_VER" >> "$GITHUB_ENV" + echo "📦 转换后版本: $FNOS_VER" + + wget -q https://static2.fnnas.com/fnpack/fnpack-1.2.1-linux-amd64 -O /usr/local/bin/fnpack + chmod +x /usr/local/bin/fnpack + + - name: 创建 FPK 目录 + run: | + fnpack create voicehub --template docker + echo "📁 模板结构:" + find voicehub -type f | sort + + - name: 写入 manifest & 准备图标 + run: | + # 复制仓库中的 fnos 模板 + cp -r fnos/* voicehub/ + + # 从 public 目录复制图标到构建目录 + mkdir -p voicehub/app/ui/images + cp public/assets/logo.png voicehub/app/ui/images/icon-64.png + cp public/assets/logo.png voicehub/app/ui/images/icon-256.png + cp public/assets/logo.png voicehub/ICON.PNG + cp public/assets/logo.png voicehub/ICON_256.PNG + + # 更新版本号 + sed -i "s/^version=.*/version=${APP_VER}/" voicehub/manifest + chmod +x voicehub/cmd/main + echo "✅ manifest, cmd/main & icons 已就绪" + + - name: 从仓库读取 docker-compose-prebuild-nju.yml 并写入 + run: | + # 从项目根目录读取仓库内的 compose 文件 + SRC_COMPOSE="docker-compose/docker-compose-prebuild-nju.yml" + # 目标路径 + DST_COMPOSE="voicehub/app/docker/docker-compose.yaml" + + # 创建目录 + mkdir -p voicehub/app/docker + + # 复制内容并进行修改: + # 1. 移除 postgres 服务的端口映射,以避免 fnOS 5432 端口冲突 + # 2. 移除 voicehub 服务的代码挂载 (.:/app),避免覆盖镜像内的预构建代码导致 MODULE_NOT_FOUND + # 3. 确保容器间通过 service name 互联 + # 直接使用 awk 工具更精确地过滤掉整个 volumes 块和 ports 块中我们不想要的行,或者删除空块 + awk ' + /^[[:space:]]+volumes:/ { in_volumes=1; print; next } + /^[[:space:]]+ports:/ { in_ports=1; print; next } + /^[[:space:]]+restart:/ { in_volumes=0; in_ports=0; print; next } + /^[[:space:]]+[a-zA-Z0-9_]+:/ { in_volumes=0; in_ports=0; print; next } + + in_volumes && /^[[:space:]]+-[[:space:]]+.:\/app/ { next } + in_volumes && /^[[:space:]]+-[[:space:]]+\/app\/node_modules/ { next } + in_ports && /^[[:space:]]+-[[:space:]]+.5432:5432./ { next } + + { print } + ' "$SRC_COMPOSE" > "$DST_COMPOSE.tmp" + + # 清理由于上述删除可能产生的空的 volumes: 或 ports: 标签 + # 使用 grep 检查下一行是否是配置项或空行 + sed -E '/^[[:space:]]+volumes:[[:space:]]*$/{N;/\n[[:space:]]+-/!d}' "$DST_COMPOSE.tmp" > "$DST_COMPOSE.tmp2" + sed -E '/^[[:space:]]+ports:[[:space:]]*$/{N;/\n[[:space:]]+-/!d}' "$DST_COMPOSE.tmp2" > "$DST_COMPOSE" + rm "$DST_COMPOSE.tmp" "$DST_COMPOSE.tmp2" + + echo "✅ 已处理 compose 文件:移除数据库端口暴露 & 移除应用代码挂载" + cat "$DST_COMPOSE" + + - name: 最终目录 & 打包 + run: | + echo "📦 FPK 结构:" + find voicehub -type f | sort + echo "---" + cd voicehub + fnpack build + ls -lh *.fpk 2>/dev/null || { echo "❌ 打包失败"; exit 1; } + mv *.fpk ../ + + - name: 上传 Artifact + uses: actions/upload-artifact@v7 + with: + name: voicehub-fpk-${{ env.APP_VER }} + path: "*.fpk" + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..01aadd0 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,222 @@ +name: Build and Push Docker Image + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + reason: + description: 'Reason for manual trigger' + required: false + default: 'Manual trigger' + +env: + REGISTRY: ghcr.io + +jobs: + # 阶段1:优先构建并推送 amd64 架构,立即可用 + build-amd64: + if: github.repository == 'laoshuikaixue/VoiceHub' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: '0' + + - name: Prepare lowercase names + id: names + run: | + echo "image-name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + echo "actor-name=$(echo '${{ github.actor }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Extract version from package.json + id: extract-version + run: | + FULL_VERSION=$(node -p "require('./package.json').version") + MAJOR_MINOR_VERSION=$(echo "$FULL_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.[0-9]+$/\1/') + echo "full-version=$FULL_VERSION" >> $GITHUB_OUTPUT + echo "major-minor-version=$MAJOR_MINOR_VERSION" >> $GITHUB_OUTPUT + echo "Extracted full version: $FULL_VERSION" + echo "Extracted major.minor version: $MAJOR_MINOR_VERSION" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ steps.names.outputs.actor-name }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ steps.names.outputs.image-name }} + tags: | + # 正式标签 - amd64 构建完成后立即可用 + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + type=raw,value=${{ steps.extract-version.outputs.full-version }} + type=raw,value=${{ steps.extract-version.outputs.major-minor-version }} + # 架构后缀标签 - 用于后续多架构合并 + type=raw,value=latest-amd64,enable={{is_default_branch}} + type=raw,value=${{ steps.extract-version.outputs.full-version }}-amd64 + type=raw,value=${{ steps.extract-version.outputs.major-minor-version }}-amd64 + + - name: Build and push amd64 image + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + cache-from: type=gha,scope=amd64 + cache-to: type=gha,mode=max,scope=amd64 + + # 阶段2:并行构建其余架构,各自推送带后缀的独立标签 + build-other-arches: + if: github.repository == 'laoshuikaixue/VoiceHub' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + arch: + - { platform: linux/arm64, suffix: -arm64 } + - { platform: linux/arm/v7, suffix: -armv7 } + - { platform: linux/arm/v6, suffix: -armv6 } + - { platform: linux/ppc64le, suffix: -ppc64le } + - { platform: linux/s390x, suffix: -s390x } + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: '0' + + - name: Prepare lowercase names + id: names + run: | + echo "image-name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + echo "actor-name=$(echo '${{ github.actor }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Extract version from package.json + id: extract-version + run: | + FULL_VERSION=$(node -p "require('./package.json').version") + MAJOR_MINOR_VERSION=$(echo "$FULL_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.[0-9]+$/\1/') + echo "full-version=$FULL_VERSION" >> $GITHUB_OUTPUT + echo "major-minor-version=$MAJOR_MINOR_VERSION" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ steps.names.outputs.actor-name }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ steps.names.outputs.image-name }} + tags: | + type=raw,value=latest${{ matrix.arch.suffix }},enable={{is_default_branch}} + type=raw,value=${{ steps.extract-version.outputs.full-version }}${{ matrix.arch.suffix }} + type=raw,value=${{ steps.extract-version.outputs.major-minor-version }}${{ matrix.arch.suffix }} + + - name: Build and push ${{ matrix.arch.platform }} image + uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ matrix.arch.platform }} + cache-from: type=gha,scope=${{ matrix.arch.suffix }} + cache-to: type=gha,mode=max,scope=${{ matrix.arch.suffix }} + + # 阶段3:所有架构构建完成后,合并为多架构 manifest 清单并推送正式标签 + merge-manifests: + if: (github.repository == 'laoshuikaixue/VoiceHub' || github.event_name == 'workflow_dispatch') && github.event_name != 'pull_request' + runs-on: ubuntu-latest + needs: [build-amd64, build-other-arches] + permissions: + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Prepare lowercase names + id: names + run: | + echo "image-name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + echo "actor-name=$(echo '${{ github.actor }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Extract version from package.json + id: extract-version + run: | + FULL_VERSION=$(node -p "require('./package.json').version") + MAJOR_MINOR_VERSION=$(echo "$FULL_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.[0-9]+$/\1/') + echo "full-version=$FULL_VERSION" >> $GITHUB_OUTPUT + echo "major-minor-version=$MAJOR_MINOR_VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ steps.names.outputs.actor-name }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push multi-architecture manifest lists + env: + IMAGE_BASE: ${{ env.REGISTRY }}/${{ steps.names.outputs.image-name }} + FULL_VER: ${{ steps.extract-version.outputs.full-version }} + MAJOR_MINOR_VER: ${{ steps.extract-version.outputs.major-minor-version }} + run: | + # 所有架构后缀列表 + ARCH_SUFFIXES=("-amd64" "-arm64" "-armv7" "-armv6" "-ppc64le" "-s390x") + + # 生成指定标签的所有架构镜像参数 + build_arch_args() { + local tag=$1 + local args="" + for suffix in "${ARCH_SUFFIXES[@]}"; do + args="$args ${IMAGE_BASE}:${tag}${suffix}" + done + echo "$args" + } + + # 合并 latest 标签(仅默认分支) + if [ "${{ github.ref == 'refs/heads/main' }}" = "true" ]; then + echo "Merging multi-arch manifest for latest tag" + docker buildx imagetools create -t "${IMAGE_BASE}:latest" $(build_arch_args "latest") + fi + + # 合并完整版本号标签 + echo "Merging multi-arch manifest for ${FULL_VER} tag" + docker buildx imagetools create -t "${IMAGE_BASE}:${FULL_VER}" $(build_arch_args "${FULL_VER}") + + # 合并主次版本号标签 + echo "Merging multi-arch manifest for ${MAJOR_MINOR_VER} tag" + docker buildx imagetools create -t "${IMAGE_BASE}:${MAJOR_MINOR_VER}" $(build_arch_args "${MAJOR_MINOR_VER}") diff --git a/.github/workflows/docker-postgres.yml b/.github/workflows/docker-postgres.yml new file mode 100644 index 0000000..561fb0c --- /dev/null +++ b/.github/workflows/docker-postgres.yml @@ -0,0 +1,79 @@ +name: Build and Push PostgreSQL Docker Image + +on: + push: + branches: [main] + paths: + - 'Dockerfile-postgres' + - '.github/workflows/docker-postgres.yml' + workflow_dispatch: + inputs: + reason: + description: 'Reason for manual trigger' + required: false + default: 'Manual trigger' + +env: + REGISTRY: ghcr.io + +jobs: + build-and-push: + if: github.repository == 'laoshuikaixue/VoiceHub' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: '0' + + # 转换仓库名、用户名全小写,解决GHCR大写报错 + - name: Generate lowercase image & actor name + id: lowercase + run: | + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + ACTOR_LOWER=$(echo "${{ github.actor }}" | tr '[:upper:]' '[:lower:]') + IMAGE_FULL="${REPO_LOWER}-postgres" + echo "repo_lower=$REPO_LOWER" >> $GITHUB_OUTPUT + echo "actor_lower=$ACTOR_LOWER" >> $GITHUB_OUTPUT + echo "image_name=$IMAGE_FULL" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ steps.lowercase.outputs.actor_lower }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=15-alpine,enable={{is_default_branch}} + + - name: Build and push PostgreSQL Docker image + id: build-and-push + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile-postgres + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/ppc64le,linux/s390x,linux/386,linux/riscv64 + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + sbom: false diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..a2fb4f2 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,32 @@ +name: Nix CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +jobs: + nix-check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/determinate-nix-action@v3 + + - name: Setup Cachix binary cache + uses: cachix/cachix-action@v14 + with: + name: voicehub + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Run nix flake check + run: nix flake check + + - name: Build voicehub (validate hashes) + run: nix build .#voicehub diff --git a/.github/workflows/update-nix-pnpm-hash.yml b/.github/workflows/update-nix-pnpm-hash.yml new file mode 100644 index 0000000..95cc199 --- /dev/null +++ b/.github/workflows/update-nix-pnpm-hash.yml @@ -0,0 +1,110 @@ +name: 更新 Nix pnpm 依赖哈希 + +on: + push: + paths: + - pnpm-lock.yaml + - flake.nix + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - pnpm-lock.yaml + - flake.nix + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: update-nix-pnpm-hash-${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true + +jobs: + update-hash: + name: 同步 pnpmDeps 哈希 + if: github.actor != 'github-actions[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-latest + env: + TARGET_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} + CHECKOUT_REPOSITORY: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + + steps: + - name: 检出触发分支 + uses: actions/checkout@v4 + with: + repository: ${{ env.CHECKOUT_REPOSITORY }} + ref: ${{ env.TARGET_BRANCH }} + + - name: 安装 Nix + uses: DeterminateSystems/determinate-nix-action@v3 + + - name: 配置 Git 提交身份 + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: 计算 pnpmDeps 哈希 + shell: bash + run: | + set -euo pipefail + + python3 - <<'PY' + from pathlib import Path + + path = Path("flake.nix") + text = path.read_text(encoding="utf-8") + marker = "pnpmDeps = pkgs.fetchPnpmDeps {" + start = text.index(marker) + key = 'hash = "' + hash_start = text.index(key, start) + len(key) + hash_end = text.index('"', hash_start) + path.write_text(text[:hash_start] + text[hash_end:], encoding="utf-8") + PY + + set +e + nix build .#voicehub --no-link 2>&1 | tee nix-build.log + status="${PIPESTATUS[0]}" + set -e + + if [ "$status" -eq 0 ]; then + echo "Nix 构建在空哈希下意外成功,无法提取新的 pnpmDeps 哈希。" + exit 1 + fi + + new_hash="$(grep -Eo 'got:[[:space:]]+sha256-[A-Za-z0-9+/=_-]+' nix-build.log | tail -n 1 | sed -E 's/got:[[:space:]]+//')" + + if [ -z "$new_hash" ]; then + echo "没有在 Nix 输出中找到 got: sha256-...,请查看上方日志。" + exit 1 + fi + + HASH="$new_hash" python3 - <<'PY' + import os + from pathlib import Path + + new_hash = os.environ["HASH"] + path = Path("flake.nix") + text = path.read_text(encoding="utf-8") + marker = "pnpmDeps = pkgs.fetchPnpmDeps {" + start = text.index(marker) + key = 'hash = "' + hash_start = text.index(key, start) + len(key) + hash_end = text.index('"', hash_start) + path.write_text(text[:hash_start] + new_hash + text[hash_end:], encoding="utf-8") + PY + + echo "新的 pnpmDeps 哈希: $new_hash" + + - name: 提交哈希更新 + shell: bash + run: | + set -euo pipefail + + if git diff --quiet -- flake.nix; then + echo "pnpmDeps 哈希已经是最新,无需提交。" + exit 0 + fi + + git add flake.nix + git commit -m "ci: 同步 pnpmDeps 哈希" + git push origin "HEAD:${TARGET_BRANCH}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3029fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist +docs +.pnpm-store +.mimosa + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Backups +backups/ +*.backup + +# Misc +.DS_Store +.fleet +.idea +.kiro +.cursor +.vercel +.netlify +.vscode +.trae +.qoder +.figma +.codex +.agents +scripts/package-lock.json + +# Nix build output +result + +# Local env files +.env +.env.* +!.env.example + +# Tencent Cloud EdgeOne +.edgeone/* +.tef_dist/* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..09a9f99 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,19 @@ +.nuxt/ +.output/ +dist/ +node_modules/ +public/ +coverage/ +*.min.js +*.min.css +.DS_Store +*.log +vendor/ +oh_modules/ +**/oh_modules/ +VoiceHub-hmos/oh_modules/ +VoiceHub-hmos/oh-package-lock.json5 +app/drizzle/ +**/build/ +**/.preview/ +VoiceHub-hmos/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5ce0b03 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..83e9384 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,8 @@ +.git +node_modules +.nuxt +.output +.env +.env.* +!.env.example +README.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1835958 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,110 @@ +# VoiceHub Agent 行为规范 + +## 1. 项目概览 + +VoiceHub — Nuxt 4 校园广播站点歌管理系统。 + +- **前端**: Nuxt 4 (Vue 3), Tailwind CSS +- **图标**: 自定义 `Icon.vue` 组件(内联 SVG),辅以 `@lucide/vue` +- **后端**: Nuxt Server API (Nitro) +- **数据库**: PostgreSQL, Drizzle ORM +- **语言**: 后端 TypeScript, Vue SFC 用纯 JavaScript(无 `lang="ts"`) + +## 2. 规范 + +### 2.1. 语言 +注释、文档、Git 信息均用简体中文。注释中禁止写思考过程等无用内容,只需关键部分。 +- CSS 注释只保留关键信息(变量用途、值来源、防误删警告),禁止写设计意图、过程或对比说明(如“取值与现状一致”“视觉零变化”“浅色模式下需清晰可见”“对齐某分支观感”等) + +### 2.2. Vue 组件 +- 统一 ` + + ``` +- **同步规则**:新增主题时需在 `app/assets/css/themes/` 下创建对应的 CSS 文件,**同时**在 `public/themes/` 下创建对应的图片目录并放入 SVG 文件。如果只新增 CSS 主题而未同步图片,组件将无法加载主题图片。 + +#### 自定义主题 + +如需新增自定义主题: + +1. 在 `app/assets/css/themes/` 下创建新主题,如 `ocean.css`,使用 `:root[data-theme="ocean"]` 作为根选择器定义所有设计变量 +3. 在 `app/assets/css/main.css` 中添加对应的 `@import` 语句 +4. 在 `app/composables/useTheme.ts` 中将新主题 ID 加入 `Theme` 类型和 `THEMES` 数组 +5. 在 `public/themes/` 下创建对应主题的 SVG 图片文件夹(如 `public/themes/ocean/`),**放入与 `ClassicDark/` 和 `ClassicLight/` 相同的 SVG 图片**,否则使用该主题时组件无法加载主题图片。 + +## 使用说明 + +### 普通用户 + +1. 访问主页,查看当前排期 +2. 注册/登录账号 +3. 在仪表盘中点歌或给喜欢的歌曲投票 + - 支持搜索网易云音乐、QQ音乐和哔哩哔哩平台 + - 可以试听歌曲并选择音质 + - 支持给已有歌曲投票 + - **网易云音乐登录功能**: + - 扫码登录网易云音乐账号 + - 登录后可一键添加当前排期歌曲到个人歌单 + - 支持从个人歌单中直接投稿歌曲 + - 支持从最近播放记录中投稿歌曲 + - 可搜索并投稿播客和电台内容 +4. 使用内置播放器播放歌曲 + - 支持多种音质切换(标准、HQ、无损、Hi-Res等) + - 实时切换音质并保持播放进度 +5. 查看通知中心获取歌曲状态更新 + +### 管理员 + +1. 使用管理员账号登录(默认账号:admin,密码:admin123) +2. 进入管理后台,选择相应功能标签 +3. **排期管理**:可以看到左侧"待排歌曲"和右侧"播放顺序" + - 通过拖拽将歌曲从左侧添加到右侧的排期列表 + - 可以在右侧拖拽调整歌曲播放顺序 + - 支持播出时段管理,可设置不同时段的播放安排 + - **草稿功能**:支持保存排期草稿,允许管理员先保存未完成的排期安排 + - 点击"保存草稿"按钮保存当前排期为草稿状态 + - 草稿不会影响公开展示的排期,可以随时修改 + - 点击"保存并发布"按钮将草稿发布为正式排期 + - 点击"保存顺序"按钮保存排期 +4. **打印排期**:专业的排期打印和导出功能 + - 选择纸张大小(A4、A3、Letter、Legal)和页面方向 + - 自定义显示内容:歌曲封面、歌名、歌手、投稿人、热度等 + - 快捷日期选择:今天、明天、本周、下周 + - 智能分组显示:按日期分组,有多个播出时段时自动按时间排序 + - 实时预览:所见即所得的打印预览 + - PDF导出:支持导出高质量PDF文件,自动处理跨域图片 +5. **歌曲管理**:查看和管理所有歌曲 + - 支持播放歌曲并实时切换音质 + - 动态获取最新的音乐播放链接 + - 提供歌曲下载功能,支持批量下载管理 + - 批量更新歌曲信息和状态 +6. **数据分析**:查看系统使用统计和数据分析 + - 实时统计数据:用户活跃度、歌曲热度、投票趋势 + - 学期对比分析:不同学期的数据对比 + - 用户参与度分析:用户行为和参与度统计 + - 趋势分析:系统使用趋势和预测 +7. **数据库管理**:数据库备份恢复和维护 + - 创建和下载数据库备份 + - 上传和恢复备份文件 + - 序列重置:修复数据库序列问题 + - 数据库状态检查和完整性验证 +8. **学期管理**:设置和管理学期信息 + - 创建新学期(如"2024-2025学年上学期") + - 设置当前活跃学期 + - 点歌记录自动关联到当前学期 +9. **用户管理**:添加、编辑和删除用户 + - 单个添加:填写用户信息(包括姓名、账号、年级、班级) + - 批量导入:通过EXCEL文件批量添加用户 + - 可以重置用户密码 +10. **黑名单管理**:管理歌曲和关键词黑名单 + - 添加具体歌曲或关键词到黑名单 + - 自动过滤包含黑名单内容的点歌请求 + - 支持启用/禁用黑名单项 +11. **系统设置**:配置系统参数和功能开关 + - 站点信息配置:标题、Logo、描述等 + - 投稿限额设置:每日/每周投稿限制 + - 播出时段管理:配置不同的播出时间段 + - 功能开关:启用/禁用特定功能 +12. **通知管理**:向用户发送系统通知 + - 支持按全体用户、年级、班级或多班级发送 + - 实时显示发送进度和结果 + - 通知历史记录和管理 + +## 数据库管理 + +### Drizzle ORM + Neon Database + +项目使用 Drizzle ORM 作为数据库 ORM,配合 Neon Database 提供现代化的数据库解决方案。 + +#### 核心文件结构 + +- **`drizzle.config.ts`** - Drizzle ORM 主配置文件 +- **`app/drizzle/db.ts`** - 数据库连接和客户端配置 +- **`app/drizzle/schema.ts`** - 数据库表结构定义(TypeScript 类型安全) +- **`app/drizzle/migrations/`** - 数据库迁移脚本目录 + +### 数据库初始化 + +首次部署时,系统会自动初始化数据库结构。 + +#### 自动初始化(推荐) + +使用部署脚本自动完成数据库初始化: + +```bash +pnpm run deploy +``` + +该命令会: + +1. 检查环境变量配置 +2. 安装依赖 +3. 执行数据库迁移 +4. 创建默认管理员账户 +5. 构建应用 + +#### 手动初始化 + +如需手动管理数据库: + +1. 生成迁移文件 + +```bash +pnpm run db:generate +``` + +2. 执行数据库迁移 + +```bash +pnpm run db:migrate +``` + +3. 推送模式变更到数据库(开发环境) + +```bash +pnpm run db:push +``` + +4. 启动 Drizzle Studio(数据库管理界面) + +```bash +pnpm run db:studio +``` + +访问 https://local.drizzle.studio 查看和管理数据库 + +5. 清空数据库并创建管理员 + +```bash +pnpm run clear-db +``` + +### 数据库备份与恢复 + +#### 通过管理后台 + +1. 创建备份 + - 登录管理后台 + - 进入"数据库管理"页面 + - 点击"创建备份"按钮 + - 选择备份类型(完整备份/用户数据备份) + +2. 下载备份 + - 备份完成后在列表中找到备份文件 + - 点击"下载"按钮保存到本地 + +3. 恢复备份 + - 点击"上传备份"按钮 + - 选择备份文件 + - 选择恢复模式: + - 增量恢复:合并备份数据与现有数据 + - 完全恢复:替换所有现有数据(谨慎使用) + - 确认并执行恢复 + +#### 使用 PostgreSQL 命令行 + +```bash +# 备份数据库 +pg_dump -h localhost -U username -d database_name > backup.sql + +# 恢复数据库 +psql -h localhost -U username -d database_name < backup.sql +``` + +### 备份文件格式 + +- **完整备份**:包含所有数据表的 JSON 格式文件 +- **用户备份**:仅包含用户相关数据 +- **元数据**:包含创建时间、创建者、表信息等 + +## 开发指南 + +### 组合式API + +项目使用了Vue 3的组合式API,主要包括: + +- `useAuth`: 处理用户认证、登录、注册和权限控制 +- `useSongs`: 处理歌曲相关操作,包括获取歌曲列表、点歌和投票 +- `useAdmin`: 处理管理员操作,包括排期管理和标记播放 +- `useNotifications`: 处理通知系统,包括获取、标记已读和设置 +- `useAudioQuality`: 处理音质管理,包括音质设置和持久化 +- `useSemesters`: 处理学期管理,包括创建学期和设置活跃学期 + +### 添加新功能 + +1. 在 `server/api` 中添加新的API端点 +2. 在 `app/composables` 中添加相应的组合式函数 +3. 在 `app/components` 中创建UI组件 +4. 在 `app/pages` 中整合组件和功能 + +### 数据库模型修改 + +如需修改数据库模型: + +1. 编辑`app/drizzle/schema.ts`文件中的表结构定义 +2. 生成新的迁移文件:`pnpm run db:generate` +3. 应用迁移到数据库:`pnpm run db:migrate` +4. 确保同时更新 `types/index.ts` 中的TypeScript类型定义 +5. 使用Drizzle Studio查看数据库:`pnpm run db:studio` + +### 国际化 (i18n) + +VoiceHub 内置一套无第三方依赖的手写国际化方案,支持 `zh-CN`(简体中文)与 `en-US`(English)两种语言。 + +#### 架构概览 + +- **词典文件**:`app/utils/locale/zh-CN.ts`、`app/utils/locale/en-US.ts`。两者结构必须完全一致(键对齐),值可为字符串、带 `{0}`/`{1}` 占位符的模板或格式化函数。 +- **运行时**:`app/utils/locale/index.ts` 提供 `useLocale()`、`setLocale()`、`loadLocaleMessages()` 等。 + - 中文(`FALLBACK_LOCALE`)作为**兜底与合并基底静态内置**;其余语言在被激活时才**动态按需加载**,默认语言用户不会下载多余语言包。 + - `mergeLocaleFallback` 保证非兜底语言缺失某键时自动回退中文,不会出现空文本。 + - 当前语言用 `useState('voicehub-locale')` 存储,**SSR 下按请求隔离**,避免跨请求语言串扰。 +- **初始化插件**:`app/plugins/locale.ts`(服务端 + 客户端通用)。仅当用户手动选择过语言(`manual` 偏好)时信任 `cookie`,否则每次进入按 `Accept-Language` / 浏览器语言**跟随系统语言**,系统语言变化后自动切换;手动选择长期保留。渲染前 `await` 目标语言词典以消除首屏闪烁与水合不匹配,并驱动 ``。 + +#### 组件中使用 + +```js +// 取带兜底的响应式文案分区 +const { pages, songs } = useLocale() +const locale = computed(() => pages.value?.forgotPassword || {}) + +// 键查找 + {0}/{count} 占位符替换(复用共享助手,勿自行实现) +const { t } = useLocaleText(locale) +t('title') +``` + +- 文案访问统一复用 `~/composables/useLocaleText.ts`(`t`/`msg`/`nested`/`format`)与 `~/composables/useSafeLocale.ts`,**禁止**在组件内重复实现 `callLocale`、`getNestedMessage` 等私有取值函数。 +- 新增文案键时,**必须同时在 `zh-CN.ts` 和 `en-US.ts` 添加**,保持键结构完全一致。 + +#### 服务端错误码本地化 + +服务端错误消息与语言解耦,通过「稳定错误码 + 客户端词典」实现本地化: + +1. 服务端抛错使用 `createApiError`(`server/utils/apiError.ts`)而非裸 `createError`: + + ```ts + import { createApiError } from '~~/server/utils/apiError' + // createApiError(statusCode, code, message, data?) + throw createApiError(429, 'AUTH_RATE_LIMITED_MINUTES', '操作过于频繁,请等待 ${waitMinutes} 分钟后再试', { params: [waitMinutes] }) + ``` + + - `code` 建议取自 `server/config/constants.ts` 的 `SERVER_ERROR_CODES`;同时写入 `statusMessage` 与 `data.code`。 + - `message` 为默认兜底文案(词典未命中时展示),动态值用 `data.params` 承载。 +2. 客户端统一用 `useServerErrors().localize(err, fallback)` 展示错误:按 `err.data.code` 命中 `serverErrors` 词典,用 `data.params` 替换 `{0}`/`{1}`,未命中再回退服务端 `message`。 +3. 新增错误码需在**三处同步**:`SERVER_ERROR_CODES`、`zh-CN.ts` 的 `serverErrors`、`en-US.ts` 的 `serverErrors`(键完全对齐)。 + +### OAuth 平台扩展指南 + +VoiceHub 采用配置化与策略模式(Strategy Pattern)相结合的灵活 OAuth 扩展机制,所有 OAuth 提供商及认证设置现均已迁移至管理员后台界面。你可以直接在后台动态配置,无需修改环境变量和重启服务。 + +对于想要通过代码深度定制 OAuth 行为(如自定义用户信息解析逻辑等)的开发者,可参考以下机制: + +#### 扩展步骤 + +##### 1. 定义 OAuth 策略 + +在 `server/utils/oauth-strategies.ts` 文件中,实现 `OAuthStrategy` 接口。该接口定义了 OAuth 流程中的三个核心方法: + +```typescript +export interface OAuthStrategy { + /** + * 获取授权跳转 URL + * @param redirectUri 回调地址(通常是 /api/auth/[provider]/callback) + * @param state 包含安全校验信息的加密字符串 + */ + getAuthorizeUrl(redirectUri: string, state: string): string + + /** + * 使用 code 换取 access_token + * @param code 授权码 + * @param redirectUri 回调地址 + */ + exchangeToken(code: string, redirectUri: string): Promise + + /** + * 获取用户信息 + * @param accessToken 访问令牌 + */ + getUserInfo(accessToken: string): Promise +} +``` + +**示例:接入 Google 登录** + +```typescript +// server/utils/oauth-strategies.ts +const googleStrategy: OAuthStrategy = { + getAuthorizeUrl(redirectUri, state) { + const clientId = process.env.GOOGLE_CLIENT_ID + // Google 授权端点 + return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=email%20profile&state=${encodeURIComponent(state)}` + }, + + async exchangeToken(code, redirectUri) { + // ... 实现 Google 的 Token 交换逻辑 + // 通常是发送 POST 请求到 https://oauth2.googleapis.com/token + }, + + async getUserInfo(accessToken) { + // ... 实现获取 Google 用户信息的逻辑 + // 通常是发送 GET 请求到 https://www.googleapis.com/oauth2/v3/userinfo + // 需返回统一的 OAuthUserInfo 格式: + // { id, username, email, name, avatar } + } +} +``` + +##### 2. 注册策略 + +在同一个文件 (`server/utils/oauth-strategies.ts`) 的 `strategies` 对象中注册你的新策略: + +```typescript +const strategies: Record = { + github: githubStrategy, + casdoor: casdoorStrategy, + google: googleStrategy // <--- 注册新平台 + // ... 其他平台 +} +``` + +##### 3. 完成! + +现在,你可以在管理员后台的 **站点配置 -> OAuth 第三方登录配置** 中直接填写该平台的信息并启用它。系统会自动处理路由分发、State 校验、CSRF 保护和用户绑定逻辑。 + +#### Casdoor 配置说明 + +项目已内置对 [Casdoor](https://casdoor.org/) 的支持。Casdoor 是一个开源的 UI 优先的身份认证管理系统 (IAM),支持 OAuth 2.0、OIDC 等多种协议。 + +要启用 Casdoor 登录,只需进入管理员后台的 **站点配置 -> OAuth 第三方登录配置**,开启 Casdoor 选项,并填入以下信息: + +- **Casdoor 服务器 URL** (如 `https://your-casdoor-domain.com`) +- **Casdoor Client ID** +- **Casdoor Client Secret** +- **Casdoor 组织名称** + +配置保存后,系统会立即启用 Casdoor 登录策略。 + +#### 前端图标配置 + +当添加了新的服务端 OAuth 策略后,如果需要在前端登录页面显示对应的图标按钮,请按照以下步骤操作: + +1. 在 `app/components/Auth/Providers` 目录下创建一个以 Provider 名称(首字母大写)命名的文件夹,例如 `Google`。 +2. 在该文件夹内创建一个 `Icon.vue` 组件,放入对应的 SVG 图标代码。 + - 建议 SVG 大小设置为 `w-5 h-5` 以保持样式统一。 +3. 系统会自动检测并加载该图标,无需额外配置。 + +例如:`app/components/Auth/Providers/Google/Icon.vue` + +**注意:** 对于 Casdoor,请创建 `app/components/Auth/Providers/Casdoor/Icon.vue`,并填入 Casdoor 的图标代码。 + +#### OAuth 工具函数 + +为了统一前端 OAuth 提供商的名称显示,系统提供了 `getProviderDisplayName` 工具函数。 + +**位置**: `app/utils/oauth.ts` + +**使用方法**: + +```typescript +import { getProviderDisplayName } from '~/utils/oauth' + +// 获取显示名称 +const displayName = getProviderDisplayName('github') // 返回 "GitHub" +const displayName2 = getProviderDisplayName('casdoor') // 返回 "Casdoor" +const displayName3 = getProviderDisplayName('google') // 返回 "Google" (默认首字母大写) +``` + +**扩展**: +当添加新的 OAuth 提供商时,可以在 `app/utils/oauth.ts` 的 `map` 对象中添加对应的映射关系,以实现自定义显示名称。 + +#### 添加绑定卡片 + +为了在账号管理页面显示新添加的 OAuth 提供商绑定选项,你需要修改 `app/components/Auth/OAuthBindingCard.vue` 文件。 + +**1. 添加计算属性** + +在 ` + + diff --git a/app/assets/css/components.css b/app/assets/css/components.css new file mode 100644 index 0000000..fdea3c7 --- /dev/null +++ b/app/assets/css/components.css @@ -0,0 +1,410 @@ +/* 通用组件样式 - 统一设计系统 */ + +/* 按钮组件 */ +.btn-base { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--radius-lg); + font-size: var(--text-sm); + font-weight: var(--font-medium); + line-height: 1; + text-decoration: none; + border: none; + cursor: pointer; + transition: all var(--transition-normal); + white-space: nowrap; + user-select: none; +} + +.btn-base:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.btn-base:disabled { + background-color: var(--btn-disabled-bg); + color: var(--btn-disabled-text); + cursor: not-allowed; + opacity: 0.6; +} + +.btn-base svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* 按钮变体 */ +.btn-primary { + background-color: var(--btn-primary-bg); + color: var(--btn-primary-text); +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--btn-primary-hover); +} + +.btn-secondary { + background-color: var(--btn-secondary-bg); + color: var(--btn-secondary-text); + border: 1px solid var(--btn-secondary-border); +} + +.btn-secondary:hover:not(:disabled) { + background-color: var(--btn-secondary-hover); +} + +.btn-success { + background-color: var(--btn-success-bg); + color: var(--btn-success-text); +} + +.btn-success:hover:not(:disabled) { + background-color: var(--btn-success-hover); +} + +.btn-warning { + background-color: var(--btn-warning-bg); + color: var(--btn-warning-text); +} + +.btn-warning:hover:not(:disabled) { + background-color: var(--btn-warning-hover); +} + +.btn-error { + background-color: var(--btn-error-bg); + color: var(--btn-error-text); +} + +.btn-error:hover:not(:disabled) { + background-color: var(--btn-error-hover); +} + +/* 按钮尺寸 */ +.btn-sm { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--text-xs); +} + +.btn-lg { + padding: var(--spacing-lg) var(--spacing-2xl); + font-size: var(--text-base); +} + +/* 输入框组件 */ +.input-base { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-lg); + color: var(--input-text); + font-size: var(--text-sm); + line-height: 1.5; + transition: + border-color var(--transition-normal), + box-shadow var(--transition-normal); +} + +.input-base:focus { + outline: none; + border-color: var(--input-border-focus); + box-shadow: var(--input-shadow-focus); +} + +.input-base::placeholder { + color: var(--input-placeholder); +} + +.input-base:disabled { + background-color: var(--bg-quaternary); + color: var(--text-disabled); + cursor: not-allowed; +} + +/* 卡片组件 */ +.card-base { + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: var(--radius-xl); + padding: var(--spacing-2xl); + box-shadow: var(--card-shadow); + transition: + border-color var(--transition-normal), + box-shadow var(--transition-normal); +} + +.card-base:hover { + border-color: var(--card-hover-border); +} + +.card-interactive { + cursor: pointer; + transition: + transform var(--transition-normal), + border-color var(--transition-normal), + box-shadow var(--transition-normal); +} + +.card-interactive:hover { + transform: translateY(-2px); + border-color: var(--card-hover-border); + box-shadow: var(--shadow-lg); +} + +/* 模态框组件 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--modal-backdrop); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal-backdrop); + padding: var(--spacing-xl); +} + +.modal-content { + background: var(--modal-bg); + border: 1px solid var(--modal-border); + border-radius: var(--radius-xl); + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-xl); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-xl) var(--spacing-2xl); + border-bottom: 1px solid var(--modal-header-border); +} + +.modal-title { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0; +} + +.modal-body { + padding: var(--spacing-2xl); +} + +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-md); + padding: var(--spacing-xl) var(--spacing-2xl); + border-top: 1px solid var(--modal-footer-border); +} + +/* 表格组件 */ +.table-container { + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: var(--radius-xl); + overflow: hidden; +} + +.table-header { + background: var(--table-header-bg); + border-bottom: 1px solid var(--table-border); + padding: var(--spacing-lg) var(--spacing-xl); + font-weight: var(--font-semibold); + color: var(--text-secondary); + font-size: var(--text-sm); +} + +.table-row { + border-bottom: 1px solid var(--table-border); + padding: var(--spacing-lg) var(--spacing-xl); + transition: background-color var(--transition-normal); +} + +.table-row:hover { + background-color: var(--table-row-hover); +} + +.table-row:last-child { + border-bottom: none; +} + +.table-row.selected { + background-color: var(--table-selected); +} + +/* 状态徽章 */ +.badge-base { + display: inline-flex; + align-items: center; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-md); + font-size: var(--text-xs); + font-weight: var(--font-medium); + line-height: 1; +} + +.badge-success { + background-color: var(--status-approved-light); + color: var(--status-approved); + border: 1px solid var(--success-border); +} + +.badge-warning { + background-color: var(--status-pending-light); + color: var(--status-pending); + border: 1px solid var(--warning-border); +} + +.badge-error { + background-color: var(--error-light); + color: var(--error); + border: 1px solid var(--error-border); +} + +.badge-info { + background-color: var(--info-light); + color: var(--info); + border: 1px solid var(--info-border); +} + +/* 加载动画 */ +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--spinner-secondary); + border-top: 3px solid var(--spinner-primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.spinner-sm { + width: 16px; + height: 16px; + border-width: 2px; +} + +.spinner-lg { + width: 48px; + height: 48px; + border-width: 4px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* 工具提示 */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip-content { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: var(--bg-quaternary); + color: var(--text-primary); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-md); + font-size: var(--text-xs); + white-space: nowrap; + z-index: var(--z-tooltip); + opacity: 0; + visibility: hidden; + transition: + opacity var(--transition-normal), + visibility var(--transition-normal); +} + +.tooltip:hover .tooltip-content { + opacity: 1; + visibility: visible; +} + +/* 分页组件 */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--spacing-xl); +} + +.pagination-btn { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--btn-secondary-bg); + border: 1px solid var(--btn-secondary-border); + border-radius: var(--radius-md); + color: var(--btn-secondary-text); + font-size: var(--text-sm); + cursor: pointer; + transition: all var(--transition-normal); +} + +.pagination-btn:hover:not(:disabled) { + background: var(--btn-secondary-hover); +} + +.pagination-btn:disabled { + background: var(--btn-disabled-bg); + color: var(--btn-disabled-text); + cursor: not-allowed; + opacity: 0.6; +} + +.pagination-info { + font-size: var(--text-sm); + color: var(--text-tertiary); + margin: 0 var(--spacing-lg); +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-3xl) var(--spacing-xl); + text-align: center; + color: var(--text-tertiary); +} + +.empty-state svg { + width: 48px; + height: 48px; + margin-bottom: var(--spacing-lg); + color: var(--text-disabled); +} + +.empty-state-title { + font-size: var(--text-lg); + font-weight: var(--font-medium); + color: var(--text-secondary); + margin-bottom: var(--spacing-sm); +} + +.empty-state-description { + font-size: var(--text-sm); + color: var(--text-tertiary); + margin-bottom: var(--spacing-lg); +} diff --git a/app/assets/css/lyric-player.module.css b/app/assets/css/lyric-player.module.css new file mode 100644 index 0000000..9c8d703 --- /dev/null +++ b/app/assets/css/lyric-player.module.css @@ -0,0 +1,197 @@ +/* + 给各个内部元素使用的样式 +*/ + +/* 给每行歌词应用的样式 */ +.lyricLine { + position: absolute; + backface-visibility: hidden; + transform-origin: left; + width: var(--amll-lp-width, 100%); + min-width: var(--amll-lp-width, 100%); + max-width: var(--amll-lp-width, 100%); + width: 100%; + height: fit-content; + padding: 0.5em 1em; + contain: content; + will-change: transform; + transition: + opacity 0.25s, + filter 0.2s, + background-color 0.25s, + box-shadow 0.25s; + box-sizing: border-box; + border-radius: 0.25em; + + padding-left: 1em; + padding-right: 1em; + + @media screen and (max-width: 500px) { + padding-left: 20px; + padding-right: 20px; + } + + &.dirty { + opacity: 0; + visibility: hidden; + } + + &:has(> *):hover { + background-color: var(--amll-lp-hover-bg-color, #fff1); + /* box-shadow: 0 0 0 4px var(--amll-lp-hover-bg-color, #fff1); */ + } + + &:has(> *):active { + background-color: var(--amll-lp-hover-bg-color, #ffffff05); + /* box-shadow: 0 0 0 var(--amll-lp-hover-bg-color, #fff1); */ + } +} + +.lyricBgLine { + opacity: 0.0001; + font-size: max(calc(1em * var(--amll-lp-bg-line-scale, 0.7)), 10px); + transition: + opacity 0.25s, + scale 0.5s, + filter 0.2s, + background-color 0.25s, + box-shadow 0.25s; + /* 因为字体大小缩小了,故内边距要和主行字体大小统一,行边距计算公式为 100% / font-size 转 em 单位 */ + padding: 1vh calc(var(--amll-lp-line-padding-x, 1em) / var(--amll-lp-bg-line-scale, 0.7)); + + &.active { + transition: + opacity 0.5s 0.25s, + scale 1.5s cubic-bezier(0, 1, 0, 1) 0.25s, + filter 0.2s, + background-color 0.25s, + box-shadow 0.25s; + opacity: 0.4; + } +} + +:global(.amll-lyric-player) { + &:hover .lyricLine { + filter: unset !important; + } + + &.hasDuetLine { + .lyricLine:not(.lyricDuetLine) { + padding-right: 15%; + } + + .lyricDuetLine { + padding-left: 15%; + } + } + + &:not(:global(.playing)) > .lyricBgLine { + opacity: 0.4; + } +} + +.lyricDuetLine { + text-align: right; + transform-origin: right; +} + +.lyricMainLine { + transition: opacity 0.3s 0.1s; + margin: -1em; + padding: 1em; + /* 为主歌词行增加文字阴影,以提升亮色背景下的可读性 */ + text-shadow: + 0 2px 6px var(--mask-60), + 0 0 10px var(--mask-50); + + & span { + display: inline-block; + } + + .romanWord { + font-size: 0.5em; + line-height: 1em; + } + + & > span, + span.emphasizeWrapper { + white-space: pre-wrap; + display: inline-block; + padding: 1em; + margin: -1em; + will-change: transform; + + &.emphasize, + span.emphasize { + padding: 1em; + margin: -1em; + backface-visibility: hidden; + + & > span { + padding: 1em; + margin: -1em; + will-change: transform; + backface-visibility: hidden; + } + } + } +} + +.lyricSubLine { + font-size: max(0.5em, 10px); + line-height: 1.5em; + transition: opacity 0.2s 0.25s; + opacity: 0.3; + /* 为副歌词行增加轻度文字阴影,避免白字贴白背景 */ + text-shadow: 0 1px 3px var(--mask-50); +} + +.disableSpring > * { + transition: + filter 0.25s, + transform 0.5s, + background-color 0.25s, + box-shadow 0.25s; +} + +.interludeDots { + height: clamp(0.5em, 1vh, 3em); + transition: opacity 0.25s; + transform-origin: center; + width: fit-content; + padding: 2.5% 0.75em; + position: absolute; + display: flex; + gap: 0.25em; + left: 0; + opacity: 0; + + &.enabled { + opacity: 1; + } + + & > * { + width: clamp(0.5em, 1vh, 3em); + height: clamp(0.5em, 1vh, 3em); + display: inline-block; + border-radius: 50%; + aspect-ratio: 1 / 1; + background-color: var(--amll-lp-color, white); + margin-right: 4px; + } + + &.duet { + right: 0; + transform-origin: center; + } +} + +@supports (mix-blend-mode: plus-lighter) { + .lyricSubLine { + opacity: 0.3; + } +} + +.tmpDisableTransition { + transition: none !important; +} diff --git a/app/assets/css/main.css b/app/assets/css/main.css new file mode 100644 index 0000000..2b3a7d1 --- /dev/null +++ b/app/assets/css/main.css @@ -0,0 +1,492 @@ +/* 导入浏览器主题保护样式 */ +@import url('./theme-protection.css'); + +/* 导入主题变量(亮色覆盖暗色,由 data-theme 控制) */ +@import url('./themes/ClassicDark.css'); +@import url('./themes/ClassicLight.css'); +@import url('./themes/ModernLight.css'); + +@unocss; + +/* 防止浏览器自动应用深色主题 */ +:root { + color-scheme: light dark; +} + +/* 全局控件默认强调色 — 跟随主题主色调,但允许显式 accent-* 类覆盖 */ +input[type="checkbox"]:not([class*="accent-"]), +input[type="radio"]:not([class*="accent-"]), +input[type="range"]:not([class*="accent-"]) { + accent-color: var(--accent-color); +} + +/* 流体背景与全局样式 */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + /* UnoCSS preflight 未重置 border-style,补齐后 utility 边框与加载转圈才能渲染 */ + border-style: solid; + border-width: 0; +} + +/* 字体显示策略优化 */ +@font-face { + font-family: 'MiSans'; + font-display: swap; /* 确保字体加载期间文本可见 */ +} + +body { + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Open Sans', + sans-serif; + color: var(--text-primary); + background-color: var(--bg-primary); + line-height: 1.6; + overflow-x: hidden; + font-size: var(--text-base); + /* 字体渲染优化 */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Open Sans', + sans-serif; +} + +/* 链接样式 */ +a { + text-decoration: none; + color: inherit; +} + +/* 简洁现代背景 */ +body { + background: var(--bg-primary); + color: var(--text-primary); +} + +/* 可选的微妙纹理效果 */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + radial-gradient(circle at 20% 50%, rgba(var(--color-indigo-rgb), 0.03) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, var(--bg-texture-warm-glow) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(var(--color-indigo-rgb), 0.02) 0%, transparent 50%); + z-index: -1; + pointer-events: none; +} + +/* 容器 */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +/* 常用工具类 */ +.flex { + display: flex; +} +.flex-col { + flex-direction: column; +} +.items-center { + align-items: center; +} +.justify-between { + justify-content: space-between; +} +.justify-center { + justify-content: center; +} +.gap-2 { + gap: var(--spacing-sm); +} +.gap-4 { + gap: var(--spacing-lg); +} +.gap-6 { + gap: var(--spacing-2xl); +} + +.grid { + display: grid; +} +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +/* 间距 */ +.mt-2 { + margin-top: var(--spacing-sm); +} +.mt-4 { + margin-top: var(--spacing-lg); +} +.mt-6 { + margin-top: var(--spacing-2xl); +} +.mt-8 { + margin-top: var(--spacing-3xl); +} +.mb-2 { + margin-bottom: var(--spacing-sm); +} +.mb-4 { + margin-bottom: var(--spacing-lg); +} +.mb-6 { + margin-bottom: var(--spacing-2xl); +} +.mb-8 { + margin-bottom: var(--spacing-3xl); +} +.ml-2 { + margin-left: var(--spacing-sm); +} +.ml-4 { + margin-left: var(--spacing-lg); +} +.mr-2 { + margin-right: var(--spacing-sm); +} +.mr-4 { + margin-right: var(--spacing-lg); +} + +.p-2 { + padding: var(--spacing-sm); +} +.p-4 { + padding: var(--spacing-lg); +} +.p-6 { + padding: var(--spacing-2xl); +} +.p-8 { + padding: var(--spacing-3xl); +} + +/* 文本 */ +.text-center { + text-align: center; +} +.text-sm { + font-size: var(--text-sm); +} +.text-lg { + font-size: var(--text-lg); +} +.text-xl { + font-size: var(--text-xl); +} +.text-2xl { + font-size: var(--text-2xl); +} +.text-3xl { + font-size: var(--text-3xl); +} +.font-bold { + font-weight: var(--font-bold); +} +.text-primary { + color: var(--primary); +} +.text-secondary { + color: var(--text-secondary); +} +.text-tertiary { + color: var(--text-tertiary); +} +.text-success { + color: var(--success); +} +.text-danger { + color: var(--error); +} +.text-warning { + color: var(--warning); +} + +/* 卡片 */ +.card { + background: var(--card-bg); + backdrop-filter: blur(12px); + border: 1px solid var(--card-border); + border-radius: var(--radius-xl); + padding: var(--spacing-2xl); + box-shadow: + 0 4px 6px var(--mask-10), + 0 1px 3px var(--mask-8), + inset 0 1px 0 var(--overlay-5); + transition: all var(--transition-normal); + position: relative; + overflow: hidden; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--overlay-10), transparent); +} + +.card:hover { + transform: translateY(-4px); + border-color: var(--card-hover-border); + box-shadow: + 0 12px 24px var(--mask-15), + 0 4px 8px var(--mask-10), + inset 0 1px 0 var(--overlay-10); +} + +/* 按钮 */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md) var(--spacing-2xl); + font-weight: var(--font-semibold); + border-radius: var(--radius-lg); + transition: all var(--transition-normal); + cursor: pointer; + text-decoration: none; + border: none; + outline: none; + font-size: var(--text-sm); + position: relative; + overflow: hidden; + box-shadow: 0 2px 4px var(--mask-10); +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, var(--overlay-10), transparent); + transition: left 0.5s; +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: linear-gradient(135deg, var(--btn-primary-bg) 0%, var(--primary-hover) 100%); + color: var(--btn-primary-text); + border: 1px solid var(--primary-border); +} + +.btn-primary:hover { + background: linear-gradient(135deg, var(--primary-hover) 0%, var(--primary) 100%); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--btn-primary-shadow); +} + +.btn-secondary { + background: linear-gradient(135deg, var(--btn-secondary-bg) 0%, var(--bg-quaternary) 100%); + color: var(--btn-secondary-text); + border: 1px solid var(--btn-secondary-border); +} + +.btn-secondary:hover { + background: linear-gradient(135deg, var(--bg-quaternary) 0%, var(--btn-secondary-hover) 100%); + color: var(--text-primary); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--overlay-10); +} + +.btn-outline { + background: transparent; + border: 2px solid var(--border-tertiary); + color: var(--text-primary); + backdrop-filter: blur(8px); +} + +.btn-outline:hover { + background: var(--bg-hover); + border-color: var(--primary); + color: var(--primary); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--primary-20); +} + +.btn:disabled { + background: var(--btn-disabled-bg); + color: var(--btn-disabled-text); + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.btn:disabled::before { + display: none; +} + +/* 列表样式 */ +.list-disc { + list-style-type: disc; + list-style-position: inside; +} + +.list-none { + list-style-type: none; +} + +.pl-5 { + padding-left: 1.25rem; +} + +/* 表单 */ +.input { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + background: var(--input-bg); + border: 2px solid var(--input-border); + border-radius: var(--radius-lg); + color: var(--input-text); + outline: none; + transition: all var(--transition-normal); + font-size: var(--text-sm); + position: relative; + backdrop-filter: blur(8px); +} + +.input:focus { + border-color: var(--input-border-focus); + box-shadow: + var(--input-shadow-focus), + 0 4px 12px var(--primary-10); + background: var(--bg-secondary); + transform: translateY(-1px); +} + +.input::placeholder { + color: var(--input-placeholder); + transition: color var(--transition-normal); +} + +.input:focus::placeholder { + color: var(--text-quaternary); +} + +.form-group { + margin-bottom: var(--spacing-lg); + position: relative; +} + +.form-group label { + display: block; + margin-bottom: var(--spacing-sm); + font-weight: var(--font-semibold); + color: var(--text-secondary); + transition: color var(--transition-normal); +} + +.form-group:focus-within label { + color: var(--primary); +} + +/* 响应式 */ +@media (min-width: 640px) { + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .sm\:flex-row { + flex-direction: row; + } + .sm\:items-center { + align-items: center; + } + .sm\:justify-between { + justify-content: space-between; + } +} + +@media (min-width: 768px) { + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .md\:flex-row { + flex-direction: row; + } +} + +@media (min-width: 1024px) { + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .lg\:flex-row { + flex-direction: row; + } +} + +/* 玻璃态效果 */ +.glass { + background: var(--bg-secondary); + backdrop-filter: blur(10px); + border: 1px solid var(--border-secondary); + border-radius: var(--radius-xl); +} + +/* 手机设备上的优化 */ +@media (max-width: 640px) { + .home { + padding: 0 !important; + } + + .main-content { + grid-template-columns: 1fr !important; + gap: 2rem !important; + } + + .action-card { + padding: 0.75rem !important; + } + + .modal-content { + width: 95% !important; + max-width: none !important; + } +} + +/* 重置 fixed 元素外边距:space-y-* 布局规则会给子元素加 margin-top,导致弹窗遮罩定位偏移 */ +.fixed { + margin: 0 !important; +} diff --git a/app/assets/css/markdown.css b/app/assets/css/markdown.css new file mode 100644 index 0000000..76eb3cf --- /dev/null +++ b/app/assets/css/markdown.css @@ -0,0 +1,145 @@ +/* 投稿须知 Markdown 富文本共享样式 */ + +.markdown-body { + line-height: 1.7; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4 { + font-weight: 700; + margin: 0.8em 0 0.4em; + color: var(--text-primary-lighter); +} + +.markdown-body h1 { font-size: 1.3em; } +.markdown-body h2 { font-size: 1.15em; } +.markdown-body h3 { font-size: 1.05em; } +.markdown-body h4 { font-size: 1em; } + +.markdown-body p { margin-bottom: 0.5em; } + +.markdown-body ul, +.markdown-body ol { + padding-left: 1.5em; + margin-bottom: 0.5em; + list-style-position: outside; +} + +.markdown-body ul { list-style-type: disc; } +.markdown-body ol { list-style-type: decimal; } +.markdown-body li { margin-bottom: 0.25em; } + +.markdown-body a { + color: var(--color-accent); + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 700; + color: var(--text-primary-lighter); +} + +.markdown-body em { font-style: italic; } + +.markdown-body code { + background: var(--bg-tertiary-10); + padding: 0.15em 0.4em; + border-radius: 4px; + font-size: 0.9em; +} + +.markdown-body pre { + background: var(--bg-primary-30); + padding: 0.75em 1em; + border-radius: 8px; + overflow-x: auto; +} + +/* 代码块内的 code 重置内联样式 */ +.markdown-body pre code { + background: none; + padding: 0; + border-radius: 0; + font-size: inherit; + white-space: pre; +} + +.markdown-body table { + width: 100%; + border-collapse: collapse; +} + +.markdown-body th, +.markdown-body td { + border: 1px solid var(--panel-border-10); + padding: 0.4em 0.8em; +} + +/* marked 通过 align 属性指定对齐,CSS 需显式支持 */ +.markdown-body th[align='left'], +.markdown-body td[align='left'] { text-align: left; } +.markdown-body th[align='center'], +.markdown-body td[align='center'] { text-align: center; } +.markdown-body th[align='right'], +.markdown-body td[align='right'] { text-align: right; } + +.markdown-body hr { + border-color: var(--panel-border-10); + margin: 0.8em 0; +} + +.markdown-body blockquote { + border-left: 3px solid var(--color-accent); + padding-left: 1em; + margin: 0.5em 0; + color: var(--text-muted); +} + +.markdown-body img { + max-width: 100%; + height: auto; + border-radius: 8px; +} + +.markdown-body del { + text-decoration: line-through; + opacity: 0.6; +} + +/* 任务列表:移除圆点 + 自定义勾选框 */ +.markdown-body li:has(input[type='checkbox']) { + list-style-type: none; +} + +.markdown-body input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + width: 1em; + height: 1em; + margin: 0 0.35em 0 0; + border: 1.5px solid var(--panel-border-subtle); + border-radius: 3px; + background: transparent; + vertical-align: middle; + position: relative; + cursor: default; +} + +.markdown-body input[type='checkbox']:checked { + background: var(--color-accent); + border-color: var(--color-accent); +} + +.markdown-body input[type='checkbox']:checked::after { + content: ''; + position: absolute; + left: 3px; + top: 1px; + width: 4px; + height: 7px; + border: solid var(--text-primary); + border-width: 0 1.5px 1.5px 0; + transform: rotate(45deg); +} diff --git a/app/assets/css/mobile-admin.css b/app/assets/css/mobile-admin.css new file mode 100644 index 0000000..daaec4a --- /dev/null +++ b/app/assets/css/mobile-admin.css @@ -0,0 +1,455 @@ +/* 移动端管理界面优化样式 */ + +/* 确保打印时不受移动端样式影响 */ +@media print { + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + .schedule-item-print, + .print-page, + .schedule-content { + width: 100% !important; + max-width: none !important; + box-sizing: border-box !important; + } +} + +/* 通用移动端表单优化 */ +@media screen and (max-width: 768px) { + /* 模态框移动端优化 */ + .modal-overlay { + padding: 0; + align-items: flex-end; + } + + .modal-content { + width: 100% !important; + max-width: none !important; + margin: 0 !important; + border-radius: 16px 16px 0 0 !important; + max-height: 90vh; + overflow-y: auto; + } + + .modal-header { + padding: 16px 20px !important; + position: sticky; + top: 0; + background: inherit; + z-index: 1; + } + + .modal-header h3 { + font-size: 18px !important; + } + + .close-btn { + width: 36px !important; + height: 36px !important; + } + + .modal-body { + padding: 20px !important; + } + + .modal-footer { + padding: 16px 20px !important; + position: sticky; + bottom: 0; + background: inherit; + border-top: 1px solid var(--border-secondary) !important; + flex-direction: column !important; + gap: 12px !important; + } + + /* 表单元素移动端优化 */ + .form-group { + margin-bottom: 20px !important; + } + + .form-group label { + font-size: 15px !important; + margin-bottom: 8px !important; + } + + .form-input, + .form-select, + .form-textarea { + padding: 14px 16px !important; + font-size: 16px !important; /* 防止iOS缩放 */ + border-radius: 10px !important; + min-height: 44px !important; /* 触摸友好的最小高度 */ + box-sizing: border-box !important; + } + + .form-textarea { + min-height: 120px !important; + resize: vertical; + } + + .form-row { + grid-template-columns: 1fr !important; + gap: 20px !important; + } + + /* 按钮移动端优化 */ + .btn-primary, + .btn-secondary, + .btn-danger, + .btn-warning { + padding: 14px 20px !important; + font-size: 16px !important; + min-height: 48px !important; + border-radius: 10px !important; + width: 100% !important; + box-sizing: border-box !important; + } + + .modal-footer .btn-primary, + .modal-footer .btn-secondary { + order: 1; + } + + .modal-footer .btn-secondary { + order: 2; + } + + /* 搜索输入框优化 */ + .search-input { + padding: 12px 16px !important; + font-size: 16px !important; + border-radius: 10px !important; + min-height: 44px !important; + } + + /* 工具栏按钮优化 */ + .create-btn, + .refresh-btn, + .upload-btn { + padding: 12px 16px !important; + font-size: 15px !important; + min-height: 44px !important; + border-radius: 10px !important; + } + + /* 操作按钮优化 */ + .action-btn { + width: 40px !important; + height: 40px !important; + border-radius: 8px !important; + } + + .action-btn svg { + width: 18px !important; + height: 18px !important; + } + + /* 分页按钮优化 */ + .page-btn { + padding: 12px 16px !important; + font-size: 15px !important; + min-height: 44px !important; + border-radius: 8px !important; + } + + /* 选择框优化 */ + .checkbox, + .radio { + width: 20px !important; + height: 20px !important; + margin: 0 !important; + } + + /* 标签和徽章优化 */ + .role-badge, + .status-badge, + .permission-tag { + padding: 6px 10px !important; + font-size: 12px !important; + border-radius: 6px !important; + } + + /* 错误消息优化 */ + .error-message { + padding: 14px 16px !important; + font-size: 15px !important; + border-radius: 10px !important; + margin-top: 12px !important; + } + + /* 加载状态优化 */ + .loading-overlay { + border-radius: 12px !important; + } + + .spinner { + width: 32px !important; + height: 32px !important; + } + + .loading-text { + font-size: 15px !important; + margin-top: 12px !important; + } +} + +/* 小屏幕设备进一步优化 */ +@media screen and (max-width: 480px) { + .modal-content { + border-radius: 12px 12px 0 0 !important; + } + + .modal-header { + padding: 12px 16px !important; + } + + .modal-header h3 { + font-size: 16px !important; + } + + .close-btn { + width: 32px !important; + height: 32px !important; + } + + .modal-body { + padding: 16px !important; + } + + .modal-footer { + padding: 12px 16px !important; + } + + .form-group { + margin-bottom: 16px !important; + } + + .form-group label { + font-size: 14px !important; + } + + .form-input, + .form-select, + .form-textarea { + padding: 12px 14px !important; + font-size: 16px !important; + min-height: 40px !important; + } + + .btn-primary, + .btn-secondary, + .btn-danger, + .btn-warning { + padding: 12px 16px !important; + font-size: 15px !important; + min-height: 44px !important; + } + + .action-btn { + width: 36px !important; + height: 36px !important; + } + + .action-btn svg { + width: 16px !important; + height: 16px !important; + } +} + +/* 触摸设备特殊优化 */ +@media (hover: none) and (pointer: coarse) { + /* 增加触摸目标大小 */ + .nav-item, + .action-btn, + .page-btn, + .close-btn { + min-width: 44px !important; + min-height: 44px !important; + } + + /* 移除悬停效果 */ + .nav-item:hover, + .action-btn:hover, + .page-btn:hover, + .close-btn:hover { + transform: none !important; + } + + /* 添加触摸反馈 */ + .nav-item:active, + .action-btn:active, + .page-btn:active, + .close-btn:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } +} + +/* 横屏模式优化 */ +@media screen and (max-width: 768px) and (orientation: landscape) { + .modal-content { + max-height: 85vh !important; + } + + .modal-header { + padding: 12px 20px !important; + } + + .modal-body { + padding: 16px 20px !important; + } + + .modal-footer { + padding: 12px 20px !important; + } +} + +/* 移动端专用交互优化 */ +@media screen and (max-width: 768px) { + /* 滑动手势支持 */ + .sidebar { + touch-action: pan-y; + } + + .table-container { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + } + + .activity-list, + .modal-body { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + } + + /* 触摸反馈 */ + .nav-item, + .action-btn, + .page-btn, + .close-btn, + .mobile-menu-btn, + .user-card, + .data-card, + .stat-card { + -webkit-tap-highlight-color: transparent; + tap-highlight-color: transparent; + } + + /* 防止双击缩放 */ + .nav-item, + .action-btn, + .page-btn, + .close-btn, + .mobile-menu-btn { + touch-action: manipulation; + } + + /* 长按菜单禁用 */ + .action-btn, + .page-btn, + .close-btn { + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + } + + /* 拖拽优化 */ + .user-card, + .data-card, + .role-card { + touch-action: pan-y; + } + + /* 输入框优化 */ + .form-input, + .form-select, + .form-textarea, + .search-input { + -webkit-appearance: none; + appearance: none; + touch-action: manipulation; + } + + /* IOS特定优化 */ + .form-input[type='text'], + .form-input[type='email'], + .form-input[type='password'], + .search-input { + -webkit-text-size-adjust: 100%; + } +} + +/* 触摸设备特殊交互 */ +@media (hover: none) and (pointer: coarse) { + /* 卡片点击效果 */ + .user-card:active, + .data-card:active, + .role-card:active, + .stat-card:active { + transform: scale(0.98); + transition: transform 0.1s ease; + } + + /* 按钮点击效果 */ + .nav-item:active, + .action-btn:active, + .page-btn:active, + .close-btn:active, + .mobile-menu-btn:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } + + /* 移除悬停效果 */ + .nav-item:hover, + .action-btn:hover, + .page-btn:hover, + .close-btn:hover, + .user-card:hover, + .data-card:hover, + .role-card:hover, + .stat-card:hover { + transform: none !important; + box-shadow: none !important; + } + + /* 焦点样式优化 */ + .form-input:focus, + .form-select:focus, + .form-textarea:focus, + .search-input:focus { + transform: none; + box-shadow: 0 0 0 3px var(--primary-20) !important; + } +} + +/* 下拉刷新支持 */ +@media screen and (max-width: 768px) { + .overview-dashboard, + .user-manager, + .role-manager { + overscroll-behavior-y: contain; + } +} + +/* 安全区域适配 */ +@supports (padding: max(0px)) { + @media screen and (max-width: 768px) { + .mobile-header { + padding-left: max(16px, env(safe-area-inset-left)); + padding-right: max(16px, env(safe-area-inset-right)); + padding-top: 0; + } + + .sidebar { + padding-left: max(0px, env(safe-area-inset-left)); + } + + .modal-content { + margin-bottom: max(0px, env(safe-area-inset-bottom)); + } + } +} diff --git a/app/assets/css/print-fix.css b/app/assets/css/print-fix.css new file mode 100644 index 0000000..8aa288d --- /dev/null +++ b/app/assets/css/print-fix.css @@ -0,0 +1,278 @@ +/* PDF导出宽度样式 */ + +@media print { + /* 重置所有可能影响宽度的样式 */ + * { + box-sizing: border-box !important; + } + + /* 强制覆盖移动端样式 */ + .schedule-item-print .item-content { + flex-direction: row !important; + align-items: center !important; + gap: 12px !important; + } + + .schedule-item-print .sequence-number { + align-self: center !important; + } + + .schedule-item-print .cover-section { + width: 40px !important; + height: 40px !important; + } + + .schedule-item-print .song-info { + width: auto !important; + flex: 1 !important; + } + + .schedule-item-print .requester-info, + .schedule-item-print .votes-info, + .schedule-item-print .playtime-info { + width: auto !important; + justify-content: flex-end !important; + flex-shrink: 0 !important; + } + + /* 确保页面容器宽度正确 */ + html, + body { + width: 100% !important; + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + /* 打印页面容器 - 固定合理宽度 */ + .print-page { + width: 210mm !important; + max-width: 210mm !important; + min-width: auto !important; + box-sizing: border-box !important; + margin: 0 auto !important; + padding: 20mm !important; + background: white !important; + color: black !important; + } + + /* 排期内容容器 */ + .schedule-content { + width: 100% !important; + max-width: 170mm !important; + box-sizing: border-box !important; + } + + /* 排期项目 */ + .schedule-item-print { + width: 100% !important; + max-width: 170mm !important; + box-sizing: border-box !important; + margin-bottom: 8px !important; + page-break-inside: avoid !important; + } + + /* 排期项目内容 */ + .schedule-item-print .item-content { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + width: 100% !important; + gap: 12px !important; + padding: 8px !important; + box-sizing: border-box !important; + } + + /* 序号 */ + .schedule-item-print .sequence-number { + width: 30px !important; + height: 30px !important; + flex-shrink: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: #f0f0f0 !important; + border-radius: 50% !important; + font-weight: bold !important; + font-size: 14px !important; + } + + /* 封面 */ + .schedule-item-print .cover-section { + width: 40px !important; + height: 40px !important; + flex-shrink: 0 !important; + } + + .schedule-item-print .song-cover, + .schedule-item-print .cover-placeholder { + width: 100% !important; + height: 100% !important; + border-radius: 4px !important; + } + + /* 歌曲信息 */ + .schedule-item-print .song-info { + flex: 1 !important; + min-width: 0 !important; + margin-right: 12px !important; + } + + .schedule-item-print .song-title { + font-size: 16px !important; + font-weight: 600 !important; + color: #000 !important; + margin-bottom: 2px !important; + white-space: nowrap !important; + } + + /* 歌名文本块承载裁剪,flex 容器上的 text-overflow 不生效 */ + .schedule-item-print .title-text { + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + + .schedule-item-print .song-artist { + font-size: 14px !important; + color: #333 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + + /* 附加信息 */ + .schedule-item-print .requester-info, + .schedule-item-print .votes-info, + .schedule-item-print .playtime-info { + display: flex !important; + align-items: center !important; + font-size: 12px !important; + color: #666 !important; + white-space: nowrap !important; + flex-shrink: 0 !important; + margin-left: 8px !important; + } + + .schedule-item-print .label { + font-weight: 500 !important; + margin-right: 4px !important; + } + + .schedule-item-print .value { + color: #333 !important; + } + + /* 页面头部和尾部 */ + .page-header, + .page-footer { + width: 100% !important; + box-sizing: border-box !important; + color: #000 !important; + } + + /* 学校Logo */ + .school-logo-print { + object-fit: contain !important; + } + + .logo { + object-fit: contain !important; + } + + .page-header { + text-align: center !important; + margin-bottom: 20px !important; + padding-bottom: 10px !important; + border-bottom: 2px solid #000 !important; + } + + .page-footer { + text-align: center !important; + margin-top: 20px !important; + padding-top: 10px !important; + border-top: 1px solid #ccc !important; + font-size: 12px !important; + color: #666 !important; + } + + /* 隐藏不需要打印的元素 */ + .settings-panel, + .preview-header, + .mobile-header, + .sidebar, + .mobile-overlay, + .back-to-top-btn { + display: none !important; + } + + /* 确保主内容区域正确显示 */ + .main-content { + margin-left: 0 !important; + padding: 0 !important; + width: 100% !important; + } + + /* 移除所有阴影和边框效果 */ + * { + box-shadow: none !important; + text-shadow: none !important; + } + + /* 确保文字颜色正确 */ + * { + color: inherit !important; + } + + /* 页面分页控制 */ + .print-page { + page-break-after: always !important; + } + + .print-page:last-child { + page-break-after: auto !important; + } + + /* 防止内容被截断 */ + .schedule-item-print { + break-inside: avoid !important; + page-break-inside: avoid !important; + } + + /* 优化分页效果 */ + .page-header { + page-break-after: avoid !important; + } + + .page-footer { + page-break-before: avoid !important; + } + + /* 确保歌曲信息完整显示 */ + .schedule-item-print .item-content { + page-break-inside: avoid !important; + } + + /* 防止封面图片被截断 */ + .schedule-item-print .cover-section { + page-break-inside: avoid !important; + } + + /* 防止歌曲标题和艺术家信息被截断 */ + .schedule-item-print .song-info { + page-break-inside: avoid !important; + } + + /* 防止附加信息被截断 */ + .schedule-item-print .requester-info, + .schedule-item-print .votes-info, + .schedule-item-print .playtime-info { + page-break-inside: avoid !important; + } +} + +/* 专门针对PDF导出的样式 */ +@page { + margin: 20mm; + size: A4; +} diff --git a/app/assets/css/sf-pro-icons.css b/app/assets/css/sf-pro-icons.css new file mode 100644 index 0000000..81fe09b --- /dev/null +++ b/app/assets/css/sf-pro-icons.css @@ -0,0 +1,38 @@ +/* SF Pro 字体导入 */ +@import url('https://cdn.jsdelivr.net/npm/sf-font@1.0.0/stylesheet.min.css'); + +/* SF Symbols 图标类 */ +.sf-symbol { + font-family: + 'SF Pro Icons', + 'SF Symbols', + -apple-system, + BlinkMacSystemFont, + sans-serif; + font-weight: 500; + font-style: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sf-symbol.semibold { + font-weight: 600; +} + +/* SF Pro 文本类 */ +.sf-pro { + font-family: + 'SF Pro', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Helvetica, + Arial, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/app/assets/css/theme-protection.css b/app/assets/css/theme-protection.css new file mode 100644 index 0000000..08d45c0 --- /dev/null +++ b/app/assets/css/theme-protection.css @@ -0,0 +1,79 @@ +/* 温和的浏览器主题保护样式 */ +/* 只保护关键元素,不干扰正常的CSS */ + +/* 全局颜色方案声明 */ +:root { + color-scheme: light dark; +} + +/* 只保护表单元素的外观 */ +input[type='text'], +input[type='email'], +input[type='password'], +input[type='search'], +input[type='url'], +input[type='tel'], +input[type='number'], +input[type='date'], +input[type='time'], +input[type='datetime-local'], +textarea, +select { + /* 禁用浏览器默认样式 */ + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + /* 禁用浏览器的颜色方案自动调整 */ + color-scheme: none; + -webkit-color-scheme: none; +} + +/* 保护按钮外观 */ +button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +/* 保护选中文本颜色 */ +::selection { + background-color: var(--selection-bg, rgba(79, 70, 229, 0.3)); + color: inherit; +} + +::-moz-selection { + background-color: var(--selection-bg, rgba(79, 70, 229, 0.3)); + color: inherit; +} + +/* 保护占位符颜色 */ +::placeholder { + opacity: 0.6; +} + +/* 在深色模式偏好下保护表单元素 */ +@media (prefers-color-scheme: dark) { + input[type='text'], + input[type='email'], + input[type='password'], + input[type='search'], + input[type='url'], + input[type='tel'], + input[type='number'], + textarea, + select { + color-scheme: none; + -webkit-color-scheme: none; + } +} + +/* 高对比度模式下的调整 */ +@media (prefers-contrast: high) { + input, + textarea, + select, + button { + border-width: 2px; + } +} diff --git a/app/assets/css/themes/ClassicDark.css b/app/assets/css/themes/ClassicDark.css new file mode 100644 index 0000000..1ff7c3d --- /dev/null +++ b/app/assets/css/themes/ClassicDark.css @@ -0,0 +1,841 @@ +/* 经典深色主题 — 全部设计变量 */ +:root[data-theme='ClassicDark'] { + /* 主色调 */ + --primary: #3b82f6; + --primary-hover: #2563eb; + --primary-light: rgba(59, 130, 246, 0.1); + --primary-border: rgba(59, 130, 246, 0.2); + /* 控件强调色(与 --primary 一致,供 accent-color 使用) */ + --accent-color: #3b82f6; + /* 主色光晕(供阴影 glow 使用) */ + --primary-glow: rgba(59, 130, 246, 0.15); + --primary-glow-5: rgba(59, 130, 246, 0.05); + --primary-glow-10: rgba(59, 130, 246, 0.1); + --primary-glow-20: rgba(59, 130, 246, 0.15); + --primary-glow-40: rgba(59, 130, 246, 0.3); + /* 成功色光晕 */ + --success-glow: rgba(16, 185, 129, 0.15); + --success-glow-5: rgba(16, 185, 129, 0.05); + --success-glow-10: rgba(16, 185, 129, 0.1); + --success-glow-20: rgba(16, 185, 129, 0.15); + /* 警告色光晕 */ + --warning-glow: rgba(245, 158, 11, 0.15); + --warning-glow-5: rgba(245, 158, 11, 0.05); + --warning-glow-10: rgba(245, 158, 11, 0.1); + --warning-glow-20: rgba(245, 158, 11, 0.15); + /* 错误色光晕 */ + --error-glow: rgba(239, 68, 68, 0.15); + --error-glow-5: rgba(239, 68, 68, 0.05); + --error-glow-10: rgba(239, 68, 68, 0.1); + --error-glow-20: rgba(239, 68, 68, 0.15); + /* 成功色 */ + --success: #10b981; + --success-hover: #059669; + --success-light: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.2); + /* 警告色 */ + --warning: #f59e0b; + --warning-hover: #d97706; + --warning-light: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.2); + /* 错误色 */ + --error: #ef4444; + --error-hover: #dc2626; + --error-light: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.2); + /* 信息色光晕 */ + --info: #3b82f6; + --info-hover: #2563eb; + --info-light: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.2); + /* 信息色光晕(蓝色系,与 info 品牌色一致) */ + --info-glow: rgba(59, 130, 246, 0.15); + --info-glow-5: rgba(59, 130, 246, 0.05); + --info-glow-10: rgba(59, 130, 246, 0.1); + --info-glow-20: rgba(59, 130, 246, 0.15); + /* 背景色 */ + --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; + --border-color: var(--border-quaternary); + /* 阴影 */ + --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); + /* 圆角 */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-2xl: 16px; + /* 间距 */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; + /* 字体大小 */ + --text-xs: 12px; + --text-sm: 14px; + --text-base: 16px; + --text-lg: 18px; + --text-xl: 20px; + --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; + --z-modal-backdrop: 400; + --z-modal: 500; + --z-popover: 600; + --z-tooltip: 700; + --z-toast: 800; + --z-app-loader: 9000; + /* 特殊状态颜色 */ + --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); + /* 投票相关颜色 */ + --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); + /* 卡片颜色 */ + --card-bg: var(--bg-secondary); + --card-border: var(--border-secondary); + --card-hover-border: var(--border-tertiary); + --card-shadow: var(--shadow-md); + /* 按钮颜色 */ + --btn-primary-bg: var(--primary); + --btn-primary-hover: var(--primary-hover); + --btn-primary-text: var(--text-primary); + --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-success-bg: var(--success); + --btn-success-hover: var(--success-hover); + --btn-success-text: var(--text-primary); + --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-border: var(--warning-border); + + --btn-error-bg: var(--error); + --btn-error-hover: var(--error-hover); + --btn-error-text: var(--text-primary); + --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); + /* 模态框颜色 */ + --modal-backdrop: rgba(0, 0, 0, 0.7); + --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); + + --notification-warning-bg: var(--warning-light); + --notification-warning-border: var(--warning-border); + --notification-warning-text: var(--warning); + + --notification-error-bg: var(--error-light); + --notification-error-border: var(--error-border); + --notification-error-text: var(--error); + + --notification-info-bg: var(--info-light); + --notification-info-border: var(--info-border); + --notification-info-text: var(--info); + /* 加载动画颜色 */ + --spinner-primary: var(--primary); + --spinner-secondary: var(--bg-tertiary); + /* 滚动条颜色 */ + --scrollbar-track: var(--bg-tertiary); + --scrollbar-thumb: var(--bg-quaternary); + --scrollbar-thumb-hover: #4a4a4a; + /* 用途化变量 — 品牌色(已语义化:blue=通用强调色, red=错误, green=成功, yellow=警告) */ + --color-accent: #0b5afe; + --color-accent-light: #3b82f6; + --color-accent-hover: #2563eb; + --color-accent-light-hover: #1d4ed8; + --color-accent-lighter: #60a5fa; + --color-accent-glow: rgba(11, 90, 254, 0.4); + --color-error: #ef4444; + --color-error-light: #f87171; + --color-error-hover: #dc2626; + --color-error-glow: rgba(239, 68, 68, 0.4); + --color-success: #10b981; + --color-success-light: #4ade80; + --color-success-hover: #22c55e; + --color-success-glow: rgba(34, 197, 94, 0.4); + --color-warning: #f59e0b; + --color-warning-light: #fbbf24; + --color-warning-hover: #d97706; + --color-warning-glow: rgba(245, 158, 11, 0.4); + /* --color-warning 透明度变体 */ + --color-warning-alpha-14: rgba(245, 158, 11, 0.14); + --color-warning-alpha-24: rgba(245, 158, 11, 0.24); + --color-warning-alpha-48: rgba(245, 158, 11, 0.48); + --color-orange: #fb923c; + --color-orange-light: #fdba74; + --color-orange-hover: #ff6900; + --color-orange-glow: rgba(251, 146, 60, 0.4); + + /* === Alpha (--color-accent) === + 以下为 --color-accent 的各透明度变体(alpha-前缀 + 透明度百分比)=== + --color-accent-alpha-10 ~ --color-accent-alpha-80 对应原 --color-accent-* 系列 === */ + --color-accent-alpha-10: rgba(11, 90, 254, 0.08); + --color-accent-alpha-12: rgba(11, 90, 254, 0.12); + --color-accent-alpha-15: rgba(11, 90, 254, 0.15); + --color-accent-alpha-18: rgba(11, 90, 254, 0.18); + --color-accent-alpha-20: rgba(11, 90, 254, 0.16); + --color-accent-alpha-22: rgba(11, 90, 254, 0.22); + --color-accent-alpha-25: rgba(11, 90, 254, 0.25); + --color-accent-alpha-30: rgba(11, 90, 254, 0.25); + --color-accent-alpha-40: rgba(11, 90, 254, 0.3); + --color-accent-alpha-50: rgba(11, 90, 254, 0.4); + --color-accent-alpha-60: rgba(11, 90, 254, 0.5); + --color-accent-alpha-65: rgba(11, 90, 254, 0.65); + --color-accent-alpha-80: rgba(11, 90, 254, 0.6); + + /* 用途化变量 — 通知/状态/场景语义色(用途命名,替代原 --color-brand-* 颜色命名) + ═══════════════════════════════════════════════════════════════ + 新增 --color-accent-alpha-* / --color-collab / --color-indigo 等语义变量后, + 以下变量供全局 CSS / Vue 组件统一引用,避免直接在业务代码中硬写颜色值。 === */ + + /* ─── 通知图标颜色(index.vue 通知列表) ─── */ + --color-notif-info: var(--color-accent); + --color-notif-success: var(--color-success); + --color-notif-error: var(--color-error); + --color-notif-warning: var(--color-warning); + --color-notif-checked: var(--color-accent); + --color-notif-collab: var(--color-collab); + --color-notif-indigo: var(--color-indigo); + /* ─── 通知容器/Toast 语义色(Notification.vue / NotificationContainer.vue) ─── */ + --color-toast-success: var(--color-success); + --color-toast-error: var(--color-error); + --color-toast-info-bg: var(--color-accent-alpha-20); + --color-toast-info-text: var(--color-accent); + /* ─── 通用 UI 组件语义色 ─── */ + --color-checked: var(--color-accent); + /* 勾选框选中颜色 */ + --color-search-focus: var(--color-accent); + /* 搜索框 focus 边框 */ + --color-search-btn-bg: var(--color-accent); + /* 搜索按钮背景 */ + --color-progress-fill: var(--color-accent); + /* 进度条填充色 */ + --color-spinner-border: var(--color-accent); + /* 加载指示器边框 */ + --color-tab-active: var(--color-accent); + /* Tab 激活下边框 */ + --color-page-btn-active: var(--color-accent); + /* 分页按钮激活态 */ + --color-checkbox-checked: var(--color-accent-light); + /* 勾选框选中态背景 */ + --color-checkbox-checked-border: var(--color-accent-light); + --color-mobile-page-input: var(--color-accent); + /* 移动端页码输入框 focus */ + --color-page-nav-hover: var(--color-accent-light); + /* 翻页按钮 hover 边框 */ + --color-visibility-public: var(--color-accent-light); + /* 歌曲可见性"公开"标签 */ + --color-text-cover-bg: var(--color-accent); + /* 文字封面背景 */ + --color-like-btn-bg: var(--color-accent); + /* 点赞按钮背景 */ + --color-replay-cancel-bg: var(--color-accent); + /* 重播取消按钮背景 */ + --color-semester-toggle: var(--color-accent); + /* 学期切换按钮文字 */ + --color-admin-badge: var(--color-accent-light); + /* 管理员徽章文字 */ + --color-error-text: var(--color-error); + /* 错误文本 */ + --color-pending-tag: var(--color-warning-light); + /* 等待中标签 */ + /* 首页标题分隔线 */ + --title-divider-bg: var(--color-accent); + --like-btn-bg: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent) 100%); + --like-btn-hover-bg: linear-gradient(180deg, var(--color-accent-hover) 0%, var(--color-accent) 100%); + --like-btn-border: var(--overlay-16); + --song-card-border: var(--overlay-6); + --song-card-bg: var(--overlay-7); + --song-card-footer-bg: var(--overlay-2); + --song-card-divider: var(--overlay-4); + --chip-bg: var(--overlay-4); + --chip-bg-hover: var(--overlay-8); + --chip-border: transparent; + --like-btn-bg-mobile: var(--overlay-5); + --like-btn-border-mobile: var(--overlay-10); + --withdraw-btn-bg-mobile: var(--overlay-8); + --section-tab-bg: var(--panel-bg-overlay); + --section-tab-border: var(--panel-bg-raised); + --surface-subtle: rgba(255, 255, 255, 0.03); + --surface-card-bg: rgba(0, 0, 0, 0.4); + --surface-card-bg-medium: rgba(0, 0, 0, 0.3); + --surface-card-bg-soft: rgba(0, 0, 0, 0.2); + /* ─── OAuth 第三方平台独立品牌色(用途命名 --oauth-*,不再复用通用语义色)─── + 每个平台有专属颜色变量,语义清晰,互不干扰。 + 旧 --color-oauth-* 保留向后兼容,但新增 --oauth-* 系列为主用。 === */ + --oauth-qq: #22d3ee; + --oauth-wx: #10b981; + --oauth-alipay: #0b5afe; + --oauth-sina: #dc2626; + --oauth-baidu: #6366f1; + --oauth-douyin: #fe2c55; + --oauth-huawei: #dc2626; + --oauth-xiaomi: #ff6900; + --oauth-gitee: #dc2626; + --oauth-bilibili: #06b6d4; + --oauth-kuaishou: #ff4906; + --oauth-kuaishou-orange: var(--oauth-kuaishou); + --oauth-google-blue: #4285f4; + --oauth-google-red: #ea4335; + --oauth-google-yellow: #fbbc05; + --oauth-google-green: #34a853; + --oauth-casdoor: #67c23a; + --oauth-casdoor-green: var(--oauth-casdoor); + /* Gitea 品牌路径的镂空色(供 Icon.vue SVG path 使用) */ + --gitea-cutout: var(--panel-bg-dialog); + /* ─── Auth/OAuth 品牌色(旧命名,向后兼容,指向新 --oauth-* 系列)─── */ + --color-oauth-qq: var(--oauth-qq); + --color-oauth-wx: var(--oauth-wx); + --color-oauth-alipay: var(--oauth-alipay); + --color-oauth-sina: var(--oauth-sina); + --color-oauth-baidu: var(--oauth-baidu); + --color-oauth-douyin: var(--oauth-douyin); + --color-oauth-huawei: var(--oauth-huawei); + --color-oauth-xiaomi: var(--oauth-xiaomi); + --color-oauth-gitee: var(--oauth-gitee); + --color-oauth-bilibili: var(--oauth-bilibili); + --color-oauth-kuaishou: var(--oauth-kuaishou); + --requestform-audio-match-hover-border: rgba(96, 165, 250, 0.35); + --requestform-audio-match-hover-border-light: rgba(96, 165, 250, 0.22); + --requestform-qq-login-shadow: rgba(6, 182, 212, 0.15); + --requestform-qq-login-shadow-hover: rgba(6, 182, 212, 0.25); + --requestform-user-icon-bg: rgba(6, 182, 212, 0.12); + --requestform-user-icon-border: rgba(6, 182, 212, 0.22); + --requestform-login-btn-hover-shadow: rgba(37, 99, 235, 0.35); + --requestform-login-btn-hover-shadow-lg: rgba(37, 99, 235, 0.45); + --requestform-like-btn-hover-shadow: rgba(220, 38, 38, 0.35); + --requestform-like-btn-hover-shadow-lg: rgba(220, 38, 38, 0.45); + --requestform-source-healthy-bg: rgba(34, 197, 94, 0.15); + --requestform-source-healthy-border: rgba(34, 197, 94, 0.3); + --requestform-source-healthy-glow: rgba(34, 197, 94, 0.6); + --requestform-source-checking-bg: rgba(251, 191, 36, 0.15); + --requestform-source-checking-border: rgba(251, 191, 36, 0.3); + --requestform-source-checking-glow: rgba(251, 191, 36, 0.6); + --requestform-modal-overlay-bg: rgba(20, 20, 25, 0.95); + --requestform-submit-btn-hover-shadow: rgba(0, 67, 248, 0.4); + --requestform-delete-btn-hover-shadow: rgba(236, 65, 65, 0.5); + --requestform-delete-btn-hover-shadow-sm: rgba(236, 65, 65, 0.3); + --requestform-replay-btn-bg: rgba(0, 117, 248, 0.1); + --requestform-replay-btn-bg-hover: rgba(0, 117, 248, 0.2); + --requestform-replay-btn-border: rgba(0, 117, 248, 0.3); + --requestform-replay-btn-border-hover: rgba(0, 117, 248, 0.5); + --requestform-mobile-chip-border: rgba(113, 113, 122, 0.45); + --requestform-mobile-chip-border-sm: rgba(113, 113, 122, 0.4); + --requestform-mobile-chip-bg: rgba(39, 39, 42, 0.75); + --requestform-mobile-chip-text: rgba(212, 212, 216, 0.9); + --requestform-mobile-button-text: rgba(228, 228, 231, 0.9); + --requestform-chip-required-bg: rgba(234, 179, 8, 0.1); + --requestform-chip-required-border: rgba(234, 179, 8, 0.35); + --requestform-chip-required-border-sm: rgba(234, 179, 8, 0.3); + --requestform-chip-valid-border: rgba(16, 185, 129, 0.35); + --requestform-chip-invalid-bg: rgba(248, 113, 113, 0.1); + --requestform-chip-invalid-border: rgba(248, 113, 113, 0.35); + /* 用途化变量 — 音频播放器(AudioPlayer) */ + --audio-player-bar-bg: rgba(20, 20, 25, 0.85); + --audio-player-progress-track-bg: rgba(74, 74, 74, 0.39); + --audio-player-quality-active-bg-start: rgba(0, 122, 255, 0.25); + --audio-player-quality-active-bg-mid: rgba(0, 122, 255, 0.15); + --audio-player-quality-active-bg-end: rgba(0, 122, 255, 0.1); + --audio-player-quality-active-border: rgba(0, 122, 255, 0.6); + --audio-player-quality-active-shadow-lg: rgba(0, 122, 255, 0.4); + --audio-player-quality-active-shadow: rgba(0, 122, 255, 0.3); + --audio-player-quality-active-ring: rgba(0, 122, 255, 0.2); + /* 用途化变量 — 音量控制(VolumeControl) */ + --volume-control-panel-bg: rgba(30, 30, 35, 0.85); + --volume-control-muted-color: rgba(255, 100, 100, 0.9); + /* 用途化变量 — 备份管理器(BackupManager) */ + --backup-manager-danger-bg: rgba(211, 47, 47, 0.1); + --backup-manager-file-remove-bg: rgba(211, 47, 47, 0.2); + --backup-manager-file-remove-bg-hover: rgba(211, 47, 47, 0.3); + --backup-manager-warning-box-bg: rgba(255, 152, 0, 0.1); + /* 用途化变量 — 用户歌曲模态框滚动条(UserSongsModal) */ + --usersongs-scroll-thumb: rgba(113, 113, 122, 0.3); + --usersongs-scroll-thumb-hover: rgba(113, 113, 122, 0.5); + /* 用途化变量 — 用户管理器(UserManager) */ + --user-manager-tree-line: rgba(63, 63, 70, 0.55); + --user-manager-tree-active-bg: rgba(37, 99, 235, 0.16); + /* 用途化变量 — 数据分析面板滚动条(DataAnalysisPanel) */ + --data-analysis-scroll-thumb: rgba(79, 70, 229, 0.3); + --data-analysis-scroll-thumb-hover: rgba(79, 70, 229, 0.5); + /* 用途化变量 — 歌曲列表(ScheduleList/SongList) */ + --schedule-list-divider: rgba(217, 217, 217, 0.5); + --schedule-list-title-shadow: rgba(0, 179, 248, 0.3); + --schedule-list-title-shadow-sm: rgba(0, 179, 248, 0.2); + /* 用途化变量 — 点歌券标识(CardCodeTag) */ + --card-code-bg: rgba(245, 158, 11, 0.1); + --card-code-border: rgba(245, 158, 11, 0.25); + --card-code-text: #fcd34d; + /* 用途化变量 — 认证模块(Auth) */ + --auth-error-input-shadow: rgba(244, 63, 94, 0.1); + /* 用途化变量 — 面板背景色(深色模式) */ + --panel-bg: #21242d; + --panel-bg-alt: #27272a; + --panel-bg-hover: #3f3f46; + --panel-bg-deep: #1a1a1a; + --panel-bg-deepest: #0a0a0a; + --panel-bg-overlay: #1a1d24; + --panel-bg-elevated: #1e1e24; + --panel-bg-dialog: #18181b; + --panel-bg-subtle: #141418; + --panel-bg-tier-tertiary: #1a1a2e; + --panel-bg-quaternary: #040e15; + --panel-bg-contrast: #2a2e38; + --panel-bg-dark: #111827; + --panel-bg-darker: #12121a; + --panel-bg-darkest: #0a0a0f; + --panel-bg-compact: #2a2a2a; + --panel-bg-flat: #2a2a2a; + --panel-bg-raised: #282830; + --panel-bg-inset: #1a1a20; + /* 面板背景色 alpha 变体 */ + --panel-bg-dark-40: rgba(17, 24, 39, 0.4); + --panel-bg-dark-50: rgba(17, 24, 39, 0.5); + --panel-bg-dark-80: rgba(17, 24, 39, 0.8); + --panel-bg-darkest-20: rgba(10, 10, 15, 0.2); + --panel-bg-darkest-30: rgba(10, 10, 15, 0.3); + --panel-bg-darkest-40: rgba(10, 10, 15, 0.4); + --panel-bg-darkest-50: rgba(10, 10, 15, 0.5); + --panel-bg-darkest-60: rgba(10, 10, 15, 0.6); + --panel-bg-darkest-80: rgba(10, 10, 15, 0.8); + --panel-bg-darkest-95: rgba(10, 10, 15, 0.95); + --panel-bg-deepest-30: rgba(10, 10, 10, 0.3); + --panel-bg-deepest-40: rgba(10, 10, 10, 0.4); + --panel-bg-deepest-50: rgba(10, 10, 10, 0.5); + --panel-bg-deepest-80: rgba(10, 10, 10, 0.8); + --panel-bg-deepest-95: rgba(10, 10, 10, 0.95); + --panel-bg-hover-50: rgba(63, 63, 70, 0.5); + /* 通用表面色(无 panel 前缀) */ + --panel-bg-secondary: #2a2a35; + --panel-bg-tertiary: #333340; + --table-row-bg: #1a1a25; + /* 用途化变量 — 文字灰度(深色模式) */ + --text-muted: #9ca3af; + --text-muted-light: #a1a1aa; + --text-muted-dark: #a0a0a0; + --text-tertiary-hover: #888; + --text-primary-light: #f4f4f5; + --text-primary-lighter: #e4e4e7; + --text-link: #60a5fa; + --text-link-hover: #93c5fd; + --text-highlight: #ffd700; + /* 用途化变量 — 边框与分割线(深色模式) */ + --panel-border: #2a2a2a; + --panel-border-active: #242f38; + --panel-border-subtle: #1f1f1f; + --panel-border-10: rgba(31, 31, 31, 0.1); + --panel-border-light: #3a3a3a; + --panel-border-dark: #4a4a4a; + --panel-border-highlight: #3f3f46; + --panel-border-brand: #0b5afe; + --panel-border-error: #ef4444; + /* 用途化变量 — 状态色(深色模式) */ + --status-info-bg: #040e15; + --status-info-border: #0b5afe; + --status-info-glow: rgba(11, 90, 254, 0.15); + --status-info-icon: #22d3ee; + --status-success-icon: #07c160; + --status-warning-icon: #fde047; + --status-error-icon: #ff6b6b; + --status-error-deep: #cf0a2c; + + /* 用途化变量 — 通知/状态语义色与场景渐变辅助色(替代原 --color-collab/pink/cyan/teal/indigo) + collab=协作, pink=年度回顾装饰, cyan=QQ/抖音, teal=QQ登录按钮, indigo=选中通知 === */ + --color-collab: #8b5cf6; + --color-collab-hover: #7c3aed; + --color-collab-light: #a78bfa; + --color-collab-lighter: #e9d5ff; + --color-collab-glow: rgba(139, 92, 246, 0.3); + --color-collab-10: rgba(139, 92, 246, 0.1); + --color-pink: #f472b6; + --color-pink-alpha-10: rgba(244, 114, 182, 0.1); + --color-pink-alpha-20: rgba(244, 114, 182, 0.2); + + /* === 语义化视觉覆盖层变量 === + --shadow-color / --shadow-color-deep : 卡片悬浮阴影 + --modal-overlay : 全屏弹窗遮罩 + --shimmer-overlay : 加载 shimmer 光效 */ + --shadow-color: rgba(0, 0, 0, 0.2); + --shadow-color-deep: rgba(0, 0, 0, 0.4); + --modal-overlay: rgba(0, 0, 0, 0.8); + --shimmer-overlay: rgba(255, 255, 255, 0.2); + + --color-cyan: #22d3ee; + --color-teal: #06b6d4; + --color-indigo: #6366f1; + --color-indigo-rgb: 99 102 241; + --color-indigo-hover: #4f46e5; + /* 选中文本背景色(供 ::selection 使用) */ + --selection-bg: rgba(139, 92, 246, 0.3); + + /* === WarpCanvas 星空配色(用途命名,替代原 --color-collab/indigo 语义歧义) === + 以下变量别名指向 --color-collab / --color-indigo,保持视觉一致性的同时语义更清晰 === */ + --warp-canvas-primary: var(--color-collab); + --warp-canvas-primary-light: var(--color-collab-light); + --warp-canvas-indigo: var(--color-indigo); + /* 用途化变量 — 滚动条与轨道 */ + --scrollbar-track-dark: #1a1a1f; + --scrollbar-track-hover: #27272a; + /* === Alpha (--primary) === */ + --primary-5: rgba(59, 130, 246, 0.05); + --primary-10: rgba(59, 130, 246, 0.1); + --primary-15: rgba(59, 130, 246, 0.15); + --primary-18: rgba(59, 130, 246, 0.18); + --primary-20: rgba(59, 130, 246, 0.2); + --primary-25: rgba(59, 130, 246, 0.25); + --primary-30: rgba(59, 130, 246, 0.3); + --primary-35: rgba(59, 130, 246, 0.35); + --primary-40: rgba(59, 130, 246, 0.4); + --primary-60: rgba(59, 130, 246, 0.6); + --primary-80: rgba(59, 130, 246, 0.8); + --primary-50: rgba(59, 130, 246, 0.5); + /* === Alpha (--primary-hover) === */ + --primary-hover-5: rgba(37, 99, 235, 0.05); + --primary-hover-10: rgba(37, 99, 235, 0.1); + --primary-hover-20: rgba(37, 99, 235, 0.2); + --primary-hover-30: rgba(37, 99, 235, 0.3); + /* === Alpha (--success) === */ + --success-5: rgba(16, 185, 129, 0.05); + --success-10: rgba(16, 185, 129, 0.1); + --success-20: rgba(16, 185, 129, 0.2); + --success-40: rgba(16, 185, 129, 0.4); + --success-50: rgba(16, 185, 129, 0.5); + --success-70: rgba(16, 185, 129, 0.7); + --success-80: rgba(16, 185, 129, 0.8); + --success-30: rgba(16, 185, 129, 0.3); + /* === Alpha (--success-hover) === */ + --success-hover-5: rgba(5, 150, 105, 0.05); + --success-hover-10: rgba(5, 150, 105, 0.1); + --success-hover-20: rgba(5, 150, 105, 0.2); + /* === Alpha (--warning) === */ + --warning-5: rgba(245, 158, 11, 0.05); + --warning-10: rgba(245, 158, 11, 0.1); + --warning-15: rgba(245, 158, 11, 0.15); + --warning-20: rgba(245, 158, 11, 0.2); + --warning-25: rgba(245, 158, 11, 0.25); + --warning-30: rgba(245, 158, 11, 0.3); + --warning-40: rgba(245, 158, 11, 0.4); + --warning-50: rgba(245, 158, 11, 0.5); + --warning-80: rgba(245, 158, 11, 0.8); + --warning-200: rgba(245, 158, 11, 0.2); + --warning-300: rgba(245, 158, 11, 0.3); + /* === Alpha (--warning-hover) === */ + --warning-hover-5: rgba(217, 119, 6, 0.05); + --warning-hover-10: rgba(217, 119, 6, 0.1); + --warning-hover-15: rgba(217, 119, 6, 0.15); + --warning-hover-20: rgba(217, 119, 6, 0.2); + --warning-hover-25: rgba(217, 119, 6, 0.25); + --warning-hover-30: rgba(217, 119, 6, 0.3); + --warning-hover-40: rgba(217, 119, 6, 0.4); + --warning-hover-50: rgba(217, 119, 6, 0.5); + /* === Alpha (--error) === */ + --error-5: rgba(239, 68, 68, 0.05); + --error-10: rgba(239, 68, 68, 0.1); + --error-15: rgba(239, 68, 68, 0.15); + --error-20: rgba(239, 68, 68, 0.2); + --error-30: rgba(239, 68, 68, 0.3); + --error-40: rgba(239, 68, 68, 0.4); + --error-50: rgba(239, 68, 68, 0.5); + --error-60: rgba(239, 68, 68, 0.6); + --error-70: rgba(239, 68, 68, 0.7); + --error-80: rgba(239, 68, 68, 0.8); + /* === Alpha (--error-hover) === */ + --error-hover-5: rgba(220, 38, 38, 0.05); + --error-hover-10: rgba(220, 38, 38, 0.1); + --error-hover-15: rgba(220, 38, 38, 0.15); + --error-hover-20: rgba(220, 38, 38, 0.2); + --error-hover-30: rgba(220, 38, 38, 0.3); + /* === Alpha (--info) === */ + --info-5: rgba(59, 130, 246, 0.05); + --info-10: rgba(59, 130, 246, 0.1); + --info-20: rgba(59, 130, 246, 0.2); + --info-30: rgba(59, 130, 246, 0.3); + --info-40: rgba(59, 130, 246, 0.4); + --info-50: rgba(59, 130, 246, 0.5); + /* === Alpha (--info-hover) === */ + --info-hover-5: rgba(37, 99, 235, 0.05); + --info-hover-10: rgba(37, 99, 235, 0.1); + --info-hover-20: rgba(37, 99, 235, 0.2); + --info-hover-30: rgba(37, 99, 235, 0.3); + --info-hover-40: rgba(37, 99, 235, 0.4); + --info-hover-50: rgba(37, 99, 235, 0.5); + /* === Alpha (--bg-primary) === */ + --bg-primary-5: rgba(17, 17, 17, 0.05); + --bg-primary-10: rgba(17, 17, 17, 0.1); + --bg-primary-20: rgba(17, 17, 17, 0.2); + --bg-primary-25: rgba(17, 17, 17, 0.25); + --bg-primary-30: rgba(17, 17, 17, 0.3); + --bg-primary-40: rgba(17, 17, 17, 0.4); + --bg-primary-45: rgba(17, 17, 17, 0.45); + --bg-primary-50: rgba(17, 17, 17, 0.5); + --bg-primary-60: rgba(17, 17, 17, 0.6); + --bg-primary-70: rgba(17, 17, 17, 0.7); + --bg-primary-80: rgba(17, 17, 17, 0.8); + --bg-primary-85: rgba(17, 17, 17, 0.85); + --bg-primary-90: rgba(17, 17, 17, 0.9); + --bg-primary-95: rgba(17, 17, 17, 0.95); + /* === Alpha (--bg-secondary) === */ + --bg-secondary-5: rgba(26, 26, 26, 0.05); + --bg-secondary-10: rgba(26, 26, 26, 0.1); + --bg-secondary-15: rgba(26, 26, 26, 0.15); + --bg-secondary-20: rgba(26, 26, 26, 0.2); + --bg-secondary-30: rgba(26, 26, 26, 0.3); + --bg-secondary-40: rgba(26, 26, 26, 0.4); + --bg-secondary-50: rgba(26, 26, 26, 0.5); + --bg-secondary-60: rgba(26, 26, 26, 0.6); + --bg-secondary-70: rgba(26, 26, 26, 0.7); + --bg-secondary-80: rgba(26, 26, 26, 0.8); + --bg-secondary-90: rgba(26, 26, 26, 0.9); + --bg-secondary-95: rgba(26, 26, 26, 0.95); + /* === Alpha (--bg-tertiary) === */ + --bg-tertiary-5: rgba(42, 42, 42, 0.05); + --bg-tertiary-10: rgba(42, 42, 42, 0.1); + --bg-tertiary-20: rgba(42, 42, 42, 0.2); + --bg-tertiary-30: rgba(42, 42, 42, 0.3); + --bg-tertiary-40: rgba(42, 42, 42, 0.4); + --bg-tertiary-50: rgba(42, 42, 42, 0.5); + --bg-tertiary-60: rgba(42, 42, 42, 0.6); + --bg-tertiary-70: rgba(42, 42, 42, 0.7); + --bg-tertiary-80: rgba(42, 42, 42, 0.8); + /* === Alpha (--bg-quaternary) === */ + --bg-quaternary-10: rgba(58, 58, 58, 0.1); + --bg-quaternary-50: rgba(58, 58, 58, 0.5); + /* === Alpha (--border-secondary) === */ + --border-secondary-30: rgba(42, 42, 42, 0.3); + --border-secondary-40: rgba(42, 42, 42, 0.4); + --border-secondary-50: rgba(42, 42, 42, 0.5); + --border-secondary-60: rgba(42, 42, 42, 0.6); + --border-secondary-70: rgba(42, 42, 42, 0.7); + --border-secondary-80: rgba(42, 42, 42, 0.8); + /* === Alpha (--border-tertiary) === */ + --border-tertiary-20: rgba(58, 58, 58, 0.2); + --border-tertiary-30: rgba(58, 58, 58, 0.3); + --border-tertiary-50: rgba(58, 58, 58, 0.5); + /* === Alpha (--text-primary) === */ + --text-primary-10: rgba(255, 255, 255, 0.1); + --text-primary-20: rgba(255, 255, 255, 0.2); + --text-primary-30: rgba(255, 255, 255, 0.3); + --text-primary-40: rgba(255, 255, 255, 0.4); + --text-primary-50: rgba(255, 255, 255, 0.5); + --text-primary-60: rgba(255, 255, 255, 0.6); + --text-primary-70: rgba(255, 255, 255, 0.7); + --text-primary-80: rgba(255, 255, 255, 0.8); + --text-primary-90: rgba(255, 255, 255, 0.9); + /* === Alpha (--text-error) === */ + --text-error-20: rgba(239, 68, 68, 0.2); + --text-error-50: rgba(239, 68, 68, 0.5); + --text-error-60: rgba(239, 68, 68, 0.6); + --text-error-70: rgba(239, 68, 68, 0.7); + --text-error-80: rgba(239, 68, 68, 0.8); + /* === 通用浅色文字(深色模式) === */ + --light: rgba(255, 255, 255, 0.95); + /* === Alpha (--text-success) === */ + --text-success-50: rgba(16, 185, 129, 0.5); + --text-success-70: rgba(16, 185, 129, 0.7); + --text-success-80: rgba(16, 185, 129, 0.8); + /* === Alpha (--text-warning) === */ + --text-warning-80: rgba(245, 158, 11, 0.8); + /* 用途化变量 — 首页(index) */ + --index-footer-bg: rgba(28, 28, 30, 0.9); + --index-visualizer-bg: rgba(30, 41, 59, 0.5); + --index-card-glow: rgba(139, 92, 246, 0.15); + --index-brand-badge-bg: rgba(139, 92, 246, 0.1); + --index-brand-badge-border: rgba(139, 92, 246, 0.2); + --index-main-icon-shadow: rgba(139, 92, 246, 0.5); + --index-pulsing-ring-shadow: rgba(0, 122, 255, 0.4); + /* 用途化变量 — 年度回顾(year-review) */ + --year-review-home-btn-shadow: rgba(139, 92, 246, 0.2); + --year-review-blob-1-bg: rgba(139, 92, 246, 0.4); + --year-review-blob-2-bg: rgba(244, 114, 182, 0.2); + --year-review-gradient-overlay-end: rgba(244, 114, 182, 0.15); + --year-review-stat-number-shadow: rgba(37, 99, 235, 0.5); + --year-review-artist-label-border: rgba(168, 85, 247, 0.3); + --year-review-artist-bg-blur: rgba(168, 85, 247, 0.2); + --year-review-stats-bg-gradient: rgba(30, 58, 138, 0.4); + --app-loading-brand-primary: #6366f1; + --app-loading-brand-secondary: #818cf8; + --app-loading-primary-glow: rgba(99, 102, 241, 0.42); + --app-loading-primary-glow-accelerating: rgba(99, 102, 241, 0.8); + --app-loading-secondary-glow: rgba(129, 140, 248, 0.28); + --app-loading-text-color: rgba(165, 180, 252, 0.78); + /* 用途化变量 — 模态框通用(Modal shared) */ + --lyrics-modal-enter-shadow: rgba(0, 0, 0, 0.4); + --lyrics-modal-bg-accent-blue: rgba(84, 130, 255, 0.32); + --lyrics-modal-bg-accent-pink: rgba(255, 88, 126, 0.25); + --lyrics-modal-badge-menu-bg: rgba(245, 245, 245, 0.9); + /* 全屏播放页:背景恒为模糊暗化封面,文字/图标统一高对比亮色 */ + --lyrics-modal-text: #ffffff; + --lyrics-modal-text-secondary: rgba(255, 255, 255, 0.88); + --lyrics-modal-text-muted: rgba(255, 255, 255, 0.66); + --lyrics-modal-surface: rgba(255, 255, 255, 0.1); + --lyrics-modal-surface-strong: rgba(255, 255, 255, 0.2); + --lyrics-modal-surface-border: rgba(255, 255, 255, 0.14); + --lyrics-modal-track: rgba(255, 255, 255, 0.22); + /* 无封面时的渐变兜底底色(保持深色沉浸感) */ + --lyrics-modal-bg-base-start: #141418; + --lyrics-modal-bg-base-end: #0a0a0a; + /* 音质菜单为浅色底,文字需固定深色 */ + --lyrics-modal-badge-menu-text: #1c1c21; + --lyrics-modal-badge-menu-active-bg: #ffffff; + --audio-player-dropdown-bg-fallback: rgba(128, 128, 128, 0.85); + --audio-player-liquid-glass-bg-fallback: rgba(128, 128, 128, 0.25); + /* === Overlay / 遮罩层半透明色(深色模式 = 白色半透明) === */ + --overlay-1: rgba(255, 255, 255, 0.01); + --overlay-2: rgba(255, 255, 255, 0.02); + --overlay-3: rgba(255, 255, 255, 0.03); + --overlay-4: rgba(255, 255, 255, 0.04); + --overlay-5: rgba(255, 255, 255, 0.05); + --overlay-6: rgba(255, 255, 255, 0.06); + --overlay-7: rgba(255, 255, 255, 0.07); + --overlay-8: rgba(255, 255, 255, 0.08); + --overlay-9: rgba(255, 255, 255, 0.09); + --overlay-10: rgba(255, 255, 255, 0.1); + --overlay-12: rgba(255, 255, 255, 0.12); + --overlay-14: rgba(255, 255, 255, 0.14); + --overlay-15: rgba(255, 255, 255, 0.15); + --overlay-16: rgba(255, 255, 255, 0.16); + --overlay-18: rgba(255, 255, 255, 0.18); + --overlay-20: rgba(255, 255, 255, 0.2); + --overlay-22: rgba(255, 255, 255, 0.22); + --overlay-25: rgba(255, 255, 255, 0.25); + --overlay-30: rgba(255, 255, 255, 0.3); + --overlay-35: rgba(255, 255, 255, 0.35); + --overlay-40: rgba(255, 255, 255, 0.4); + --overlay-42: rgba(255, 255, 255, 0.42); + --overlay-45: rgba(255, 255, 255, 0.45); + --overlay-50: rgba(255, 255, 255, 0.5); + --overlay-52: rgba(255, 255, 255, 0.52); + --overlay-55: rgba(255, 255, 255, 0.55); + --overlay-58: rgba(255, 255, 255, 0.58); + --overlay-60: rgba(255, 255, 255, 0.6); + --overlay-62: rgba(255, 255, 255, 0.62); + --overlay-70: rgba(255, 255, 255, 0.7); + --overlay-78: rgba(255, 255, 255, 0.78); + --overlay-80: rgba(255, 255, 255, 0.8); + --overlay-85: rgba(255, 255, 255, 0.85); + --overlay-86: rgba(255, 255, 255, 0.86); + --overlay-88: rgba(255, 255, 255, 0.88); + --overlay-90: rgba(255, 255, 255, 0.9); + --overlay-92: rgba(255, 255, 255, 0.92); + --overlay-95: rgba(255, 255, 255, 0.95); + /* === Mask / 暗色遮罩(深色模式 = 标准不透明度) === */ + --mask-2: rgba(0, 0, 0, 0.01); + --mask-4: rgba(0, 0, 0, 0.02); + --mask-5: rgba(0, 0, 0, 0.03); + --mask-6: rgba(0, 0, 0, 0.04); + --mask-8: rgba(0, 0, 0, 0.05); + --mask-10: rgba(0, 0, 0, 0.06); + --mask-15: rgba(0, 0, 0, 0.08); + --mask-18: rgba(0, 0, 0, 0.09); + --mask-20: rgba(0, 0, 0, 0.1); + --mask-25: rgba(0, 0, 0, 0.12); + --mask-30: rgba(0, 0, 0, 0.15); + --mask-35: rgba(0, 0, 0, 0.17); + --mask-40: rgba(0, 0, 0, 0.2); + --mask-45: rgba(0, 0, 0, 0.22); + --mask-50: rgba(0, 0, 0, 0.25); + --mask-60: rgba(0, 0, 0, 0.3); + --mask-70: rgba(0, 0, 0, 0.35); + --mask-75: rgba(0, 0, 0, 0.4); + --mask-80: rgba(0, 0, 0, 0.5); + --mask-85: rgba(0, 0, 0, 0.6); + --mask-95: rgba(0, 0, 0, 0.7); + /* 打印专用变量(覆盖主题配色,白底黑字) */ + --print-bg: #ffffff; + --print-bg-subtle: #f8f9fa; + --print-bg-subtler: #f3f4f6; + --print-bg-deep: #f0f0f0; + --print-bg-darker: #f5f5f5; + --print-text-primary: #1a1a1a; + --print-text-secondary: #3a3a3c; + --print-text-tertiary: #636366; + --print-border: #d1d5db; + --print-border-light: #e5e5ea; + --print-muted: #6c757d; + /* Year Review 专用变量 */ + --year-review-text-tertiary: #6b7280; + --year-review-text-yellow: #fde047; + --year-review-text-cyan: #67e8f9; + /* OAuth 第三方平台品牌色(--oauth-casdoor-green / --oauth-kuaishou-orange 已在独立变量区定义,此处省略) */ + /* === 用途化变量 — 背景纹理光晕(body::before) === */ + --bg-texture-warm-glow: transparent; + /* === 用途化变量 — 按钮阴影 === */ + --btn-primary-shadow: rgba(99, 102, 241, 0.2); + /* === 用途化变量 — Canvas 动画背景叠加色 === */ + --canvas-bg-overlay: rgba(7, 7, 9, 0.18); + /* === 用途化变量 — Canvas 动画光晕遮罩末端 === */ + --canvas-glow-overlay-end: rgba(245, 245, 247, 0.7); + /* === 用途化变量 — 歌词消息 fallback === */ + --lyric-msg-fallback-text: #ffffff; + /* === 用途化变量 — 模态框阴影 === */ + --modal-hover-shadow: rgba(0, 0, 0, 0.1); + /* === 用途化变量 — 移动端表格阴影 === */ + --mobile-table-shadow: rgba(0, 0, 0, 0.5); +} diff --git a/app/assets/css/themes/ClassicLight.css b/app/assets/css/themes/ClassicLight.css new file mode 100644 index 0000000..3ad839c --- /dev/null +++ b/app/assets/css/themes/ClassicLight.css @@ -0,0 +1,841 @@ +/* 经典浅色主题 — 全部设计变量 */ +:root[data-theme='ClassicLight'] { + /* 主色调 */ + --primary: #3b82f6; + --primary-hover: #2563eb; + --primary-light: rgba(59, 130, 246, 0.1); + --primary-border: rgba(59, 130, 246, 0.2); + /* 控件强调色(与 --primary 一致,供 accent-color 使用) */ + --accent-color: #3b82f6; + /* 主色光晕(供阴影 glow 使用) */ + --primary-glow: rgba(59, 130, 246, 0.15); + --primary-glow-5: rgba(59, 130, 246, 0.05); + --primary-glow-10: rgba(59, 130, 246, 0.1); + --primary-glow-20: rgba(59, 130, 246, 0.15); + --primary-glow-40: rgba(59, 130, 246, 0.3); + /* 成功色光晕 */ + --success-glow: rgba(16, 185, 129, 0.15); + --success-glow-5: rgba(16, 185, 129, 0.05); + --success-glow-10: rgba(16, 185, 129, 0.1); + --success-glow-20: rgba(16, 185, 129, 0.15); + /* 警告色光晕 */ + --warning-glow: rgba(245, 158, 11, 0.15); + --warning-glow-5: rgba(245, 158, 11, 0.05); + --warning-glow-10: rgba(245, 158, 11, 0.1); + --warning-glow-20: rgba(245, 158, 11, 0.15); + /* 错误色光晕 */ + --error-glow: rgba(239, 68, 68, 0.15); + --error-glow-5: rgba(239, 68, 68, 0.05); + --error-glow-10: rgba(239, 68, 68, 0.1); + --error-glow-20: rgba(239, 68, 68, 0.15); + /* 成功色 */ + --success: #10b981; + --success-hover: #059669; + --success-light: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.2); + /* 警告色 */ + --warning: #f59e0b; + --warning-hover: #d97706; + --warning-light: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.2); + /* 错误色 */ + --error: #ef4444; + --error-hover: #dc2626; + --error-light: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.2); + /* 信息色光晕 */ + --info: #3b82f6; + --info-hover: #1d4ed8; + --info-light: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.2); + /* 信息色光晕(蓝色系,与 info 品牌色一致) */ + --info-glow: rgba(59, 130, 246, 0.15); + --info-glow-5: rgba(59, 130, 246, 0.05); + --info-glow-10: rgba(59, 130, 246, 0.1); + --info-glow-20: rgba(59, 130, 246, 0.15); + /* 背景色 */ + --bg-primary: #fafafa; + --bg-secondary: #ffffff; + --bg-tertiary: #f0f0f0; + --bg-quaternary: #e5e5e5; + --bg-hover: #f5f5f5; + /* 文字颜色 */ + --text-primary: #18181b; + --text-secondary: #3f3f46; + --text-tertiary: #71717a; + --text-quaternary: #6b7280; + --text-disabled: #6b7280; + /* 边框颜色 */ + --border-primary: #ececf1; + --border-secondary: #dedee6; + --border-tertiary: #c9c9d4; + --border-quaternary: #a3a3a3; + --border-color: var(--border-quaternary); + /* 阴影 */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.02); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.03); + --shadow-lg: 0 8px 12px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 16px 20px rgba(0, 0, 0, 0.06); + /* 圆角 */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-2xl: 16px; + /* 间距 */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; + /* 字体大小 */ + --text-xs: 12px; + --text-sm: 14px; + --text-base: 16px; + --text-lg: 18px; + --text-xl: 20px; + --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; + --z-modal-backdrop: 400; + --z-modal: 500; + --z-popover: 600; + --z-tooltip: 700; + --z-toast: 800; + --z-app-loader: 9000; + /* 特殊状态颜色 */ + --status-pending: #f59e0b; + --status-pending-light: rgba(245, 158, 11, 0.1); + --status-approved: #10b981; + --status-approved-light: rgba(16, 185, 129, 0.1); + --status-played: #8e8e93; + --status-played-light: rgba(142, 142, 147, 0.1); + /* 投票相关颜色 */ + --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); + /* 卡片颜色 */ + --card-bg: var(--bg-secondary); + --card-border: var(--border-secondary); + --card-hover-border: var(--border-tertiary); + --card-shadow: var(--shadow-md); + /* 按钮颜色 */ + --btn-primary-bg: var(--primary); + --btn-primary-hover: var(--primary-hover); + --btn-primary-text: #ffffff; + --btn-primary-border: var(--primary-border); + + --btn-secondary-bg: #f5f5f5; + --btn-secondary-hover: #e5e5e5; + --btn-secondary-text: var(--text-primary); + --btn-secondary-border: #c9c9d4; + + --btn-success-bg: var(--success); + --btn-success-hover: var(--success-hover); + --btn-success-text: #ffffff; + --btn-success-border: var(--success-border); + + --btn-warning-bg: var(--warning); + --btn-warning-hover: var(--warning-hover); + --btn-warning-text: #ffffff; + --btn-warning-border: var(--warning-border); + + --btn-error-bg: var(--error); + --btn-error-hover: var(--error-hover); + --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); + /* 模态框颜色 */ + --modal-backdrop: rgba(0, 0, 0, 0.45); + --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); + + --notification-warning-bg: var(--warning-light); + --notification-warning-border: var(--warning-border); + --notification-warning-text: var(--warning); + + --notification-error-bg: var(--error-light); + --notification-error-border: var(--error-border); + --notification-error-text: var(--error); + + --notification-info-bg: var(--info-light); + --notification-info-border: var(--info-border); + --notification-info-text: var(--info); + /* 加载动画颜色 */ + --spinner-primary: var(--primary); + --spinner-secondary: var(--bg-tertiary); + /* 滚动条颜色 */ + --scrollbar-track: var(--bg-tertiary); + --scrollbar-thumb: var(--bg-quaternary); + --scrollbar-thumb-hover: #a3a3a3; + /* 用途化变量 — 品牌色(已语义化:blue=通用强调色, red=错误, green=成功, yellow=警告) */ + --color-accent: #0b5afe; + --color-accent-light: #3b82f6; + --color-accent-hover: #2563eb; + --color-accent-light-hover: #1d4ed8; + --color-accent-lighter: #60a5fa; + --color-accent-glow: rgba(11, 90, 254, 0.4); + --color-error: #ef4444; + --color-error-light: #f87171; + --color-error-hover: #dc2626; + --color-error-glow: rgba(239, 68, 68, 0.4); + --color-success: #10b981; + --color-success-light: #4ade80; + --color-success-hover: #22c55e; + --color-success-glow: rgba(34, 197, 94, 0.4); + --color-warning: #f59e0b; + --color-warning-light: #fbbf24; + --color-warning-hover: #d97706; + --color-warning-glow: rgba(245, 158, 11, 0.4); + /* --color-warning 透明度变体 */ + --color-warning-alpha-14: rgba(245, 158, 11, 0.14); + --color-warning-alpha-24: rgba(245, 158, 11, 0.24); + --color-warning-alpha-48: rgba(245, 158, 11, 0.48); + --color-orange: #fb923c; + --color-orange-light: #fdba74; + --color-orange-hover: #f97316; + --color-orange-glow: rgba(251, 146, 60, 0.4); + + /* === Alpha (--color-accent) === + 以下为 --color-accent 的各透明度变体(alpha-前缀 + 透明度百分比)=== + --color-accent-alpha-10 ~ --color-accent-alpha-80 对应原 --color-accent-* 系列 === */ + --color-accent-alpha-10: rgba(11, 90, 254, 0.08); + --color-accent-alpha-12: rgba(11, 90, 254, 0.12); + --color-accent-alpha-15: rgba(11, 90, 254, 0.15); + --color-accent-alpha-18: rgba(11, 90, 254, 0.18); + --color-accent-alpha-20: rgba(11, 90, 254, 0.16); + --color-accent-alpha-22: rgba(11, 90, 254, 0.22); + --color-accent-alpha-25: rgba(11, 90, 254, 0.25); + --color-accent-alpha-30: rgba(11, 90, 254, 0.25); + --color-accent-alpha-40: rgba(11, 90, 254, 0.3); + --color-accent-alpha-50: rgba(11, 90, 254, 0.4); + --color-accent-alpha-60: rgba(11, 90, 254, 0.5); + --color-accent-alpha-65: rgba(11, 90, 254, 0.65); + --color-accent-alpha-80: rgba(11, 90, 254, 0.6); + + /* 用途化变量 — 通知/状态/场景语义色(用途命名,替代原 --color-brand-* 颜色命名) + ═══════════════════════════════════════════════════════════════ + 新增 --color-accent-alpha-* / --color-collab / --color-indigo 等语义变量后, + 以下变量供全局 CSS / Vue 组件统一引用,避免直接在业务代码中硬写颜色值。 === */ + + /* ─── 通知图标颜色(index.vue 通知列表) ─── */ + --color-notif-info: var(--color-accent); + --color-notif-success: var(--color-success); + --color-notif-error: var(--color-error); + --color-notif-warning: var(--color-warning); + --color-notif-checked: var(--color-accent); + --color-notif-collab: var(--color-collab); + --color-notif-indigo: var(--color-indigo); + /* ─── 通知容器/Toast 语义色(Notification.vue / NotificationContainer.vue) ─── */ + --color-toast-success: var(--color-success); + --color-toast-error: var(--color-error); + --color-toast-info-bg: var(--color-accent-alpha-20); + --color-toast-info-text: var(--color-accent); + /* ─── 通用 UI 组件语义色 ─── */ + --color-checked: var(--color-accent); + /* 勾选框选中颜色 */ + --color-search-focus: var(--color-accent); + /* 搜索框 focus 边框 */ + --color-search-btn-bg: var(--color-accent); + /* 搜索按钮背景 */ + --color-progress-fill: var(--color-accent); + /* 进度条填充色 */ + --color-spinner-border: var(--color-accent); + /* 加载指示器边框 */ + --color-tab-active: var(--color-accent); + /* Tab 激活下边框 */ + --color-page-btn-active: var(--color-accent); + /* 分页按钮激活态 */ + --color-checkbox-checked: var(--color-accent-light); + /* 勾选框选中态背景 */ + --color-checkbox-checked-border: var(--color-accent-light); + --color-mobile-page-input: var(--color-accent); + /* 移动端页码输入框 focus */ + --color-page-nav-hover: var(--color-accent-light); + /* 翻页按钮 hover 边框 */ + --color-visibility-public: var(--color-accent-light); + /* 歌曲可见性"公开"标签 */ + --color-text-cover-bg: var(--color-accent); + /* 文字封面背景 */ + --color-like-btn-bg: var(--color-accent); + /* 点赞按钮背景 */ + --color-replay-cancel-bg: var(--color-accent); + /* 重播取消按钮背景 */ + --color-semester-toggle: var(--color-accent); + /* 学期切换按钮文字 */ + --color-admin-badge: var(--color-accent-light); + /* 管理员徽章文字 */ + --color-error-text: var(--color-error); + /* 错误文本 */ + --color-pending-tag: var(--color-warning-light); + /* 等待中标签 */ + /* 首页标题分隔线 */ + --title-divider-bg: #60a5fa; + --like-btn-bg: #ffffff; + --like-btn-hover-bg: rgba(11, 90, 254, 0.06); + --like-btn-border: rgba(11, 90, 254, 0.45); + --song-card-border: #cdd1d9; + --song-card-bg: #ffffff; + --song-card-footer-bg: #ffffff; + --song-card-divider: var(--panel-border-subtle); + --chip-bg: #ffffff; + --chip-bg-hover: var(--bg-hover); + --chip-border: var(--panel-border-highlight); + --like-btn-bg-mobile: #ffffff; + --like-btn-border-mobile: rgba(11, 90, 254, 0.45); + --withdraw-btn-bg-mobile: var(--error-light); + --section-tab-bg: #ffffff; + --section-tab-border: var(--song-card-border); + --surface-subtle: #ffffff; + --surface-card-bg: var(--mask-5); + --surface-card-bg-medium: var(--mask-30); + --surface-card-bg-soft: var(--mask-20); + /* ─── OAuth 第三方平台独立品牌色(用途命名 --oauth-*,不再复用通用语义色)─── + 每个平台有专属颜色变量,语义清晰,互不干扰。 + 旧 --color-oauth-* 保留向后兼容,但新增 --oauth-* 系列为主用。 === */ + --oauth-qq: #22d3ee; + --oauth-wx: #10b981; + --oauth-alipay: #0b5afe; + --oauth-sina: #dc2626; + --oauth-baidu: #6366f1; + --oauth-douyin: #fe2c55; + --oauth-huawei: #dc2626; + --oauth-xiaomi: #ff6900; + --oauth-gitee: #dc2626; + --oauth-bilibili: #06b6d4; + --oauth-kuaishou: #ff4906; + --oauth-kuaishou-orange: var(--oauth-kuaishou); + --oauth-google-blue: #4285f4; + --oauth-google-red: #ea4335; + --oauth-google-yellow: #fbbc05; + --oauth-google-green: #34a853; + --oauth-casdoor: #67c23a; + --oauth-casdoor-green: var(--oauth-casdoor); + /* Gitea 品牌路径的镂空色(供 Icon.vue SVG path 使用) */ + --gitea-cutout: var(--panel-bg-dialog); + /* ─── Auth/OAuth 品牌色(旧命名,向后兼容,指向新 --oauth-* 系列)─── */ + --color-oauth-qq: var(--oauth-qq); + --color-oauth-wx: var(--oauth-wx); + --color-oauth-alipay: var(--oauth-alipay); + --color-oauth-sina: var(--oauth-sina); + --color-oauth-baidu: var(--oauth-baidu); + --color-oauth-douyin: var(--oauth-douyin); + --color-oauth-huawei: var(--oauth-huawei); + --color-oauth-xiaomi: var(--oauth-xiaomi); + --color-oauth-gitee: var(--oauth-gitee); + --color-oauth-bilibili: var(--oauth-bilibili); + --color-oauth-kuaishou: var(--oauth-kuaishou); + --requestform-audio-match-hover-border: rgba(96, 165, 250, 0.35); + --requestform-audio-match-hover-border-light: rgba(96, 165, 250, 0.22); + --requestform-qq-login-shadow: rgba(6, 182, 212, 0.15); + --requestform-qq-login-shadow-hover: rgba(6, 182, 212, 0.25); + --requestform-user-icon-bg: rgba(6, 182, 212, 0.12); + --requestform-user-icon-border: rgba(6, 182, 212, 0.22); + --requestform-login-btn-hover-shadow: rgba(37, 99, 235, 0.35); + --requestform-login-btn-hover-shadow-lg: rgba(37, 99, 235, 0.45); + --requestform-like-btn-hover-shadow: rgba(220, 38, 38, 0.35); + --requestform-like-btn-hover-shadow-lg: rgba(220, 38, 38, 0.45); + --requestform-source-healthy-bg: rgba(34, 197, 94, 0.15); + --requestform-source-healthy-border: rgba(34, 197, 94, 0.3); + --requestform-source-healthy-glow: rgba(34, 197, 94, 0.6); + --requestform-source-checking-bg: rgba(251, 191, 36, 0.15); + --requestform-source-checking-border: rgba(251, 191, 36, 0.3); + --requestform-source-checking-glow: rgba(251, 191, 36, 0.6); + --requestform-modal-overlay-bg: rgba(20, 20, 25, 0.95); + --requestform-submit-btn-hover-shadow: rgba(0, 67, 248, 0.4); + --requestform-delete-btn-hover-shadow: rgba(236, 65, 65, 0.5); + --requestform-delete-btn-hover-shadow-sm: rgba(236, 65, 65, 0.3); + --requestform-replay-btn-bg: rgba(0, 117, 248, 0.1); + --requestform-replay-btn-bg-hover: rgba(0, 117, 248, 0.2); + --requestform-replay-btn-border: rgba(0, 117, 248, 0.3); + --requestform-replay-btn-border-hover: rgba(0, 117, 248, 0.5); + --requestform-mobile-chip-border: rgba(113, 113, 122, 0.45); + --requestform-mobile-chip-border-sm: rgba(113, 113, 122, 0.4); + --requestform-mobile-chip-bg: rgba(240, 240, 248, 0.75); + --requestform-mobile-chip-text: rgba(38, 38, 38, 0.9); + --requestform-mobile-button-text: rgba(38, 38, 38, 0.9); + --requestform-chip-required-bg: rgba(234, 179, 8, 0.1); + --requestform-chip-required-border: rgba(234, 179, 8, 0.35); + --requestform-chip-required-border-sm: rgba(234, 179, 8, 0.3); + --requestform-chip-valid-border: rgba(16, 185, 129, 0.35); + --requestform-chip-invalid-bg: rgba(248, 113, 113, 0.1); + --requestform-chip-invalid-border: rgba(248, 113, 113, 0.35); + /* 用途化变量 — 音频播放器(AudioPlayer) */ + --audio-player-bar-bg: rgba(255, 255, 255, 0.92); + --audio-player-progress-track-bg: rgba(74, 74, 74, 0.39); + --audio-player-quality-active-bg-start: rgba(0, 122, 255, 0.25); + --audio-player-quality-active-bg-mid: rgba(0, 122, 255, 0.15); + --audio-player-quality-active-bg-end: rgba(0, 122, 255, 0.1); + --audio-player-quality-active-border: rgba(0, 122, 255, 0.6); + --audio-player-quality-active-shadow-lg: rgba(0, 122, 255, 0.4); + --audio-player-quality-active-shadow: rgba(0, 122, 255, 0.3); + --audio-player-quality-active-ring: rgba(0, 122, 255, 0.2); + /* 用途化变量 — 音量控制(VolumeControl) */ + --volume-control-panel-bg: rgba(255, 255, 255, 0.92); + --volume-control-muted-color: rgba(255, 100, 100, 0.9); + /* 用途化变量 — 备份管理器(BackupManager) */ + --backup-manager-danger-bg: rgba(211, 47, 47, 0.1); + --backup-manager-file-remove-bg: rgba(211, 47, 47, 0.2); + --backup-manager-file-remove-bg-hover: rgba(211, 47, 47, 0.3); + --backup-manager-warning-box-bg: rgba(255, 152, 0, 0.1); + /* 用途化变量 — 用户歌曲模态框滚动条(UserSongsModal) */ + --usersongs-scroll-thumb: rgba(113, 113, 122, 0.3); + --usersongs-scroll-thumb-hover: rgba(113, 113, 122, 0.5); + /* 用途化变量 — 用户管理器(UserManager) */ + --user-manager-tree-line: rgba(63, 63, 70, 0.55); + --user-manager-tree-active-bg: rgba(37, 99, 235, 0.16); + /* 用途化变量 — 数据分析面板滚动条(DataAnalysisPanel) */ + --data-analysis-scroll-thumb: rgba(79, 70, 229, 0.3); + --data-analysis-scroll-thumb-hover: rgba(79, 70, 229, 0.5); + /* 用途化变量 — 歌曲列表(ScheduleList/SongList) */ + --schedule-list-divider: rgba(217, 217, 217, 0.5); + --schedule-list-title-shadow: rgba(0, 179, 248, 0.3); + --schedule-list-title-shadow-sm: rgba(0, 179, 248, 0.2); + /* 用途化变量 — 点歌券标识(CardCodeTag) */ + --card-code-bg: rgba(245, 158, 11, 0.1); + --card-code-border: rgba(245, 158, 11, 0.25); + --card-code-text: #92400e; + /* 用途化变量 — 认证模块(Auth) */ + --auth-error-input-shadow: rgba(244, 63, 94, 0.1); + /* 用途化变量 — 面板背景色(亮色模式) */ + --panel-bg: #ffffff; + --panel-bg-alt: #f8f9fa; + --panel-bg-hover: #e9ecef; + --panel-bg-deep: #f0f0f0; + --panel-bg-deepest: #f8f9fa; + --panel-bg-overlay: #f8f9fa; + --panel-bg-elevated: #ffffff; + --panel-bg-dialog: #ffffff; + --panel-bg-subtle: #f8f9fa; + --panel-bg-tier-tertiary: #e5e5e5; + --panel-bg-quaternary: #e9ecef; + --panel-bg-contrast: #f8f9fa; + --panel-bg-dark: #f0f0f0; + --panel-bg-darker: #f5f5f5; + --panel-bg-darkest: #f0f0f0; + --panel-bg-compact: #f8f9fa; + --panel-bg-flat: #ffffff; + --panel-bg-raised: #ffffff; + --panel-bg-inset: #f8f9fa; + /* 面板背景色 alpha 变体 */ + --panel-bg-dark-40: rgba(240, 240, 240, 0.4); + --panel-bg-dark-50: rgba(240, 240, 240, 0.5); + --panel-bg-dark-80: rgba(240, 240, 240, 0.8); + --panel-bg-darkest-20: rgba(240, 240, 240, 0.2); + --panel-bg-darkest-30: rgba(240, 240, 240, 0.3); + --panel-bg-darkest-40: rgba(240, 240, 240, 0.4); + --panel-bg-darkest-50: rgba(240, 240, 240, 0.5); + --panel-bg-darkest-60: rgba(240, 240, 240, 0.6); + --panel-bg-darkest-80: rgba(240, 240, 240, 0.8); + --panel-bg-darkest-95: rgba(240, 240, 240, 0.95); + --panel-bg-deepest-30: rgba(248, 249, 250, 0.3); + --panel-bg-deepest-40: rgba(248, 249, 250, 0.4); + --panel-bg-deepest-50: rgba(248, 249, 250, 0.5); + --panel-bg-deepest-80: rgba(248, 249, 250, 0.8); + --panel-bg-deepest-95: rgba(248, 249, 250, 0.95); + --panel-bg-hover-50: rgba(233, 236, 239, 0.5); + /* 通用表面色(无 panel 前缀) */ + --panel-bg-secondary: #f0f0f0; + --panel-bg-tertiary: #e5e5e5; + --table-row-bg: #fafafa; + /* 用途化变量 — 文字灰度(亮色模式) */ + --text-muted: #71717a; + --text-muted-light: #a1a1aa; + --text-muted-dark: #525252; + --text-tertiary-hover: #525252; + --text-primary-light: #18181b; + --text-primary-lighter: #27272a; + --text-link: #0b5afe; + --text-link-hover: #1d4ed8; + --text-highlight: #856404; + /* 用途化变量 — 边框与分割线(亮色模式) */ + --panel-border: #dedee6; + --panel-border-active: #0b5afe; + --panel-border-subtle: #ececf1; + --panel-border-10: rgba(240, 240, 240, 0.1); + --panel-border-light: #dedee6; + --panel-border-dark: #a3a3a3; + --panel-border-highlight: #c9c9d4; + --panel-border-brand: #0b5afe; + --panel-border-error: #ef4444; + /* 用途化变量 — 状态色(亮色模式) */ + --status-info-bg: #e7f5ff; + --status-info-border: #0b5afe; + --status-info-glow: rgba(11, 90, 254, 0.15); + --status-info-icon: #1677ff; + --status-success-icon: #07c160; + --status-warning-icon: #a16207; + --status-error-icon: #ff4d4f; + --status-error-deep: #d32f2f; + + /* 用途化变量 — 通知/状态语义色与场景渐变辅助色(替代原 --color-collab/pink/cyan/teal/indigo) + collab=协作, pink=年度回顾装饰, cyan=QQ/抖音, teal=QQ登录按钮, indigo=选中通知 === */ + --color-collab: #4f46e5; + --color-collab-hover: #1d4ed8; + --color-collab-light: #818cf8; + --color-collab-lighter: #e0e7ff; + --color-collab-glow: rgba(79, 70, 229, 0.3); + --color-collab-10: rgba(79, 70, 229, 0.1); + --color-pink: #f472b6; + --color-pink-alpha-10: rgba(244, 114, 182, 0.1); + --color-pink-alpha-20: rgba(244, 114, 182, 0.2); + + /* === 语义化视觉覆盖层变量 === + --shadow-color / --shadow-color-deep : 卡片悬浮阴影 + --modal-overlay : 全屏弹窗遮罩 + --shimmer-overlay : 加载 shimmer 光效 */ + --shadow-color: rgba(0, 0, 0, 0.2); + --shadow-color-deep: rgba(0, 0, 0, 0.4); + --modal-overlay: rgba(0, 0, 0, 0.8); + --shimmer-overlay: rgba(255, 255, 255, 0.2); + + --color-cyan: #22d3ee; + --color-teal: #06b6d4; + --color-indigo: #2563eb; + --color-indigo-rgb: 37 99 235; + --color-indigo-hover: #1e40af; + /* 选中文本背景色(供 ::selection 使用) */ + --selection-bg: rgba(59, 130, 246, 0.25); + + /* === WarpCanvas 星空配色(用途命名,替代原 --color-collab/indigo 语义歧义) === + 以下变量别名指向 --color-collab / --color-indigo,保持视觉一致性的同时语义更清晰 === */ + --warp-canvas-primary: var(--color-collab); + --warp-canvas-primary-light: var(--color-collab-light); + --warp-canvas-indigo: var(--color-indigo); + /* 用途化变量 — 滚动条与轨道 */ + --scrollbar-track-dark: #f0f0f0; + --scrollbar-track-hover: #fafafa; + /* === Alpha (--primary) === */ + --primary-5: rgba(59, 130, 246, 0.05); + --primary-10: rgba(59, 130, 246, 0.1); + --primary-15: rgba(59, 130, 246, 0.15); + --primary-18: rgba(59, 130, 246, 0.18); + --primary-20: rgba(59, 130, 246, 0.2); + --primary-25: rgba(59, 130, 246, 0.25); + --primary-30: rgba(59, 130, 246, 0.3); + --primary-35: rgba(59, 130, 246, 0.35); + --primary-40: rgba(59, 130, 246, 0.4); + --primary-60: rgba(59, 130, 246, 0.6); + --primary-80: rgba(59, 130, 246, 0.8); + --primary-50: rgba(59, 130, 246, 0.5); + /* === Alpha (--primary-hover) === */ + --primary-hover-5: rgba(37, 99, 235, 0.05); + --primary-hover-10: rgba(37, 99, 235, 0.1); + --primary-hover-20: rgba(37, 99, 235, 0.2); + --primary-hover-30: rgba(37, 99, 235, 0.3); + /* === Alpha (--success) === */ + --success-5: rgba(16, 185, 129, 0.05); + --success-10: rgba(16, 185, 129, 0.1); + --success-20: rgba(16, 185, 129, 0.2); + --success-40: rgba(16, 185, 129, 0.4); + --success-50: rgba(16, 185, 129, 0.5); + --success-70: rgba(16, 185, 129, 0.7); + --success-80: rgba(16, 185, 129, 0.8); + --success-30: rgba(16, 185, 129, 0.3); + /* === Alpha (--success-hover) === */ + --success-hover-5: rgba(5, 150, 105, 0.05); + --success-hover-10: rgba(5, 150, 105, 0.1); + --success-hover-20: rgba(5, 150, 105, 0.2); + /* === Alpha (--warning) === */ + --warning-5: rgba(245, 158, 11, 0.05); + --warning-10: rgba(245, 158, 11, 0.1); + --warning-15: rgba(245, 158, 11, 0.15); + --warning-20: rgba(245, 158, 11, 0.2); + --warning-25: rgba(245, 158, 11, 0.25); + --warning-30: rgba(245, 158, 11, 0.3); + --warning-40: rgba(245, 158, 11, 0.4); + --warning-50: rgba(245, 158, 11, 0.5); + --warning-80: rgba(245, 158, 11, 0.8); + --warning-200: rgba(245, 158, 11, 0.2); + --warning-300: rgba(245, 158, 11, 0.3); + /* === Alpha (--warning-hover) === */ + --warning-hover-5: rgba(217, 119, 6, 0.05); + --warning-hover-10: rgba(217, 119, 6, 0.1); + --warning-hover-15: rgba(217, 119, 6, 0.15); + --warning-hover-20: rgba(217, 119, 6, 0.2); + --warning-hover-25: rgba(217, 119, 6, 0.25); + --warning-hover-30: rgba(217, 119, 6, 0.3); + --warning-hover-40: rgba(217, 119, 6, 0.4); + --warning-hover-50: rgba(217, 119, 6, 0.5); + /* === Alpha (--error) === */ + --error-5: rgba(239, 68, 68, 0.05); + --error-10: rgba(239, 68, 68, 0.1); + --error-15: rgba(239, 68, 68, 0.15); + --error-20: rgba(239, 68, 68, 0.2); + --error-30: rgba(239, 68, 68, 0.3); + --error-40: rgba(239, 68, 68, 0.4); + --error-50: rgba(239, 68, 68, 0.5); + --error-60: rgba(239, 68, 68, 0.6); + --error-70: rgba(239, 68, 68, 0.7); + --error-80: rgba(239, 68, 68, 0.8); + /* === Alpha (--error-hover) === */ + --error-hover-5: rgba(220, 38, 38, 0.05); + --error-hover-10: rgba(220, 38, 38, 0.1); + --error-hover-15: rgba(220, 38, 38, 0.15); + --error-hover-20: rgba(220, 38, 38, 0.2); + --error-hover-30: rgba(220, 38, 38, 0.3); + /* === Alpha (--info) === */ + --info-5: rgba(59, 130, 246, 0.05); + --info-10: rgba(59, 130, 246, 0.1); + --info-20: rgba(59, 130, 246, 0.2); + --info-30: rgba(59, 130, 246, 0.3); + --info-40: rgba(59, 130, 246, 0.4); + --info-50: rgba(59, 130, 246, 0.5); + /* === Alpha (--info-hover) === */ + --info-hover-5: rgba(37, 99, 235, 0.05); + --info-hover-10: rgba(37, 99, 235, 0.1); + --info-hover-20: rgba(37, 99, 235, 0.2); + --info-hover-30: rgba(37, 99, 235, 0.3); + --info-hover-40: rgba(37, 99, 235, 0.4); + --info-hover-50: rgba(37, 99, 235, 0.5); + /* === Alpha (--bg-primary) === */ + --bg-primary-5: rgba(250, 250, 250, 0.05); + --bg-primary-10: rgba(250, 250, 250, 0.1); + --bg-primary-20: rgba(250, 250, 250, 0.2); + --bg-primary-25: rgba(250, 250, 250, 0.25); + --bg-primary-30: rgba(250, 250, 250, 0.3); + --bg-primary-40: rgba(250, 250, 250, 0.4); + --bg-primary-45: rgba(250, 250, 250, 0.45); + --bg-primary-50: rgba(250, 250, 250, 0.5); + --bg-primary-60: rgba(250, 250, 250, 0.6); + --bg-primary-70: rgba(250, 250, 250, 0.7); + --bg-primary-80: rgba(250, 250, 250, 0.8); + --bg-primary-85: rgba(250, 250, 250, 0.85); + --bg-primary-90: rgba(250, 250, 250, 0.9); + --bg-primary-95: rgba(250, 250, 250, 0.95); + /* === Alpha (--bg-secondary) === */ + --bg-secondary-5: rgba(255, 255, 255, 0.05); + --bg-secondary-10: rgba(255, 255, 255, 0.1); + --bg-secondary-15: rgba(255, 255, 255, 0.15); + --bg-secondary-20: rgba(255, 255, 255, 0.2); + --bg-secondary-30: rgba(255, 255, 255, 0.3); + --bg-secondary-40: rgba(255, 255, 255, 0.4); + --bg-secondary-50: rgba(255, 255, 255, 0.5); + --bg-secondary-60: rgba(255, 255, 255, 0.6); + --bg-secondary-70: rgba(255, 255, 255, 0.7); + --bg-secondary-80: rgba(255, 255, 255, 0.8); + --bg-secondary-90: rgba(255, 255, 255, 0.9); + --bg-secondary-95: rgba(255, 255, 255, 0.95); + /* === Alpha (--bg-tertiary) === */ + --bg-tertiary-5: rgba(240, 240, 240, 0.05); + --bg-tertiary-10: rgba(240, 240, 240, 0.1); + --bg-tertiary-20: rgba(240, 240, 240, 0.2); + --bg-tertiary-30: rgba(240, 240, 240, 0.3); + --bg-tertiary-40: rgba(240, 240, 240, 0.4); + --bg-tertiary-50: rgba(240, 240, 240, 0.5); + --bg-tertiary-60: rgba(240, 240, 240, 0.6); + --bg-tertiary-70: rgba(240, 240, 240, 0.7); + --bg-tertiary-80: rgba(240, 240, 240, 0.8); + /* === Alpha (--bg-quaternary) === */ + --bg-quaternary-10: rgba(229, 229, 229, 0.1); + --bg-quaternary-50: rgba(229, 229, 229, 0.5); + /* === Alpha (--border-secondary) === */ + --border-secondary-30: rgba(222, 222, 230, 0.3); + --border-secondary-40: rgba(222, 222, 230, 0.4); + --border-secondary-50: rgba(222, 222, 230, 0.5); + --border-secondary-60: rgba(222, 222, 230, 0.6); + --border-secondary-70: rgba(222, 222, 230, 0.7); + --border-secondary-80: rgba(222, 222, 230, 0.8); + /* === Alpha (--border-tertiary) === */ + --border-tertiary-20: rgba(201, 201, 212, 0.2); + --border-tertiary-30: rgba(201, 201, 212, 0.3); + --border-tertiary-50: rgba(201, 201, 212, 0.5); + /* === Alpha (--text-primary) === */ + --text-primary-10: rgba(38, 38, 38, 0.1); + --text-primary-20: rgba(38, 38, 38, 0.2); + --text-primary-30: rgba(38, 38, 38, 0.3); + --text-primary-40: rgba(38, 38, 38, 0.4); + --text-primary-50: rgba(38, 38, 38, 0.5); + --text-primary-60: rgba(38, 38, 38, 0.6); + --text-primary-70: rgba(38, 38, 38, 0.7); + --text-primary-80: rgba(38, 38, 38, 0.8); + --text-primary-90: rgba(38, 38, 38, 0.9); + /* === Alpha (--text-error) === */ + --text-error-20: rgba(239, 68, 68, 0.2); + --text-error-50: rgba(239, 68, 68, 0.5); + --text-error-60: rgba(239, 68, 68, 0.6); + --text-error-70: rgba(239, 68, 68, 0.7); + --text-error-80: rgba(239, 68, 68, 0.8); + /* === 通用浅色文字(亮色模式) === */ + --light: rgba(26, 26, 26, 0.95); + /* === Alpha (--text-success) === */ + --text-success-50: rgba(16, 185, 129, 0.5); + --text-success-70: rgba(16, 185, 129, 0.7); + --text-success-80: rgba(16, 185, 129, 0.8); + /* === Alpha (--text-warning) === */ + --text-warning-80: rgba(245, 158, 11, 0.8); + /* 用途化变量 — 首页(index) */ + --index-footer-bg: rgba(250, 250, 250, 0.9); + --index-visualizer-bg: rgba(231, 245, 255, 0.5); + --index-card-glow: rgba(139, 92, 246, 0.15); + --index-brand-badge-bg: rgba(139, 92, 246, 0.1); + --index-brand-badge-border: rgba(139, 92, 246, 0.2); + --index-main-icon-shadow: rgba(139, 92, 246, 0.5); + --index-pulsing-ring-shadow: rgba(0, 122, 255, 0.4); + /* 用途化变量 — 年度回顾(year-review) */ + --year-review-home-btn-shadow: rgba(139, 92, 246, 0.2); + --year-review-blob-1-bg: rgba(139, 92, 246, 0.4); + --year-review-blob-2-bg: rgba(244, 114, 182, 0.2); + --year-review-gradient-overlay-end: rgba(244, 114, 182, 0.15); + --year-review-stat-number-shadow: rgba(37, 99, 235, 0.5); + --year-review-artist-label-border: rgba(168, 85, 247, 0.3); + --year-review-artist-bg-blur: rgba(168, 85, 247, 0.2); + --year-review-stats-bg-gradient: rgba(30, 58, 138, 0.4); + --app-loading-brand-primary: #3b82f6; + --app-loading-brand-secondary: #2563eb; + --app-loading-primary-glow: rgba(59, 130, 246, 0.2); + --app-loading-primary-glow-accelerating: rgba(59, 130, 246, 0.65); + --app-loading-secondary-glow: rgba(37, 99, 235, 0.18); + --app-loading-text-color: rgba(37, 99, 235, 0.9); + /* 用途化变量 — 模态框通用(Modal shared) */ + --lyrics-modal-enter-shadow: rgba(0, 0, 0, 0.4); + --lyrics-modal-bg-accent-blue: rgba(84, 130, 255, 0.32); + --lyrics-modal-bg-accent-pink: rgba(255, 88, 126, 0.25); + --lyrics-modal-badge-menu-bg: rgba(245, 245, 245, 0.9); + /* 全屏播放页:背景恒为模糊暗化封面,文字/图标统一高对比亮色 */ + --lyrics-modal-text: #ffffff; + --lyrics-modal-text-secondary: rgba(255, 255, 255, 0.88); + --lyrics-modal-text-muted: rgba(255, 255, 255, 0.66); + --lyrics-modal-surface: rgba(255, 255, 255, 0.1); + --lyrics-modal-surface-strong: rgba(255, 255, 255, 0.2); + --lyrics-modal-surface-border: rgba(255, 255, 255, 0.14); + --lyrics-modal-track: rgba(255, 255, 255, 0.22); + /* 无封面时的渐变兜底底色(浅色主题下也保持深色沉浸感) */ + --lyrics-modal-bg-base-start: #17171b; + --lyrics-modal-bg-base-end: #0a0a0c; + /* 音质菜单为浅色底,文字需固定深色 */ + --lyrics-modal-badge-menu-text: #18181b; + --lyrics-modal-badge-menu-active-bg: #ffffff; + --audio-player-dropdown-bg-fallback: rgba(128, 128, 128, 0.85); + --audio-player-liquid-glass-bg-fallback: rgba(128, 128, 128, 0.25); + /* === Overlay / 遮罩层半透明色(浅色模式 = 黑色半透明) === */ + --overlay-1: rgba(0, 0, 0, 0.01); + --overlay-2: rgba(0, 0, 0, 0.02); + --overlay-3: rgba(0, 0, 0, 0.03); + --overlay-4: rgba(0, 0, 0, 0.04); + --overlay-5: rgba(0, 0, 0, 0.05); + --overlay-6: rgba(0, 0, 0, 0.06); + --overlay-7: rgba(0, 0, 0, 0.07); + --overlay-8: rgba(0, 0, 0, 0.08); + --overlay-9: rgba(0, 0, 0, 0.09); + --overlay-10: rgba(0, 0, 0, 0.1); + --overlay-12: rgba(0, 0, 0, 0.12); + --overlay-14: rgba(0, 0, 0, 0.14); + --overlay-15: rgba(0, 0, 0, 0.15); + --overlay-16: rgba(0, 0, 0, 0.16); + --overlay-18: rgba(0, 0, 0, 0.18); + --overlay-20: rgba(0, 0, 0, 0.2); + --overlay-22: rgba(0, 0, 0, 0.22); + --overlay-25: rgba(0, 0, 0, 0.25); + --overlay-30: rgba(0, 0, 0, 0.3); + --overlay-35: rgba(0, 0, 0, 0.35); + --overlay-40: rgba(0, 0, 0, 0.4); + --overlay-42: rgba(0, 0, 0, 0.42); + --overlay-45: rgba(0, 0, 0, 0.45); + --overlay-50: rgba(0, 0, 0, 0.5); + --overlay-52: rgba(0, 0, 0, 0.52); + --overlay-55: rgba(0, 0, 0, 0.55); + --overlay-58: rgba(0, 0, 0, 0.58); + --overlay-60: rgba(0, 0, 0, 0.6); + --overlay-62: rgba(0, 0, 0, 0.62); + --overlay-70: rgba(0, 0, 0, 0.7); + --overlay-78: rgba(0, 0, 0, 0.78); + --overlay-80: rgba(0, 0, 0, 0.8); + --overlay-85: rgba(0, 0, 0, 0.85); + --overlay-86: rgba(0, 0, 0, 0.86); + --overlay-88: rgba(0, 0, 0, 0.88); + --overlay-90: rgba(0, 0, 0, 0.9); + --overlay-92: rgba(0, 0, 0, 0.92); + --overlay-95: rgba(0, 0, 0, 0.95); + /* === Mask / 暗色遮罩(浅色模式 = 更淡) === */ + --mask-2: rgba(0, 0, 0, 0.01); + --mask-4: rgba(0, 0, 0, 0.02); + --mask-5: rgba(0, 0, 0, 0.03); + --mask-6: rgba(0, 0, 0, 0.04); + --mask-8: rgba(0, 0, 0, 0.05); + --mask-10: rgba(0, 0, 0, 0.06); + --mask-15: rgba(0, 0, 0, 0.08); + --mask-18: rgba(0, 0, 0, 0.09); + --mask-20: rgba(0, 0, 0, 0.1); + --mask-25: rgba(0, 0, 0, 0.12); + --mask-30: rgba(0, 0, 0, 0.15); + --mask-35: rgba(0, 0, 0, 0.17); + --mask-40: rgba(0, 0, 0, 0.2); + --mask-45: rgba(0, 0, 0, 0.22); + --mask-50: rgba(0, 0, 0, 0.25); + --mask-60: rgba(0, 0, 0, 0.3); + --mask-70: rgba(0, 0, 0, 0.35); + --mask-75: rgba(0, 0, 0, 0.4); + --mask-80: rgba(0, 0, 0, 0.5); + --mask-85: rgba(0, 0, 0, 0.6); + --mask-95: rgba(0, 0, 0, 0.7); + /* 打印专用变量(覆盖主题配色,白底黑字) */ + --print-bg: #ffffff; + --print-bg-subtle: #fafafa; + --print-bg-subtler: #f5f5f5; + --print-bg-deep: #f0f0f0; + --print-bg-darker: #f5f5f5; + --print-text-primary: #262626; + --print-text-secondary: #404040; + --print-text-tertiary: #5c5c5c; + --print-border: #e5e5e5; + --print-border-light: #f0f0f0; + --print-muted: #737373; + /* Year Review 专用变量 */ + --year-review-text-tertiary: #737373; + --year-review-text-yellow: #fde047; + --year-review-text-cyan: #67e8f9; + /* OAuth 第三方平台品牌色(--oauth-casdoor-green / --oauth-kuaishou-orange 已在独立变量区定义,此处省略) */ + /* === 用途化变量 — 背景纹理光晕(body::before) === */ + --bg-texture-warm-glow: rgba(255, 255, 255, 0.01); + /* === 用途化变量 — 按钮阴影 === */ + --btn-primary-shadow: rgba(99, 102, 241, 0.2); + /* === 用途化变量 — Canvas 动画背景叠加色 === */ + --canvas-bg-overlay: rgba(250, 250, 252, 0.92); + /* === 用途化变量 — Canvas 动画光晕遮罩末端 === */ + --canvas-glow-overlay-end: rgba(245, 245, 247, 0.7); + /* === 用途化变量 — 歌词消息 fallback === */ + --lyric-msg-fallback-text: #e4e4e7; + /* === 用途化变量 — 模态框阴影 === */ + --modal-hover-shadow: rgba(0, 0, 0, 0.08); + /* === 用途化变量 — 移动端表格阴影 === */ + --mobile-table-shadow: rgba(0, 0, 0, 0.5); +} diff --git a/app/assets/css/themes/ModernLight.css b/app/assets/css/themes/ModernLight.css new file mode 100644 index 0000000..5707c4d --- /dev/null +++ b/app/assets/css/themes/ModernLight.css @@ -0,0 +1,841 @@ +/* 现代浅色主题 — 全部设计变量 */ +:root[data-theme='ModernLight'] { + /* 主色调 */ + --primary: #a78bfa; + --primary-hover: #9580fa; + --primary-light: rgba(167, 139, 250, 0.1); + --primary-border: rgba(167, 139, 250, 0.2); + /* 控件强调色(与 --primary 一致,供 accent-color 使用) */ + --accent-color: #a78bfa; + /* 主色光晕(供阴影 glow 使用) */ + --primary-glow: rgba(167, 139, 250, 0.07); + --primary-glow-5: rgba(167, 139, 250, 0.03); + --primary-glow-10: rgba(167, 139, 250, 0.06); + --primary-glow-20: rgba(167, 139, 250, 0.12); + --primary-glow-40: rgba(167, 139, 250, 0.25); + /* 成功色光晕 */ + --success-glow: rgba(52, 211, 153, 0.07); + --success-glow-5: rgba(52, 211, 153, 0.03); + --success-glow-10: rgba(52, 211, 153, 0.06); + --success-glow-20: rgba(52, 211, 153, 0.12); + /* 警告色光晕 */ + --warning-glow: rgba(251, 191, 36, 0.07); + --warning-glow-5: rgba(251, 191, 36, 0.03); + --warning-glow-10: rgba(251, 191, 36, 0.06); + --warning-glow-20: rgba(251, 191, 36, 0.12); + /* 错误色光晕 */ + --error-glow: rgba(248, 113, 113, 0.07); + --error-glow-5: rgba(248, 113, 113, 0.03); + --error-glow-10: rgba(248, 113, 113, 0.06); + --error-glow-20: rgba(248, 113, 113, 0.12); + /* 成功色 */ + --success: #34d399; + --success-hover: #10b981; + --success-light: rgba(52, 211, 153, 0.1); + --success-border: rgba(52, 211, 153, 0.18); + /* 警告色 */ + --warning: #fbbf24; + --warning-hover: #f59e0b; + --warning-light: rgba(251, 191, 36, 0.1); + --warning-border: rgba(251, 191, 36, 0.18); + /* 错误色 */ + --error: #f87171; + --error-hover: #ef4444; + --error-light: rgba(248, 113, 113, 0.1); + --error-border: rgba(248, 113, 113, 0.18); + /* 信息色光晕 */ + --info: #38bdf8; + --info-hover: #0ea5e9; + --info-light: rgba(56, 189, 248, 0.1); + --info-border: rgba(56, 189, 248, 0.18); + /* 信息色光晕(蓝色系,与 info 品牌色一致) */ + --info-glow: rgba(56, 189, 248, 0.07); + --info-glow-5: rgba(56, 189, 248, 0.03); + --info-glow-10: rgba(56, 189, 248, 0.06); + --info-glow-20: rgba(56, 189, 248, 0.12); + /* 背景色 */ + --bg-primary: #fcfcfa; + --bg-secondary: #f9f8f5; + --bg-tertiary: #f3f2ee; + --bg-quaternary: #edeae5; + --bg-hover: #f7f6f2; + /* 文字颜色 */ + --text-primary: #333338; + --text-secondary: #7a7a8a; + --text-tertiary: #9a9aa8; + --text-quaternary: #adadb8; + --text-disabled: #d0d0d5; + /* 边框颜色 */ + --border-primary: #eae8ef; + --border-secondary: #e2dfea; + --border-tertiary: #d4d0df; + --border-quaternary: #b9b5c9; + --border-color: var(--border-tertiary); + /* 阴影 */ + --shadow-sm: 0 1px 2px rgba(167, 139, 250, 0.02); + --shadow-md: 0 2px 6px rgba(167, 139, 250, 0.03); + --shadow-lg: 0 8px 20px rgba(167, 139, 250, 0.05); + --shadow-xl: 0 12px 28px rgba(167, 139, 250, 0.07); + /* 圆角 */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-2xl: 16px; + /* 间距 */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; + /* 字体大小 */ + --text-xs: 12px; + --text-sm: 14px; + --text-base: 16px; + --text-lg: 18px; + --text-xl: 20px; + --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; + --z-modal-backdrop: 400; + --z-modal: 500; + --z-popover: 600; + --z-tooltip: 700; + --z-toast: 800; + --z-app-loader: 9000; + /* 特殊状态颜色 */ + --status-pending: #fbbf24; + --status-pending-light: rgba(251, 191, 36, 0.1); + --status-approved: #34d399; + --status-approved-light: rgba(52, 211, 153, 0.1); + --status-played: #aeaeb8; + --status-played-light: rgba(174, 174, 184, 0.1); + /* 投票相关颜色 */ + --vote-heart: #f87171; + --vote-heart-hover: #ef4444; + --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); + /* 卡片颜色 */ + --card-bg: var(--bg-secondary); + --card-border: var(--border-secondary); + --card-hover-border: var(--border-tertiary); + --card-shadow: var(--shadow-md); + /* 按钮颜色 */ + --btn-primary-bg: var(--primary); + --btn-primary-hover: var(--primary-hover); + --btn-primary-text: #ffffff; + --btn-primary-border: var(--primary-border); + + --btn-secondary-bg: #f9f8f5; + --btn-secondary-hover: #f3f2ee; + --btn-secondary-text: var(--text-primary); + --btn-secondary-border: #d4d0df; + + --btn-success-bg: var(--success); + --btn-success-hover: var(--success-hover); + --btn-success-text: #ffffff; + --btn-success-border: var(--success-border); + + --btn-warning-bg: var(--warning); + --btn-warning-hover: var(--warning-hover); + --btn-warning-text: #ffffff; + --btn-warning-border: var(--warning-border); + + --btn-error-bg: var(--error); + --btn-error-hover: var(--error-hover); + --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); + /* 模态框颜色 */ + --modal-backdrop: rgba(0, 0, 0, 0.3); + --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); + + --notification-warning-bg: var(--warning-light); + --notification-warning-border: var(--warning-border); + --notification-warning-text: var(--warning); + + --notification-error-bg: var(--error-light); + --notification-error-border: var(--error-border); + --notification-error-text: var(--error); + + --notification-info-bg: var(--info-light); + --notification-info-border: var(--info-border); + --notification-info-text: var(--info); + /* 加载动画颜色 */ + --spinner-primary: var(--primary); + --spinner-secondary: var(--bg-tertiary); + /* 滚动条颜色 */ + --scrollbar-track: var(--bg-tertiary); + --scrollbar-thumb: var(--bg-quaternary); + --scrollbar-thumb-hover: #b0b0b0; + /* 用途化变量 — 品牌色(已语义化:blue=通用强调色, red=错误, green=成功, yellow=警告) */ + --color-accent: #a78bfa; + --color-accent-light: #c4b5fd; + --color-accent-hover: #9580fa; + --color-accent-light-hover: #8b68fa; + --color-accent-lighter: #ddd6fe; + --color-accent-glow: rgba(167, 139, 250, 0.25); + --color-error: #f87171; + --color-error-light: #fca5a5; + --color-error-hover: #ef4444; + --color-error-glow: rgba(248, 113, 113, 0.25); + --color-success: #34d399; + --color-success-light: #6ee7b7; + --color-success-hover: #22c55e; + --color-success-glow: rgba(52, 211, 153, 0.25); + --color-warning: #fbbf24; + --color-warning-light: #fde047; + --color-warning-hover: #f59e0b; + --color-warning-glow: rgba(251, 191, 36, 0.25); + /* --color-warning 透明度变体 */ + --color-warning-alpha-14: rgba(251, 191, 36, 0.14); + --color-warning-alpha-24: rgba(251, 191, 36, 0.24); + --color-warning-alpha-48: rgba(251, 191, 36, 0.48); + --color-orange: #fdba74; + --color-orange-light: #fed7aa; + --color-orange-hover: #fb923c; + --color-orange-glow: rgba(251, 146, 60, 0.25); + + /* === Alpha (--color-accent) === + 以下为 --color-accent 的各透明度变体(alpha-前缀 + 透明度百分比)=== + --color-accent-alpha-10 ~ --color-accent-alpha-80 对应原 --color-accent-* 系列 === */ + --color-accent-alpha-10: rgba(167, 139, 250, 0.07); + --color-accent-alpha-12: rgba(167, 139, 250, 0.1); + --color-accent-alpha-15: rgba(167, 139, 250, 0.12); + --color-accent-alpha-18: rgba(167, 139, 250, 0.14); + --color-accent-alpha-20: rgba(167, 139, 250, 0.13); + --color-accent-alpha-22: rgba(167, 139, 250, 0.22); + --color-accent-alpha-25: rgba(167, 139, 250, 0.2); + --color-accent-alpha-30: rgba(167, 139, 250, 0.2); + --color-accent-alpha-40: rgba(167, 139, 250, 0.25); + --color-accent-alpha-50: rgba(167, 139, 250, 0.32); + --color-accent-alpha-60: rgba(167, 139, 250, 0.4); + --color-accent-alpha-65: rgba(167, 139, 250, 0.5); + --color-accent-alpha-80: rgba(167, 139, 250, 0.48); + + /* 用途化变量 — 通知/状态/场景语义色(用途命名,替代原 --color-brand-* 颜色命名) + ═══════════════════════════════════════════════════════════════ + 新增 --color-accent-alpha-* / --color-collab / --color-indigo 等语义变量后, + 以下变量供全局 CSS / Vue 组件统一引用,避免直接在业务代码中硬写颜色值。 === */ + + /* ─── 通知图标颜色(index.vue 通知列表) ─── */ + --color-notif-info: var(--color-accent); + --color-notif-success: var(--color-success); + --color-notif-error: var(--color-error); + --color-notif-warning: var(--color-warning); + --color-notif-checked: var(--color-accent); + --color-notif-collab: var(--color-collab); + --color-notif-indigo: var(--color-indigo); + /* ─── 通知容器/Toast 语义色(Notification.vue / NotificationContainer.vue) ─── */ + --color-toast-success: var(--color-success); + --color-toast-error: var(--color-error); + --color-toast-info-bg: var(--color-accent-alpha-20); + --color-toast-info-text: var(--color-accent); + /* ─── 通用 UI 组件语义色 ─── */ + --color-checked: var(--color-accent); + /* 勾选框选中颜色 */ + --color-search-focus: var(--color-accent); + /* 搜索框 focus 边框 */ + --color-search-btn-bg: var(--color-accent); + /* 搜索按钮背景 */ + --color-progress-fill: var(--color-accent); + /* 进度条填充色 */ + --color-spinner-border: var(--color-accent); + /* 加载指示器边框 */ + --color-tab-active: var(--color-accent); + /* Tab 激活下边框 */ + --color-page-btn-active: var(--color-accent); + /* 分页按钮激活态 */ + --color-checkbox-checked: var(--color-accent-light); + /* 勾选框选中态背景 */ + --color-checkbox-checked-border: var(--color-accent-light); + --color-mobile-page-input: var(--color-accent); + /* 移动端页码输入框 focus */ + --color-page-nav-hover: var(--color-accent-light); + /* 翻页按钮 hover 边框 */ + --color-visibility-public: var(--color-accent-light); + /* 歌曲可见性"公开"标签 */ + --color-text-cover-bg: var(--color-accent); + /* 文字封面背景 */ + --color-like-btn-bg: var(--color-accent); + /* 点赞按钮背景 */ + --color-replay-cancel-bg: var(--color-accent); + /* 重播取消按钮背景 */ + --color-semester-toggle: var(--color-accent); + /* 学期切换按钮文字 */ + --color-admin-badge: var(--color-accent-light); + /* 管理员徽章文字 */ + --color-error-text: var(--color-error); + /* 错误文本 */ + --color-pending-tag: var(--color-warning-light); + /* 等待中标签 */ + /* 首页标题分隔线 */ + --title-divider-bg: #c4b5fd; + --like-btn-bg: #ffffff; + --like-btn-hover-bg: rgba(167, 139, 250, 0.08); + --like-btn-border: rgba(167, 139, 250, 0.55); + --song-card-border: #d7d3e2; + --song-card-bg: #ffffff; + --song-card-footer-bg: #ffffff; + --song-card-divider: var(--panel-border-subtle); + --chip-bg: #ffffff; + --chip-bg-hover: var(--bg-hover); + --chip-border: var(--panel-border-highlight); + --like-btn-bg-mobile: #ffffff; + --like-btn-border-mobile: rgba(167, 139, 250, 0.55); + --withdraw-btn-bg-mobile: var(--error-light); + --section-tab-bg: #ffffff; + --section-tab-border: var(--song-card-border); + --surface-subtle: #ffffff; + --surface-card-bg: var(--mask-5); + --surface-card-bg-medium: var(--mask-30); + --surface-card-bg-soft: var(--mask-20); + /* ─── OAuth 第三方平台独立品牌色(用途命名 --oauth-*,不再复用通用语义色)─── + 每个平台有专属颜色变量,语义清晰,互不干扰。 + 旧 --color-oauth-* 保留向后兼容,但新增 --oauth-* 系列为主用。 === */ + --oauth-qq: #67e8f9; + --oauth-wx: #34d399; + --oauth-alipay: #93c5fd; + --oauth-sina: #fca5a5; + --oauth-baidu: #c4b5fd; + --oauth-douyin: #fda4af; + --oauth-huawei: #fca5a5; + --oauth-xiaomi: #fed7aa; + --oauth-gitee: #fca5a5; + --oauth-bilibili: #22d3ee; + --oauth-kuaishou: #fd8c6a; + --oauth-kuaishou-orange: var(--oauth-kuaishou); + --oauth-google-blue: #93c5fd; + --oauth-google-red: #fca5a5; + --oauth-google-yellow: #fde047; + --oauth-google-green: #86efac; + --oauth-casdoor: #a3e635; + --oauth-casdoor-green: var(--oauth-casdoor); + /* Gitea 品牌路径的镂空色(供 Icon.vue SVG path 使用) */ + --gitea-cutout: var(--panel-bg-dialog); + /* ─── Auth/OAuth 品牌色(旧命名,向后兼容,指向新 --oauth-* 系列)─── */ + --color-oauth-qq: var(--oauth-qq); + --color-oauth-wx: var(--oauth-wx); + --color-oauth-alipay: var(--oauth-alipay); + --color-oauth-sina: var(--oauth-sina); + --color-oauth-baidu: var(--oauth-baidu); + --color-oauth-douyin: var(--oauth-douyin); + --color-oauth-huawei: var(--oauth-huawei); + --color-oauth-xiaomi: var(--oauth-xiaomi); + --color-oauth-gitee: var(--oauth-gitee); + --color-oauth-bilibili: var(--oauth-bilibili); + --color-oauth-kuaishou: var(--oauth-kuaishou); + --requestform-audio-match-hover-border: rgba(167, 139, 250, 0.25); + --requestform-audio-match-hover-border-light: rgba(167, 139, 250, 0.15); + --requestform-qq-login-shadow: rgba(34, 211, 238, 0.1); + --requestform-qq-login-shadow-hover: rgba(34, 211, 238, 0.18); + --requestform-user-icon-bg: rgba(34, 211, 238, 0.1); + --requestform-user-icon-border: rgba(34, 211, 238, 0.18); + --requestform-login-btn-hover-shadow: rgba(167, 139, 250, 0.28); + --requestform-login-btn-hover-shadow-lg: rgba(167, 139, 250, 0.35); + --requestform-like-btn-hover-shadow: rgba(248, 113, 113, 0.28); + --requestform-like-btn-hover-shadow-lg: rgba(248, 113, 113, 0.35); + --requestform-source-healthy-bg: rgba(52, 211, 153, 0.12); + --requestform-source-healthy-border: rgba(52, 211, 153, 0.25); + --requestform-source-healthy-glow: rgba(52, 211, 153, 0.48); + --requestform-source-checking-bg: rgba(251, 191, 36, 0.12); + --requestform-source-checking-border: rgba(251, 191, 36, 0.25); + --requestform-source-checking-glow: rgba(251, 191, 36, 0.48); + --requestform-modal-overlay-bg: rgba(30, 30, 35, 0.95); + --requestform-submit-btn-hover-shadow: rgba(167, 139, 250, 0.3); + --requestform-delete-btn-hover-shadow: rgba(248, 113, 113, 0.4); + --requestform-delete-btn-hover-shadow-sm: rgba(248, 113, 113, 0.24); + --requestform-replay-btn-bg: rgba(167, 139, 250, 0.08); + --requestform-replay-btn-bg-hover: rgba(167, 139, 250, 0.15); + --requestform-replay-btn-border: rgba(167, 139, 250, 0.24); + --requestform-replay-btn-border-hover: rgba(167, 139, 250, 0.4); + --requestform-mobile-chip-border: rgba(160, 160, 160, 0.38); + --requestform-mobile-chip-border-sm: rgba(160, 160, 160, 0.32); + --requestform-mobile-chip-bg: rgba(243, 243, 243, 0.6); + --requestform-mobile-chip-text: rgba(30, 30, 30, 0.75); + --requestform-mobile-button-text: rgba(30, 30, 30, 0.75); + --requestform-chip-required-bg: rgba(251, 191, 36, 0.08); + --requestform-chip-required-border: rgba(251, 191, 36, 0.28); + --requestform-chip-required-border-sm: rgba(251, 191, 36, 0.24); + --requestform-chip-valid-border: rgba(52, 211, 153, 0.28); + --requestform-chip-invalid-bg: rgba(248, 113, 113, 0.08); + --requestform-chip-invalid-border: rgba(248, 113, 113, 0.28); + /* 用途化变量 — 音频播放器(AudioPlayer) */ + --audio-player-bar-bg: rgba(30, 30, 35, 0.75); + --audio-player-progress-track-bg: rgba(160, 160, 160, 0.25); + --audio-player-quality-active-bg-start: rgba(167, 139, 250, 0.2); + --audio-player-quality-active-bg-mid: rgba(167, 139, 250, 0.12); + --audio-player-quality-active-bg-end: rgba(167, 139, 250, 0.08); + --audio-player-quality-active-border: rgba(167, 139, 250, 0.48); + --audio-player-quality-active-shadow-lg: rgba(167, 139, 250, 0.32); + --audio-player-quality-active-shadow: rgba(167, 139, 250, 0.24); + --audio-player-quality-active-ring: rgba(167, 139, 250, 0.16); + /* 用途化变量 — 音量控制(VolumeControl) */ + --volume-control-panel-bg: rgba(245, 245, 245, 0.7); + --volume-control-muted-color: rgba(255, 100, 100, 0.7); + /* 用途化变量 — 备份管理器(BackupManager) */ + --backup-manager-danger-bg: rgba(248, 113, 113, 0.08); + --backup-manager-file-remove-bg: rgba(248, 113, 113, 0.15); + --backup-manager-file-remove-bg-hover: rgba(248, 113, 113, 0.24); + --backup-manager-warning-box-bg: rgba(253, 186, 116, 0.1); + /* 用途化变量 — 用户歌曲模态框滚动条(UserSongsModal) */ + --usersongs-scroll-thumb: rgba(160, 160, 160, 0.24); + --usersongs-scroll-thumb-hover: rgba(160, 160, 160, 0.4); + /* 用途化变量 — 用户管理器(UserManager) */ + --user-manager-tree-line: rgba(160, 160, 160, 0.44); + --user-manager-tree-active-bg: rgba(167, 139, 250, 0.1); + /* 用途化变量 — 数据分析面板滚动条(DataAnalysisPanel) */ + --data-analysis-scroll-thumb: rgba(167, 139, 250, 0.24); + --data-analysis-scroll-thumb-hover: rgba(167, 139, 250, 0.4); + /* 用途化变量 — 歌曲列表(ScheduleList/SongList) */ + --schedule-list-divider: rgba(160, 160, 160, 0.4); + --schedule-list-title-shadow: rgba(167, 139, 250, 0.24); + --schedule-list-title-shadow-sm: rgba(167, 139, 250, 0.16); + /* 用途化变量 — 点歌券标识(CardCodeTag) */ + --card-code-bg: rgba(251, 191, 36, 0.08); + --card-code-border: rgba(251, 191, 36, 0.2); + --card-code-text: #f59e0b; + /* 用途化变量 — 认证模块(Auth) */ + --auth-error-input-shadow: rgba(248, 113, 113, 0.08); + /* 用途化变量 — 面板背景色(亮色模式) */ + --panel-bg: #fcfcfa; + --panel-bg-alt: #f9f8f5; + --panel-bg-hover: #f3f2ee; + --panel-bg-deep: #f0eef5; + --panel-bg-deepest: #f9f8f5; + --panel-bg-overlay: #f9f8f5; + --panel-bg-elevated: #ffffff; + --panel-bg-dialog: #ffffff; + --panel-bg-subtle: #f9f8f5; + --panel-bg-tier-tertiary: #edeae5; + --panel-bg-quaternary: #f7f6f2; + --panel-bg-contrast: #f9f8f5; + --panel-bg-dark: #f0eef5; + --panel-bg-darker: #f7f6f2; + --panel-bg-darkest: #edeae5; + --panel-bg-compact: #f9f8f5; + --panel-bg-flat: #ffffff; + --panel-bg-raised: #ffffff; + --panel-bg-inset: #f7f6f2; + /* 面板背景色 alpha 变体 */ + --panel-bg-dark-40: rgba(240, 238, 245, 0.4); + --panel-bg-dark-50: rgba(240, 238, 245, 0.5); + --panel-bg-dark-80: rgba(240, 238, 245, 0.8); + --panel-bg-darkest-20: rgba(237, 234, 229, 0.2); + --panel-bg-darkest-30: rgba(237, 234, 229, 0.3); + --panel-bg-darkest-40: rgba(237, 234, 229, 0.4); + --panel-bg-darkest-50: rgba(237, 234, 229, 0.5); + --panel-bg-darkest-60: rgba(237, 234, 229, 0.6); + --panel-bg-darkest-80: rgba(237, 234, 229, 0.8); + --panel-bg-darkest-95: rgba(237, 234, 229, 0.95); + --panel-bg-deepest-30: rgba(249, 248, 245, 0.3); + --panel-bg-deepest-40: rgba(249, 248, 245, 0.4); + --panel-bg-deepest-50: rgba(249, 248, 245, 0.5); + --panel-bg-deepest-80: rgba(249, 248, 245, 0.8); + --panel-bg-deepest-95: rgba(249, 248, 245, 0.95); + --panel-bg-hover-50: rgba(243, 242, 238, 0.5); + /* 通用表面色(无 panel 前缀) */ + --panel-bg-secondary: #f3f2ee; + --panel-bg-tertiary: #edeae5; + --table-row-bg: #fcfcfa; + /* 用途化变量 — 文字灰度(亮色模式) */ + --text-muted: #9a9aa8; + --text-muted-light: #adadb8; + --text-muted-dark: #7a7a8a; + --text-tertiary-hover: #7a7a8a; + --text-primary-light: #333338; + --text-primary-lighter: #4a4a5e; + --text-link: #a78bfa; + --text-link-hover: #9580fa; + --text-highlight: #92400e; + /* 用途化变量 — 边框与分割线(亮色模式) */ + --panel-border: #e2dfea; + --panel-border-active: #a78bfa; + --panel-border-subtle: #edeaf2; + --panel-border-10: rgba(243, 242, 238, 0.1); + --panel-border-light: #e2dfea; + --panel-border-dark: #b9b5c9; + --panel-border-highlight: #d4d0df; + --panel-border-brand: #a78bfa; + --panel-border-error: #f87171; + /* 用途化变量 — 状态色(亮色模式) */ + --status-info-bg: #f0f9ff; + --status-info-border: #38bdf8; + --status-info-glow: rgba(56, 189, 248, 0.1); + --status-info-icon: #38bdf8; + --status-success-icon: #34d399; + --status-warning-icon: #fde047; + --status-error-icon: #f87171; + --status-error-deep: #f87171; + + /* 用途化变量 — 通知/状态语义色与场景渐变辅助色(替代原 --color-collab/pink/cyan/teal/indigo) + collab=协作, pink=年度回顾装饰, cyan=QQ/抖音, teal=QQ登录按钮, indigo=选中通知 === */ + --color-collab: #a78bfa; + --color-collab-hover: #9580fa; + --color-collab-light: #c4b5fd; + --color-collab-lighter: #ede9fe; + --color-collab-glow: rgba(167, 139, 250, 0.24); + --color-collab-10: rgba(167, 139, 250, 0.1); + --color-pink: #f9a8d4; + --color-pink-alpha-10: rgba(249, 168, 212, 0.08); + --color-pink-alpha-20: rgba(249, 168, 212, 0.15); + + /* === 语义化视觉覆盖层变量 === + --shadow-color / --shadow-color-deep : 卡片悬浮阴影 + --modal-overlay : 全屏弹窗遮罩 + --shimmer-overlay : 加载 shimmer 光效 */ + --shadow-color: rgba(0, 0, 0, 0.2); + --shadow-color-deep: rgba(0, 0, 0, 0.4); + --modal-overlay: rgba(0, 0, 0, 0.8); + --shimmer-overlay: rgba(255, 255, 255, 0.2); + + --color-cyan: #67e8f9; + --color-teal: #22d3ee; + --color-indigo: #a78bfa; + --color-indigo-rgb: 167 139 250; + --color-indigo-hover: #c4b5fd; + /* 选中文本背景色(供 ::selection 使用) */ + --selection-bg: rgba(167, 139, 250, 0.25); + + /* === WarpCanvas 星空配色(用途命名,替代原 --color-collab/indigo 语义歧义) === + 以下变量别名指向 --color-collab / --color-indigo,保持视觉一致性的同时语义更清晰 === */ + --warp-canvas-primary: var(--color-collab); + --warp-canvas-primary-light: var(--color-collab-light); + --warp-canvas-indigo: var(--color-indigo); + /* 用途化变量 — 滚动条与轨道 */ + --scrollbar-track-dark: #f3f2ee; + --scrollbar-track-hover: #fcfcfa; + /* === Alpha (--primary) === */ + --primary-5: rgba(167, 139, 250, 0.04); + --primary-10: rgba(167, 139, 250, 0.08); + --primary-15: rgba(167, 139, 250, 0.12); + --primary-18: rgba(167, 139, 250, 0.14); + --primary-20: rgba(167, 139, 250, 0.16); + --primary-25: rgba(167, 139, 250, 0.2); + --primary-30: rgba(167, 139, 250, 0.24); + --primary-35: rgba(167, 139, 250, 0.28); + --primary-40: rgba(167, 139, 250, 0.32); + --primary-60: rgba(167, 139, 250, 0.48); + --primary-80: rgba(167, 139, 250, 0.64); + --primary-50: rgba(167, 139, 250, 0.4); + /* === Alpha (--primary-hover) === */ + --primary-hover-5: rgba(149, 128, 250, 0.04); + --primary-hover-10: rgba(149, 128, 250, 0.08); + --primary-hover-20: rgba(149, 128, 250, 0.16); + --primary-hover-30: rgba(149, 128, 250, 0.24); + /* === Alpha (--success) === */ + --success-5: rgba(52, 211, 153, 0.04); + --success-10: rgba(52, 211, 153, 0.08); + --success-20: rgba(52, 211, 153, 0.16); + --success-40: rgba(52, 211, 153, 0.32); + --success-50: rgba(52, 211, 153, 0.4); + --success-70: rgba(52, 211, 153, 0.56); + --success-80: rgba(52, 211, 153, 0.64); + --success-30: rgba(52, 211, 153, 0.24); + /* === Alpha (--success-hover) === */ + --success-hover-5: rgba(16, 185, 129, 0.04); + --success-hover-10: rgba(16, 185, 129, 0.08); + --success-hover-20: rgba(16, 185, 129, 0.16); + /* === Alpha (--warning) === */ + --warning-5: rgba(251, 191, 36, 0.04); + --warning-10: rgba(251, 191, 36, 0.08); + --warning-15: rgba(251, 191, 36, 0.1); + --warning-20: rgba(251, 191, 36, 0.16); + --warning-25: rgba(251, 191, 36, 0.2); + --warning-30: rgba(251, 191, 36, 0.24); + --warning-40: rgba(251, 191, 36, 0.32); + --warning-50: rgba(251, 191, 36, 0.4); + --warning-80: rgba(251, 191, 36, 0.64); + --warning-200: rgba(251, 191, 36, 0.16); + --warning-300: rgba(251, 191, 36, 0.24); + /* === Alpha (--warning-hover) === */ + --warning-hover-5: rgba(245, 158, 11, 0.04); + --warning-hover-10: rgba(245, 158, 11, 0.08); + --warning-hover-15: rgba(245, 158, 11, 0.12); + --warning-hover-20: rgba(245, 158, 11, 0.16); + --warning-hover-25: rgba(245, 158, 11, 0.2); + --warning-hover-30: rgba(245, 158, 11, 0.24); + --warning-hover-40: rgba(245, 158, 11, 0.32); + --warning-hover-50: rgba(245, 158, 11, 0.4); + /* === Alpha (--error) === */ + --error-5: rgba(248, 113, 113, 0.04); + --error-10: rgba(248, 113, 113, 0.08); + --error-15: rgba(248, 113, 113, 0.12); + --error-20: rgba(248, 113, 113, 0.16); + --error-30: rgba(248, 113, 113, 0.24); + --error-40: rgba(248, 113, 113, 0.32); + --error-50: rgba(248, 113, 113, 0.4); + --error-60: rgba(248, 113, 113, 0.48); + --error-70: rgba(248, 113, 113, 0.56); + --error-80: rgba(248, 113, 113, 0.64); + /* === Alpha (--error-hover) === */ + --error-hover-5: rgba(239, 68, 68, 0.04); + --error-hover-10: rgba(239, 68, 68, 0.08); + --error-hover-15: rgba(239, 68, 68, 0.12); + --error-hover-20: rgba(239, 68, 68, 0.16); + --error-hover-30: rgba(239, 68, 68, 0.24); + /* === Alpha (--info) === */ + --info-5: rgba(56, 189, 248, 0.04); + --info-10: rgba(56, 189, 248, 0.08); + --info-20: rgba(56, 189, 248, 0.16); + --info-30: rgba(56, 189, 248, 0.24); + --info-40: rgba(56, 189, 248, 0.32); + --info-50: rgba(56, 189, 248, 0.4); + /* === Alpha (--info-hover) === */ + --info-hover-5: rgba(14, 165, 233, 0.04); + --info-hover-10: rgba(14, 165, 233, 0.08); + --info-hover-20: rgba(14, 165, 233, 0.16); + --info-hover-30: rgba(2, 132, 199, 0.3); + --info-hover-40: rgba(2, 132, 199, 0.4); + --info-hover-50: rgba(2, 132, 199, 0.5); + /* === Alpha (--bg-primary) === */ + --bg-primary-5: rgba(252, 252, 250, 0.04); + --bg-primary-10: rgba(252, 252, 250, 0.08); + --bg-primary-20: rgba(252, 252, 250, 0.16); + --bg-primary-25: rgba(252, 252, 250, 0.2); + --bg-primary-30: rgba(252, 252, 250, 0.24); + --bg-primary-40: rgba(252, 252, 250, 0.32); + --bg-primary-45: rgba(252, 252, 250, 0.36); + --bg-primary-50: rgba(252, 252, 250, 0.4); + --bg-primary-60: rgba(252, 252, 250, 0.48); + --bg-primary-70: rgba(252, 252, 250, 0.56); + --bg-primary-80: rgba(252, 252, 250, 0.64); + --bg-primary-85: rgba(252, 252, 250, 0.68); + --bg-primary-90: rgba(252, 252, 250, 0.72); + --bg-primary-95: rgba(252, 252, 250, 0.76); + /* === Alpha (--bg-secondary) === */ + --bg-secondary-5: rgba(249, 248, 245, 0.04); + --bg-secondary-10: rgba(249, 248, 245, 0.08); + --bg-secondary-15: rgba(249, 248, 245, 0.12); + --bg-secondary-20: rgba(249, 248, 245, 0.16); + --bg-secondary-30: rgba(249, 248, 245, 0.24); + --bg-secondary-40: rgba(249, 248, 245, 0.32); + --bg-secondary-50: rgba(249, 248, 245, 0.4); + --bg-secondary-60: rgba(249, 248, 245, 0.48); + --bg-secondary-70: rgba(249, 248, 245, 0.56); + --bg-secondary-80: rgba(249, 248, 245, 0.64); + --bg-secondary-90: rgba(249, 248, 245, 0.72); + --bg-secondary-95: rgba(249, 248, 245, 0.76); + /* === Alpha (--bg-tertiary) === */ + --bg-tertiary-5: rgba(243, 242, 238, 0.04); + --bg-tertiary-10: rgba(243, 242, 238, 0.08); + --bg-tertiary-20: rgba(243, 242, 238, 0.16); + --bg-tertiary-30: rgba(243, 242, 238, 0.24); + --bg-tertiary-40: rgba(243, 242, 238, 0.32); + --bg-tertiary-50: rgba(243, 242, 238, 0.4); + --bg-tertiary-60: rgba(243, 242, 238, 0.48); + --bg-tertiary-70: rgba(243, 242, 238, 0.56); + --bg-tertiary-80: rgba(243, 242, 238, 0.64); + /* === Alpha (--bg-quaternary) === */ + --bg-quaternary-10: rgba(237, 234, 229, 0.1); + --bg-quaternary-50: rgba(237, 234, 229, 0.4); + /* === Alpha (--border-secondary) === */ + --border-secondary-30: rgba(226, 223, 234, 0.24); + --border-secondary-40: rgba(226, 223, 234, 0.32); + --border-secondary-50: rgba(226, 223, 234, 0.4); + --border-secondary-60: rgba(226, 223, 234, 0.48); + --border-secondary-70: rgba(226, 223, 234, 0.56); + --border-secondary-80: rgba(226, 223, 234, 0.64); + /* === Alpha (--border-tertiary) === */ + --border-tertiary-20: rgba(212, 208, 223, 0.16); + --border-tertiary-30: rgba(212, 208, 223, 0.24); + --border-tertiary-50: rgba(212, 208, 223, 0.4); + /* === Alpha (--text-primary) === */ + --text-primary-10: rgba(51, 51, 56, 0.08); + --text-primary-20: rgba(51, 51, 56, 0.16); + --text-primary-30: rgba(51, 51, 56, 0.24); + --text-primary-40: rgba(51, 51, 56, 0.32); + --text-primary-50: rgba(51, 51, 56, 0.4); + --text-primary-60: rgba(51, 51, 56, 0.48); + --text-primary-70: rgba(51, 51, 56, 0.56); + --text-primary-80: rgba(51, 51, 56, 0.64); + --text-primary-90: rgba(51, 51, 56, 0.72); + /* === Alpha (--text-error) === */ + --text-error-20: rgba(248, 113, 113, 0.16); + --text-error-50: rgba(248, 113, 113, 0.4); + --text-error-60: rgba(248, 113, 113, 0.48); + --text-error-70: rgba(248, 113, 113, 0.56); + --text-error-80: rgba(248, 113, 113, 0.64); + /* === 通用浅色文字(亮色模式) === */ + --light: rgba(30, 30, 30, 0.76); + /* === Alpha (--text-success) === */ + --text-success-50: rgba(52, 211, 153, 0.4); + --text-success-70: rgba(52, 211, 153, 0.56); + --text-success-80: rgba(52, 211, 153, 0.64); + /* === Alpha (--text-warning) === */ + --text-warning-80: rgba(251, 191, 36, 0.64); + /* 用途化变量 — 首页(index) */ + --index-footer-bg: rgba(249, 248, 245, 0.72); + --index-visualizer-bg: rgba(243, 242, 238, 0.4); + --index-card-glow: rgba(167, 139, 250, 0.12); + --index-brand-badge-bg: rgba(167, 139, 250, 0.08); + --index-brand-badge-border: rgba(167, 139, 250, 0.16); + --index-main-icon-shadow: rgba(167, 139, 250, 0.4); + --index-pulsing-ring-shadow: rgba(167, 139, 250, 0.32); + /* 用途化变量 — 年度回顾(year-review) */ + --year-review-home-btn-shadow: rgba(167, 139, 250, 0.16); + --year-review-blob-1-bg: rgba(167, 139, 250, 0.32); + --year-review-blob-2-bg: rgba(249, 168, 212, 0.15); + --year-review-gradient-overlay-end: rgba(249, 168, 212, 0.12); + --year-review-stat-number-shadow: rgba(167, 139, 250, 0.4); + --year-review-artist-label-border: rgba(196, 181, 253, 0.24); + --year-review-artist-bg-blur: rgba(196, 181, 253, 0.16); + --year-review-stats-bg-gradient: rgba(167, 139, 250, 0.32); + --app-loading-brand-primary: #a78bfa; + --app-loading-brand-secondary: #c4b5fd; + --app-loading-primary-glow: rgba(167, 139, 250, 0.16); + --app-loading-primary-glow-accelerating: rgba(167, 139, 250, 0.52); + --app-loading-secondary-glow: rgba(196, 181, 253, 0.14); + --app-loading-text-color: rgba(167, 139, 250, 0.72); + /* 用途化变量 — 模态框通用(Modal shared) */ + --lyrics-modal-enter-shadow: rgba(0, 0, 0, 0.32); + --lyrics-modal-bg-accent-blue: rgba(167, 139, 250, 0.26); + --lyrics-modal-bg-accent-pink: rgba(255, 88, 126, 0.2); + --lyrics-modal-badge-menu-bg: rgba(252, 252, 250, 0.9); + /* 全屏播放页:背景恒为模糊暗化封面,文字/图标统一高对比暖亮色 */ + --lyrics-modal-text: #fdfcfa; + --lyrics-modal-text-secondary: rgba(253, 252, 248, 0.88); + --lyrics-modal-text-muted: rgba(253, 252, 248, 0.68); + --lyrics-modal-surface: rgba(255, 255, 255, 0.1); + --lyrics-modal-surface-strong: rgba(255, 255, 255, 0.2); + --lyrics-modal-surface-border: rgba(255, 255, 255, 0.14); + --lyrics-modal-track: rgba(255, 255, 255, 0.22); + /* 无封面时的渐变兜底底色(暖调深色,保留主题暖色气质) */ + --lyrics-modal-bg-base-start: #1f1a14; + --lyrics-modal-bg-base-end: #0d0b08; + /* 音质菜单为浅色底,文字需固定深色 */ + --lyrics-modal-badge-menu-text: #333338; + --lyrics-modal-badge-menu-active-bg: #ffffff; + --audio-player-dropdown-bg-fallback: rgba(128, 128, 128, 0.7); + --audio-player-liquid-glass-bg-fallback: rgba(128, 128, 128, 0.2); + /* === Overlay / 遮罩层半透明色(浅色模式 = 黑色半透明) === */ + --overlay-1: rgba(0, 0, 0, 0.01); + --overlay-2: rgba(0, 0, 0, 0.02); + --overlay-3: rgba(0, 0, 0, 0.03); + --overlay-4: rgba(0, 0, 0, 0.04); + --overlay-5: rgba(0, 0, 0, 0.05); + --overlay-6: rgba(0, 0, 0, 0.06); + --overlay-7: rgba(0, 0, 0, 0.07); + --overlay-8: rgba(0, 0, 0, 0.08); + --overlay-9: rgba(0, 0, 0, 0.09); + --overlay-10: rgba(0, 0, 0, 0.1); + --overlay-12: rgba(0, 0, 0, 0.12); + --overlay-14: rgba(0, 0, 0, 0.14); + --overlay-15: rgba(0, 0, 0, 0.15); + --overlay-16: rgba(0, 0, 0, 0.16); + --overlay-18: rgba(0, 0, 0, 0.18); + --overlay-20: rgba(0, 0, 0, 0.2); + --overlay-22: rgba(0, 0, 0, 0.22); + --overlay-25: rgba(0, 0, 0, 0.25); + --overlay-30: rgba(0, 0, 0, 0.3); + --overlay-35: rgba(0, 0, 0, 0.35); + --overlay-40: rgba(0, 0, 0, 0.4); + --overlay-42: rgba(0, 0, 0, 0.42); + --overlay-45: rgba(0, 0, 0, 0.45); + --overlay-50: rgba(0, 0, 0, 0.5); + --overlay-52: rgba(0, 0, 0, 0.52); + --overlay-55: rgba(0, 0, 0, 0.55); + --overlay-58: rgba(0, 0, 0, 0.58); + --overlay-60: rgba(0, 0, 0, 0.6); + --overlay-62: rgba(0, 0, 0, 0.62); + --overlay-70: rgba(0, 0, 0, 0.7); + --overlay-78: rgba(0, 0, 0, 0.78); + --overlay-80: rgba(0, 0, 0, 0.8); + --overlay-85: rgba(0, 0, 0, 0.85); + --overlay-86: rgba(0, 0, 0, 0.86); + --overlay-88: rgba(0, 0, 0, 0.88); + --overlay-90: rgba(0, 0, 0, 0.9); + --overlay-92: rgba(0, 0, 0, 0.92); + --overlay-95: rgba(0, 0, 0, 0.95); + /* === Mask / 暗色遮罩(浅色模式 = 更淡) === */ + --mask-2: rgba(0, 0, 0, 0.01); + --mask-4: rgba(0, 0, 0, 0.02); + --mask-5: rgba(0, 0, 0, 0.03); + --mask-6: rgba(0, 0, 0, 0.04); + --mask-8: rgba(0, 0, 0, 0.05); + --mask-10: rgba(0, 0, 0, 0.06); + --mask-15: rgba(0, 0, 0, 0.08); + --mask-18: rgba(0, 0, 0, 0.09); + --mask-20: rgba(0, 0, 0, 0.1); + --mask-25: rgba(0, 0, 0, 0.12); + --mask-30: rgba(0, 0, 0, 0.15); + --mask-35: rgba(0, 0, 0, 0.17); + --mask-40: rgba(0, 0, 0, 0.2); + --mask-45: rgba(0, 0, 0, 0.22); + --mask-50: rgba(0, 0, 0, 0.25); + --mask-60: rgba(0, 0, 0, 0.3); + --mask-70: rgba(0, 0, 0, 0.35); + --mask-75: rgba(0, 0, 0, 0.4); + --mask-80: rgba(0, 0, 0, 0.5); + --mask-85: rgba(0, 0, 0, 0.6); + --mask-95: rgba(0, 0, 0, 0.7); + /* 打印专用变量(覆盖主题配色,白底黑字) */ + --print-bg: #ffffff; + --print-bg-subtle: #fafafa; + --print-bg-subtler: #f5f5f5; + --print-bg-deep: #f0f0f0; + --print-bg-darker: #f5f5f5; + --print-text-primary: #262626; + --print-text-secondary: #404040; + --print-text-tertiary: #5c5c5c; + --print-border: #e5e5e5; + --print-border-light: #f0f0f0; + --print-muted: #737373; + /* Year Review 专用变量 */ + --year-review-text-tertiary: #737373; + --year-review-text-yellow: #fde047; + --year-review-text-cyan: #67e8f9; + /* OAuth 第三方平台品牌色(--oauth-casdoor-green / --oauth-kuaishou-orange 已在独立变量区定义,此处省略) */ + /* === 用途化变量 — 背景纹理光晕(body::before) === */ + --bg-texture-warm-glow: rgba(243, 242, 238, 0.016); + /* === 用途化变量 — 按钮阴影 === */ + --btn-primary-shadow: rgba(167, 139, 250, 0.16); + /* === 用途化变量 — Canvas 动画背景叠加色 === */ + --canvas-bg-overlay: rgba(252, 252, 250, 0.74); + /* === 用途化变量 — Canvas 动画光晕遮罩末端 === */ + --canvas-glow-overlay-end: rgba(249, 248, 245, 0.56); + /* === 用途化变量 — 歌词消息 fallback === */ + --lyric-msg-fallback-text: #333338; + /* === 用途化变量 — 模态框阴影 === */ + --modal-hover-shadow: rgba(167, 139, 250, 0.06); + /* === 用途化变量 — 移动端表格阴影 === */ + --mobile-table-shadow: rgba(0, 0, 0, 0.4); +} diff --git a/app/assets/css/transitions.css b/app/assets/css/transitions.css new file mode 100644 index 0000000..2fe3a5e --- /dev/null +++ b/app/assets/css/transitions.css @@ -0,0 +1,265 @@ +/* 页面过渡动画 */ + +/* 通用页面过渡 */ +.page-enter-active, +.page-leave-active { + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.page-enter-from { + opacity: 0; + transform: translateY(20px) scale(0.98); +} + +.page-leave-to { + opacity: 0; + transform: translateY(-20px) scale(1.02); +} + +.page-view { + width: 100%; + min-width: 0; + animation: page-view-enter 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94); + /* will-change 声明 transform/filter 会使本元素成为 fixed 定位包含块,导致未 Teleport 的弹窗遮罩错位,仅保留 opacity */ + will-change: opacity; +} + +.page-view[data-route='index'] { + animation-duration: 0.75s; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + animation-name: page-view-index-enter; +} + +/* 强制改密页必须立即稳定居中,避免路由重定向时出现位移。 */ +.page-view[data-route='change-password'] { + min-height: 100dvh; + animation: none !important; + opacity: 1; + transform: none !important; + filter: none !important; + will-change: auto; +} + +@keyframes page-view-enter { + 0% { + opacity: 0; + transform: translateY(20px) scale(0.98); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes page-view-index-enter { + 0% { + opacity: 0; + transform: translateY(30px) scale(0.96); + filter: blur(6px); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +@keyframes page-view-mobile-enter { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +/* 首页特殊过渡 */ +.page-enter-active[data-route='index'], +.page-leave-active[data-route='index'] { + transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); +} + +.page-enter-from[data-route='index'] { + opacity: 0; + transform: translateY(30px) scale(0.96); + filter: blur(6px); +} + +.page-leave-to[data-route='index'] { + opacity: 0; + transform: translateY(-30px) scale(1.04); + filter: blur(6px); +} + +/* 布局过渡 */ +.layout-enter-active, +.layout-leave-active { + transition: all 0.3s ease-in-out; +} + +.layout-enter-from, +.layout-leave-to { + opacity: 0; + transform: scale(0.98); +} + +/* 加载动画 */ +.page-loading { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-spinner { + width: 60px; + height: 60px; + border: 3px solid var(--overlay-10); + border-top: 3px solid var(--color-indigo); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* 特殊效果:从首页到后台的滑动效果 */ +.slide-left-enter-active, +.slide-left-leave-active { + transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: absolute; + width: 100%; +} + +.slide-left-enter-from { + transform: translateX(100%); + opacity: 0; +} + +.slide-left-leave-to { + transform: translateX(-100%); + opacity: 0; +} + +/* 特殊效果:从后台到首页的滑动效果 */ +.slide-right-enter-active, +.slide-right-leave-active { + transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: absolute; + width: 100%; +} + +.slide-right-enter-from { + transform: translateX(-100%); + opacity: 0; +} + +.slide-right-leave-to { + transform: translateX(100%); + opacity: 0; +} + +/* 淡入淡出效果 */ +.fade-enter-active, +.fade-leave-active { + transition: all 0.4s ease-in-out; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +/* 缩放效果 */ +.scale-enter-active, +.scale-leave-active { + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.scale-enter-from { + opacity: 0; + transform: scale(0.9); +} + +.scale-leave-to { + opacity: 0; + transform: scale(1.1); +} + +/* 移动端优化 */ +@media (max-width: 768px) { + .page-enter-active, + .page-leave-active { + transition-duration: 0.3s; + } + + .page-view, + .page-view[data-route='index'] { + animation-duration: 0.3s; + animation-name: page-view-mobile-enter; + will-change: opacity; + } + + .slide-left-enter-active, + .slide-left-leave-active, + .slide-right-enter-active, + .slide-right-leave-active { + transition-duration: 0.3s; + } + + .page-enter-from, + .page-leave-to { + transform: translateY(15px) scale(0.99); + } +} + +/* 减少动画效果(用户偏好) */ +@media (prefers-reduced-motion: reduce) { + .page-enter-active, + .page-leave-active, + .layout-enter-active, + .layout-leave-active, + .slide-left-enter-active, + .slide-left-leave-active, + .slide-right-enter-active, + .slide-right-leave-active, + .scale-enter-active, + .scale-leave-active, + .fade-enter-active, + .fade-leave-active { + transition-duration: 0.1s; + } + + .page-enter-from, + .page-leave-to { + transform: none; + filter: none; + } + + .page-view, + .page-view[data-route='index'] { + animation-duration: 0.1s; + animation-name: page-view-reduced-enter; + } +} + +@keyframes page-view-reduced-enter { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/app/assets/css/variables.css b/app/assets/css/variables.css new file mode 100644 index 0000000..f6a61f7 --- /dev/null +++ b/app/assets/css/variables.css @@ -0,0 +1,156 @@ +/* 全局样式与媒体查询(设计变量已拆分到 themes/ClassicDark/ 和 themes/ClassicLight/ 子目录) */ + +/* 品牌色 RGB 值(用于 rgba() 动态组合) */ +:root { + --color-indigo-rgb: 99 102 241; +} + +/* 高对比度模式支持 */ +@media (prefers-contrast: high) { + :root { + --border-primary: #444444; + --border-secondary: #555555; + --border-tertiary: #666666; + --text-tertiary: #aaaaaa; + --text-quaternary: #888888; + } +} + +/* 减少动画模式支持 */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition-fast: 0s; + --transition-normal: 0s; + --transition-slow: 0s; + } +} + +/* 全局滚动条样式 */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} + +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: var(--scrollbar-track); +} + +*::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: var(--radius-md); +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +/* 全局选择文本样式 */ +::selection { + background: var(--primary-light); + color: var(--text-primary); +} + +/* 全局焦点样式 */ +*:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +*:focus:not(:focus-visible) { + outline: none; +} + +/* 全局禁用状态样式 */ +*:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +/* 全局链接样式 */ +a { + color: var(--primary); + text-decoration: none; + transition: color var(--transition-normal); +} + +a:hover { + color: var(--primary-hover); +} + +/* 全局按钮重置 */ +button { + border: none; + background: none; + cursor: pointer; + font-family: inherit; +} + +* { + -webkit-tap-highlight-color: transparent; +} + +button, +a, +input, +textarea { + -webkit-tap-highlight-color: transparent; +} + +@media (hover: none) and (pointer: coarse) { + *:focus { + outline: none !important; + } + button, + a { + -webkit-user-select: none; + user-select: none; + } +} + +/* 全局输入框重置 */ +input, +textarea, +select { + font-family: inherit; + font-size: inherit; +} + +/* 全局图片样式 */ +img { + max-width: 100%; + height: auto; +} + +/* 全局表格样式 */ +table { + border-collapse: collapse; + width: 100%; +} + +/* 全局代码样式 */ +code, +pre { + font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; + background: var(--bg-tertiary); + border-radius: var(--radius-sm); +} + +code { + padding: 2px 4px; + font-size: 0.875em; +} + +pre { + padding: var(--spacing-lg); + overflow-x: auto; +} + +pre code { + padding: 0; + background: none; +} diff --git a/app/assets/css/year-review.css b/app/assets/css/year-review.css new file mode 100644 index 0000000..69ce3dc --- /dev/null +++ b/app/assets/css/year-review.css @@ -0,0 +1,177 @@ +/* Year Review Global Styles */ + +/* Animations */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes reverse-spin { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-20px); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.animate-spin-slow { + animation: spin 12s linear infinite; +} + +.animate-reverse-spin-slow { + animation: reverse-spin 15s linear infinite; +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.animate-bounce { + animation: bounce 1s infinite; +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +/* Utilities */ +.text-transparent { + color: transparent; +} + +.bg-clip-text { + -webkit-background-clip: text; + background-clip: text; +} + +.backdrop-blur-md { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.backdrop-blur-sm { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +/* Gradients */ +.bg-gradient-intro { + background: linear-gradient(135deg, var(--panel-bg-dark) 0%, var(--bg-primary) 50%, var(--panel-bg-dark) 100%); +} + +.bg-gradient-text-intro { + background-image: linear-gradient(to right, var(--panel-bg), var(--panel-border-light), var(--text-muted)); +} + +.bg-gradient-artist { + background: linear-gradient(to bottom right, var(--color-collab-lighter), var(--color-pink), var(--color-orange)); +} + +.bg-gradient-numbers { + background-image: linear-gradient(to bottom, var(--color-accent-light), var(--color-accent-light-hover)); +} + +/* Slide Transitions */ +.slide-up-enter-active, +.slide-up-leave-active, +.slide-down-enter-active, +.slide-down-leave-active { + transition: all 0.5s ease-in-out; + position: absolute; + width: 100%; + height: 100%; +} + +.slide-up-enter-from { + transform: translateY(100%); + z-index: 10; +} + +.slide-up-leave-to { + transform: translateY(-20%); /* Parallax effect */ + opacity: 0.5; + z-index: 0; +} + +.slide-down-enter-from { + transform: translateY(-100%); + z-index: 10; +} + +.slide-down-leave-to { + transform: translateY(20%); /* Parallax effect */ + opacity: 0.5; + z-index: 0; +} + +/* Content Entry Animations */ +.entry-enter-active { + transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); +} + +.entry-enter-from { + opacity: 0; + transform: translateY(40px); +} + +.entry-enter-to { + opacity: 1; + transform: translateY(0); +} + +.delay-100 { + transition-delay: 100ms; +} +.delay-200 { + transition-delay: 200ms; +} +.delay-300 { + transition-delay: 300ms; +} +.delay-400 { + transition-delay: 400ms; +} +.delay-500 { + transition-delay: 500ms; +} diff --git a/app/components/AMLL/LyricPlayer.vue b/app/components/AMLL/LyricPlayer.vue new file mode 100644 index 0000000..9a0e7f6 --- /dev/null +++ b/app/components/AMLL/LyricPlayer.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/app/components/Account/SocialBindings.vue b/app/components/Account/SocialBindings.vue new file mode 100644 index 0000000..63e2653 --- /dev/null +++ b/app/components/Account/SocialBindings.vue @@ -0,0 +1,627 @@ + + + diff --git a/app/components/Admin/ApiKeyManager.vue b/app/components/Admin/ApiKeyManager.vue new file mode 100644 index 0000000..17e5a2e --- /dev/null +++ b/app/components/Admin/ApiKeyManager.vue @@ -0,0 +1,1165 @@ + + + + + diff --git a/app/components/Admin/BlacklistManager.vue b/app/components/Admin/BlacklistManager.vue new file mode 100644 index 0000000..dfc77f3 --- /dev/null +++ b/app/components/Admin/BlacklistManager.vue @@ -0,0 +1,673 @@ + + + + + diff --git a/app/components/Admin/CardCodesManager.vue b/app/components/Admin/CardCodesManager.vue new file mode 100644 index 0000000..8bfd696 --- /dev/null +++ b/app/components/Admin/CardCodesManager.vue @@ -0,0 +1,822 @@ +