import path from 'node:path' import { defineConfig, loadEnv } from 'vite' import detectPort from 'detect-port' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createStyleImportPlugin, VxeTableResolve } from 'vite-plugin-style-import' import Unocss from 'unocss/vite' import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import' import { createProjectShellAliases, resolveProjectShellComponentsDir } from './vite.shell-aliases' const DEFAULT_BACKEND_TARGET = 'http://127.0.0.1:8086' /** * Docker 开发会把 VITE_BASE_PATH 设为 host.docker.internal;若宿主机直接 pnpm dev 仍残留 * .env.development.local,代理会连不上并导致登录页「服务器错误」。非 Docker 时自动回退本机地址。 */ function resolveViteBackendTarget(rawTarget: string | undefined): string { const target = (rawTarget || DEFAULT_BACKEND_TARGET).trim() const isDockerDev = process.env.DOCKER_DEV === '1' || process.env.DOCKER_DEV === 'true' if (!isDockerDev && target.includes('host.docker.internal')) { console.warn( `[vite] VITE_BASE_PATH=${target} 仅适用于 Docker 容器内;已回退代理至 ${DEFAULT_BACKEND_TARGET}` ) return DEFAULT_BACKEND_TARGET } return target } export default defineConfig(async ({ mode }) => { const DEFAULT_PORT = 9012 const env = loadEnv(mode, process.cwd(), '') const backendTarget = resolveViteBackendTarget(env.VITE_BASE_PATH) const port = await detectPort(DEFAULT_PORT) const shellComponentsDir = resolveProjectShellComponentsDir() const shellSrc = path.join(process.cwd(), 'node_modules/@fskj-admin/shell/src') const shellSrcPattern = new RegExp(shellSrc.replace(/[\\/]/g, '[\\\\/]')) return { base: '/', define: { 'import.meta.env.VITE_BASE_PATH': JSON.stringify(backendTarget) }, plugins: [ vue(), vueJsx(), Unocss(), AutoImport({ imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], dts: 'src/auto-import.d.ts', // shell 与业务仓 SFC 均依赖 auto-import(ref/computed 等);须覆盖 alias 解析后的 shell 绝对路径 include: [ shellSrcPattern, /[\\/]node_modules[\\/]@fskj-admin[\\/]shell[\\/]/, /[\\/]src[\\/]/, /\.vue$/, /\.vue\?vue/, /\.vue\?vue&type=script/ ], // 默认 exclude 含 /node_modules/,会跳过 npm 内 @fskj-admin/shell,导致 BasicLayout 等 computed 未注入 exclude: [ /[\\/]node_modules[\\/](?!.*@fskj-admin[\\/]shell[\\/])/, /[\\/]\.git[\\/]/ ], viteOptimizeDeps: true }), Components({ dirs: ['src/components', shellComponentsDir], extensions: ['vue'], dts: 'src/components.d.ts', // 与 AutoImport 一致:须处理 npm 内 @fskj-admin/shell 的 SFC,否则 platformops 等页 // 使用的 BasicLeftTree / ProTable 等壳组件无法自动注入(Failed to resolve component) include: [ shellSrcPattern, /[\\/]node_modules[\\/]@fskj-admin[\\/]shell[\\/]/, /[\\/]src[\\/]/, /\.vue$/, /\.vue\?vue/, /\.vue\?vue&type=script/ ], exclude: [ /[\\/]node_modules[\\/](?!.*@fskj-admin[\\/]shell[\\/])/, /[\\/]\.git[\\/]/ ] }), createSvgIconsPlugin({ iconDirs: [path.resolve(process.cwd(), 'src/assets/svg')], symbolId: 'icon-[dir]-[name]' }), createStyleImportPlugin({ resolves: [VxeTableResolve()] }), lazyImport({ resolvers: [ VxeResolver({ libraryName: 'vxe-table' }), VxeResolver({ libraryName: 'vxe-pc-ui' }) ] }) ], css: { preprocessorOptions: { scss: { silenceDeprecations: ['legacy-js-api'] } } }, resolve: { alias: [ { find: '@/components/form/ElImageUploadOne.vue', replacement: path.resolve(process.cwd(), 'src/components/form/ElImageUploadOne.vue') }, { find: '@/components/form/ElImageUploadMore.vue', replacement: path.resolve(process.cwd(), 'src/components/form/ElImageUploadMore.vue') }, { find: '@/components/form/ImageUploadTip.vue', replacement: path.resolve(process.cwd(), 'src/components/form/ImageUploadTip.vue') }, { find: '@/components/form/ElEditor.vue', replacement: path.resolve(process.cwd(), 'src/components/form/ElEditor.vue') }, { find: '@/components/layout/GlobalMainNav.vue', replacement: path.resolve(process.cwd(), 'src/components/layout/GlobalMainNav.vue') }, { find: '@/utils/formatMainNavLabel', replacement: path.resolve(process.cwd(), 'src/utils/formatMainNavLabel.ts') }, // pnpm + shell 源码直引时 dedupe 可能失效,须强制单例 pinia/vue-router { find: 'pinia', replacement: path.resolve(process.cwd(), 'node_modules/pinia/dist/pinia.mjs') }, { find: 'vue-router', replacement: path.resolve(process.cwd(), 'node_modules/vue-router/dist/vue-router.mjs') }, // xe-utils 当前安装版本无 es 构建,改走 dist UMD 构建 { find: 'xe-utils', replacement: path.resolve(process.cwd(), 'node_modules/xe-utils/dist/xe-utils.umd.js') }, // dayjs 主入口为 UMD(dayjs.min.js),element-plus ESM 需 default 导出 { find: /^dayjs\/plugin\/(.+)\.js$/, replacement: path.resolve(process.cwd(), 'node_modules/dayjs/esm/plugin/$1/index.js') }, { find: /^dayjs$/, replacement: path.resolve(process.cwd(), 'node_modules/dayjs/esm/index.js') }, ...createProjectShellAliases() ], // monorepo + pnpm 下避免 pinia/vue 多实例,否则 getActivePinia() 报错 dedupe: ['pinia', 'vue', 'vue-router', '@vue/runtime-core'] }, optimizeDeps: { exclude: [ '@fskj-admin/shell', '@shell/router/appRouterHolder', '@shell/router/createAppRouter', '@shell/router/businessRouterExtension', '@shell/home/homeExtension', '@shell/bootstrap/createAdminApp', '@shell/router/platformOpsRouterExtension', '@shell/platformops/register', '@shell/platformops/essentialRoutes', '@shell/stores/user', '@shell/stores/router', '@shell/stores/menu', '@shell/stores/theme', '@shell/stores/permission', '@/stores/user', '@/stores/permission', '@shell/utils/utils' ], // shell 被 exclude 后其 CJS 依赖须单独预打包;与框架仓对齐,避免 dev 首次点「菜单配置」触发 optimized dependencies changed 整页刷新 entries: [ './index.html', './src/main.ts', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/index.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/subsystemMenu.vue' ], include: [ 'pinia', 'vue', 'vue-router', 'xe-utils', 'nprogress', 'lodash', 'jsencrypt', 'echarts', 'vue-cropper', 'splitpanes', '@element-plus/icons-vue', 'element-plus', 'vxe-table', 'vxe-pc-ui', '@vxe-ui/plugin-render-element' ], needsInterop: ['xe-utils', 'nprogress', 'lodash'] }, server: { host: '0.0.0.0', port, open: '/#/login', // dev 启动时预热应用维护/菜单配置相关 shell 页面,减少首次点击菜单配置时的依赖发现与 reload warmup: { clientFiles: [ './src/main.ts', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/index.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/subsystemMenu.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/components/menuButton.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/components/menuList.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/components/menuForm.vue', './node_modules/@fskj-admin/shell/src/platformops/views/subsystem/components/menuPermission.vue' ] }, proxy: { [env.VITE_BASE_API]: { target: backendTarget, changeOrigin: true // 勿 strip /admin:后端 jeesharp.web.adminPath=/admin,登录等白名单为 /admin/login } } }, build: { chunkSizeWarningLimit: 1500 } } })