拉钩教育管理系统项目实战(五) - 课程管理

364 阅读3分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

课程管理

布局与准备工作

基础布局与内容展示和之前一样

  • 单独封装组件

  • 使用分页查询课程信息接口

  • 实现查询、重置,筛选功能

// src/services/coures.js 新建文件,封装课程相关接口

// 引入接口模块
import request from '@/utils/request'

// 分页查询课程信息
export const getQueryCourses = data => {
  return request({
    method: 'post',
    url: '/boss/course/getQueryCourses',
    data
  })
}
src/views/course/index.vue  课程管理组件,直接引入子组件
<template>
  <!-- 子组件实例 -->
  <course-content></course-content>
</template>

<script>
// 引入子组件
import courseContent from './son/content'
export default {
  name: 'course',
  // 注册子组件
  components: {
    courseContent
  }
}
</script>

src/views/course/son/content.vue  课程管理直接子组件

<template>
  <!-- 子组件容器 -->
  <div class="course-content">
    <!-- 顶部筛选功能区 -->
    <el-card class="box-card">
      <!-- 标题 -->
      <div slot="header" class="clearfix">
        <span>数据筛选</span>
      </div>
      <!-- 数据筛选表单 -->
      <el-form :inline="true" :model="form" class="demo-form-inline" ref="form">
        <el-form-item label="课程名称" prop="courseName">
          <el-input v-model="form.courseName" placeholder="课程名称"></el-input>
        </el-form-item>
        <!-- 选择器:选择课程当前状态 -->
        <el-form-item label="当前状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择当前状态">
            <el-option
              v-for="item in selectOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item>
          <!-- 重置和查询功能按钮 - 设置点击事件 -->
          <el-button @click="reset()">重置</el-button>
          <el-button @click="queryCoures()" type="primary">查询</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- 底部展示区卡片 -->
    <el-card class="box-card">
      <!-- 顶部标题和添加课程按钮 -->
      <div slot="header" class="clearfix">
        <span>课程列表</span>
        <el-button style="float: right; padding: 3px 0" type="text">添加课程</el-button>
      </div>
      <!-- 用于展示课程的列表 -->
      <el-table
        :data="tableData"
        style="width: 100%">
        <el-table-column
          prop="id"
          label="ID"/>
        <el-table-column
          prop="courseName"
          label="课程名称"/>
        <el-table-column
          prop="price"
          label="价格"/>
        <el-table-column
          prop="sortNum"
          label="排序"/>
        <el-table-column
          prop="status"
          label="上架状态">
        </el-table-column>
        <el-table-column
          label="操作">
          <template slot-scope="scope">
            <!-- 操作按钮 -->
            <el-button @click="editCoures(scope.row)">编辑</el-button>
            <el-button @click="managementCoures(scope.row)">内容管理</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页组件 -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page.sync="form.currentPage"
        :page-sizes="[10, 15, 20]"
        :page-size.sync="form.pageSize"
        layout="total, sizes, prev, pager, next"
        :total="total">
    </el-pagination>
    </el-card>
  </div>
</template>

<script>
// 引入封装的接口:分页查询课程信息
import { getQueryCourses } from '@/services/coures'
export default {
  name: 'course-content',
  data () {
    return {
      // 顶部筛选数据
      form: {
        currentPage: 1,
        pageSize: 10,
        courseName: '',
        status: ''
      },
      // 课程当前状态选择器选项
      selectOptions: [
        { value: '', label: '全部' },
        { value: '1', label: '上架' },
        { value: '0', label: '下架' }
      ],
      // 课程展示列表数据
      tableData: [],
      // 课程总数,绑定分页组件
      total: 0
    }
  },
  // 钩子函数
  created () {
    // 初始化获取课程
    this.getCourses()
  },
  methods: {
    // 页码变化,直接获取课程
    handleCurrentChange () {
      this.getCourses()
    },
    // 每页条数变化,先重置页码,之后获取课程
    handleSizeChange () {
      this.form.currentPage = 1
      console.log('更改了每页条数')
      this.getCourses()
    },
    // 重置
    reset () {
      // 使用表单组件的重置功能
      this.$refs.form.resetFields()
    },
    // 查询课程,先重置页码,再查询课程
    queryCoures () {
      this.form.currentPage = 1
      this.getCourses()
    },
    // 编辑课程
    editCoures () {
      console.log('编辑课程')
    },
    // 内容管理
    managementCoures () {
      console.log('内容管理')
    },
    // 获取课程函数
    async getCourses () {
      // 获取数据
      const { data } = await getQueryCourses(this.form)
      if (data.code === '000000') {
        // 获取成功,把数据交给列表和分页组件
        this.tableData = data.data.records
        this.total = data.data.total
      }
    }
  }
}
</script>

上下架状态显示

使用开关组件,注意需要绑定一下数据,因为我们返回的是1和0,而默认需要布尔值

状态需要使用请求回来的数据中

src/views/course/son/content.vue  修改展示列表中上下架状态显示

<el-table-column
  label="是否上架">
  <template slot-scope="scope">
    <!-- 上架状态展示及操作开关,重设开关时候的值为0和1 -->
    <el-switch
      v-model="scope.row.status"
      active-color="#409eff"
      :active-value="1"
      :inactive-value="0">
    </el-switch>
  </template>
</el-table-column>

上下架操作

  • 使用上下架接口-params

  • 开关组件提供了事件可以进行控制

  • 设置成功后显示提示信息

// src/services/coures.js  封装课程上下架接口

// 课程上下架
export const changeState = data => {
  return request({
    method: 'get',
    url: '/boss/course/changeState',
    params: {
      courseId: data.courseId,
      status: data.status
    }
  })
}
src/views/course/son/content.vue  给开关组件添加状态修改事件

<el-table-column
  label="是否上架">
  <template slot-scope="scope">
    <!-- 上架状态展示及操作开关,重设开关时候的值为0和1,监听状态修改 -->
    <el-switch
      @change="switchChange(scope.row)"
      v-model="scope.row.status"
      active-color="#409eff"
      :active-value="1"
      :inactive-value="0">
    </el-switch>
  </template>
</el-table-column>

<script>
// 引入封装的接口:分页查询课程信息、课程上下架
import { getQueryCourses, changeState } from '@/services/coures'
.............
async switchChange (row) {
  // 调用上下架接口
  const { data } = await changeState({
    // 设置传入参数
    courseId: row.id,
    status: row.status
  })
  if (data.code === '000000') {
    // 如果修改成功弹出提示、更新数据
    this.$message.success('状态修改成功')
    this.getCourses()
  }
},
.............
</script>

上下架优化

为了避免用户频繁操作,导致可能会出现问题

  • 设置在调用上下架接口之前禁用掉开关,调用后再开启-disabled

  • 在每个数据中都添加一个信号值

src/views/course/son/content.vue  给每个开关添加禁用属性,在初次获取数据的时候就给每条数据添加一个标记值,当修改开关的时候修改信号值

