Files
laravel_swoole/.clinerules/admin-rule.md
2026-02-11 15:49:19 +08:00

35 KiB
Raw Permalink Blame History

Vue3 后台管理项目开发规范

项目概述

本项目是一个基于 Vue3 的后台管理系统,采用现代化技术栈构建,提供高效、美观的管理界面。

技术栈

  • Vue 3: 渐进式 JavaScript 框架
  • Vite: 下一代前端构建工具
  • Ant Design Vue: 基于 Vue 3 的 UI 组件库
  • Vue Router: Vue.js 官方路由管理器
  • Pinia: Vue 3 官方状态管理库
  • Axios: HTTP 客户端
  • JavaScript: 主要开发语言(非 TypeScript
  • Composition API: 组合式 API 开发模式

图标系统

项目采用 Ant Design Vue 图标库,已全局引入:

  • Ant Design Vue Icons: Ant Design Vue 官方图标库

重要提示: 图标已全局默认引入,开发时请勿重复引入或按需引入,直接使用即可。

开发规范

1. 项目结构

resources/admin/
├── src/
│   ├── api/                    # API 接口层
│   │   ├── auth.js            # 认证相关接口
│   │   ├── menu.js            # 菜单接口
│   │   └── system.js          # 系统相关接口
│   ├── assets/                 # 静态资源
│   │   ├── images/            # 图片资源
│   │   └── style/             # 全局样式
│   ├── components/             # 公共组件
│   │   ├── scEditor/          # 富文本编辑器
│   │   ├── scForm/            # 表单组件
│   │   ├── scIconPicker/      # 图标选择器
│   │   ├── scTable/           # 表格组件
│   │   └── scUpload/          # 上传组件
│   ├── config/                 # 配置文件
│   │   ├── index.js           # 主配置
│   │   ├── routes.js          # 路由配置
│   │   └── upload.js          # 上传配置
│   ├── hooks/                  # 组合式 API Hooks
│   │   ├── useI18n.js         # 国际化 Hook
│   │   └── useTable.js        # 表格 Hook
│   ├── i18n/                   # 国际化配置
│   │   ├── index.js           # i18n 配置
│   │   └── locales/           # 语言包
│   ├── layouts/                # 布局组件
│   │   ├── components/        # 布局子组件
│   │   ├── other/             # 其他布局
│   │   └── index.vue          # 主布局
│   ├── pages/                  # 页面组件
│   │   ├── auth/              # 认证页面
│   │   │   ├── users/         # 用户管理
│   │   │   │   ├── index.vue  # 主页面
│   │   │   │   └── components/ # 页面私有组件
│   │   │   │       ├── SaveDialog.vue      # 新增/编辑弹窗
│   │   │   │       ├── RoleDialog.vue      # 角色设置弹窗
│   │   │   │       └── DetailDialog.vue    # 详情弹窗
│   │   │   ├── roles/         # 角色管理
│   │   │   │   ├── index.vue
│   │   │   │   └── components/
│   │   │   │       ├── SaveDialog.vue
│   │   │   │       └── PermissionDialog.vue
│   │   │   └── ...
│   │   ├── home/              # 首页
│   │   ├── login/             # 登录页
│   │   ├── system/            # 系统管理
│   │   └── ucenter/           # 个人中心
│   ├── router/                 # 路由配置
│   │   ├── index.js           # 主路由
│   │   └── systemRoutes.js    # 系统路由
│   ├── stores/                 # 状态管理
│   │   ├── index.js           # Store 入口
│   │   ├── persist.js         # 持久化配置
│   │   └── modules/           # Store 模块
│   ├── utils/                  # 工具函数
│   │   ├── request.js         # Axios 封装
│   │   └── tool.js            # 工具函数
│   ├── App.vue                 # 根组件
│   ├── boot.js                 # 引导文件
│   ├── main.js                 # 入口文件
│   └── style.css               # 全局样式
├── public/                     # 公共资源
├── index.html                  # HTML 模板
├── package.json                # 依赖配置
├── vite.config.js             # Vite 配置
└── README.md                   # 项目说明

2. 页面组件开发规范

页面组件目录结构

每个页面模块应遵循以下目录结构:

pages/
└── {module}/                # 模块目录(如 auth, system 等)
    └── {resource}/         # 资源目录(如 users, roles 等)
        ├── index.vue       # 主页面(列表页)
        └── components/      # 页面私有组件目录
            ├── SaveDialog.vue       # 新增/编辑弹窗(字段较少时使用)
            ├── SaveDrawer.vue       # 新增/编辑抽屉(字段较多时使用)
            ├── DetailDialog.vue     # 详情弹窗
            ├── RoleDialog.vue       # 角色设置弹窗
            ├── SearchDrawer.vue    # 高级搜索抽屉
            └── ...                  # 其他页面专用组件

目录结构说明:

  • index.vue: 页面的主入口组件,通常是列表展示页面
  • components/: 存放该页面专用的私有组件
    • 弹窗/抽屉组件:根据表单字段数量选择使用弹窗或抽屉
      • 字段较少3-5个使用 *Dialog.vue 弹窗组件
      • 字段较多5个以上使用 *Drawer.vue 抽屉组件
    • SaveDialog.vue / SaveDrawer.vue: 用于新增和编辑数据的表单组件
    • DetailDialog.vue: 用于查看数据详情的弹窗
    • SearchDrawer.vue: 用于高级搜索条件的抽屉组件
    • 其他与该页面紧密关联的组件

组件命名规范:

  • 页面私有组件使用 PascalCase 命名
  • 建议以功能命名:
    • 表单类:SaveDialog.vue, SaveDrawer.vue, EditDialog.vue
    • 功能类:RoleDialog.vue, PermissionDialog.vue
    • 搜索类:SearchDrawer.vue
  • 避免 Dialog.vueDrawer.vue 这样过于通用的命名

页面布局规范

基本布局要求:

  1. 使用 Flex 布局: 页面容器必须使用 Flex 布局,确保占满全部内容区域
  2. 表格高度自适应: 表格使用封装的 scTable 组件,高度占满剩余内容空间

标准页面布局模板:

<template>
  <div class="pages {module}-{resource}-page">
    <!-- 工具栏区域 -->
    <div class="tool-bar">
      <div class="left-panel">
        <!-- 搜索表单 -->
      </div>
      <div class="right-panel">
        <!-- 操作按钮 -->
      </div>
    </div>
    
    <!-- 表格内容区域 -->
    <div class="table-content">
      <scTable
        :columns="columns"
        :data-source="tableData"
        :loading="loading"
        :pagination="pagination"
        :row-key="rowKey"
        @refresh="refreshTable"
        @paginationChange="handlePaginationChange"
      />
    </div>
  </div>
</template>

<style scoped lang="scss">
.{module}-{resource}-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 0;

  .tool-bar {
    // 工具栏样式
  }

  .table-content {
    flex: 1;
    overflow: hidden;
  }
}
</style>

