Skip to content

前端工程结构 ​

适用端:Web | 类型:机制

Vue 3.5 + Vite 8 + TypeScript 6 + Element Plus 2.13 + Pinia 3,包管理只用 pnpm。本页讲清目录职责、环境变量与 mode 的关系、alias 的顺序陷阱,以及「自动导入到底覆盖了什么」。

前置阅读:环境准备与启动。

它解决什么问题 ​

CRUD 页面的配对约定是全项目最重要的结构规则:

src/api/<module>/<feature>/index.ts   ↔   src/views/<module>/<feature>/index.vue
                                          src/views/<module>/<feature>/<Feature>Form.vue

例:src/api/system/post/index.ts ↔ src/views/system/post/{index.vue, PostForm.vue}。新增功能照这个形状放文件,别人不用问就能找到。

代码在哪 ​

根目录 ​

文件 / 目录作用
package.jsonname: ainb-ui-admin-vue3,version: 2026.07-snapshot,engines: { node >= 20.19.0, pnpm >= 8.6.0 }
vite.config.ts主构建配置(159 行,alias / server / css / build 全在这里)
build/vite/index.ts插件工厂 createVitePlugins(isBuild, env)
build/vite/optimize.tsoptimizeDeps 的 include / exclude 清单
vitest.config.ts独立的单测配置(不复用 vite.config.ts,避免带上整套构建插件)
.env .env.local .env.dev .env.test .env.stage .env.prod6 个环境文件
types/全局 d.ts:global.d.ts(PageParam / PageResult)、router.d.ts(RouteMeta)、env.d.ts
uno.config.tsUnoCSS 原子类配置
eslint.config.mjs / stylelint.config.js / prettier.config.js代码风格(见下文「风格约定」)
src/lib/★ vendored 的 form-create + form-designer 源码副本

src/ 一级目录 ​

目录职责备注
api/后端接口封装一级:agent ai bpm crm im infra login system
views/页面一级:Error Home IFrame Login Profile Redirect agent ai bpm crm im infra system
components/全局组件,63 个目录 + index.tsunplugin 自动注册,无需 import
hooks/组合式函数hooks/web/ 23 个 + hooks/event/useScrollTo.ts(共 24)
store/modules/Piniauser / permission / dict / app / tagsView / …
router/index.ts + modules/remaining.ts(静态路由 697 行)详见路由与权限机制
config/axios/请求封装(唯一非业务的 config 目录)详见axios 封装与 API 约定
directives/v-hasPermi / v-hasRole / v-mountedFocus
utils/dict / formatTime / dateUtil / tree / auth / download / encrypt / routerHelper / permission / formRules / table / …
layout/布局与菜单渲染
lib/★ vendored form-create / form-designer零 diff,ESLint 已排除
plugins/echarts / elementPlus / formCreate / svgIcon / tongji / unocss / vueI18n / animate.css
locales/zh-CN.ts / en.ts
styles/variables.scss(被自动注入每个 SCSS)、index.scss(含 v-hasPermi 的隐藏样式)
types/手写 d.ts 13 个 + 生成的 auto-imports.d.ts / auto-components.d.ts

域与后端模块的对应 ​

前端目录后端模块说明
views/system(20 个功能目录)enlev-module-system★ 其中 resource / rule / role/grant / permissionDiag 是定制权限体系
views/infra(21 个)enlev-module-infra★ 其中 dataModel / formDesign / dataInterface / portal / billTemplate / build 是低代码定制
views/bpm(10 个)enlev-module-bpmsimple 是 SIMPLE 流程设计器
views/ai(8 个)enlev-module-aimodel/apiKey 等
views/agent(7 个:assistant / config / def / promptTemplate / tooldef / usage / workflow)★ enlev-module-agent全自研
views/crm、views/im模块未启用上游遗留,靠后端菜单过滤不显示

核心机制 ​

环境变量与 mode ​

