Skip to content

代码规范

本文档定义 vue3-element-admin 的编码约定。第一次阅读建议先看命名、目录、组件和 Composables,遇到争议时再回到具体规则。

先看什么?

你要规范什么推荐章节
变量、函数、类型怎么命名命名规范
页面和组件怎么组织组件规范
可复用逻辑怎么抽取Composables 规范
API、Store 和类型怎么写接口与类型约定
提交前检查什么自查清单

参考来源

来源说明
Vue 官方风格指南Vue 组件命名、Props 定义等
JSDoc 官方规范注释格式:完整句子 + 句号
TypeScript 官方规范类型定义最佳实践
Airbnb JavaScript Style GuideJS 命名、导入顺序等
Google TypeScript Style GuideTS 编码规范
VueUseComposables 命名参考
Element Plus组件库源码参考
Vben Admin企业级项目结构参考

命名规范

JavaScript / TypeScript 变量

类型风格示例参考
变量camelCaseuserName, isLoadingAirbnb
常量UPPER_SNAKE_CASEMAX_COUNT, API_BASE_URLAirbnb
函数camelCasegetUserInfo, handleClickAirbnb
PascalCaseUserService, ApiClientAirbnb
枚举PascalCaseStatusEnum, ThemeModeTypeScript
枚举值UPPER_SNAKE_CASEStatusEnum.ACTIVEGoogle TS
类型/接口PascalCaseUserInfo, ApiResponseTypeScript
泛型参数单字母大写或描述性T, TData, TResponseTypeScript
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;
};

布尔值命名

使用 ishascanshould 等前缀,表意清晰。

typescript
// ✅ 正确
const isLoading = ref(false);
const hasPermission = computed(() => /* ... */);
const canEdit = ref(true);
const shouldRefresh = ref(false);

// ❌ 避免
const loading = ref(false);      // 不够明确
const permission = ref(true);    // 名词,不是布尔语义

参考:Airbnb - Naming Conventions

常用变量命名(Vue 业务场景)

类型命名
列表数据xxxList
表单数据formData
查询参数queryParams
弹窗状态dialogState
下拉选项xxxOptions
表单引用xxxFormRef
选中项selectedIds
加载状态loading

方法命名规范(Vue 业务场景)

函数/方法统一使用 camelCase以动词开头,体现"动作 + 业务对象 + 场景"。布尔状态使用 is/has/can/should 等前缀。该约定偏向可读性与可维护性,适用于页面、组件、Store、Composables。

核心原则

  • 方法/函数用动词:描述行为,例如 fetchUserListsubmitUserForm
  • 状态/布尔用 is/has/can/should:描述状态,例如 isLoadinghasMorecanEdit
  • 分层约定(本项目推荐)
    • 业务动作用语义动词(fetch/submit/delete/create/update 等),可被多个场景复用。
    • 事件入口/流程编排使用 handleX/onX(例如点击、分页变化、弹窗确认),负责组织多个业务动作与 UI 状态变更。

handle 前缀使用规则

核心判断标准:函数是"单一动作"还是"流程编排"?

类型特点是否用 handle
业务动作单一职责,只做一件事❌ 不用
流程编排组合多个动作,有流程控制✅ 使用

判断方法:函数内部做了几件事?

typescript
// ✅ 单一动作 → 不用 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
ComposablesuseX

说明:一个页面通常只有一个业务,方法名无需重复业务名(如 openDialog 而非 openUserDialog)。多业务页面自行添加区分。

常见争议与建议

  • handleX/onX vs 直接动词

    • 推荐:优先用有业务语义的动词(deleteUserexportUsers)。
    • 使用 handle/on 的场景:当它只负责"桥接 UI 事件 + 组织流程",例如 handleSubmit 内部调用 validateFormsubmitUserFormcloseDialog
  • 异步方法是否加 Async 后缀

    • fetch/load/search 本身已隐含异步语义,一般不必再加 Async
    • 只有在同名同步/异步同时存在且易混淆时,才考虑 xxxAsync
  • 方法抽取原则

    • 多次调用:抽取为独立方法,提高复用性。
    • 单次调用:直接内联,避免过度抽象。
    • 示例:
typescript
// ❌ 过度抽取:只调用一次的方法
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 等多处调用