布局样式说明:

  • 页面根元素使用 display: flexflex-direction: column 实现垂直布局
  • height: 100% 确保页面占满父容器高度
  • padding: 0 去除内边距(由布局组件控制外边距)
  • table-content 使用 flex: 1 占据剩余空间
  • overflow: hidden 防止出现滚动条(表格内部自己处理滚动)

侧边栏布局示例 (如用户管理页面):

<template>
  <div class="pages user-page">
    <!-- 左侧部门树 -->
    <div class="left-box">
      <div class="header">
        <!-- 搜索框 -->
      </div>
      <div class="body">
        <!-- 树形组件 -->
      </div>
    </div>
    
    <!-- 右侧内容区 -->
    <div class="right-box">
      <!-- 工具栏 -->
      <div class="tool-bar">...</div>
      
      <!-- 表格 -->
      <div class="table-content">...</div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.user-page {
  display: flex;
  flex-direction: row;
  height: 100%;
  padding: 0;

  .left-box {
    width: 260px;
    border-right: 1px solid #f0f0f0;
    display: flex;
    flex-direction: column;
    background: #fff;

    .header {
      padding: 12px 16px;
      border-bottom: 1px solid #f0f0f0;
      background: #fafafa;
    }

    .body {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
    }
  }

  .right-box {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;

    .table-content {
      flex: 1;
      overflow: hidden;
      background: #f5f5f5;
    }
  }
}
</style>

列表搜索区域规范