pnpm dev 的脚本是 vite --mode env.local。这里 env.local 是mode 名,不是文件名——Vite 无论如何都会加载 .env.local 这一档,所以最终生效的是 .env + .env.local。

vite.config.ts 里读 env 的写法也值得知道(开发态从命令行参数取 mode,构建态用 ConfigEnv.mode):

ts
const isBuild = command === 'build'
if (!isBuild) {
    env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
} else {
    env = loadEnv(mode, root)
}

各环境的关键差异:

文件NODE_ENVVITE_BASE_URLVITE_OUT_DIR其它
.env(公共)———VITE_APP_TITLE=启凌智搭、VITE_PORT=80、VITE_APP_TENANT_ENABLE=true、默认登录租户/账号/密码、API 加密开关与 AES 密钥
.env.localdevelopmenthttp://localhost:48080—VITE_API_URL=/admin-api、VITE_APP_CAPTCHA_ENABLE=false、VITE_SOURCEMAP=false
.env.devproductionhttp://api-dashboard.enlev.cndistVITE_SOURCEMAP=true、验证码开
.env.prodproduction''(同源)dist-prodVITE_DROP_DEBUGGER/CONSOLE=true
.env.test——dist-testVITE_BASE_PATH=/admin-ui-vue3/
.env.stage——dist-stageVITE_BASE_PATH 指向静态站

.env.local 是纳入 git 跟踪的

不合常理但确实如此。不要把它加进 .gitignore,也不要把里面的值搬到别处——它是本地联调的唯一事实来源。pnpm dev-server(--mode dev)连的是远端 api-dashboard.enlev.cn,不能用来验证本地后端改动。

alias:顺序敏感的 vendored 转发 ​

@ 指向 src/,其余全是把上游的 @form-create/* npm 包名转发到工程内源码:

ts
// vite.config.ts:75-121(节选,注释为原文)
alias: [
    { find: /\@\//, replacement: `${pathResolve('src')}/` },
    // form-create 运行时已改为工程内源码(src/lib/form-create),不再走 npm 包。
    // ⚠️ 顺序敏感:Rollup 的 alias 对字符串 find 只在 `id === find` 或
    // `id.startsWith(find + '/')` 时命中,所以更长的前缀必须排在前面。
    { find: '@form-create/element-ui/auto-import', replacement: pathResolve('src/lib/form-create/element-ui/auto-import.js') },
    { find: '@form-create/element-ui', replacement: pathResolve('src/lib/form-create/element-ui/src/index.js') },
    { find: '@form-create/utils/lib', replacement: pathResolve('src/lib/form-create/utils/lib') },
    { find: '@form-create/utils', replacement: pathResolve('src/lib/form-create/utils/lib/index.js') },
    // 控件包是 @form-create/component-elm-<name>/src/index 这种带子路径的写法,
    // 字符串 find 在 `component-elm-` 这种半个路径段上匹配不到,只能用正则
    { find: /^@form-create\/component-elm-(.*)/, replacement: `${pathResolve('src/lib/form-create/components/elm-')}$1` },
    // 设计器同样已 vendored(src/lib/form-designer,见其 VENDOR.md)
    { find: '@form-create/designer', replacement: pathResolve('src/lib/form-designer/index.js') }
]

改这块之前先读注释:把 @form-create/utils 排到 @form-create/utils/lib 前面,后者的子路径导入会被前者截胡,报错还很难懂。

自动导入到底覆盖什么 ​

build/vite/index.ts 原文:

ts
AutoImport({
  include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
  imports: [
    'vue', 'vue-router',
    {
      '@/hooks/web/useI18n': ['useI18n'],
      '@/hooks/web/useMessage': ['useMessage'],
      '@/hooks/web/useTable': ['useTable'],
      '@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
      '@/utils/formRules': ['required'],
      '@/utils/dict': ['DICT_TYPE'],
      '@/utils/table': ['indexMethod']
    }
  ],
  dts: !isBuild && 'src/types/auto-imports.d.ts',
  resolvers: [ElementPlusResolver()]
}),
Components({
  dts: !isBuild && 'src/types/auto-components.d.ts',
  resolvers: [ElementPlusResolver()],
  globs: ['src/components/**/**.{vue, md}', '!src/components/DiyEditor/components/mobile/**']
})

翻译成人话:

能直接用、不用 import说明
ref / computed / watch / onMounted / …vue 全部 API
useRoute / useRouter / onBeforeRouteLeave / …vue-router 全部 API
useI18n / useMessage / useTable / useCrudSchemas项目自己的 4 个 hook
DICT_TYPE / required / indexMethod字典枚举、必填规则、序号列方法
ElMessage / ElMessageBox / ElNotification / …Element Plus(走 ElementPlusResolver)
src/components/** 下所有 .vue全局组件;DiyEditor/components/mobile/** 被排除

不在自动导入范围内(必须显式 import):src/utils/dict 里除 DICT_TYPE 之外的函数(getIntDictOptions / getStrDictOptions)、src/api/**、src/store/**、其它 hooks(useTagsView / useCache / useFieldPermission …)、src/views/** 下的局部组件。

dts: !isBuild 意味着类型声明只在 dev 时生成

src/types/auto-imports.d.ts 与 auto-components.d.ts 只有跑 pnpm dev 才会写出来,且被 .gitignore 的 auto-*.d.ts 忽略(src/lib/**/auto-*.d.ts 有例外规则,那是 vendored 的上游文件)。新拉代码或新增全局组件后直接 pnpm ts:check,会报一堆「找不到组件」——先 pnpm dev 起一次再检查。

