import fs from 'node:fs';
import { resolve } from 'path';
import type { Plugin, UserConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import visualizer from 'rollup-plugin-visualizer';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import svgLoader from 'vite-svg-loader';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import UnoCSS from 'unocss/vite';
import configArcoStyleImportPlugin from './config/plugin/arcoStyleImport';
import configCompressPlugin from './config/plugin/compress';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
// Admin API 目标:由 VITE_OMS_ADMIN_URL 决定(开发走代理,避免浏览器 CORS)
const omsAdminTarget = (
env.VITE_OMS_ADMIN_URL || env.VITE_API_ADMIN_URL || 'http://api.nanafa.com/admin'
).replace(/\/$/, '');
// API 根(itfx/track):优先 VITE_API_ORIGIN,否则从 Admin URL 去掉 /admin
const apiOrigin = (
env.VITE_API_ORIGIN ||
omsAdminTarget.replace(/\/admin\/?$/, '') ||
'http://api.nanafa.com'
).replace(/\/$/, '');
// ideship:PATHBASE=/track → /track/Track/Order/...
// nanafa:网关仍吃 /track/Track/Track/Order/...(前端历史双 Track)
// 下列服务挂在 /track 根下(不是 /Track/xxx 控制器),前端却写成 Track/xxx,需剥掉网关前缀
const trackUseSingleSegment =
String(env.VITE_DEPLOY_TARGET || '').toLowerCase() === 'ideship' ||
/ideship\.com/i.test(apiOrigin);
const TRACK_SPA_SEGMENTS = [
'guide',
'order',
'warningOrder',
'state',
'flag',
'content',
'pull',
'push',
'kpiRegion',
'kpiEarlyWarningConfig',
'query',
];
/** 后端挂在 PATHBASE 根下的服务(非 /Track/Order 这类) */
const TRACK_ROOT_SERVICES = ['Warning', 'ProviderService', 'Dictionary'];
const isTrackSpaPath = (path: string) => {
if (/^\/track\/?$/i.test(path)) return true;
return TRACK_SPA_SEGMENTS.some(
(seg) =>
new RegExp(`^/track/${seg}(/|$)`, 'i').test(path),
);
};
/** /Track/Warning|ProviderService|Dictionary → /track/Warning|... */
const rewriteTrackRootServicePath = (path: string) => {
for (const name of TRACK_ROOT_SERVICES) {
const prefix = `/Track/${name}`;
if (path === prefix || path.startsWith(`${prefix}/`)) {
return `/track${path.slice('/Track'.length)}`;
}
}
return null;
};
const rewriteTrackProxyPath = (path: string) => {
// 小写 /track SPA 不应进代理;若误入则原样返回,避免变成 /track/track/...
if (path === '/track' || path.startsWith('/track/')) {
return path;
}
const rootService = rewriteTrackRootServicePath(path);
if (rootService) return rootService;
if (trackUseSingleSegment) {
if (path.startsWith('/Track/Track/')) {
return `/track/Track/${path.slice('/Track/Track/'.length)}`;
}
if (path.startsWith('/Track/') || path === '/Track') {
return `/track${path}`;
}
}
return `/track${path}`;
};
return {
server: {
open: true,
fs: {
strict: true,
},
// 添加中间件,在代理之前拦截前端路由
middlewareMode: false,
proxy: {
// Admin wwwroot 静态资源:/uploads/... → {admin}/uploads/...
'/uploads': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// 上传静态资源:/api/upload/xxx -> {apiOrigin}/upload/xxx(Admin 多为 /admin/uploads)
// 必须放在通用 /api 规则前,避免被转发到 /itfx/upload 导致 404
'/api/upload': {
target: apiOrigin,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ''),
timeout: 30000,
proxyTimeout: 30000,
},
// Admin API:目标由 VITE_OMS_ADMIN_URL 决定
'/rbac': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// OMS 订单等 admin 接口:/order/list、/order/detail(与 Swagger OMS-Order 一致)
'/order': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const orderSpaPrefixes = [
'/order/guide',
'/order/priceCheck',
'/order/placeOrder',
'/order/myOrders',
'/order/fulfillOrders',
'/order/waybillManage',
'/order/pickupOrder',
'/order/customs9610Order',
'/order/batchJobs',
'/order/templateMapping',
'/order/platformStore',
'/order/platformStoreOrder',
];
if (orderSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
// OMS 仓储 admin 接口:/warehouse/sku/list 等(与 Swagger OMS-Warehouse 一致)
'/warehouse': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const warehouseSpaExact = [
'/warehouse/parcel',
'/warehouse/inventory',
'/warehouse/sku',
'/warehouse/binLocation',
];
if (warehouseSpaExact.some((p) => path === p || path === `${p}/`)) {
return '/index.html';
}
// inboundNotice 页面无更深 API 子路径冲突;入库 API 为 /warehouse/inbound/*
if (path === '/warehouse/inboundNotice' || path === '/warehouse/inboundNotice/') {
return '/index.html';
}
},
},
'/plugin': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
if (path === '/Plugin/AddPackage' || path.startsWith('/Plugin/AddPackage/')) {
return '/index.html';
}
},
},
'/Plugin': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
if (path === '/Plugin/AddPackage' || path.startsWith('/Plugin/AddPackage/')) {
return '/index.html';
}
},
},
'/monitor': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// 知识库:API /kb/admin/*、/kb/portal/*;页面 /kb/category、/kb/article
'/kb': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const kbSpaPages = ['/kb/category', '/kb/article'];
if (kbSpaPages.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
'/customer': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
'/wechat': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
// H5 页面路由与 API 同前缀:刷新 /wechat/register、/wechat/bind 须回退 index.html
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const wechatSpaPages = ['/wechat/register', '/wechat/bind'];
const isSpaPage = wechatSpaPages.some(
(p) => path === p || path.startsWith(`${p}/`),
);
if (isSpaPage) {
return '/index.html';
}
},
},
'/recharge': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// OMS 财务 admin 接口:/finance/receivable-payable/list 等
'/finance': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const financeSpaExact = [
'/finance/rechargeRecord',
'/finance/accountFlow',
'/finance/receivablePayable',
];
if (financeSpaExact.some((p) => path === p || path === `${p}/`)) {
return '/index.html';
}
},
},
'/channel': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
// 页面与 API 同前缀:刷新/直链必须 bypass,否则会打到后端 404
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const channelSpaPrefixes = [
'/channel/salesProducts',
'/channel/serviceChannels',
'/channel/priceArea',
'/channel/blacklist',
'/channel/remoteZones',
'/channel/weightRules',
'/channel/weightRounding',
'/channel/autoTrigger',
'/channel/issueTemplate',
'/channel/labelDesign',
'/channel/guide',
];
if (channelSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
'/file-storage': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// 开发环境面单 PDF 预览:同源代理 COS 签名链接(dev 后端桶可能与 printUrl 不一致)
'/__cos__': {
target: 'https://cos.ideship.com',
changeOrigin: true,
secure: false,
timeout: 30000,
proxyTimeout: 30000,
router: (req) => {
const path = req.url?.split('?')[0] ?? '';
const host = path.replace(/^\/__cos__\//, '').split('/')[0];
return host ? `https://${host}` : 'https://cos.ideship.com';
},
rewrite: (path) => path.replace(/^\/__cos__\/[^/]+/, '') || '/',
},
'/marketing': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const marketingSpaPrefixes = [
'/marketing/memberLevel',
'/marketing/pointsRule',
'/marketing/pointsFlow',
'/marketing/couponTemplate',
'/marketing/couponGrant',
'/marketing/commissionRule',
'/marketing/commissionDetail',
'/marketing/commissionWithdraw',
'/marketing/referralTree',
];
if (marketingSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
'/operation': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
// 与页面同前缀的 API 代理:必须在代理层 bypass,否则刷新会先于/绕过 unshift 中间件仍被转发到后端 404
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const operationSpaPrefixes = [
'/operation/inboundScan',
'/operation/outboundScan',
'/operation/printLabel',
'/operation/parcelArrival',
'/operation/fulfillPack',
];
const isSpaPage = operationSpaPrefixes.some(
(p) => path === p || path.startsWith(`${p}/`),
);
if (isSpaPage) {
return '/index.html';
}
},
},
'/system': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const systemSpaExact = ['/system/dict', '/system/warehouseAddress', '/system/announcement'];
if (systemSpaExact.some((p) => path === p || path === `${p}/`)) {
return '/index.html';
}
const systemSpaPrefixes = [
'/system/users',
'/system/roles',
'/system/menus',
'/system/organizations',
'/system/batchEmail',
'/system/otherConfig',
'/system/aiAssistant',
'/system/aiSkills',
'/system/mcpTools',
'/system/dataPerm',
];
if (systemSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
// 小红书 OMS-Xhs 接口(开发默认走本机 Admin API,与桌面 Agent 一致)
'/xhs': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// MCP 工具 / AI Agent(须与 nginx /mcp/ 反代一致)
'/mcp': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// 兼容旧 Agent 路径 /ai/agent/chat
'/ai': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
},
// 微信公众号服务器回调(兼容 WMS 路径 /api/apiwechat/verification)
'/api/apiwechat': {
target: omsAdminTarget,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 轨迹(track):{apiOrigin}/track/...
// 注意:Windows 下 /Track 可能误匹配小写 SPA `/track/pull`,必须 bypass 到 index.html
'/Track': {
target: apiOrigin,
changeOrigin: true,
secure: false,
bypass(req) {
const path = (req.url || '').split('?')[0] || '';
// 大写 /Track/... 是轨迹 API
if (path.startsWith('/Track')) return undefined;
// 小写 /track/... 是前端页面 —— 必须回 index.html,返回原 path 会找实体文件 404
if (isTrackSpaPath(path)) return '/index.html';
return undefined;
},
rewrite: rewriteTrackProxyPath,
configure: (proxy) => {
proxy.on('error', (err, req, res) => {
console.error('proxy error', err);
});
},
},
// trackApi/*(另一套生成客户端):开发环境同样代理到 /track/*
'/trackApi': {
target: apiOrigin,
changeOrigin: true,
secure: false,
rewrite: (path) => {
const rest = path.replace(/^\/trackApi\/?/, '');
if (trackUseSingleSegment && rest.startsWith('Track/')) {
return `/track/${rest}`;
}
// Warning / ProviderService / Dictionary 已是根服务
if (
rest.startsWith('Warning') ||
rest.startsWith('ProviderService') ||
rest.startsWith('Dictionary')
) {
return `/track/${rest}`;
}
return `/track/${rest}`;
},
},
// API(itfx):{apiOrigin}/itfx
// 使用正则表达式匹配,排除前端路由路径
'^/api/(?!upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface|downstreamInterface/newPort|appList|appList/createApplication|requestLog|callStatistics|apiDebugger|trackApi)': {
target: `${apiOrigin}/itfx`,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ''),
timeout: 30000,
proxyTimeout: 30000,
},
// 智谱 AI(开发环境代理,避免浏览器 CORS)
'/zhipu-api': {
target: 'https://open.bigmodel.cn',
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/zhipu-api/, '/api/paas/v4'),
timeout: 120000,
proxyTimeout: 120000,
},
// 外部域名(用于 ApiTest 调试,避免浏览器 CORS)
// 示例:把 https://lsp.globex.cn/rs/xxx 代理到 /external/rs/xxx
'/external': {
target: 'https://lsp.globex.cn',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/external/, ''),
timeout: 30000,
proxyTimeout: 30000,
},
// api.nanafa.com 整站代理(ApiTest 调试):/nanafa/itfx/xxx → http://api.nanafa.com/itfx/xxx
'/nanafa': {
target: 'http://api.nanafa.com',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/nanafa/, ''),
timeout: 60000,
proxyTimeout: 60000,
},
},
},
plugins: [
{
name: 'filter-known-build-warnings',
configResolved(config) {
const rawWarn = config.logger.warn;
config.logger.warn = (msg, options) => {
if (
msg.includes('[baseline-browser-mapping]') ||
msg.includes('~@/assets/logo.png referenced in ~@/assets/logo.png') ||
msg.includes('new URL("../assets/hi.png", import.meta.url) doesn\'t exist at build time')
) {
return;
}
rawWarn(msg, options);
};
},
},
// 登录首屏不预取业务重型库(即使被错误打进依赖图也不 preload)
{
name: 'omit-heavy-modulepreload',
transformIndexHtml(html) {
return html.replace(
/]*(?:vendor-handsontable|vendor-echarts|vendor-codemirror|vendor-vxe|vendor-element-plus|vendor-mermaid|xlsx-|cytoscape|katex)[^>]*>\s*/g,
'',
);
},
},
// eslint({
// cache: false,
// include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
// exclude: ['node_modules'],
// }),
vue(),
vueJsx(),
svgLoader({ svgoConfig: {} }),
configArcoStyleImportPlugin(),
UnoCSS(),
AutoImport({
imports: ['vue', 'vue-router', 'vue-i18n', 'pinia', '@vueuse/core'],
dts: 'src/auto-imports.d.ts',
dirs: ['src/composables', 'src/store'],
vueTemplate: true,
eslintrc: {
enabled: true,
},
}),
Components({
extensions: ['vue'],
include: [/\.vue$/, /\.vue\?vue/],
dts: 'src/components.d.ts',
dirs: ['src/components'],
resolvers: [],
// directoryAsNamespace: true,
}),
// 自定义插件:在代理之前拦截前端路由
{
name: 'exclude-frontend-routes-from-proxy',
configureServer(server) {
// 仅精确匹配的 SPA 路径(页面 URL 与后端 API 前缀相同,不能用「前缀匹配」,否则 /warehouse/parcel/list 会返回 index.html)
const frontendRoutesExact = [
'/system/dict',
'/system/warehouseAddress',
'/system/announcement',
// 仓储页面路径与 API 同段,必须精确匹配(/warehouse/parcel/list 走代理)
'/warehouse/parcel',
'/warehouse/inventory',
'/warehouse/sku',
'/warehouse/inboundNotice',
'/warehouse/binLocation',
'/Plugin/AddPackage',
];
// 前端路由路径列表(这些路径不应该被代理,应该走前端路由)
// 所有前端路由都使用驼峰命名,以这些前缀开头的路径都是前端路由
const frontendRoutes = [
// API 相关前端路由
'/api/upstreamInterface',
'/api/upstreamInterfaceConfig',
'/api/upstreamInterfaceConfigEdit',
'/api/downstreamInterface',
'/api/downstreamInterface/newPort',
'/api/appList',
'/api/appList/createApplication',
'/api/requestLog',
'/api/callStatistics',
'/api/apiDebugger',
// Channel 相关前端路由
'/channel/salesProducts',
'/channel/serviceChannels',
'/channel/priceArea',
'/channel/blacklist',
'/channel/remoteZones',
'/channel/weightRules',
'/channel/weightRounding',
'/channel/autoTrigger',
'/channel/issueTemplate',
'/channel/labelDesign',
'/channel/guide',
// Marketing 相关前端路由(API 为 kebab-case,如 /marketing/member-level/list)
'/marketing/memberLevel',
'/marketing/pointsRule',
'/marketing/pointsFlow',
'/marketing/couponTemplate',
'/marketing/couponGrant',
'/marketing/commissionRule',
'/marketing/commissionDetail',
'/marketing/commissionWithdraw',
'/marketing/referralTree',
// Order 相关前端路由
'/order/guide',
'/order/priceCheck',
'/order/placeOrder',
'/order/myOrders',
'/order/fulfillOrders',
'/order/waybillManage',
'/order/pickupOrder',
'/order/customs9610Order',
'/order/batchJobs',
'/order/templateMapping',
'/order/platformStore',
'/order/platformStoreOrder',
// Xhs 相关前端路由(勿用 /xhs 前缀,否则 API 会被误判为 SPA)
'/xhs/startService',
'/xhs/analysis',
'/xhs/creation',
'/xhs/publishHistory',
'/xhs/skills',
'/xhs/humanSettings',
'/xhs/agentPair',
'/track/guide',
'/track/order',
'/track/warningOrder',
'/track/state',
'/track/flag',
'/track/content',
'/track/pull',
'/track/push',
'/track/kpiRegion',
'/track/kpiEarlyWarningConfig',
'/track/query',
// Operation 相关前端路由
'/operation/inboundScan',
'/operation/outboundScan',
'/operation/printLabel',
'/operation/parcelArrival',
'/operation/fulfillPack',
// System 相关前端路由
'/system/users',
'/system/roles',
'/system/menus',
'/system/organizations',
'/system/batchEmail',
'/system/aiAssistant',
'/system/aiSkills',
'/system/mcpTools',
'/system/otherConfig',
'/system/dataPerm',
// User 相关前端路由
'/user/recharge',
'/user/marketing/invite',
'/user/marketing/member',
'/user/marketing/coupons',
'/user/marketing/commission',
// Knowledge 前端路由(API 为 /kb/admin/*、/kb/portal/*)
'/kb/category',
'/kb/article',
// Finance 相关前端路由
'/finance/rechargeRecord',
'/finance/accountFlow',
'/finance/receivablePayable',
];
// 直接在 configureServer 中设置中间件,确保在代理之前执行
const officialRoot = resolve(__dirname, 'IDE官网');
const officialMime: Record = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.json': 'application/json; charset=utf-8',
};
const serveOfficialFile = (res: any, next: any, relativePath: string) => {
let decodedPath = relativePath;
try {
decodedPath = decodeURIComponent(relativePath);
} catch {
next();
return;
}
const file = resolve(officialRoot, decodedPath);
if (!file.startsWith(officialRoot) || !fs.existsSync(file)) {
next();
return;
}
const ext = file.slice(file.lastIndexOf('.')).toLowerCase();
res.setHeader('Content-Type', officialMime[ext] || 'application/octet-stream');
fs.createReadStream(file).pipe(res);
};
const middleware = (req: any, res: any, next: any) => {
const url = req.url?.split('?')[0] || '';
if (url === '/' ) {
serveOfficialFile(res, next, 'index.html');
return;
}
if (url === '/9610.html') {
serveOfficialFile(res, next, '9610.html');
return;
}
if (
url.startsWith('/assets/css/') ||
url.startsWith('/assets/js/') ||
url.startsWith('/assets/img/') ||
url.startsWith('/assets/data/') ||
url.startsWith('/assets/vendor/')
) {
serveOfficialFile(res, next, url.slice(1));
return;
}
const matchesSpaExact = frontendRoutesExact.some(
(route) => url === route || url === `${route}/`,
);
// 检查是否是前端路由(前缀匹配,勿把与 API 同前缀的页面放在此列表,应放入 frontendRoutesExact)
const isFrontendRoute =
matchesSpaExact ||
frontendRoutes.some((route) => url === route || url.startsWith(`${route}/`));
if (isFrontendRoute) {
// 标记请求,让代理中间件知道不要代理
(req as any).__skipProxy = true;
(req as any).__isFrontendRoute = true;
// /api/* 与后端同前缀的 SPA 必须显式回退;/track/* 也回退,防止被 /Track 代理误伤
if (url.startsWith('/api/') || url.toLowerCase().startsWith('/track')) {
req.url = '/index.html';
}
next();
return;
}
// 对于 /api/ 路径,检查是否是驼峰命名(前端路由的特征)
if (url.startsWith('/api/')) {
const pathAfterApi = url.substring(5); // 去掉 '/api/'
// 如果路径包含大写字母(驼峰命名),很可能是前端路由
if (/[A-Z]/.test(pathAfterApi)) {
(req as any).__skipProxy = true;
(req as any).__isFrontendRoute = true;
next();
return;
}
}
next();
};
// 使用 unshift 将中间件插入到最前面,确保优先执行
const stack = (server.middlewares as any).stack;
if (Array.isArray(stack)) {
stack.unshift({ route: '', handle: middleware });
} else {
server.middlewares.use(middleware);
}
},
},
// 默认不在构建期做 brotli(会扫几百个 chunk,拖慢 CI/本地 build)。
// Nginx/CDN 在线压缩即可。需要预生成 .br 时:VITE_BUILD_COMPRESS=brotli pnpm build
...(process.env.VITE_BUILD_COMPRESS === 'brotli'
? ((): Plugin[] => {
const p = configCompressPlugin('brotli');
return Array.isArray(p) ? p : [p];
})()
: []),
...(process.env.REPORT === 'true'
? [
visualizer({
filename: './node_modules/.cache/visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
]
: []),
],
resolve: {
alias: [
// 统一走 lodash-es,避免与 lodash CJS 整包重复打入
{ find: 'lodash', replacement: 'lodash-es' },
{
find: '@',
replacement: resolve(__dirname, './src'),
},
{
find: 'assets',
replacement: resolve(__dirname, './src/assets'),
},
{
find: 'vue-i18n',
replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
},
{
find: 'vue',
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
},
{
find: 'xe-utils',
replacement: resolve(
__dirname,
'./node_modules/.pnpm/xe-utils@3.7.5/node_modules/xe-utils'
), // Add xe-utils alias
},
],
extensions: ['.ts', '.js'],
},
define: {
'process.env': {},
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
},
optimizeDeps: {
// 预构建常用依赖,加速开发体验
include: [
'vue',
'vue-router',
'pinia',
'@arco-design/web-vue',
'axios',
'dayjs',
'lodash-es',
],
// 确保 Vue 相关依赖的预构建顺序
esbuildOptions: {
target: 'es2017',
// 启用 tree-shaking
treeShaking: true,
},
},
build: {
// 启用压缩(使用esbuild,Vite默认,性能更好)
minify: 'esbuild',
// 生产环境移除 console 和 debugger
esbuild: {
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
},
// 启用CSS代码分割
cssCodeSplit: true,
// 生成 source map(默认关闭,避免线上包体积变大;需要排查问题时可临时打开)
sourcemap: false,
// 构建输出配置:使用 Vite 默认的代码分割策略,避免过度自定义导致库初始化顺序问题
rollupOptions: {
onwarn(warning, warn) {
const message = warning.message || '';
if (
message.includes('~@/assets/logo.png referenced in ~@/assets/logo.png') ||
message.includes('new URL("../assets/hi.png", import.meta.url) doesn\'t exist at build time') ||
message.includes('Use of eval in "node_modules/vg-print/dist/vg-print.es.js"') ||
message.includes('dynamically imported by') && message.includes('but also statically imported by')
) {
return;
}
warn(warning);
},
output: {
manualChunks(id) {
if (!id.includes('node_modules')) return;
if (id.includes('handsontable') || id.includes('@handsontable')) {
return 'vendor-handsontable';
}
if (id.includes('codemirror') || id.includes('@codemirror')) {
return 'vendor-codemirror';
}
if (id.includes('element-plus')) {
return 'vendor-element-plus';
}
if (id.includes('@arco-design')) {
return 'vendor-arco';
}
if (id.includes('vxe-table') || id.includes('xe-utils')) {
return 'vendor-vxe';
}
if (id.includes('echarts')) {
return 'vendor-echarts';
}
// mermaid 仅 MarkdownViewer 动态加载;单独分包避免污染主图
if (
id.includes('node_modules/mermaid') ||
id.includes('node_modules/cytoscape') ||
id.includes('node_modules/katex') ||
id.includes('node_modules/dagre') ||
id.includes('node_modules/khroma')
) {
return 'vendor-mermaid';
}
if (id.includes('lodash')) {
return 'vendor-lodash';
}
},
// 确保文件名包含 hash,便于浏览器缓存
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const name = assetInfo.name ?? 'asset';
const info = name.split('.');
const ext = info[info.length - 1] ?? 'bin';
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(name)) {
return `assets/media/[name]-[hash].${ext}`;
}
if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/.test(name)) {
return `assets/img/[name]-[hash].${ext}`;
}
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(name)) {
return `assets/fonts/[name]-[hash].${ext}`;
}
return `assets/[name]-[hash].${ext}`;
},
},
},
// 设置chunk大小警告限制(提高警告阈值,因为我们已经做了代码分割)
chunkSizeWarningLimit: 2000,
// 启用 terser 压缩以获得更好的压缩率(可选,esbuild更快但压缩率稍低)
// minify: 'terser', // 如果需要更好的压缩率,可以启用,但构建会慢一些
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
hack: `true; @import (reference) "${resolve(
'./src/assets/style/breakpoint.less'
)}";`,
},
javascriptEnabled: true,
},
},
},
} as UserConfig;
});