import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path'; // C-01: vite 代理精确收窄 —— 只代理后端真实存在的 API 路径,SPA 路由一律放行。 // 后端真实挂载(chunyu_project/chunyu_project/urls.py + 各 app urls.py): // /api/*(api.urls:getIpData/baiduFanyi/tool/weather/air-quality/currency/captcha/shorturl/...) // /api/apidirectory/*、/user/*、/bug/reports/*、/article/articles|my-articles|comments/* // /learn/*、/history/records/*、/message/*、/chat/*、/tool/*、/search/*、/app/*、 // /logs/api/*、/shorturl/*、/s//(短链跳转)、/admin/、/swagger*、/redoc/、/i18n/、/media/ // SPA 路由(/open-api*、/post/*、/user-home*、/api-docs 旧重定向等)不在此列, // 由 bypass 精确放行,避免前缀误伤(如 /api 劫持 /open-api?不——新 SPA 已迁走, // 但 /api-docs 旧重定向、/article 旧重定向等仍需放行到前端)。 const BACKEND = 'http://127.0.0.1:8002' // 纯 SPA 前缀:命中即放行(返回 undefined → 走 vite dev 中间件/前端路由) const SPA_PREFIXES = [ '/open-api', '/post/', '/user-home', '/articles', '/utility', '/learn-manage', '/learn-editor', '/article-manage', '/article-editor', '/bug-detail', '/tool-detail', '/course-learn', '/shorturl-detail', '/currency-detail', '/ip-location', '/baidu-translate', '/qrcode-generator', '/color-picker', '/changelog', '/favorites', '/console', '/profile', '/settings', '/history', '/favorites', '/use', '/document', '/manual', '/privacy', '/agreement', '/messages', '/chat', '/search', '/bug', '/learn', '/wallet', '/task-center', '/invite', '/forgot-password', '/register', ] // 精确 SPA 路由(无尾部、与后端前缀撞名的几个) const SPA_EXACT = new Set([ '/api-docs', '/api-directory', '/api', '/user', '/article-manage', '/article-editor', '/bug-detail', ]) // C-01 旧分享链接对应的 SPA 页面(已迁到 /open-api*,但旧 URL 仍由前端 承接) const LEGACY_API_PAGES = new Set([ '/api/air-quality-details', '/api/weather-details', '/api/qrcode-generator', '/api/ip-location', '/api/password-generator', ]) function isSpaPath(url: string | undefined): boolean { if (!url) return false const p = url.split('?')[0] if (SPA_EXACT.has(p)) return true if (LEGACY_API_PAGES.has(p)) return true // 旧重定向路由也属 SPA if (p.startsWith('/api-detail') || p.startsWith('/api-docs') || p.startsWith('/api-directory')) return true if (p.startsWith('/article/') || p === '/article') return true if (p.startsWith('/user/') || p === '/user') return true return SPA_PREFIXES.some((pre) => p === pre || p.startsWith(pre + '/') || (pre.endsWith('/') ? p.startsWith(pre) : p.startsWith(pre + '/') || p === pre)) } function spaBypass(req: { url?: string }) { if (isSpaPath(req.url)) return req.url // 放行到前端 } // 后端真实 API 上下文 → 全部走精确前缀(每个都以 / 结尾或带参数段, // 且各自的 bypass 先放行 SPA,避免 /message 误伤 /messages、/s 误伤 /settings) const proxy: Record = { '/ws': { target: 'ws://127.0.0.1:8002', ws: true, rewrite: (path: string) => path.replace(/^\/ws/, ''), }, '/media/': { target: BACKEND, changeOrigin: true }, // /api/*:后端真实 API(含 /api/apidirectory/*、/api/shorturl/* 等) '^/api/': { target: BACKEND, changeOrigin: true, bypass: spaBypass }, // /user/*:后端 user.urls 全是 /user//;SPA 已迁到 /user-home*, // 仅放行旧分享链接 /user/<数字id>(LegacyUserHomeRedirect),其余一律代理 '^/user/': { target: BACKEND, changeOrigin: true, bypass: (req: { url?: string }) => { const p = (req.url || '').split('?')[0] if (/^\/user\/\d+\/?$/.test(p)) return req.url // 旧用户主页分享链接 → SPA 重定向 }, }, // /bug/reports/*:后端仅 reports/ 三条;SPA 的 /bug 与 /bug-detail 放行 '^/bug/reports/': { target: BACKEND, changeOrigin: true }, // /article/articles|my-articles|comments/*:后端仅这几段;SPA /post/*、/articles、/article-manage 等放行 '^/article/(articles|my-articles|comments)/': { target: BACKEND, changeOrigin: true }, // /learn/*:后端 learn.urls;SPA 无 /learn/ 冲突(/learn 是列表页、/learn-manage 等已迁出判断) // 但 /learn 本体是 SPA 列表页 → 仅代理带子段的后端路径,根 /learn 放行 '^/learn/(courses|chapters|my-courses|my-progress|materials|favorites|cdn)/': { target: BACKEND, changeOrigin: true }, // /history/records/*:后端仅 records/;SPA /history 是列表页 '^/history/records/': { target: BACKEND, changeOrigin: true }, // /message/*:后端 message.urls;SPA /messages(多 s)天然不命中 '^/message/': { target: BACKEND, changeOrigin: true }, // /chat/*:后端 chat.urls;SPA /chat 本体是页面 → 仅代理后端子段 '^/chat/(friend-requests|friends|users|conversations|messages|upload|favorite-stickers)/': { target: BACKEND, changeOrigin: true }, // /tool/*:后端 tool.urls;SPA /tool-detail 放行(不以 /tool/ 开头,天然安全) '^/tool/': { target: BACKEND, changeOrigin: true }, // /search/*:后端 search.urls(GET /search/ 本体 + suggestions/hot-keywords) // SPA /search 本体是搜索页 → 无查询串的裸 /search 放行到前端 '^/search/': { target: BACKEND, changeOrigin: true, bypass: (req: { url?: string }) => { const raw = req.url || '' const p = raw.split('?')[0] if (p === '/search/' && !raw.includes('?')) return req.url }, }, // /app/*:后端 changelog;SPA 无 /app 冲突 '^/app/': { target: BACKEND, changeOrigin: true }, // /logs/api/*:后端日志;SPA 无冲突 '^/logs/api/': { target: BACKEND, changeOrigin: true }, // /shorturl/*:后端 shorturl.urls;SPA /shorturl-detail 放行 '^/shorturl/': { target: BACKEND, changeOrigin: true }, // /s//:短链跳转(后端 ShortUrlRedirectView);SPA /settings、/shorturl-detail 必须放行 '/s': { target: BACKEND, changeOrigin: true, bypass: (req: { url?: string }) => { const p = (req.url || '').split('?')[0] if (p === '/s' || p.startsWith('/settings') || p.startsWith('/shorturl-detail') || p.startsWith('/src') || p.includes('.')) return req.url if (!/^\/s\/[^/]+\/?$/.test(p)) return req.url // 非短码形态一律放行 }, }, '/i18n': { target: BACKEND, changeOrigin: true, bypass: spaBypass }, } // https://vite.dev/config/ export default defineConfig({ server: { hmr: true, host: '::', allowedHosts: [ '.mymoyu.top', 'localhost', '127.0.0.1', '192.168.1.4' ], proxy, }, plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, })