Files
BYW-VoiceHub/nuxt.config.ts
T

931 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import wasm from 'vite-plugin-wasm'
import { fileURLToPath } from 'url'
// 解析自定义 SEO 和 PWA 配置
let customSeoConfig: { title?: string; shortName?: string; description?: string; logo?: string } =
{}
try {
if (process.env.NUXT_PUBLIC_SEO_CONFIG) {
customSeoConfig = JSON.parse(process.env.NUXT_PUBLIC_SEO_CONFIG)
}
} catch (e) {
console.warn('解析 NUXT_PUBLIC_SEO_CONFIG 失败,请检查 JSON 格式:', e)
}
const siteTitle =
customSeoConfig.title || process.env.NUXT_PUBLIC_SITE_TITLE || 'VoiceHub校园广播站点歌系统'
const siteShortName = customSeoConfig.shortName || '校园广播'
const siteDescription =
customSeoConfig.description ||
process.env.NUXT_PUBLIC_SITE_DESCRIPTION ||
'校园广播站点歌系统 - 让你的声音被听见'
const siteLogo = customSeoConfig.logo || process.env.NUXT_PUBLIC_SITE_LOGO || '/themes/ClassicDark/logo.svg'
const readNumberEnv = (value: string | undefined, fallback: number): number => {
if (!value) return fallback
const parsed = Number(value)
if (!Number.isFinite(parsed)) return fallback
// For unit-interval settings (for example, Sentry sample rates), reject
// out-of-range environment values and fall back to the safe default.
if (fallback >= 0 && fallback <= 1) {
return parsed >= 0 && parsed <= 1 ? parsed : fallback
}
return parsed
}
const ssrInlineLyricPackages = ['@applemusic-like-lyrics/lyric']
const backendSentryDsnDefault =
'https://2fca0c8a939c8909e02c082ec847e8e8@o4508946125619200.ingest.de.sentry.io/4511244961448016'
const frontendSentryDsnDefault =
'https://3c4fe5353816bcdce36e7cc28703c8fa@o4508946125619200.ingest.de.sentry.io/4511244934774864'
// Sentry 仅在生产环境启用;Vercel/Netlify 预览部署(测试环境)自动关闭,避免收集测试错误
// 可显式设置 SENTRY_ENABLED=true/false 强制覆盖,未设置时按 NODE_ENV 与部署环境判断
const isPreviewDeployment =
process.env.VERCEL_ENV === 'preview' ||
process.env.CONTEXT === 'deploy-preview' ||
process.env.CONTEXT === 'branch-deploy'
const sentryRuntimeEnabled =
process.env.SENTRY_ENABLED !== undefined
? process.env.SENTRY_ENABLED === 'true'
: process.env.NODE_ENV === 'production' && !isPreviewDeployment
const jwtSecret = process.env.JWT_SECRET || ''
// 构造绝对路径 Logo URL 用于 SEO 标签,如果没有 host,则回退为相对路径
const host = process.env.NUXT_PUBLIC_HOST
if (!host && !siteLogo.startsWith('http') && process.env.NODE_ENV === 'production') {
console.warn(
'警告: 在生产环境中未配置 NUXT_PUBLIC_HOST,且 siteLogo 使用了相对路径。这可能会导致网站无法正确抓取和显示预览图。'
)
}
const absoluteLogo =
siteLogo.startsWith('http') || siteLogo.startsWith('//') || !host
? siteLogo
: (host.startsWith('http') ? '' : 'https://') +
host.replace(/\/$/, '') +
'/' +
siteLogo.replace(/^\//, '')
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2026-01-30',
future: {
compatibilityVersion: 4
},
srcDir: 'app',
serverDir: fileURLToPath(new URL('./server', import.meta.url)),
dir: {
public: fileURLToPath(new URL('./public', import.meta.url))
},
devtools: { enabled: process.env.NODE_ENV === 'development' },
devServer: {
host: '0.0.0.0', // 允许局域网访问
port: 3000
},
modules: [
'@unocss/nuxt',
'@vite-pwa/nuxt',
...(process.env.NODE_ENV === 'development' || process.env.npm_lifecycle_event?.includes('lint')
? ['@nuxt/eslint']
: [])
],
// UnoCSS 配置
unocss: {
presets: [
// Tailwind 3 / Windi CSS compact preset — 完全兼容 Tailwind 类名
() => import('@unocss/preset-wind3').then(m => m.presetWind3())
],
// 将 Tailwind 语义化颜色映射到 CSS 变量,实现主题自适应
theme: {
colors: {
// === 主色调 ===
primary: 'var(--primary)',
'primary-hover': 'var(--primary-hover)',
'primary-light': 'var(--primary-light)',
'primary-border': 'var(--primary-border)',
// === 语义色 ===
success: 'var(--success)',
'success-hover': 'var(--success-hover)',
'success-light': 'var(--success-light)',
'success-border': 'var(--success-border)',
warning: 'var(--warning)',
'warning-hover': 'var(--warning-hover)',
'warning-light': 'var(--warning-light)',
'warning-border': 'var(--warning-border)',
error: 'var(--error)',
'error-hover': 'var(--error-hover)',
'error-light': 'var(--error-light)',
'error-border': 'var(--error-border)',
// === 背景色 ===
'bg-primary': 'var(--bg-primary)',
'bg-secondary': 'var(--bg-secondary)',
'bg-tertiary': 'var(--bg-tertiary)',
'bg-quaternary': 'var(--bg-quaternary)',
'bg-hover': 'var(--bg-hover)',
// === 文字颜色 ===
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'text-tertiary': 'var(--text-tertiary)',
'text-quaternary': 'var(--text-quaternary)',
'text-disabled': 'var(--text-disabled)',
// === 边框颜色 ===
'border-primary': 'var(--border-primary)',
'border-secondary': 'var(--border-secondary)',
'border-tertiary': 'var(--border-tertiary)',
'border-quaternary': 'var(--border-quaternary)',
// === Alpha 透明度变量 ===
'primary-5': 'var(--primary-5)',
'primary-10': 'var(--primary-10)',
'primary-20': 'var(--primary-20)',
'primary-30': 'var(--primary-30)',
'primary-40': 'var(--primary-40)',
'primary-50': 'var(--primary-50)',
'primary-60': 'var(--primary-60)',
'primary-80': 'var(--primary-80)',
'primary-hover-5': 'var(--primary-hover-5)',
'primary-hover-10': 'var(--primary-hover-10)',
'primary-hover-20': 'var(--primary-hover-20)',
'primary-hover-30': 'var(--primary-hover-30)',
'success-5': 'var(--success-5)',
'success-10': 'var(--success-10)',
'success-20': 'var(--success-20)',
'success-30': 'var(--success-30)',
'success-40': 'var(--success-40)',
'success-50': 'var(--success-50)',
'success-70': 'var(--success-70)',
'success-80': 'var(--success-80)',
'success-hover-5': 'var(--success-hover-5)',
'success-hover-10': 'var(--success-hover-10)',
'success-hover-20': 'var(--success-hover-20)',
'warning-5': 'var(--warning-5)',
'warning-10': 'var(--warning-10)',
'warning-15': 'var(--warning-15)',
'warning-20': 'var(--warning-20)',
'warning-25': 'var(--warning-25)',
'warning-30': 'var(--warning-30)',
'warning-40': 'var(--warning-40)',
'warning-50': 'var(--warning-50)',
'warning-80': 'var(--warning-80)',
'warning-200': 'var(--warning-200)',
'warning-300': 'var(--warning-300)',
'warning-hover-5': 'var(--warning-hover-5)',
'warning-hover-10': 'var(--warning-hover-10)',
'warning-hover-15': 'var(--warning-hover-15)',
'warning-hover-20': 'var(--warning-hover-20)',
'warning-hover-25': 'var(--warning-hover-25)',
'warning-hover-30': 'var(--warning-hover-30)',
'warning-hover-40': 'var(--warning-hover-40)',
'warning-hover-50': 'var(--warning-hover-50)',
'error-5': 'var(--error-5)',
'error-10': 'var(--error-10)',
'error-15': 'var(--error-15)',
'error-20': 'var(--error-20)',
'error-30': 'var(--error-30)',
'error-40': 'var(--error-40)',
'error-50': 'var(--error-50)',
'error-60': 'var(--error-60)',
'error-70': 'var(--error-70)',
'error-80': 'var(--error-80)',
'error-hover-5': 'var(--error-hover-5)',
'error-hover-10': 'var(--error-hover-10)',
'error-hover-15': 'var(--error-hover-15)',
'error-hover-20': 'var(--error-hover-20)',
'error-hover-30': 'var(--error-hover-30)',
'info-5': 'var(--info-5)',
'info-10': 'var(--info-10)',
'info-20': 'var(--info-20)',
'info-30': 'var(--info-30)',
'info-40': 'var(--info-40)',
'info-50': 'var(--info-50)',
'info-hover-5': 'var(--info-hover-5)',
'info-hover-10': 'var(--info-hover-10)',
'info-hover-20': 'var(--info-hover-20)',
'info-hover-30': 'var(--info-hover-30)',
'info-hover-40': 'var(--info-hover-40)',
'info-hover-50': 'var(--info-hover-50)',
'bg-primary-5': 'var(--bg-primary-5)',
'bg-primary-10': 'var(--bg-primary-10)',
'bg-primary-20': 'var(--bg-primary-20)',
'bg-primary-25': 'var(--bg-primary-25)',
'bg-primary-30': 'var(--bg-primary-30)',
'bg-primary-40': 'var(--bg-primary-40)',
'bg-primary-45': 'var(--bg-primary-45)',
'bg-primary-50': 'var(--bg-primary-50)',
'bg-primary-60': 'var(--bg-primary-60)',
'bg-primary-70': 'var(--bg-primary-70)',
'bg-primary-80': 'var(--bg-primary-80)',
'bg-primary-85': 'var(--bg-primary-85)',
'bg-primary-90': 'var(--bg-primary-90)',
'bg-primary-95': 'var(--bg-primary-95)',
'bg-secondary-5': 'var(--bg-secondary-5)',
'bg-secondary-10': 'var(--bg-secondary-10)',
'bg-secondary-15': 'var(--bg-secondary-15)',
'bg-secondary-20': 'var(--bg-secondary-20)',
'bg-secondary-30': 'var(--bg-secondary-30)',
'bg-secondary-40': 'var(--bg-secondary-40)',
'bg-secondary-50': 'var(--bg-secondary-50)',
'bg-secondary-60': 'var(--bg-secondary-60)',
'bg-secondary-70': 'var(--bg-secondary-70)',
'bg-secondary-80': 'var(--bg-secondary-80)',
'bg-secondary-90': 'var(--bg-secondary-90)',
'bg-secondary-95': 'var(--bg-secondary-95)',
'bg-tertiary-20': 'var(--bg-tertiary-20)',
'bg-tertiary-30': 'var(--bg-tertiary-30)',
'bg-tertiary-40': 'var(--bg-tertiary-40)',
'bg-tertiary-50': 'var(--bg-tertiary-50)',
'bg-tertiary-60': 'var(--bg-tertiary-60)',
'bg-tertiary-70': 'var(--bg-tertiary-70)',
'bg-tertiary-80': 'var(--bg-tertiary-80)',
'bg-quaternary-10': 'var(--bg-quaternary-10)',
'bg-quaternary-50': 'var(--bg-quaternary-50)',
'border-secondary-30': 'var(--border-secondary-30)',
'border-secondary-40': 'var(--border-secondary-40)',
'border-secondary-50': 'var(--border-secondary-50)',
'border-secondary-60': 'var(--border-secondary-60)',
'border-secondary-70': 'var(--border-secondary-70)',
'border-secondary-80': 'var(--border-secondary-80)',
'border-tertiary-20': 'var(--border-tertiary-20)',
'border-tertiary-30': 'var(--border-tertiary-30)',
'border-tertiary-50': 'var(--border-tertiary-50)',
'text-primary-10': 'var(--text-primary-10)',
'text-primary-20': 'var(--text-primary-20)',
'text-primary-60': 'var(--text-primary-60)',
'text-primary-80': 'var(--text-primary-80)',
'text-error-20': 'var(--text-error-20)',
'text-error-50': 'var(--text-error-50)',
'text-error-60': 'var(--text-error-60)',
'text-error-70': 'var(--text-error-70)',
'text-error-80': 'var(--text-error-80)',
'text-success-50': 'var(--text-success-50)',
'text-success-70': 'var(--text-success-70)',
'text-success-80': 'var(--text-success-80)',
'text-warning-80': 'var(--text-warning-80)',
// === 用途化变量 — 品牌色 ===
// brand-* 旧命名(向后兼容)
'brand-blue': 'var(--color-accent)',
'brand-blue-light': 'var(--color-accent-light)',
'brand-blue-hover': 'var(--color-accent-hover)',
'brand-blue-light-hover': 'var(--color-accent-light-hover)',
'brand-blue-glow': 'var(--color-accent-glow)',
'brand-red': 'var(--color-error)',
'brand-red-light': 'var(--color-error-light)',
'brand-red-hover': 'var(--color-error-hover)',
'brand-red-glow': 'var(--color-error-glow)',
'brand-green': 'var(--color-success)',
'brand-green-light': 'var(--color-success-light)',
'brand-green-hover': 'var(--color-success-hover)',
'brand-green-glow': 'var(--color-success-glow)',
'brand-yellow': 'var(--color-warning)',
'brand-yellow-light': 'var(--color-warning-light)',
'brand-yellow-hover': 'var(--color-warning-hover)',
'brand-yellow-glow': 'var(--color-warning-glow)',
'brand-orange': 'var(--color-orange)',
'brand-orange-light': 'var(--color-orange-light)',
'brand-orange-hover': 'var(--color-orange-hover)',
'brand-orange-glow': 'var(--color-orange-glow)',
'brand-purple': 'var(--color-collab)',
'brand-purple-hover': 'var(--color-collab-hover)',
'brand-purple-light': 'var(--color-collab-light)',
'brand-purple-lighter': 'var(--color-collab-lighter)',
'brand-purple-glow': 'var(--color-collab-glow)',
'brand-pink': 'var(--color-pink)',
'brand-cyan': 'var(--color-cyan)',
'brand-teal': 'var(--color-teal)',
'brand-indigo': 'var(--color-indigo)',
'brand-indigo-hover': 'var(--color-indigo-hover)',
// === 用途化变量 — 通用语义色(accent=强调色, error=错误, success=成功, warning=警告, collab=协作, orange=橙色) ===
'accent': 'var(--color-accent)',
'accent-light': 'var(--color-accent-light)',
'accent-hover': 'var(--color-accent-hover)',
'accent-light-hover': 'var(--color-accent-light-hover)',
'accent-glow': 'var(--color-accent-glow)',
'color-accent': 'var(--color-accent)',
'color-accent-light': 'var(--color-accent-light)',
'color-accent-hover': 'var(--color-accent-hover)',
'color-accent-light-hover': 'var(--color-accent-light-hover)',
'color-accent-glow': 'var(--color-accent-glow)',
'color-accent-alpha-10': 'var(--color-accent-alpha-10)',
'color-accent-alpha-12': 'var(--color-accent-alpha-12)',
'color-accent-alpha-15': 'var(--color-accent-alpha-15)',
'color-accent-alpha-18': 'var(--color-accent-alpha-18)',
'color-accent-alpha-20': 'var(--color-accent-alpha-20)',
'color-accent-alpha-25': 'var(--color-accent-alpha-25)',
'color-accent-alpha-30': 'var(--color-accent-alpha-30)',
'color-accent-alpha-40': 'var(--color-accent-alpha-40)',
'color-accent-alpha-50': 'var(--color-accent-alpha-50)',
'color-accent-alpha-60': 'var(--color-accent-alpha-60)',
'color-accent-alpha-80': 'var(--color-accent-alpha-80)',
'error': 'var(--color-error)',
'error-light': 'var(--color-error-light)',
'error-hover': 'var(--color-error-hover)',
'error-glow': 'var(--color-error-glow)',
'color-error': 'var(--color-error)',
'color-error-light': 'var(--color-error-light)',
'color-error-hover': 'var(--color-error-hover)',
'color-error-glow': 'var(--color-error-glow)',
'success': 'var(--color-success)',
'success-light': 'var(--color-success-light)',
'success-hover': 'var(--color-success-hover)',
'success-glow': 'var(--color-success-glow)',
'color-success': 'var(--color-success)',
'color-success-light': 'var(--color-success-light)',
'color-success-hover': 'var(--color-success-hover)',
'color-success-glow': 'var(--color-success-glow)',
'warning': 'var(--color-warning)',
'warning-light': 'var(--color-warning-light)',
'warning-hover': 'var(--color-warning-hover)',
'warning-glow': 'var(--color-warning-glow)',
'color-warning': 'var(--color-warning)',
'color-warning-light': 'var(--color-warning-light)',
'color-warning-hover': 'var(--color-warning-hover)',
'color-warning-glow': 'var(--color-warning-glow)',
'orange': 'var(--color-orange)',
'orange-light': 'var(--color-orange-light)',
'orange-hover': 'var(--color-orange-hover)',
'orange-glow': 'var(--color-orange-glow)',
'color-orange': 'var(--color-orange)',
'color-orange-light': 'var(--color-orange-light)',
'color-orange-hover': 'var(--color-orange-hover)',
'color-orange-glow': 'var(--color-orange-glow)',
'collab': 'var(--color-collab)',
'collab-hover': 'var(--color-collab-hover)',
'collab-light': 'var(--color-collab-light)',
'collab-lighter': 'var(--color-collab-lighter)',
'collab-glow': 'var(--color-collab-glow)',
'color-collab': 'var(--color-collab)',
'color-collab-hover': 'var(--color-collab-hover)',
'color-collab-light': 'var(--color-collab-light)',
'color-collab-lighter': 'var(--color-collab-lighter)',
'color-collab-glow': 'var(--color-collab-glow)',
'color-collab-10': 'var(--color-collab-10)',
'pink': 'var(--color-pink)',
'color-pink': 'var(--color-pink)',
'color-pink-alpha-10': 'var(--color-pink-alpha-10)',
'color-pink-alpha-20': 'var(--color-pink-alpha-20)',
'cyan': 'var(--color-cyan)',
'color-cyan': 'var(--color-cyan)',
'teal': 'var(--color-teal)',
'color-teal': 'var(--color-teal)',
'indigo': 'var(--color-indigo)',
'indigo-hover': 'var(--color-indigo-hover)',
'color-indigo': 'var(--color-indigo)',
'color-indigo-hover': 'var(--color-indigo-hover)',
// === 用途化变量 — OAuth 第三方平台独立品牌色(--oauth-*) ===
'oauth-qq': 'var(--oauth-qq)',
'oauth-wx': 'var(--oauth-wx)',
'oauth-alipay': 'var(--oauth-alipay)',
'oauth-sina': 'var(--oauth-sina)',
'oauth-baidu': 'var(--oauth-baidu)',
'oauth-douyin': 'var(--oauth-douyin)',
'oauth-huawei': 'var(--oauth-huawei)',
'oauth-xiaomi': 'var(--oauth-xiaomi)',
'oauth-gitee': 'var(--oauth-gitee)',
'oauth-bilibili': 'var(--oauth-bilibili)',
'oauth-kuaishou': 'var(--oauth-kuaishou)',
'oauth-casdoor': 'var(--oauth-casdoor)',
// Google 四色
'oauth-google-blue': 'var(--oauth-google-blue)',
'oauth-google-red': 'var(--oauth-google-red)',
'oauth-google-yellow': 'var(--oauth-google-yellow)',
'oauth-google-green': 'var(--oauth-google-green)',
// 旧命名向后兼容
'color-oauth-qq': 'var(--oauth-qq)',
'color-oauth-wx': 'var(--oauth-wx)',
'color-oauth-alipay': 'var(--oauth-alipay)',
'color-oauth-sina': 'var(--oauth-sina)',
'color-oauth-baidu': 'var(--oauth-baidu)',
'color-oauth-douyin': 'var(--oauth-douyin)',
'color-oauth-huawei': 'var(--oauth-huawei)',
'color-oauth-xiaomi': 'var(--oauth-xiaomi)',
'color-oauth-gitee': 'var(--oauth-gitee)',
'color-oauth-bilibili': 'var(--oauth-bilibili)',
'color-oauth-kuaishou': 'var(--oauth-kuaishou)',
// === 用途化变量 — 面板背景色 ===
'panel-bg': 'var(--panel-bg)',
'panel-bg-alt': 'var(--panel-bg-alt)',
'panel-bg-hover': 'var(--panel-bg-hover)',
'panel-bg-deep': 'var(--panel-bg-deep)',
'panel-bg-deepest': 'var(--panel-bg-deepest)',
'panel-bg-overlay': 'var(--panel-bg-overlay)',
'panel-bg-elevated': 'var(--panel-bg-elevated)',
'panel-bg-dialog': 'var(--panel-bg-dialog)',
'panel-bg-subtle': 'var(--panel-bg-subtle)',
'panel-bg-tertiary': 'var(--panel-bg-tertiary)',
'panel-bg-quaternary': 'var(--panel-bg-quaternary)',
'panel-bg-contrast': 'var(--panel-bg-contrast)',
'panel-bg-dark': 'var(--panel-bg-dark)',
'panel-bg-darker': 'var(--panel-bg-darker)',
'panel-bg-darkest': 'var(--panel-bg-darkest)',
'panel-bg-compact': 'var(--panel-bg-compact)',
'panel-bg-flat': 'var(--panel-bg-flat)',
'panel-bg-raised': 'var(--panel-bg-raised)',
'panel-bg-inset': 'var(--panel-bg-inset)',
'panel-bg-dark-40': 'var(--panel-bg-dark-40)',
'panel-bg-dark-50': 'var(--panel-bg-dark-50)',
'panel-bg-dark-80': 'var(--panel-bg-dark-80)',
'panel-bg-darkest-20': 'var(--panel-bg-darkest-20)',
'panel-bg-darkest-30': 'var(--panel-bg-darkest-30)',
'panel-bg-darkest-40': 'var(--panel-bg-darkest-40)',
'panel-bg-darkest-50': 'var(--panel-bg-darkest-50)',
'panel-bg-darkest-60': 'var(--panel-bg-darkest-60)',
'panel-bg-darkest-80': 'var(--panel-bg-darkest-80)',
'panel-bg-darkest-95': 'var(--panel-bg-darkest-95)',
'panel-bg-deepest-30': 'var(--panel-bg-deepest-30)',
'panel-bg-deepest-40': 'var(--panel-bg-deepest-40)',
'panel-bg-deepest-50': 'var(--panel-bg-deepest-50)',
'panel-bg-deepest-80': 'var(--panel-bg-deepest-80)',
'panel-bg-deepest-95': 'var(--panel-bg-deepest-95)',
'panel-bg-hover-50': 'var(--panel-bg-hover-50)',
// === 用途化变量 — 文字灰度 ===
'text-muted': 'var(--text-muted)',
'text-muted-light': 'var(--text-muted-light)',
'text-muted-dark': 'var(--text-muted-dark)',
'text-tertiary-hover': 'var(--text-tertiary-hover)',
'text-primary-light': 'var(--text-primary-light)',
'text-primary-lighter': 'var(--text-primary-lighter)',
'text-link': 'var(--text-link)',
'text-link-hover': 'var(--text-link-hover)',
'text-highlight': 'var(--text-highlight)',
// === 用途化变量 — 边框与分割线 ===
'panel-border': 'var(--panel-border)',
'panel-border-active': 'var(--panel-border-active)',
'panel-border-subtle': 'var(--panel-border-subtle)',
'panel-border-light': 'var(--panel-border-light)',
'panel-border-dark': 'var(--panel-border-dark)',
'panel-border-highlight': 'var(--panel-border-highlight)',
'panel-border-brand': 'var(--panel-border-brand)',
'panel-border-error': 'var(--panel-border-error)',
// === 用途化变量 — 状态色 ===
'status-info-bg': 'var(--status-info-bg)',
'status-info-border': 'var(--status-info-border)',
'status-info-glow': 'var(--status-info-glow)',
'status-info-icon': 'var(--status-info-icon)',
'status-success-icon': 'var(--status-success-icon)',
'status-warning-icon': 'var(--status-warning-icon)',
'status-error-icon': 'var(--status-error-icon)',
'status-error-deep': 'var(--status-error-deep)',
// === 卡片 ===
'card-bg': 'var(--card-bg)',
'card-border': 'var(--card-border)',
'card-hover-border': 'var(--card-hover-border)',
// === 输入框 ===
'input-bg': 'var(--input-bg)',
'input-border': 'var(--input-border)',
'input-text': 'var(--input-text)',
'input-placeholder': 'var(--input-placeholder)',
// === 模态框 ===
'modal-bg': 'var(--modal-bg)',
'modal-border': 'var(--modal-border)',
// === Overlay / 遮罩层半透明(深色=白半透明,浅色=黑半透明) ===
'overlay-1': 'var(--overlay-1)',
'overlay-2': 'var(--overlay-2)',
'overlay-3': 'var(--overlay-3)',
'overlay-4': 'var(--overlay-4)',
'overlay-5': 'var(--overlay-5)',
'overlay-6': 'var(--overlay-6)',
'overlay-7': 'var(--overlay-7)',
'overlay-8': 'var(--overlay-8)',
'overlay-10': 'var(--overlay-10)',
'overlay-12': 'var(--overlay-12)',
'overlay-14': 'var(--overlay-14)',
'overlay-15': 'var(--overlay-15)',
'overlay-16': 'var(--overlay-16)',
'overlay-18': 'var(--overlay-18)',
'overlay-20': 'var(--overlay-20)',
'overlay-22': 'var(--overlay-22)',
'overlay-25': 'var(--overlay-25)',
'overlay-30': 'var(--overlay-30)',
'overlay-40': 'var(--overlay-40)',
'overlay-42': 'var(--overlay-42)',
'overlay-45': 'var(--overlay-45)',
'overlay-50': 'var(--overlay-50)',
'overlay-52': 'var(--overlay-52)',
'overlay-55': 'var(--overlay-55)',
'overlay-58': 'var(--overlay-58)',
'overlay-60': 'var(--overlay-60)',
'overlay-62': 'var(--overlay-62)',
'overlay-70': 'var(--overlay-70)',
'overlay-78': 'var(--overlay-78)',
'overlay-80': 'var(--overlay-80)',
'overlay-85': 'var(--overlay-85)',
'overlay-86': 'var(--overlay-86)',
'overlay-88': 'var(--overlay-88)',
'overlay-90': 'var(--overlay-90)',
'overlay-92': 'var(--overlay-92)',
'overlay-95': 'var(--overlay-95)',
// === Mask / 暗色遮罩 ===
'mask-2': 'var(--mask-2)',
'mask-4': 'var(--mask-4)',
'mask-5': 'var(--mask-5)',
'mask-6': 'var(--mask-6)',
'mask-8': 'var(--mask-8)',
'mask-10': 'var(--mask-10)',
'mask-15': 'var(--mask-15)',
'mask-20': 'var(--mask-20)',
'mask-25': 'var(--mask-25)',
'mask-30': 'var(--mask-30)',
'mask-40': 'var(--mask-40)',
'mask-50': 'var(--mask-50)',
'mask-60': 'var(--mask-60)',
'mask-70': 'var(--mask-70)',
'mask-80': 'var(--mask-80)',
'mask-95': 'var(--mask-95)',
},
// 圆角映射
borderRadius: {
sm: 'var(--radius-sm)',
md: 'var(--radius-md)',
lg: 'var(--radius-lg)',
xl: 'var(--radius-xl)',
'2xl': 'var(--radius-2xl)',
},
// 间距映射
spacing: {
xs: 'var(--spacing-xs)',
sm: 'var(--spacing-sm)',
md: 'var(--spacing-md)',
lg: 'var(--spacing-lg)',
xl: 'var(--spacing-xl)',
'2xl': 'var(--spacing-2xl)',
'3xl': 'var(--spacing-3xl)',
},
},
},
// 引入全局CSS
css: [
'~/assets/css/variables.css',
'~/assets/css/components.css',
'~/assets/css/main.css',
'~/assets/css/transitions.css',
'~/assets/css/mobile-admin.css',
'~/assets/css/print-fix.css',
'~/assets/css/sf-pro-icons.css',
'~/assets/css/markdown.css'
],
// 配置运行时配置
runtimeConfig: {
// 服务器私有键(不会暴露到客户端)
jwtSecret,
// Redis配置(可选)
redisUrl: process.env.REDIS_URL || '',
sentry: {
dsn: process.env.SENTRY_DSN || backendSentryDsnDefault,
environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'development',
release:
process.env.SENTRY_RELEASE ||
process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.COMMIT_REF ||
'',
tracesSampleRate: readNumberEnv(process.env.SENTRY_TRACES_SAMPLE_RATE, 0.1),
enabled: sentryRuntimeEnabled
},
// 公共键(会暴露到客户端)
public: {
// 配置后启用内部 API 的 CORS 来源校验;未配置时仅回环 Host 跳过,云平台公开 Host 仍校验
host: process.env.NUXT_PUBLIC_HOST || '',
apiBase: '/api',
oauth: {
github: !!process.env.GITHUB_CLIENT_ID,
casdoor: !!process.env.CASDOOR_CLIENT_ID,
google: !!process.env.GOOGLE_CLIENT_ID
},
siteTitle,
siteLogo,
siteDescription,
isNetlify: process.env.NETLIFY === 'true',
sentry: {
dsn: process.env.NUXT_PUBLIC_SENTRY_DSN || frontendSentryDsnDefault,
environment:
process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT ||
process.env.SENTRY_ENVIRONMENT ||
process.env.NODE_ENV ||
'development',
release:
process.env.NUXT_PUBLIC_SENTRY_RELEASE ||
process.env.SENTRY_RELEASE ||
process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.COMMIT_REF ||
'',
tracesSampleRate: readNumberEnv(
process.env.NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ||
process.env.SENTRY_TRACES_SAMPLE_RATE,
0.1
),
replaysSessionSampleRate: readNumberEnv(
process.env.NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
0
),
replaysOnErrorSampleRate: readNumberEnv(
process.env.NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
0
),
enabled: sentryRuntimeEnabled
}
}
},
// 配置环境变量
app: {
head: {
// 首帧前同步恢复主题:SSR 输出的 HTML 无 data-theme,主题 CSS 变量未定义会导致加载页背景透明、主页内容透出
script: [
{
tagPriority: 'critical',
// 与 useTheme.ts 默认逻辑一致:无保存值或非法值时回退 ClassicDark;System 按系统配色偏好解析
innerHTML: "(function(){var t='ClassicDark';try{var s=localStorage.getItem('voicehub-theme');if(['ClassicDark','ClassicLight','ModernLight'].indexOf(s)>-1){t=s}else if(s==='System'){t=window.matchMedia('(prefers-color-scheme: dark)').matches?'ClassicDark':'ClassicLight'}}catch(e){}document.documentElement.setAttribute('data-theme',t)})()"
}
],
title: siteTitle,
meta: [
{ charset: 'utf-8' },
{ name: 'referrer', content: 'no-referrer' },
{
name: 'viewport',
content:
'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover'
},
{
name: 'description',
content: siteDescription
},
// Open Graph 标签
{ property: 'og:type', content: 'website' },
{ property: 'og:title', content: siteTitle },
{ property: 'og:description', content: siteDescription },
{ property: 'og:site_name', content: siteTitle },
{ property: 'og:image', content: absoluteLogo },
// Twitter 标签
{ name: 'twitter:card', content: 'summary' },
{ name: 'twitter:title', content: siteTitle },
{ name: 'twitter:description', content: siteDescription },
{ name: 'twitter:image', content: absoluteLogo },
// 移动端优化
{ name: 'theme-color', content: '#111111' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
{ name: 'apple-mobile-web-app-title', content: siteShortName },
{ name: 'mobile-web-app-capable', content: 'yes' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
// 优先加载常规字体,确保页面快速显示
{
rel: 'preload',
as: 'style',
crossorigin: 'anonymous',
href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css'
},
{
rel: 'stylesheet',
crossorigin: 'anonymous',
href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css'
},
// 延迟加载其他字重,避免阻塞页面渲染
{
rel: 'preload',
as: 'style',
crossorigin: 'anonymous',
href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Medium.min.css',
onload: "this.onload=null;this.rel='stylesheet'"
},
{
rel: 'preload',
as: 'style',
crossorigin: 'anonymous',
href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Semibold.min.css',
onload: "this.onload=null;this.rel='stylesheet'"
},
{
rel: 'preload',
as: 'style',
crossorigin: 'anonymous',
href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Bold.min.css',
onload: "this.onload=null;this.rel='stylesheet'"
}
]
}
},
features: {
inlineStyles: false
},
// TypeScript配置
typescript: {
strict: true
},
// 服务器端配置
nitro: {
preset: process.env.VERCEL
? 'vercel'
: process.env.NETLIFY
? 'netlify'
: process.env.NITRO_PRESET || 'node-server',
// 增强错误处理和稳定性
experimental: {
wasm: true,
asyncContext: true
},
// @applemusic-like-lyrics/lyric 的 WASM 模块含顶层 await,需保留现代 ESM 语法。
esbuild: {
options: {
target: 'esnext'
}
},
externals: {
inline: ssrInlineLyricPackages
},
timing: true,
// 增加请求超时时间
routeRules: {
// 完全禁用所有API路由的缓存,确保每次都请求数据库
'/api/**': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
Connection: 'keep-alive'
}
},
// 静态资源文件缓存配置
'/_nuxt/**': {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
},
'/assets/**': {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
},
'/favicon.ico': {
headers: {
'Cache-Control': 'public, max-age=86400'
}
},
// 图片、CSS、JS等静态资源缓存
'/**/*.{png,jpg,jpeg,gif,webp,svg,ico}': {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
},
'/**/*.{css,js}': {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
},
// 认证相关页面不缓存
'/login': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0'
}
},
'/dashboard': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0'
}
},
'/change-password': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0'
}
},
'/auth/**': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0'
}
},
'/notification-settings': {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0'
}
}
},
// 根据部署环境调整配置
...(process.env.VERCEL
? {
vercel: {
functions: {
maxDuration: 60
}
}
}
: process.env.NETLIFY
? {
// Netlify 环境:确保 Drizzle 正确打包
experimental: {
wasm: true
}
}
: {
// 其他环境:使用标准配置
})
},
// Vite 配置
vite: {
plugins: [wasm()],
optimizeDeps: {
include: ['drizzle-orm'],
exclude: [
'@applemusic-like-lyrics/vue',
'@applemusic-like-lyrics/lyric',
'#app-manifest',
'nuxt'
]
},
build: {
target: 'esnext',
// Vite 8 默认使用 Lightning CSS,会把连续声明中的标准毛玻璃属性误判为重复项
cssMinify: 'esbuild',
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes('node_modules')) return
if (id.includes('@lucide/vue')) return 'icons'
if (id.includes('@pixi')) return 'pixi'
if (id.includes('@applemusic-like-lyrics')) return 'lyric-engine'
if (id.includes('drizzle-orm') || id.includes('postgres')) return 'database'
if (id.includes('xlsx') || id.includes('jspdf') || id.includes('jszip')) return 'office'
}
}
}
},
// 添加 WASM 支持配置
assetsInclude: ['**/*.wasm'],
// SSR配置
ssr: {
noExternal: [
...(process.env.VERCEL ? [] : ['drizzle-orm', 'postgres']),
...ssrInlineLyricPackages
]
}
},
pwa: {
registerType: 'autoUpdate',
manifest: {
name: siteTitle,
short_name: siteShortName,
description: siteDescription,
theme_color: '#111111',
background_color: '#111111',
display: 'standalone',
orientation: 'portrait',
scope: '/',
start_url: '/',
icons: [
{
src: '/assets/logo.png',
sizes: '128x128',
type: 'image/png',
purpose: 'any'
},
{
src: '/assets/logo-144.png',
sizes: '144x144',
type: 'image/png',
purpose: 'any'
}
]
},
workbox: {
navigateFallback: null,
globPatterns: ['**/*.{js,css,html,png,svg,ico,woff2}']
},
devOptions: {
enabled: true,
type: 'module'
},
injectRegister: 'auto'
}
})