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: !!process.env.AGGREGATE_OAUTH_APP_ID }, siteTitle: process.env.NUXT_PUBLIC_SITE_TITLE || '鏍″洯骞挎挱绔欑偣姝岀郴缁?, siteLogo: process.env.NUXT_PUBLIC_SITE_LOGO || '', siteDescription: process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '鏍″洯骞挎挱绔欑偣姝岀郴缁?- 璁╀綘鐨勫0闊宠鍚', 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 || 'ClassicLight', enabledThemes: (process.env.NUXT_PUBLIC_ENABLED_THEMES || '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 || '鏍″洯骞挎挱绔欑偣姝岀郴缁?- 璁╀綘鐨勫0闊宠鍚' }, // 绉诲姩绔紭鍖? { name: 'theme-color', content: '#111111' }, { name: 'x-default-theme', content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'System' }, { 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='System'}`, ` }`, ` 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: true, preset: process.env.VERCEL ? 'vercel' : process.env.NETLIFY ? 'netlify' : process.env.NITRO_PRESET || 'node-server', // 澧炲己閿欒澶勭悊鍜岀ǔ瀹氭€? experimental: { wasm: true, asyncContext: true }, timing: true, // 澧炲姞璇锋眰瓒呮椂鏃堕棿 routeRules: { // 瀹屽叏绂佺敤鎵€鏈堿PI璺敱鐨勭紦瀛橈紝纭繚姣忔閮借姹傛暟鎹簱 '/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' } }, // 鍥剧墖銆丆SS銆丣S绛夐潤鎬佽祫婧愮紦瀛? '/**/*.{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 姝g‘鎵撳寘 experimental: { wasm: true } } : { // 鍏朵粬鐜锛氫娇鐢ㄦ爣鍑嗛厤缃? }) }, // Vite 閰嶇疆 vite: { build: { chunkSizeWarningLimit: 2000, cssCodeSplit: true, rollupOptions: { maxParallelFileOps: 2, output: { manualChunks(id) { if (id.includes('node_modules/lucide-vue-next')) { return 'lucide' } } } } }, 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'] }, build: { sourcemap: false, rollupOptions: { output: { manualChunks(id) { if (!id.includes('node_modules')) return if (id.includes('lucide-vue-next')) 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.NETLIFY ? ['drizzle-orm', 'postgres'] : process.env.VERCEL ? [] : ['drizzle-orm', 'postgres'] } } })