<el-table-column
  label="是否上架">
  <template slot-scope="scope">
    <!-- 上架状态展示及操作开关,重设开关时候的值为0和1,监听状态修改,添加是否禁用属性disabled,使用row中的数据 -->
    <el-switch
      :disabled="scope.row.disabled"
      @change="switchChange(scope.row)"
      v-model="scope.row.status"
      active-color="#409eff"
      :active-value="1"
      :inactive-value="0">
    </el-switch>
  </template>
</el-table-column>
............
<script>
...............
// 状态修改事件函数
async switchChange (row) {
  // 调用接口前将开关组件禁用
  row.disabled = true
  // 调用上下架接口
  const { data } = await changeState({
    // 设置传入参数
    courseId: row.id,
    status: row.status
  })
  if (data.code === '000000') {
    // 如果修改成功弹出提示、更新数据
    this.$message.success('状态修改成功')
    this.getCourses()
  }
  // 最后启用开关
  row.disabled = false
}
................

新建课程添加菜单布局

  • 新建组件

  • 设置路由

  • 点击添加课程按钮跳转到页面

  • 使用进度条组件,使用v-for

  • 步骤对应操作区域可以使用v-show进行切换

  • 添加下一步

  • 还有保存按钮

src/router/index.js  添加新路由

{// 添加课程
    path: '/addCourse',
    name: 'addCourse',
    component: () => import(/* webpackChunkName:'addCourse' */ '@/views/course/add')
}
src/views/course/son/content.vue  给添加课程按钮添加点击事件跳转到添加课程组件

<!-- 添加课程按钮添加事件跳转添加页面 -->
<el-button style="float: right; padding: 3px 0" type="text" @click="$router.push('/addCourse')">添加课程</el-button>
src/views/course/add.vue  添加课程页面组件

<template>
  <!-- 容器 -->
  <el-card>
    <!-- 顶部导航 -->
    <div slot="header" class="clearfix">
      <!-- 步骤条组件 -->
      <el-steps :active="nowSteps" simple>
        <!-- 循环遍历数据添加结构 -->
        <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon"></el-step>
      </el-steps>
    </div>
    <!-- 底部表单 -->
    <el-form ref="form" :model="form" label-width="80px">
      <!-- 五个步骤,绑定数据控制显示和隐藏 -->
      <div v-show="nowSteps === 0">基本信息</div>
      <div v-show="nowSteps === 1">课程封面</div>
      <div v-show="nowSteps === 2">销售信息</div>
      <div v-show="nowSteps === 3">秒杀信息</div>
      <div v-show="nowSteps === 4">课程详情</div>
      <!-- 最下方按钮 -->
      <el-form-item>
        <!-- 下一步按钮前四步都显示可跳转到下一步 -->
        <el-button v-show="nowSteps < 4" @click="nowSteps += 1" type="primary">下一步</el-button>
        <!-- 保存按钮只有最后一步才显示,用于保存数据 -->
        <el-button v-show="nowSteps === 4"  type="primary">保存</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
export default {
  name: 'addCourse',
  data () {
    return {
      // 当前步骤
      nowSteps: 0,
      // 步骤条组件数据
      steps: [
        { id: 0, title: '基本信息', icon: 'el-icon-edit' },
        { id: 1, title: '课程封面', icon: 'el-icon-picture-outline' },
        { id: 2, title: '销售信息', icon: 'el-icon-shopping-cart-full' },
        { id: 3, title: '秒杀信息', icon: 'el-icon-present' },
        { id: 4, title: '课程详情', icon: 'el-icon-document' }
      ],
      // form表单
      form: {}
    }
  }
}
</script>

步骤条操作

  • 给步骤条绑定点击事件,可以跳转回对应步骤

  • 修改鼠标样式

src/views/course/add.vue  给步骤条添加点击事件修改当前步骤,并且CSS设置手型鼠标

<!-- 步骤条组件 -->
<el-steps :active="nowSteps" simple>
  <!-- 循环遍历数据添加结构,添加点击事件(更改当前步骤) -->
  <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon" @click.native="nowSteps = item.id"></el-step>
</el-steps>


<style lang="scss" scoped>
// 步骤条
.el-step {
  // 手型鼠标
  cursor: pointer
}
</style>

基本信息和课程封面、销售、秒杀、课程详情布局

  • 使用计数器组件

  • 使用上传组件

  • 样式可能需要深度作用选择器建议使用::v-deep

  • 输入框组件可以设置单位展示

  • 秒杀活动使用开关组件控制显示和隐藏

  • 课程详情可以先使用文本域代替,后面要使用富文本编辑器

src/views/course/add.vue  书写全部课程信息结构(但是还没绑定数据)

