Skip to main content

前端项目专用开发规范

适用框架:admin-plus(基于 vue-admin-beautiful)v13.3.0
技术栈:Vue 3 + TypeScript + Vite 5 + Pinia + Vue Router 4 + Element Plus 2.x
最后更新:2026-03-06


目录

  1. 项目初始化清单
  2. 目录结构
  3. 路径别名速查表
  4. 新增页面开发流程
  5. API 函数规范
  6. HTTP 请求规范
  7. 组件开发规范
  8. 状态管理规范(Pinia)
  9. 路由规范
  10. 认证流程规范
  11. i18n 规范
  12. 样式规范
  13. TypeScript 规范
  14. 测试规范
  15. 构建 & 环境规范
  16. .gitignore 必含项

1. 项目初始化清单

包管理器

必须使用 pnpm,禁止使用 npm 或 yarn。

# 安装依赖
pnpm install

# 新增依赖
pnpm add <package>

# 新增开发依赖
pnpm add -D <package>

项目根目录已有 pnpm-lock.yaml,不要提交 package-lock.jsonyarn.lock

Node 版本要求

Node.js ≥ 20,推荐使用 LTS 版本。

node -v  # 确认 >= 20.x.x

建议在项目根目录添加 .nvmrc.node-version 文件固定版本:

20

从框架仓库 clone 后必须做的改动

从 admin-plus 框架 clone 新项目后,按以下清单逐项修改:

1. package.json

{
"name": "<your-project-name>",
"version": "0.1.0",
"description": "<项目描述>"
}

2. vite.config.ts

修改 banner 注释(build.rollupOptions 或插件配置中):

// 修改为项目名称
const banner = `/* <your-project-name> v${pkg.version} */`

3. 清理框架 demo 页面

删除或替换 src/views/ 下的框架演示路由和页面,只保留必要的基础页面(login、404、403 等)。

4. 修改 src/config/index.ts

export default {
// Token 在 localStorage 中的 key 名,项目间建议保持唯一
tokenTableName: '<project>-token',
// 请求超时(毫秒)
requestTimeout: 30000,
// 后端约定的成功码
successCode: [200, 0],
// ...
}

5. 配置环境变量文件

在项目根目录创建以下文件(不提交到 git,.env.*.local 用于本地覆盖):

.env.development

# 本地开发 API 地址
VITE_APP_BASE_API=http://localhost:8000

# 应用 ID(用于多租户/埋点区分)
VITE_APP_ID=your-app-dev

# 应用标题(显示在浏览器 tab)
VITE_APP_TITLE=MyApp (Dev)

# Google OAuth Client ID
VITE_GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com

# 开发端口(与 vite.config.ts 中 server.port 保持一致)
VITE_PORT=15000

.env.production

# 生产 API 地址
VITE_APP_BASE_API=https://api.example.com

# 应用 ID
VITE_APP_ID=your-app-prod

# 应用标题
VITE_APP_TITLE=MyApp

# Google OAuth Client ID(生产环境)
VITE_GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com

⚠️ .env.development.local / .env.production.local 可用于个人本地覆盖,已在 .gitignore 中排除,不要提交。


2. 目录结构