示例(弹窗/抽屉的分层)

弹窗场景建议把"开关动作"和"事件入口"区分开:

typescript
// 复杂弹窗 - 方式一:聚合对象
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 前缀:

jsonc
// .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-fullflex-centerflex-y-centerflex-x-between 等快捷类。新增或重构页面应先复用已有约定,再新增页面私有类。

承载方式代码位置职责适用场景
全局页面类src/styles/page.scss统一列表页、管理页的页面骨架page-containerpage-searchpage-contentpage-toolbarpage-table
BEM页面或组件的 scoped SCSS提供可搜索的业务/组件语义锚点profile-carduser-name-cell__textsingle-upload__image
UnoCSS模板 class 或 Attributify 属性处理无业务语义的局部微调m-0pr-10pxw-fullflex-y-center<div flex gap-10px>
SCSSsrc/styles/* 或 SFC style lang="scss"承载原子类不适合表达的组合样式@media:deep()grid-template-columns、主题变量

判断顺序如下:

  1. 判断是否属于列表页、管理页的通用骨架;如属于,优先复用 page-* 全局类。
  2. 判断元素是否承担页面或组件结构语义;如承担结构语义,使用 BEM 命名。
  3. 判断样式是否仅为一次性的无语义微调;如属于微调,使用 UnoCSS 或项目快捷类。
  4. 判断样式是否包含组合规则、交互态或复用需求;如存在复杂表达,放入 SCSS。

选型原则

  1. 全局页面骨架优先复用。 列表页、管理页优先使用 page-containerpage-searchpage-contentpage-toolbarpage-table-wrapperpage-tabledialog-footer,不要为相同结构重复创建 user-page__toolbarrole-page__table
  2. 页面私有结构使用 BEM。 业务卡片、列表项、单元格、上传控件等稳定结构,应使用页面或组件前缀,例如 profile-carduser-name-celldept-cardsingle-uploadlayout-logo
  3. 单属性微调用 UnoCSS。 仅涉及 margin: 0padding-right: 10pxwidth: 100%gap: 8px 等无业务语义的简单属性时,直接写 UnoCSS,不新建 BEM 类。
  4. 项目快捷类可以作为一个微调单元。 wh-fullflex-centerflex-y-centerflex-x-between 等来自 uno.config.ts,适合纯布局对齐;Attributify 写法同样只用于简单布局片段。
  5. 原子类保持克制。 新增或重构业务页面中,同一元素上的 UnoCSS 原子类不应超过 3 个;超过后应提炼为 BEM 类,并将样式收敛到 SCSS。配置驱动的通用组件内部如需保留密集原子类,应控制在组件边界内,不作为业务页面结构范式。
  6. BEM 负责结构,UnoCSS 只做补充。 class="profile-card m-0" 可接受;class="profile-card flex flex-col gap-4 p-4 bg-white rounded shadow" 应改为 profile-card + SCSS。
  7. 状态与变体分层表达。 通用状态使用 is-*,例如 is-collapsedis-loading;绑定到具体块的变体使用 BEM Modifier,例如 layout--topprofile-icon--successtodo-row--done。避免 text-grayred-status 这类样式结果名。
  8. Element Plus 微调按复杂度选择。 单次宽度、间距、对齐可使用组件 style、UnoCSS 或属性配置;覆盖内部结构、复杂交互态或多处复用时,使用 BEM/SCSS,并通过 :deep() 控制影响范围。

BEM 命名

格式统一为 block__element--modifier,类名使用 kebab-case。

类型规则示例
Block页面/组件前缀 + 语义名profile-carduser-name-cellsingle-upload
ElementBlock 下的组成部分profile-card__headerprofile-card__titlesingle-upload__image
Modifier状态或变体profile-card--compactprofile-icon--warningtodo-row--done

常用 Element 词汇固定使用:__header__body__footer__title__desc__meta__icon__avatar__actions__list__item__label__value

正确示例

vue
<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>

反例

vue
<!-- ❌ 仅为单个属性创建 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 变量,不硬编码主题色。

参考:BEM 官方文档Element Plus 源码UnoCSS 原子化 CSS 思路

CSS 变量

项目主题变量集中定义在 src/styles/theme.scss,Element Plus 主题变量在 src/styles/element-plus-vars.scsssrc/styles/element-plus-overrides.scss 中维护。业务页面不直接硬编码主题色、卡片边框、页面背景等视觉常量,应优先复用已有 CSS 变量。

scss
// ✅ 正确:复用项目现有变量
: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);
}

常用变量优先级如下:

  1. Element Plus 语义变量:--el-color-primary--el-text-color-primary--el-border-color-extra-light--el-fill-color-light
  2. 项目页面变量:--page-bg--content-bg--page-padding--page-gap
  3. 项目卡片变量:--card-border--card-border-hover--card-radius--card-shadow
  4. 布局 Sass 变量:$navbar-height$sidebar-width$sidebar-width-collapsed,仅用于 SCSS。

参考:CSS Custom Properties


文件命名规范

总览

文件类型命名风格示例参考
Vue 组件PascalCaseUserCard.vue, PageHeader.vueVue 官方
页面组件kebab-caseindex.vue, user-list.vueVue 官方
TS/JS 模块kebab-caseuser-service.ts, format-date.ts社区惯例
多词模块kebab-casetags-view.ts, dict-sync.ts文件系统兼容
类型文件kebab-casetypes.ts, common.ts社区惯例
测试文件源文件名 + .teststorage.test.tsVitest
样式文件kebab-casevariables.scss, reset.scss社区惯例

Vue 组件文件

src/components/
├── DictSelect/
│   └── index.vue         # 目录组件入口
├── Pagination/
│   └── index.vue
├── Upload/
│   ├── FileUpload.vue
│   ├── MultiImageUpload.vue
│   └── SingleImageUpload.vue
└── OperationColumn/
    └── index.vue

参考:Vue 风格指南 - 单文件组件文件名

页面文件

src/views/
├── dashboard/
│   └── index.vue         # 页面入口
├── system/
│   ├── user/
│   │   ├── index.vue     # 用户列表页
│   │   └── components/   # 页面私有组件
│   │       └── UserForm.vue
│   └── role/
│       └── index.vue

API 文件

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.ts

Store 文件

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      # 用户查询、表单、列表、个人中心类型

就近维护类型的原则:

  1. API 入参、出参和列表项类型放在同模块 types.ts
  2. 跨模块复用的通用类型放在 src/api/common.ts
  3. 自动生成或全局声明类型放在项目根目录 types/,不放入 src/api

Composables 文件

src/composables/
├── sse/
│   ├── index.ts                # 统一导出
│   ├── useSse.ts
│   ├── useDictSync.ts
│   └── useOnlineCount.ts
├── usePageTable.ts
├── useTableSelection.ts
└── index.ts                    # 统一导出

参考:VueUse 源码结构


组件规范

组件命名

vue
<!-- ✅ 推荐:PascalCase,多词命名 -->
<template>
  <UserCard />
  <PageHeader />
  <DictSelect />
</template>

<!-- ⚠️ 单词组件仅用于基础能力或项目已有通用组件 -->
<template>
  <Breadcrumb />
  <Fullscreen />
  <Pagination />
</template>

项目已关闭 vue/multi-word-component-names 强制校验,新增业务组件仍优先使用多词命名;已有基础组件、布局组件、工具组件可保持当前命名。

参考:Vue 风格指南 - 组件名应该是多词的

Props 定义

typescript
// ✅ 推荐:业务组件优先使用 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 类型声明,上传、封装组件等需要运行时默认值的场景可使用对象声明。

参考:Vue 官方文档 - Props 类型声明

Emits 定义

typescript
// ✅ 正确:类型化的 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 保持一致:templatescriptstyle

vue
<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>

参考:Vue 风格指南 - 组件选项顺序


导入顺序规范

按以下顺序组织导入语句,组间空行分隔:

typescript
// 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";

参考:Airbnb - Importseslint-plugin-import


接口与类型约定

本节约定 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可扩展、错误信息更友好
联合类型typeinterface 不支持
函数类型type语法更简洁
工具类型typePartial<T>, Pick<T, K>
typescript
// ✅ 对象结构用 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">;

参考:TypeScript 官方文档 - Interfaces vs Types

API 响应类型

typescript
// 通用响应结构
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>>;

类型文件组织

typescript
// 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, useTableSelectionVue 官方约定
动词 + 名词useFetchData, useLocalStorage表意清晰
返回对象{ data, loading, error }解构友好
typescript
// ✅ 正确命名
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 命名规范

参数设计

typescript
// ✅ 使用 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 });

返回值设计

typescript
// ✅ 返回响应式引用和方法
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 + 模块名 + StoreuseUserStore
Store IDcamelCase"user", "tagsView"
StatecamelCaseuserInfo, isLoggedIn
GettercamelCasefullName, isAdmin
ActioncamelCase 动词fetchUser, logout

Setup Store 写法(推荐)

typescript
// 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,
  };
});

参考:Pinia 官方文档 - Setup Stores

Store Hook(在 setup 外使用)

typescript
// 定义 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 规范

文件结构

typescript
// 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 方法
分页查询getPageGET
列表查询getListGET
详情查询getFormData / getByIdGET
新增createPOST
修改updatePUT
删除deleteByIdsDELETE
导出exportGET/POST
导入importPOST

注释规范

注释不是越少越好,而是只写有信息量的内容。好的注释补充代码没有直接表达出的背景、意图、约束和边界;坏的注释复述代码、制造噪音,或只起视觉分隔作用。

注释取舍

场景建议写法说明
类型、接口、属性、常量单行 JSDoc说明用途、约束或默认语义
函数、方法、Composable多行 JSDoc说明用途、参数、返回值或约束
业务规则、兼容策略、异常兜底行内注释或块级注释解释为什么这样做
大型配置对象、复杂 Composable 的分组简短普通注释例如 // 面板状态// 菜单数据// 认证
不容易从代码看出的交互约定多行 JSDoc 或行内注释方法级说明用多行 JSDoc,语句级说明用行内注释
函数内部的普通步骤不写注释通过命名、拆分函数和空行表达
纯视觉分隔标题禁止使用不使用横线、等号包裹标题,也不写低价值标题
typescript
// ✅ 合理:分组信息有助于阅读,保持简短
// 面板状态
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 官方规范

  1. 类型、接口、属性、常量等短契约可使用单行 JSDoc,例如 /** 用户状态。 */
  2. 函数、方法、Composable 的说明使用多行 JSDoc,至少包含 /**、摘要行、*/ 三行。
  3. @param / @returns 等标签时,必须多行格式。
  4. 摘要句号结尾,标签后的说明也句号结尾。
  5. 避免文件头、作者、模块标题等重复信息,除非能提供维护者必须知道的背景。
  6. 如果函数不需要说明,就不要硬写注释。