<template>
  <!-- 容器 -->
  <el-card>
    <!-- 顶部导航 -->
    <div slot="header" class="clearfix">
      <!-- 步骤条组件 -->
      <el-steps :active="nowSteps" simple>
        <!-- 循环遍历数据添加结构,添加点击事件(更改当前步骤) -->
        <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon" @click.native="nowSteps = item.id"></el-step>
      </el-steps>
    </div>
    <!-- 底部表单 -->
    <el-form ref="form" :model="form" label-width="80px">
      <!-- 基本信息 -->
      <div v-show="nowSteps === 0">
        <el-form-item label="课程名称">
          <el-input v-model="form.name" placeholder="请输入课程名称"></el-input>
        </el-form-item>
        <el-form-item label="课程简介">
          <el-input v-model="form.name" placeholder="请输入课程简介"></el-input>
        </el-form-item>
        <el-form-item label="课程概述">
          <!-- 设置样式让两个输入框在一行显示 -->
          <el-input v-model="form.name" style="width: 49%; minwidth: 300px; margin-right: 10px" placeholder="请输入第一条课程概述">
            <!-- 使用slot="append"在后天添加单位或者描述 -->
            <template slot="append">0 / 20</template>
          </el-input>
          <el-input v-model="form.name" style="width: 49%; minwidth: 300px;" placeholder="请输入第二条课程概述"></el-input>
        </el-form-item>
        <el-form-item label="讲师姓名">
          <el-input v-model="form.name" placeholder="请输入讲师姓名"></el-input>
        </el-form-item>
        <el-form-item label="讲师简介">
          <el-input v-model="form.name" placeholder="请输入讲师简介"></el-input>
        </el-form-item>
        <el-form-item label="课程排序">
          <el-input-number v-model="num" controls-position="right" :min="1"></el-input-number>
        </el-form-item>
      </div>
      <!-- 课程封面 -->
      <div v-show="nowSteps === 1">
        <el-form-item label="课程封面">
          <!-- 上传组件 -->
          <el-upload
            class="avatar-uploader"
            action="https://jsonplaceholder.typicode.com/posts/"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload">
            <img v-if="imageUrl" :src="imageUrl" class="avatar">
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
        <el-form-item label="解锁封面">
          <el-upload
            class="avatar-uploader"
            action="https://jsonplaceholder.typicode.com/posts/"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload">
            <img v-if="imageUrl" :src="imageUrl2" class="avatar">
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
      </div>
      <!-- 销售信息 -->
      <div v-show="nowSteps === 2">
        <el-form-item label="售卖价格">
          <el-input v-model="form.name" placeholder="请输入售卖价格">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="商品原价">
          <el-input v-model="form.name" placeholder="请输入商品原价">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="销量">
          <el-input v-model="form.name" placeholder="请输入销量">
            <template slot="append">单</template>
          </el-input>
        </el-form-item>
        <el-form-item label="活动标签">
          <el-input v-model="form.name" placeholder="请输入活动标签"></el-input>
        </el-form-item>
      </div>
      <!-- 限时秒杀 -->
      <div v-show="nowSteps === 3">
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="限时秒杀开关" label-width="120px">
          <el-switch
            v-model="miaosha"
            active-color="#409eff"
            >
          </el-switch>
        </el-form-item>
        <!-- 限时秒杀信息,根据秒杀开关是否打开决定是否显示 -->
        <div v-if="miaosha">
          <el-form-item label="开始时间">
            <el-date-picker
              v-model="value1"
              type="datetime"
              placeholder="选择开始日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="结束时间">
            <el-date-picker
              v-model="value1"
              type="datetime"
              placeholder="选择结束日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="秒杀价">
            <el-input v-model="form.name" placeholder="请输入秒杀价">
              <template slot="append">元</template>
            </el-input>
          </el-form-item>
          <el-form-item label="库存">
            <el-input v-model="form.name" placeholder="请输入库存">
              <template slot="append">个</template>
            </el-input>
          </el-form-item>
        </div>
      </div>
      <!-- 课程详情 -->
      <div v-show="nowSteps === 4">
        <!-- 这里使用文本域,后期换成富文本编辑器 -->
        <el-form-item label="课程详情" label-width="120px">
          <el-input
            type="textarea"
            :rows="12"
            placeholder="请输入课程详细描述信息"
            v-model="textarea">
          </el-input>
        </el-form-item>
      </div>
      <!-- 最下方按钮 -->
      <el-form-item>
        <!-- 下一步按钮前四步都显示可跳转到下一步 -->
        <el-button v-show="nowSteps < 4" @click="nowSteps += 1" type="primary">下一步</el-button>
        <!-- 保存按钮只有最后一步才显示,用于保存数据 -->
        <el-button v-show="nowSteps === 4"  type="primary">保存</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
export default {
  name: 'addCourse',
  data () {
    return {
      // 当前步骤
      nowSteps: 4,
      // 步骤条组件数据
      steps: [
        { id: 0, title: '基本信息', icon: 'el-icon-edit' },
        { id: 1, title: '课程封面', icon: 'el-icon-picture-outline' },
        { id: 2, title: '销售信息', icon: 'el-icon-shopping-cart-full' },
        { id: 3, title: '秒杀信息', icon: 'el-icon-present' },
        { id: 4, title: '课程详情', icon: 'el-icon-document' }
      ],
      // form表单
      form: {},
      // 基本信息计数器数据,后期会全部写在form内
      num: 1,
      // 两个上传的封面地址
      imageUrl: '',
      imageUrl2: '',
      // 秒杀开关,后期会全部写在form内
      miaosha: false
    }
  },
  methods: {
    // 上传成功钩子函数
    handleAvatarSuccess (res, file) {
      // 设置展示图片地址
      this.imageUrl = URL.createObjectURL(file.raw)
    },
    // 上传之前钩子函数,检查是否符合要求
    beforeAvatarUpload (file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    }
  }
}
</script>

<style lang="scss" scoped>
// 步骤条
.el-step {
  // 手型鼠标
  cursor: pointer
}
// 上传组件样式,使用::v-deep进行深度设置样式
::v-deep .avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
::v-deep .avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

数据绑定

使用保存或更新课程信息接口,根据里面的接口信息设置名称

src/views/course/add.vue  将全部form组件和数据绑定,这里先暂时注释掉一些暂时用不到的数据,后期可能会用到

<template>
  <!-- 容器 -->
  <el-card>
    <!-- 顶部导航 -->
    <div slot="header" class="clearfix">
      <!-- 步骤条组件 -->
      <el-steps :active="nowSteps" simple>
        <!-- 循环遍历数据添加结构,添加点击事件(更改当前步骤) -->
        <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon" @click.native="nowSteps = item.id"></el-step>
      </el-steps>
    </div>
    <!-- 底部表单 -->
    <el-form ref="form" :model="form" label-width="80px">
      <!-- 基本信息 -->
      <div v-show="nowSteps === 0">
        <el-form-item label="课程名称">
          <el-input v-model="form.courseName" placeholder="请输入课程名称"></el-input>
        </el-form-item>
        <el-form-item label="课程简介">
          <el-input v-model="form.brief" placeholder="请输入课程简介"></el-input>
        </el-form-item>
        <el-form-item label="课程概述">
          <!-- 设置样式让两个输入框在一行显示 -->
          <el-input v-model="form.previewFirstField" style="width: 49%; minwidth: 300px; margin-right: 10px" placeholder="请输入第一条课程概述">
            <!-- 使用slot="append"在后天添加单位或者描述 -->
            <template slot="append">0 / 20</template>
          </el-input>
          <el-input v-model="form.previewSecondField" style="width: 49%; minwidth: 300px;" placeholder="请输入第二条课程概述"></el-input>
        </el-form-item>
        <el-form-item label="讲师姓名">
          <el-input v-model="form.teacherDTO.teacherName" placeholder="请输入讲师姓名"></el-input>
        </el-form-item>
        <el-form-item label="讲师简介">
          <el-input v-model="form.teacherDTO.description" placeholder="请输入讲师简介"></el-input>
        </el-form-item>
        <el-form-item label="课程排序">
          <el-input-number v-model="form.sortNum" controls-position="right" :min="1"></el-input-number>
        </el-form-item>
      </div>
      <!-- 课程封面 -->
      <div v-show="nowSteps === 1">
        <el-form-item label="课程封面">
          <!-- 上传组件 -->
          <el-upload
            class="avatar-uploader"
            action="https://jsonplaceholder.typicode.com/posts/"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload">
            <img v-if="form.courseListImg" :src="form.courseListImg" class="avatar">
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
        <el-form-item label="解锁封面">
          <el-upload
            class="avatar-uploader"
            action="https://jsonplaceholder.typicode.com/posts/"
            :show-file-list="false"
            :on-success="handleAvatarSuccess2"
            :before-upload="beforeAvatarUpload">
            <img v-if="form.courseImgUrl" :src="form.courseImgUrl" class="avatar">
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
      </div>
      <!-- 销售信息 -->
      <div v-show="nowSteps === 2">
        <el-form-item label="售卖价格">
          <el-input v-model="form.discounts" placeholder="请输入售卖价格">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="商品原价">
          <el-input v-model="form.price" placeholder="请输入商品原价">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="销量">
          <el-input v-model="form.sales" placeholder="请输入销量">
            <template slot="append">单</template>
          </el-input>
        </el-form-item>
        <el-form-item label="活动标签">
          <el-input v-model="form.discountsTag" placeholder="请输入活动标签"></el-input>
        </el-form-item>
      </div>
      <!-- 限时秒杀 -->
      <div v-show="nowSteps === 3">
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="限时秒杀开关" label-width="120px">
          <el-switch
            v-model="form.activityCourse"
            active-color="#409eff"
            >
          </el-switch>
        </el-form-item>
        <!-- 限时秒杀信息,根据秒杀开关是否打开决定是否显示 -->
        <div v-if="form.activityCourse">
          <el-form-item label="开始时间">
            <el-date-picker
              v-model="form.activityCourseDTO.beginTime"
              type="datetime"
              placeholder="选择开始日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="结束时间">
            <el-date-picker
              v-model="form.activityCourseDTO.endTime"
              type="datetime"
              placeholder="选择结束日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="秒杀价">
            <el-input v-model="form.activityCourseDTO.amount" placeholder="请输入秒杀价">
              <template slot="append">元</template>
            </el-input>
          </el-form-item>
          <el-form-item label="库存">
            <el-input v-model="form.activityCourseDTO.stock" placeholder="请输入库存">
              <template slot="append">个</template>
            </el-input>
          </el-form-item>
        </div>
      </div>
      <!-- 课程详情 -->
      <div v-show="nowSteps === 4">
        <!-- 这里使用文本域,后期换成富文本编辑器 -->
        <el-form-item label="课程详情" label-width="120px">
          <el-input
            type="textarea"
            :rows="12"
            placeholder="请输入课程详细描述信息"
            v-model="form.courseDescriptionMarkDown">
          </el-input>
        </el-form-item>
      </div>
      <!-- 最下方按钮 -->
      <el-form-item>
        <!-- 下一步按钮前四步都显示可跳转到下一步 -->
        <el-button v-show="nowSteps < 4" @click="nowSteps += 1" type="primary">下一步</el-button>
        <!-- 保存按钮只有最后一步才显示,用于保存数据 -->
        <el-button v-show="nowSteps === 4"  type="primary">保存</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
