该功能涉及到的因素更多,分为两部分记录过程
功能分析:
- 展示上架状态
- 上下架操作
- 添加课程
- 编辑课程
- 课程内容管理(第二部分再做讲解)
基础布局等不再多做赘述
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
type="primary"
>添加课程</el-button>
</div>
<el-table
:data="courses"
v-loading="isLoading"
>
<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
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>
表单结构搭建
基本信息
完善表单结构(封面是在第二步骤)
- 课程排序功能使用了Element的InputNumber计数器组件
// 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发送
- 设置处理函数,组件取消自动上传了,同时将上传文件的信息通过参数Option传入
- Upload组件提供了http-request属性用于覆盖默认的上传行为,用于实现自定义上传
// 自定义文件上传操作
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
}
大功告成!