{project}/
├── src/
│ ├── api/ # API 函数(每个文件对应一个业务域)
│ │ ├── user.ts # 用户相关 API
│ │ └── {domain}.ts # 其他业务域 API
│ ├── assets/ # 静态资源(图片、音频等,经 Vite 处理)
│ ├── components/ # 全局共享组件(自动引入,无需 import)
│ │ └── MyButton/
│ │ └── index.vue
│ ├── config/ # 应用配置
│ │ └── index.ts # tokenTableName / requestTimeout / successCode 等
│ ├── constants/ # 常量定义
│ ├── directives/ # Vue 自定义指令
│ ├── enum/ # 枚举
│ │ └── ApiUrls.ts # 所有 API 路径统一在此定义
│ ├── hooks/ # Composition API hooks
│ ├── i18n/
│ │ ├── index.ts # vue-i18n 初始化
│ │ └── locales/
│ │ ├── zh.ts # 中文语言包
│ │ └── en.ts # 英文语言包
│ ├── layouts/ # 页面布局(引用 library/layouts/)
│ ├── modules/ # 子包(按需懒加载)
│ ├── plugins/ # 本地插件(项目级,非 library)
│ ├── router/
│ │ ├── index.ts # 路由定义(静态路由 + 动态路由挂载)
│ │ └── permissions.ts # 路由守卫(beforeEach,内置 token 检查)
│ ├── store/
│ │ └── modules/
│ │ ├── user.ts # 认证状态(token / username / uid)
│ │ ├── acl.ts # 权限(roles / permissions)
│ │ ├── routes.ts # 动态路由状态
│ │ ├── settings.ts # 应用全局设置
│ │ └── tabs.ts # Tab 导航状态
│ ├── styles/ # 全局样式
│ │ └── element-plus/
│ │ └── index.scss # Element Plus 主题变量覆盖
│ ├── types/ # TypeScript 类型定义(项目级)
│ ├── utils/
│ │ ├── Request.ts # Axios 封装(主 HTTP 客户端,默认导出 instance)
│ │ ├── token.ts # Token 存取工具(get/set/remove)
│ │ └── ... # 其他工具函数
│ └── views/ # 页面组件(按功能模块分子目录)
│ ├── login/
│ │ └── index.vue
│ ├── error/
│ │ ├── 404.vue
│ │ └── 403.vue
│ └── {module}/
│ └── index.vue # 新业务模块页面
├── library/ # Vab 框架层(一般不修改)
│ ├── components/ # Vab 框架组件(VabMenu / VabHeader 等,自动引入)
│ ├── layouts/ # Vab 布局实现
│ ├── plugins/
│ │ └── vab/ # @gp 别名指向此目录(全局插件)
│ ├── styles/
│ │ └── variables/
│ │ └── variables.module.scss # 全局 SCSS 变量(已自动注入所有 .scss)
│ └── types/ # Vab 内部类型声明
├── types/ # 全局 TypeScript 类型声明(/# 别名指向此目录)
├── public/ # 静态资源(不经过 Vite 处理,原样输出到 dist/)
│ └── favicon.ico
├── tests/
│ └── unit/ # 单元测试(Vitest)
│ └── {module}.spec.ts
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .gitignore
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
└── vite.config.ts

3. 路径别名速查表

别名实际路径用途示例
@src/import { useUserStore } from '@/store/modules/user'
~项目根目录import pkg from '~/package.json'
/#types/import type { UserInfo } from '/#/user'
@vablibrary/import VabIcon from '@vab/components/VabIcon/index.vue'
@gplibrary/plugins/vabimport { setupVab } from '@gp'(全局插件)

⚠️ 路径别名已在 vite.config.tstsconfig.json 中同步配置,新增别名两处都要更新。


4. 新增页面开发流程

Step 1:创建页面组件

src/views/{模块}/ 下创建 index.vue

<template>
<div class="user-list">
<el-table :data="tableData" v-loading="loading">
<el-table-column prop="name" :label="t('user.list.name')" />
</el-table>
</div>
</template>

<script setup lang="ts">
import type { UserItem } from '@/types/user'
import { getUserList } from '@/api/user'

const { t } = useI18n()

const loading = ref(false)
const tableData = ref<UserItem[]>([])

const fetchData = async () => {
loading.value = true
try {
const res = await getUserList()
tableData.value = res.data
} finally {
loading.value = false
}
}

onMounted(fetchData)
</script>

<style scoped lang="scss">
.user-list {
padding: 16px;
}
</style>

注意:refonMounteduseI18n 等均已自动引入,无需手动 import

Step 2:定义 API 函数

src/api/{模块}.ts 下定义 API 函数(详见第 5 节):

// src/api/user.ts
import instance from '@/utils/Request'
import { ApiUrls } from '@/enum/ApiUrls'
import type { UserListResponse } from '@/types/user'

export const getUserList = () =>
instance.get<UserListResponse>(ApiUrls.USER_LIST)

Step 3:添加路由

src/router/index.ts 中添加路由配置:

{
path: '/user',
component: Layout,
meta: { title: 'user.menu.title', icon: 'user' },
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/views/user/index.vue'),
meta: { title: 'user.list.title', icon: 'list', roles: ['admin'] },
},
],
}

Step 4:新建 Pinia Store(如需全局状态)