搜索区域设计原则:

  1. 精简显示: 搜索条件不全部显示,只显示最常用的 1-2 项
  2. 去除 label 属性: 搜索输入框不使用 label 属性,通过 placeholder 直接说明用途,保持界面简洁
  3. 优先使用表格筛选: 对于状态等枚举类型的筛选,优先使用 Ant Design Vue Table 的自定义过滤器功能
  4. 高级搜索抽屉: 其他复杂搜索条件使用抽屉弹出(需要在页面 components 目录下创建 SearchDrawer.vue 组件)
  5. 保持界面整洁: 避免搜索区域占用过多空间

实现方式:

方式一: 使用表格自定义筛选(推荐)

<template>
  <div class="pages-base-layout role-page">
    <div class="tool-bar">
      <div class="left-panel">
        <a-space>
          <a-input v-model:value="searchForm.keyword" placeholder="角色名称" allow-clear style="width: 180px" />
          <a-button type="primary" @click="handleSearch">
            <template #icon><search-outlined /></template>
            搜索
          </a-button>
          <a-button @click="handleReset">
            <template #icon><redo-outlined /></template>
            重置
          </a-button>
        </a-space>
      </div>
      <div class="right-panel">
        <a-button type="primary" @click="handleAdd">
          <template #icon><plus-outlined /></template>
          新增
        </a-button>
      </div>
    </div>
    <div class="table-content">
      <scTable
        :columns="columns"
        :data-source="tableData"
        :loading="loading"
        :pagination="pagination"
        @refresh="refreshTable"
        @paginationChange="handlePaginationChange"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useTable } from '@/hooks/useTable'

// 使用 useTable Hook
const {
  tableRef,
  searchForm,
  tableData,
  loading,
  pagination,
  handleSearch,
  handleReset,
  handlePaginationChange,
  refreshTable
} = useTable({
  api: api.roles.list.get,
  searchForm: {
    keyword: '',
    status: null
  },
  columns: [],
  needPagination: true
})

// 表格列配置(包含筛选器)
const columns = [
  { 
    title: '角色名称', 
    dataIndex: 'name', 
    key: 'name', 
    width: 200 
  },
  { 
    title: '描述', 
    dataIndex: 'description', 
    key: 'description', 
    ellipsis: true 
  },
  { 
    title: '状态', 
    dataIndex: 'status', 
    key: 'status', 
    width: 100, 
    align: 'center',
    filters: [
      { text: '正常', value: 1 },
      { text: '禁用', value: 0 }
    ],
    filterMultiple: false,
    slot: 'status'
  },
  // ...其他列
]
</script>

方式二: 使用高级搜索抽屉(复杂搜索条件)

<template>
  <div class="pages-base-layout user-page">
    <div class="tool-bar">
      <div class="left-panel">
        <a-space>
          <a-input v-model:value="searchForm.username" placeholder="用户名" allow-clear style="width: 140px" />
          <a-button type="primary" @click="handleSearch">搜索</a-button>
          <a-button @click="handleReset">重置</a-button>
          <a-button @click="showAdvancedSearch = true">高级搜索</a-button>
        </a-space>
      </div>
    </div>
    
    <div class="table-content">
      <scTable
        :columns="columns"
        :data-source="tableData"
        :loading="loading"
        :pagination="pagination"
        @refresh="refreshTable"
        @paginationChange="handlePaginationChange"
      />
    </div>
  </div>

  <!-- 高级搜索抽屉 -->
  <SearchDrawer
    v-model:visible="showAdvancedSearch"
    :form-data="searchForm"
    @confirm="handleAdvancedSearch"
  />
</template>

<script setup>
import { ref } from 'vue'
import { useTable } from '@/hooks/useTable'
import SearchDrawer from './components/SearchDrawer.vue'

const showAdvancedSearch = ref(false)

const handleAdvancedSearch = (formData) => {
  // 更新搜索表单数据
  Object.assign(searchForm, formData)
  showAdvancedSearch.value = false
  handleSearch()
}
</script>

SearchDrawer.vue 组件示例:

