Files
FastapiAdmin/frontend/web/vite.config.ts
T
zhangtao 5ff72b086f refactor: 重构项目模块结构与代码细节优化
1.  调整后端模块路由与插件文件结构,迁移部分模块代码至api/v1目录
2.  优化代码注释格式与文档字符串,简化冗余代码
3.  添加监控仪表盘、工单评论等新模块功能
4.  修复前端部分组件交互逻辑与类型定义
5.  清理冗余的初始化数据与废弃配置文件
6.  新增租户注册表单字段与国际化支持
7.  优化HTTP请求拦截器与快捷键功能
2026-07-10 00:18:11 +08:00

322 lines
11 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import autoprefixer from "autoprefixer";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "url";
import vueDevTools from "vite-plugin-vue-devtools";
import viteCompression from "vite-plugin-compression";
import Components from "unplugin-vue-components/vite";
import AutoImport from "unplugin-auto-import/vite";
import ElementPlus from "unplugin-element-plus/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import tailwindcss from "@tailwindcss/vite";
import vitePluginStart from "./build/vitePluginStart";
import { name, version, engines, dependencies, devDependencies } from "./package.json";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const __APP_INFO__ = {
pkg: { name, version, engines, dependencies, devDependencies },
buildTimestamp: Date.now(),
};
/**
* 返回所有 Element Plus 组件的样式入口(style/index + style/css)。
*
* 这些路径路由懒加载组件首次渲染时若未预热,vite optimizer 会重新处理
* → "new dependencies optimized → reloading" 循环,导致菜单切换卡顿 + 整页刷新。
* 提前加入 optimizeDeps.include 可消除此问题(仅影响 dev 启动预编译,不影响生产)。
*
* - style/css:预编译 CSS(兜底)
* - style/indexSCSS 入口。useSource: true 时必须预热,否则懒加载触发 optimizer。
*
* 性能权衡
* --------
* - 当前实现:一次性预热 ~244 个 EP 路径(122 个组件 × 2 入口),dev 启动慢 1-2 分钟
* 但后续路由切换零卡顿。
* - 未来优化:扫描 src/ 中实际用到的 el-xxx 组件名,只预热这部分。
* 实测本项目用到了约 70 个不同 EP 组件,可减少 40%+ 预热开销。
* 需在新增 EP 组件时同步更新扫描结果,否则会再次触发菜单卡顿。
*/
function elementPlusStyleIncludes(): string[] {
// 预构建所有 Element Plus 组件的样式,避免开发时访问新页面触发依赖优化刷新
const componentsDir = path.join(
process.cwd(),
"node_modules",
"element-plus",
"es",
"components"
);
try {
const result: string[] = [];
for (const entry of fs.readdirSync(componentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const styleDir = path.join(componentsDir, entry.name, "style");
// 只包含实际存在 style 目录的组件
if (fs.existsSync(styleDir) && fs.statSync(styleDir).isDirectory()) {
result.push(`element-plus/es/components/${entry.name}/style/index`);
result.push(`element-plus/es/components/${entry.name}/style/css`);
}
}
return result;
} catch {
return [];
}
}
export default ({ mode }: { mode: string }) => {
const root = process.cwd();
const env = loadEnv(mode, root);
const isProduction = mode === "prod";
return defineConfig({
define: {
__APP_VERSION__: JSON.stringify(env.VITE_VERSION),
__APP_NAME__: JSON.stringify(env.VITE_APP_TITLE),
__APP_INFO__: JSON.stringify(__APP_INFO__),
},
base: env.VITE_BASE_URL,
server: {
host: true,
port: Number(env.VITE_PORT),
open: true,
proxy: {
[env.VITE_APP_BASE_API]: {
target: env.VITE_API_BASE_URL,
secure: false,
changeOrigin: true,
},
"/static": {
target: env.VITE_API_BASE_URL,
secure: false,
changeOrigin: true,
},
},
},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"@views": resolvePath("src/views"),
"@views/*": resolvePath("src/views/*"),
"@imgs": resolvePath("src/assets/images"),
"@icons": resolvePath("src/assets/images/svg"),
"@utils": resolvePath("src/utils"),
"@stores": resolvePath("src/store"),
"@plugins": resolvePath("src/plugins"),
"@styles": resolvePath("src/styles"),
"@api": resolvePath("src/api"),
"@fa_imgs": resolvePath("src/assets/fa_imgs"),
"@fa_imgs/*": resolvePath("src/assets/fa_imgs/*"),
},
},
build: {
target: "es2024",
outDir: "dist",
chunkSizeWarningLimit: 4000,
minify: isProduction ? "terser" : false,
terserOptions: isProduction
? {
compress: {
keep_infinity: true,
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log", "console.info"],
},
format: {
comments: true,
},
}
: {},
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
if (id.includes("echarts") || id.includes("zrender")) return "echarts";
if (id.includes("element-plus")) return "element-plus";
if (id.includes("@wangeditor")) return "wangeditor";
if (id.includes("codemirror")) return "codemirror";
if (id.includes("exceljs")) return "exceljs";
if (id.includes("@vue-flow") || id.includes("dagre")) return "vue-flow";
if (id.includes("highlight.js") || id.includes("highlightjs")) return "highlight";
if (id.includes("xgplayer")) return "xgplayer";
if (id.includes("markdown-it")) return "markdown";
if (id.includes("@iconify-json")) return "iconify-icons";
if (id.includes("xlsx")) return "xlsx";
if (id.includes("crypto-js")) return "crypto";
if (id.includes("js-beautify")) return "beautify";
if (id.includes("dayjs")) return "dayjs";
if (
id.includes("vue/") ||
id.includes("vue-router") ||
id.includes("pinia") ||
id.includes("vue-i18n") ||
id.includes("@vueuse")
)
return "vue-vendor";
const module = id
.toString()
.replace(/^.*[/\\]node_modules[/\\]\.pnpm[/\\][^/\\]+[/\\]node_modules[/\\]/, "")
.split("node_modules/")
.pop()
?.split("/")[0];
if (
!module ||
[
"birpc",
"hookable",
"tslib",
"copy-anything",
"danmu.js",
"lodash-unified",
"perfect-debounce",
].includes(module)
)
return;
return module;
},
entryFileNames: "js/[name].[hash].js",
chunkFileNames: "js/[name].[hash].js",
assetFileNames: (assetInfo: any) => {
const info = assetInfo.name.split(".");
let extType = info[info.length - 1];
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = "media";
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
extType = "img";
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = "fonts";
}
return `${extType}/[name].[hash].[ext]`;
},
},
},
dynamicImportVarsOptions: {
warnOnError: true,
exclude: [],
include: ["src/views/**/*.vue"],
},
},
plugins: [
vue(),
vitePluginStart(),
tailwindcss(),
AutoImport({
imports: ["vue", "vue-router", "pinia", "@vueuse/core", "vue-i18n"],
dts: "src/types/import/auto-imports.d.ts",
resolvers: [ElementPlusResolver()],
eslintrc: {
enabled: true,
filepath: "./.auto-import.json",
globalsPropValue: true,
},
vueTemplate: true,
}),
Components({
dirs: ["src/components"],
dts: "src/types/import/components.d.ts",
resolvers: [ElementPlusResolver()],
}),
ElementPlus({
useSource: true,
}),
viteCompression({
verbose: false, // 是否在控制台输出压缩结果
disable: false, // 是否禁用
algorithm: "gzip", // 压缩算法
ext: ".gz", // 压缩后的文件名后缀
threshold: 10240, // 只有大小大于该值的资源会被处理 10240B = 10KB
deleteOriginFile: false, // 压缩后是否删除原文件
}),
/** 仅开发启用:避免生产包体积膨胀与运行期 DevTools 开销 */
...(isProduction ? [] : [vueDevTools()]),
],
optimizeDeps: {
include: [
"@vue-flow/core",
"@vue-flow/background",
"@vue-flow/controls",
"@vue-flow/minimap",
"vue",
"vue-router",
"vue-i18n",
"vue-json-pretty",
"vue-web-terminal",
"vue3-cron-plus",
"vuedraggable",
"vue-draggable-plus",
"element-plus",
"@element-plus/icons-vue",
"element-plus/es",
"element-plus/es/locale/lang/en",
"element-plus/es/locale/lang/zh-cn",
"pinia",
"axios",
"@vueuse/core",
"codemirror",
"codemirror-editor-vue3",
"@wangeditor-next/editor",
"@wangeditor-next/editor-for-vue",
"exceljs",
"echarts/core",
"echarts/renderers",
"echarts/charts",
"echarts/components",
"nprogress",
"qs",
"path-to-regexp",
"path-browserify",
"xgplayer",
"@iconify/vue",
"qrcode.vue",
"xlsx",
"highlight.js",
"dagre",
"dompurify",
"js-beautify",
"markdown-it",
"markdown-it-highlightjs",
"clipboard",
"crypto-js",
"file-saver",
"mitt",
"ohash",
"pinia-plugin-persistedstate",
...elementPlusStyleIncludes(),
],
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
// Element Plus 主题色(@use with 直接改 common/var 模块默认值)。
@use "@styles/element-plus/theme.scss" as *;
// 业务工具 mixin 注入(供 src 内 SCSS 文件直接使用,无需手动引入)。
@use "@styles/core/mixin.scss" as *;
`,
},
},
postcss: {
plugins: [
autoprefixer(),
{
postcssPlugin: "internal:charset-removal",
AtRule: {
charset: (atRule: any) => {
if (atRule.name === "charset") {
atRule.remove();
}
},
},
},
],
},
},
});
};
function resolvePath(paths: string) {
return path.resolve(__dirname, paths);
}