src/store/modules/{模块}.ts 新建 store(详见第 8 节):

// src/store/modules/user-mgmt.ts
import { defineStore } from 'pinia'

export const useUserMgmtStore = defineStore('userMgmt', {
state: () => ({
selectedUserId: null as number | null,
}),
actions: {
setSelectedUser(id: number) {
this.selectedUserId = id
},
},
})

Step 5:添加 i18n 文案(如需国际化)

src/i18n/locales/zh.tsen.ts 中添加对应 key:

// zh.ts
export default {
user: {
list: {
title: '用户列表',
name: '用户名',
},
},
}

// en.ts
export default {
user: {
list: {
title: 'User List',
name: 'Username',
},
},
}

Step 6:编写单元测试

tests/unit/ 下创建对应测试文件:

// tests/unit/user.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserList from '@/views/user/index.vue'
import * as userApi from '@/api/user'

describe('UserList', () => {
it('renders correctly', async () => {
vi.spyOn(userApi, 'getUserList').mockResolvedValue({
data: [{ id: 1, name: 'Tom' }],
})
const wrapper = mount(UserList)
expect(wrapper.exists()).toBe(true)
})
})

Step 7:本地验证并提交 MR

# 本地开发验证
pnpm dev

# 运行单元测试
pnpm test:unit

# Lint 检查
pnpm lint

# 确认无误后提交
git add .
git commit -m "feat(user): add user list page"
git push origin feature/user-list

# 在 GitLab/GitHub 创建 MR/PR,等待 Code Review

5. API 函数规范

文件命名

src/api/{业务域}.ts

每个文件对应一个业务域,例如:

  • src/api/user.ts — 用户相关
  • src/api/order.ts — 订单相关
  • src/api/product.ts — 商品相关

函数命名

使用动词前缀,语义清晰:

操作前缀示例
查询列表getgetUserList
查询详情getgetUserById
创建createcreateUser
更新updateupdateUser
删除deletedeleteUser
批量操作batchbatchDeleteUsers

API 路径管理

所有 API 路径统一在 src/enum/ApiUrls.ts 中定义,禁止在 API 函数中硬编码路径字符串:

// src/enum/ApiUrls.ts
export enum ApiUrls {
// 用户模块
USER_LIST = '/api/v1/users',
USER_DETAIL = '/api/v1/users/:id',
USER_CREATE = '/api/v1/users',
USER_UPDATE = '/api/v1/users/:id',
USER_DELETE = '/api/v1/users/:id',

// 认证模块
AUTH_LOGIN = '/api/v1/auth/login',
AUTH_LOGOUT = '/api/v1/auth/logout',
AUTH_GOOGLE_CALLBACK = '/api/v1/auth/google/callback',
}

响应类型定义

每个 API 函数的响应必须有 TypeScript 类型定义:

// src/types/user.ts
export interface UserItem {
id: number
username: string
email: string
roles: string[]
createdAt: string
}

export interface UserListResponse {
code: number
message: string
data: UserItem[]
total: number
}

export interface CreateUserParams {
username: string
email: string
password: string
roles?: string[]
}

完整示例

// src/api/user.ts
import instance from '@/utils/Request'
import { ApiUrls } from '@/enum/ApiUrls'
import type {
UserItem,
UserListResponse,
CreateUserParams,
} from '@/types/user'

/** 获取用户列表 */
export const getUserList = (params?: { page?: number; size?: number }) =>
instance.get<UserListResponse>(ApiUrls.USER_LIST, { params })

/** 获取用户详情 */
export const getUserById = (id: number) =>
instance.get<{ data: UserItem }>(`${ApiUrls.USER_LIST}/${id}`)

/** 创建用户 */
export const createUser = (data: CreateUserParams) =>
instance.post<{ data: UserItem }>(ApiUrls.USER_CREATE, data)

/** 更新用户 */
export const updateUser = (id: number, data: Partial<CreateUserParams>) =>
instance.put<{ data: UserItem }>(`${ApiUrls.USER_LIST}/${id}`, data)

/** 删除用户 */
export const deleteUser = (id: number) =>
instance.delete<void>(`${ApiUrls.USER_LIST}/${id}`)

6. HTTP 请求规范

使用方式