<template>
  <a-drawer v-model:open="visible" title="高级搜索" placement="right" width="400">
    <a-form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
      <a-form-item label="姓名">
        <a-input v-model:value="formData.real_name" placeholder="请输入姓名" allow-clear />
      </a-form-item>
      <a-form-item label="邮箱">
        <a-input v-model:value="formData.email" placeholder="请输入邮箱" allow-clear />
      </a-form-item>
      <a-form-item label="手机号">
        <a-input v-model:value="formData.phone" placeholder="请输入手机号" allow-clear />
      </a-form-item>
      <a-form-item label="状态">
        <a-select v-model:value="formData.status" placeholder="请选择状态" allow-clear>
          <a-select-option :value="1">正常</a-select-option>
          <a-select-option :value="0">禁用</a-select-option>
        </a-select>
      </a-form-item>
    </a-form>
    <template #footer>
      <a-space>
        <a-button @click="handleCancel">取消</a-button>
        <a-button type="primary" @click="handleConfirm">确定</a-button>
      </a-space>
    </template>
  </a-drawer>
</template>

<script setup>
defineProps({
  visible: Boolean,
  formData: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:visible', 'confirm'])

const handleCancel = () => {
  emit('update:visible', false)
}

const handleConfirm = () => {
  emit('confirm', { ...formData })
}
</script>

搜索条件选择建议:

常用搜索条件(显示在工具栏):

  • 主要关键字搜索(如用户名、角色名称)
  • 无需筛选的简单字段

表格筛选(推荐用于):

  • 状态、类型等枚举值字段
  • 不需要组合条件的单一筛选

高级搜索抽屉(用于):

  • 次要字段(如邮箱、手机号)
  • 日期范围筛选
  • 多条件组合筛选
  • 复杂的搜索逻辑

scTable 组件筛选器优化说明:

scTable 组件已支持 Ant Design Vue Table 的筛选功能,使用方式:

  1. columns 配置中添加 filters 属性
  2. 通过 @filterChange 事件处理筛选变化
  3. 将筛选条件合并到搜索表单中提交
// 在 useTable Hook 中处理筛选变化
const handleFilterChange = (pagination, filters) => {
  // 将筛选条件添加到搜索表单
  if (filters.status) {
    searchForm.status = filters.status[0] // filterMultiple: false 时取第一个值
  }
  handleSearch()
}

3. 组件开发规范

组件命名

  • 单文件组件: 使用 PascalCase 命名,如 UserList.vue
  • 公共组件: 以 sc 开头,如 scTable.vue
  • 页面私有组件: 使用语义化命名,如 SaveDialog.vue, RoleDialog.vue

组件结构

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

<script setup>
// 导入
import { ref, computed, onMounted } from 'vue'

// 响应式数据
const state = ref({})

// 计算属性
const computedValue = computed(() => {})

// 方法
const handleAction = () => {}

// 生命周期
onMounted(() => {})
</script>

<style scoped>
/* 样式 */
</style>

4. API 接口开发规范

API 文件组织

每个业务模块对应一个 API 文件,统一放在 src/api/ 目录下。

// src/api/auth.js
import request from '@/utils/request'

export default {
  // 认证相关
  login: {
    post: async function (params) {
      return await request.post('auth/login', params)
    },
  },
  logout: {
    post: async function () {
      return await request.post('auth/logout')
    },
  },
  me: {
    get: async function () {
      return await request.get('auth/me')
    },
  },

  // 权限和菜单
  permissions: {
    menu: {
      get: async function () {
        return await request.get('permissions/menu')
      },
    },
    tree: {
      get: async function () {
        return await request.get('permissions/tree')
      },
    },
  },
}

使用示例

import authApi from '@/api/auth'

const login = async () => {
  try {
    const res = await authApi.login.post({
      username: 'admin',
      password: '123456'
    })
    // 处理响应
  } catch (error) {
    // 处理错误
  }
}

// 获取用户菜单
const getMenu = async () => {
  const res = await authApi.permissions.menu.get()
  return res.data
}

5. 路由开发规范

路由类型

项目包含两类路由:

  1. 静态路由: 定义在 src/router/systemRoutes.js 中的基础路由如登录页、404 页面等
  2. 动态路由: 用户登录后,通过 API 获取菜单数据,动态添加到路由中

静态路由定义

// src/router/systemRoutes.js
export default [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/pages/login/index.vue'),
    meta: {
      title: '登录',
      hidden: true
    }
  },
  {
    path: '/',
    name: 'Layout',
    component: () => import('@/layouts/index.vue'),
    redirect: '/dashboard',
    children: [
      // 动态路由将被添加到这里
    ]
  }
]

动态路由加载

用户登录后,系统会自动执行以下流程:

  1. 调用后端 API 获取用户菜单和权限信息
  2. 将菜单数据转换为路由格式
  3. 将动态路由添加到路由器中
  4. 生成菜单树用于侧边栏展示

路由元信息 (meta):

  • title: 页面标题
  • icon: 菜单图标(使用 Ant Design Vue 图标名称)
  • hidden: 是否隐藏菜单
  • noAuth: 是否不需要认证
  • keepAlive: 是否缓存页面
  • affix: 是否固定标签页

后端菜单数据格式:

菜单权限节点数据结构如下:

字段 类型 说明
id number 权限ID
title string 权限标题(用于菜单显示)
name string 权限编码(唯一标识)
type string 权限类型menu菜单、api接口、button按钮、url链接
parent_id number 父级ID顶级菜单为 0
path string 路由路径(如 /system/users
component string 前端组件路径(如 system/users/index
meta object 元数据(包含 icon、hidden、keepAlive 等)
sort number 排序
status number 状态1启用 0禁用
children array 子权限列表(树形结构)

菜单节点规范:

  1. 顶级菜单parent_id=0不需要 component 值

    • 顶级菜单作为分组容器,不需要指定组件路径
    • 示例:系统管理菜单
  2. 只有最后一级的菜单才需要 component 值

    • 如果菜单没有子菜单,则需要指定 component
    • 如果菜单有子菜单,则不需要指定 component
  3. 非菜单类型(如 button不需要 component 值

    • 按钮权限只用于权限控制,不涉及页面路由
  4. 所有页面组件的菜单在前端处理时都拉平挂载在 Layouts 框架组件下

    • 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下
    • 不需要在前端维护嵌套的路由结构

数据示例:

// 示例:顶级菜单(无 component
{
  id: 1,
  title: '系统管理',
  name: 'system',
  type: 'menu',
  parent_id: 0,
  path: '/system',
  component: null,  // 顶级菜单不需要 component
  meta: {
    icon: 'Setting',
    hidden: false,
    hiddenBreadcrumb: false,
    keepAlive: false
  },
  sort: 1,
  status: 1,
  children: []
}

// 示例:最后一级菜单(有 component
{
  id: 2,
  title: '用户管理',
  name: 'system.users',
  type: 'menu',
  parent_id: 0,
  path: '/system/users',
  component: 'system/users/index',  // 最后一级菜单需要 component
  meta: {
    icon: 'User',
    hidden: false,
    hiddenBreadcrumb: false,
    keepAlive: true
  },
  sort: 1,
  status: 1,
  children: []
}

// 示例:按钮权限(无 component
{
  id: 3,
  title: '查看用户',
  name: 'system.users.view',
  type: 'button',
  parent_id: 2,
  path: null,
  component: null,  // 非菜单类型不需要 component
  meta: null,
  sort: 1,
  status: 1,
  children: []
}

// 示例API 权限
{
  id: 4,
  title: '获取用户列表',
  name: 'system.users.list',
  type: 'api',
  parent_id: 0,
  path: 'admin.users.index',
  component: null,
  meta: null,
  sort: 1,
  status: 1,
  children: []
}

路由转换逻辑:

前端会将后端返回的菜单数据转换为 Vue Router 路由格式,所有页面路由都会拉平挂载在 Layout 布局组件下:

// 将后端菜单转换为路由格式
function transformMenusToRoutes(menus) {
  return menus.map(menu => {
    const route = {
      path: menu.path,
      name: menu.name,
      meta: {
        title: menu.title,
        icon: menu.meta?.icon,
        hidden: menu.meta?.hidden || false,
        keepAlive: menu.meta?.keepAlive || false
      }
    }
    
    // 只有菜单类型且有 component 值的才加载组件
    if (menu.type === 'menu' && menu.component) {
      route.component = loadComponent(menu.component)
    }
    
    // 不处理 children所有菜单都会拉平到 Layout 下
    // if (menu.children) {
    //   route.children = transformMenusToRoutes(menu.children)
    // }
    
    return route
  })
}

// 加载组件函数
function loadComponent(componentPath) {
  return () => import(`@/pages/${componentPath}.vue`)
}

菜单拉平处理说明:

由于前端将所有页面菜单拉平挂载在 Layout 下,因此:

  • 后端菜单的 parent_id 主要用于构建菜单树的层级关系(侧边栏显示)
  • 路由层面不需要维护嵌套结构,所有页面路由都在同一层级
  • component 值只需要在最后一级菜单(叶子节点)中设置
  • 路由 path 使用后端返回的 path 字段
  • 路由 name 使用后端返回的 name 字段(权限编码)
  • 路由 meta.title 使用后端返回的 title 字段(权限标题)

路由守卫

系统通过路由守卫实现权限控制和动态路由加载:

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const isLoggedIn = userStore.isLoggedIn()
  
  // 白名单直接放行
  if (whiteList.includes(to.path)) {
    return next()
  }
  
  // 未登录跳转登录页
  if (!isLoggedIn) {
    return next({ path: '/login', query: { redirect: to.fullPath } })
  }
  
  // 动态路由加载
  if (!isDynamicRouteLoaded) {
    const menus = userStore.getMenu()
    const dynamicRoutes = transformMenusToRoutes(menus)
    
    // 添加动态路由
    dynamicRoutes.forEach(route => {
      router.addRoute('Layout', route)
    })
    
    isDynamicRouteLoaded = true
    next({ ...to, replace: true })
  } else {
    next()
  }
})

6. 状态管理规范

Pinia Store 定义

使用组合式 API 定义 Store。

User Store用户认证与权限

src/stores/modules/user.js 负责管理用户认证信息和权限数据:

// src/stores/modules/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { resetRouter } from '../../router'

export const useUserStore = defineStore('user', () => {
  // State
  const token = ref('')              // 访问令牌
  const refreshToken = ref('')       // 刷新令牌
  const userInfo = ref(null)         // 用户信息
  const menu = ref([])               // 用户菜单
  const permissions = ref([])        // 用户权限节点

  // Getters
  const isLoggedIn = () => !!token.value

  // Actions
  function setToken(newToken) {
    token.value = newToken
  }

  function setUserInfo(info) {
    userInfo.value = info
  }

  // 设置菜单(合并静态菜单和后端菜单)
  function setMenu(newMenu) {
    const staticMenus = userRoutes || []
    let mergedMenus = [...staticMenus]

    if (newMenu && newMenu.length > 0) {
      const menuMap = new Map()
      
      // 添加静态菜单
      staticMenus.forEach(m => {
        if (m.path) menuMap.set(m.path, m)
      })
      
      // 添加后端菜单(覆盖重复路径)
      newMenu.forEach(m => {
        if (m.path) menuMap.set(m.path, m)
      })
      
      mergedMenus = Array.from(menuMap.values())
    }
    menu.value = mergedMenus
  }

  function getMenu() {
    return menu.value
  }

  function setPermissions(data) {
    permissions.value = data
  }

  function hasPermission(permission) {
    if (!permissions.value || permissions.value.length === 0) {
      return false
    }
    return permissions.value.includes(permission)
  }

  function logout() {
    token.value = ''
    refreshToken.value = ''
    userInfo.value = null
    menu.value = []
    resetRouter()
  }

  return {
    token,
    refreshToken,
    userInfo,
    menu,
    permissions,
    setToken,
    setUserInfo,
    setMenu,
    getMenu,
    setPermissions,
    hasPermission,
    logout,
    isLoggedIn
  }
})

Store 使用

import { useUserStore } from '@/stores/modules/user'

const userStore = useUserStore()

// 使用 state
console.log(userStore.token)
console.log(userStore.menu)
console.log(userStore.permissions)

// 调用 action
userStore.setToken('xxx')
userStore.setMenu(menus)
userStore.setPermissions(permissions)

// 检查权限
if (userStore.hasPermission('user.create')) {
  // 有权限,执行操作
}

// 登出
userStore.logout()

Store 持久化

使用 pinia-plugin-persistedstate 实现数据持久化:

{
  persist: {
    key: 'user-store',
    storage: customStorage,
    pick: ['token', 'refreshToken', 'userInfo', 'menu']
  }
}

权限指令

项目提供权限指令,用于在模板中控制元素显示:

<template>
  <!-- 只有拥有 user.create 权限时显示 -->
  <a-button v-permission="'user.create'">新增</a-button>
  
  <!-- 拥有多个权限之一时显示 -->
  <a-button v-permission="['user.create', 'user.update']">编辑</a-button>
</template>

<script setup>
import { useUserStore } from '@/stores/modules/user'

const userStore = useUserStore()
</script>

7. 表格开发规范

使用 useTable Hook

项目提供了 useTable Hook 简化表格开发:

import { useTable } from '@/hooks/useTable'
import { ref } from 'vue'

const {
  loading,
  dataSource,
  pagination,
  handleSearch,
  handleReset,
  handlePageChange
} = useTable({
  api: userApi.getList, // API 方法
  immediate: true // 是否立即加载
})

// 搜索参数
const searchParams = ref({
  keyword: '',
  status: ''
})

scTable 组件使用

<template>
  <sc-table
    :columns="columns"
    :data-source="dataSource"
    :loading="loading"
    :pagination="pagination"
    @page-change="handlePageChange"
  >
    <template #action="{ record }">
      <a-button @click="handleEdit(record)">编辑</a-button>
    </template>
  </sc-table>
</template>

8. 表单开发规范

scForm 组件使用

<template>
  <sc-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    :items="formItems"
    @submit="handleSubmit"
  />
</template>

<script setup>
import { ref } from 'vue'

const formRef = ref(null)
const formData = ref({
  username: '',
  email: ''
})

const formRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
  ]
}

