【web应用】基于Vue3和Spring Boot的课程管理前后端数据交互过程

文章目录


一、系统架构概述

本文将详细解析一个基于Vue3 + Element Plus前端框架和Spring Boot后端框架的课程管理系统中前后端数据交互的全过程。该系统实现了课程信息的增删改查(CRUD)和导出功能,采用了RESTful API设计风格,是典型的前后端分离架构实现。

二、前端数据交互流程分析

1. 组件初始化与数据请求

<script setup>语法糖中,系统通过onMounted生命周期钩子(虽未显式写出,但getList()在最后执行)自动加载课程列表数据:

javascript 复制代码
// 组件挂载后自动执行
getList()

function getList() {
  loading.value = true
  listCourse(queryParams.value).then(response => {
    courseList.value = response.rows
    total.value = response.total
    loading.value = false
  })
}

2. API请求封装

前端通过@/api/course/course模块封装了所有API请求:

javascript 复制代码
// 查询课程列表
export function listCourse(query) {
  return request({
    url: '/course/course/list',
    method: 'get',
    params: query
  })
}

// 新增课程
export function addCourse(data) {
  return request({
    url: '/course/course',
    method: 'post',
    data: data
  })
}

这里使用了axios的封装(@/utils/request),自动处理了请求拦截、响应拦截和错误处理。

3. 查询参数处理

前端通过queryParams响应式对象管理查询条件:

javascript 复制代码
const queryParams = ref({
  pageNum: 1,
  pageSize: 10,
  code: null,
  subject: null,
  name: null,
  applicablePerson: null
})

当用户点击搜索按钮时,触发handleQuery方法:

javascript 复制代码
function handleQuery() {
  queryParams.value.pageNum = 1 // 重置为第一页
  getList() // 重新获取数据
}

三、后端数据处理流程

1. 控制器接收请求

Spring Boot控制器通过@RestController注解暴露API接口:

java 复制代码
@RestController
@RequestMapping("/course/course")
public class CourseController extends BaseController {
    @Autowired
    private ICourseService courseService;

    @GetMapping("/list")
    public TableDataInfo list(Course course) {
        startPage(); // 启动分页
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list); // 封装分页结果
    }
}

2. 分页处理机制

后端使用了MyBatis分页插件(通过继承BaseController):

java 复制代码
protected void startPage() {
    PageDomain pageDomain = TableSupport.buildPageRequest();
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) {
        PageHelper.startPage(pageNum, pageSize);
    }
}

3. 服务层业务处理

服务接口与实现:

java 复制代码
public interface ICourseService {
    List<Course> selectCourseList(Course course);
}

@Service
public class CourseServiceImpl implements ICourseService {
    @Override
    public List<Course> selectCourseList(Course course) {
        return courseMapper.selectCourseList(course);
    }
}

四、典型操作的数据流

1. 查询操作数据流

复制代码
前端组件 → API封装(listCourse) → Axios请求 → 
Spring Controller(list方法) → Service层 → Mapper → 数据库 → 
返回数据沿原路返回 → 前端更新courseList显示

2. 新增操作数据流

复制代码
前端表单提交 → API封装(addCourse) → Axios POST请求 → 
Spring Controller(add方法) → Service层 → Mapper → 数据库 → 
返回操作结果 → 前端显示成功消息并刷新列表

3. 删除操作数据流

复制代码
前端选择记录 → 调用handleDelete → API封装(delCourse) → Axios DELETE请求 → 
Spring Controller(remove方法) → Service层 → Mapper → 数据库 → 
返回操作结果 → 前端显示消息并刷新列表

五、关键技术点解析

1. 前端表单验证

使用Element Plus的表单验证规则:

javascript 复制代码
const rules = {
  code: [{ required: true, message: "课程编码不能为空", trigger: "blur" }],
  subject: [{ required: true, message: "课程学科不能为空", trigger: "change" }]
  // 其他字段验证...
}

2. 后端权限控制

通过Spring Security注解实现方法级权限控制:

java 复制代码
@PreAuthorize("@ss.hasPermi('course:course:add')")
@PostMapping
public AjaxResult add(@RequestBody Course course) {
    return toAjax(courseService.insertCourse(course));
}

3. 数据导出实现

导出功能通过Excel工具类实现:

java 复制代码
@PostMapping("/export")
public void export(HttpServletResponse response, Course course) {
    List<Course> list = courseService.selectCourseList(course);
    ExcelUtil<Course> util = new ExcelUtil<>(Course.class);
    util.exportExcel(response, list, "课程管理数据");
}

前端调用:

javascript 复制代码
function handleExport() {
  proxy.download('course/course/export', {...queryParams.value}, `course_${new Date().getTime()}.xlsx`)
}