始终使用 src/utils/Request.ts 的默认导出 instance,禁止在业务代码中直接 import axios

// ✅ 正确
import instance from '@/utils/Request'

// ❌ 错误
import axios from 'axios'

自动附加 Token

请求拦截器已配置自动从 localStorage 读取 token 并附加 Authorization: Bearer {token} 头,业务代码无需手动处理。

统一响应格式

后端所有接口必须遵循以下格式:

{
"code": 0,
"message": "ok",
"data": { ... }
}

错误码处理规则

响应拦截器已统一处理以下场景:

场景处理行为
code === 0正常,返回 data
code === -1业务错误,弹出 message 提示,抛出错误
HTTP 401token 过期/无效,清除本地 token,跳转 /login
HTTP 403无权限,跳转 /403
网络错误 / 超时弹出通用错误提示

业务代码只需 try/catch 处理预期的业务异常,通用错误框架已处理。

请求取消

框架支持 AbortController 取消请求,适用于搜索防抖等场景:

const controller = new AbortController()
instance.get('/api/search', {
params: { q: keyword },
signal: controller.signal,
})

// 取消请求
controller.abort()

7. 组件开发规范

基本写法

所有组件统一使用 <script setup lang="ts"> 语法:

<template>
<div class="my-component">
<slot />
</div>
</template>

<script setup lang="ts">
// Props 必须有 TypeScript 类型定义
interface Props {
title: string
count?: number
variant?: 'primary' | 'secondary'
}

const props = withDefaults(defineProps<Props>(), {
count: 0,
variant: 'primary',
})

const emit = defineEmits<{
change: [value: number]
close: []
}>()
</script>

<style scoped lang="scss">
.my-component {
// 样式
}
</style>

命名规范

  • 组件文件名:使用 PascalCase,如 UserCard.vueSearchInput.vue
  • 组件目录:建议使用 index.vue 作为主文件(如 UserCard/index.vue
  • 组件名defineOptions):与文件名保持一致
// 如需显式声明组件名(用于 DevTools 调试)
defineOptions({ name: 'UserCard' })

自动引入说明

以下内容无需手动 import,已通过 unplugin-auto-importunplugin-vue-components 自动引入:

类型包含内容
Vue APIrefcomputedwatchonMounteddefineProps 等全部 Composition API
Vue RouteruseRouteruseRoute
PiniadefineStorestoreToRefs
VueUse@vueuse/core 全部函数
Element Plus所有 El* 组件
src/components/项目自定义全局组件
library/components/Vab 框架组件(VabMenuVabHeader 等)

组件存放位置

类型存放位置是否自动引入
全局共享组件src/components/✅ 是
页面内局部组件src/views/{模块}/components/❌ 否(需手动 import)
Vab 框架组件library/components/✅ 是

8. 状态管理规范(Pinia)

Store 文件规范

每个业务模块一个 store 文件,存放于 src/store/modules/

// src/store/modules/product.ts
import { defineStore } from 'pinia'
import type { ProductItem } from '@/types/product'
import { getProductList } from '@/api/product'

export const useProductStore = defineStore('product', {
state: () => ({
list: [] as ProductItem[],
loading: false,
total: 0,
currentPage: 1,
}),

getters: {
isEmpty: (state) => state.list.length === 0,
},

actions: {
async fetchList(params?: { page?: number }) {
this.loading = true
try {
const res = await getProductList(params)
this.list = res.data.items
this.total = res.data.total
} finally {
this.loading = false
}
},

reset() {
this.$reset()
},
},
})

