Files
laravel_swoole/.clinerules/admin-rule.md
2026-02-10 08:58:31 +08:00

20 KiB
Raw 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/              # 认证页面
│   │   ├── 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. 组件开发规范

组件命名

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

组件结构

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

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

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

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

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

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

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

3. 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
}

4. 路由开发规范

路由类型

项目包含两类路由:

  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: 是否固定标签页

后端菜单数据格式:

菜单权限节点需要遵循以下规范:

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

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

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

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

    • 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下
    • 不需要在前端维护嵌套的路由结构
// 示例:顶级菜单(无 component
{
  name: '系统管理',
  code: 'system',
  type: 'menu',
  parent_id: 0,
  route: '/system',
  component: null,  // 顶级菜单不需要 component
  meta: {
    icon: 'Setting',
    hidden: false
  },
  sort: 1
}

// 示例:最后一级菜单(有 component
{
  name: '用户管理',
  code: 'system.users',
  type: 'menu',
  parent_id: 0,
  route: '/system/users',
  component: 'system/users/index',  // 最后一级菜单需要 component
  meta: {
    icon: 'User',
    hidden: false
  },
  sort: 1
}

// 示例:按钮权限(无 component
{
  name: '查看用户',
  code: 'system.users.view',
  type: 'button',
  parent_id: 0,
  route: 'admin.users.index',
  component: null,  // 非菜单类型不需要 component
  meta: null,
  sort: 1
}

路由转换逻辑:

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

// 将后端菜单转换为路由格式
function transformMenusToRoutes(menus) {
  return menus.map(menu => {
    const route = {
      path: menu.route,
      name: menu.name || menu.code,
      meta: {
        title: menu.name,
        icon: menu.meta?.icon,
        hidden: menu.meta?.hidden,
        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 值只需要在最后一级菜单(叶子节点)中设置

路由守卫

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

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()
  }
})

5. 状态管理规范

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>

6. 表格开发规范

使用 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>

7. 表单开发规范

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>

8. 图标使用规范

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: 警告

9. 国际化 (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: '确定要删除该用户吗?'
  }
}

10. 文件上传规范

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>

11. 富文本编辑器规范

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>

12. 样式规范

全局样式

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

组件样式

使用 scoped 避免样式污染:

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

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

命名规范

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

13. 工具函数使用

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)

14. 开发流程

登录流程

  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. 编写组件文档

15. 常用命令

# 安装依赖
npm install

# 开发环境启动
npm run dev

# 生产环境构建
npm run build

# 预览生产构建
npm run preview

# 代码格式化
npm run format

# 代码检查
npm run lint

16. 注意事项

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

17. 代码质量

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

18. 性能优化

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