import { fileURLToPath, URL } from 'node:url' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' import { VitePWA } from 'vite-plugin-pwa' type GetModuleInfo = (id: string) => { importers: readonly string[] } | null /** pako / jszip 等 office 深依赖路径无特征,沿 importer 链上溯: * 任一条链经过 @vue-office 即视为 office 生态模块 */ function fromVueOffice(id: string, getModuleInfo: GetModuleInfo, seen = new Set()): boolean { if (seen.has(id)) return false seen.add(id) const importers = getModuleInfo(id)?.importers ?? [] return importers.some( (imp) => imp.includes('@vue-office') || fromVueOffice(imp, getModuleInfo, seen), ) } /** 大体积三方库归入命名 chunk:主入口显著瘦身,命名亦供 PWA 预缓存排除 * (这些块全部只被懒加载路径引用,拆出不影响首屏)。 */ function vendorChunk(id: string, getModuleInfo: GetModuleInfo): string | undefined { if (!id.includes('node_modules')) return undefined if (id.includes('@vue-office/pdf') || id.includes('pdfjs')) return 'office-pdf' if (id.includes('@vue-office/excel') || id.includes('exceljs') || id.includes('xlsx')) return 'office-excel' if (id.includes('@vue-office/docx') || id.includes('docx')) return 'office-docx' if (id.includes('echarts') || id.includes('zrender')) return 'echarts' if (id.includes('naive-ui')) return 'naive-ui' if (id.includes('highlight.js')) return 'hljs' if (fromVueOffice(id, getModuleInfo)) return 'office-shared' return undefined } export default defineConfig({ plugins: [ vue(), tailwindcss(), VitePWA({ registerType: 'autoUpdate', manifest: { name: 'OCI Portal', short_name: 'OCI Portal', description: '自托管 OCI 多租户管理面板', lang: 'zh-CN', display: 'standalone', theme_color: '#F5F4ED', background_color: '#F5F4ED', icons: [ { src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }, { src: '/pwa-512.png', sizes: '512x512', type: 'image/png' }, { src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, ], }, workbox: { globPatterns: ['**/*.{js,css,html,svg,png,woff2}'], // 预缓存只收应用壳(入口/naive-ui/echarts/路由块);office 全家 // (pdf/excel/docx/shared,合计 ~6MB)安装时不下载,首次使用再入缓存 globIgnores: ['**/office-*.js'], navigateFallback: 'index.html', // API 与 AI 网关直通不落 SPA 壳也不缓存:运维数据必须实时 navigateFallbackDenylist: [/^\/api\//, /^\/ai\//], runtimeCaching: [ { // 未进预缓存的大块兜底:assets 带内容 hash 不可变,CacheFirst 恒安全 urlPattern: /\/assets\/.+\.js$/, handler: 'CacheFirst', options: { cacheName: 'lazy-vendor', expiration: { maxEntries: 30, maxAgeSeconds: 30 * 24 * 3600 }, }, }, ], }, }), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, // noVNC 1.7 使用 top-level await,需较新的构建目标(现代浏览器均支持); // 生产构建与 dev 依赖预构建(esbuild)都要提高 target,否则 dev 会报同一错误 build: { target: 'es2022', // 现存最大懒块 office-pdf(pdfjs 全量)~2.8MB,均为按需加载不入首屏; // 阈值取其上放宽,只为「新失控块」鸣笛而不对既有三方整包常鸣 chunkSizeWarningLimit: 2900, rollupOptions: { output: { manualChunks: (id, { getModuleInfo }) => vendorChunk(id, getModuleInfo), }, }, }, optimizeDeps: { esbuildOptions: { target: 'es2022', }, }, server: { port: 5173, host: '127.0.0.1', proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, ws: true, }, // 只代理网关端点 /ai/*;裸 '/ai' 前缀会误拦 /ai-gateway 页面路由 '^/ai/': { target: 'http://localhost:8080', changeOrigin: true, ws: true, }, }, }, })