美文网首页
课程管理(一)

课程管理(一)

作者: amanohina | 来源:发表于2021-03-15 16:05 被阅读0次

    该功能涉及到的因素更多,分为两部分记录过程
    功能分析:

    • 展示上架状态
    • 上下架操作
    • 添加课程
    • 编辑课程
    • 课程内容管理(第二部分再做讲解)
      基础布局等不再多做赘述

    course/index.vue 课程组件

    <template>
      <div class="course">
        <course-list></course-list>
      </div>
    </template>
    
    <script>
    import CourseList from './components/list.vue'
    
    export default {
      name: 'CourseIndex',
      components: {
        CourseList
      }
    }
    </script>
    
    <style lang="scss" scoped></style>
    

    course/components/list.vue(新建)

    <template>
      <div class="course-list">
        <el-card>
          <div slot="header">
            <span>数据筛选</span>
          </div>
          <el-form
            :inline="true"
            ref="form"
            label-position="left"
            :model="filterParams"
          >
            <el-form-item label="课程名称:" prop="courseName">
              <el-input v-model="filterParams.courseName"></el-input>
            </el-form-item>
            <el-form-item label="状态:" prop="status">
              <el-select v-model="filterParams.status">
                <el-option label="全部" value=""></el-option>
                <el-option label="上架" value="1"></el-option>
                <el-option label="下架" value="0"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button
                :disabled="isLoading"
                @click="handleReset"
              >重置</el-button>
              <el-button
                type="primary"
                :disabled="isLoading"
                @click="handleFilter"
              >查询</el-button>
            </el-form-item>
          </el-form>
        </el-card>
    
        <el-card>
          <div slot="header">
            <span>查询结果:</span>
            <el-button
              style="float: right; margin-top: -10px"
              type="primary"
            >添加课程</el-button>
          </div>
          <el-table
            :data="courses"
            v-loading="isLoading"
            style="width: 100%; margin-bottom: 20px"
          >
            <el-table-column
              prop="id"
              label="ID"
              width="100">
            </el-table-column>
            <el-table-column
              prop="courseName"
              label="课程名称"
              width="230">
            </el-table-column>
            <el-table-column
              prop="price"
              label="价格">
            </el-table-column>
            <el-table-column
              prop="sortNum"
              label="排序">
            </el-table-column>
            <el-table-column
              prop="status"
              label="上架状态">
            待处理
            </el-table-column>
            <el-table-column
              prop="price"
              label="操作"
              width="200"
              align="center"
            >
              <template>
                <el-button>编辑</el-button>
                <el-button>内容管理</el-button>
              </template>
            </el-table-column>
          </el-table>
          <el-pagination
            background
            layout="prev, pager, next"
            :total="totalCount"
            :disabled="isLoading"
            :current-page="filterParams.currentPage"
            @current-change="handleCurrentChange"
          />
        </el-card>
      </div>
    </template>
    
    <script>
    import { getQueryCourses } from '@/services/course'
    
    export default {
      name: 'CourseList',
      data () {
        return {
          // 筛选功能参数(表单数据)
          filterParams: {
            currentPage: 1,
            pageSize: 10,
            courseName: '',
            status: ''
          },
          // 课程信息
          courses: [],
          // 数据总条数
          totalCount: 0,
          // 加载状态
          isLoading: true
        }
      },
    
      created () {
        // 加载课程
        this.loadCourses()
      },
    
      methods: {
        // 加载课程
        async loadCourses () {
          this.isLoading = true
          const { data } = await getQueryCourses(this.filterParams)
          if (data.code === '000000') {
            // 保存课程信息
            this.courses = data.data.records
            this.totalCount = data.data.total
            this.isLoading = false
          }
        },
        // 分页页码点击操作
        handleCurrentChange (page) {
          this.filterParams.currentPage = page
          this.loadCourses()
        },
        // 筛选操作
        handleFilter () {
          this.filterParams.currentPage = 1
          this.loadCourses()
        },
        // 重置操作
        handleReset () {
          this.$refs.form.resetFields()
          this.filterParams.currentPage = 1
          this.loadCourses()
        }
      }
    }
    </script>
    
    <style lang="scss" scoped>
    .el-card {
      margin-bottom: 20px;
    }
    </style>
    

    services/course.js 课程接口模块(新建)

    // 分页查询课程信息
    export const getQueryCourses = data => {
      return request({
        method: 'POST',
        url: '/boss/course/getQueryCourses',
        data
      })
    }
    

    至此,准备工作已完成

    上下架功能

    上架状态展示

    使用Element的Switch开关组件进行设置,这样可以将状态展示和上下架操作结合为一个组件,操作更加直观了
    添加到上架状态对应的位置(上述代码中待处理标记)

    • 每条数据中的status代表上架状态,上架为1,下架为0
    • 通过v-model结合作用域插槽获取数据进行绑定
      • 由于组件默认通过布尔值判断,需要通过组件拓展的value类型进行设置


    上下架操作处理

    通过课程上下架接口操作:地址
    这里需要注意一点:为什么我这里不写data了?因为这块是GET请求方式,需要设置为params属性,这是axios内部的特点

    // services/course.js
    ...
    
    // 课程上下架
    export const changeState = params => {
      return request({
        method: 'GET',
        url: '/boss/course/changeState',
        params
      })
    }
    

    引入之后,切换开关时发送请求,通过文档得知,Switch组件具有change事件,进行设置。

    • 默认参数为切换后新的状态值,这里我们需要的是要切换的课程信息用于请求操作
    <el-switch
        ...
      @change="onStateChange(scope.row)">
    </el-switch>
    ... 
    <script>
      ...
        // 上下架按钮操作
        async onStateChange (course) {
          // 接收操作的课程对象,并发送请求更改上下架状态
          const { data } = await changeState({
            courseId: course.id,
            status: course.status
          })
          if (data.code === '000000') {
            this.$message.success(`${course.status === 0 ? '下架' : '上架'}成功`)
          }
        }
      }
    }
    </script>
    

    设置完毕之后,为了避免用户在一次上下架未完成时频繁点击,可以进行触发限制

    • 在请求课程信息后,在每条课程信息对象添加siStatusLoading属性
    // list.vue
    ...
    // 加载课程(准备工作中设置)
    async loadCourses () {
      this.isLoading = true
      const { data } = await getQueryCourses(this.filterParams)
      if (data.code === '000000') {
        // 给媒体数据设置属性,标识状态是否处于切换中,默认 false(本小节添加的功能)
        data.data.records.forEach(item => {
          item.isStatusLoading = false
        })
        // 保存课程信息
        this.courses = data.data.records
        this.totalCount = data.data.total
        this.isLoading = false
      }
    },
    ...
    

    将属性绑定Switch组件的disabled属性,当状态更改过程中,组件自动禁用

    // list.vue 
    ...
    <el-switch
      :disabled="scope.row.isStatusLoading"
      ...>
    </el-switch>
    ...
    

    最后呢,在请求操作过程中设置isStatusLoading属性值就可以了

    // list.vue
    ...
    // 上下架按钮操作
    async onStateChange (course) {
      // 请求发送前,更改课程操作状态
      course.isStatusLoading = true
      ...
      if (data.code === '000000') {
        ...
        // 请求完毕,更改课程操作状态
        course.isStatusLoading = false
      }
    }
    ...
    

    添加课程

    准备工作一如既往,course/中创建create.vue组件,并设置路由与list.vue点击的跳转操作

    // course/create.vue
    <template>
      <div class="course-create">
        <el-card>添加课程</el-card>
      </div>
    </template>
    
    <script>
    export default {
      name: 'CourseCreate'
    }
    </script>
    
    <style lang="scss" scoped>
    </style>
    
    // router/index.js
    ...
      {
        path: '/course/create',
        name: 'course-create',
        component: () => import(/* webpackChunkName: 'course-create' */ '@/views/course/create.vue')
      }
    ]
    
    // course/list.vue
    ...
    <el-button
      style="float: right; margin-top: -10px"
      type="primary"
      @click="$router.push({ name: 'course-create' })"
    >添加课程</el-button>
    ...
    

    步骤条设置

    对于功能比较多的操作,可以通过步骤条的方式引导用户操作,增强体验。
    使用的是Element的Steps步骤条组件进行处理,同时将create.vue的头部区域内写入该组件,将active动态绑定,以后在操作中可以更改步骤条的进度

    // create.vue
    <template>
      <div class="course-create">
        <el-card>
          <!-- 设置 slot 后 Element 会自动设置为上下两部分的布局样式(具有分割线) -->
          <div slot="header">
            <el-steps :active="activeStep" simple>
              <el-step title="基本信息" icon="el-icon-edit"></el-step>
              <el-step title="课程封面" icon="el-icon-upload"></el-step>
              <el-step title="销售信息" icon="el-icon-picture"></el-step>
              <el-step title="秒杀信息" icon="el-icon-picture"></el-step>
              <el-step title="课程详情" icon="el-icon-picture"></el-step>
            </el-steps>
          </div>
        </el-card>
      </div>
    </template>
    ...
    <script>
    ...
      data () {
        return {
          // 步骤条进度
          activeStep: 0
        }
      }
    }
    </script>
    

    由于步骤条的每一部分都是非常类似的结构,所以我们建议将数据保存到data中,结构更改为遍历创建的方式(这里由于没有进行详细的样式设计所以后期需要自行修改)

    // create.vue
    ...
    <el-steps :active="activeStep" simple>
      <el-step
        v-for="(item, i) in steps"
        :key="item.id"
        :title="item.title"
        :icon="item.icon"
      ></el-step>
    </el-steps>
    ...
    <script>
    export default {
      ...
          steps: [
            { id: 1, title: '基本信息', icon: 'el-icon-edit' },
            { id: 2, title: '课程封面', icon: 'el-icon-upload' },
            { id: 3, title: '销售信息', icon: 'el-icon-picture' },
            { id: 4, title: '秒杀信息', icon: 'el-icon-picture' },
            { id: 5, title: '课程详情', icon: 'el-icon-picture' }
          ]
    ...
    

    给不同步骤设置对应的布局容器

    • 根据activeStep设置对应容器的显示和隐藏
    • 设置下一步按钮,点击后切换功能模块
    • 操作到最后一步,隐藏下一步按钮,并且设置提交按钮
    // create.vue
    ...
    <el-card>
      ...
      <!-- 步骤容器 -->
      <el-form>
        <div v-show="activeStep === 0">
          基本信息
        </div>
        <div v-show="activeStep === 1">
          课程封面
        </div>
        <div v-show="activeStep === 2">
          销售信息
        </div>
        <div v-show="activeStep === 3">
          秒杀活动
        </div>
        <div v-show="activeStep === 4">
          课程详情
          <!-- 最后步骤中设置保存按钮 -->
          <el-form-item>
            <el-button type="primary">保存</el-button>
          </el-form-item>
        </div>
        <!-- 下一步 -->
        <el-form-item v-if="activeStep !== steps.length - 1">
          <el-button @click="activeStep++">下一步</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    

    点击步骤标题按钮,跳转到对应的步骤,并修改鼠标样式

    • 由于组件没有click事件应添加.native设置原生事件
    • 设置样式,修改鼠标样式
    // create.vue
    ...
    <el-steps :active="activeStep" simple>
      <el-step 
            ...
        @click.native="activeStep = i"
        ></el-step>
    </el-steps>
    ...
    
    <style lang="scss" scoped>
    .el-step {
      cursor: pointer
    }
    </style>
    

    表单结构搭建

    基本信息

    完善表单结构(封面是在第二步骤)

    // create.vue
    ...
    <div v-show="activeStep === 0">
      <el-form-item label="课程名称">
        <el-input></el-input>
      </el-form-item>
      <el-form-item label="课程简介">
        <el-input></el-input>
      </el-form-item>
      <el-form-item label="课程概述">
        <el-input></el-input>
      </el-form-item>
      <el-form-item label="讲师姓名">
        <el-input></el-input>
      </el-form-item>
      <el-form-item label="讲师简介">
        <el-input></el-input>
      </el-form-item>
      <el-form-item label="课程排序">
        <!-- 计数器组件 -->
        <el-input-number
          label="描述文字"
        ></el-input-number>
      </el-form-item>
    </div>
    ...
    

    课程封面
    使用Element的Upload上传组件完成
    根据文档所述,我们需要在页面中设置:

    • action:提交地址
    • show-file-list:展示文件列表
    • on-success:成功处理函数
    • before-upload:上传前的处理函数
    // create.vue
    ...
    <!-- 课程封面 -->
    <div v-show="activeStep === 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="imageUrl" class="avatar">
          <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
      </el-form-item>
    </div>
    ...
    <script>
    ...
    data () {
      return {
        ...
        // 本地预览图片地址
        imageUrl: ''
      }
    },
    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
      }
    }
    ...
    <style lang="scss" scoped>
    ...
    .avatar-uploader .el-upload {
      border: 1px dashed #d9d9d9;
      border-radius: 6px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
    }
    .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>
    

    演示效果很不错,但是边框的样式并未生效
    原因在于:

    • 样式选择器为.avatar-uopload .el-upload,说明选择器选取的元素以及不存在与create.vue这个组件中,而是出于create.vue的子组件<el-upload>
    • 同时,由于当前组件设置了scoped,使得样式只能作用在当前组件中的元素,让选择器无法生效
      如果组件没有设置scoped的话,就不存在这种问题,但是如果两种需求都需要的话,可以使用一种叫做深度作用选择器的东西:

    深度选择器

    这个内容可以参考Vue Loader文档中,深度作用选择器相关栏目
    如果希望scoped中的某个选择器能够作用得更深,比如影响子组件样式,就需要使用>>>操作符

    • 这个写法不是CSS语法或者预处理器语法,而是Vue单文件组件中提供的一种语法
    • >>>/deep/::v-deep功能相同,我们推荐使用::deep
    • 官方称之为深度作用选择器,也称之为样式穿透
    // create.vue
    <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>
    

    销售信息

    使用Element的Input输入框组件的复合型输入框进行单位设置

    • 设置方式通过组件插槽来设置前置或者后置的内容
      添加完细节之后的部分代码为
    // create.vue
    ...
    <!-- 销售信息 -->
    <div v-show="activeStep === 2">
      <el-form-item label="售卖价格">
        <el-input>
          <template slot="append">元</template>
        </el-input>
      </el-form-item>
      <el-form-item label="商品原价">
        <el-input>
          <template slot="append">元</template>
        </el-input>
      </el-form-item>
      <el-form-item label="销量">
        <el-input>
          <template slot="append">单</template>
        </el-input>
      </el-form-item>
      <el-form-item label="活动标签">
        <el-input></el-input>
      </el-form-item>
    </div>
    ...
    

    秒杀活动

    通过开关控制底部结构展示与否

    • 开关就通过我们已经讲解过的Switch组件来设置
    // create.vue
    ...
    <!-- 秒杀活动 -->
    <div v-show="activeStep === 3">
      <!-- 设置秒杀状态开关 -->
      <el-form-item label="限时秒杀开关" label-width="120px">
        <el-switch
          v-model="isSeckill"
          active-color="#13ce66"
          inactive-color="#ff4949">
        </el-switch>
      </el-form-item>
    ...
    data () {
      return {
        ...
        // 秒杀状态
        isSeckill: false
      }
    },
    

    而秒杀底部的内容部分通过v-if判断来实现

    // create.vue
    ...
    <div v-show="activeStep === 3">
      <!-- 设置秒杀状态开关 -->
      <el-form-item label="限时秒杀开关" label-width="120px">
        ...
      </el-form-item>
      <template v-if="isSeckill">
            <!-- 其他部分的基础结构 -->
      </template>
    </div>
    ...
    

    细节部分不做赘述,都是重复工作
    有一点要提到的是,秒杀的开始和结束时间应该使用Element组件中的DateTimePicker日期时间选择器组件设置

    // create.vue
    ...
    <el-form-item label="开始时间">
      <!-- <el-input></el-input> -->
      <el-date-picker
        type="datetime"
        placeholder="选择开始时间">
      </el-date-picker>
    </el-form-item>
    <el-form-item label="结束时间">
      <!-- <el-input></el-input> -->
      <el-date-picker
        type="datetime"
        placeholder="选择结束时间">
      </el-date-picker>
    </el-form-item>
    ...
    

    课程详情

    课程详情部分先试用一个文本域代替一下富文本,最后再进行富文本插入的办法讲解

    基本数据绑定

    老规矩,接口操作

    // services/course.js
    ...
    // 保存或者更改课程信息
    export const saveOrUpdateCourse = data => {
      return request({
        method: 'POST',
        url: '/boss/course/saveOrUpdateCourse',
        data
      })
    }
    

    引入,并且提交时要提交所有的保存了的数据信息,属性很多,要注意区分
    接口文档详细信息自行参考:接口
    其接口的数据要添加到data中,无用数据可以自行删除
    都是重复性的工作,不再多做赘述

    上传课程封面

    观察文档接口,接口中需要的两个属性,courseListImg,courseImgUrl类型均为String,代表的是一个服务器的图片地址,所以说,在选取图片之后要先上传到服务器获取线上地址,在提交时将这个线上地址发送给接口

    // services/course.js
    ...
    // 上传图片
    export const uploadCourseImage = (data, onUploadProgress) => {
      // 接口要求的请求数据类型为:multipart/form-data
      // 所以需要提交 FormData 数据对象
      return request({
        method: 'POST',
        url: '/boss/course/upload',
        data
      })
    }
    

    引入到页面中
    要进行图片上传,有两种方式:

    • Element的Upload组件支持自动上传,根据文档中的Attribute进行对应的属性配置就可以了
      • 通过属性方式设置。属性很多,配置比较繁琐
      • 由于Element内部不是通过Axios发送请求,所以Token信息还需要单独设置
    • 自定义上传(推荐)
      • Upload组件提供了http-request属性用于覆盖默认的上传行为,用于实现自定义上传
        • 设置处理函数,组件取消自动上传了,同时将上传文件的信息通过参数Option传入
          • options.file为选择的文件信息,通过Formdata发送
    // 自定义文件上传操作
    async handleUpload (options) {
      // 创建 FormData 对象保存数据
      const fd = new FormData()
      // 添加数据的键要根据接口文档设置
      fd.append('file', options.file)
      // 发送请求
      const { data } = await uploadCourseImage(fd)
      if (data.code === '000000') {
        // 图片预览为组件在 on-success 时设置的本地预览功能
        // 默认检测 imgUrl, 这里更换为 course中对应地址即可
        // before-upload 用于在上传文件前进行规则校验(例如文件格式与大小,可自行调整)
        // data.data.name 为服务器提供的地址
        this.course.courseListImg = data.data.name
        // 提示
        this.$message.success('上传成功')
      }
    }
    
    <!-- 自定义上传 -->
    <el-upload ... >
      <!-- 图片预览修改为当前Upload对应数据 -->
      <img v-if="course.courseListImg" :src="course.courseListImg" class="avatar">
      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
    

    封装组件,不止一个位置需要上传图片的功能,所以我们封装为组件便于使用
    引入

    // create.vue
    ...
    // 引入图片上传组件
    import CourseImage from './components/course-image'
    
    export default {
      name: 'CourseCreate',
      components: {
        CourseImage
      },
    ...
    

    喜闻乐见的子传父父传子的操作,所以我们无需再多做赘述



    封装这个组建之前,可以通过传值设置必选数据之外,还可以通过传参增强组件的使用灵活性,这里演示通过传参定制上传文件的的大小

    // course-image.vue
    ...
    props: {    
        ...
      // 限制上传大小
      limit: {
        type: Number,
        default: 2
      }
    },
    ...
    // 上传文件之前的钩子
    beforeAvatarUpload (file) {
      ...
      const isLt2M = file.size / 1024 / 1024 < this.limit
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error(`上传头像图片大小不能超过 ${this.limit}MB!`)
      }
      return isJPG && isLt2M
    },
    ...
    

    传参时进行不同参数的定制就可以了

    // create.vue
    ...
    <!-- 课程封面图上传 -->
    <course-image v-model="course.courseListImg" :limit="2"></course-image>
    <!-- 解锁封面图上传 -->
    <course-image v-model="course.courseImgUrl" :limit="5"></course-image>
    ...
    

    上传进度

    upload组件自带上传进度功能,Progress进度条
    将Progress组件设置到Upload同级,并且调整尺寸

    // course-image.vue
    ...
    <!-- 进度条组件 -->
    <el-progress 
      type="circle" 
      :percentage="0"
      :width="178"
    ></el-progress>
    <!-- 上传组件 -->
    <el-upload ... >
    ...
    

    根据上传的情况,应该显示两个组件之一,通过v-if v-else控制两个组件的显示情况

    // course-image.vue
    ...
    <script>
    ...
    data () {
      return {
        ...
        // 保存下载状态
        isUploading: false
      }
    },
    ...
    async handleUpload (options) {
      // 设置进度信息展示
      this.isUploading = true
      ...
      if (data.code === '000000') {
        ...
        // 关闭进度信息展示
        this.isUploading = false
      }
    }
    ..
    </script>
    ...
    <!-- 进度条组件 -->
    <el-progress
      v-if="isUploading"
      ...
    ></el-progress>
    <!-- 上传组件 -->
    <el-upload
      v-else
      ...
    >
    ...
    

    进度条百分比显示

    Upload本身就具有上传进度处理的on-progress属性,设置http-request属性进行自定义上传之后这个属性就会无效化
    这个时候我们可以通过Axios的请求配置项onUploadProgress进行进度检测
    onUploadProgress本子就是对H5的xhr.upload.onprogress的封装

    // services/course.js
    ...
    // 上传图片(添加配置项与参数)
    export const uploadCourseImage = (data, onUploadProgress) => {
      return request({
        method: 'POST',
        url: '/boss/course/upload',
        data,
        // Axios 将 HTML5 新增的上传进度事件:progress
        onUploadProgress (event) {
            console.log(event.loaded, event.total)
        }
      })
    }
    

    将onUploadProgress设置为参数

    // services/course.js
    ...
    // 上传图片(添加配置项与参数)
    export const uploadCourseImage = (data, onUploadProgress) => {
      return request({
        method: 'POST',
        url: '/boss/course/upload',
        data,
        // Axios 将 HTML5 新增的上传进度事件:progress
        onUploadProgress
      })
    }
    

    请求时设置一个回调函数,计算百分比存储在data中

    // course-image.vue
    ...
    data () {
      return {
        ...
        // 保存上传进度百分比
        precentage: 0
      }
    },
    ...
    async handleUpload (options) {
      ...
      // 设置进度回调,进行百分比计算
      const { data } = await uploadCourseImage(fd, (event) => {
        this.precentage = Math.floor(event.loaded / event.total * 100)
      })
      ...
    }
    

    最后绑定给el-progress组件就好了

    // course-image.vue
    ...
    <el-progress
      ...
      :percentage="precentage"
    ></el-progress>
    ...
    

    重复进行上传时可能会出现回退现象,我们只需要在完成上传后清空数据就好

    // course-image.vue
    ...
    async handleUpload (options) {
      ...
      if (data.code === '000000') {
        ...
        // 上传成功后,设置进度信息归零,避免下次上传出现回退效果
        this.precentage = 0
      }
    }
    ...
    

    给进度条设置status区分上传的不同状态

    // course-image.vue
    ...
    <el-progress
      ...
      :status="precentage === 100 ? 'success' : undefined"
    ></el-progress>
    ...
    

    销售和秒杀 都是简单的绑定数据输入框传递,除了要注意一下秒杀需要一个开关来设置视图显示与否,所以不再多做赘述,另外,我们已经提到腻的内容就是修改需要id,添加不需要id,通过接口传送数据这种事情我们已经是熟练的老手了(不)
    只需要注意一点:
    后端接口如果不支持秒杀时间中的时分秒,测试的时候只需要日期就行了,或者设置type=date改成DatePicker日期选择器(但是实际上的项目是都可以选择的)

    富文本编辑器

    普通的textarea没有格式,需要输入大段文本内容时就非常的不友好,这个时候可以通过富文本编辑器来输入有格式的文本内容

    • 使用起来接近日常使用的文档形式,类似于md,word
    • 本质上是插件将输入内容自动通过不同标签组织起来,最终生成带有标签的文本
      常见的有:
    • CKeditor
    • quill
    • wangEditor
    • ueditor
    • tinymce
      我们以wangEditor为例

    安装

    npm i wangeditor -S

    如若安装有问题,可以通过npm audit -fix修复,没出现问题就忽略

    使用

    根据wangEditor的文档操作就行了

    import E from "wangeditor";
    const editor = new E("#div1");
    editor.create();
    

    封装一下富文本编辑器,作为公共组件,以便复用

    • 如果要将富文本编辑器换为其他的,可以在组件里直接动手,方便直接
    // src/components/TextEditor/index.vue --- 公共组件目录
    <template>
      <div ref="editor" class="text-editor"></div>
    </template>
    
    <script>
    // 引入富文本编辑器
    import E from 'wangeditor'
    export default {
      name: 'TextEditor',
      // 由于需要进行 DOM 操作,使用 mounted 钩子
      mounted () {
        // 初始化富文本编辑器
        this.initEditor()
      },
      methods: {
        initEditor () {
          // 创建富文本编辑器实例
          const editor = new E(this.$refs.editor)
          // 初始化富文本编辑器
          editor.create()
        }
      }
    }
    </script>
    
    <style lang="scss" scoped></style>
    

    引入,绑定数据。父组件使用v-model,公共组件接收,经典时尚重复操作,不再赘述
    如果父组件使用时希望给编辑器设置初始值,通过方法设置

    • 测试时,修改父组件的course.courseDescriptionMarkDown 的初始值
    // TextEditor/index.vue
    ...
    // 由于需要进行 DOM 操作,使用 mounted 钩子
    mounted () {
      // 初始化富文本编辑器
      this.initEditor()
    },
    methods: {
      initEditor () {
        ...
        // 初始化后设置内容
        editor.txt.html(this.value)
      }
    }
    ...
    

    当富文本编辑器输入完毕之后需要提交,需要将内容传出给父组件,这个时候使用编辑器提供的方法操作

    • onChange回调用于在内容改变时触发
    • 回调必须设置在editor.create()前,否则编辑器就已经创建完毕,设置无效
    • 通过组件自定义事件传出给父组件的v-model绑定
    // TextEditor/index.vue
    ...
    methods: {
      initEditor () {
        const editor = new E(this.$refs.editor)
        // 设置回调
        editor.config.onchange = function (value) {
          // value 为输入的内容,通过自定义事件传出即可 (注意 this 指向,建议使用箭头函数)
          this.$emit('input', value)
        }
        editor.create()
        editor.txt.html(this.value)
      }
    }
    

    富文本编辑器图片上传处理

    wangEditor默认支持图片上传,可以通过“网络图片”选项的输入线上图片地址处理
    鉴于服务器响应格式有需求,我们自定义上传
    设置到页面中观察,选择文件后触发customUploadImg回调

    • 参数1 resultFiles为文件信息所在的数组,上传时取出数据发送就可以了
    • 参数2 insertImgFn为上传完毕接收到地址后,根据图片地址生成img标签并插入到富文本编辑器时使用
      引入之前封装的图片上传函数,进行处理
    // TextEditor/index.vue
    ...
    // 引入文件上传接口
    import { uploadCourseImage } from '@/services/course'
    ...
    initEditor () {
      ...
      // 配置 自定义上传图片 功能
      editor.config.customUploadImg = async function (resultFiles, insertImgFn) {
        // 发送请求(参数需要 FormData 类型)
        const fd = new FormData()
        fd.append('file', resultFiles[0])
        const { data } = await uploadCourseImage(fd)
        if (data.code === '000000') {
          // 根据地址创建 img 并插入到富文本编辑器
          insertImgFn(data.data.name)
        }
      }
      ...
    }
    ...
    

    一套测试完成,无BUG

    抽离组件

    编辑和新增是类似的,可以封装到create-or-edit.vue组件中
    引入组件的时候,其他地方的组件目录等级要记得修改
    经典时尚编辑或修改,不再赘述

    图片上传组件改进(如果不需要设置本地预览的话就无需这个操作)

    测试之后发现,课程封面图无法显示,需要在course-image中判断是否传入了图片

    • 新增:value为空,imageUrl为空,选择后imageUrl为预览地址
    • 编辑:value为地址,imageUrl为空,选择后均有值,但是应该显示imageUrl。上述代码比较复杂,应该使用计算属性设置
    // course/components/course-images.vue
    ...
    computed: {
      previewUrl () {
        // 有 imageUrl 优先使用,没有时使用 value,都没有返回 undefined
        return this.imageUrl || this.value
      }
    },
    ...
    <!-- 替换原来的 imageUrl 即可 -->
    <img v-if="previewUrl" :src="previewUrl" class="avatar">
    

    秒杀细节改进

    如果编辑的课程没有处于秒杀状态,就响应数据的activityCourseDTO为null,这个时候操作秒杀按钮就会报错,要在这里添加检测,如果不是秒杀状态,那么就将这个对象属性初始化就可以了

    // create-or-edit.vue
    ...
    async loadCourse () {
      const { data } = await getCourseById(this.courseId)
      if (data.code === '000000') {
        // 为非秒杀课程初始化属性
        if (!data.data.activityCourse) {
          data.data.activityCourseDTO = {
            beginTime: '',
            endTime: '',
            amount: 0,
            stock: 0
          }
        }
        this.course = data.data
      }
    },
    

    富文本编辑器组件改进

    由于编辑请求为异步操作,而富文本编辑器中的DOM功能为同步,所以编辑时会出现富文本编辑器显示默认文本的情况,这个时候通过watch来侦听value变化,并进行初始化内容更新(新增功能不存在这个问题)

    // src/components/TextEditor.vue
    ...
    data () {
      return {
        editor: null,
        // 要编辑的数据是否加载完毕
        isLoaded: false
      }
    },
    watch: {
      value () {
        // 编辑数据加载成功后,为富文本编辑器更新初始内容即可
        if (!this.isLoaded) {
          this.editor.txt.html(this.value)
          this.isLoaded = true
        }
      }
    },
    ...
    initEditor () {
      ...
      // 将富文本编辑器实例保存给 this 以便在 watch 中操作
      this.editor = editor
    }
    

    大功告成!

    相关文章

      网友评论

          本文标题:课程管理(一)

          本文链接:https://www.haomeiwen.com/subject/afxwqltx.html