vue-admin

SKILL.md

Vue3 管理后台开发规范

触发条件

  • 开发 Vue3 管理后台项目
  • 实现 CRUD 页面
  • 使用 Element Plus 组件
  • 实现权限管理
  • 创建自定义组件

Part 1: 技术栈

技术 版本 用途
Vue 3.4+ 前端框架
Element Plus 2.5+ UI 组件库
TypeScript 5.0+ 类型安全
Vite 5.0+ 构建工具
Pinia 2.1+ 状态管理
Vue Router 4.2+ 路由
Axios 1.6+ HTTP 客户端

Part 2: 目录结构

src/
├── api/                    # API 请求
│   ├── system/             # 系统模块 API
│   │   ├── user/           # 用户模块
│   │   │   ├── index.ts    # API 定义
│   │   │   └── types.ts    # 类型定义
│   │   └── role/           # 角色模块
│   └── index.ts            # API 统一导出
├── components/             # 全局组件
│   ├── Form/               # 表单组件
│   │   ├── UserSelect.vue
│   │   └── DeptTree.vue
│   ├── Table/              # 表格组件
│   │   └── ProTable.vue
│   └── Dialog/             # 弹窗组件
│       └── FormDialog.vue
├── composables/            # 组合式函数
│   ├── useTable.ts         # 表格逻辑
│   ├── useForm.ts          # 表单逻辑
│   └── useDialog.ts        # 弹窗逻辑
├── directives/             # 自定义指令
│   ├── permission.ts       # 权限指令
│   └── loading.ts          # 加载指令
├── enums/                  # 枚举定义
│   ├── ResultCode.ts       # 响应码
│   └── MenuType.ts         # 菜单类型
├── hooks/                  # 自定义 hooks
│   ├── useLoading.ts
│   └── usePermission.ts
├── layout/                 # 布局组件
│   ├── components/
│   │   ├── Sidebar.vue
│   │   ├── Navbar.vue
│   │   └── AppMain.vue
│   └── index.vue
├── router/                 # 路由配置
│   ├── index.ts
│   ├── staticRoutes.ts     # 静态路由
│   └── dynamicRoutes.ts    # 动态路由
├── stores/                 # Pinia 状态
│   ├── modules/
│   │   ├── user.ts
│   │   ├── permission.ts
│   │   └── app.ts
│   └── index.ts
├── styles/                 # 全局样式
│   ├── variables.scss      # CSS 变量
│   ├── reset.scss          # 重置样式
│   └── index.scss          # 入口
├── utils/                  # 工具函数
│   ├── request.ts          # Axios 封装
│   ├── auth.ts             # Token 管理
│   ├── permission.ts       # 权限判断
│   └── format.ts           # 格式化
└── views/                  # 页面组件
    ├── system/
    │   ├── user/
    │   │   ├── index.vue       # 列表页
    │   │   ├── components/     # 页面组件
    │   │   │   └── UserForm.vue
    │   │   └── hooks/          # 页面 hooks
    │   │       └── useUser.ts
    │   └── role/
    └── dashboard/

Part 3: 命名规范

文件命名

类型 规范 示例
组件 PascalCase UserSelect.vue
页面 kebab-case user-profile.vue
组合式函数 camelCase + use useUserStore.ts
工具函数 camelCase formatDate.ts
常量 UPPER_SNAKE_CASE API_PREFIX.ts
类型 PascalCase UserVO.ts

变量命名

类型 规范 示例
变量 camelCase userList
常量 UPPER_SNAKE_CASE MAX_COUNT
私有变量 _ 前缀 _internalState
布尔值 is/has/can 前缀 isLoading, hasPermission

方法命名

动作 前缀 示例
获取/加载 fetch/load fetchUserList
提交/保存 submit/save submitForm
新增 create createUser
更新 update updateUser
删除 delete deleteUser
打开/关闭 open/close openDialog
事件处理 handle handleSubmit

handle 使用规则:

类型 特点 是否用 handle
单一动作 只做一件事
流程编排 组合多个动作
// ✅ 单一动作 → 不用 handle
function openDialog() {
  dialogState.visible = true;
}

