开发规范
本文档定义 vue3-element-admin 项目的编码规范,基于 Vue 官方风格指南、TypeScript 最佳实践及业界标杆项目经验整理。
参考来源
| 来源 | 说明 |
|---|---|
| Vue 官方风格指南 | Vue 组件命名、Props 定义等 |
| 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 |
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: number;
data: T;
message: string;
};布尔值命名
使用 is、has、can、should 等前缀,表意清晰。
typescript
// ✅ 正确
const isLoading = ref(false);
const hasPermission = computed(() => /* ... */);
const canEdit = ref(true);
const shouldRefresh = ref(false);
// ❌ 避免
const loading = ref(false); // 不够明确
const permission = ref(true); // 名词,不是布尔语义方法命名规范(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 状态变更。
- 业务动作用语义动词(
命名约定表(推荐)
| 场景 | 推荐命名 | 示例 | 说明 |
|---|---|---|---|
| 打开/关闭弹窗/抽屉 | openX / closeX / toggleX | openUserModal() / closeUserDrawer() | open/close 明确动作;toggle 用于切换 |
| 显示/隐藏(UI 表现) | showX / hideX | showToast() / hideTooltip() | 更偏展示层(toast/tooltip) |
| 表单提交/确认/保存(业务动作) | submitX / confirmX / saveX | submitUserForm() / saveDraft() | 业务动作命名,submit 多为提交到后端;save 常用于本地或草稿 |
| 查询/加载数据 | fetchX / loadX / searchX | fetchUserList() / loadDeptOptions() | fetch/load 表示异步取数;search 强调关键字 |
| 筛选/重置筛选 | applyFilter(s) / resetFilter(s) | applyFilters() / resetFilters() | 和查询行为分离,便于复用 |
| 新增/编辑/删除 | createX / updateX / deleteX | createUser() / deleteUser(id) | 比 handleDeleteClick 更语义化 |
| 批量操作 | batchX / batchDeleteX | batchDeleteUsers() | 明确“批量”语义 |
| 状态切换 | enableX / disableX / toggleXStatus | toggleUserStatus() | 避免 change 过于泛化 |
| 事件入口/流程编排(可选) | handleX / onX | handleSubmit() / onRowClick() | 当函数负责编排多个动作时使用,通常会调用 submitX/openX/closeX/fetchX 等业务动作 |
| Composables | useX | usePagination() / useDialog() | Vue 社区约定 |
常见争议与建议
handleX/onXvs 直接动词- 推荐:优先用有业务语义的动词(
deleteUser、exportUsers)。 - 使用
handle/on的场景:当它只负责“桥接 UI 事件 + 组织流程”,例如handleSubmit内部调用validateForm、submitUserForm、closeDialog。
- 推荐:优先用有业务语义的动词(
异步方法是否加
Async后缀fetch/load/search本身已隐含异步语义,一般不必再加Async。- 只有在同名同步/异步同时存在且易混淆时,才考虑
xxxAsync。
示例(Composition API)
typescript
const isUserModalOpen = ref(false);
const isLoading = ref(false);
function openUserModal() {
isUserModalOpen.value = true;
}
function closeUserModal() {
isUserModalOpen.value = false;
}
async function fetchUserList() {
// ...
}
async function submitUserForm(payload: any) {
// ...
}
async function deleteUser(id: number) {
// ...
}
function handleSubmit() {
// 仅作为事件入口/流程编排时使用 handle
// validateForm() -> submitUserForm() -> closeUserModal() -> fetchUserList()
}示例(弹窗/抽屉的分层)
弹窗场景建议把“开关动作”和“事件入口”区分开:
typescript
const isUserDialogOpen = ref(false);
function openUserDialog() {
isUserDialogOpen.value = true;
}
function closeUserDialog() {
isUserDialogOpen.value = false;
}
async function handleCreateClick() {
// resetFormData() / loadOptions() ...
openUserDialog();
}
async function handleEditClick(id: number) {
// await fetchUserDetail(id) / fillFormData() ...
openUserDialog();
}
async function handleDialogConfirm() {
// validateForm() -> submitUserForm() -> closeUserDialog() -> fetchUserList()
}避免: 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>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);
}文件命名规范
总览
| 文件类型 | 命名风格 | 示例 | 参考 |
|---|---|---|---|
| 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 | user.ts, api-response.ts | 社区惯例 |
| 测试文件 | 源文件名 + .test | storage.test.ts | Vitest |
| 样式文件 | kebab-case | variables.scss, reset.scss | 社区惯例 |
Vue 组件文件
src/components/
├── UserCard.vue # PascalCase(单文件组件)
├── PageHeader/
│ ├── index.vue # 目录组件入口
│ └── PageHeaderNav.vue # 子组件页面文件
src/views/
├── dashboard/
│ └── index.vue # 页面入口
├── system/
│ ├── user/
│ │ ├── index.vue # 用户列表页
│ │ └── components/ # 页面私有组件
│ │ └── UserForm.vue
│ └── role/
│ └── index.vueAPI 文件
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 | 改代码时类型就在旁边 | 类型分散 |
本项目选择集中式的原因:
- API 类型被多处引用(Store、组件、工具函数)
- 便于统一维护和查找
- 避免循环引用问题
注:两种方式都是业界认可的实践,没有绝对的对错。选择哪种取决于项目规模和团队偏好。
Composables 文件
src/composables/
├── table/
│ └── useTableSelection.ts # use 前缀
├── websocket/
│ ├── index.ts # 统一导出
│ ├── useStomp.ts
│ ├── useDictSync.ts
│ └── useOnlineCount.ts
└── index.ts # 统一导出参考:VueUse 源码结构
组件规范
组件命名
vue
<!-- ✅ 正确:PascalCase,多词命名 -->
<template>
<UserCard />
<PageHeader />
<DictSelect />
</template>
<!-- ❌ 避免:单词命名(除非是基础组件如 Button) -->
<template>
<Card />
<Header />
</template>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,
});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>导入顺序规范
按以下顺序组织导入语句,组间空行分隔:
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";接口与类型约定
本节约定 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> |
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">;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, useTableSelection | Vue 官方约定 |
| 动词 + 名词 | 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 + 模块名 + Store | useUserStore |
| Store ID | camelCase | "user", "tagsView" |
| State | camelCase | userInfo, isLoggedIn |
| Getter | camelCase | fullName, isAdmin |
| Action | camelCase 动词 | 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,
};
});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 方法 |
|---|---|---|
| 分页查询 | getPage | GET |
| 列表查询 | getList | GET |
| 详情查询 | getById | GET |
| 新增 | create | POST |
| 修改 | update | PUT |
| 删除 | deleteByIds | DELETE |
| 导出 | export | GET/POST |
| 导入 | import | POST |
注释规范
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 为 0Git 提交规范
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目录结构规范
src/
├── api/ # API 接口
│ ├── system/ # 系统管理模块
│ │ ├── user.ts
│ │ └── role.ts
│ └── auth.ts # 认证接口
├── assets/ # 静态资源
│ ├── images/
│ └── icons/
├── components/ # 公共组件
│ ├── DictSelect/ # 复杂组件用目录
│ │ └── index.vue
│ └── Pagination.vue # 简单组件单文件
│ └── Pagination.vue # 简单组件单文件
├── composables/ # 组合式函数
│ ├── table/
│ └── websocket/
├── constants/ # 常量定义
├── directives/ # 自定义指令
├── enums/ # 枚举定义
├── hooks/ # 生命周期钩子(可选)
├── layouts/ # 布局组件
├── router/ # 路由配置
├── store/ # 状态管理
│ ├── modules/
│ └── index.ts
├── styles/ # 全局样式
├── types/ # 类型定义
│ ├── api/
│ └── ui/
├── utils/ # 工具函数
├── views/ # 页面组件
│ └── system/
│ └── user/
│ ├── index.vue
│ └── components/
├── App.vue
└── main.tsESLint / 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 |
| Composables | use 前缀 + camelCase |
| Store | use + 模块名 + Store |
| API 方法 | 动词 + 名词 |
| Git Commit | Conventional Commits |
遵循这些规范,可以保证代码的一致性、可读性和可维护性。
