commit 4ce4649aaf1244fbf34d9d1b6643464ef5270088 Author: HenryDu8133 <813367384@qq.com> Date: Sun Sep 6 08:19:02 2026 +0800 初次提交 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4e46f8a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +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* +app/node_modules/ +VoiceHub-main/ +app/pages/app/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b85a6e2 --- /dev/null +++ b/.env.example @@ -0,0 +1,110 @@ +# 数据库连接地址(必填) +# 格式: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 配置(可选) +# 错误监控与性能追踪,配置后系统将自动上报运行时异常 +# +# SENTRY_DSN 客户端(浏览器)使用的 DSN(公开可暴露) +# 示例:https://publicKey@o0.ingest.sentry.io/0 +# SENTRY_DSN= +# +# SENTRY_SERVER_DSN 服务端(Node)使用的 DSN(保密,不应暴露给浏览器) +# 如未单独配置,服务端会自动降级使用 SENTRY_DSN +# SENTRY_SERVER_DSN= +# +# 采样率配置(0~1 的小数,默认 1.0 表示 100%) +# 错误采样率 +# SENTRY_ERROR_SAMPLE_RATE=1.0 +# 性能追踪(Traces)采样率 +# SENTRY_TRACES_SAMPLE_RATE=1.0 +# 浏览器会话回放(Replay)采样率,需同时设置下面两项才会生效 +# 生产环境建议分别设为 0.1 和 1.0,即 10% 正常用户 + 100% 错误用户进行回放 +# SENTRY_REPLAY_SESSION_SAMPLE_RATE= +# SENTRY_REPLAY_ON_ERROR_SAMPLE_RATE= +# +# 项目版本号,用于区分不同版本的错误;未填时自动使用 package.json 的 version +# SENTRY_RELEASE= +# +# 环境标签,默认继承 NODE_ENV +# SENTRY_ENVIRONMENT= + +# Redis 缓存服务(可选,暂不推荐) +# 如果设置了 REDIS_URL,系统将自动启用 Redis 缓存功能 +# 格式: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..62f108c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,124 @@ +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: 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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..163e716 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,90 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + day: 'saturday' + time: '09:00' + timezone: 'Asia/Shanghai' + open-pull-requests-limit: 10 + commit-message: + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + labels: + - 'dependencies' + - 'automated' + - 'chore' + allow: + - dependency-type: 'direct' + - dependency-type: 'indirect' + ignore: + # 忽略主要版本更新,避免破坏性变更 + - dependency-name: '*' + update-types: ['version-update:semver-major'] + groups: + # 将所有非重大(minor/patch)更新合并到一个 PR + npm-minor-patch: + applies-to: version-updates + patterns: + - '*' + update-types: + - 'minor' + - 'patch' + + # Enable version updates for Docker + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'weekly' + day: 'saturday' + time: '09:00' + timezone: 'Asia/Shanghai' + open-pull-requests-limit: 5 + reviewers: + - 'laoshuikaixue' + assignees: + - 'laoshuikaixue' + commit-message: + prefix: 'chore' + include: 'scope' + labels: + - 'dependencies' + - 'automated' + - 'chore' + ignore: + - dependency-name: '*node*' + groups: + docker-all: + applies-to: version-updates + patterns: + - '*' + + # Enable version updates for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + day: 'saturday' + time: '09:00' + timezone: 'Asia/Shanghai' + open-pull-requests-limit: 5 + commit-message: + prefix: 'chore' + include: 'scope' + labels: + - 'dependencies' + - 'automated' + - 'chore' + groups: + gha-all: + applies-to: version-updates + patterns: + - '*' diff --git a/.github/workflows/build-fpk.yml b/.github/workflows/build-fpk.yml new file mode 100644 index 0000000..e5f305b --- /dev/null +++ b/.github/workflows/build-fpk.yml @@ -0,0 +1,107 @@ +# .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: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: 读取版本号 & 安装 fnpack + run: | + VER=$(node -p "require('./package.json').version") + echo "APP_VER=$VER" >> "$GITHUB_ENV" + echo "📦 版本: $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/images/logo.png voicehub/app/ui/images/icon-64.png + cp public/images/logo.png voicehub/app/ui/images/icon-256.png + cp public/images/logo.png voicehub/ICON.PNG + cp public/images/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 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..5cc8261 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,84 @@ +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 + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # 提取版本号并处理成不同格式 + - name: Extract version from package.json + id: extract-version + run: | + # 提取完整版本号 + FULL_VERSION=$(node -p "require('./package.json').version") + # 提取主版本.次版本 (如 1.2 从 1.2.3) + 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 QEMU + uses: docker/setup-qemu-action@v4 + with: + platforms: arm,arm64 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + # 使用从package.json提取的完整版本号 + type=raw,value=${{ steps.extract-version.outputs.full-version }} + # 使用主版本.次版本格式 + type=raw,value=${{ steps.extract-version.outputs.major-minor-version }} + + - name: Build and push Docker image + id: build-and-push + 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,linux/arm64,linux/arm/v7 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff6d922 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist +docs + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Backups +backups/ +*.backup + +# Misc +.DS_Store +.fleet +.idea +.kiro +.cursor +.vercel +.netlify +.vscode +.trae +.qoder +.figma +scripts/package-lock.json + +# 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5202258 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# VoiceHub Code of Conduct + +## Our Commitment + +VoiceHub is an open source, community-driven campus broadcasting and song request system. + +We are committed to providing a friendly, respectful, and harassment-free environment for everyone who participates in this project — including users, contributors, maintainers, and community members — regardless of age, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual orientation. + +We aim to foster an open, inclusive, and constructive community where collaboration and learning are encouraged. + +--- + +## Expected Behavior + +We encourage all participants to: + +- Be respectful and considerate in discussions, reviews, and feedback +- Communicate thoughtfully, especially when opinions differ +- Welcome newcomers and help them learn +- Provide constructive criticism focused on improving the project +- Take responsibility for mistakes and be willing to correct them +- Respect the campus-oriented and community-serving nature of this project + +--- + +## Unacceptable Behavior + +The following behaviors are not tolerated within the VoiceHub community: + +- Harassment, discrimination, or exclusion of any kind +- Insulting, demeaning, or hostile language +- Personal attacks, threats, or intimidation +- Sexualized language, imagery, or unwanted attention +- Publishing others’ private information without explicit permission +- Deliberate disruption of discussions, development, or community activities +- Any behavior that would be considered inappropriate in an educational or professional environment + +--- + +## Scope + +This Code of Conduct applies to all VoiceHub-related spaces, including but not limited to: + +- GitHub repositories, issues, pull requests, and discussions +- Official communication channels and community platforms +- Public spaces where individuals represent VoiceHub, including events, documentation, or social media + +--- + +## Enforcement + +If you experience or witness behavior that violates this Code of Conduct, please report it to the project maintainers. + +All reports will be reviewed fairly and promptly. Maintainers will handle reports with discretion and respect the privacy of all parties involved. + +Project maintainers have the responsibility and authority to take appropriate action in response to violations. + +--- + +## Enforcement Actions + +Depending on the severity and frequency of the behavior, maintainers may take one or more of the following actions: + +1. **Private Reminder** – A private message explaining the issue and expected behavior. +2. **Formal Warning** – A clear warning with expectations for future conduct. +3. **Temporary Restriction** – Temporary limitation or suspension from community interaction. +4. **Permanent Ban** – Permanent removal from participation in the project or community spaces. + +Actions will be taken in a manner proportional to the impact on the community. + +--- + +## Attribution + +This Code of Conduct is inspired by the Contributor Covenant, version 2.1. +https://www.contributor-covenant.org/version/2/1/code_of_conduct diff --git a/CODE_OF_CONDUCT.zh-CN.md b/CODE_OF_CONDUCT.zh-CN.md new file mode 100644 index 0000000..2469832 --- /dev/null +++ b/CODE_OF_CONDUCT.zh-CN.md @@ -0,0 +1,76 @@ +# VoiceHub 行为准则 + +## 我们的承诺 + +VoiceHub 是一个面向校园与社区的开源广播与点歌系统项目。 + +我们致力于为所有参与本项目的人 —— 包括用户、贡献者、维护者以及社区成员 —— 提供一个友善、尊重、免于骚扰的环境,无论其年龄、残疾状况、族裔、性别认同与表达、经验水平、国籍、外表、宗教或性取向如何。 + +我们希望构建一个开放、包容、理性且富有建设性的社区氛围,鼓励协作与学习。 + +--- + +## 鼓励的行为 + +我们鼓励所有参与者: + +- 在讨论、评审和反馈中保持尊重与克制 +- 在观点不一致时进行理性、善意的交流 +- 友好对待新参与者,帮助他们熟悉项目 +- 提供以改进项目为目标的建设性意见 +- 勇于承认并纠正错误 +- 尊重 VoiceHub 作为校园与社区服务项目的定位 + +--- + +## 不可接受的行为 + +在 VoiceHub 社区中,以下行为是不被允许的: + +- 任何形式的骚扰、歧视或排斥行为 +- 侮辱性、贬损性或带有敌意的言辞 +- 人身攻击、威胁或恐吓 +- 带有性暗示的语言、图像或不受欢迎的关注 +- 未经许可公开他人的私人信息 +- 蓄意干扰社区讨论、开发流程或正常协作 +- 任何在教育或专业环境中被认为不恰当的行为 + +--- + +## 适用范围 + +本行为准则适用于所有与 VoiceHub 相关的空间,包括但不限于: + +- GitHub 仓库、Issue、Pull Request 与讨论区 +- 官方社区交流渠道 +- 任何以 VoiceHub 名义进行的公开活动、文档或社交媒体发言 + +--- + +## 执行与举报 + +如果你遭遇或目睹了违反本行为准则的情况,请向项目维护者进行反馈。 + +所有举报都会被认真、公正地对待。维护者将尊重相关人员的隐私,并谨慎处理相关信息。 + +项目维护者有责任并有权对违规行为采取适当措施。 + +--- + +## 处理措施 + +根据违规行为的严重程度和影响,维护者可能采取以下一种或多种措施: + +1. **私下提醒** —— 通过私下沟通说明问题并明确期望行为 +2. **正式警告** —— 明确指出违规行为及后续要求 +3. **临时限制** —— 暂时限制或暂停其参与社区互动 +4. **永久封禁** —— 永久取消其参与项目或社区的资格 + +所有处理措施都会遵循“与社区影响相匹配”的原则。 + +--- + +## 致谢 + +本行为准则参考并改编自 Contributor Covenant 2.1 版本: +https://www.contributor-covenant.org/version/2/1/code_of_conduct diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..159407e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# 支持: linux/amd64, linux/arm64, linux/arm/v7 + +# ========================================== +# 第一阶段:依赖安装与构建阶段 +# ========================================== +FROM node:22-alpine AS builder + +WORKDIR /app +ENV NODE_OPTIONS="--max-old-space-size=1024" +ENV VITE_BUILD_THREADS=1 +ENV NITRO_PRESET=node-server +# 1. 仅复制依赖相关文件和 scripts 目录(因为 postinstall 需要),利用 Docker 缓存层 +COPY package.json package-lock.json ./ +COPY scripts ./scripts + +# 配置 npm 镜像加速并安装依赖,严格内存优化 +RUN set -eux; \ + npm config set registry https://registry.npmmirror.com; \ + npm config set maxsockets 3; \ + npm config set progress false; \ + npm config set cache /tmp/npm-cache; \ + npm install --package-lock-only --ignore-scripts; \ + npm ci --prefer-offline --no-audit --no-fund --maxsockets=3 + +# 2. 复制源代码并进行构建 +# 只有在 package.json 没变而源代码改变时,才会从这一步开始重新执行,跳过上面的耗时依赖安装 +COPY . . + +# 构建应用,使用内存优化脚本适应4GB总内存 +RUN chmod +x scripts/memory-build.js && \ + node scripts/memory-build.js +RUN npm prune --omit=dev + +# ========================================== +# 第二阶段:运行阶段 +# ========================================== +FROM node:22-alpine AS runtime + +USER root +WORKDIR /app + +# 从构建阶段复制必要文件,剔除无关的开发依赖 +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/package-lock.json ./ +COPY --from=builder /app/drizzle.config.ts ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/.output ./.output +COPY --from=builder /app/app/drizzle ./app/drizzle +COPY --from=builder /app/scripts ./scripts +# 如果有 vendor 目录再取消注释 +# COPY --from=builder /app/vendor ./vendor + +# 环境变量配置 +ENV NODE_ENV=production \ + PORT=3000 \ + NPM_CONFIG_UPDATE_NOTIFIER=false \ + PREBUILT=true \ + NODE_OPTIONS="--max-old-space-size=512" + +# 暴露端口 +EXPOSE $PORT + +# 启动命令:先执行数据库迁移,再启动应用 +CMD ["sh", "-c", "node scripts/deploy.js && node .output/server/index.mjs"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd43398 --- /dev/null +++ b/README.md @@ -0,0 +1,1745 @@ +# VoiceHub - 校园广播站点歌系统 + +这是一个使用Nuxt 4全栈框架开发的现代化校园广播站点歌管理系统。系统提供完整的点歌、投票、排期管理、通知推送、数据分析、权限控制和数据库管理功能,支持多角色权限管理和灵活的系统配置。 + +
+ +[交流群](https://qm.qq.com/cgi-bin/qm/qr?k=5DV4vGlqn82YaNi7a3xW4zjmS8ZUr6cz&jump_from=webapi&authKey=axAl02PMsIVVAwrXij0YUUrOrUTeLpqLipu5XcTvyBUOzeWaOnicBB+fmBwNJs5S) | [使用学校收集表](https://laoshuikaixue.feishu.cn/share/base/form/shrcniUKakpNYP6KH7qrU20qq5e) | [项目宣传片](https://www.bilibili.com/video/BV1B9ArzMEkA) + +
+ +## 项目截图 + +image +image +image +image +image + +## 主要功能 + +### 🎵 核心功能 + +- **智能点歌系统**:用户可以点歌或给已有歌曲投票,支持网易云音乐、QQ音乐和哔哩哔哩搜索,可选择期望播出时段 +- **多平台登录支持**: + - **网易云音乐登录**:支持扫码登录,登录后可搜索个人歌单、收藏及播客电台内容 + - **一键添加到歌单**:登录后支持将排期中的网易云音乐歌曲一键添加到个人歌单 + - **从歌单投稿**:支持从个人歌单中直接投稿歌曲到系统 + - **从最近播放投稿**:支持从最近播放记录中投稿歌曲 + - **播客电台投稿**:支持搜索和投稿播客电台内容 +- **投稿限额管理**:灵活配置用户投稿限制,支持按时间段、用户角色设置不同的投稿额度,有效控制系统负载 +- **歌曲去重功能**:智能识别重复歌曲,优化歌曲库管理,避免重复播放 +- **歌曲管理**:按热度排序,避免重复播放,动态URL防止链接过期,支持黑名单管理 +- **音乐播放器**:内置音乐播放器,支持进度控制和音质实时切换 +- **音质切换**:支持多种音质选择(标准、HQ、无损、Hi-Res等),动态获取最新播放链接 +- **音乐下载功能**:支持管理员下载歌曲到本地,提供多种音质选择和批量下载 +- **歌曲重播功能**:支持用户对已播放过的歌曲发起重播申请,支持查看申请记录和撤回申请 + +### 👥 用户管理 + +- **用户管理**:管理员添加用户,支持按年级班级分类 +- **权限控制**:多级权限管理,支持普通用户、管理员、超级管理员 +- **黑名单管理**:支持歌曲和艺术家黑名单,自动过滤不当内容 + +### 📅 排期管理 + +- **排期管理**:管理员可以通过拖拽界面进行歌曲排期和顺序管理 +- **排期草稿**:支持保存排期草稿功能,允许管理员分步完成排期安排 + - 草稿状态不影响公开展示,可随时修改和完善 + - 支持草稿发布为正式排期,确保排期质量 +- **播出时段**:灵活配置播出时段,**支持多时段管理** +- **打印排期**:支持自定义纸张大小、内容选择、编写备注和PDF导出的打印功能 +- **学期管理**:管理员可设置当前学期,自动关联点歌记录 +- **公开展示**:公开展示歌曲播放排期,按日期分组展示 + +### 🔔 通知系统 + +- **实时通知**:歌曲被选中、投票和系统通知 +- **通知设置**:用户可自定义通知偏好,支持独立页面设置 +- **批量通知**:管理员可向特定用户群体发送通知 +- **社交账号绑定**:支持绑定MeoW等账号,同步推送通知到外部平台 +- **验证码验证**:安全的验证码验证机制,支持动态样式反馈 + +### 💾 数据管理 + +- **数据库备份**:完整的数据库备份和恢复功能 +- **数据库重置**:支持安全的数据库重置操作,可选择性保留用户数据或完全重置 +- **文件导入导出**:支持备份文件的上传、下载和管理 +- **数据库自检**:自动数据库验证和修复机制,确保系统稳定性 + +### 🎨 用户体验 + +- **现代UI**:响应式设计,深色主题,流畅的动画效果 +- **玻璃态设计**:现代化的视觉效果和交互体验 +- **交互反馈**:hover效果,点击反馈,状态变化动画 +- **移动端优化**:适配支持移动设备访问,触摸友好的交互设计 + +## 技术栈 + +### 前端技术 + +- **Nuxt 4**:Vue.js全栈框架,提供SSR和SPA支持 +- **Vue 3**:响应式前端框架,使用Composition API +- **TypeScript**:类型安全的JavaScript,提供完整的类型定义 +- **Tailwind CSS**:实用优先的CSS框架,响应式设计 +- **Vue Router**:前端路由管理 + +### 后端技术 + +- **Nuxt Server API**:服务端API路由,支持中间件和认证 +- **Drizzle ORM**:现代化数据库ORM,提供类型安全的数据库操作和高性能查询 +- **Neon Database**:Serverless PostgreSQL数据库,支持自动启停和无缝扩展 +- **PostgreSQL**:关系型数据库,支持复杂查询和事务处理 +- **Redis**:高性能缓存数据库,提升系统响应速度(可选,暂不推荐,可能存在潜在的问题) +- **JWT**:标准JWT认证机制,支持24小时token有效期 +- **bcrypt**:密码加密,安全的哈希算法 +- **Multer**:文件上传处理,支持多种存储方式 + +## 系统架构 + +系统采用了现代化的 Serverless 全栈架构: + +- **前端**:使用 Nuxt 4 + Vue 3 组合式API构建响应式用户界面 +- **后端**:使用 Nuxt Server API 构建 RESTful API 服务 +- **数据库**:使用 Drizzle ORM + Neon Database,提供类型安全和高性能的数据库操作 +- **认证**:标准 JWT 认证系统 +- **缓存**:可选的 Redis 缓存层,提升系统响应速度 +- **部署**:支持 Vercel、Netlify、EdgeOne 等 Serverless 平台一键部署 + +## 部署指南 + +### 一键部署 + +本项目可以一键部署到Vercel/Netlify/EdgeOne平台: + +[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flaoshuikaixue%2FVoiceHub&env=DATABASE_URL,JWT_SECRET,NODE_ENV&envDefaults=%7B%22NODE_ENV%22%3A%22production%22%7D&envDescription=%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AF%B4%E6%98%8E&envLink=https%3A%2F%2Fgithub.com%2Flaoshuikaixue%2FVoiceHub%23%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AF%B4%E6%98%8E) +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/laoshuikaixue/VoiceHub) +[![Deploy to EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https://github.com/laoshuikaixue/VoiceHub&env=DATABASE_URL,JWT_SECRET&env-description=%E9%9C%80%E8%A6%81%E9%85%8D%E7%BD%AE%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9C%B0%E5%9D%80%E3%80%81JWT%E5%AF%86%E9%92%A5) + +在部署过程中,需要输入必要的环境变量: + +1. `DATABASE_URL`:PostgreSQL数据库连接地址 +2. `JWT_SECRET`:JWT令牌签名密钥 + +### Claw 部署 + +[![Claw](https://ap-southeast-1.run.claw.cloud/logo.svg)](https://ap-southeast-1.run.claw.cloud/) + +1. **点击部署按钮**:选择上方的 Claw 部署按钮 +2. **打开应用程序启动板**:打开 App Launchpad (应用程序启动板) +3. **创建应用**:选 Create App (创建应用) +4. **相关配置**: + ``` + Application Name:VoiceHub 或 其它 + Image Name: ghcr.io/laoshuikaixue/voicehub:latest + Usage:按需调整 + Network:3000 ,开 Public Access + Environment Variables: + DATABASE_URL=postgresql://user:password@postgres:5432/voicehub + JWT_SECRET=your-jwt-secret-here + # 按实际情况填写 + ``` +5. **等待部署**:平台会自动构建和部署应用 +6. **访问应用**:部署完成后,您将获得一个可访问的 URL + +### Docker 部署 + +VoiceHub 支持通过 Docker 进行容器化部署,提供了多种部署方式。 + +#### 方式一:使用 Docker Compose(推荐) + +这是最简单的部署方式,会自动创建应用和数据库容器。 + +1. 克隆项目 + +```bash +git clone https://github.com/laoshuikaixue/VoiceHub.git +cd VoiceHub +``` + +2. 修改 docker-compose.yml 中的环境变量 + +```yaml +environment: + - DATABASE_URL=postgresql://user:password@postgres:5432/voicehub + - JWT_SECRET=your-jwt-secret-here # 请修改为强随机字符串 + - NODE_ENV=production +``` + +3. 启动服务 + +```bash +docker-compose up -d +``` + +4. 访问应用 + 打开浏览器访问 http://localhost:3000 + +默认管理员账号: + +- 用户名:admin +- 密码:admin123 + +#### 方式二:使用预构建镜像 + +如果你已有 PostgreSQL 数据库,可以直接使用预构建的镜像。 + +使用 GitHub 镜像源: + +```bash +docker run -d \ + -p 3000:3000 \ + -e DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require" \ + -e JWT_SECRET="your-very-secure-jwt-secret-key" \ + -e NODE_ENV=production \ + --name voicehub \ + ghcr.io/laoshuikaixue/voicehub:latest +``` + +使用南京大学镜像源: + +```bash +docker run -d \ + -p 3000:3000 \ + -e DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require" \ + -e JWT_SECRET="your-very-secure-jwt-secret-key" \ + -e NODE_ENV=production \ + --name voicehub \ + ghcr.nju.edu.cn/laoshuikaixue/voicehub:latest +``` + +#### 方式三:本地构建镜像 + +如果需要自定义构建,可以本地构建镜像。 + +```bash +git clone https://github.com/laoshuikaixue/VoiceHub.git +cd VoiceHub + +# 构建镜像(不使用缓存,确保完全重新构建) +docker build --no-cache -t voicehub . + +# 运行容器 +docker run -d \ + -p 3000:3000 \ + -e DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require" \ + -e JWT_SECRET="your-very-secure-jwt-secret-key" \ + -e NODE_ENV=production \ + --name voicehub \ + voicehub +``` + +### Linux 服务器部署 + +本项目提供了针对 Ubuntu/Debian 服务器的一键部署脚本,支持自动安装 Node.js 22、配置环境变量、安装依赖和构建项目。 + +**一键部署命令:** + +```bash +bash <(curl -sL https://raw.githubusercontent.com/laoshuikaixue/VoiceHub/main/sh/deploy.sh) +``` + +**更新部署:** +项目更新时,可使用更新脚本快速更新: + +```bash +# 一键更新命令 +bash <(curl -sL https://raw.githubusercontent.com/laoshuikaixue/VoiceHub/main/sh/update.sh) +``` + +**日常管理:** +部署完成后,可使用 `voicehub` 命令进行日常管理(需在部署时安装) + +### 本地开发部署 + +#### 前提条件 + +- Node.js 20+ +- PostgreSQL 数据库(推荐使用 Neon) +- Redis 数据库(可选,暂不推荐) + +#### 快速开始 + +1. 克隆项目 + +```bash +git clone https://github.com/laoshuikaixue/VoiceHub.git +cd VoiceHub +``` + +2. 安装依赖 + +```bash +pnpm install --frozen-lockfile +``` + +3. 配置环境变量 + +复制 `.env.example` 文件并重命名为 `.env`: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件,配置必需的环境变量: + +```env +# 数据库连接地址(必填) +DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require" + +# JWT 认证密钥(必填) +JWT_SECRET="your-very-secure-jwt-secret-key" + +# 应用运行环境(可选) +NODE_ENV=development +``` + +4. 初始化数据库 + +首次运行需要初始化数据库结构: + +```bash +# 生成数据库迁移文件 +pnpm run db:generate + +# 执行数据库迁移 +pnpm run db:migrate +``` + +或使用一键部署命令(推荐): + +```bash +pnpm run deploy +``` + +5. 创建管理员账户 + +系统会在首次部署时自动创建管理员账户。如需手动创建: + +```bash +pnpm run create-admin +``` + +默认管理员账户: + +- 用户名:admin +- 密码:admin123 + +6. 启动开发服务器 + +```bash +pnpm run dev +``` + +应用将在 http://localhost:3000 启动。 + +### 生产环境部署 + +1. 构建生产版本 + +```bash +pnpm run build +``` + +2. 启动生产服务器 + +```bash +pnpm run start +``` + +### 数据库管理命令 + +```bash +# 生成迁移文件 +pnpm run db:generate + +# 执行数据库迁移 +pnpm run db:migrate + +# 推送模式变更到数据库(开发环境) +pnpm run db:push + +# 启动 Drizzle Studio(数据库管理界面) +pnpm run db:studio + +# 清空数据库并重新创建管理员 +pnpm run clear-db + +# 安全迁移(带备份) +pnpm run safe-migrate +``` + +### 升级与迁移 + +有关如何升级现有部署和迁移数据,请参阅 [升级指南](UPGRADE.md)。 + +## 系统配置 + +### 站点配置管理 + +VoiceHub 提供了完整的站点配置管理功能,支持通过管理后台动态配置系统参数: + +#### 基本信息配置 + +- **站点标题**:自定义系统显示名称 +- **站点描述**:系统功能描述和介绍 +- **站点Logo**:支持上传自定义Logo图片 + +#### 播放时段管理 + +- **多时段支持**:支持配置多个播放时段(如午间、晚间) +- **时段名称**:自定义时段显示名称 +- **开始/结束时间**:精确到分钟的时间控制 +- **时段排序**:支持拖拽调整时段显示顺序 + +#### 通知系统配置 + +- **通知开关**:控制系统通知功能的启用状态 +- **通知类型**:配置不同类型通知的发送规则 +- **通知模板**:自定义通知消息的格式和内容 + +### 数据库备份与恢复 + +系统提供了完整的数据备份和恢复解决方案: + +#### 备份功能 + +- **完整备份**:包含所有数据表的完整系统备份 +- **用户数据备份**:仅备份用户相关数据(用户、歌曲、投票等) +- **增量备份**:支持基于时间的增量备份策略 + +#### 恢复功能 + +- **合并模式**:将备份数据与现有数据合并,保留现有数据 +- **替换模式**:完全替换现有数据(谨慎使用) +- **数据验证**:恢复前自动验证备份文件完整性 + +### 权限与角色管理 + +VoiceHub 实现了细粒度的权限控制系统: + +#### 角色类型 + +- **超级管理员 (SUPER_ADMIN)**:拥有所有系统权限,包括用户管理、系统配置、数据库管理等 +- **管理员 (ADMIN)**:拥有日常管理权限,如用户管理、排期管理、歌曲管理、系统配置等 +- **歌曲管理员 (SONG_ADMIN)**:专门负责歌曲相关管理,包括排期管理、歌曲管理、打印排期等 +- **普通用户 (USER)**:基本的点歌、投票和查看权限 + +#### 权限分类 + +- **内容管理权限**:排期管理、歌曲管理、打印排期等 +- **用户管理权限**:创建、编辑、删除用户账户 +- **系统管理权限**:通知管理、播放时间管理、学期管理、黑名单管理、站点配置、数据库管理等 + +#### 权限继承与分配 + +- **SUPER_ADMIN**:拥有所有权限 +- **ADMIN**:拥有除数据库管理外的所有权限 +- **SONG_ADMIN**:拥有内容管理相关权限(排期、歌曲、打印) +- **USER**:仅拥有基本的点歌和查看权限 + +#### 权限验证 + +- 前端基于角色动态显示界面元素和菜单 +- 后端API进行严格的权限验证 +- 支持页面级和功能级的权限控制 + +## 环境变量说明 + +| 变量名 | 必填 | 说明 | 示例值 | +|--------------|----|---------------------------------|---------------------------------------------------------------------| +| DATABASE_URL | 是 | PostgreSQL数据库连接字符串 | `postgresql://username:password@host:port/database?sslmode=require` | +| JWT_SECRET | 是 | JWT令牌签名密钥,建议使用强随机字符串 | `your-very-secure-jwt-secret-key` | +| NODE_ENV | 否 | 运行环境,development或production | `production` | +| REDIS_URL | 否 | Redis缓存服务连接字符串,填写后自动启用Redis缓存功能 | `redis://default:password@host:port` | +| NITRO_PRESET | 否 | Nitro预设 | `vercel` | +| NUXT_PUBLIC_HOST | 否 | 用于 CORS 和反向代理的主机名验证 | `your-app.com` | + +## 项目结构 + +``` +VoiceHub/ +├── app/ # Nuxt 4 应用主目录 +│ ├── app.vue # 应用入口文件 +│ ├── assets/ # 静态资源目录 +│ │ └── css/ # CSS样式文件 +│ │ ├── components.css # 组件样式 +│ │ ├── lyric-player.module.css # 歌词播放器样式 +│ │ ├── main.css # 主样式文件 +│ │ ├── mobile-admin.css # 移动端管理样式 +│ │ ├── print-fix.css # 打印样式修复 +│ │ ├── sf-pro-icons.css # SF Pro图标字体 +│ │ ├── theme-protection.css # 主题保护样式 +│ │ ├── transitions.css # 过渡动画样式 +│ │ ├── variables.css # CSS变量定义 +│ │ └── year-review.css # 年度回顾样式 +│ ├── components/ # Vue组件目录 +│ │ ├── Admin/ # 管理员功能组件 +│ │ │ ├── ApiKeyManager.vue # API密钥管理 +│ │ │ ├── BackupManager.vue # 数据库备份管理 +│ │ │ ├── BatchUpdateModal.vue # 批量更新模态框 +│ │ │ ├── BlacklistManager.vue # 黑名单管理 +│ │ │ ├── DataAnalysisPanel.vue # 数据分析面板 +│ │ │ ├── DatabaseManager.vue # 数据库管理 +│ │ │ ├── EmailTemplateManager.vue # 邮件模板管理 +│ │ │ ├── NotificationSender.vue # 通知发送管理 +│ │ │ ├── OverviewDashboard.vue # 管理概览仪表板 +│ │ │ ├── PlayTimeManager.vue # 播放时间管理 +│ │ │ ├── RequestTimeManager.vue # 点歌时间管理 +│ │ │ ├── ScheduleForm.vue # 排期表单 +│ │ │ ├── ScheduleItemPrint.vue # 排期项目打印 +│ │ │ ├── ScheduleManager.vue # 排期管理 +│ │ │ ├── SchedulePrinter.vue # 排期打印功能 +│ │ │ ├── SemesterManager.vue # 学期管理 +│ │ │ ├── Sidebar.vue # 管理后台侧边栏 +│ │ │ ├── SiteConfigManager.vue # 站点配置管理 +│ │ │ ├── SmtpManager.vue # SMTP邮件服务管理 +│ │ │ ├── SongDownloadDialog.vue # 歌曲下载弹窗 +│ │ │ ├── SongManagement.vue # 歌曲管理 +│ │ │ ├── SubmissionRemarkDialog.vue # 投稿备注弹窗 +│ │ │ ├── UserManager.vue # 用户管理 +│ │ │ ├── UserSongsModal.vue # 用户歌曲查看弹窗 +│ │ │ └── VotersModal.vue # 投票人员查看弹窗 +│ │ ├── AMLL/ # Apple Music-Like Lyrics组件 +│ │ │ └── LyricPlayer.vue # AMLL歌词播放器 +│ │ ├── Auth/ # 认证相关组件 +│ │ │ ├── Providers/ # 第三方登录提供商组件 +│ │ │ │ ├── Casdoor/ # Casdoor登录组件 +│ │ │ │ │ └── Icon.vue # Casdoor图标 +│ │ │ │ └── GitHub/ # GitHub登录组件 +│ │ │ │ └── Icon.vue # GitHub图标 +│ │ │ ├── ChangePasswordForm.vue # 修改密码表单 +│ │ │ ├── LoginForm.vue # 登录表单 +│ │ │ ├── OAuthBindingCard.vue # OAuth绑定卡片 +│ │ │ ├── OAuthButtons.vue # OAuth登录按钮组 +│ │ │ ├── TwoFactorSetup.vue # 双重认证设置组件 +│ │ │ └── TwoFactorVerify.vue # 双重认证验证组件 +│ │ ├── Common/ # 通用组件 +│ │ │ └── UserSearchModal.vue # 用户搜索弹窗 +│ │ ├── Notifications/ # 通知系统组件 +│ │ │ └── NotificationSettings.vue # 通知设置 +│ │ ├── Player/ # 播放器相关组件 +│ │ │ └── PlayerLyric/ # 播放器歌词子组件 +│ │ ├── Songs/ # 歌曲相关组件 +│ │ │ ├── BilibiliEpisodesModal.vue # Bilibili剧集选择弹窗 +│ │ │ ├── DuplicateSongModal.vue # 重复歌曲处理对话框 +│ │ │ ├── ImportSongsModal.vue # 导入歌曲弹窗 +│ │ │ ├── NeteaseLoginModal.vue # 网易云音乐登录弹窗 +│ │ │ ├── PlaylistSelectionModal.vue # 歌单选择弹窗 +│ │ │ ├── PodcastEpisodesModal.vue # 播客节目弹窗 +│ │ │ ├── RecentSongsModal.vue # 最近播放弹窗 +│ │ │ ├── RequestForm.vue # 点歌表单 +│ │ │ ├── ScheduleList.vue # 排期列表展示 +│ │ │ └── SongList.vue # 歌曲列表 +│ │ ├── UI/ # 通用UI组件 +│ │ │ ├── AudioPlayer/ # 音频播放器组件模块 +│ │ │ │ ├── AudioElement.vue # 音频元素组件 +│ │ │ │ ├── PlayerControls.vue # 播放器控制组件 +│ │ │ │ └── PlayerInfo.vue # 播放器信息组件 +│ │ │ ├── Common/ # 通用UI组件 +│ │ │ │ ├── CustomSelect.vue # 自定义选择器 +│ │ │ │ ├── DataTable.vue # 通用数据表格组件 +│ │ │ │ ├── ErrorBoundary.vue # 错误边界组件 +│ │ │ │ ├── LoadingState.vue # 加载状态组件 +│ │ │ │ ├── Pagination.vue # 翻页组件 +│ │ │ │ ├── SearchFilter.vue # 搜索过滤组件 +│ │ │ │ └── StatCard.vue # 统计卡片组件 +│ │ │ ├── AppleMusicLyrics.vue # 类Apple Music风格歌词显示组件 +│ │ │ ├── AudioPlayer.vue # 主音频播放器组件 +│ │ │ ├── BilibiliframeModal.vue # Bilibili视频预览弹窗 +│ │ │ ├── ConfirmDialog.vue # 确认对话框 +│ │ │ ├── Icon.vue # 图标组件 +│ │ │ ├── LyricsModal.vue # 全屏歌词模态框组件 +│ │ │ ├── MarqueeText.vue # 滚动文本显示组件 +│ │ │ ├── Notification.vue # 单个通知组件 +│ │ │ ├── NotificationContainer.vue # 通知容器组件 +│ │ │ ├── PageTransition.vue # 页面过渡动画 +│ │ │ └── ProgressBar.vue # 进度条组件 +│ │ ├── year-review/ # 年度回顾组件 +│ │ └── SiteFooter.vue # 站点页脚 +│ ├── composables/ # Vue 3 组合式API +│ │ ├── useAdmin.ts # 管理员功能hooks +│ │ ├── useAudioPlayer.ts # 音频播放器hooks +│ │ ├── useAudioPlayerControl.ts # 音频播放器控制hooks +│ │ ├── useAudioPlayerEnhanced.ts # 增强音频播放器hooks +│ │ ├── useAudioPlayerSync.ts # 音频播放器同步hooks +│ │ ├── useAudioQuality.ts # 音质管理hooks +│ │ ├── useAudioVisualizer.ts # 音频可视化hooks +│ │ ├── useAuth.ts # 认证功能hooks +│ │ ├── useBackgroundRenderer.ts # 背景渲染hooks +│ │ ├── useBilibiliPreview.ts # Bilibili视频预览hooks +│ │ ├── useErrorHandler.ts # 错误处理hooks +│ │ ├── useLyricManager.ts # 歌词管理hooks +│ │ ├── useLyricPlayer.ts # 类Apple Music风格歌词播放器hooks +│ │ ├── useLyrics.ts # 歌词功能hooks +│ │ ├── useLyricSettings.ts # 歌词设置hooks +│ │ ├── useMediaSession.ts # 媒体会话API hooks +│ │ ├── useMusicSources.ts # 音乐源管理hooks +│ │ ├── useMusicWebSocket.ts # 音乐WebSocket hooks +│ │ ├── useNotifications.ts # 通知功能hooks +│ │ ├── usePermissions.ts # 权限管理hooks +│ │ ├── useProgress.ts # 进度管理hooks +│ │ ├── useProgressEvents.ts # 进度事件hooks +│ │ ├── useRequestDedup.ts # 请求去重hooks +│ │ ├── useSemesters.ts # 学期管理hooks +│ │ ├── useSiteConfig.js # 站点配置hooks +│ │ ├── useSongPlayer.ts # 歌曲播放器hooks +│ │ ├── useSongs.ts # 歌曲管理hooks +│ │ ├── useToast.ts # Toast提示hooks +│ │ └── useWebAuthn.ts # WebAuthn 认证 hooks +│ ├── drizzle/ # 数据库相关 +│ │ ├── db.ts # 数据库连接 +│ │ ├── schema.ts # 数据库模型 +│ │ └── migrations/ # 数据库迁移文件 +│ ├── layouts/ # 布局组件 +│ │ └── default.vue # 默认布局模板 +│ ├── middleware/ # 中间件 +│ │ └── auth.global.ts # 全局认证中间件 +│ ├── pages/ # 页面组件(Nuxt 4路由) +│ │ ├── account/ # 账户管理页面 +│ │ │ └── index.vue # 账户中心(绑定管理) +│ │ ├── auth/ # 认证相关页面 +│ │ │ └── error.vue # 认证错误页面 +│ │ ├── change-password.vue # 修改密码页面 +│ │ ├── dashboard.vue # 用户仪表盘 +│ │ ├── index.vue # 首页 +│ │ ├── login.vue # 登录页面 +│ │ ├── notification-settings.vue # 通知设置页面 +│ │ └── year-review.vue # 年度回顾页面 +│ ├── plugins/ # Nuxt插件 +│ │ ├── auth.client.ts # 客户端认证插件 +│ │ └── auth.server.ts # 服务端认证插件 +│ ├── public/ # 静态文件目录 +│ │ ├── images/ # 图片资源 +│ │ │ ├── logo.png # PNG格式Logo +│ │ │ ├── logo.svg # SVG格式Logo +│ │ │ ├── search.svg # 搜索图标 +│ │ │ └── thumbs-up.svg # 点赞图标 +│ │ ├── favicon.ico # 网站图标 +│ │ └── robots.txt # 搜索引擎爬虫配置 +│ └── utils/ # 工具函数 +│ ├── core/ # 核心工具 +│ │ └── security.ts # 安全相关工具 +│ ├── lyric/ # 歌词处理工具 +│ │ ├── exclude.ts # 歌词排除规则 +│ │ ├── lyricFormat.ts # 歌词格式化 +│ │ ├── lyricParser.ts # 歌词解析器 +│ │ ├── lyricStripper.ts # 歌词清理 +│ │ ├── parseLrc.ts # LRC格式解析 +│ │ └── qrc-parser.ts # QRC格式解析 +│ ├── musicSdk/ # 音乐SDK工具 +│ │ ├── tx/ # QQ音乐SDK +│ │ └── wy/ # 网易云音乐SDK +│ ├── bilibiliSource.ts # 哔哩哔哩音源 +│ ├── lyricAdapter.ts # 歌词适配器 +│ ├── musicSources.ts # 音乐源配置 +│ ├── musicUrl.ts # 音乐URL处理 +│ ├── neteaseApi.ts # 网易云音乐API +│ ├── oauth.ts # OAuth工具 +│ ├── timeUtils.ts # 时间工具 +│ └── url.ts # URL处理工具 +├── server/ # 服务端代码 +│ ├── api/ # API路由 +│ │ ├── admin/ # 管理员API +│ │ │ ├── api-keys/ # API密钥管理API +│ │ │ │ ├── [id].delete.ts # 删除API密钥 +│ │ │ │ ├── [id].get.ts # 获取API密钥详情 +│ │ │ │ ├── [id].put.ts # 更新API密钥 +│ │ │ │ ├── index.get.ts # 获取API密钥列表 +│ │ │ │ ├── index.post.ts # 创建API密钥 +│ │ │ │ └── logs.get.ts # API使用日志 +│ │ │ ├── backup/ # 备份管理API +│ │ │ │ ├── delete/ # 删除备份子目录 +│ │ │ │ ├── download/ # 下载备份子目录 +│ │ │ │ ├── clear.post.ts # 清空备份历史 +│ │ │ │ ├── download.get.ts # 下载备份 +│ │ │ │ ├── export.post.ts # 创建备份 +│ │ │ │ ├── list.get.ts # 获取备份列表 +│ │ │ │ ├── restore-chunk.post.ts # 恢复备份分片 +│ │ │ │ ├── restore.post.ts # 恢复备份 +│ │ │ │ └── upload.post.ts # 上传备份文件 +│ │ │ ├── blacklist/ # 黑名单管理API +│ │ │ │ ├── [id].delete.ts # 删除黑名单项 +│ │ │ │ ├── [id].patch.ts # 更新黑名单项 +│ │ │ │ ├── index.get.ts # 获取黑名单列表 +│ │ │ │ └── index.post.ts # 添加黑名单项 +│ │ │ ├── database/ # 数据库管理API +│ │ │ │ ├── cleanup.post.ts # 数据库清理 +│ │ │ │ ├── performance.get.ts # 数据库性能监控 +│ │ │ │ ├── pool-status.get.ts # 连接池状态 +│ │ │ │ ├── reset.post.ts # 重置数据库 +│ │ │ │ └── status.get.ts # 数据库状态 +│ │ │ ├── db-status.get.ts # 数据库状态检查 +│ │ │ ├── email-templates/ # 邮件模板管理API +│ │ │ │ ├── index.delete.ts # 删除邮件模板 +│ │ │ │ ├── index.get.ts # 获取邮件模板列表 +│ │ │ │ ├── index.post.ts # 创建/更新邮件模板 +│ │ │ │ └── preview.post.ts # 预览邮件模板 +│ │ │ ├── fix-sequence.post.ts # 修复数据库序列 +│ │ │ ├── notifications/ # 管理员通知API +│ │ │ │ └── send.post.ts # 发送通知 +│ │ │ ├── play-times/ # 播放时间管理API +│ │ │ │ ├── [id].ts # 播放时间操作 +│ │ │ │ ├── index.post.ts # 创建播放时间 +│ │ │ │ └── index.ts # 播放时间列表 +│ │ │ ├── replay-requests/ # 重播申请管理API +│ │ │ │ ├── index.get.ts # 获取重播申请列表 +│ │ │ │ └── reject.post.ts # 拒绝重播申请 +│ │ │ ├── request-times/ # 点歌时间管理API +│ │ │ │ ├── [id].ts # 点歌时间操作 +│ │ │ │ ├── index.post.ts # 创建点歌时间 +│ │ │ │ └── index.ts # 点歌时间列表 +│ │ │ ├── schedule/ # 排期管理API +│ │ │ │ ├── bulk-publish.post.ts # 批量发布排期 +│ │ │ │ ├── draft.post.ts # 保存排期草稿 +│ │ │ │ ├── full.get.ts # 获取完整排期数据(包含草稿) +│ │ │ │ ├── move-date.post.ts # 排期日期迁移 +│ │ │ │ ├── publish.post.ts # 发布排期草稿 +│ │ │ │ ├── remove.post.ts # 移除排期 +│ │ │ │ └── sequence.post.ts # 更新排期顺序 +│ │ │ ├── schedule.post.ts # 创建排期 +│ │ │ ├── semesters/ # 学期管理API +│ │ │ │ ├── [id].delete.ts # 删除学期 +│ │ │ │ ├── index.get.ts # 获取学期列表 +│ │ │ │ ├── index.post.ts # 创建学期 +│ │ │ │ └── set-active.post.ts # 设置活跃学期 +│ │ │ ├── smtp/ # SMTP邮件服务API +│ │ │ │ ├── reload.post.ts # 重新加载SMTP配置 +│ │ │ │ ├── test-connection.post.ts # 测试SMTP连接 +│ │ │ │ └── test-email.post.ts # 发送测试邮件 +│ │ │ ├── songs/ # 管理员歌曲管理API +│ │ │ │ ├── delete.post.ts # 删除歌曲 +│ │ │ │ ├── mark-played.post.ts # 标记歌曲已播放 +│ │ │ │ └── reject.post.ts # 驳回歌曲 +│ │ │ ├── stats.get.ts # 统计数据 +│ │ │ ├── activities.get.ts # 活动管理API +│ │ │ ├── stats/ # 详细统计API +│ │ │ │ ├── active-users.get.ts # 活跃用户统计 +│ │ │ │ ├── realtime.get.ts # 实时统计 +│ │ │ │ ├── semester-comparison.get.ts # 学期对比统计 +│ │ │ │ ├── top-songs.get.ts # 热门歌曲统计 +│ │ │ │ ├── trends.get.ts # 趋势分析 +│ │ │ │ └── user-engagement.get.ts # 用户参与度统计 +│ │ │ ├── system-settings/ # 系统设置API +│ │ │ │ ├── index.post.ts # 更新系统设置 +│ │ │ │ └── index.ts # 获取系统设置 +│ │ │ └── users/ # 用户管理API +│ │ │ ├── [id]/ # 用户详情操作子目录 +│ │ │ │ ├── reset-password.post.ts # 重置用户密码 +│ │ │ │ ├── songs.get.ts # 获取用户点歌记录 +│ │ │ │ ├── status-logs.get.ts # 获取用户状态变更日志 +│ │ │ │ └── status.put.ts # 更新用户状态 +│ │ │ ├── [id].delete.ts # 删除用户 +│ │ │ ├── [id].put.ts # 更新用户 +│ │ │ ├── [id].ts # 用户详情 +│ │ │ ├── batch-grade-update.post.ts # 批量年级更新 +│ │ │ ├── batch-status.put.ts # 批量状态更新 +│ │ │ ├── batch-update.post.ts # 批量更新用户 +│ │ │ ├── batch.post.ts # 批量操作用户 +│ │ │ ├── index.get.ts # 获取用户列表 +│ │ │ ├── index.post.ts # 创建用户 +│ │ │ ├── index.ts # 用户管理 +│ │ │ └── status-logs.get.ts # 用户状态日志 +│ │ ├── api-enhanced/ # 网易云音乐API +│ │ │ └── netease/ # 网易云增强接口代理 +│ │ │ └── [...path].ts # 转发网易云API请求 +│ │ ├── auth/ # 认证API +│ │ │ ├── 2fa/ # 2FA验证API +│ │ │ │ ├── send-email.post.ts # 发送2FA验证邮件 +│ │ │ │ └── verify.post.ts # 验证2FA代码 +│ │ │ ├── webauthn/ # WebAuthn 相关 API +│ │ │ │ ├── login/ # 登录验证 +│ │ │ │ │ ├── options.post.ts # 获取登录 Challenge +│ │ │ │ │ └── verify.post.ts # 验证登录签名 +│ │ │ │ ├── register/ # 设备注册 +│ │ │ │ │ ├── options.get.ts # 获取注册 Challenge +│ │ │ │ │ └── verify.post.ts # 验证注册签名 +│ │ │ │ └── rename.post.ts # 重命名设备 +│ │ │ ├── [provider]/ # OAuth提供商路由 +│ │ │ │ ├── callback.get.ts # OAuth回调处理 +│ │ │ │ └── index.get.ts # OAuth授权跳转 +│ │ │ ├── bind.post.ts # 绑定社交账号 +│ │ │ ├── change-password.post.ts # 修改密码 +│ │ │ ├── identities.get.ts # 获取已绑定身份列表 +│ │ │ ├── login.post.ts # 用户登录 +│ │ │ ├── logout.post.ts # 用户登出 +│ │ │ ├── set-initial-password.post.ts # 设置初始密码 +│ │ │ ├── unbind.post.ts # 解绑社交账号 +│ │ │ └── verify.get.ts # 验证Token并获取用户信息 +│ │ ├── bilibili/ # Bilibili相关API +│ │ │ ├── playurl.get.ts # 获取播放链接 +│ │ │ └── search.get.ts # Bilibili视频搜索 +│ │ ├── blacklist/ # 黑名单API +│ │ │ └── check.post.ts # 检查黑名单 +│ │ ├── meow/ # MeoW账号绑定API +│ │ │ ├── bind.post.ts # 绑定MeoW账号 +│ │ │ └── unbind.post.ts # 解绑MeoW账号 +│ │ ├── music/ # 音乐相关API +│ │ │ ├── state.post.ts # 音乐状态管理 +│ │ │ └── websocket.ts # 音乐WebSocket连接 +│ │ ├── native-api/ # 原生音乐API +│ │ │ └── search/ # 搜索API +│ │ │ ├── tx.get.ts # 腾讯音乐搜索 +│ │ │ └── wy.get.ts # 网易云音乐搜索 +│ │ ├── notifications/ # 通知系统API +│ │ │ ├── [id]/ # 通知操作子目录 +│ │ │ │ └── read.post.ts # 标记通知已读 +│ │ │ ├── [id].delete.ts # 删除通知 +│ │ │ ├── clear-all.delete.ts # 清空所有通知 +│ │ │ ├── index.ts # 通知列表 +│ │ │ ├── meow/ # MeoW通知API +│ │ │ │ ├── send-verification.post.ts # 发送验证码 +│ │ │ │ └── test.post.ts # 测试通知 +│ │ │ ├── read-all.post.ts # 标记所有已读 +│ │ │ ├── settings.post.ts # 更新通知设置 +│ │ │ └── settings.ts # 获取通知设置 +│ │ ├── open/ # 开放API(无需认证) +│ │ │ ├── schedules.get.ts # 获取公开排期 +│ │ │ └── songs.get.ts # 获取公开歌曲列表 +│ │ ├── play-times/ # 播放时间API +│ │ │ └── index.ts # 播放时间管理 +│ │ ├── request-times/ # 点歌时间API +│ │ │ └── index.ts # 点歌时间管理 +│ │ ├── progress/ # 进度条API +│ │ │ ├── events.ts # 进度事件 +│ │ │ └── id.ts # 进度ID管理 +│ │ ├── proxy/ # 代理服务API +│ │ │ └── image.get.ts # 图片代理(解决HTTP/HTTPS混合内容及跨域问题) +│ │ ├── semesters/ # 学期API +│ │ │ ├── current.get.ts # 获取当前学期 +│ │ │ └── options.get.ts # 获取学期选项 +│ │ ├── site-config.get.ts # 站点配置API +│ │ ├── songs/ # 歌曲相关API +│ │ │ ├── [id]/ # 歌曲详情操作 +│ │ │ │ ├── update.put.ts # 更新歌曲信息 +│ │ │ │ └── voters.get.ts # 获取投票人员 +│ │ │ ├── collaborators/ # 联合投稿管理 +│ │ │ │ └── reply.post.ts # 处理联合投稿邀请 +│ │ │ ├── add.post.ts # 添加歌曲 +│ │ │ ├── count.get.ts # 歌曲统计 +│ │ │ ├── import.post.ts # 导入歌曲 +│ │ │ ├── index.get.ts # 歌曲列表 +│ │ │ ├── public.get.ts # 公开歌曲列表 +│ │ │ ├── request.post.ts # 点歌请求 +│ │ │ ├── replay.post.ts # 提交重播申请 +│ │ │ ├── replay.delete.ts # 撤回重播申请 +│ │ │ ├── submission-status.get.ts # 投稿状态 +│ │ │ ├── vote.post.ts # 投票 +│ │ │ └── withdraw.post.ts # 撤回歌曲 +│ │ ├── system/ # 系统API +│ │ │ ├── location.get.ts # 获取系统位置信息 +│ │ │ ├── reconnect.post.ts # 重连数据库 +│ │ │ └── status.get.ts # 系统状态 +│ │ ├── user/ # 用户相关API +│ │ │ ├── 2fa/ # 2FA管理API +│ │ │ │ ├── disable.post.ts # 关闭双重认证 +│ │ │ │ ├── enable.post.ts # 开启双重认证 +│ │ │ │ └── generate.post.ts # 生成双重认证密钥 +│ │ │ ├── email/ # 用户邮箱管理 +│ │ │ │ ├── bind.post.ts # 绑定邮箱 +│ │ │ │ ├── resend-verification.post.ts # 重发验证邮件 +│ │ │ │ ├── send-code.post.ts # 发送验证码 +│ │ │ │ ├── unbind.post.ts # 解绑邮箱 +│ │ │ │ └── verify-code.post.ts # 验证邮箱验证码 +│ │ │ └── year-review.get.ts # 获取年度回顾数据 +│ │ └── users/ # 用户API +│ │ ├── meow/ # 用户MeoW相关子目录 +│ │ ├── social-accounts/ # 社交账号管理 +│ │ │ ├── meow.delete.ts # 删除MeoW绑定 +│ │ │ └── meow.post.ts # MeoW账号操作 +│ │ ├── search.get.ts # 搜索用户 +│ │ └── social-accounts.get.ts # 获取社交账号 +│ ├── config/ # 服务端配置 +│ │ └── constants.ts # 风控阈值与时间窗口常量 +│ ├── error.ts # 全局错误处理 +│ ├── middleware/ # 服务端中间件 +│ │ ├── api-auth.ts # API认证中间件 +│ │ ├── api-cors.ts # API跨域中间件 +│ │ └── auth.ts # 认证中间件 +│ ├── plugins/ # 服务端插件 +│ │ └── error-handler.ts # 错误处理插件 +│ ├── services/ # 业务服务层 +│ │ ├── apiLogService.ts # API日志服务 +│ │ ├── cacheService.ts # 缓存服务(Redis缓存管理) +│ │ ├── meowNotificationService.ts # MeoW通知服务 +│ │ ├── notificationService.ts # 通知服务 +│ │ ├── securityService.ts # 安全服务 +│ │ ├── smtpService.ts # SMTP邮件服务 +│ │ └── userService.ts # 用户服务 +│ ├── utils/ # 服务端工具函数 +│ │ ├── __tests__/ # 工具函数测试目录 +│ │ ├── musicSdk/ # 音乐SDK工具 +│ │ ├── auth.ts # 认证工具函数 +│ │ ├── cache-helpers.ts # 缓存辅助工具 +│ │ ├── database-health.ts # 数据库健康检查 +│ │ ├── database-manager.ts # 数据库管理工具 +│ │ ├── geo.ts # 地理位置工具 +│ │ ├── ip-utils.ts # IP地址工具 +│ │ ├── jwt-enhanced.ts # JWT工具 +│ │ ├── log-manager.ts # 日志管理工具 +│ │ ├── native_common.ts # 原生API通用工具 +│ │ ├── native_tx.ts # 腾讯音乐原生API +│ │ ├── native_wy.ts # 网易云音乐原生API +│ │ ├── oauth-strategies.ts # OAuth策略配置 +│ │ ├── oauth-token.ts # OAuth令牌工具 +│ │ ├── oauth.ts # OAuth通用工具 +│ │ ├── open-api-cache.ts # 开放API缓存 +│ │ ├── permissions.js # 权限系统配置 +│ │ ├── redis.ts # Redis连接和操作工具 +│ │ ├── siteUtils.ts # 站点工具函数 +│ │ ├── studentMask.ts # 学生隐私工具 +│ │ ├── submissionLimit.ts # 投稿限额工具 +│ │ ├── twoFactorStore.ts # 双重认证存储工具 +│ │ ├── user.ts # 用户相关工具函数 +│ │ ├── webauthn-config.ts # WebAuthn配置工具 +│ │ └── webauthn-token.ts # WebAuthn令牌工具 +│ ├── workers/ # 服务端工作进程 +│ │ └── audioEncoderWorker.js # 音频编码工作进程 +│ └── tsconfig.json # 服务端TypeScript配置 +├── types/ # TypeScript类型定义 +│ ├── global.d.ts # 全局类型定义 +│ └── index.ts # 通用类型定义 +├── .env.example # 环境变量示例文件 +├── .gitignore # Git忽略文件配置 +├── .vercelignore # Vercel部署忽略文件 +├── docker-compose/ # Docker Compose配置目录 +├── docker-compose.yml # Docker编排文件 +├── Dockerfile # Docker构建文件 +├── drizzle.config.ts # Drizzle配置文件 +├── LICENSE # 开源许可证文件 +├── netlify.toml # Netlify部署配置 +├── nuxt.config.ts # Nuxt 4主配置文件 +├── package.json # Node.js项目配置和依赖 +├── README.md # 项目说明文档 +├── tsconfig.json # TypeScript配置文件 +└── vercel.json # Vercel部署配置 +``` + +### 目录说明 + +#### 核心目录 (app/) + +- **`app/components/`**: Vue组件库,按功能模块组织 + - **`Admin/`**: 管理后台组件(排期、用户、数据分析等) + - **`Admin_Backup/`**: 管理组件备份目录 + - **`AMLL/`**: Apple Music-Like Lyrics歌词播放器组件 + - **`Auth/`**: 认证相关组件(登录、OAuth绑定等) + - **`Common/`**: 通用业务组件 + - **`Notifications/`**: 通知系统组件 + - **`Player/`**: 播放器相关组件 + - **`Songs/`**: 歌曲相关组件(点歌、导入、歌单等) + - **`UI/`**: 通用UI组件(播放器、对话框、进度条等) + - **`year-review/`**: 年度回顾功能组件 +- **`app/pages/`**: 页面组件,Nuxt 4 自动路由 +- **`app/composables/`**: Vue 3组合式API,业务逻辑复用 +- **`app/drizzle/`**: Drizzle ORM配置、数据库连接和迁移文件 + +#### 配置目录 (app/) + +- **`app/assets/css/`**: 样式文件,支持CSS变量和主题 +- **`app/plugins/`**: Nuxt插件,扩展框架功能 +- **`app/middleware/`**: 中间件,处理路由和认证 +- **`app/utils/`**: 客户端工具函数 + - **`core/`**: 核心工具(安全等) + - **`lyric/`**: 歌词处理工具集 + - **`musicSdk/`**: 音乐平台SDK工具 + +#### 服务端目录 (server/) + +- **`server/api/`**: 服务端API,RESTful接口设计 + - **`admin/`**: 管理员专用API(用户、排期、统计等) + - **`auth/`**: 认证相关API + - **`songs/`**: 歌曲管理API + - **`notifications/`**: 通知系统API + - **`open/`**: 公共API(无需认证) +- **`server/config/`**: 服务端配置(常量、环境配置等) +- **`server/middleware/`**: 服务端中间件(认证、日志等) +- **`server/plugins/`**: 服务端插件(错误处理等) +- **`server/services/`**: 业务逻辑服务层 +- **`server/utils/`**: 服务端工具函数 + - **`musicSdk/`**: 音乐平台SDK服务端工具 + +#### 静态资源 + +- **`app/public/`**: 静态文件 +- **`app/public/images/`**: 图片资源,包含Logo和图标文件 + +## 使用说明 + +### 普通用户 + +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` + +### OAuth 平台扩展指南 + +VoiceHub 采用策略模式(Strategy Pattern)实现了灵活的 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. 配置环境变量 + +在 `.env` 文件中添加新平台所需的配置(如 Client ID 和 Secret): + +```bash +GOOGLE_CLIENT_ID="your-client-id" +GOOGLE_CLIENT_SECRET="your-client-secret" +``` + +##### 4. 完成! + +现在,你可以通过 `/api/auth/google` 访问新的登录流程,VoiceHub 会自动处理路由分发、State 校验、CSRF 保护和用户绑定逻辑。 + +#### Casdoor 配置说明 + +项目已内置对 [Casdoor](https://casdoor.org/) 的支持。Casdoor 是一个开源的 UI 优先的身份认证管理系统 (IAM),支持 OAuth 2.0、OIDC 等多种协议。 + +要启用 Casdoor 登录,只需在 `.env` 文件中配置以下环境变量: + +```bash +# Casdoor 配置 +CASDOOR_ENDPOINT="https://your-casdoor-domain.com" # Casdoor 服务地址 +CASDOOR_CLIENT_ID="your-client-id" # 应用 Client ID +CASDOOR_CLIENT_SECRET="your-client-secret" # 应用 Client Secret +``` + +配置完成后,系统会自动识别并启用 Casdoor 登录策略。 + +#### 多环境部署与回调地址代理 + +在 Vercel 等平台进行多环境部署时,GitHub OAuth App 通常只能配置单一的回调地址。为了解决这个问题,项目可以使用 [VoiceHub-Auth-Broker](https://github.com/laoshuikaixue/VoiceHub-Auth-Broker) 中间件,具体使用方法可见仓库内的文档。 + +该中间件可以作为统一的回调入口,根据 `state` 参数动态转发回调请求到正确的部署环境,从而实现一个 GitHub OAuth App 支持无限个预览/生产环境。详情请参考该项目文档。 + +#### 前端图标配置 + +当添加了新的服务端 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/VoiceHub-main/app/assets/css/components.css b/VoiceHub-main/app/assets/css/components.css new file mode 100644 index 0000000..fdea3c7 --- /dev/null +++ b/VoiceHub-main/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/VoiceHub-main/app/assets/css/lyric-player.module.css b/VoiceHub-main/app/assets/css/lyric-player.module.css new file mode 100644 index 0000000..6e35018 --- /dev/null +++ b/VoiceHub-main/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 rgba(0, 0, 0, 0.6), + 0 0 10px rgba(0, 0, 0, 0.5); + + & 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 rgba(0, 0, 0, 0.5); +} + +.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/VoiceHub-main/app/assets/css/main.css b/VoiceHub-main/app/assets/css/main.css new file mode 100644 index 0000000..69b6bb1 --- /dev/null +++ b/VoiceHub-main/app/assets/css/main.css @@ -0,0 +1,474 @@ +/* 导入浏览器主题保护样式 */ +@import url('./theme-protection.css'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* 防止浏览器自动应用深色主题 */ +:root { + color-scheme: light dark; +} + +/* 流体背景与全局样式 */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* 字体显示策略优化 */ +@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(120, 119, 198, 0.03) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.01) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(120, 119, 198, 0.02) 0%, transparent 50%); + 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 rgba(0, 0, 0, 0.1), + 0 1px 3px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + 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, rgba(255, 255, 255, 0.1), transparent); +} + +.card:hover { + transform: translateY(-4px); + border-color: var(--card-hover-border); + box-shadow: + 0 12px 24px rgba(0, 0, 0, 0.15), + 0 4px 8px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +/* 按钮 */ +.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 rgba(0, 0, 0, 0.1); +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), 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 rgba(102, 126, 234, 0.3); +} + +.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%, #4a4a4a 100%); + color: var(--text-primary); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1); +} + +.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 rgba(102, 126, 234, 0.2); +} + +.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 rgba(102, 126, 234, 0.1); + 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; + } +} diff --git a/VoiceHub-main/app/assets/css/mobile-admin.css b/VoiceHub-main/app/assets/css/mobile-admin.css new file mode 100644 index 0000000..21b67dd --- /dev/null +++ b/VoiceHub-main/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 rgba(102, 126, 234, 0.2) !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/VoiceHub-main/app/assets/css/print-fix.css b/VoiceHub-main/app/assets/css/print-fix.css new file mode 100644 index 0000000..1805e4b --- /dev/null +++ b/VoiceHub-main/app/assets/css/print-fix.css @@ -0,0 +1,273 @@ +/* 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; + 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/VoiceHub-main/app/assets/css/sf-pro-icons.css b/VoiceHub-main/app/assets/css/sf-pro-icons.css new file mode 100644 index 0000000..81fe09b --- /dev/null +++ b/VoiceHub-main/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/VoiceHub-main/app/assets/css/theme-protection.css b/VoiceHub-main/app/assets/css/theme-protection.css new file mode 100644 index 0000000..238230b --- /dev/null +++ b/VoiceHub-main/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: rgba(79, 70, 229, 0.3); + color: inherit; +} + +::-moz-selection { + background-color: 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/VoiceHub-main/app/assets/css/transitions.css b/VoiceHub-main/app/assets/css/transitions.css new file mode 100644 index 0000000..f1f8a85 --- /dev/null +++ b/VoiceHub-main/app/assets/css/transitions.css @@ -0,0 +1,252 @@ +/* 页面过渡动画 */ + +/* 通用页面过渡 */ +.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 { + animation: page-view-enter 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94); + will-change: transform, opacity, filter; +} + +.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; +} + +@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, #0a0a0a 0%, #1a1a1a 100%); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-spinner { + width: 60px; + height: 60px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top: 3px solid #4f46e5; + 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/VoiceHub-main/app/assets/css/variables.css b/VoiceHub-main/app/assets/css/variables.css new file mode 100644 index 0000000..49025aa --- /dev/null +++ b/VoiceHub-main/app/assets/css/variables.css @@ -0,0 +1,361 @@ +/* 全局CSS变量 - 统一配色方案 */ +:root { + /* 主色调 */ + --primary: #3b82f6; + --primary-hover: #2563eb; + --primary-light: rgba(59, 130, 246, 0.1); + --primary-border: rgba(59, 130, 246, 0.2); + + /* 成功色 */ + --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); + + /* 背景色 */ + --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; + + /* 阴影 */ + --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; + + /* 特殊状态颜色 */ + --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; +} + +/* 深色主题特定调整 */ +@media (prefers-color-scheme: dark) { + :root { + /* 在深色模式下可以进行微调 */ + } +} + +/* 高对比度模式支持 */ +@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/VoiceHub-main/app/assets/css/year-review.css b/VoiceHub-main/app/assets/css/year-review.css new file mode 100644 index 0000000..d8164fa --- /dev/null +++ b/VoiceHub-main/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, #111827 0%, #000000 50%, #111827 100%); +} + +.bg-gradient-text-intro { + background-image: linear-gradient(to right, #ffffff, #e5e7eb, #6b7280); +} + +.bg-gradient-artist { + background: linear-gradient(to bottom right, #e9d5ff, #f472b6, #fb923c); +} + +.bg-gradient-numbers { + background-image: linear-gradient(to bottom, #60a5fa, #1d4ed8); +} + +/* 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/VoiceHub-main/app/components/AMLL/LyricPlayer.vue b/VoiceHub-main/app/components/AMLL/LyricPlayer.vue new file mode 100644 index 0000000..0e30712 --- /dev/null +++ b/VoiceHub-main/app/components/AMLL/LyricPlayer.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/VoiceHub-main/app/components/Admin/ApiKeyManager.vue b/VoiceHub-main/app/components/Admin/ApiKeyManager.vue new file mode 100644 index 0000000..3ed920d --- /dev/null +++ b/VoiceHub-main/app/components/Admin/ApiKeyManager.vue @@ -0,0 +1,1080 @@ + + + + + diff --git a/VoiceHub-main/app/components/Admin/BlacklistManager.vue b/VoiceHub-main/app/components/Admin/BlacklistManager.vue new file mode 100644 index 0000000..e85d18e --- /dev/null +++ b/VoiceHub-main/app/components/Admin/BlacklistManager.vue @@ -0,0 +1,575 @@ + + + + + diff --git a/VoiceHub-main/app/components/Admin/DataAnalysisPanel.vue b/VoiceHub-main/app/components/Admin/DataAnalysisPanel.vue new file mode 100644 index 0000000..6fe4099 --- /dev/null +++ b/VoiceHub-main/app/components/Admin/DataAnalysisPanel.vue @@ -0,0 +1,1132 @@ + + + + + diff --git a/VoiceHub-main/app/components/Admin/DatabaseManager.vue b/VoiceHub-main/app/components/Admin/DatabaseManager.vue new file mode 100644 index 0000000..fd08635 --- /dev/null +++ b/VoiceHub-main/app/components/Admin/DatabaseManager.vue @@ -0,0 +1,883 @@ + + + diff --git a/VoiceHub-main/app/components/Admin/EmailTemplateManager.vue b/VoiceHub-main/app/components/Admin/EmailTemplateManager.vue new file mode 100644 index 0000000..137ab25 --- /dev/null +++ b/VoiceHub-main/app/components/Admin/EmailTemplateManager.vue @@ -0,0 +1,345 @@ +