命名规范

  • Store ID:camelCase,与模块名一致(如 'userMgmt'
  • Store Hook:use{Module}Store(如 useProductStore

使用规范

// 在组件中使用
const productStore = useProductStore()

// 解构响应式状态(必须用 storeToRefs)
const { list, loading } = storeToRefs(productStore)

// 调用 action(直接调用,不用 storeToRefs)
productStore.fetchList({ page: 1 })

禁止事项

  • ❌ 禁止跨 store 直接修改另一个 store 的 state
  • ❌ 禁止在 state 外直接修改属性(应通过 action
  • ✅ 跨 store 通信:在 action 内引入并调用其他 store 的 action

9. 路由规范

路由配置格式

// src/router/index.ts
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: Layout, // 使用 Vab 布局组件
redirect: '/dashboard/index',
meta: {
title: 'dashboard.title', // i18n key
icon: 'grid', // 菜单图标
},
children: [
{
path: 'index',
name: 'Dashboard', // name 必须唯一,使用 PascalCase
component: () => import('@/views/dashboard/index.vue'), // 懒加载
meta: {
title: 'dashboard.index.title',
icon: 'home',
roles: ['admin', 'editor'], // 有权限访问的角色列表,不填则所有人可见
noClosable: true, // 是否禁止关闭 Tab(首页建议设 true)
},
},
],
},
]

meta 字段说明

字段类型说明
titlestringi18n key,显示在菜单和面包屑
iconstring菜单图标名称
rolesstring[]可访问角色列表,不设置则所有已登录用户可见
noClosableboolean禁止关闭该 Tab(默认 false)
hiddenboolean在菜单中隐藏(默认 false)
keepAliveboolean是否开启 keep-alive 缓存(默认 false)

路由懒加载

所有业务页面必须使用懒加载:

// ✅ 正确
component: () => import('@/views/user/index.vue')

// ❌ 错误(静态 import 会打包进主 chunk)
import UserList from '@/views/user/index.vue'
component: UserList

已内置路由

以下路由已在框架中处理,不要重复定义:

  • /login — 登录页
  • /404 — 404 页面
  • /403 — 无权限页面
  • /:pathMatch(.*) — 通配符,自动跳转 404

10. 认证流程规范

整体流程

用户访问页面

路由守卫 (src/router/permissions.ts) beforeEach

检查 localStorage token
├── 无 token → 跳转 /login
└── 有 token → 检查用户信息
├── 无用户信息 → 调用 userStore.getUserInfo()
└── 有用户信息 → 放行

Google OAuth 登录流程

前端跳转 Google 授权页

Google 回调带 code 参数返回前端 /auth/callback

前端调用后端 API:POST /api/v1/auth/google/callback { code }

后端验证 code,返回 JWT token

前端存储 token:
localStorage.setItem(tokenTableName, token)

调用 userStore.getUserInfo() 获取用户信息

跳转首页 /dashboard

Token 操作

使用框架提供的工具函数,不要直接操作 localStorage

import { getToken, setToken, removeToken } from '@/utils/token'

// 存储
setToken(token)

// 读取
const token = getToken()

// 清除
removeToken()

tokenTableName 的值在 src/config/index.ts 中配置,工具函数会自动使用。

登出

import { useUserStore } from '@/store/modules/user'

const userStore = useUserStore()

// 完整登出:清除 token + 重置所有 store + 跳转登录页
await userStore.logout()

路由守卫

src/router/permissions.ts 已实现完整的守卫逻辑,新项目无需修改,只需确保:

  1. useUserStore 中的 token getter 正确返回当前 token
  2. getUserInfo action 在 token 有效时能正确获取并填充用户信息

11. i18n 规范

基本使用

<template>
<!-- 模板中使用 $t -->
<h1>{{ $t('user.login.title') }}</h1>
<el-button>{{ $t('common.btn.submit') }}</el-button>
</template>

<script setup lang="ts">
// script 中使用 useI18n(已自动引入)
const { t } = useI18n()

const message = t('user.login.welcomeBack', { name: username })
</script>

Key 命名规范

格式:{模块}.{功能}.{描述}

// 正确示例
'user.login.title' // 用户模块 > 登录功能 > 标题
'user.list.searchPlaceholder' // 用户模块 > 列表功能 > 搜索框占位
'common.btn.submit' // 公共 > 按钮 > 提交
'common.btn.cancel' // 公共 > 按钮 > 取消
'common.msg.success' // 公共 > 消息 > 成功
'order.detail.totalAmount' // 订单模块 > 详情功能 > 总金额

// 错误示例
'submitBtn' // 没有模块层级
'用户列表' // 不能用中文作为 key
'USER_LIST_TITLE' // 不要用大写

语言包结构

// src/i18n/locales/zh.ts
export default {
common: {
btn: {
submit: '提交',
cancel: '取消',
confirm: '确认',
delete: '删除',
edit: '编辑',
search: '搜索',
reset: '重置',
},
msg: {
success: '操作成功',
failed: '操作失败',
deleteConfirm: '确定要删除吗?',
},
},
user: {
login: {
title: '登录',
welcomeBack: '欢迎回来,{name}',
},
list: {
title: '用户列表',
name: '用户名',
email: '邮箱',
},
},
}

强制要求

  • 所有面向用户的文字必须走 i18n,禁止硬编码中文或英文
  • 新增 key 时,zh.tsen.ts 必须同步添加
  • 不要在同一个 key 下同时出现文字和子对象(避免 vue-i18n 报错)

12. 样式规范

技术选型

  • 预处理器:SCSS(已配置,直接使用)
  • 组件内样式:<style scoped lang="scss">
  • 全局样式:src/styles/

全局 SCSS 变量

@vab/styles/variables/variables.module.scss 中的变量已通过 Vite css.preprocessorOptions.additionalData 自动注入到所有 .scss 文件,无需 @import

/* ✅ 直接使用,无需 import */
.my-component {
color: $--vab-color-primary;
font-size: $--vab-font-size-base;
}

/* ❌ 错误,不要手动 import 变量文件 */
@import '@vab/styles/variables/variables.module.scss';

Element Plus 主题定制

src/styles/element-plus/index.scss 中覆盖 CSS 变量:

:root {
--el-color-primary: #1890ff;
--el-border-radius-base: 4px;
}

组件样式规范

<style scoped lang="scss">
/* 1. 组件根元素使用语义化类名(与组件名对应) */
.user-card {
/* 2. 布局属性 */
display: flex;
flex-direction: column;
gap: 12px;

/* 3. 使用全局 SCSS 变量 */
padding: $--vab-padding-base;
border-radius: $--vab-border-radius-base;

/* 4. 嵌套选择器 */
&__header {
font-size: 16px;
font-weight: 600;
}

&__body {
flex: 1;
}
}
</style>

禁止事项

  • ❌ 禁止内联样式(style="color: red"),除非是动态绑定(:style="{ color: computedColor }"
  • ❌ 禁止在 scoped 样式中使用全局选择器(如 .el-table 直接修改),如需穿透用 :deep()
  • ✅ 穿透第三方组件样式:使用 :deep(.el-table__header)

13. TypeScript 规范

当前配置说明

tsconfig.jsonstrict 模式当前为 false(框架历史原因),但新增代码必须按 strict 标准编写

  • 显式标注所有变量、参数、返回值类型
  • 不使用 any(除非框架层绝对必要)
  • 不使用隐式 anynoImplicitAny 等效要求)

类型定义存放

src/types/             # 业务类型定义
user.ts # 用户相关类型
order.ts # 订单相关类型
common.ts # 通用类型(分页、响应包装等)

types/ # 全局声明文件(/@ 别名)
global.d.ts # 全局类型扩展(Window、环境变量等)
shims.d.ts # 模块声明

通用类型示例

// src/types/common.ts

/** 标准分页参数 */
export interface PaginationParams {
page: number
size: number
}

/** 标准分页响应 */
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
size: number
}