export default {
  name: 'addCourse',
  data () {
    return {
      // 当前步骤
      nowSteps: 0,
      // 步骤条组件数据
      steps: [
        { id: 0, title: '基本信息', icon: 'el-icon-edit' },
        { id: 1, title: '课程封面', icon: 'el-icon-picture-outline' },
        { id: 2, title: '销售信息', icon: 'el-icon-shopping-cart-full' },
        { id: 3, title: '秒杀信息', icon: 'el-icon-present' },
        { id: 4, title: '课程详情', icon: 'el-icon-document' }
      ],
      // form表单数据
      form: {
        // id: 0,
        // 课程名称
        courseName: '',
        // 课程简介
        brief: '',
        teacherDTO: {
          // id: 0,
          // courseId: 0,
          // 讲师姓名
          teacherName: '',
          // teacherHeadPicUrl: '',
          // position: '',
          // 讲师简介
          description: ''
        },
        // 课程详情/描述信息
        courseDescriptionMarkDown: '',
        // 原价
        price: null,
        // 优惠价格
        discounts: null,
        // priceTag: '',
        // 活动标签
        discountsTag: '',
        // isNew: true,
        // isNewDes: '',
        // 课程列表展示图片
        courseListImg: '',
        // 课程解锁封面
        courseImgUrl: '',
        // 课程排序
        sortNum: 0,
        // 课程概述/预览第一段
        previewFirstField: '',
        // 课程概述/预览第二段
        previewSecondField: '',
        // 当课程前状态
        status: 0,
        // 销量
        sales: null,
        // 是否开启秒杀
        activityCourse: true,
        activityCourseDTO: {
          // id: 0,
          // courseId: 0,
          // 开始时间
          beginTime: '',
          // 结束时间
          endTime: '',
          // 秒杀价格
          amount: null,
          // 库存
          stock: null
        }
        // autoOnlineTime: ''
      }
    }
  },
  methods: {
    // 上传课程封面成功钩子函数
    handleAvatarSuccess (res, file) {
      this.form.courseListImg = URL.createObjectURL(file.raw)
    },
    // 上传解锁封面成功钩子函数
    handleAvatarSuccess2 (res, file) {
      this.form.courseImgUrl = URL.createObjectURL(file.raw)
    },
    // 上传之前钩子函数,检查是否符合要求
    beforeAvatarUpload (file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    }
  }
}
</script>

<style lang="scss" scoped>
// 步骤条
.el-step {
  // 手型鼠标
  cursor: pointer
}
// 上传组件样式,使用::v-deep进行深度设置样式
::v-deep .avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
::v-deep .avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

基本信息

设置输入框计数(上一步已经包括了)

封面上传

使用上传图片接口,获取上传的图片地址添加到上传的数据中

