Ruo-Yi基础篇(六):第六章. 前端业务模块开发实战:课程管理
Ruo-Yi基础篇(六):第六章. 前端业务模块开发实战:课程管理
Prorise第六章. 前端业务模块开发实战:课程管理
6.1. 任务启动:环境初始化与需求分析
6.1.1. 核心目标
本章的核心任务是,在若依(RuoYi-Vue3)前端项目中,手动从零开始开发一个功能完备的“课程管理”模块。此过程旨在模拟一个真实的企业级开发场景:后端接口已开发完毕,前端工程师需依据接口契约,独立完成视图层的全部构建工作。
完成本章的学习后,我们将不再仅仅满足于使用代码生成器,而是具备深度定制、优化乃至重构复杂业务模块的能力,将对若依前端的理解从“应用层”深化至“原理层”。
6.1.2. 准备工作:数据库初始化
开发工作始于数据环境的统一。在开始编写任何前端代码之前,我们必须确保本地数据库中拥有与后端开发环境一致的表结构和基础数据。
请在您的数据库客户端(如 Navicat, DataGrip 等)中执行下面的 SQL 脚本。此脚本将完成两项关键操作:
- 创建名为
tb_course的课程管理表。 - 向
tb_course表中插入 20 条结构化的测试数据,以供后续开发与调试使用。
操作前置条件: 为确保数据环境的纯净与一致性,如果您本地数据库中已存在名为 tb_course 或 course 的数据表,请务-必在执行此脚本前将其删除,以避免任何潜在的字段冲突或数据不一致问题。
1 | -- 删除已存在的旧表 (如果存在) |
6.2. 接口契约:定义 API 服务模块
6.2.1. 场景设定:接收后端 API 接口文档
在完成数据库初始化后,我们进入了真实开发流程中的关键一步:前后端协作的起点。我们假设已从后端开发团队处获得一份详尽的“课程管理模块 API 接口文档”。这份文档是前后端的“契约”,它精确定义了数据请求的路径、方法、参数和响应结构。后续所有的前端开发工作,都将严格围绕这份契约展开。
API 关键信息概览:
- 模块根路径:
/course/Course - 数据字典依赖:
- 字典类型:
course_subject - 字典数据: 在后续开发中,我们需要从系统中获取此字典,其数据结构为
0=JavaEE,1=Python,2=鸿蒙。
- 字典类型:
6.2.2. API 接口清单
下表详细列出了我们将要对接的全部后端接口。
| 功能描述 | HTTP 方法 | URL (基于根路径) | 请求参数/体 | 关键响应格式 |
|---|---|---|---|---|
| 查询课程列表(分页) | GET | /list | query (URL Params) | { code, msg, rows, total } |
| 获取课程详情 | GET | /{id} | id (Path Variable) | { code, msg, data } |
| 新增课程 | POST | / | data (Request Body) | { code, msg } |
| 修改课程 | PUT | / | data (Request Body) | { code, msg } |
| 删除课程 | DELETE | /{id} | id (Path Variable) | { code, msg } |
6.2.3. 构建 API 服务层
在开始构建任何 UI 界面之前,最佳的工程实践是先创建一个专门的 API 服务层。我们将所有与“课程管理”相关的 HTTP 请求封装成一个个独立的、语义清晰的函数。
设计原则:服务层解耦
建立独立的 API 服务层是前端工程化的核心实践。它至少带来两大好处:
- 解耦: 将数据获取的复杂逻辑(如 URL 构造、请求方法、参数处理)与视图组件(
.vue文件)的业务逻辑彻底分离。组件只关心“调用一个函数”,而不用关心这个函数背后是如何发送 HTTP 请求的。 - 复用: 同一个 API 函数(例如
getCourse(id))可能在项目的多个不同页面或组件中被需要。将其封装后,任何地方都可以通过import安全地复用,避免了代码的重复编写。
现在,我们在指定的目录下创建 course.js 文件。
文件路径: src/api/course/course.js
首先,我们需要导入若依框架封装好的 axios 实例,它位于 @/utils/request。
1 | // **文件路径**: src/api/course/course.js |
@/utils/request.js 并不仅仅是一个简单的 axios 实例。它内置了请求拦截器(用于自动附加认证 Token)和响应拦截器(用于统一处理 HTTP 错误码和数据结构),极大地简化了我们的业务代码。
接下来,我们根据 API 接口清单,逐一实现对应的函数。
1 | // **文件路径**: src/api/course/course.js |
至此,我们的 API 服务层已经构建完毕。我们拥有了一套与后端契约完全匹配、可随时调用的前端函数库。这是我们接下来构建视图层(index.vue)的坚实基础。
6.3. 视图层构建:index.vue 的分步实现
在完成了 API 服务层的封装后,我们现在将工作的重心转移到用户直接交互的界面层。本节,我们将采用“渐进式”的开发策略,从零开始,一步步地为项目添加 src/views/course/course/index.vue 文件,并为其注入生命力。这种开发方式能让我们清晰地看到一个复杂组件是如何从最基础的结构“生长”为功能完备的形态的。
6.3.1. 步骤一:初始化组件结构与响应式状态
任务:
本步骤的核心任务是奠定整个组件的根基。我们将完成两项工作:
- 创建文件并搭建静态布局: 在
<template>中构建出页面的宏观结构,包含所有功能区域的静态占位。 - 定义全部响应式状态: 在
<script setup>中,预先定义好驱动整个组件行为所需的所有响应式变量。
首先,请在 src/views/course/ 目录下创建一个新的 course 文件夹,并在其中新建 index.vue 文件。
文件路径: src/views/course/course/index.vue
1. 搭建 <template> 静态骨架
我们先不关心任何动态逻辑,只专注于用 HTML 和 Element Plus 组件搭建出页面的视觉框架。这个框架将包含五个主要功能区域。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 编写 <script setup> 并定义响应式状态
现在,我们为刚刚搭建的骨架定义“灵魂”——即驱动其所有动态行为的数据状态。
首先,我们引入所有必需的模块,包括 Vue 的核心 API 和我们在上一步中封装好的 API 服务函数。
1 | // **文件路径**: src/views/course/course/index.vue |
name="Course" 是一个重要的工程实践。它为 <script setup> 组件定义了一个明确的名称,这在 Vue Devtools 中进行调试,以及在使用 <keep-alive> 进行组件缓存时都至关重要。
接下来,我们定义组件所需的所有响应式变量。提前定义好全部变量,有助于我们从宏观上理解组件的状态复杂度。
1 | // ... imports ... |
为什么 loading、total 这些变量使用 ref,而 form、queryParams 却要包裹在 reactive 中,还要用 toRefs 解构?
这是 Vue 3 Composition API 的一个核心实践模式。对于独立的、单一的值(如布尔值、数字、字符串),或者整个数组的替换,使用 ref 更为直接。而对于一组逻辑上紧密关联的数据,比如一个表单的所有字段 (form) 或一个查询的所有参数 (queryParams),将它们组织在一个 reactive 对象中,可以使数据结构更清晰,管理更方便。
toRefs 的作用则是为了在模板中能方便地直接使用 queryParams 里的属性(如 v-model="queryParams.code"),同时保持其与原始 reactive 对象的响应式链接。如果不使用 toRefs 直接解构,会丢失响应性。
至此,我们已经完成了 index.vue 的初始化工作。我们拥有了一个结构清晰的静态模板和一套完备的、准备驱动视图变化的响应式数据。下一步,我们将开始编写业务逻辑,让页面真正“动”起来。
6.3.2. 步骤二:实现核心功能 - 列表查询与渲染
任务:
在上一步中,我们已经搭建了页面的骨架并定义了数据状态。现在,我们的核心任务是打通前后端的数据链路:调用 API 从后端获取课程列表数据,并将其动态渲染到 <el-table> 组件中。
1. 完善 <template> 表格区域
首先,我们为 <el-table> 组件添加所有需要展示的列(<el-table-column>)。
- 绑定数据源: 将
:data属性绑定到我们在上一步中定义的courseList响应式变量。 - 定义列: 为
tb_course表的每个关键字段创建一个对应的列,并通过prop属性指定其对应的数据字段名。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 实现 getList 数据获取函数
接下来,我们在 <script setup> 区域编写获取列表数据的核心逻辑。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>` 部分) |
现在,当组件被加载时,getList 函数会自动执行,从后端获取第一页的数据并渲染到表格中。

3. 深度解析:若依的 <dict-tag> 组件与 useDict
在表格的“课程学科”列,我们没有直接显示 scope.row.subject 的值(它可能是 ‘0’, ‘1’, ‘2’),而是使用了一个特殊的组件 <dict-tag>。
<dict-tag/> 这行代码是如何工作的?course_subject 这个数据又是从哪里来的?
问得非常好,这触及了若依前端框架一个非常便捷的设计。course_subject 来自于我们在脚本顶部通过 const { course_subject } = proxy.useDict('course_subject'); 获取的数据。
useDict 是若依封装的一个组合式函数。当你传入字典类型名(如 'course_subject'),它会执行以下操作:
首先检查全局的 Pinia/Vuex Store 中是否已经缓存了 'course_subject' 的字典数据。
如果有缓存,直接从缓存返回。
如果没有缓存,它会向后端发起一个异步请求(通常是 /system/dict/data/type/course_subject),获取该字典的完整数据(一个包含 label, value, elTagType 等属性的数组)。
获取成功后,它会将这份数据存入 Store 进行缓存,然后再返回给你。
这样,course_subject 就得到了一个类似 [{label: "JavaEE", value: "0", elTagType: "primary"}, ...] 的数组。
那 <dict-tag> 组件呢?
<dict-tag> 是一个展示型组件。它接收 options(我们传入的 course_subject 数组)和 value(当前行的学科值,如 ‘0’)。组件内部会根据 value 在 options 数组中查找匹配的对象,然后使用该对象的 label 作为显示文本,elTagType 作为 Element Plus 标签的 type,最终渲染出一个带颜色和正确文本的 <el-tag>。
明白了,所以 useDict 负责“取数据”,<dict-tag> 负责“展示数据”,两者结合,就高效地解决了后端返回的原始码值与前端需要展示的人性化文本之间的转换问题。
通过以上步骤,我们不仅成功地将后端数据显示在了页面上,还深入理解了若依框架中处理数据字典的核心机制。我们的页面已经从一个静态的骨架,变成了一个能够展示真实数据的动态表格。
6.3.3. 步骤三:构建交互功能 - 搜索、重置与分页
任务:
目前,我们的页面已经能够展示数据,但它还是一个“只读”的静态列表。本步骤的任务是为页面注入交互能力,我们将实现两个核心功能:
- 分页浏览: 激活底部的分页组件,让用户可以浏览所有数据。
- 条件搜索: 激活顶部的搜索表单,允许用户根据特定条件筛选数据。
1. 完善 <template> 搜索表单区域
首先,我们在 6.3.1 步骤预留的 <el-form> 区域内,添加具体的表单项。
v-model绑定: 将每个输入控件(如el-input,el-select)的v-model指令,与queryParams对象中对应的属性进行双向绑定。- 事件监听: 为“搜索”和“重置”按钮绑定
@click事件,并为输入框添加@keyup.enter事件以提升用户体验。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 实现搜索、重置与分页的逻辑函数
现在,我们在 <script setup> 区域内,编写与上述模板交互所需的逻辑函数。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>` 部分) |
3. 深度解析:交互功能背后的设计思想
我们的页面现在已经完全具备了交互性,让我们深入分析其工作流程和设计模式。
分页功能的工作流:
- 数据双向绑定: 我们使用
v-model:page和v-model:limit将<pagination>组件的内部状态与父组件的queryParams.pageNum和queryParams.pageSize进行了双向绑定。这意味着当用户在分页组件上进行操作(如点击“下一页”),queryParams对象的值会 自动更新。 - 事件驱动更新: 当分页组件的状态发生改变时,它会向外触发一个名为
@pagination的事件。 - 直接调用: 我们将
@pagination事件直接绑定到了getList函数。 - 闭环流程: 这一系列操作形成了一个完美的数据驱动闭环:用户操作 → 更新
queryParams→ 触发事件 → 调用getList→ API 携带新参数请求 → 更新表格数据。父组件(index.vue)无需关心分页组件的内部实现,只需提供数据和响应事件即可,这是组件化开发的典范。
搜索功能的逻辑核心:
在 handleQuery 函数中,queryParams.value.pageNum = 1; 这一行代码的目的是什么?可以去掉吗?
这是条件搜索功能中一个至关重要的细节,绝对不能去掉。设想一个场景:用户当前正在浏览列表的第 5 页,此时他输入了一个新的搜索条件并点击了“搜索”。
如果我们不去重置页码,getList 函数会带着 pageNum: 5 和新的搜索条件去请求数据。但新的搜索结果可能总共只有 2 页,那么请求第 5 页自然会返回空数据,用户就会看到一个空白的表格,这是一种糟糕的用户体验。
因此,任何一次新的条件搜索,都必须将页码重置回第一页,以确保用户能从头开始浏览新的结果集。
proxy.resetForm("queryRef") 这个函数似乎很方便,它是如何工作的?
proxy.resetForm 是若依挂载到全局的一个工具函数。它的工作原理如下:
它接收一个字符串参数,即我们在 <el-form> 上通过 ref="queryRef" 定义的引用名称。
在函数内部,它通过 proxy.$refs['queryRef'] 获取到 Element Plus 的表单组件实例。
获取到实例后,它会调用该实例自身提供的 resetFields() 方法。这个方法是 Element Plus Form 组件的内置功能,能够将所有表单项的值重置为初始值,并移除校验结果。
所以,若依的 resetForm 本质上是对 Element Plus 原生功能的一个便捷封装,让我们无需在每个组件中都手动编写获取 ref 并调用方法的代码。
通过本步骤的构建,我们的页面不再仅仅是数据的展示板,而成为了一个功能性的、可交互的应用程序界面。用户现在可以自如地在海量数据中穿梭和筛选。
6.3.4. 步骤四:功能实现:数据行选择与状态同步
任务:
在实现具体的“修改”和“删除”功能之前,我们必须先完成一项基础但至关重要的交互设计:根据用户在表格中的行选择行为,动态地控制操作按钮(如“修改”、“删除”)的可用状态。
具体的用户体验目标如下:
- 默认状态: 当表格没有任何行被选中时,“修改”和“批量删除”按钮应处于禁用状态。
- 单选状态: 当用户只选中一行数据时,“修改”和“批量删除”按钮都应变为可用状态。
- 多选状态: 当用户选中多行数据时,“修改”按钮应变回禁用状态(因为不能同时修改多条记录),而“批量删除”按钮应保持可用。
1. 完善 <template> 按钮与表格区域
首先,我们在 6.3.1 步骤预留的 <el-row> 和 <el-table> 区域中,添加完整的按钮和事件监听。
- 按钮工具栏: 添加“修改”和“删除”按钮,并将其
:disabled属性分别绑定到我们预先定义好的single和multiple响应式变量。 - 表格事件监听: 为
<el-table>组件添加@selection-change事件监听器,并将其指向我们将要创建的handleSelectionChange函数。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 实现 handleSelectionChange 状态同步函数
现在,我们在 <script setup> 区域内,编写处理表格选择变化的 handleSelectionChange 函数。这个函数是本节的逻辑核心。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>` 部分) |
3. 深度解析:UI 状态与数据状态的同步
通过以上代码,我们已经完美地实现了预期的交互效果。这背后体现了现代前端框架中一个核心的设计思想:UI 状态是数据状态的直接反映。
我理解了代码的功能,但 single.value = selection.length != 1; 和 multiple.value = !selection.length; 这种“反向”的逻辑判断感觉有点绕。为什么不定义成 isSingleSelected 然后在模板里写 :disabled="!isSingleSelected" 呢?
这是一个非常好的问题,它涉及到代码的可读性和语义的直观性。若依在这里选择的命名方式,是站在 模板(Template) 的角度来思考的。
HTML 的 disabled 属性,其本身的含义就是“当此表达式为 true 时,禁用该元素”。
因此,将变量命名为 single(可理解为“非单选状态”)和 multiple(可理解为“非多选状态”),在模板中使用 :disabled="single",其语义可以直接解读为:“当处于‘非单选’状态时,禁用此按钮”。这与 disabled 属性的原生语义高度契合,形成了非常直观的“语义对仗”,降低了阅读模板代码时的心智负担。
如果我们采用 isSingleSelected 的命名,模板就得写成 :disabled="!isSingleSelected"。虽然也能实现功能,但在阅读时就需要多一次“取反”的逻辑转换,直观性稍差。这体现了若依在前端代码规范上对于“模板可读性”的重视。
工作流程总结:
- 事件触发: 用户在表格的复选框上进行任何勾选或取消勾选的操作。
- 回调执行: Element Plus 的
<el-table>组件立即触发@selection-change事件,并调用我们绑定的handleSelectionChange函数,同时将当前所有被选中行的数据selection作为参数传入。 - 状态计算:
handleSelectionChange函数内部不执行任何 DOM 操作,它只做纯粹的数据计算:根据传入的selection数组的长度,更新ids、single和multiple这三个ref变量的值。 - 响应式更新: 由于
single和multiple是响应式变量,当它们的值发生变化时,Vue 的响应式系统会自动侦测到这一变化。 - 视图同步: Vue 自动将变化同步到模板中与这两个变量绑定的地方——即“修改”和“删除”按钮的
:disabled属性上,从而实现按钮状态的瞬间切换。
这个流程清晰地展示了数据驱动视图(Data-Driven View)的开发模式。我们作为开发者,始终在操作数据,而将繁琐的 UI 更新工作完全交由框架来处理。这是现代前端开发效率得以极大提升的关键所在。
6.3.5. 步骤五:功能实现:新增与修改
任务:
至此,我们的页面已经具备了数据读取和交互的能力。本步骤的核心任务是实现数据的“写”操作,即 C (Create) 和 U (Update)。我们将激活“新增”和“修改”按钮,并利用 <el-dialog> 组件构建一个可复用的表单,以完成以下功能:
- 点击“新增”按钮,弹出一个空的表单供用户填写并提交。
- 选中一条数据后点击“修改”按钮,弹出一个已回填该行数据的表单供用户编辑并提交。
1. 完善 <template> 对话框与按钮区域
我们首先在 6.3.1 步骤预留的 <el-dialog> 和 <el-row> 区域内,添加完整的表单结构和事件绑定。
- 按钮绑定: 为“新增”和“修改”按钮绑定
@click事件,分别指向handleAdd和handleUpdate函数。 - 对话框表单: 在
<el-dialog>内部,使用<el-form>构建与tb_course表字段对应的表单项,并将每个表单控件与form响应式对象中的属性进行v-model双向绑定。 - 提交与取消: 为对话框底部的“确定”和“取消”按钮绑定
@click事件,分别指向submitForm和cancel函数。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
v-hasPermi 是若依的权限控制指令。它会在组件挂载时检查当前用户是否拥有指定的权限标识(如 'course:course:add')。如果没有,该指令会直接将按钮从 DOM 中移除,这是一种比 v-if 更彻底的前端权限控制方案。
2. 实现新增与修改的核心逻辑函数
现在,我们在 <script setup> 区域内,编写驱动整个新增/修改流程的所有逻辑函数。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>` 部分) |
3. 深度解析:对话框复用与表单提交流程
对话框复用模式 (Dialog Reuse Pattern):
这是中后台系统中非常经典和高效的设计模式。我们没有为“新增”和“修改”创建两个独立的对话框,而是通过几个状态变量 (open, title, form) 来控制同一个 <el-dialog> 组件的行为,其优势显而易见:
- 代码量减半: 避免了大量重复的模板和样式代码。
- 维护性更高: 当表单结构需要调整时,只需修改一处地方。
- 逻辑清晰:
handleAdd和handleUpdate两个入口函数职责分明,一个负责清空表单,一个负责填充表单,而最终的提交逻辑则由submitForm统一处理。
submitForm 的核心逻辑:submitForm 函数是整个流程的“决策者”,其内部通过 form.value.id != null 这一简单而可靠的判断,来区分是执行更新操作还是创建操作。
- 从
handleAdd流程进入时,reset()函数已将form.value.id设为null,因此会调用addCourseAPI。 - 从
handleUpdate流程进入时,getCourseAPI 返回的数据中包含了id,因此会调用updateCourseAPI。
若依工具集成: 在 submitForm 函数中,我们使用了 proxy.$modal.msgSuccess("...") 来显示操作成功的提示。这是若依对 Element Plus 的 ElMessage 组件的便捷封装。通过将其挂载到全局 proxy 上,我们无需在每个需要消息提示的组件中都手动 import { ElMessage } from 'element-plus',显著提升了开发效率和代码的整洁度。
至此,我们的“课程管理”模块已经具备了完整的 C (Create)、R (Read)、U (Update) 功能。用户不仅可以查看和筛选数据,还可以对其进行创建和修改,模块的核心价值已经基本实现。
6.3.6. 步骤六:功能实现:删除与批量删除
任务:
现在,我们的模块已经具备了完整的 C(Create)、R(Read)、U(Update) 功能。本步骤的任务是完成 CRUD 闭环的最后一块拼图:D (Delete)。我们将激活列表中的“删除”按钮和顶部工具栏的“删除”按钮,以实现以下目标:
- 用户可以点击任一行数据后的“删除”按钮,删除单条记录。
- 用户可以勾选多行数据,然后点击顶部工具栏的“删除”按钮,一次性批量删除多条记录。
- 在执行删除这一破坏性操作前,必须向用户提供一个明确的、可反悔的二次确认对话框。
1. 完善 <template> 删除按钮区域
我们首先在 6.3.1 步骤预留的位置,添加完整的删除按钮及其事件绑定。
- 行内删除按钮: 在
<el-table-column>的操作列中,添加一个“删除”按钮,并通过scope.row将当前行数据传递给handleDelete函数。 - 批量删除按钮: 顶部工具栏的“删除”按钮已在
6.3.4步骤中添加并绑定了:disabled状态,现在我们为其@click事件绑定handleDelete函数。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 实现 handleDelete 核心逻辑函数
现在,我们在 <script setup> 区域内,编写处理删除操作的核心函数 handleDelete。这个函数巧妙地复用了逻辑,以同时支持单行删除和批量删除。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>` 部分) |
3. 深度解析:逻辑复用与交互体验
逻辑复用 (const _ids = row.id || ids.value;):
这一行代码是 handleDelete 函数设计的精髓所在。
- 当用户点击行内删除按钮时,Element Plus 会将该行的数据对象
row作为参数传给@click的处理函数。此时,row.id是一个具体的值(例如15),row.id || ids.value的结果就是15。delCourse(15)将被调用。 - 当用户点击顶部工具栏的删除按钮时,我们没有传递任何参数给
@click,所以row是undefined。此时row.id为undefined(一个 falsy 值),row.id || ids.value的结果就是ids.value(例如[1, 5, 8])。delCourse([1, 5, 8])将被调用。 - 后端的
delCourse接口被设计为既能接受单个id也能接受id数组,从而完美地支持了前端的这种统一调用。
交互体验 (proxy.$modal.confirm):
对于删除这样的高风险、不可逆操作,提供一个“安全缓冲带”——即二次确认对话框——是用户体验设计的基本准则。
我看到 handleDelete 的逻辑使用了 .then().then().catch() 这种 Promise 链式调用。为什么要这样写?
这是处理异步操作(特别是需要用户交互的异步操作)的经典模式,非常清晰且健壮。让我们分解一下:
proxy.$modal.confirm(...) 返回一个 Promise。如果用户点击“确定”,这个 Promise 会 resolve;如果用户点击“取消”或关闭对话框,它会 reject。
第一个 .then(function() { return delCourse(_ids); }): 只有在用户点击“确定”后,这个回调才会执行。它的核心任务是发起真正的删除 API 请求 delCourse(_ids)。关键点 在于它 return 了 delCourse 的调用结果,而 delCourse 本身也返回一个 Promise。这样,就把 API 请求的 Promise 串联到了整个链条上。
第二个 .then(() => { ... }): 这个回调的执行时机是,当且仅当 delCourse 的 Promise 成功 resolve 时(即后端成功删除了数据并返回了成功响应)。这时我们才去刷新列表和提示成功,这是最准确的时机。
.catch(() => {}): 这个 catch 会捕获两种失败情况:一是用户在第一步点击了“取消”,二是 delCourse 的 API 请求失败了。在这两种情况下,我们都不需要做特殊处理,所以提供一个空的 catch 块来“静默处理”这个 rejection,避免在控制台出现不必要的 Uncaught (in promise) 错误。
明白了,这种链式结构将“用户确认”、“API 请求”、“成功后处理”和“失败/取消处理”这四个环节清晰地串联了起来,代码逻辑非常线性,易于理解和维护。
至此,我们的“课程管理”模块已经完成了全部的 CRUD(增删改查)功能。它已经是一个功能完整、交互健壮的业务模块。在下一节,我们将为其添加“导出”等辅助功能,让其更加完善。
6.3.7. 步骤七:功能实现:数据导出
任务:
作为本模块的收尾工作,我们将实现一个常见且实用的辅助功能:数据导出。我们将激活顶部工具栏的“导出”按钮,当用户点击该按钮时,前端会请求后端接口,将当前查询条件下的所有数据以 Excel 文件的形式下载到用户的本地计算机。
1. 完善 <template> 导出按钮区域
我们首先在 6.3.1 步骤预留的 <el-row> 区域中,添加“导出”按钮并绑定其事件。
1 | <!-- **文件路径**: src/views/course/course/index.vue --> |
2. 实现 handleExport 核心逻辑函数
现在,我们在 <script setup> 区域内,编写处理导出操作的核心函数 handleExport。
1 | // **文件路径**: src/views/course/course/index.vue (`<script setup>`部分) |
3. 深度解析:若依的 proxy.download 方法
handleExport 函数的实现异常简洁,只有一行代码。这是因为若依框架将复杂的文件下载逻辑封装到了全局的 proxy.download 方法中。让我们揭开这个“黑盒”,理解其内部的工作机制。
proxy.download 这个函数做了什么?它为什么需要三个参数?
proxy.download(url, params, filename) 是若依封装的一个专门用于处理后端文件下载请求的工具函数。它极大地简化了前端处理文件流的复杂性。让我们逐一解析它的三个参数和内部流程:
1. url (必需): 第一个参数,字符串类型,代表后端提供文件下载的接口路径。在这里是 'course/course/export'。
2. params (可选): 第二个参数,一个对象,代表需要传递给后端接口的查询参数。我们使用了 { ...queryParams.value } 这个 ES6 的扩展运算符,它会创建一个 queryParams.value 对象的浅拷贝。这意味着,如果用户在页面上设置了搜索条件(如课程学科为“JavaEE”),那么导出请求也会带上这个条件,后端就会只导出符合条件的数据。
3. filename (可选): 第三个参数,字符串类型,代表下载到本地后,你希望文件保存的名称。我们使用了 course_${new Date().getTime()}.xlsx 这种模板字符串,动态地生成一个带时间戳的唯一文件名,避免了文件名冲突。
那它内部具体是怎么实现下载的呢?
proxy.download 内部的核心逻辑大致如下:
它会使用 axios 向指定的 url 发起一个 POST 请求(通常文件导出用 POST 更合适,可以携带更多参数),并将 params作为请求体。最关键的一步是,它会在 axios 的配置中设置 responseType: 'blob'。
responseType: 'blob' 告诉 axios,期望服务器返回的是二进制数据(文件流),而不是通常的 JSON 文本。axios 会将接收到的二进制数据封装成一个 Blob 对象。
函数会在内存中创建一个隐藏的 <a> 标签。
它使用 URL.createObjectURL(blob) 将 Blob 对象转换成一个临时的、唯一的 URL,这个 URL 指向内存中的文件数据。
将这个临时 URL 赋值给 <a> 标签的 href 属性,并将 filename 赋值给 download 属性。然后,通过 JavaScript 代码模拟用户点击了这个 <a> 标签。
浏览器的默认行为会响应这个点击,弹出一个文件保存对话框,文件名就是我们指定的 filename。
下载完成后,函数会调用 URL.revokeObjectURL() 释放之前创建的临时 URL,以回收内存。
明白了,所以 proxy.download 实际上是为我们自动化了“请求二进制流 -> 创建 Blob -> 生成临时 URL -> 模拟点击下载”这一整套标准的浏览器文件下载流程。
通过本步骤的实现,我们的“课程管理”模块已经成为一个功能高度完备的业务单元,不仅囊括了核心的 CRUD 操作,还提供了数据导出这样的高级辅助功能。至此,index.vue 文件的手动构建工作已全部完成。