const formItems = [
  {
    type: 'input',
    prop: 'username',
    label: '用户名',
    placeholder: '请输入用户名'
  },
  {
    type: 'input',
    prop: 'email',
    label: '邮箱',
    placeholder: '请输入邮箱'
  }
]

const handleSubmit = async () => {
  await formRef.value.validate()
  // 提交逻辑
}
</script>

9. 图标使用规范

Ant Design Vue Icons

<!-- 直接使用无需导入 -->
<template>
  <a-icon type="user" />
  <a-icon type="setting" />
</template>

常用图标

  • user: 用户
  • setting: 设置
  • delete: 删除
  • edit: 编辑
  • plus: 添加
  • search: 搜索
  • reload: 刷新
  • download: 下载
  • upload: 上传
  • eye: 查看
  • eye-invisible: 隐藏
  • check-circle: 成功
  • close-circle: 失败
  • info-circle: 信息
  • warning: 警告

10. 国际化 (i18n) 规范

使用 i18n

import { useI18n } from '@/hooks/useI18n'

const { t } = useI18n()

// 使用
console.log(t('common.save'))
console.log(t('user.deleteConfirm'))

语言文件组织

// src/i18n/locales/zh.js
export default {
  common: {
    save: '保存',
    cancel: '取消',
    confirm: '确认',
    delete: '删除'
  },
  user: {
    deleteConfirm: '确定要删除该用户吗?'
  }
}