六、完整交互示例:新增课程

  1. 前端操作

    • 用户填写表单并点击"确定"按钮
    • 触发submitForm方法
    • 表单验证通过后调用addCourseAPI
  2. 网络请求

    http 复制代码
    POST /course/course HTTP/1.1
    Content-Type: application/json
    
    {
      "code": "CS101",
      "subject": "1",
      "name": "计算机科学导论",
      "price": 99.9,
      "applicablePerson": "计算机专业新生",
      "info": "计算机科学入门课程"
    }
  3. 后端处理

    • Controller接收请求并调用Service

    • Service调用Mapper插入数据

    • 返回操作结果:

      json 复制代码
      {
        "code": 200,
        "msg": "操作成功"
      }
  4. 前端响应

    • 显示成功消息
    • 关闭对话框
    • 调用getList()刷新列表

七、完整代码

前端页面

c 复制代码
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="课程编码" prop="code">
        <el-input
          v-model="queryParams.code"
          placeholder="请输入课程编码"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="课程学科" prop="subject">
        <el-select v-model="queryParams.subject" placeholder="请选择课程学科" clearable>
          <el-option
            v-for="dict in course_subject"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="课程名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入课程名称"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="适用人群" prop="applicablePerson">
        <el-input
          v-model="queryParams.applicablePerson"
          placeholder="请输入适用人群"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['course:course:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['course:course:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['course:course:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
          v-hasPermi="['course:course:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <el-table v-loading="loading" :data="courseList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="课程id" align="center" prop="id" />
      <el-table-column label="课程编码" align="center" prop="code" />
      <el-table-column label="课程学科" align="center" prop="subject">
        <template #default="scope">
          <dict-tag :options="course_subject" :value="scope.row.subject"/>
        </template>
      </el-table-column>
      <el-table-column label="课程名称" align="center" prop="name" />
      <el-table-column label="价格" align="center" prop="price" />
      <el-table-column label="适用人群" align="center" prop="applicablePerson" />
      <el-table-column label="课程介绍" align="center" prop="info" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['course:course:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['course:course:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <pagination
      v-show="total>0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改课程管理对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="courseRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="课程编码" prop="code">
          <el-input v-model="form.code" placeholder="请输入课程编码" />
        </el-form-item>
        <el-form-item label="课程学科" prop="subject">
          <el-select v-model="form.subject" placeholder="请选择课程学科">
            <el-option
              v-for="dict in course_subject"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="课程名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入课程名称" />
        </el-form-item>
        <el-form-item label="价格" prop="price">
          <el-input v-model="form.price" placeholder="请输入价格" />
        </el-form-item>
        <el-form-item label="适用人群" prop="applicablePerson">
          <el-input v-model="form.applicablePerson" placeholder="请输入适用人群" />
        </el-form-item>
        <el-form-item label="课程介绍" prop="info">
          <el-input v-model="form.info" placeholder="请输入课程介绍" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup name="Course">
import { listCourse, getCourse, delCourse, addCourse, updateCourse } from "@/api/course/course"

const { proxy } = getCurrentInstance()
const { course_subject } = proxy.useDict('course_subject')

const courseList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")

const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    code: null,
    subject: null,
    name: null,
    applicablePerson: null,
  },
  rules: {
    code: [
      { required: true, message: "课程编码不能为空", trigger: "blur" }
    ],
    subject: [
      { required: true, message: "课程学科不能为空", trigger: "change" }
    ],
    name: [
      { required: true, message: "课程名称不能为空", trigger: "blur" }
    ],
    price: [
      { required: true, message: "价格不能为空", trigger: "blur" }
    ],
    applicablePerson: [
      { required: true, message: "适用人群不能为空", trigger: "blur" }
    ],
    info: [
      { required: true, message: "课程介绍不能为空", trigger: "blur" }
    ],
  }
})

const { queryParams, form, rules } = toRefs(data)

/** 查询课程管理列表 */
function getList() {
  loading.value = true
  listCourse(queryParams.value).then(response => {
    courseList.value = response.rows
    total.value = response.total
    loading.value = false
  })
}

// 取消按钮
function cancel() {
  open.value = false
  reset()
}

// 表单重置
function reset() {
  form.value = {
    id: null,
    code: null,
    subject: null,
    name: null,
    price: null,
    applicablePerson: null,
    info: null,
    createTime: null,
    updateTime: null
  }
  proxy.resetForm("courseRef")
}

/** 搜索按钮操作 */
function handleQuery() {
  queryParams.value.pageNum = 1
  getList()
}

/** 重置按钮操作 */
function resetQuery() {
  proxy.resetForm("queryRef")
  handleQuery()
}

// 多选框选中数据
function handleSelectionChange(selection) {
  ids.value = selection.map(item => item.id)
  single.value = selection.length != 1
  multiple.value = !selection.length
}

/** 新增按钮操作 */
function handleAdd() {
  reset()
  open.value = true
  title.value = "添加课程管理"
}