/** 标准 API 响应包装 */
export interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}

禁止事项

// ❌ 禁止 any
const data: any = await fetchData()

// ❌ 禁止隐式 any 函数参数
function process(item) { ... }

// ✅ 正确
const data: UserItem = await fetchData()
function process(item: UserItem): void { ... }

// ⚠️ 确实无法确定类型时,用 unknown 替代 any
const raw: unknown = JSON.parse(str)

14. 测试规范

框架

  • 测试框架:Vitest
  • 组件测试:@vue/test-utils
  • 测试文件位置:tests/unit/

测试文件命名

tests/unit/
user.spec.ts # 测试 src/api/user.ts 或 src/views/user/
user-store.spec.ts # 测试 src/store/modules/user.ts
utils.spec.ts # 测试 src/utils/ 下的工具函数

覆盖率要求

类型最低覆盖率
整体项目≥ 70%
核心业务逻辑(store / utils)≥ 90%
API 函数≥ 80%

示例

// tests/unit/user-store.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import * as userApi from '@/api/user'

describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

it('should fetch user info successfully', async () => {
vi.spyOn(userApi, 'getUserInfo').mockResolvedValue({
data: { id: 1, username: 'tom', roles: ['admin'] },
})

const store = useUserStore()
await store.getUserInfo()

expect(store.username).toBe('tom')
expect(store.roles).toContain('admin')
})

