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' } })