// ✅ 流程编排 → 使用 handle
async function handleSubmit() {
  const { valid } = await formRef.value.validate();
  if (!valid) return;
  await submitForm();
  closeDialog();
}

CSS 命名(BEM)

Block__Element--Modifier
.user-card {
  padding: 16px;
  background: var(--el-bg-color);
  border-radius: 8px;

  &__header {
    display: flex;
    align-items: center;
  }

  &__avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
  }

  &__name {
    font-size: 16px;
    font-weight: 600;
  }

  &--active {
    border: 2px solid var(--el-color-primary);
  }
}

Part 4: 组件规范

组件优先原则

需求 使用 避免
按钮 <el-button> 自定义 .btn
输入框 <el-input> 自定义 .input
表格 <el-table> 自定义表格
弹窗 <el-dialog> 自定义弹窗
表单 <el-form> 自定义表单

CRUD 页面模板

<template>
  <div class="app-container">
    <!-- 搜索栏 -->
    <el-form :model="queryParams" ref="queryFormRef" :inline="true">
      <el-form-item label="关键词" prop="keywords">
        <el-input
          v-model="queryParams.keywords"
          placeholder="请输入"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">
          <i-ep-search />搜索
        </el-button>
        <el-button @click="handleReset">
          <i-ep-refresh />重置
        </el-button>
      </el-form-item>
    </el-form>

    <!-- 操作栏 -->
    <el-row :gutter="10" class="mb-[10px]">
      <el-col :span="1.5">
        <el-button type="primary" v-permission="['sys:user:add']" @click="handleAdd">
          <i-ep-plus />新增
        </el-button>
      </el-col>
    </el-row>

    <!-- 数据表格 -->
    <el-table v-loading="loading" :data="tableData" border>
      <el-table-column label="ID" prop="id" width="80" />
      <el-table-column label="用户名" prop="username" min-width="120" />
      <el-table-column label="状态" prop="status" width="100">
        <template #default="{ row }">
          <el-tag :type="row.status === 1 ? 'success' : 'danger'">
            {{ row.status === 1 ? '正常' : '禁用' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="{ row }">
          <el-button link type="primary" v-permission="['sys:user:edit']" @click="handleEdit(row)">
            编辑
          </el-button>
          <el-button link type="danger" v-permission="['sys:user:delete']" @click="handleDelete(row)">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <el-pagination
      v-model:current-page="queryParams.pageNum"
      v-model:page-size="queryParams.pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next, jumper"
      @change="handleQuery"
    />
  </div>
</template>

<script setup lang="ts">
import type { UserVO, UserPageQuery } from "@/api/system/user/types";

defineOptions({ name: "UserList" });

const loading = ref(false);
const tableData = ref<UserVO[]>([]);
const total = ref(0);
const queryParams = reactive<UserPageQuery>({
  pageNum: 1,
  pageSize: 20,
  keywords: "",
});

function handleQuery() {
  loading.value = true;
  UserAPI.getPage(queryParams)
    .then(({ data }) => {
      tableData.value = data.list;
      total.value = data.total;
    })
    .finally(() => {
      loading.value = false;
    });
}

function handleReset() {
  queryFormRef.value?.resetFields();
  handleQuery();
}

function handleAdd() {
  // 打开新增弹窗
}

function handleEdit(row: UserVO) {
  // 打开编辑弹窗
}

async function handleDelete(row: UserVO) {
  const { confirm } = await ElMessageBox.confirm("确认删除该用户?", "警告", {
    type: "warning",
  });
  if (confirm) {
    await UserAPI.deleteById(row.id);
    ElMessage.success("删除成功");
    handleQuery();
  }
}

onMounted(() => handleQuery());
</script>

Part 5: API 规范

API 定义模板

// api/system/user/types.ts
export interface UserVO {
  id: number;
  username: string;
  nickname: string;
  mobile: string;
  email: string;
  status: number;
  createTime: string;
}

export interface UserForm {
  id?: number;
  username: string;
  nickname: string;
  mobile: string;
  email: string;
  status: number;
  roleIds: number[];
}

export interface UserPageQuery extends PageQuery {
  username?: string;
  status?: number;
  deptId?: number;
}

// api/system/user/index.ts
import request from "@/utils/request";
import type { UserVO, UserForm, UserPageQuery } from "./types";

class UserAPI {
  /** 获取用户分页列表 */
  static getPage(params: UserPageQuery) {
    return request<PageResult<UserVO>>({
      url: "/api/v1/users/page",
      method: "get",
      params,
    });
  }

  /** 获取用户详情 */
  static getById(id: number) {
    return request<Result<UserVO>>({
      url: `/api/v1/users/${id}`,
      method: "get",
    });
  }

  /** 获取用户表单数据 */
  static getFormData(id: number) {
    return request<Result<UserForm>>({
      url: `/api/v1/users/${id}/form`,
      method: "get",
    });
  }

  /** 新增用户 */
  static create(data: UserForm) {
    return request<Result<number>>({
      url: "/api/v1/users",
      method: "post",
      data,
    });
  }

  /** 更新用户 */
  static update(data: UserForm) {
    return request<Result<void>>({
      url: "/api/v1/users",
      method: "put",
      data,
    });
  }

  /** 删除用户 */
  static deleteById(id: number) {
    return request<Result<void>>({
      url: `/api/v1/users/${id}`,
      method: "delete",
    });
  }

  /** 批量删除用户 */
  static batchDelete(ids: number[]) {
    return request<Result<void>>({
      url: "/api/v1/users/batch",
      method: "delete",
      data: { ids },
    });
  }

  /** 获取用户选项列表 */
  static getOptions() {
    return request<Result<OptionType[]>>({
      url: "/api/v1/users/options",
      method: "get",
    });
  }
}

export default UserAPI;

响应格式

// 标准响应
interface Result<T> {
  code: number;
  msg: string;
  data: T;
}

// 分页响应
interface PageResult<T> {
  code: number;
  msg: string;
  data: {
    list: T[];
    total: number;
  };
}

// 分页查询参数
interface PageQuery {
  pageNum: number;
  pageSize: number;
  keywords?: string;
}

// 选项类型
interface OptionType {
  value: string | number;
  label: string;
  children?: OptionType[];
}

Part 6: 权限控制

路由守卫

// router/guard.ts
import { useUserStore } from "@/stores/modules/user";

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();

  // 登录页直接放行
  if (to.path === "/login") {
    return next();
  }

  // 未登录跳转登录页
  if (!userStore.token) {
    return next(`/login?redirect=${to.path}`);
  }

  // 已加载路由直接放行
  if (userStore.routes.length > 0) {
    return next();
  }

  // 加载动态路由
  await userStore.loadRoutes();
  next({ ...to, replace: true });
});