src/components/index.ts 里的 setupGlobCom 只手工全局注册了 Icon 与 LottieAnimation 两个,其余全靠 unplugin。

构建产物与分包 ​

ts
build: {
    chunkSizeWarningLimit: 2000,
    minify: 'oxc',
    outDir: env.VITE_OUT_DIR || 'dist',
    reportCompressedSize: false,
    // 用独立的 .map 文件,不要内联。
    // 内联时 vite 的 build-import-analysis 会走
    // chunk.code.replace(convertSourceMap.mapFileCommentRegex, ''),
    // 而这个正则在本项目这种体量的 chunk 上会回溯到爆栈:
    // RangeError: Maximum call stack size exceeded,构建直接失败。
    sourcemap: env.VITE_SOURCEMAP === 'true',
    rollupOptions: {
        output: {
            minify: { compress: { dropDebugger: …, dropConsole: … } },
            codeSplitting: {
                groups: [
                    { name: 'echarts', test: /node_modules[\\/]echarts[\\/]/ },
                    // form-create 运行时已 vendored 到 src/lib,不再在 node_modules 下,
                    // 这条 test 必须跟着改,否则它会散进主 chunk。
                    { name: 'form-create', test: /src[\\/]lib[\\/]form-create[\\/]/ },
                    { name: 'form-designer', test: /src[\\/]lib[\\/]form-designer[\\/]/ },
                    { name: 'monaco-editor', test: /node_modules[\\/]monaco-editor[\\/]/ }
                ]
            }
        }
    }
}

四个大依赖各自成 chunk。压缩产物由 VITE_COMPRESS(gzip / brotli,可逗号组合)控制,只在 build 时生效。构建脚本自带 --max_old_space_size=8192,直接 pnpm build:prod 即可。

SCSS 变量注入 ​

每个 SCSS 文件都会被自动前置 @use "…/src/styles/variables.scss" as *;,所以变量无需手工导入。这里有一段专门防 Windows 上重复注入的守卫:

ts
additionalData: (source: string, filename: string) => {
    const normalizedFilename = normalizePath(filename)
    // Windows 下更容易触发重复注入:定义或显式转导变量的文件,不能再次注入同一个
    // `@use ... as *`,否则 Sass 会报 duplicate global variables。
    if (normalizedFilename.endsWith('/src/styles/variables.scss') ||
        normalizedFilename.endsWith('/src/styles/global.module.scss')) {
        return source
    }
    return `@use "${getRelativeScssUsePath(filename, variablesScssPath)}" as *;\n${source}`
},
api: 'modern-compiler'

