Skip to content

代码规范

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

先看什么?

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

参考来源

来源说明
Vue 官方风格指南Vue 组件命名、Props 定义等
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 类名

风格适用场景示例
kebab-case自定义类名.user-card, .nav-menu
BEM复杂组件.user-card__header, .user-card--active
vue
<template>
  <div class="user-card">
    <div class="user-card__header">标题</div>
    <div class="user-card__body user-card__body--highlight">内容</div>
  </div>
</template>

<style lang="scss" scoped>
.user-card {
  &__header {
    /* ... */
  }
  &__body {
    &--highlight {
      /* ... */
    }
  }
}
</style>

参考:BEM 官方文档Element Plus 源码

CSS 变量

scss
// ✅ 正确:使用 -- 前缀,kebab-case
:root {
  --color-primary: #409eff;
  --font-size-base: 14px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
}

// 使用
.button {
  color: var(--color-primary);
  font-size: var(--font-size-base);
}

参考: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-caseuser.ts, api-response.ts社区惯例
测试文件源文件名 + .teststorage.test.tsVitest
样式文件kebab-casevariables.scss, reset.scss社区惯例

Vue 组件文件

src/components/
├── UserCard.vue          # PascalCase(单文件组件)
├── PageHeader/
│   ├── index.vue         # 目录组件入口
│   └── PageHeaderNav.vue # 子组件

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

页面文件

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

API 文件

src/api/
├── system/
│   ├── user.ts           # 用户相关 API
│   ├── role.ts           # 角色相关 API
│   └── menu.ts
├── auth.ts               # 认证 API(单文件不建目录)
├── file.ts
└── types.ts              # 公共类型(可选)

Store 文件

src/store/
├── index.ts
└── modules/
    ├── user.ts           # 用户状态
    ├── app.ts            # 应用状态
    ├── permission.ts     # 权限状态
    ├── settings.ts       # 设置状态
    └── tags-view.ts      # 标签页状态(多词用 kebab-case)

类型文件

类型文件的组织方式有两种主流实践,本项目采用集中式管理

src/types/
├── api/                  # API 相关类型
│   ├── user.ts          # 用户相关类型
│   ├── role.ts          # 角色相关类型
│   └── common.ts        # 公共类型(分页、响应等)
├── ui/                   # UI 相关类型
│   ├── settings.ts      # 设置类型
│   └── tagsview.ts      # 标签页类型
├── index.ts             # 统一导出
└── global.d.ts          # 全局类型声明

两种组织方式对比

方式代表项目优点缺点
集中式Vben Admin、本项目统一管理、便于复用跳转文件多
就近式Element Plus、VueUse改代码时类型就在旁边类型分散

本项目选择集中式的原因

  1. API 类型被多处引用(Store、组件、工具函数)
  2. 便于统一维护和查找
  3. 避免循环引用问题

注:两种方式都是业界认可的实践,没有绝对的对错。选择哪种取决于项目规模和团队偏好。

Composables 文件

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

参考:VueUse 源码结构


组件规范

组件命名

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

<!-- ❌ 避免:单词命名(除非是基础组件如 Button) -->
<template>
  <Card />
  <Header />
</template>

参考: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({
  userId: Number,
  showAvatar: Boolean,
});

参考: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);

组件结构顺序

vue
<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 "@/types";

// 4. 内部模块 - Store
import { useUserStore } from "@/store";

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

<template>
  <!-- 模板内容 -->
</template>

<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 "@/types";

// 4. 内部模块 - 绝对路径(按层级)
import { useUserStore } from "@/store";
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/types/api/user.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/store/modules/user.ts
import { defineStore } from "pinia";
import type { UserInfo } from "@/types";
import AuthAPI from "@/api/auth";

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 "@/store";

export function useUserStoreHook() {
  return useUserStore(store);
}

// 在非 setup 上下文使用(如路由守卫)
import { useUserStoreHook } from "@/store";

router.beforeEach((to, from, next) => {
  const userStore = useUserStoreHook();
  if (!userStore.isLoggedIn) {
    next("/login");
  }
});

API 规范

文件结构

typescript
// src/api/system/user.ts
import request from "@/utils/request";
import type { UserInfo, UserQuery, UserForm, PageResult } from "@/types";

const USER_BASE_URL = "/api/v1/users";

class UserAPI {
  /** 获取用户分页列表 */
  static getPage(params: UserQuery) {
    return request.get<PageResult<UserInfo>>(`${USER_BASE_URL}/page`, { params });
  }

  /** 获取用户详情 */
  static getById(id: number) {
    return request.get<UserInfo>(`${USER_BASE_URL}/${id}`);
  }

  /** 新增用户 */
  static create(data: UserForm) {
    return request.post<void>(USER_BASE_URL, data);
  }

  /** 修改用户 */
  static update(id: number, data: UserForm) {
    return request.put<void>(`${USER_BASE_URL}/${id}`, data);
  }

  /** 删除用户 */
  static deleteByIds(ids: number[]) {
    return request.delete<void>(`${USER_BASE_URL}/${ids.join(",")}`);
  }
}

export default UserAPI;

命名约定

操作方法名HTTP 方法
分页查询getPageGET
列表查询getListGET
详情查询getByIdGET
新增createPOST
修改updatePUT
删除deleteByIdsDELETE
导出exportGET/POST
导入importPOST

注释规范

JSDoc 注释

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; // 后端接口响应较慢,需要较长超时时间

// ❌ 避免无意义注释
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 接口
│   ├── system/            # 系统管理模块
│   │   ├── user.ts
│   │   └── role.ts
│   └── auth.ts            # 认证接口
├── assets/                 # 静态资源
│   ├── images/
│   └── icons/
├── components/             # 公共组件
│   ├── DictSelect/        # 复杂组件用目录
│   │   └── index.vue
│   └── Pagination.vue     # 简单组件单文件
├── composables/            # 组合式函数
│   ├── sse/
│   ├── useRecentMenus.ts
│   └── useTableSelection.ts
├── constants/              # 常量定义
├── directives/             # 自定义指令
├── enums/                  # 枚举定义
├── hooks/                  # 生命周期钩子(可选)
├── layouts/                # 布局组件
├── router/                 # 路由配置
├── store/                  # 状态管理
│   ├── modules/
│   └── index.ts
├── styles/                 # 全局样式
├── types/                  # 类型定义
│   ├── api/
│   └── ui/
├── utils/                  # 工具函数
├── views/                  # 页面组件
│   └── system/
│       └── user/
│           ├── index.vue
│           └── components/
├── App.vue
└── main.ts

ESLint / Prettier 配置

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

bash
# 检查代码
pnpm lint:eslint

# 格式化代码
pnpm lint:prettier

# 检查样式
pnpm lint:stylelint

# 全部检查
pnpm lint

关键规则

  • 使用 2 空格缩进
  • 使用双引号
  • 语句末尾不加分号(Prettier 默认)
  • 组件名必须多词
  • Props 必须有类型定义
  • 未使用的变量报错

总结

类别规范
变量camelCase
常量UPPER_SNAKE_CASE
类/接口/类型PascalCase
文件(TS/JS)kebab-case
文件(Vue 组件)PascalCase
CSS 类名kebab-case / BEM
Composablesuse 前缀 + camelCase
Storeuse + 模块名 + Store
API 方法动词 + 名词
Git CommitConventional Commits

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

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