import wasm from 'vite-plugin-wasm' import glsl from 'vite-plugin-glsl' import { fileURLToPath } from 'url' import { index, uniqueIndex } from 'drizzle-orm' const customSeoConfig: Record | null = (() => { const raw = process.env.NUXT_PUBLIC_SEO_CONFIG if (!raw) return null try { const parsed = JSON.parse(raw) if (parsed && typeof parsed === 'object') return parsed as Record return null } catch { return null } })() function readNumberEnv(name: string, defaultValue: number): number { const raw = process.env[name] if (raw === undefined || raw === '') return defaultValue const num = Number(raw) return Number.isFinite(num) ? num : defaultValue } // 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: [ '@nuxtjs/tailwindcss', ...((process.env.NODE_ENV === 'development' || process.env.npm_lifecycle_event?.includes('lint')) ? ['@nuxt/eslint'] : []), ...((process.env.PWA_ENABLED || 'false') === 'true' ? ['@vite-pwa/nuxt'] : []) ], pwa: (process.env.PWA_ENABLED || 'false') === 'true' ? { registerType: 'autoUpdate', manifest: { name: process.env.NUXT_PUBLIC_SITE_TITLE || '校园广播站点歌系统', short_name: process.env.NUXT_PUBLIC_SITE_TITLE || 'VoiceHub', description: process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '校园广播站点歌系统', display: (process.env.PWA_DISPLAY as any) || 'standalone', background_color: process.env.PWA_BACKGROUND_COLOR || '#000000', theme_color: process.env.PWA_THEME_COLOR || '#111111', icons: [ { src: '/images/logo.png', sizes: '192x192', type: 'image/png' }, { src: '/images/logo.png', sizes: '512x512', type: 'image/png' } ] }, workbox: { navigateFallback: '/', globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico,webp,woff2}'] }, client: { installPrompt: true, periodicSyncForUpdates: 3600 }, devOptions: { enabled: process.env.NODE_ENV === 'development' } } : {}, // 引入全局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' ], build: { transpile: [] }, // 实验性优化功能 experimental: { payloadExtraction: true, renderJsonPayloads: true, viewTransition: true }, // 配置运行时配置 runtimeConfig: { // 服务器私有键(不会暴露到客户端) jwtSecret: process.env.JWT_SECRET || 'your-secret-key', // Redis配置(可选) redisUrl: process.env.REDIS_URL || '', redisKeyPrefix: process.env.REDIS_KEY_PREFIX || '', // Sentry 服务端配置(不会暴露到客户端) sentry: { serverDsn: process.env.SENTRY_SERVER_DSN || process.env.SENTRY_DSN || '', dsn: process.env.SENTRY_DSN || '', errorSampleRate: readNumberEnv('SENTRY_ERROR_SAMPLE_RATE', 1.0), tracesSampleRate: readNumberEnv('SENTRY_TRACES_SAMPLE_RATE', 1.0), replaySessionSampleRate: readNumberEnv('SENTRY_REPLAY_SESSION_SAMPLE_RATE', 0), replayOnErrorSampleRate: readNumberEnv('SENTRY_REPLAY_ON_ERROR_SAMPLE_RATE', 0), release: process.env.SENTRY_RELEASE || '', environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'production' }, // 公共键(会暴露到客户端) public: { 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, aggregate: false }, siteTitle: process.env.NUXT_PUBLIC_SITE_TITLE || '校园广播站点歌系统', siteLogo: process.env.NUXT_PUBLIC_SITE_LOGO || '', siteDescription: process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '校园广播站点歌系统 - 让你的声音被听见', isNetlify: process.env.NETLIFY === 'true', // SEO 自定义配置(通过 NUXT_PUBLIC_SEO_CONFIG 注入 JSON 字符串) seoConfig: customSeoConfig ?? {}, // 图形验证码相关 captcha: { enabled: (process.env.CAPTCHA_ENABLED || 'false') === 'true', provider: process.env.CAPTCHA_PROVIDER || 'turnstile', siteKey: process.env.CAPTCHA_SITE_KEY || '' }, // PWA 相关结构(本地不启用模块,但保留配置项以便上层应用使用) pwa: { enabled: (process.env.PWA_ENABLED || 'false') === 'true', display: process.env.PWA_DISPLAY || 'standalone', backgroundColor: process.env.PWA_BACKGROUND_COLOR || '#000000', themeColor: process.env.PWA_THEME_COLOR || '#111111' }, // 主题配置 defaultTheme: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicDark', enabledThemes: (process.env.NUXT_PUBLIC_ENABLED_THEMES || 'System,ClassicDark,ClassicLight,ModernLight') .split(',') .map((s) => s.trim()) .filter(Boolean), // Sentry 客户端配置(仅公开 DSN 与采样率) sentry: { dsn: process.env.SENTRY_DSN || '', tracesSampleRate: readNumberEnv('SENTRY_TRACES_SAMPLE_RATE', 1.0), replaySessionSampleRate: readNumberEnv('SENTRY_REPLAY_SESSION_SAMPLE_RATE', 0), replayOnErrorSampleRate: readNumberEnv('SENTRY_REPLAY_ON_ERROR_SAMPLE_RATE', 0) } } }, // 配置环境变量 app: { head: { title: process.env.NUXT_PUBLIC_SITE_TITLE || '校园广播站点歌系统', htmlAttrs: { lang: 'zh-CN' }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover' }, { name: 'description', content: process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '校园广播站点歌系统 - 让你的声音被听见' }, // 移动端优化 { name: 'theme-color', content: '#111111' }, { name: 'x-default-theme', content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicDark' }, { 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: 'VoiceHub管理' }, { name: 'mobile-web-app-capable', content: 'yes' }, { name: 'format-detection', content: 'telephone=no' } ], script: [ { type: 'text/javascript', tagPosition: 'head', children: [ `(function(){try{`, ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null;`, ` try{s=localStorage.getItem(k)}catch(e){s=null}`, ` if(!s){`, ` try{`, ` var meta=document.querySelector('meta[name="x-default-theme"]');`, ` if(meta && meta.content){s=meta.content}`, ` }catch(e){s=null}`, ` if(!s){s='ClassicDark'}`, ` }`, ` if(s==='System'||s==='system'){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`, ` else if(typeof s==='string'){eff=s.endsWith('Dark')||s==='dark'?'dark':(s.endsWith('Light')||s==='light'?'light':s)}`, ` else{eff='dark'}`, ` r.setAttribute('data-theme', (typeof s==='string'?s:eff));`, ` if(eff==='dark'){r.classList.add('dark'); r.classList.remove('light'); r.style.colorScheme='dark'}`, ` else if(eff==='light'){r.classList.add('light'); r.classList.remove('dark'); r.style.colorScheme='light'}`, ` else{r.classList.remove('dark','light'); r.style.colorScheme='light dark'}`, ` try{`, ` var tm=d.querySelector('meta[name="theme-color"]');`, ` if(tm){tm.content=(eff==='light'?'#f8fafc':'#111111')}`, ` }catch(e){}`, `}catch(e){}})();` ].join('') } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'preload', as: 'image', href: '/images/logo.png' }, { rel: 'preload', as: 'image', href: '/images/site-logo.png' }, { rel: 'preload', as: 'image', href: '/images/school-logo-white.png' }, // 优先加载常规字体,确保页面快速显示 { rel: 'preload', as: 'style', href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css' }, { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css' }, // 延迟加载其他字重,避免阻塞页面渲染 { rel: 'preload', as: 'style', 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', 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', 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: { compressPublicAssets: false, // 禁用公共资产压缩,防止构建时内存溢出 preset: process.env.VERCEL ? 'vercel' : process.env.NETLIFY ? 'netlify' : process.env.NITRO_PRESET || 'node-server', // 明确关闭 prerender / 爬取链接,避免 Node 端不必要的预渲染浪费内存和依赖数据库环境 prerender: { crawlLinks: false, routes: [] }, minify: true, sourceMap: false, // 增强错误处理和稳定性 experimental: { wasm: true, asyncContext: true }, // 强制把巨型 Node native 库外置,防止 Nitro 打包时撑爆内存 (OOM 137) externals: { external: [ 'drizzle-orm', 'postgres' ] }, 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 环境:使用标准配置 } : process.env.NETLIFY ? { // Netlify 环境:确保 Drizzle 正确打包 experimental: { wasm: true } } : { // 其他环境:使用标准配置 }) }, // Vite 配置 vite: { build: { chunkSizeWarningLimit: 2000, cssCodeSplit: true, sourcemap: false, target: 'esnext', reportCompressedSize: false, rollupOptions: { maxParallelFileOps: 1, cache: false } }, resolve: { alias: [ { find: '@applemusic-like-lyrics/core/style.css', replacement: fileURLToPath( new URL('./vendor/amll-core/src/styles/index.css', import.meta.url) ) }, { find: '@applemusic-like-lyrics/core', replacement: fileURLToPath(new URL('./vendor/amll-core/src/index.ts', import.meta.url)) } ] }, plugins: [wasm(), glsl()], optimizeDeps: { include: ['drizzle-orm'], exclude: ['@applemusic-like-lyrics/vue', '@applemusic-like-lyrics/lyric'] }, // SSR配置: Vercel/Netlify serverless 需要 noExternal 把 drizzle/postgres inline; // Node-server 环境直接从 node_modules require 即可, 避免打包 inline 导致 OOM ssr: { noExternal: process.env.NETLIFY ? ['drizzle-orm', 'postgres'] : process.env.VERCEL ? [] : [] } } })