/** 修改按钮操作 */
function handleUpdate(row) {
  reset()
  const _id = row.id || ids.value
  getCourse(_id).then(response => {
    form.value = response.data
    open.value = true
    title.value = "修改课程管理"
  })
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs["courseRef"].validate(valid => {
    if (valid) {
      if (form.value.id != null) {
        updateCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功")
          open.value = false
          getList()
        })
      } else {
        addCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功")
          open.value = false
          getList()
        })
      }
    }
  })
}

/** 删除按钮操作 */
function handleDelete(row) {
  const _ids = row.id || ids.value
  proxy.$modal.confirm('是否确认删除课程管理编号为"' + _ids + '"的数据项?').then(function() {
    return delCourse(_ids)
  }).then(() => {
    getList()
    proxy.$modal.msgSuccess("删除成功")
  }).catch(() => {})
}

/** 导出按钮操作 */
// function handleExport()
// {
//   proxy.download('course/course/export', {
//     ...queryParams.value
//   }, `course_${new Date().getTime()}.xlsx`)
// }

function handleExport()
{
  proxy.download('course/course/export', {...queryParams.value}, `course_${new Date().getTime()}.xlsx`)
}

getList()
</script>

前端API接口

c 复制代码
import request from '@/utils/request'

// 查询课程管理列表
export function listCourse(query) {
  return request({
    url: '/course/course/list',
    method: 'get',
    params: query
  })
}

export function getCourseList(query){
  return request({
    url:'',
    method:'get',
    params:query
  })
}

// 查询课程管理详细
export function getCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'get'
  })
}

// 新增课程管理
export function addCourse(data) {
  return request({
    url: '/course/course',
    method: 'post',
    data: data
  })
}

// 修改课程管理
export function updateCourse(data) {
  return request({
    url: '/course/course',
    method: 'put',
    data: data
  })
}

// 删除课程管理
export function delCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'delete'
  })
}

后端控制器

c 复制代码
package com.ruoyi.course.controller;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.course.domain.Course;
import com.ruoyi.course.service.ICourseService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;

/**
 * 课程管理Controller
 * 
 * @author 
 * @date 2025-05-27
 */
@RestController
@RequestMapping("/course/course")
public class CourseController extends BaseController
{
    @Autowired
    private ICourseService courseService;

    /**
     * 查询课程管理列表
     */
    @PreAuthorize("@ss.hasPermi('course:course:list')")
    @GetMapping("/list")
    public TableDataInfo list(Course course)
    {
        startPage();
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list);
    }

    /**
     * 导出课程管理列表
     */
    @PreAuthorize("@ss.hasPermi('course:course:export')")
    @Log(title = "课程管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, Course course)
    {
        List<Course> list = courseService.selectCourseList(course);
        ExcelUtil<Course> util = new ExcelUtil<Course>(Course.class);
        util.exportExcel(response, list, "课程管理数据");
    }

    /**
     * 获取课程管理详细信息
     */
    @PreAuthorize("@ss.hasPermi('course:course:query')")
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id)
    {
        return success(courseService.selectCourseById(id));
    }

    /**
     * 新增课程管理
     */
    @PreAuthorize("@ss.hasPermi('course:course:add')")
    @Log(title = "课程管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody Course course)
    {
        return toAjax(courseService.insertCourse(course));
    }

    /**
     * 修改课程管理
     */
    @PreAuthorize("@ss.hasPermi('course:course:edit')")
    @Log(title = "课程管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody Course course)
    {
        return toAjax(courseService.updateCourse(course));
    }

    /**
     * 删除课程管理
     */
    @PreAuthorize("@ss.hasPermi('course:course:remove')")
    @Log(title = "课程管理", businessType = BusinessType.DELETE)
	@DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids)
    {
        return toAjax(courseService.deleteCourseByIds(ids));
    }
}

八、总结与优化建议

1. 当前实现特点

  1. 完整的前后端分离架构
  2. 标准的RESTful API设计
  3. 完善的权限控制体系
  4. 统一的数据封装格式
  5. 前后端参数校验机制

2. 可优化方向

  1. 前端优化

    • 添加加载状态管理
    • 实现更细粒度的错误提示
    • 添加操作防抖/节流
  2. 后端优化

    • 增加数据缓存机制
    • 实现更复杂的查询条件
    • 添加操作日志记录
  3. 交互优化

    • 实现批量操作的结果分项显示
    • 添加数据变更的实时提示
    • 优化大数据量的分页加载

这种前后端分离的开发模式,通过明确的接口约定和职责划分,使得前后端可以并行开发,提高了开发效率,也便于系统的维护和扩展。理解这种数据交互机制,对于现代Web应用的开发具有重要意义。

相关推荐
前端大卫5 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘5 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare5 小时前
浅浅看一下设计模式
前端
Lee川5 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix6 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人6 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl6 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人6 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼6 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端