课程管理(一)

2021-03-15  本文已影响0人  amanohina

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

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

上下架操作处理

通过课程上下架接口操作:地址
这里需要注意一点:为什么我这里不写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>

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

// 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' }
      ]
...

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

// 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>

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

// 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上传组件完成
根据文档所述,我们需要在页面中设置:

// 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>

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

深度选择器

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

// 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>
...

秒杀活动

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

// 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
  })
}

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

// 自定义文件上传操作
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没有格式,需要输入大段文本内容时就非常的不友好,这个时候可以通过富文本编辑器来输入有格式的文本内容

安装

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,公共组件接收,经典时尚重复操作,不再赘述
如果父组件使用时希望给编辑器设置初始值,通过方法设置

// TextEditor/index.vue
...
// 由于需要进行 DOM 操作,使用 mounted 钩子
mounted () {
  // 初始化富文本编辑器
  this.initEditor()
},
methods: {
  initEditor () {
    ...
    // 初始化后设置内容
    editor.txt.html(this.value)
  }
}
...

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

// 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回调

// 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中判断是否传入了图片

// 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
}

大功告成!

上一篇下一篇

猜你喜欢

热点阅读