typescript
/**
 * 格式化文件大小。
 *
 * @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]}`;
}

组件注释

vue
<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>

行内注释

行内注释应靠近相关代码,说明原因、约束或例外情况。普通赋值、简单判断、函数调用不需要注释。

typescript
// ✅ 解释"为什么",而非"是什么"
const timeout = 30000; // 后端接口响应较慢,需要较长超时时间

// ✅ 解释例外情况
if (EXCLUDED_PATHS.includes(route.path)) return; // 跳过登录、错误页和重定向页

// ❌ 避免无意义注释
const count = 0; // 设置 count 为 0

Git 提交规范

Commit Message 格式

<type>(<scope>): <subject>

<body>

<footer>

Type 类型

类型说明
feat新功能
fix修复 Bug
docs文档更新
style代码格式(不影响功能)
refactor重构(非新功能、非修复)
perf性能优化
test测试相关
chore构建/工具变动
revert回滚

示例

bash
# 新功能
feat(user): 添加用户导入功能

# 修复
fix(auth): 修复 token 过期后未跳转登录页的问题

# 文档
docs: 更新开发规范文档

# 重构
refactor(store): 使用 setup store 语法重构 user store

参考:Conventional CommitsAngular Commit Guidelines


目录结构规范

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.ts

ESLint / Prettier 配置

项目已配置 ESLint + Prettier,确保代码风格一致:

bash
# 检查代码
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
Composablesuse 前缀 + camelCase
Storeuse + 模块名 + Store
API 方法动词 + 名词
Git CommitConventional Commits

遵循这些规范,可以保证代码的一致性、可读性和可维护性。

基于 MIT 许可发布 · 如需部署协助或二开定制,请查看 支持与合作