按钮权限

<template>
  <!-- 指令方式 -->
  <el-button v-permission="['sys:user:add']" type="primary">新增</el-button>

  <!-- 函数方式 -->
  <el-button v-if="hasPermission(['sys:user:edit'])">编辑</el-button>
</template>

<script setup lang="ts">
import { hasPermission } from "@/utils/permission";
</script>

权限指令

// directives/permission.ts
import { useUserStore } from "@/stores/modules/user";

export const permission: Directive = {
  mounted(el, binding) {
    const userStore = useUserStore();
    const permissions = binding.value as string[];

    const hasPermission = permissions.some((p) =>
      userStore.permissions.includes(p)
    );

    if (!hasPermission) {
      el.parentNode?.removeChild(el);
    }
  },
};

Part 7: 代码质量检查清单

  • 使用 TypeScript 严格模式
  • 为所有变量和函数定义类型
  • 优先使用 Element Plus 组件
  • 遵循命名规范
  • 为公共函数添加 JSDoc 注释
  • 处理加载和错误状态
  • 实现权限检查
  • 使用组合式函数复用逻辑
  • 组件保持在 300 行以内
  • 使用 <script setup> 语法
  • API 按模块组织
  • 使用 BEM 命名 CSS
Weekly Installs
3
GitHub Stars
3
First Seen
13 days ago
Installed on
amp3
cline3
opencode3
cursor3
kimi-cli3
warp3