it('should clear token on logout', async () => {
const store = useUserStore()
store.$patch({ token: 'test-token' })

await store.logout()

expect(store.token).toBe('')
expect(localStorage.getItem('token')).toBeNull()
})
})

常用命令

# 运行全部单元测试
pnpm test:unit

# 带 watch 模式(开发时使用)
pnpm test:unit --watch

# 生成覆盖率报告
pnpm test:unit --coverage

# 运行特定测试文件
pnpm test:unit tests/unit/user.spec.ts

15. 构建 & 环境规范

常用命令

命令说明
pnpm dev启动开发服务器(默认端口 15000)
pnpm build构建生产包(使用 .env.production
pnpm build:test构建测试环境包(使用 .env.test
pnpm preview预览构建产物
pnpm lint运行 ESLint + Stylelint 检查
pnpm lint:fix自动修复可修复的 Lint 问题
pnpm test:unit运行单元测试
pnpm type-checkTypeScript 类型检查(不输出文件)

开发服务器

// vite.config.ts(参考)
server: {
port: 15000,
open: true,
proxy: {
'/api': {
target: env.VITE_APP_BASE_API,
changeOrigin: true,
},
},
}

构建输出

  • 输出目录:dist/
  • 构建工具:Vite 5(编译器:SWC,速度优于 Babel)
  • 代码分割:框架已配置路由级 chunk 分割(lazy import 自动生效)

环境变量使用

在代码中通过 import.meta.env 访问:

// 访问环境变量(必须以 VITE_ 开头才会暴露给前端)
const baseURL = import.meta.env.VITE_APP_BASE_API
const appId = import.meta.env.VITE_APP_ID

// TypeScript 类型扩展(在 types/global.d.ts 中)
interface ImportMetaEnv {
VITE_APP_BASE_API: string
VITE_APP_ID: string
VITE_APP_TITLE: string
VITE_GOOGLE_CLIENT_ID: string
}

CI/CD 注意事项

  • 构建前执行 pnpm lintpnpm test:unit(失败则不部署)
  • 生产构建产物中不应包含 source map(build.sourcemap: false
  • 构建产物 dist/ 不提交到 git

16. .gitignore 必含项

以下条目必须在 .gitignore 中存在:

# 构建产物
dist/
dist-ssr/

# 依赖
node_modules/

# 环境变量(本地覆盖,不提交)
.env.*.local

# 自动生成文件(由 unplugin-auto-import / unplugin-vue-components 生成)
src/auto-imports.d.ts
src/components.d.ts
src/auto-components.d.ts

# IDE
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea/

# 系统文件
.DS_Store
Thumbs.db

# 日志
*.log
npm-debug.log*
pnpm-debug.log*

# 测试覆盖率
coverage/

# 临时文件
*.local

附录:快速参考卡

新建页面最小步骤

# 1. 创建页面文件
touch src/views/{module}/index.vue

# 2. 创建 API 文件
touch src/api/{module}.ts

# 3. 在 src/enum/ApiUrls.ts 添加路径常量
# 4. 在 src/types/{module}.ts 添加类型定义
# 5. 在 src/router/index.ts 添加路由
# 6. 在 src/i18n/locales/zh.ts 和 en.ts 添加文案
# 7. 创建测试文件
touch tests/unit/{module}.spec.ts

常见问题

Q: 组件 import 报错找不到
A: 检查组件是否在 src/components/ 下,重启 pnpm dev 刷新自动引入。

Q: ref / computed 报错未定义
A: 检查 src/auto-imports.d.ts 是否已生成(首次 pnpm dev 后自动生成)。

Q: Element Plus 组件样式不生效
A: 确认 src/styles/element-plus/index.scss 已在入口文件 import,并检查 CSS 变量覆盖优先级。

Q: 路由跳转后 404
A: 确认路由 name 唯一,检查父路由 component 是否为 Layout,子路由 path 不要带 / 前缀。

Q: 请求 401 但 token 存在
A: 检查 src/config/index.tstokenTableName 是否与存储时的 key 一致。