# 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.vue` 或 `Drawer.vue` 这样过于通用的命名
#### 页面布局规范
**基本布局要求**:
1. **使用 Flex 布局**: 页面容器必须使用 Flex 布局,确保占满全部内容区域
2. **表格高度自适应**: 表格使用封装的 `scTable` 组件,高度占满剩余内容空间
**标准页面布局模板**:
```vue
```
**布局样式说明**:
- 页面根元素使用 `display: flex` 和 `flex-direction: column` 实现垂直布局
- `height: 100%` 确保页面占满父容器高度
- `padding: 0` 去除内边距(由布局组件控制外边距)
- `table-content` 使用 `flex: 1` 占据剩余空间
- `overflow: hidden` 防止出现滚动条(表格内部自己处理滚动)
**侧边栏布局示例** (如用户管理页面):
```vue
```
#### 列表搜索区域规范
**搜索区域设计原则**:
1. **精简显示**: 搜索条件不全部显示,只显示最常用的 1-2 项
2. **去除 label 属性**: 搜索输入框不使用 label 属性,通过 placeholder 直接说明用途,保持界面简洁
3. **优先使用表格筛选**: 对于状态等枚举类型的筛选,优先使用 Ant Design Vue Table 的自定义过滤器功能
4. **高级搜索抽屉**: 其他复杂搜索条件使用抽屉弹出(需要在页面 components 目录下创建 SearchDrawer.vue 组件)
5. **保持界面整洁**: 避免搜索区域占用过多空间
**实现方式**:
**方式一: 使用表格自定义筛选(推荐)**
```vue
```
**方式二: 使用高级搜索抽屉(复杂搜索条件)**
```vue
```
**SearchDrawer.vue 组件示例**:
```vue
正常
禁用
取消
确定
```
**搜索条件选择建议**:
常用搜索条件(显示在工具栏):
- 主要关键字搜索(如用户名、角色名称)
- 无需筛选的简单字段
表格筛选(推荐用于):
- 状态、类型等枚举值字段
- 不需要组合条件的单一筛选
高级搜索抽屉(用于):
- 次要字段(如邮箱、手机号)
- 日期范围筛选
- 多条件组合筛选
- 复杂的搜索逻辑
**scTable 组件筛选器优化说明**:
scTable 组件已支持 Ant Design Vue Table 的筛选功能,使用方式:
1. 在 `columns` 配置中添加 `filters` 属性
2. 通过 `@filterChange` 事件处理筛选变化
3. 将筛选条件合并到搜索表单中提交
```javascript
// 在 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`
#### 组件结构
```vue
```
### 4. API 接口开发规范
#### API 文件组织
每个业务模块对应一个 API 文件,统一放在 `src/api/` 目录下。
```javascript
// 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')
},
},
},
}
```
#### 使用示例
```javascript
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 获取菜单数据,动态添加到路由中
#### 静态路由定义
```javascript
// 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 布局下
- 不需要在前端维护嵌套的路由结构
**数据示例**:
```javascript
// 示例:顶级菜单(无 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 布局组件下:
```javascript
// 将后端菜单转换为路由格式
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` 字段(权限标题)
#### 路由守卫
系统通过路由守卫实现权限控制和动态路由加载:
```javascript
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` 负责管理用户认证信息和权限数据:
```javascript
// 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 使用
```javascript
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` 实现数据持久化:
```javascript
{
persist: {
key: 'user-store',
storage: customStorage,
pick: ['token', 'refreshToken', 'userInfo', 'menu']
}
}
```
#### 权限指令
项目提供权限指令,用于在模板中控制元素显示:
```vue
新增
编辑
```
### 7. 表格开发规范
#### 使用 useTable Hook
项目提供了 `useTable` Hook 简化表格开发:
```javascript
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 组件使用
```vue
编辑
```
### 8. 表单开发规范
#### scForm 组件使用
```vue
```
### 9. 图标使用规范
#### Ant Design Vue Icons
```vue
```
#### 常用图标
- `user`: 用户
- `setting`: 设置
- `delete`: 删除
- `edit`: 编辑
- `plus`: 添加
- `search`: 搜索
- `reload`: 刷新
- `download`: 下载
- `upload`: 上传
- `eye`: 查看
- `eye-invisible`: 隐藏
- `check-circle`: 成功
- `close-circle`: 失败
- `info-circle`: 信息
- `warning`: 警告
### 10. 国际化 (i18n) 规范
#### 使用 i18n
```javascript
import { useI18n } from '@/hooks/useI18n'
const { t } = useI18n()
// 使用
console.log(t('common.save'))
console.log(t('user.deleteConfirm'))
```
#### 语言文件组织
```javascript
// src/i18n/locales/zh.js
export default {
common: {
save: '保存',
cancel: '取消',
confirm: '确认',
delete: '删除'
},
user: {
deleteConfirm: '确定要删除该用户吗?'
}
}
```
### 11. 文件上传规范
#### scUpload 组件使用
```vue
```
### 12. 富文本编辑器规范
#### scEditor 组件使用
```vue
```
### 13. 样式规范
#### 全局样式
在 `src/style.css` 中定义全局样式。
#### 页面公共样式
页面布局的公共样式已提取到全局样式文件中,各页面可直接使用:
**在 `src/assets/style/pages.scss` 中定义的公共类:**
```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;
}
}
}
```
**使用示例:**
```vue
```
#### 组件样式
使用 `scoped` 避免样式污染:
```vue
```
#### 命名规范
- 使用 BEM 命名法
- 类名使用 kebab-case
### 14. 工具函数使用
#### request.js (HTTP 请求)
```javascript
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 (工具函数)
```javascript
import { formatDate, deepClone } from '@/utils/tool'
// 格式化日期
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
// 深拷贝
deepClone(originalObject)
```
### 15. 开发流程
#### 登录流程
1. 用户输入用户名和密码
2. 调用 `authApi.login.post()` 发送登录请求
3. 后端返回 `token`、`refreshToken`、`userInfo`、`menu`、`permissions`
4. 前端保存数据到 Store(持久化)
5. 路由守卫检测到登录状态,加载动态路由
6. 跳转到首页或重定向页
```javascript
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. 常用命令
```bash
# 安装依赖
npm install
# 开发环境启动
npm run dev
# 生产环境构建
npm run build
# 预览生产构建
npm run preview
# 代码格式化
npm run format
# 代码检查
npm run lint
```
### 17. 注意事项
1. **禁止重复引入图标**: Ant Design Vue 图标已全局引入,直接使用即可
2. **使用组合式 API**: 新代码统一使用 `