1523 lines
35 KiB
Markdown
1523 lines
35 KiB
Markdown
# 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
|
||
<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: flex` 和 `flex-direction: column` 实现垂直布局
|
||
- `height: 100%` 确保页面占满父容器高度
|
||
- `padding: 0` 去除内边距(由布局组件控制外边距)
|
||
- `table-content` 使用 `flex: 1` 占据剩余空间
|
||
- `overflow: hidden` 防止出现滚动条(表格内部自己处理滚动)
|
||
|
||
**侧边栏布局示例** (如用户管理页面):
|
||
|
||
```vue
|
||
<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. **保持界面整洁**: 避免搜索区域占用过多空间
|
||
|
||
**实现方式**:
|
||
|
||
**方式一: 使用表格自定义筛选(推荐)**
|
||
|
||
```vue
|
||
<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>
|
||
```
|
||
|
||
**方式二: 使用高级搜索抽屉(复杂搜索条件)**
|
||
|
||
```vue
|
||
<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 组件示例**:
|
||
|
||
```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. 将筛选条件合并到搜索表单中提交
|
||
|
||
```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
|
||
<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/` 目录下。
|
||
|
||
```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
|
||
<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 简化表格开发:
|
||
|
||
```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
|
||
<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 组件使用
|
||
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<!-- 直接使用,无需导入 -->
|
||
<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
|
||
|
||
```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
|
||
<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 组件使用
|
||
|
||
```vue
|
||
<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` 中定义的公共类:**
|
||
|
||
```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
|
||
<!-- 标准页面布局 -->
|
||
<style scoped lang="scss">
|
||
.user-page {
|
||
@extend .pages-base-layout;
|
||
}
|
||
</style>
|
||
|
||
<!-- 侧边栏布局 -->
|
||
<style scoped lang="scss">
|
||
.user-page {
|
||
@extend .pages-sidebar-layout;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 组件样式
|
||
|
||
使用 `scoped` 避免样式污染:
|
||
|
||
```vue
|
||
<style scoped>
|
||
.user-list {
|
||
padding: 20px;
|
||
}
|
||
|
||
.user-list .table {
|
||
margin-top: 20px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 命名规范
|
||
|
||
- 使用 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**: 新代码统一使用 `<script setup>` 语法
|
||
3. **组件复用**: 优先使用项目提供的公共组件(scTable、scForm 等)
|
||
4. **API 统一管理**: 所有接口统一在 `src/api/` 目录下管理
|
||
5. **路由懒加载**: 路由组件必须使用动态导入
|
||
6. **环境变量**: 通过 `import.meta.env` 访问环境变量
|
||
7. **不要编写 demo**: 开发过程中不编写示例代码
|
||
8. **测试提示**: 如需测试,提示用户是否运行测试,不主动运行
|
||
|
||
### 18. 代码质量
|
||
|
||
- 遵循 Vue 3 官方风格指南
|
||
- 保持代码简洁、可读
|
||
- 适当添加注释说明复杂逻辑
|
||
- 使用语义化的变量和函数命名
|
||
- 避免过多的嵌套层级
|
||
|
||
### 19. 性能优化
|
||
|
||
- 合理使用 `v-if` 和 `v-show`
|
||
- 列表渲染必须设置 `key`
|
||
- 大列表使用虚拟滚动
|
||
- 图片懒加载
|
||
- 路由懒加载
|
||
- 组件按需引入(除图标外)
|