代码规范
本文档定义 vue3-element-admin 的编码约定。第一次阅读建议先看命名、目录、组件和 Composables,遇到争议时再回到具体规则。
先看什么?
| 你要规范什么 | 推荐章节 |
|---|---|
| 变量、函数、类型怎么命名 | 命名规范 |
| 页面和组件怎么组织 | 组件规范 |
| 可复用逻辑怎么抽取 | Composables 规范 |
| API、Store 和类型怎么写 | 接口与类型约定 |
| 提交前检查什么 | 自查清单 |
参考来源
| 来源 | 说明 |
|---|---|
| Vue 官方风格指南 | Vue 组件命名、Props 定义等 |
| JSDoc 官方规范 | 注释格式:完整句子 + 句号 |
| TypeScript 官方规范 | 类型定义最佳实践 |
| Airbnb JavaScript Style Guide | JS 命名、导入顺序等 |
| Google TypeScript Style Guide | TS 编码规范 |
| VueUse | Composables 命名参考 |
| Element Plus | 组件库源码参考 |
| Vben Admin | 企业级项目结构参考 |
命名规范
JavaScript / TypeScript 变量
| 类型 | 风格 | 示例 | 参考 |
|---|---|---|---|
| 变量 | camelCase | userName, isLoading | Airbnb |
| 常量 | UPPER_SNAKE_CASE | MAX_COUNT, API_BASE_URL | Airbnb |
| 函数 | camelCase | getUserInfo, handleClick | Airbnb |
| 类 | PascalCase | UserService, ApiClient | Airbnb |
| 枚举 | PascalCase | StatusEnum, ThemeMode | TypeScript |
| 枚举值 | UPPER_SNAKE_CASE | StatusEnum.ACTIVE | Google TS |
| 类型/接口 | PascalCase | UserInfo, ApiResponse | TypeScript |
| 泛型参数 | 单字母大写或描述性 | T, TData, TResponse | TypeScript |
// ✅ 正确示例
const userName = "admin";
const MAX_RETRY_COUNT = 3;
function getUserById(id: number): Promise<UserInfo> {}
enum StatusEnum {
ACTIVE = 1,
INACTIVE = 0,
}
interface UserInfo {
id: number;
name: string;
}
type ApiResponse<T> = {
code: string;
msg: string;
data: T;
};布尔值命名
使用 is、has、can、should 等前缀,表意清晰。
// ✅ 正确
const isLoading = ref(false);
const hasPermission = computed(() => /* ... */);
const canEdit = ref(true);
const shouldRefresh = ref(false);
// ❌ 避免
const loading = ref(false); // 不够明确
const permission = ref(true); // 名词,不是布尔语义常用变量命名(Vue 业务场景)
| 类型 | 命名 |
|---|---|
| 列表数据 | xxxList |
| 表单数据 | formData |
| 查询参数 | queryParams |
| 弹窗状态 | dialogState |
| 下拉选项 | xxxOptions |
| 表单引用 | xxxFormRef |
| 选中项 | selectedIds |
| 加载状态 | loading |
方法命名规范(Vue 业务场景)
函数/方法统一使用 camelCase,以动词开头,体现"动作 + 业务对象 + 场景"。布尔状态使用 is/has/can/should 等前缀。该约定偏向可读性与可维护性,适用于页面、组件、Store、Composables。
核心原则
- 方法/函数用动词:描述行为,例如
fetchUserList、submitUserForm。 - 状态/布尔用 is/has/can/should:描述状态,例如
isLoading、hasMore、canEdit。 - 分层约定(本项目推荐):
- 业务动作用语义动词(
fetch/submit/delete/create/update等),可被多个场景复用。 - 事件入口/流程编排使用
handleX/onX(例如点击、分页变化、弹窗确认),负责组织多个业务动作与 UI 状态变更。
- 业务动作用语义动词(
handle 前缀使用规则
核心判断标准:函数是"单一动作"还是"流程编排"?
| 类型 | 特点 | 是否用 handle |
|---|---|---|
| 业务动作 | 单一职责,只做一件事 | ❌ 不用 |
| 流程编排 | 组合多个动作,有流程控制 | ✅ 使用 |
判断方法:函数内部做了几件事?
// ✅ 单一动作 → 不用 handle
function openDialog() {
dialogState.visible = true;
}
function closeDialog() {
dialogState.visible = false;
resetForm();
}
// ✅ 流程编排 → 使用 handle
async function handleSubmit() {
const valid = await validateForm();
if (!valid) return;
await submitForm(formData);
closeDialog();
fetchList();
}为什么这样区分?
- 业务动作(不用 handle):可被多处复用
- 流程编排(使用 handle):作为事件入口,组织多个业务动作
命名约定表
| 场景 | 命名 |
|---|---|
| 打开/关闭弹窗 | openDialog / closeDialog |
| 显示/隐藏 | showX / hideX |
| 提交/保存 | submitForm / saveX |
| 查询/加载 | fetchList / loadOptions |
| 重置 | resetForm / resetQuery |
| 初始化 | initX |
| 新增/编辑/删除 | createX / updateX / deleteX |
| 批量操作 | batchX |
| 状态切换 | toggleX |
| 表单校验 | validateForm |
| 导入/导出 | importX / exportX |
| 事件入口 | handleSubmit / handleDelete |
| Composables | useX |
说明:一个页面通常只有一个业务,方法名无需重复业务名(如
openDialog而非openUserDialog)。多业务页面自行添加区分。
常见争议与建议
handleX/onXvs 直接动词- 推荐:优先用有业务语义的动词(
deleteUser、exportUsers)。 - 使用
handle/on的场景:当它只负责"桥接 UI 事件 + 组织流程",例如handleSubmit内部调用validateForm、submitUserForm、closeDialog。
- 推荐:优先用有业务语义的动词(
异步方法是否加
Async后缀fetch/load/search本身已隐含异步语义,一般不必再加Async。- 只有在同名同步/异步同时存在且易混淆时,才考虑
xxxAsync。
方法抽取原则
- 多次调用:抽取为独立方法,提高复用性。
- 单次调用:直接内联,避免过度抽象。
- 示例:
// ❌ 过度抽取:只调用一次的方法
async function fillForm(id: string) {
const data = await UserAPI.getFormData(id);
Object.assign(formData, data);
}
async function handleEditClick(id: string) {
await loadFormOptions();
await fillForm(id); // 唯一调用处
openDialog();
}
// ✅ 直接内联:单次调用的逻辑
async function handleEditClick(id: string) {
await loadFormOptions();
const data = await UserAPI.getFormData(id);
Object.assign(formData, data);
openDialog();
}
// ✅ 合理抽取:多处调用
function resetForm() {
userFormRef.value?.resetFields();
userFormRef.value?.clearValidate();
Object.assign(formData, initialFormData);
}
// resetForm 被 closeDialog、handleSubmit 等多处调用示例(弹窗/抽屉的分层)
弹窗场景建议把"开关动作"和"事件入口"区分开:
// 复杂弹窗 - 方式一:聚合对象
const dialogState = reactive({
visible: false,
title: "",
mode: "create" as "create" | "edit",
});
function openDialog() {
dialogState.visible = true;
}
function closeDialog() {
dialogState.visible = false;
resetForm();
}
async function handleCreateClick() {
dialogState.title = "新增用户";
dialogState.mode = "create";
openDialog();
}
async function handleEditClick(id: number) {
dialogState.title = "编辑用户";
dialogState.mode = "edit";
await fetchDetail(id);
openDialog();
}
async function handleSubmit() {
// 流程编排:校验 -> 提交 -> 关闭 -> 刷新
const valid = await validateForm();
if (!valid) return;
await submitForm(formData);
closeDialog();
fetchList();
}
async function fetchList() {
// 加载列表数据
}
async function deleteUser(id: number) {
// 删除用户
}避免: doSomething / click / ok / data / submit(无业务语义),以及 dlg / btn / qry 等缩写。
ESLint / TypeScript 命名约束(可选)
可通过 @typescript-eslint/naming-convention 强制布尔变量使用 is/has/can 前缀:
// .eslintrc.cjs / .eslintrc.js (片段)
{
"rules": {
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"types": ["boolean"],
"format": ["camelCase"],
"prefix": ["is", "has", "can"],
},
{
"selector": "function",
"format": ["camelCase"],
},
],
},
}CSS 类名
前端 Vue3 页面样式与现有代码分层保持一致,采用 BEM 定义语义、UnoCSS 补充微调、SCSS 承载复杂样式。项目已在 src/styles/page.scss 提供 page-* 页面骨架类,在 uno.config.ts 提供 wh-full、flex-center、flex-y-center、flex-x-between 等快捷类。新增或重构页面应先复用已有约定,再新增页面私有类。
| 承载方式 | 代码位置 | 职责 | 适用场景 |
|---|---|---|---|
| 全局页面类 | src/styles/page.scss | 统一列表页、管理页的页面骨架 | page-container、page-search、page-content、page-toolbar、page-table |
| BEM | 页面或组件的 scoped SCSS | 提供可搜索的业务/组件语义锚点 | profile-card、user-name-cell__text、single-upload__image |
| UnoCSS | 模板 class 或 Attributify 属性 | 处理无业务语义的局部微调 | m-0、pr-10px、w-full、flex-y-center、<div flex gap-10px> |
| SCSS | src/styles/* 或 SFC style lang="scss" | 承载原子类不适合表达的组合样式 | @media、:deep()、grid-template-columns、主题变量 |
判断顺序如下:
- 判断是否属于列表页、管理页的通用骨架;如属于,优先复用
page-*全局类。 - 判断元素是否承担页面或组件结构语义;如承担结构语义,使用 BEM 命名。
- 判断样式是否仅为一次性的无语义微调;如属于微调,使用 UnoCSS 或项目快捷类。
- 判断样式是否包含组合规则、交互态或复用需求;如存在复杂表达,放入 SCSS。
选型原则
- 全局页面骨架优先复用。 列表页、管理页优先使用
page-container、page-search、page-content、page-toolbar、page-table-wrapper、page-table、dialog-footer,不要为相同结构重复创建user-page__toolbar、role-page__table。 - 页面私有结构使用 BEM。 业务卡片、列表项、单元格、上传控件等稳定结构,应使用页面或组件前缀,例如
profile-card、user-name-cell、dept-card、single-upload、layout-logo。 - 单属性微调用 UnoCSS。 仅涉及
margin: 0、padding-right: 10px、width: 100%、gap: 8px等无业务语义的简单属性时,直接写 UnoCSS,不新建 BEM 类。 - 项目快捷类可以作为一个微调单元。
wh-full、flex-center、flex-y-center、flex-x-between等来自uno.config.ts,适合纯布局对齐;Attributify 写法同样只用于简单布局片段。 - 原子类保持克制。 新增或重构业务页面中,同一元素上的 UnoCSS 原子类不应超过 3 个;超过后应提炼为 BEM 类,并将样式收敛到 SCSS。配置驱动的通用组件内部如需保留密集原子类,应控制在组件边界内,不作为业务页面结构范式。
- BEM 负责结构,UnoCSS 只做补充。
class="profile-card m-0"可接受;class="profile-card flex flex-col gap-4 p-4 bg-white rounded shadow"应改为profile-card+ SCSS。 - 状态与变体分层表达。 通用状态使用
is-*,例如is-collapsed、is-loading;绑定到具体块的变体使用 BEM Modifier,例如layout--top、profile-icon--success、todo-row--done。避免text-gray、red-status这类样式结果名。 - Element Plus 微调按复杂度选择。 单次宽度、间距、对齐可使用组件
style、UnoCSS 或属性配置;覆盖内部结构、复杂交互态或多处复用时,使用 BEM/SCSS,并通过:deep()控制影响范围。
BEM 命名
格式统一为 block__element--modifier,类名使用 kebab-case。
| 类型 | 规则 | 示例 |
|---|---|---|
| Block | 页面/组件前缀 + 语义名 | profile-card、user-name-cell、single-upload |
| Element | Block 下的组成部分 | profile-card__header、profile-card__title、single-upload__image |
| Modifier | 状态或变体 | profile-card--compact、profile-icon--warning、todo-row--done |
常用 Element 词汇固定使用:__header、__body、__footer、__title、__desc、__meta、__icon、__avatar、__actions、__list、__item、__label、__value。
正确示例
<template>
<div class="page-container page-container--split user-page">
<aside class="page-aside" :class="{ 'is-collapsed': sidebarCollapsed }">
<div class="page-aside__inner">
<UserDeptTree />
</div>
</aside>
<main class="page-main">
<el-card class="page-content" shadow="never">
<div class="page-toolbar">
<div class="page-toolbar__left">
<el-button type="primary">新增</el-button>
</div>
<div class="page-toolbar__right">
<el-button class="page-icon-btn">
<el-icon><Refresh /></el-icon>
</el-button>
</div>
</div>
<div class="user-name-cell">
<span class="user-name-cell__text">YL</span>
<span>有来用户</span>
</div>
</el-card>
</main>
</div>
</template>
<style lang="scss" scoped>
.user-name-cell {
display: inline-flex;
gap: 8px;
align-items: center;
}
.user-name-cell__text {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 12px;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border-radius: 50%;
}
</style>反例
<!-- ❌ 仅为单个属性创建 BEM 类,应直接使用 class="m-0" -->
<el-tag class="profile-tags__item" />
<style scoped>
.profile-tags__item {
margin: 0;
}
</style>
<!-- ❌ BEM 与大量 UnoCSS 混写,降低可检索性和维护性 -->
<section class="profile-card flex flex-col gap-4 p-5 bg-white rounded shadow">
...
</section>
<!-- ❌ 新增业务页面主结构不应使用原子类密集堆叠 -->
<div class="grid min-h-34px grid-cols-1 items-center gap-1 border-b pb-2.5">
...
</div>提交前自查
- [ ] 列表页、管理页已优先复用
page-*全局页面骨架。 - [ ] 页面私有结构、卡片、列表项、单元格有可搜索的 BEM 语义类。
- [ ] 新增业务页面中,单个元素的 UnoCSS 原子类不超过 3 个。
- [ ] 没有为了 1 个 CSS 属性新建 BEM 类。
- [ ] 没有
profile-card flex flex-col gap-4 p-4 ...这类 BEM + 大量原子类混写。 - [ ] 颜色、边框、背景优先使用项目 CSS 变量,不硬编码主题色。
CSS 变量
项目主题变量集中定义在 src/styles/theme.scss,Element Plus 主题变量在 src/styles/element-plus-vars.scss 与 src/styles/element-plus-overrides.scss 中维护。业务页面不直接硬编码主题色、卡片边框、页面背景等视觉常量,应优先复用已有 CSS 变量。
// ✅ 正确:复用项目现有变量
:root {
--page-bg: #f6f8fc;
--content-bg: #ffffff;
--card-border: #dfe7f1;
--card-radius: 8px;
--page-gap: 10px;
}
.profile-card {
background: var(--content-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
}
.profile-title {
color: var(--el-text-color-primary);
}常用变量优先级如下:
- Element Plus 语义变量:
--el-color-primary、--el-text-color-primary、--el-border-color-extra-light、--el-fill-color-light。 - 项目页面变量:
--page-bg、--content-bg、--page-padding、--page-gap。 - 项目卡片变量:
--card-border、--card-border-hover、--card-radius、--card-shadow。 - 布局 Sass 变量:
$navbar-height、$sidebar-width、$sidebar-width-collapsed,仅用于 SCSS。
文件命名规范
总览
| 文件类型 | 命名风格 | 示例 | 参考 |
|---|---|---|---|
| Vue 组件 | PascalCase | UserCard.vue, PageHeader.vue | Vue 官方 |
| 页面组件 | kebab-case | index.vue, user-list.vue | Vue 官方 |
| TS/JS 模块 | kebab-case | user-service.ts, format-date.ts | 社区惯例 |
| 多词模块 | kebab-case | tags-view.ts, dict-sync.ts | 文件系统兼容 |
| 类型文件 | kebab-case | types.ts, common.ts | 社区惯例 |
| 测试文件 | 源文件名 + .test | storage.test.ts | Vitest |
| 样式文件 | kebab-case | variables.scss, reset.scss | 社区惯例 |
Vue 组件文件
src/components/
├── DictSelect/
│ └── index.vue # 目录组件入口
├── Pagination/
│ └── index.vue
├── Upload/
│ ├── FileUpload.vue
│ ├── MultiImageUpload.vue
│ └── SingleImageUpload.vue
└── OperationColumn/
└── index.vue页面文件
src/views/
├── dashboard/
│ └── index.vue # 页面入口
├── system/
│ ├── user/
│ │ ├── index.vue # 用户列表页
│ │ └── components/ # 页面私有组件
│ │ └── UserForm.vue
│ └── role/
│ └── index.vueAPI 文件
src/api/
├── common.ts # 公共类型
├── auth/
│ ├── index.ts # 认证 API
│ └── types.ts
├── system/
│ ├── user/
│ │ ├── index.ts # 用户相关 API
│ │ └── types.ts
│ ├── role/
│ │ ├── index.ts
│ │ └── types.ts
│ └── menu/
│ ├── index.ts
│ └── types.ts
└── file/
├── index.ts
└── types.tsStore 文件
src/stores/
├── index.ts
├── user.ts # 用户状态
├── app.ts # 应用状态
├── dict.ts # 字典状态
├── permission.ts # 权限状态
├── settings.ts # 设置状态
├── tags-view.ts # 标签页状态(多词用 kebab-case)
└── tenant.ts # 租户状态类型文件
API 类型与接口模块就近维护,公共响应、分页和选项类型放在 src/api/common.ts。新增 API 模块时,优先在同级 types.ts 中定义请求参数、列表项、表单对象和响应对象,并在 index.ts 中按需重导出。
src/api/
├── common.ts # 公共类型:ApiResult、PageResult、OptionItem 等
├── auth/
│ ├── index.ts # API 方法
│ └── types.ts # 认证相关类型
└── system/
└── user/
├── index.ts # 用户 API 方法,并重导出类型
└── types.ts # 用户查询、表单、列表、个人中心类型就近维护类型的原则:
- API 入参、出参和列表项类型放在同模块
types.ts。 - 跨模块复用的通用类型放在
src/api/common.ts。 - 自动生成或全局声明类型放在项目根目录
types/,不放入src/api。
Composables 文件
src/composables/
├── sse/
│ ├── index.ts # 统一导出
│ ├── useSse.ts
│ ├── useDictSync.ts
│ └── useOnlineCount.ts
├── usePageTable.ts
├── useTableSelection.ts
└── index.ts # 统一导出参考:VueUse 源码结构
组件规范
组件命名
<!-- ✅ 推荐:PascalCase,多词命名 -->
<template>
<UserCard />
<PageHeader />
<DictSelect />
</template>
<!-- ⚠️ 单词组件仅用于基础能力或项目已有通用组件 -->
<template>
<Breadcrumb />
<Fullscreen />
<Pagination />
</template>项目已关闭 vue/multi-word-component-names 强制校验,新增业务组件仍优先使用多词命名;已有基础组件、布局组件、工具组件可保持当前命名。
Props 定义
// ✅ 推荐:业务组件优先使用 defineProps 配合 TypeScript
interface Props {
/** 用户ID */
userId: number;
/** 是否显示头像 */
showAvatar?: boolean;
/** 尺寸 */
size?: "small" | "medium" | "large";
}
const props = withDefaults(defineProps<Props>(), {
showAvatar: true,
size: "medium",
});
// ✅ 需要运行时默认值、校验或对象配置时,可以使用运行时声明
const props = defineProps({
maxFileSize: {
type: Number,
default: 10,
},
});同一组件内不要混用两种 props 风格;表单、列表、业务页面组件优先使用 TypeScript 类型声明,上传、封装组件等需要运行时默认值的场景可使用对象声明。
Emits 定义
// ✅ 正确:类型化的 emits
interface Emits {
(e: "update:modelValue", value: string): void;
(e: "change", value: string, oldValue: string): void;
(e: "submit"): void;
}
const emit = defineEmits<Emits>();
// 使用
emit("update:modelValue", newValue);
emit("change", newValue, oldValue);组件结构顺序
SFC 块顺序与 ESLint vue/block-order 保持一致:template → script → style。
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 1. Vue 核心
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
// 2. 第三方库
import { ElMessage, ElMessageBox } from "element-plus";
import { useDebounceFn } from "@vueuse/core";
import dayjs from "dayjs";
// 3. 类型导入
import type { FormInstance, FormRules } from "element-plus";
import type { UserInfo } from "@/api/system/user";
// 4. 内部模块 - Store
import { useUserStore } from "@/stores";
// 5. 内部模块 - API
import UserAPI from "@/api/system/user";
// 6. 内部模块 - 工具函数
import { formatDate } from "@/utils";
// 7. 相对路径 - 组件
import UserCard from "./components/UserCard.vue";
// 8. Props / Emits 定义
interface Props {
userId: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{ (e: "change"): void }>();
// 9. 响应式状态
const userStore = useUserStore();
const isLoading = ref(false);
const userInfo = ref<UserInfo | null>(null);
// 10. 计算属性
const displayName = computed(() => userInfo.value?.name ?? "未知");
// 11. 监听器
watch(() => props.userId, fetchUser);
// 12. 生命周期
onMounted(() => {
fetchUser();
});
// 13. 方法
async function fetchUser() {
// ...
}
// 14. 暴露(如需要)
defineExpose({ refresh: fetchUser });
</script>
<style lang="scss" scoped>
/* 样式 */
</style>导入顺序规范
按以下顺序组织导入语句,组间空行分隔:
// 1. Node 内置模块
import { resolve } from "path";
// 2. 第三方库
import { ref, computed, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import axios from "axios";
// 3. 类型导入(type-only imports)
import type { RouteRecordRaw } from "vue-router";
import type { UserInfo } from "@/api/system/user";
// 4. 内部模块 - 绝对路径(按层级)
import { useUserStore } from "@/stores";
import UserAPI from "@/api/system/user";
import { formatDate } from "@/utils";
import { STORAGE_KEYS } from "@/constants";
// 5. 内部模块 - 相对路径
import UserCard from "./components/UserCard.vue";
import { useLocalState } from "./composables";
// 6. 样式文件
import "./styles/index.scss";接口与类型约定
本节约定 vue3-element-admin 在对接后端时,TypeScript 类型与接口层的命名方式。
命名原则
- 前端类型不使用
DTO/VO/BO命名 - 统一使用"语义 + 场景"的类型命名
- 字段命名以接口返回为准(推荐
camelCase),尽量避免在前端做字段映射
TypeScript 类型命名
| 语义 | TypeScript 命名 |
|---|---|
| 创建 / 修改请求 | UserCreateRequest / UserUpdateRequest |
| 查询参数 | UserQueryParams |
| 列表项 | UserItem |
| 详情 | UserDetail |
| 分页结果 | PageResult<UserItem> |
分页与列表约定
- 列表与分页统一使用:
GET /api/v1/{resources} - 分页参数:
pageNum/pageSize
相关链接
类型定义规范
Interface vs Type
| 场景 | 推荐 | 原因 |
|---|---|---|
| 对象结构 | interface | 可扩展、错误信息更友好 |
| 联合类型 | type | interface 不支持 |
| 函数类型 | type | 语法更简洁 |
| 工具类型 | type | 如 Partial<T>, Pick<T, K> |
// ✅ 对象结构用 interface
interface UserInfo {
id: number;
name: string;
email: string;
}
// ✅ 可扩展
interface AdminInfo extends UserInfo {
permissions: string[];
}
// ✅ 联合类型用 type
type Status = "pending" | "success" | "error";
type ID = string | number;
// ✅ 函数类型用 type
type Formatter = (value: number) => string;
type AsyncFn<T> = () => Promise<T>;
// ✅ 工具类型
type PartialUser = Partial<UserInfo>;
type UserName = Pick<UserInfo, "name">;API 响应类型
// 通用响应结构
interface ApiResponse<T = unknown> {
code: number;
data: T;
message: string;
}
// 分页响应
interface PageResult<T> {
list: T[];
total: number;
}
// 具体业务类型
interface UserInfo {
id: number;
username: string;
nickname: string;
avatar?: string;
roles: string[];
}
// 查询参数
interface UserQuery {
keywords?: string;
status?: number;
deptId?: number;
pageNum: number;
pageSize: number;
}
// API 函数返回类型
type UserPageResult = ApiResponse<PageResult<UserInfo>>;类型文件组织
// src/api/system/user/types.ts
/** 用户信息 */
export interface UserInfo {
id: number;
username: string;
nickname: string;
avatar?: string;
email?: string;
mobile?: string;
status: number;
deptId: number;
roles: string[];
}
/** 用户查询参数 */
export interface UserQuery {
keywords?: string;
status?: number;
deptId?: number;
pageNum: number;
pageSize: number;
}
/** 用户表单 */
export interface UserForm {
id?: number;
username: string;
nickname: string;
password?: string;
email?: string;
mobile?: string;
status: number;
deptId: number;
roleIds: number[];
}Composables 规范
命名规范
| 规则 | 示例 | 说明 |
|---|---|---|
use 前缀 | useUserInfo, useTableSelection | Vue 官方约定 |
| 动词 + 名词 | useFetchData, useLocalStorage | 表意清晰 |
| 返回对象 | { data, loading, error } | 解构友好 |
// ✅ 正确命名
export function useTableSelection<T>() {
const selectedItems = ref<T[]>([]);
const hasSelected = computed(() => selectedItems.value.length > 0);
function handleSelectionChange(items: T[]) {
selectedItems.value = items;
}
function clearSelection() {
selectedItems.value = [];
}
return {
selectedItems,
hasSelected,
handleSelectionChange,
clearSelection,
};
}参考:VueUse 命名规范
参数设计
// ✅ 使用 options 对象,便于扩展
interface UseCounterOptions {
initialValue?: number;
min?: number;
max?: number;
}
export function useCounter(options: UseCounterOptions = {}) {
const { initialValue = 0, min = -Infinity, max = Infinity } = options;
const count = ref(initialValue);
function increment() {
if (count.value < max) count.value++;
}
function decrement() {
if (count.value > min) count.value--;
}
return { count, increment, decrement };
}
// 使用
const { count, increment } = useCounter({ initialValue: 10, max: 100 });返回值设计
// ✅ 返回响应式引用和方法
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const isLoading = ref(false);
async function execute() {
isLoading.value = true;
error.value = null;
try {
const response = await fetch(url);
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
isLoading.value = false;
}
}
return {
data: readonly(data),
error: readonly(error),
isLoading: readonly(isLoading),
execute,
refresh: execute,
};
}Store 规范
命名规范
| 元素 | 命名 | 示例 |
|---|---|---|
| Store 函数 | use + 模块名 + Store | useUserStore |
| Store ID | camelCase | "user", "tagsView" |
| State | camelCase | userInfo, isLoggedIn |
| Getter | camelCase | fullName, isAdmin |
| Action | camelCase 动词 | fetchUser, logout |
Setup Store 写法(推荐)
// src/stores/user.ts
import { defineStore } from "pinia";
import AuthAPI from "@/api/auth";
import type { UserInfo } from "@/api/system/user";
export const useUserStore = defineStore("user", () => {
// State
const userInfo = ref<UserInfo | null>(null);
const token = ref<string>("");
// Getters
const isLoggedIn = computed(() => !!token.value);
const userId = computed(() => userInfo.value?.id);
const roles = computed(() => userInfo.value?.roles ?? []);
// Actions
async function login(username: string, password: string) {
const { accessToken } = await AuthAPI.login({ username, password });
token.value = accessToken;
}
async function fetchUserInfo() {
userInfo.value = await AuthAPI.getUserInfo();
}
function logout() {
token.value = "";
userInfo.value = null;
}
return {
// State
userInfo,
token,
// Getters
isLoggedIn,
userId,
roles,
// Actions
login,
fetchUserInfo,
logout,
};
});Store Hook(在 setup 外使用)
// 定义 Hook
import { store } from "@/stores";
export function useUserStoreHook() {
return useUserStore(store);
}
// 在非 setup 上下文使用(如路由守卫)
import { useUserStoreHook } from "@/stores";
router.beforeEach((to, from, next) => {
const userStore = useUserStoreHook();
if (!userStore.isLoggedIn) {
next("/login");
}
});API 规范
文件结构
// src/api/system/user/index.ts
import request from "@/utils/request";
import type { UserInfo, UserQueryParams, UserForm } from "./types";
import type { PageResult } from "@/api/common";
const USER_BASE_URL = "/api/v1/users";
const UserAPI = {
/** 获取用户分页列表 */
getPage(queryParams: UserQueryParams) {
return request<unknown, PageResult<UserInfo>>({
url: USER_BASE_URL,
method: "get",
params: queryParams,
});
},
/** 获取用户详情 */
getFormData(userId: string) {
return request<unknown, UserForm>({
url: `${USER_BASE_URL}/${userId}/form`,
method: "get",
});
},
/** 新增用户 */
create(data: UserForm) {
return request({
url: USER_BASE_URL,
method: "post",
data,
});
},
/** 修改用户 */
update(id: string, data: UserForm) {
return request({
url: `${USER_BASE_URL}/${id}`,
method: "put",
data,
});
},
/** 删除用户 */
deleteByIds(ids: string) {
return request({
url: `${USER_BASE_URL}/${ids}`,
method: "delete",
});
},
};
export default UserAPI;
export * from "./types";命名约定
| 操作 | 方法名 | HTTP 方法 |
|---|---|---|
| 分页查询 | getPage | GET |
| 列表查询 | getList | GET |
| 详情查询 | getFormData / getById | GET |
| 新增 | create | POST |
| 修改 | update | PUT |
| 删除 | deleteByIds | DELETE |
| 导出 | export | GET/POST |
| 导入 | import | POST |
注释规范
注释不是越少越好,而是只写有信息量的内容。好的注释补充代码没有直接表达出的背景、意图、约束和边界;坏的注释复述代码、制造噪音,或只起视觉分隔作用。
注释取舍
| 场景 | 建议写法 | 说明 |
|---|---|---|
| 类型、接口、属性、常量 | 单行 JSDoc | 说明用途、约束或默认语义 |
| 函数、方法、Composable | 多行 JSDoc | 说明用途、参数、返回值或约束 |
| 业务规则、兼容策略、异常兜底 | 行内注释或块级注释 | 解释为什么这样做 |
| 大型配置对象、复杂 Composable 的分组 | 简短普通注释 | 例如 // 面板状态、// 菜单数据、// 认证 |
| 不容易从代码看出的交互约定 | 多行 JSDoc 或行内注释 | 方法级说明用多行 JSDoc,语句级说明用行内注释 |
| 函数内部的普通步骤 | 不写注释 | 通过命名、拆分函数和空行表达 |
| 纯视觉分隔标题 | 禁止使用 | 不使用横线、等号包裹标题,也不写低价值标题 |
// ✅ 合理:分组信息有助于阅读,保持简短
// 面板状态
const visible = ref(false);
const keyword = ref("");
// 菜单数据
const menuItems = ref<SearchItem[]>([]);
const results = ref<SearchItem[]>([]);
/**
* 搜索仅匹配菜单标题,避免路径命中过多造成结果噪音。
*/
function searchByTitle() {}
// ✅ 合理:大型配置对象可使用短分组
export const STORAGE_KEYS = {
// 认证
ACCESS_TOKEN: `${APP_PREFIX}:auth:access_token`,
REFRESH_TOKEN: `${APP_PREFIX}:auth:refresh_token`,
// UI
THEME: `${APP_PREFIX}:ui:theme`,
};
// ✅ 合理:解释边界处理
const localKeys = Object.keys(localStorage).filter((key) => key.startsWith(prefix)); // 只清理当前应用写入的缓存
// ❌ 视觉分隔,无额外信息:不要使用横线包裹的标题块
// ❌ 复述代码
const index = list.findIndex((item) => item.id === id); // 查找索引JSDoc 注释
规则来源:JSDoc 官方规范
- 类型、接口、属性、常量等短契约可使用单行 JSDoc,例如
/** 用户状态。 */。 - 函数、方法、Composable 的说明使用多行 JSDoc,至少包含
/**、摘要行、*/三行。 - 带
@param/@returns等标签时,必须多行格式。 - 摘要句号结尾,标签后的说明也句号结尾。
- 避免文件头、作者、模块标题等重复信息,除非能提供维护者必须知道的背景。
- 如果函数不需要说明,就不要硬写注释。
/**
* 格式化文件大小。
*
* @param bytes - 字节数。
* @param decimals - 小数位数,默认 2。
* @returns 格式化后的字符串,如 "1.50 MB"。
* @example
* formatFileSize(1024) // "1.00 KB"
* formatFileSize(1048576) // "1.00 MB"
*/
export function formatFileSize(bytes: number, decimals = 2): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
}组件注释
<script setup lang="ts">
/**
* 字典选择器组件。
*
* @description 基于 el-select 封装,自动加载字典数据。
* @example
* <DictSelect v-model="form.status" dict-code="sys_status" />
*/
interface Props {
/** 绑定值。 */
modelValue?: string | number;
/** 字典编码。 */
dictCode: string;
/** 是否可清空。 */
clearable?: boolean;
}
</script>行内注释
行内注释应靠近相关代码,说明原因、约束或例外情况。普通赋值、简单判断、函数调用不需要注释。
// ✅ 解释"为什么",而非"是什么"
const timeout = 30000; // 后端接口响应较慢,需要较长超时时间
// ✅ 解释例外情况
if (EXCLUDED_PATHS.includes(route.path)) return; // 跳过登录、错误页和重定向页
// ❌ 避免无意义注释
const count = 0; // 设置 count 为 0Git 提交规范
Commit Message 格式
<type>(<scope>): <subject>
<body>
<footer>Type 类型
| 类型 | 说明 |
|---|---|
feat | 新功能 |
fix | 修复 Bug |
docs | 文档更新 |
style | 代码格式(不影响功能) |
refactor | 重构(非新功能、非修复) |
perf | 性能优化 |
test | 测试相关 |
chore | 构建/工具变动 |
revert | 回滚 |
示例
# 新功能
feat(user): 添加用户导入功能
# 修复
fix(auth): 修复 token 过期后未跳转登录页的问题
# 文档
docs: 更新开发规范文档
# 重构
refactor(store): 使用 setup store 语法重构 user store目录结构规范
src/
├── api/ # API 接口
│ ├── common.ts # 公共类型
│ ├── auth/ # 认证接口
│ │ ├── index.ts
│ │ └── types.ts
│ └── system/ # 系统管理模块
│ ├── user/
│ │ ├── index.ts
│ │ └── types.ts
│ └── role/
│ ├── index.ts
│ └── types.ts
├── assets/ # 静态资源
│ ├── images/
│ └── icons/
├── components/ # 公共组件
│ ├── DictSelect/ # 复杂组件用目录
│ │ └── index.vue
│ └── Pagination/ # 公共组件目录
├── composables/ # 组合式函数
│ ├── sse/
│ ├── usePageTable.ts
│ └── useTableSelection.ts
├── constants/ # 常量定义
├── directives/ # 自定义指令
├── enums/ # 枚举定义
├── lang/ # 国际化
├── layouts/ # 布局组件
├── plugins/ # 插件注册
├── router/ # 路由配置
├── stores/ # 状态管理
├── styles/ # 全局样式
├── utils/ # 工具函数
├── views/ # 页面组件
│ └── system/
│ └── user/
│ ├── index.vue
│ └── components/
├── App.vue
├── main.ts
└── settings.tsESLint / Prettier 配置
项目已配置 ESLint + Prettier,确保代码风格一致:
# 检查代码
pnpm lint:eslint
# 格式化代码
pnpm lint:prettier
# 检查样式
pnpm lint:stylelint
# 全部检查
pnpm lint关键规则
- 使用 2 空格缩进
- 使用双引号
- 语句末尾加分号(
semi: true) - Vue 组件模板中使用 PascalCase
- Props 优先使用 TypeScript 类型定义
- 未使用变量按 ESLint 配置处理为警告或错误
总结
| 类别 | 规范 |
|---|---|
| 变量 | camelCase |
| 常量 | UPPER_SNAKE_CASE |
| 类/接口/类型 | PascalCase |
| 文件(TS/JS) | kebab-case |
| 文件(Vue 组件) | PascalCase |
| CSS 类名 | BEM / UnoCSS / SCSS |
| Composables | use 前缀 + camelCase |
| Store | use + 模块名 + Store |
| API 方法 | 动词 + 名词 |
| Git Commit | Conventional Commits |
遵循这些规范,可以保证代码的一致性、可读性和可维护性。
