一个真正能落地、经过大型项目验证的企业级 Vue3 项目模板。不是那种hello world 级别的脚手架而是从零到生产可用的架构骨架。一、技术栈选型先定武器领域选型理由框架Vue 3.4 (script setup)性能 组合式 API语言TypeScript 5.x严格模式类型即文档构建Vite 5.x快生态成熟路由Vue Router 4.x标配状态Pinia pinia-plugin-persistedstate比 Vuex 更适合组合式HTTPAxios封装层或 fetch 包装拦截器、取消请求UI不绑定具体库Element Plus / Ant Design Vue 均可解耦 UI 框架样式UnoCSS CSS Variables SCSS按需原子化 主题表单自建useFormcomposable 或vee-validate看团队偏好表格自建useTablecomposable配置驱动校验Zod 或vuelidate/validatorsSchema 校验测试Vitest vue/test-utils单元测试代码质量ESLint Prettier simple-git-hooks lint-staged强制约束Monorepo可选pnpm workspace超大型项目拆分二、完整目录结构my-enterprise-app/ ├── .husky/ ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── public/ ├── src/ │ ├── main.ts # 入口只做 app 实例创建 插件注册 │ ├── App.vue # 根组件只放 RouterView / │ ├── env.d.ts │ │ │ ├── assets/ # 纯静态资源 │ │ ├── images/ │ │ └── styles/ │ │ ├── variables.scss # 全局变量 │ │ ├── reset.scss # 重置样式 │ │ └── index.scss # 统一导出 │ │ │ ├── components/ # ✅ 通用基础组件纯 UI零业务 │ │ ├── base/ # 最底层Button、Input、Modal... │ │ │ ├── BaseButton.vue │ │ │ └── BaseInput.vue │ │ ├── feedback/ # 反馈类Toast、Loading、Empty... │ │ ├── layout/ # 布局类Container、Header、Sider... │ │ └── index.ts # 统一 export 自动全局注册 │ │ │ ├── layouts/ # ✅ 页面级布局壳 │ │ ├── DefaultLayout.vue # 侧边栏 头部 内容区 │ │ ├── BlankLayout.vue # 空白登录页用 │ │ └── FullPageLayout.vue # 全屏页面 │ │ │ ├── pages/ # ✅ 页面按业务域划分 │ │ ├── login/ │ │ │ ├── index.vue # 只做模板编排 │ │ │ ├── LoginForm.vue # 页面内子组件 │ │ │ └── composables/ │ │ │ └── useLogin.ts # 登录逻辑 │ │ ├── dashboard/ │ │ │ └── index.vue │ │ ├── system/ │ │ │ ├── users/ │ │ │ │ ├── index.vue │ │ │ │ ├── UserTable.vue │ │ │ │ ├── UserDrawer.vue │ │ │ │ ├── composables/ │ │ │ │ │ ├── useUserList.ts │ │ │ │ │ ├── useUserForm.ts │ │ │ │ │ └── useUserDelete.ts │ │ │ │ └── types.ts │ │ │ ├── roles/ │ │ │ └── permissions/ │ │ └── orders/ │ │ ├── index.vue │ │ ├── OrderTable.vue │ │ └── composables/ │ │ └── useOrderList.ts │ │ │ ├── composables/ # ✅ 跨页面复用的通用逻辑 │ │ ├── state/ │ │ │ ├── useToggle.ts │ │ │ ├── useCounter.ts │ │ │ └── useLoading.ts │ │ ├── ui/ │ │ │ ├── useDialog.ts # 命令式弹窗 │ │ │ ├── useMessage.ts # 全局提示 │ │ │ ├── usePagination.ts # 分页逻辑 │ │ │ └── useTable.ts # 表格通用逻辑 │ │ ├── dom/ │ │ │ ├── useEventListener.ts │ │ │ ├── useClickOutside.ts │ │ │ └── useDebounceFn.ts │ │ └── auth/ │ │ ├── usePermission.ts │ │ └── useAuth.ts │ │ │ ├── stores/ # ✅ Pinia 状态管理 │ │ ├── modules/ │ │ │ ├── app.store.ts # 应用级sidebar collapsed、theme │ │ │ ├── user.store.ts # 用户信息、token │ │ │ ├── permission.store.ts # 权限路由、按钮权限 │ │ │ └── dict.store.ts # 字典数据枚举映射 │ │ └── index.ts # 统一导出 persist 配置 │ │ │ ├── services/ # ✅ API 请求层 │ │ ├── request/ │ │ │ ├── http.ts # axios 实例 拦截器 │ │ │ ├── types.ts # 请求相关类型 │ │ │ ├── errorHandler.ts # 统一错误处理 │ │ │ └── cancelRequest.ts # 请求取消管理 │ │ ├── modules/ │ │ │ ├── user.service.ts │ │ │ ├── order.service.ts │ │ │ └── auth.service.ts │ │ └── index.ts │ │ │ ├── domain/ # ✅ 领域模型 业务规则核心 │ │ ├── user/ │ │ │ ├── types.ts # User 实体定义 │ │ │ ├── rules.ts # 业务规则canEdit、canDelete │ │ │ ├── transformers.ts # DTO ↔ Entity 转换 │ │ │ └── constants.ts # 枚举、状态码 │ │ ├── order/ │ │ │ ├── types.ts │ │ │ ├── rules.ts │ │ │ └── flow.ts # 订单流转规则 │ │ └── shared/ │ │ ├── pagination.ts # 分页通用类型 │ │ └── result.ts # ApiResultT 包装 │ │ │ ├── router/ # ✅ 路由 │ │ ├── index.ts # createRouter │ │ ├── routes/ │ │ │ ├── base.ts # 基础路由登录、404 │ │ │ ├── system.ts # 系统管理模块路由 │ │ │ └── business.ts # 业务模块路由 │ │ ├── guards/ # 导航守卫 │ │ │ ├── auth.guard.ts # 登录校验 │ │ │ ├── permission.guard.ts# 权限校验 │ │ │ └── progress.guard.ts # 进度条 │ │ └── helpers/ │ │ └── routeMatch.ts # 路由匹配工具 │ │ │ ├── directives/ # ✅ 自定义指令 │ │ ├── permission.ts # v-permission │ │ ├── debounce.ts # v-debounce │ │ ├── copy.ts # v-copy │ │ └── index.ts │ │ │ ├── plugins/ # ✅ 第三方插件初始化 │ │ ├── iconify.ts # 图标注册 │ │ ├── echarts.ts # 图表注册 │ │ └── index.ts │ │ │ ├── utils/ # ✅ 纯工具函数 │ │ ├── storage.ts # localStorage/sessionStorage 包装 │ │ ├── format.ts # 日期、金额格式化 │ │ ├── validate.ts # 通用校验 │ │ ├── tree.ts # 树形数据处理 │ │ ├── download.ts # 文件下载 │ │ └── crypto.ts # 加密解密 │ │ │ ├── config/ # ✅ 应用配置编译时确定 │ │ ├── app.ts # 应用名称、版本 │ │ ├── menu.ts # 菜单配置 │ │ └── api.ts # API 路径前缀 │ │ │ ├── constants/ # ✅ 运行时常量 │ │ ├── storageKeys.ts │ │ ├── regexps.ts │ │ └── httpStatus.ts │ │ │ └── types/ # ✅ 全局类型声明 │ ├── api.d.ts │ ├── env.d.ts │ ├── shims-vue.d.ts │ └── business.d.ts │ ├── tests/ # ✅ 测试 │ ├── unit/ │ └── e2e/ │ ├── scripts/ # ✅ 构建/开发脚本 │ ├── generateApi.js # swagger → ts types │ └── generatePage.js # 自动生成页面模板 │ ├── .env.development ├── .env.production ├── .env.staging ├── .eslintrc.cjs ├── .prettierrc.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── package.json └── README.md三、核心模块代码示例1️⃣ 入口文件main.ts—— 只做装配// src/main.ts import { createApp } from vue import App from ./App.vue import { setupRouter } from ./router import { setupStores } from ./stores import { setupDirectives } from ./directives import { setupPlugins } from ./plugins import { setupGlobalComponents } from ./components import ./assets/styles/index.scss function bootstrap() { const app createApp(App) setupStores(app) // Pinia setupRouter(app) // Vue Router setupDirectives(app) // 自定义指令 setupPlugins(app) // 第三方插件 setupGlobalComponents(app) // 全局基础组件 app.mount(#app) } bootstrap()原则入口不写业务逻辑只做接线2️⃣ HTTP 请求层services/request/http.tsimport axios, { type AxiosInstance, type AxiosRequestConfig } from axios import { useUserStore } from /stores/modules/user.store import { showMessage } from /composables/ui/useMessage import { handleBusinessError, handleHttpError } from ./errorHandler const http: AxiosInstance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { Content-Type: application/json } }) // 请求拦截 http.interceptors.request.use((config) { const userStore useUserStore() if (userStore.token) { config.headers!.Authorization Bearer ${userStore.token} } return config }) // 响应拦截 http.interceptors.response.use( (response) { const { code, data, message } response.data // 业务码处理 if (code 200) return data if (code 401) { // token 过期 → 跳转登录 window.location.href /login return Promise.reject(new Error(未登录)) } handleBusinessError(code, message) return Promise.reject(new Error(message)) }, (error) { handleHttpError(error) return Promise.reject(error) } ) export default http3️⃣ Service 层services/modules/user.service.tsimport http from ../request/http import type { User, CreateUserDto, UpdateUserDto } from /domain/user/types import type { PaginatedResult } from /domain/shared/result export const userApi { getList(params: { page: number; pageSize: number; keyword?: string }) { return http.getPaginatedResultUser(/users, { params }) }, getById(id: string) { return http.getUser(/users/${id}) }, create(data: CreateUserDto) { return http.postUser(/users, data) }, update(id: string, data: UpdateUserDto) { return http.putUser(/users/${id}, data) }, delete(id: string) { return http.delete(/users/${id}) }, batchDelete(ids: string[]) { return http.post(/users/batch-delete, { ids }) } }Service 只管怎么调接口不管什么时候调、调完干什么4️⃣ Composable —— 业务逻辑核心useUserList.ts// src/pages/system/users/composables/useUserList.ts import { ref, onMounted } from vue import { userApi } from /services/modules/user.service import { usePagination } from /composables/ui/usePagination import { useLoading } from /composables/state/useLoading import { useMessage } from /composables/ui/useMessage import type { User } from /domain/user/types export function useUserList() { const { page, pageSize, total, resetPage } usePagination() const { loading, withLoading } useLoading() const { success, error } useMessage() const list refUser[]([]) const keyword ref() const fetchList async () { try { const res await withLoading( userApi.getList({ page: page.value, pageSize: pageSize.value, keyword: keyword.value }) ) list.value res.items total.value res.total } catch (e) { error(获取用户列表失败) } } const handleSearch () { resetPage() fetchList() } const handleReset () { keyword.value resetPage() fetchList() } onMounted(fetchList) return { list, loading, page, pageSize, total, keyword, fetchList, handleSearch, handleReset } }5️⃣ 页面组件pages/system/users/index.vue—— 只做编排script setup langts import UserTable from ./UserTable.vue import UserDrawer from ./UserDrawer.vue import { useUserList } from ./composables/useUserList import { useUserDelete } from ./composables/useUserDelete const { list, loading, page, pageSize, total, keyword, handleSearch, handleReset } useUserList() const { deleting, handleDelete, handleBatchDelete } useUserDelete(() fetchList()) // 抽屉状态 const drawerVisible ref(false) const currentUserId refstring() /script template div classuser-page !-- 搜索栏 -- a-card :borderedfalse classmb-4 a-form layoutinline a-form-item label关键词 a-input v-model:valuekeyword placeholder姓名/手机号 allow-clear / /a-form-item a-form-item a-button typeprimary clickhandleSearch搜索/a-button a-button classml-2 clickhandleReset重置/a-button /a-form-item /a-form /a-card !-- 操作栏 表格 -- a-card :borderedfalse template #extra a-button typeprimary clickdrawerVisible true新增用户/a-button a-button danger classml-2 :disabled!selectedIds.length批量删除/a-button /template UserTable :datalist :loadingloading :pagination{ page, pageSize, total } deletehandleDelete edit(id) { currentUserId id; drawerVisible true } / /a-card !-- 新增/编辑抽屉 -- UserDrawer v-model:visibledrawerVisible :user-idcurrentUserId successfetchList / /div /template页面组件里没有一行数据请求代码、没有业务逻辑判断6️⃣ 领域规则domain/user/rules.tsimport type { User } from ./types export function canEditUser(user: User, currentUserId: string): boolean { if (user.id currentUserId) return true if (user.role super_admin) return false return true } export function canDeleteUser(user: User): boolean { return user.status ! active user.role ! super_admin } export function getUserDisplayName(user: User): string { return user.nickname || user.username }7️⃣ usePagination —— 通用复用逻辑// src/composables/ui/usePagination.ts import { ref } from vue export function usePagination(defaultPageSize 20) { const page ref(1) const pageSize ref(defaultPageSize) const total ref(0) const resetPage () { page.value 1 } const onChange (p: number, ps: number) { page.value p pageSize.value ps } return { page, pageSize, total, resetPage, onChange } }8️⃣ 权限指令directives/permission.tsimport type { Directive } from vue import { usePermission } from /composables/auth/usePermission export const permissionDirective: Directive { mounted(el, binding) { const { hasPermission } usePermission() const required binding.value // [user:create, user:update] if (!hasPermission(required)) { el.parentNode?.removeChild(el) } } }模板中使用a-button v-permission[user:create]新增用户/a-button四、数据流全景图┌─────────────────────────────────────────────┐ │ Template │ │ 只消费数据 触发事件 │ └──────────────────┬──────────────────────────┘ │ ┌──────────────────▼──────────────────────────┐ │ Page Component │ │ 编排 composables │ └──────┬───────────┬────────────┬────────────┘ │ │ │ ┌──────▼───┐ ┌────▼────┐ ┌───▼──────────┐ │Composable│ │Composable│ │ Domain Rules │ │(业务逻辑) │ │(UI 逻辑) │ │ (业务规则) │ └──────┬───┘ └────┬────┘ └───┬──────────┘ │ │ │ ┌──────▼───────────▼────────────▼──────────┐ │ Pinia Store │ │ 跨页面共享状态 │ └──────────────────┬───────────────────────┘ │ ┌──────────────────▼───────────────────────┐ │ Services Layer │ │ HTTP 请求封装 │ └──────────────────┬───────────────────────┘ │ ┌──────────────────▼───────────────────────┐ │ HTTP Client (Axios) │ └──────────────────┬───────────────────────┘ │ Backend API五、关键设计决策说明决策为什么这么做Service 不直接返回 response统一在拦截器拆包调用方拿到纯数据Composable 返回函数引用而非执行结果页面控制时机页面组件不await异步用withLoading包裹页面只关心 loading 状态Domain 层不依赖 Vue规则函数可单独测试、可复用到 Node.js指令做权限而非 v-if声明式、无侵入、可统一处理每个页面域有自己的 composables/避免跨页面耦合同时也允许提取到全局 composables/六、团队协作规范必加组件分类约定层级位置能否调 API能否有业务Base 组件components/base/❌❌业务组件pages/xxx/SubComponent.vue通过 props/emit✅页面pages/xxx/index.vue通过 composable编排Git Commit 规范feat(user): 新增用户批量导出功能 fix(order): 修复订单状态流转异常 refactor(domain): 重构用户权限判断逻辑 style(table): 调整表格列宽七、后续演进路线阶段动作初期按上面结构搭建先跑通中期把composables/ui/抽成内部 npm 包后期如果多项目共用拆成 pnpm monorepo成熟期Domain 层独立成包Vue 只是渲染层这就是一个能支撑 50 页面、10 开发者协作、持续迭代 3 年以上不崩盘的 Vue 3 企业级项目模板。如果这篇文章对你有帮助欢迎点赞收藏关注我我会持续分享前端项目框架和gis领域的实战经验