436 lines
18 KiB
Markdown
436 lines
18 KiB
Markdown
# 4.4 前端分层架构
|
||
|
||
:::tip 概述
|
||
前端采用 Vue 3 + TypeScript 组件化架构,分为 View(视图)→ API(接口)→ Store(状态)→ HTTP(请求)四层。辅以 Router(路由)、Directives(指令)、Hooks(组合式函数)、Utils(工具)等支撑模块。前端为本仓库 `ui/` 目录内的 EleVue 管理后台。
|
||
:::
|
||
|
||
## 架构全景
|
||
|
||
```text
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 支撑模块 │
|
||
│ │
|
||
│ Router(路由) Directives(指令) Hooks(组合式函数) │
|
||
│ src/router/ src/directives/ src/hooks/ │
|
||
│ │
|
||
│ Utils(工具) Enums(枚举) Styles(样式) │
|
||
│ src/utils/ src/enums/ src/styles/ │
|
||
└──────────────────────────────┬──────────────────────────────────────┘
|
||
│ 被主干层引用
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 主干链路 │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ View(视图层) │ │
|
||
│ │ 页面渲染、用户交互、调用 API 和 Store │ │
|
||
│ │ 文件:src/views/**/*.vue │ │
|
||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||
│ │ 调用 API 函数 │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ API(接口层) │ │
|
||
│ │ 封装后端 API 调用,定义请求参数和响应类型 │ │
|
||
│ │ 文件:src/api/**/*.ts │ │
|
||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||
│ │ 调用 http.request() │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ HTTP(请求层) │ │
|
||
│ │ Axios 封装:拦截器、Token 携带、错误处理 │ │
|
||
│ │ 文件:src/utils/http/axios/ │ │
|
||
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ 后端 API(RESTful JSON,{code, ok, msg, data}) │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ Store(状态层) │ │
|
||
│ │ 全局状态管理:Token、用户信息、权限列表、动态路由 │ │
|
||
│ │ 文件:src/store/modules/*.ts │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 各层职责详解
|
||
|
||
### View 层(视图)
|
||
|
||
**文件**:`src/views/**/*.vue`
|
||
**职责**:页面渲染、用户交互、调用 API 获取数据
|
||
|
||
```text
|
||
src/views/
|
||
├── system/ 系统管理
|
||
│ ├── user/ 用户管理
|
||
│ │ ├── index.vue 列表页
|
||
│ │ └── edit.vue 编辑弹窗
|
||
│ ├── role/ 角色管理
|
||
│ └── menu/ 菜单管理
|
||
├── content/ 内容管理
|
||
│ ├── article/ 文章管理
|
||
│ └── category/ 分类管理
|
||
├── login/ 登录页
|
||
├── dashboard/ 仪表盘
|
||
└── ...
|
||
```
|
||
|
||
**典型页面结构:**
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 搜索区 -->
|
||
<el-form :model="searchForm" inline>
|
||
<el-form-item label="名称">
|
||
<el-input v-model="searchForm.name" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="fetchData">搜索</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 操作栏 -->
|
||
<el-button v-perm="'sys:example:add'" type="primary" @click="handleAdd">新增</el-button>
|
||
|
||
<!-- 数据表格 -->
|
||
<el-table :data="tableData">
|
||
<el-table-column prop="id" label="ID" />
|
||
<el-table-column prop="name" label="名称" />
|
||
<el-table-column prop="statusText" label="状态" />
|
||
<el-table-column label="操作">
|
||
<template #default="{ row }">
|
||
<el-button v-perm="'sys:example:update'" @click="handleEdit(row)">编辑</el-button>
|
||
<el-button v-perm="'sys:example:delete'" type="danger" @click="handleDelete(row.id)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页(后端固定返回 records / total / current / size / pages) -->
|
||
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize"
|
||
:total="total" @change="fetchData" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { getExamplePage, exampleDelete } from '@/api/tool/example';
|
||
|
||
const tableData = ref([]);
|
||
const pageNo = ref(1);
|
||
const pageSize = ref(20);
|
||
const total = ref(0);
|
||
|
||
const fetchData = async () => {
|
||
const res = await getExamplePage({ pageNo: pageNo.value, pageSize: pageSize.value });
|
||
tableData.value = res.records;
|
||
total.value = res.total;
|
||
};
|
||
|
||
onMounted(() => fetchData());
|
||
</script>
|
||
```
|
||
|
||
### API 层(接口)
|
||
|
||
**文件**:`src/api/**/*.ts`
|
||
**职责**:封装后端 API 调用,一个后端模块对应一个 API 文件
|
||
|
||
**命名规范:**
|
||
|
||
| 操作 | 函数名 | HTTP 方法 | 参数传递 |
|
||
|------|--------|----------|----------|
|
||
| 分页查询 | `get{Module}Page` | GET | `params` |
|
||
| 全量列表 | `get{Module}List` | GET | `params` |
|
||
| 详情 | `get{Module}Detail` | GET | URL 参数 |
|
||
| 新增 | `{module}Add` | POST | `data` |
|
||
| 修改 | `{module}Update` | PUT | `data` |
|
||
| 删除 | `{module}Delete` | DELETE | URL 参数 |
|
||
| 批量删除 | `{module}BatchDelete` | **DELETE**(带 body) | `data`(`{ids:[]}`) |
|
||
| 导入 | `{module}Import` | POST | FormData |
|
||
| 导出 | `{module}Export` | GET | `params` |
|
||
|
||
:::warning 批量删除方法为 DELETE
|
||
本项目 `batchDelete` 路由为 **DELETE**(请求体仍传 `ids` 数组——DELETE 方法携带 body),见 [3.7 路由注册](/zh/guide/route.html)。
|
||
:::
|
||
|
||
**完整示例:**
|
||
|
||
```typescript
|
||
// src/api/tool/example.ts
|
||
import { http } from '@/utils/http/axios';
|
||
|
||
// 分页查询
|
||
export function getExamplePage(params?) {
|
||
return http.request({ url: '/example/page', method: 'GET', params });
|
||
}
|
||
|
||
// 详情
|
||
export function getExampleDetail(id) {
|
||
return http.request({ url: '/example/detail/' + id, method: 'GET' });
|
||
}
|
||
|
||
// 新增
|
||
export function exampleAdd(data: any) {
|
||
return http.request({ url: '/example/add', method: 'POST', data });
|
||
}
|
||
|
||
// 修改
|
||
export function exampleUpdate(data: any) {
|
||
return http.request({ url: '/example/update', method: 'PUT', data });
|
||
}
|
||
|
||
// 删除
|
||
export function exampleDelete(id) {
|
||
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
|
||
}
|
||
|
||
// 批量删除(DELETE + 请求体 ids 数组)
|
||
export function exampleBatchDelete(data: any) {
|
||
return http.request({ url: '/example/batchDelete', method: 'DELETE', data });
|
||
}
|
||
```
|
||
|
||
### HTTP 层(请求)
|
||
|
||
**文件**:`src/utils/http/axios/`
|
||
**职责**:Axios 封装,自动处理 Token、响应码、错误提示
|
||
|
||
```text
|
||
src/utils/http/axios/
|
||
├── index.ts Axios 实例配置和拦截器
|
||
├── Axios.ts VAxios 类封装
|
||
├── axiosTransform.ts 数据转换接口
|
||
├── checkStatus.ts HTTP 状态码处理
|
||
├── helper.ts 辅助函数
|
||
└── types.ts 类型定义
|
||
```
|
||
|
||
**核心拦截器:**
|
||
|
||
```text
|
||
请求拦截器:
|
||
┌──────────────────────────────────────────┐
|
||
│ 1. 从 Store 获取 Token │
|
||
│ 2. 自动添加 Authorization: Bearer <token> │
|
||
└──────────────────────────────────────────┘
|
||
|
||
响应拦截器:
|
||
┌──────────────────────────────────────────┐
|
||
│ code === 0 → 成功,返回 data │
|
||
│ code === 401 → Token 过期,跳转登录页 │
|
||
│ code === 403 → 权限不足,显示错误提示 │
|
||
│ 其它 code → 业务失败,显示 msg │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
### Store 层(状态)
|
||
|
||
**文件**:`src/store/modules/*.ts`
|
||
**职责**:全局状态管理(Pinia)
|
||
|
||
| Store | 文件 | 管理的状态 |
|
||
|-------|------|-----------|
|
||
| userStore | `user.ts` | Token、用户信息、权限列表、登录/登出 |
|
||
| asyncRouteStore | `asyncRoute.ts` | 动态路由、keep-alive 缓存列表 |
|
||
| projectSettingStore | `projectSetting.ts` | 项目配置(主题、布局等) |
|
||
| tabsViewStore | `tabsView.ts` | 标签页导航状态 |
|
||
| designSettingStore | `designSetting.ts` | 主题设计配置 |
|
||
| ossConfigStore | `ossConfig.ts` | 云存储配置 |
|
||
| lockscreenStore | `lockscreen.ts` | 锁屏状态 |
|
||
|
||
**userStore 核心流程:**
|
||
|
||
```text
|
||
登录:
|
||
userStore.login(userInfo)
|
||
→ POST /login(含 key/code 验证码参数)
|
||
→ 获取 access_token
|
||
→ Storage.set(ACCESS_TOKEN, token)
|
||
|
||
获取用户信息:
|
||
userStore.GetInfo()
|
||
→ GET /index/getUser
|
||
→ setPermissions(res.permissions)
|
||
→ setUserInfo(res)
|
||
|
||
获取菜单(动态路由):
|
||
→ GET /index/getMenus
|
||
|
||
登出:
|
||
userStore.logout()
|
||
→ POST /logout
|
||
→ 清除 Token、用户信息、权限列表
|
||
→ 跳转登录页
|
||
```
|
||
|
||
## 路由与动态菜单
|
||
|
||
### 路由架构
|
||
|
||
```text
|
||
静态路由(固定,src/router/)
|
||
├── /login 登录页
|
||
├── / 根路径,重定向到首页
|
||
├── /redirect/:path 重定向路由
|
||
└── /:path(.*)* 404 错误页
|
||
|
||
动态路由(从后端菜单生成)
|
||
├── /dashboard 仪表盘
|
||
├── /system/user 用户管理
|
||
├── /system/role 角色管理
|
||
├── /content/article 文章管理
|
||
└── ...
|
||
```
|
||
|
||
### 动态路由生成流程
|
||
|
||
```text
|
||
1. 用户登录 → 获取 Token → 存入 Store
|
||
2. 路由守卫(router.beforeEach)检测到 Token
|
||
3. 调用 GET /index/getMenus 获取菜单树(后端按用户权限过滤)
|
||
4. generator-routers.ts 将菜单树转换为路由表
|
||
5. router.addRoute() 动态注入路由
|
||
6. 渲染侧边栏菜单
|
||
```
|
||
|
||
### 布局系统
|
||
|
||
```text
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 顶部导航栏 │
|
||
│ [Logo] [面包屑] [搜索] [通知] [头像] │
|
||
├──────────┬──────────────────────────────────────────────────┤
|
||
│ │ │
|
||
│ 侧边栏 │ 内容区域 │
|
||
│ 菜单 │ │
|
||
│ │ ┌──────────────────────────────────────────┐ │
|
||
│ ──────── │ │ 标签页导航 │ │
|
||
│ 系统管理 │ ├──────────────────────────────────────────┤ │
|
||
│ ├ 用户 │ │ │ │
|
||
│ ├ 角色 │ │ 页面内容(keep-alive 缓存) │ │
|
||
│ └ 菜单 │ │ │ │
|
||
│ │ └──────────────────────────────────────────┘ │
|
||
└──────────┴──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 权限控制
|
||
|
||
### 按钮级权限指令
|
||
|
||
```vue
|
||
<!-- v-perm:单权限 -->
|
||
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
|
||
|
||
<!-- v-perms:多权限(满足任一即可) -->
|
||
<el-button v-perms="['sys:user:add', 'sys:user:update']">操作</el-button>
|
||
```
|
||
|
||
无权限时元素直接从 DOM 移除,而非仅隐藏。
|
||
|
||
:::tip 超级管理员
|
||
用户权限列表包含 `*:*:*` 时,表示超级管理员,拥有所有权限,`v-perm`/`v-perms` 指令始终放行。
|
||
:::
|
||
|
||
:::warning 前端权限不是安全防线
|
||
`v-perm` 只是体验层(隐藏按钮);真正的安全校验在后端 `#[Permission]` 注解 + `auth.jwt` 中间件(见 [3.8 权限节点配置](/zh/guide/permission.html))。
|
||
:::
|
||
|
||
## 支撑模块
|
||
|
||
### Hooks(组合式函数)
|
||
|
||
| Hook | 文件 | 说明 |
|
||
|------|------|------|
|
||
| `usePermission` | `hooks/web/usePermission.ts` | 权限判断(hasPermission、hasSomePermission) |
|
||
| `useTable` | `hooks/web/useTable.ts` | 表格通用逻辑(分页、搜索、刷新) |
|
||
| `useForm` | `hooks/web/useForm.ts` | 表单通用逻辑(提交、重置、校验) |
|
||
|
||
### Directives(自定义指令)
|
||
|
||
| 指令 | 说明 |
|
||
|------|------|
|
||
| `v-perm` | 单权限控制 |
|
||
| `v-perms` | 多权限控制(满足任一) |
|
||
|
||
### Utils(工具函数)
|
||
|
||
| 工具 | 文件 | 说明 |
|
||
|------|------|------|
|
||
| HTTP | `utils/http/axios/` | Axios 封装 |
|
||
| Storage | `utils/Storage.ts` | localStorage 封装 |
|
||
| Dict | `utils/dict.ts` | 字典工具(从 Store 获取字典值) |
|
||
|
||
## 数据流向
|
||
|
||
### 完整请求链路
|
||
|
||
```text
|
||
用户操作(点击搜索按钮)
|
||
│
|
||
▼
|
||
View: fetchData()
|
||
│ 调用 API 函数
|
||
▼
|
||
API: getExamplePage(params)
|
||
│ 调用 http.request()
|
||
▼
|
||
HTTP: 请求拦截器添加 Token
|
||
│ 发送 GET /example/page?pageNo=1&pageSize=20
|
||
▼
|
||
后端: Controller → Service → Model → Database
|
||
│(响应 {code, ok, msg, data},snake→camel 已在后端完成)
|
||
▼
|
||
HTTP: 响应拦截器判断 code
|
||
│ code === 0 → 返回 data
|
||
▼
|
||
API: 返回 data
|
||
│
|
||
▼
|
||
View: tableData.value = res.records; total.value = res.total
|
||
│
|
||
▼
|
||
Vue 响应式 → 表格自动刷新
|
||
```
|
||
|
||
### Store 数据流
|
||
|
||
```text
|
||
登录成功
|
||
│
|
||
▼
|
||
userStore.setToken(token) → 响应式更新
|
||
userStore.setUserInfo(info) → 响应式更新
|
||
userStore.setPermissions(perm) → 响应式更新
|
||
│
|
||
▼
|
||
其他组件通过 userStore.username / userStore.permissions 读取
|
||
│
|
||
▼
|
||
v-perm 指令读取 permissions 列表 → 控制元素显隐
|
||
```
|
||
|
||
## 前后端对应关系
|
||
|
||
| 后端 | 前端 | 说明 |
|
||
|------|------|------|
|
||
| `Http/Controllers/Api/UserController.php` | `src/api/system/user.ts` | API 函数映射 |
|
||
| `Services/UserService.php` | `src/views/system/user/` | 业务页面 |
|
||
| `Http/Requests/UserRequest.php` 的 rules | 表单 `rules` | 前端表单校验规则(体验层) |
|
||
| `Models/User.php` | — | 前端不直接操作模型 |
|
||
| `routes/api.php` | `src/api/**/*.ts` | 路由 ↔ API 函数 |
|
||
| 字典数据(`dict` 表) | `src/enums/` + 字典 Store | 枚举常量 |
|
||
| `#[Permission]` 注解 | `v-perm` 指令 | 权限码对应 |
|
||
| 分页 `records/total/current/size/pages` | el-pagination 绑定 | 固定分页结构 |
|
||
|
||
## 层间通信规范
|
||
|
||
| 调用方向 | 是否允许 | 说明 |
|
||
|----------|---------|------|
|
||
| View → API | ✅ | 标准调用 |
|
||
| View → Store | ✅ | 读取/修改全局状态 |
|
||
| View → Hooks | ✅ | 复用逻辑 |
|
||
| API → HTTP | ✅ | 调用 http.request() |
|
||
| API → Store | ❌ | API 层不应操作状态 |
|
||
| Store → API | ✅ | Store actions 中调用 API |
|
||
| HTTP → Store | ⚠️ | 仅在拦截器中读取 Token |
|
||
| Hooks → API | ✅ | Hooks 中可调用 API |
|