11. 文件上传规范

scUpload 组件使用

<template>
  <sc-upload
    v-model="imageUrl"
    :limit="1"
    accept="image/*"
    list-type="picture-card"
  />
</template>

<script setup>
import { ref } from 'vue'

const imageUrl = ref('')
</script>

12. 富文本编辑器规范

scEditor 组件使用

<template>
  <sc-editor
    v-model="content"
    :height="400"
    :toolbar="toolbarConfig"
  />
</template>

<script setup>
import { ref } from 'vue'

const content = ref('')
const toolbarConfig = [
  'bold', 'italic', 'underline', 'strike',
  'list', 'orderedList', 'quote', 'codeBlock',
  'image', 'link'
]
</script>

13. 样式规范

全局样式

src/style.css 中定义全局样式。

页面公共样式

页面布局的公共样式已提取到全局样式文件中,各页面可直接使用:

src/assets/style/pages.scss 中定义的公共类:

// 标准页面布局(垂直布局)
.pages-base-layout {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 0;

  .tool-bar {
    padding: 12px 16px;
    background: #fff;
    border-bottom: 1px solid #f0f0f0;

    .left-panel {
      display: flex;
      align-items: center;
      flex: 1;
    }

    .right-panel {
      display: flex;
      gap: 8px;
    }
  }

  .table-content {
    flex: 1;
    overflow: hidden;
    padding: 16px;
    background: #f5f5f5;
  }
}