注意上传图片的data应该是formdata对象(new FormDate.append('file',data)

使用自定义上传,自带的上传太复杂,函数提供了上传文件的相关信息,直接使用即可

封装上传功能组件,使用数据的双向绑定

// src/services/coures.js 封装上传封面接口
..................
// 上传图片
export const upLoad = data => {
  return request({
    method: 'post',
    url: '/boss/course/upload',
    data
  })
}
src/views/course/add.vue  将上传组件单独封装,然后add单独引入

<div v-show="nowSteps === 1">
  <!-- 创建子组件实例,使用v-model双向绑定 -->
  <!-- 课程封面 -->
  <upload-img v-model="form.courseListImg" title="课程封面"></upload-img>
  <!-- 解锁封面 -->
  <upload-img v-model="form.courseImgUrl" title="解锁封面"></upload-img>
</div>


// 引入子组件
import uploadImg from './son/uploadImg'

// 注册组件
components: {
  uploadImg
}
src/views/course/son/uploadImg.vue  新建自定义组件封装上传图片组件

<template>
  <el-form-item :label="title">
    <!-- 上传组件
      http-request:自定义上传
      on-success:上传成功触发
      before-upload:上传之前触发
    -->
    <el-upload
      :http-request="httpRequest"
      class="avatar-uploader"
      action=""
      :show-file-list="false"
      :on-success="handleAvatarSuccess"
      :before-upload="beforeAvatarUpload">
      <img v-if="imageUrl" :src="imageUrl" class="avatar">
      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  </el-form-item>
</template>

<script>
// 引入封装的图片上传接口
import { upLoad } from '@/services/coures'
export default {
  // 接收参数
  props: {
    title: {
      type: String
    },
    // value是v-model绑定的数据
    value: {
      type: String
    }
  },
  data () {
    return {
      // 上传后预览地址
      imageUrl: ''
    }
  },
  methods: {
    // 自定义上传
    // 参数:组件自动把上传的信息打包作为参数
    async httpRequest (obj) {
      // 创建FormData实例
      const fd = new FormData()
      // 向FormData添加文件信息
      fd.append('file', obj.file)
      // 调用接口上传图片
      const { data } = await upLoad(fd)
      if (data.code === '000000') {
        // 如果上传成功,利用v-model的自定义事件将地址传递给父组件
        this.$emit('input', data.data.name)
      }
    },
    // 上传课程封面成功钩子函数
    handleAvatarSuccess (res, file) {
      this.imageUrl = URL.createObjectURL(file.raw)
    },
    // 上传之前钩子函数,检查是否符合要求
    beforeAvatarUpload (file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    }
  }
}
</script>

<style scoped lang="scss">
// 上传组件样式,使用::v-deep进行深度设置样式
::v-deep .avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
::v-deep .avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

上传进度、进度百分比显示

使用进度条组件

上传的时候隐藏图片组件,上传完毕隐藏进度条显示图片

上传组件提供进度显示,但是我们自定义上传,导致这个功能会失效

h5的ajax中提供了onUploadProgress用于检测上传进度

上传完毕设置进度归0,上传为100后显示绿色成功

// src/services/coures.js 修改上传图片接口,添加一个用于获取进度的方法

// 上传图片接口,接受第二个参数一个函数
export const upLoad = (data, onUploadProgress) => {
  return request({
    method: 'post',
    url: '/boss/course/upload',
    data,
    // onUploadProgress是H5提供的用于检测上传进度的方法
    onUploadProgress
  })
}
src/views/course/son/uploadImg.vue  添加进度条组件,并且获取上传进度在进度条上实时显示

<template>
  <el-form-item :label="title">
    <!-- 进度条组件,使用v-if控制显示和隐藏,设置进度条和上传组件不可同时出现 -->
    <el-progress v-if="upLoading" type="circle" :percentage="percentage" :width="178"></el-progress>
    <!-- 上传组件
      http-request:自定义上传
      on-success:上传成功触发
      before-upload:上传之前触发
    -->
    <el-upload
      v-else
      :http-request="httpRequest"
      class="avatar-uploader"
      action=""
      :show-file-list="false"
      :on-success="handleAvatarSuccess"
      :before-upload="beforeAvatarUpload">
      <img v-if="imageUrl" :src="imageUrl" class="avatar">
      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  </el-form-item>
</template>

<script>
// 引入封装的图片上传接口
import { upLoad } from '@/services/coures'
export default {
  name: 'uploadImg',
  // 接收参数
  props: {
    title: {
      type: String
    },
    // value是v-model绑定的数据
    value: {
      type: String
    }
  },
  data () {
    return {
      // 上传后预览地址
      imageUrl: '',
      // 进度条进度
      percentage: 0,
      // 信号值 - 表示当前是否正在上传
      upLoading: false
    }
  },
  methods: {
    // 用于获取上传中的进度数据
    onUploadProgress (opt) {
      // 修改本地进度条进度数据
      this.percentage = Math.floor(opt.loaded / opt.total * 100)
    },
    // 自定义上传
    // 参数:组件自动把上传的信息打包作为参数
    async httpRequest (obj) {
      // 创建FormData实例
      const fd = new FormData()
      // 向FormData添加文件信息
      fd.append('file', obj.file)
      // 调用接口上传图片,并且带上获取进度的函数
      const { data } = await upLoad(fd, this.onUploadProgress)
      if (data.code === '000000') {
        // 如果上传成功,利用v-model的自定义事件将地址传递给父组件
        this.$emit('input', data.data.name)
      }
    },
    // 上传课程封面成功钩子函数
    handleAvatarSuccess (res, file) {
      this.imageUrl = URL.createObjectURL(file.raw)
      this.upLoading = false
      this.percentage = 0
    },
    // 上传之前钩子函数,检查是否符合要求
    beforeAvatarUpload (file) {
      this.upLoading = true
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    }
  }
}
</script>

<style scoped lang="scss">
// 上传组件样式,使用::v-deep进行深度设置样式
::v-deep .avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
::v-deep .avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

销售、秒杀数据绑定

绑定相关数据(上面步骤已经进行了绑定)

保存功能

最后把不需要的数据注释掉即可,因为新增课程不需要,这是编辑的时候才需要额

可以添加一个开关处理是否默认上架

保存后跳转回列表页

src/services/coures.js  封装保存更新课程接口

// 保存或者更新课程信息
export const saveOrUpdateCourse = data => {
  return request({
    method: 'post',
    url: '/boss/course/saveOrUpdateCourse',
    data
  })
}
<template>
  <!-- 容器 -->
  <el-card>
    <!-- 顶部导航 -->
    <div slot="header" class="clearfix">
      <!-- 步骤条组件 -->
      <el-steps :active="nowSteps" simple>
        <!-- 循环遍历数据添加结构,添加点击事件(更改当前步骤) -->
        <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon" @click.native="nowSteps = item.id"></el-step>
      </el-steps>
    </div>
    <!-- 底部表单 -->
    <el-form ref="form" :model="form" label-width="80px">
      <!-- 基本信息 -->
      <div v-show="nowSteps === 0">
        <el-form-item label="课程名称">
          <el-input v-model="form.courseName" placeholder="请输入课程名称"></el-input>
        </el-form-item>
        <el-form-item label="课程简介">
          <el-input v-model="form.brief" placeholder="请输入课程简介"></el-input>
        </el-form-item>
        <el-form-item label="课程概述">
          <!-- 设置样式让两个输入框在一行显示 -->
          <el-input v-model="form.previewFirstField" style="width: 49%; minwidth: 300px; margin-right: 10px" placeholder="请输入第一条课程概述">
            <!-- 使用slot="append"在后天添加单位或者描述 -->
            <template slot="append">0 / 20</template>
          </el-input>
          <el-input v-model="form.previewSecondField" style="width: 49%; minwidth: 300px;" placeholder="请输入第二条课程概述"></el-input>
        </el-form-item>
        <el-form-item label="讲师姓名">
          <el-input v-model="form.teacherDTO.teacherName" placeholder="请输入讲师姓名"></el-input>
        </el-form-item>
        <el-form-item label="讲师简介">
          <el-input v-model="form.teacherDTO.description" placeholder="请输入讲师简介"></el-input>
        </el-form-item>
        <el-form-item label="课程排序">
          <el-input-number v-model="form.sortNum" controls-position="right" :min="1"></el-input-number>
        </el-form-item>
      </div>
      <!-- 课程封面 -->
      <div v-show="nowSteps === 1">
        <!-- 创建子组件实例,使用v-model双向绑定 -->
        <!-- 课程封面 -->
        <upload-img v-model="form.courseListImg" title="课程封面"></upload-img>
        <!-- 解锁封面 -->
        <upload-img v-model="form.courseImgUrl" title="解锁封面"></upload-img>
      </div>
      <!-- 销售信息 -->
      <div v-show="nowSteps === 2">
        <el-form-item label="售卖价格">
          <el-input v-model="form.discounts" placeholder="请输入售卖价格">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="商品原价">
          <el-input v-model="form.price" placeholder="请输入商品原价">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="销量">
          <el-input v-model="form.sales" placeholder="请输入销量">
            <template slot="append">单</template>
          </el-input>
        </el-form-item>
        <el-form-item label="活动标签">
          <el-input v-model="form.discountsTag" placeholder="请输入活动标签"></el-input>
        </el-form-item>
      </div>
      <!-- 限时秒杀 -->
      <div v-show="nowSteps === 3">
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="限时秒杀开关" label-width="120px">
          <el-switch
            v-model="form.activityCourse"
            active-color="#409eff"
            >
          </el-switch>
        </el-form-item>
        <!-- 限时秒杀信息,根据秒杀开关是否打开决定是否显示 -->
        <div v-if="form.activityCourse">
          <el-form-item label="开始时间">
            <el-date-picker
              v-model="form.activityCourseDTO.beginTime"
              type="datetime"
              placeholder="选择开始日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="结束时间">
            <el-date-picker
              v-model="form.activityCourseDTO.endTime"
              type="datetime"
              placeholder="选择结束日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="秒杀价">
            <el-input v-model="form.activityCourseDTO.amount" placeholder="请输入秒杀价">
              <template slot="append">元</template>
            </el-input>
          </el-form-item>
          <el-form-item label="库存">
            <el-input v-model="form.activityCourseDTO.stock" placeholder="请输入库存">
              <template slot="append">个</template>
            </el-input>
          </el-form-item>
        </div>
      </div>
      <!-- 课程详情 -->
      <div v-show="nowSteps === 4">
        <!-- 这里使用文本域,后期换成富文本编辑器 -->
        <el-form-item label="课程详情" label-width="120px">
          <el-input
            type="textarea"
            :rows="12"
            placeholder="请输入课程详细描述信息"
            v-model="form.courseDescriptionMarkDown">
          </el-input>
        </el-form-item>
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="立即上架" label-width="120px">
          <el-switch
            v-model="form.status"
            active-color="#409eff"
            :active-value="1"
            :inactive-value="0"
            >
          </el-switch>
        </el-form-item>
      </div>
      <!-- 最下方按钮 -->
      <el-form-item>
        <!-- 下一步按钮前四步都显示可跳转到下一步 -->
        <el-button v-show="nowSteps < 4" @click="nowSteps += 1" type="primary">下一步</el-button>
        <!-- 保存按钮只有最后一步才显示,用于保存数据,添加点击事件 -->
        <el-button v-show="nowSteps === 4"  type="primary" @click="preservationCoures">保存</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
// 引入子组件
import uploadImg from './son/uploadImg'
// 引入封装的保存课程功能
import { saveOrUpdateCourse } from '@/services/coures'
export default {
  name: 'addCourse',
  data () {
    return {
      // 当前步骤
      nowSteps: 0,
      // 步骤条组件数据
      steps: [
        { id: 0, title: '基本信息', icon: 'el-icon-edit' },
        { id: 1, title: '课程封面', icon: 'el-icon-picture-outline' },
        { id: 2, title: '销售信息', icon: 'el-icon-shopping-cart-full' },
        { id: 3, title: '秒杀信息', icon: 'el-icon-present' },
        { id: 4, title: '课程详情', icon: 'el-icon-document' }
      ],
      // form表单
      form: {
        // id: 0,
        // 课程名称
        courseName: '',
        // 课程简介
        brief: '',
        teacherDTO: {
          // id: 0,
          // courseId: 0,
          // 讲师姓名
          teacherName: '',
          // teacherHeadPicUrl: '',
          // position: '',
          // 讲师简介
          description: ''
        },
        // 课程详情/描述信息
        courseDescriptionMarkDown: '',
        // 原价
        price: null,
        // 优惠价格
        discounts: null,
        // priceTag: '',
        // 活动标签
        discountsTag: '',
        // isNew: true,
        // isNewDes: '',
        // 课程列表展示图片
        courseListImg: '',
        // 课程解锁封面
        courseImgUrl: '',
        // 课程排序
        sortNum: 0,
        // 课程概述/预览第一段
        previewFirstField: '',
        // 课程概述/预览第二段
        previewSecondField: '',
        // 当课程前状态
        status: 0,
        // 销量
        sales: null,
        // 是否开启秒杀
        activityCourse: false,
        activityCourseDTO: {
          // id: 0,
          // courseId: 0,
          // 开始时间
          beginTime: '',
          // 结束时间
          endTime: '',
          // 秒杀价格
          amount: null,
          // 库存
          stock: null
        }
        // autoOnlineTime: ''
      }
    }
  },
  // 注册组件
  components: {
    uploadImg
  },
  methods: {
    // 保存按钮点击事件函数
    async preservationCoures () {
      // 调用接口
      const { data } = await saveOrUpdateCourse(this.form)
      if (data.code === '000000') {
        // 如果保存成功,跳转回课程管理页面
        this.$router.push('/course')
      }
    }
  }
}
</script>

<style lang="scss" scoped>
// 步骤条
.el-step {
  // 手型鼠标
  cursor: pointer
}
</style>

课程详情富文本编辑器

github中的流行富文本编辑器

  • ckeditor
  • quill
  • ueditor(百度研发)
  • tinymce
  • wangeditor(本项目使用,比较新,维护度比较高,中文)

封装富文本编辑器

安装为开发依赖:npm i wangeditor --save

在src - components文件夹中创建公共的基础组件

引入组件并且绑定数据,输入参数,并且触发input事件输出值

输入和输出富文本编辑器都提供了方法

src/components/editor.vue  单独创建文件夹存放公共组件,创建editor.vue单独封装富文本编辑器

<template>
  <!-- editor根标签 -->
  <div ref="div"></div>
</template>

<script>
// 引入富文本编辑器
import E from 'wangeditor'
export default {
  name: 'editor',
  // 钩子函数
  mounted () {
    // 调用函数
    this.initEditor()
  },
  methods: {
    // editor初始化函数
    initEditor () {
      // 创建实例,利用ref获得元素
      const editor = new E(this.$refs.div)
      editor.create()
      // 设置内容,暂时设置为空
      editor.txt.html('')
      // 设置change事件
      editor.config.onchange = newHtml => {
        // 事件触发后利用v-model把新数据传递回父元素
        this.$emit('input', newHtml)
      }
    }
  }
}
</script>

src/views/course/add.vue  引入富文本编辑器组件并且绑定数据

<div v-show="nowSteps === 4">
  <el-form-item label="课程详情">
    <!-- 创建富文本编辑器组件实例 -->
    <editor v-model="form.courseDescriptionMarkDown"></editor>
  </el-form-item>
  
// 引入富文本编辑器
import editor from '@/components/editor'
  
// 注册组件
  components: {
    uploadImg,
    editor
  }

富文本编辑器图片上传

默认富文本编辑器只能插入图片地址,我们可以使用富文本编辑器的图片上传功能上传本地图片

使用自己上传图片功能,因为默认的上传功能对接口有一定要求

// src/components/editor.vue  引入上传图片接口,实现上传功能


// 引入封装的图片上传接口
import { upLoad } from '@/services/coures'


// editor初始化函数
  initEditor () {
    // 创建实例,利用ref获得元素
    const editor = new E(this.$refs.div)
    
    // 上传图片配置要写在editor.create()之前!!!!!!!!!!!!!!!!!!
    editor.config.customUploadImg = async (resultFiles, insertImgFn) => {
      // 创建一个空的图片地址
      let imgUrl = ''
      // 创建FormData实例
      const fd = new FormData()
      // 向FormData添加文件信息,resultFiles[0]是上传图片信息列表(如果想多个上传,可以遍历resultFiles)
      fd.append('file', resultFiles[0])
      // 调用接口上传图片,并且带上获取进度的函数
      const { data } = await upLoad(fd, this.onUploadProgress)
      if (data.code === '000000') {
        // 如果上传成功修改url地址
        imgUrl = data.data.name
      }
      // 调用第二个参数实现插入
      insertImgFn(imgUrl)
    }
    
    editor.create()
    // 设置内容,暂时设置为空
    editor.txt.html('')
    // 设置change事件
    editor.config.onchange = newHtml => {
      // 事件触发后利用v-model把新数据传递回父元素
      this.$emit('input', newHtml)
    }
  }

编辑组件

编辑组件和添加基本相同,可以把相同部分抽离出来单独封装

设置组件 书写路由 设置路由路径参数

使用通过课程ID获取课程信息接口

// src/services/coures.js  添加查询课程信息接口

// 根据课程id查询课程信息
export const getCourseById = courseId => {
  return request({
    method: 'get',
    url: `/boss/course/getCourseById?courseId=${courseId}`
  })
}
src/views/course/edit.vue  新建编辑课程页面,与添加课程共用子组件

<template>
  <div class="addCourse">
    <!-- 标题 -->
    <div style="margin: 0 0 10px 0">编辑课程</div>
    <!-- 创建子组件实例 -->
    <addOrEdit :courseId="courseId"></addOrEdit>
  </div>
</template>

<script>
// 引入子组件
import addOrEdit from './son/addOrEdit'
export default {
  name: 'edit',
  // 引入参数
  props: ['courseId'],
  // 注册组件
  components: {
    addOrEdit
  }
}
</script>

下面两个就不贴出来了:
把编辑课程组件添加进路由
在管理主页面的编辑上添加点击事件,带着课程id参数跳转到编辑页面
src/views/course/son/addOrEdit.vue  

<template>
  <!-- 容器 -->
  <el-card>
    <!-- 顶部导航 -->
    <div slot="header" class="clearfix">
      <!-- 步骤条组件 -->
      <el-steps :active="nowSteps" simple>
        <!-- 循环遍历数据添加结构,添加点击事件(更改当前步骤) -->
        <el-step v-for="item in steps" :key="item.id" :title="item.title" :icon="item.icon" @click.native="nowSteps = item.id"></el-step>
      </el-steps>
    </div>
    <!-- 底部表单 -->
    <el-form ref="form" :model="form" label-width="80px">
      <!-- 基本信息 -->
      <div v-show="nowSteps === 0">
        <el-form-item label="课程名称">
          <el-input v-model="form.courseName" placeholder="请输入课程名称"></el-input>
        </el-form-item>
        <el-form-item label="课程简介">
          <el-input v-model="form.brief" placeholder="请输入课程简介"></el-input>
        </el-form-item>
        <el-form-item label="课程概述">
          <!-- 设置样式让两个输入框在一行显示 -->
          <el-input v-model="form.previewFirstField" style="width: 49%; minwidth: 300px; margin-right: 10px" placeholder="请输入第一条课程概述">
            <!-- 使用slot="append"在后天添加单位或者描述 -->
            <template slot="append">0 / 20</template>
          </el-input>
          <el-input v-model="form.previewSecondField" style="width: 49%; minwidth: 300px;" placeholder="请输入第二条课程概述"></el-input>
        </el-form-item>
        <el-form-item label="讲师姓名">
          <el-input v-model="form.teacherDTO.teacherName" placeholder="请输入讲师姓名"></el-input>
        </el-form-item>
        <el-form-item label="讲师简介">
          <el-input v-model="form.teacherDTO.description" placeholder="请输入讲师简介"></el-input>
        </el-form-item>
        <el-form-item label="课程排序">
          <el-input-number v-model="form.sortNum" controls-position="right" :min="1"></el-input-number>
        </el-form-item>
      </div>
      <!-- 课程封面 -->
      <div v-show="nowSteps === 1">
        <!-- 创建子组件实例,使用v-model双向绑定 -->
        <!-- 课程封面 -->
        <upload-img v-model="form.courseListImg" title="课程封面"></upload-img>
        <!-- 解锁封面 -->
        <upload-img v-model="form.courseImgUrl" title="解锁封面"></upload-img>
      </div>
      <!-- 销售信息 -->
      <div v-show="nowSteps === 2">
        <el-form-item label="售卖价格">
          <el-input v-model="form.discounts" placeholder="请输入售卖价格">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="商品原价">
          <el-input v-model="form.price" placeholder="请输入商品原价">
            <template slot="append">元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="销量">
          <el-input v-model="form.sales" placeholder="请输入销量">
            <template slot="append">单</template>
          </el-input>
        </el-form-item>
        <el-form-item label="活动标签">
          <el-input v-model="form.discountsTag" placeholder="请输入活动标签"></el-input>
        </el-form-item>
      </div>
      <!-- 限时秒杀 -->
      <div v-show="nowSteps === 3">
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="限时秒杀开关" label-width="120px">
          <el-switch
            v-model="form.activityCourse"
            active-color="#409eff"
            >
          </el-switch>
        </el-form-item>
        <!-- 限时秒杀信息,根据秒杀开关是否打开决定是否显示 -->
        <div v-if="form.activityCourse">
          <el-form-item label="开始时间">
            <el-date-picker
              v-model="form.activityCourseDTO.beginTime"
              type="datetime"
              placeholder="选择开始日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="结束时间">
            <el-date-picker
              v-model="form.activityCourseDTO.endTime"
              type="datetime"
              placeholder="选择结束日期时间">
            </el-date-picker>
          </el-form-item>
          <el-form-item label="秒杀价">
            <el-input v-model="form.activityCourseDTO.amount" placeholder="请输入秒杀价">
              <template slot="append">元</template>
            </el-input>
          </el-form-item>
          <el-form-item label="库存">
            <el-input v-model="form.activityCourseDTO.stock" placeholder="请输入库存">
              <template slot="append">个</template>
            </el-input>
          </el-form-item>
        </div>
      </div>
      <!-- 课程详情 -->
      <div v-show="nowSteps === 4">
        <el-form-item label="课程详情">
          <!-- 创建富文本编辑器组件实例 -->
          <editor v-model="form.courseDescriptionMarkDown"></editor>
        </el-form-item>
        <!-- 开关组件,绑定数据 -->
        <el-form-item label="立即上架">
          <el-switch
            v-model="form.status"
            active-color="#409eff"
            :active-value="1"
            :inactive-value="0"
            >
          </el-switch>
        </el-form-item>
      </div>
      <!-- 最下方按钮 -->
      <el-form-item>
        <!-- 下一步按钮前四步都显示可跳转到下一步 -->
        <el-button v-show="nowSteps < 4" @click="nowSteps += 1" type="primary">下一步</el-button>
        <!-- 保存按钮只有最后一步才显示,用于保存数据,添加点击事件 -->
        <el-button v-show="nowSteps === 4"  type="primary" @click="preservationCoures">保存</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
// 引入富文本编辑器
import editor from '@/components/editor'
// 引入子组件
import uploadImg from './uploadImg'
// 引入封装的保存课程功能
import { saveOrUpdateCourse, getCourseById } from '@/services/coures'
export default {
  name: 'addOrEdit',
  props: ['courseId'],
  data () {
    return {
      // 当前步骤
      nowSteps: 0,
      // 步骤条组件数据
      steps: [
        { id: 0, title: '基本信息', icon: 'el-icon-edit' },
        { id: 1, title: '课程封面', icon: 'el-icon-picture-outline' },
        { id: 2, title: '销售信息', icon: 'el-icon-shopping-cart-full' },
        { id: 3, title: '秒杀信息', icon: 'el-icon-present' },
        { id: 4, title: '课程详情', icon: 'el-icon-document' }
      ],
      // form表单
      form: {
        // id: 0,
        // 课程名称
        courseName: '',
        // 课程简介
        brief: '',
        teacherDTO: {
          // id: 0,
          // courseId: 0,
          // 讲师姓名
          teacherName: '',
          // teacherHeadPicUrl: '',
          // position: '',
          // 讲师简介
          description: ''
        },
        // 课程详情/描述信息
        courseDescriptionMarkDown: '',
        // 原价
        price: null,
        // 优惠价格
        discounts: null,
        // priceTag: '',
        // 活动标签
        discountsTag: '',
        // isNew: true,
        // isNewDes: '',
        // 课程列表展示图片
        courseListImg: '',
        // 课程解锁封面
        courseImgUrl: '',
        // 课程排序
        sortNum: 0,
        // 课程概述/预览第一段
        previewFirstField: '',
        // 课程概述/预览第二段
        previewSecondField: '',
        // 当课程前状态
        status: 0,
        // 销量
        sales: null,
        // 是否开启秒杀
        activityCourse: false,
        activityCourseDTO: {
          // id: 0,
          // courseId: 0,
          // 开始时间
          beginTime: '',
          // 结束时间
          endTime: '',
          // 秒杀价格
          amount: 0,
          // 库存
          stock: 0
        }
        // autoOnlineTime: ''
      }
    }
  },
  // 钩子函数
  created () {
    // 判断是否带着课程id,如果带着id调用方法获取id信息
    if (this.courseId) {
      this.getCourse(this.courseId)
    }
  },
  // 注册组件
  components: {
    uploadImg,
    editor
  },
  methods: {
    // 根据id获取课程信息
    async getCourse (courseId) {
      // 调用接口或获取信息
      const { data } = await getCourseById(courseId)
      if (data.code === '000000') {
        // 如果获取成功,判断是否开启了秒杀
        if (!data.data.activityCourse) {
          // 如果没有开启秒杀,补全秒杀需要的数据,防止报错
          data.data.activityCourseDTO = {
            beginTime: '',
            endTime: '',
            amount: 0,
            stock: 0
          }
        }
        // 把数据交给表单
        this.form = data.data
      }
    },
    // 保存按钮点击事件函数
    async preservationCoures () {
      // 调用接口
      const { data } = await saveOrUpdateCourse(this.form)
      if (data.code === '000000') {
        // 如果保存成功,弹出提示信息,跳转回课程管理页面
        this.$router.push('/course')
      }
    }
  }
}
</script>

<style lang="scss" scoped>
// 步骤条
.el-step {
  // 手型鼠标
  cursor: pointer
}
</style>

编辑组件改进

(上一步已经设置)提示文字需要更改

(上一步已经设置)如果原本没有开启秒杀,我们编辑想开启秒杀就会报错,因为返回的数据不包含秒杀信息,读取不到 - 我们需要在获取数据的时候判断,如果没有开启秒杀需要手动添加结构

富文本编辑器无法展示数据,因为读文本编辑器获取数据是同步的(展示的是默认值),而获取后台数据是异步的 所以导致无法获取,设置监听数据变化(只有第一次加载的时候有问题,所以只需要在第一次加载的时候显示正确数据)

src/components/editor.vue  设置首次加载编辑器的时候自动获取数据

<template>
  <!-- editor根标签 -->
  <div ref="div"></div>
</template>

<script>
// 引入封装的图片上传接口
import { upLoad } from '@/services/coures'
// 引入富文本编辑器
import E from 'wangeditor'
export default {
  name: 'editor',
  // 参数
  props: ['value'],
  data () {
    return {
      // 信号值,是否已经加载完毕
      loading: false
    }
  },
  // 钩子函数
  mounted () {
    // 调用函数
    this.initEditor()
  },
  methods: {
    // editor初始化函数
    initEditor () {
      // 创建实例,利用ref获得元素
      const editor = new E(this.$refs.div)
      // 将editor设置给this,否则后面监听器使用不了
      this.editor = editor
      // 上传图片配置要写在editor.create()之前
      editor.config.customUploadImg = async (resultFiles, insertImgFn) => {
        // 创建一个空的图片地址
        let imgUrl = ''
        // 创建FormData实例
        const fd = new FormData()
        // 向FormData添加文件信息,resultFiles[0]是上传图片信息列表(如果想多个上传,可以遍历resultFiles)
        fd.append('file', resultFiles[0])
        // 调用接口上传图片,并且带上获取进度的函数
        const { data } = await upLoad(fd, this.onUploadProgress)
        if (data.code === '000000') {
          // 如果上传成功修改url地址
          imgUrl = data.data.name
        }
        // 调用第二个参数实现插入
        insertImgFn(imgUrl)
      }
      editor.create()
      // 设置内容,暂时设置为空
      editor.txt.html('')
      // 设置change事件
      editor.config.onchange = newHtml => {
        // 事件触发后利用v-model把新数据传递回父元素
        this.$emit('input', newHtml)
      }
    }
  },
  watch: {
    // 监听value变化,只有第一次加载的时候触发
    value () {
      if (!this.loading) {
        // 如果没有加载,那么设置默认展示信息
        this.editor.txt.html(this.value)
        // 修改信号值为true,这样以后在更改数据就不会触发
        this.loading = true
      }
    }
  }
}
</script>