另有 css.lightningcss.errorRecovery: true,注释原文:「Preserve legacy star-hack declarations by stripping invalid syntax during minification.」——vendored 的老 CSS 里有 *property 这类星号 hack,不开这个会压缩失败。

风格约定 ​

只列与常见默认不同的:

  • Prettier:semi: false、printWidth: 100、singleQuote: true、trailingComma: 'none'、arrowParens: 'always'、vueIndentScriptAndStyle: false;缩进 2 空格;
  • ESLint 关掉了大量 vue/TS 规则(no-explicit-any、no-unused-vars、attributes-order、multi-word-component-names 等),但强制 vue/html-self-closing;src/lib/** 整体排除;
  • Stylelint 强制 properties-order;
  • 提交前跑 pnpm lint(= lint:eslint:check + lint:style:check + lint:format:check),lint-staged.config.mjs 也在。

怎么扩展 ​

新增一个 CRUD 页面:

  1. src/api/<域>/<功能>/index.ts:import request from '@/config/axios' + export interface XxxVO + getXxxPage / getXxx / createXxx / updateXxx / deleteXxx(可选 exportXxx、getSimpleXxxList);
  2. src/views/<域>/<功能>/index.vue:列表页(ContentWrap + Search/查询表单 + el-table + Pagination);
  3. 同目录 XxxForm.vue:新增/编辑弹窗(Dialog + el-form);
  4. 菜单与权限串入库(后端),刷新后由 permissionStore.generateRoutes() 自动生成路由——前端不需要手写路由;
  5. 按钮上加 v-hasPermi="['<域>:<功能>:create']"。

新增一个全局组件:放 src/components/<Name>/(src/components/**/**.vue 会被自动扫描),跑一次 pnpm dev 让 auto-components.d.ts 更新,之后在任意页面直接 <Name />。若组件名与 Element Plus 冲突,ElementPlusResolver 优先,改用别的名字。

新增一个 hook:放 src/hooks/web/;只有希望「免 import 直接用」的才去 build/vite/index.ts 的 AutoImport.imports 里登记(登记后同样要重跑 pnpm dev)。

新增一个环境变量:以 VITE_ 开头写进 .env(公共)或 .env.local(本地),代码里 import.meta.env.VITE_XXX 读取。现有变量集中在 config/axios/config.ts、utils/encrypt.ts、utils/auth.ts、router/index.ts、Login 组件与 plugins/tongji。

坑与注意 ​

  • useTable 与 useCrudSchemas 虽然在自动导入清单里,但 src/views 中零调用点(实测 grep 为空)——它们是上游遗留能力。现有页面走的是「手写 el-table + getList()」的路子,新写页面请跟随现有风格,不要为了用 hook 而重构。
  • 没有 useDict hook:取字典用 src/utils/dict.ts 的 getIntDictOptions / getStrDictOptions / getBoolDictOptions(底层是 dict store),DICT_TYPE 才是自动导入的。
  • src/api/types.ts 不存在:PageParam / PageResult 是 types/global.d.ts 里的全局声明,直接写、不要 import。
  • Vite 8 的分包 API 是 output.codeSplitting.groups,不是老的 manualChunks。抄网上教程时注意。
  • vitest.config.ts 的 environment 是 node,不是 jsdom——现有 58 个 *.spec.ts 都是纯逻辑测试。要测组件行为得先改 environment 并补 DOM 依赖。
  • pnpm test:e2e 是坏的:playwright.config.ts 与 tests/e2e/ 都不存在。
  • ts:check 走增量缓存,缓存脏了用 pnpm ts:check:clean;它带 --max_old_space_size=8192,内存给够。
  • 不要用 npm i / yarn:会重写 lockfile,vendored 的 src/lib 与 pnpm 的构建白名单(pnpm-workspace.yaml 的 allowBuilds)都可能受影响。

→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。

相关页面 ​