// 侧边栏布局(左右分栏)
.pages-sidebar-layout {
  display: flex;
  flex-direction: row;
  height: 100%;
  padding: 0;

  .left-box {
    width: 260px;
    border-right: 1px solid #f0f0f0;
    display: flex;
    flex-direction: column;
    background: #fff;

    .header {
      padding: 12px 16px;
      border-bottom: 1px solid #f0f0f0;
      background: #fafafa;
    }

    .body {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
    }
  }

  .right-box {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;

    .tool-bar {
      padding: 12px 16px;
      background: #fff;
      border-bottom: 1px solid #f0f0f0;

      .left-panel {
        display: flex;
        align-items: center;
        flex: 1;
      }

      .right-panel {
        display: flex;
        gap: 8px;
      }
    }

    .table-content {
      flex: 1;
      overflow: hidden;
      padding: 16px;
      background: #f5f5f5;
    }
  }
}

使用示例:

<!-- 标准页面布局 -->
<style scoped lang="scss">
.user-page {
  @extend .pages-base-layout;
}
</style>

<!-- 侧边栏布局 -->
<style scoped lang="scss">
.user-page {
  @extend .pages-sidebar-layout;
}
</style>

组件样式

使用 scoped 避免样式污染:

<style scoped>
.user-list {
  padding: 20px;
}

.user-list .table {
  margin-top: 20px;
}
</style>

命名规范

  • 使用 BEM 命名法
  • 类名使用 kebab-case

14. 工具函数使用

request.js (HTTP 请求)

import request from '@/utils/request'

// GET 请求
request.get('/api/users', { params: { page: 1 } })

// POST 请求
request.post('/api/users', { name: 'test' })

// PUT 请求
request.put('/api/users/1', { name: 'updated' })

// DELETE 请求
request.delete('/api/users/1')

tool.js (工具函数)

import { formatDate, deepClone } from '@/utils/tool'

// 格式化日期
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')

// 深拷贝
deepClone(originalObject)

15. 开发流程

登录流程

  1. 用户输入用户名和密码
  2. 调用 authApi.login.post() 发送登录请求
  3. 后端返回 tokenrefreshTokenuserInfomenupermissions
  4. 前端保存数据到 Store持久化
  5. 路由守卫检测到登录状态,加载动态路由
  6. 跳转到首页或重定向页
import { useUserStore } from '@/stores/modules/user'
import authApi from '@/api/auth'

const userStore = useUserStore()

const handleLogin = async () => {
  try {
    const res = await authApi.login.post({
      username: 'admin',
      password: '123456'
    })
    
    // 保存 token
    userStore.setToken(res.data.token)
    userStore.setRefreshToken(res.data.refreshToken)
    
    // 保存用户信息
    userStore.setUserInfo(res.data.user)
    
    // 保存菜单(合并静态菜单)
    userStore.setMenu(res.data.menu)
    
    // 保存权限节点
    userStore.setPermissions(res.data.permissions)
    
    // 跳转首页
    router.push('/dashboard')
  } catch (error) {
    message.error('登录失败')
  }
}

添加新页面

  1. src/pages/ 对应模块下创建页面组件
  2. src/api/ 中创建对应的 API 文件
  3. 在后端添加对应的菜单和权限配置
  4. 前端会自动加载动态路由(无需手动配置路由)

添加新组件

  1. src/components/ 或对应子目录下创建组件
  2. 遵循组件结构规范
  3. 添加必要的 Props 和 Emits
  4. 编写组件文档

16. 常用命令

# 安装依赖
npm install

# 开发环境启动
npm run dev

# 生产环境构建
npm run build

# 预览生产构建
npm run preview

# 代码格式化
npm run format

# 代码检查
npm run lint

17. 注意事项

  1. 禁止重复引入图标: Ant Design Vue 图标已全局引入,直接使用即可
  2. 使用组合式 API: 新代码统一使用 <script setup> 语法
  3. 组件复用: 优先使用项目提供的公共组件scTable、scForm 等)
  4. API 统一管理: 所有接口统一在 src/api/ 目录下管理
  5. 路由懒加载: 路由组件必须使用动态导入
  6. 环境变量: 通过 import.meta.env 访问环境变量
  7. 不要编写 demo: 开发过程中不编写示例代码
  8. 测试提示: 如需测试,提示用户是否运行测试,不主动运行

18. 代码质量

  • 遵循 Vue 3 官方风格指南
  • 保持代码简洁、可读
  • 适当添加注释说明复杂逻辑
  • 使用语义化的变量和函数命名
  • 避免过多的嵌套层级

19. 性能优化

  • 合理使用 v-ifv-show
  • 列表渲染必须设置 key
  • 大列表使用虚拟滚动
  • 图片懒加载
  • 路由懒加载
  • 组件按需引入(除图标外)