# 内容管理-添加与编辑文章
# 回顾
- 文章列表频道组件的封装
- v-model在组件标签上的用法
- :value
- @input
- 频道数据通过子组件进行加载
- v-model在组件标签上的用法
- 素材管理
- 加载图片列表的数据
- 动态渲染图片列表
- 添加分页功能
- page 当前页码
- per_page 每页显示的条数
- 全部和收藏的切换功能
- 实现收藏功能
- 实现删除功能
- 数据相关方法的用法
- 上传图片
- 弹窗的显示和隐藏
- 上传文件的组件用法
- action
- headers
- name
- 图标样式的调整
- 样式选择器的优先级不够
- 样式根本没有生效
- 完成上传功能
# 内容管理-添加文章
- 富文本输入域
- 文章的封面
- 频道列表
# 添加文章路由配置
目标:能够配置添加文章组件路由配置
- 配置路由映射
import PublishArticle from '@/views/article/PublishArticle.vue'
{ path: '/publish', component: PublishArticle },
- 配置左侧菜单路由
<el-menu-item index="/home/publish">发布文章</el-menu-item>
# 添加文章基本布局
目标:能够实现添加文件组件基本布局
- 表单基本布局
<el-card>
<div slot="header" class="clearfix">
<span>发布文章</span>
</div>
<!-- 发布文章的表单 -->
<el-form label-width="120px">
<!-- 文章的标题 -->
<el-form-item label="标题:">
<el-input placeholder='文章名称' style="width:400px"></el-input>
</el-form-item>
</el-form>
</el-card>
# 富文本基本使用
目标:熟悉富文本组件的基本使用
富文本组件的菜单由什么决定?options (参考官方网站)
- 安装
vue-quill-editor包
npm i vue-quill-editor
- 导入相关样式和包
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
- 配置组件
components: {
quillEditor
}
- 使用组件
<quill-editor class='rich-text' :options="editorOption"></quill-editor>
// 富文本配置对象
editorOption: {
placeholder: '',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
['image']
]
}
}
<style scoped lang="less">
.publish {
.rich-text {
height: 200px !important;
}
}
</style>
# 关于深度选择器用法
深度选择器 (opens new window) /deep/
作用:如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 /deep/ 操作符:
<style scoped lang="less">
.publish {
/deep/ .rich-text {
min-height: 300px;
.ql-container {
min-height: 300px;
}
}
}
</style>
- 上述样式编译的结果如下

图示说明:深度选择器会强制使选择器之间形成父子关系
# 文章频道
目标:基于封装好的组件实现频道选择功能
<el-form-item label="频道:">
<my-channel v-model="articleForm.channel_id"></my-channel>
</el-form-item>
import MyChannel from '@/components/MyChannel.vue'
data () {
return {
articleForm: {
channel_id: null
}
},
}
// 配置局部组件
components: {
MyChannel
}
# 封面组件封装
目标:封装单个点击图片进行选择弹层效果的组件
- 拆分单独的封面组件 MyCover.vue
- 使用封装好的组件
<my-cover></my-cover>
# 组件基本布局
目标:实现图片基本布局效果
- 基本布局
<template>
<div class="my-image">
<div class="img_btn">
<!-- 如果默认的图片路径不存在,就显示默认图片 -->
<!-- ../assets/default.png 默认图地址 -->
<el-image :src='defaultImage'>
<!-- 如果 defaultImage 不存在,就显示默认图片-->
<div slot="error" class="image-slot">
<img src="../assets/imgs/default.png" alt />
</div>
</el-image>
</div>
</div>
</template>
export default {
data () {
return {
defaultImage: null
}
}
}
- 基本样式
.my-image {
display: inline-block;
margin-right: 20px;
.img_btn {
width: 150px;
height: 150px;
border: 1px dashed #ddd;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
# 控制点击弹层
目标:控制弹出层组件的显示和隐藏
<!-- 弹窗效果 -->
<el-dialog
title=""
:visible.sync="dialogVisible"
width="400px">
图片
</el-dialog>
data () {
return {
dialogVisible: false
}
},
methods: {
openDialog () {
// 打开弹窗
this.dialogVisible = true
}
}
# Tab布局
目标:熟悉Tab组件的用法
- lable 表示选项卡标题内容
- name 用于区分选项卡是谁,通过v-model绑定区分选择哪一个
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="素材库" name="image">
素材库
</el-tab-pane>
<el-tab-pane label="上传图片" name="upload">
上传图片
</el-tab-pane>
</el-tabs>
data () {
return {
// 控制选项卡选中值
activeName: 'image'
}
},
# 素材库
- 全部与收藏基本布局
<el-radio-group @change="changeCollect" v-model="filterParams.collect" size="small">
<el-radio-button :label="false">全部</el-radio-button>
<el-radio-button :label="true">收藏</el-radio-button>
</el-radio-group>
- 调用接口获取图片列表数据
<!-- 弹窗效果 -->
<el-dialog @open='changeCollect'
// 导入调用接口获取图片列表数据的方法
import { loadImageList } from '@/api/image.js'
methods: {
async changeCollect () {
// 切换全部和收藏的按钮状态
this.filterParams.page = 1
try {
const ret = await loadImageList(this.filterParams)
this.list = ret.data.results
this.total = ret.data.total_count
} catch (e) {
console.log(e)
this.$message.error('获取素材列表失败')
}
}
}
- 渲染图片列表内容
<div class="img_list">
<div
class="img_item"
v-for="item in list"
:key="item.id"
>
<img:src="item.url" alt />
</div>
</div>
- 图片列表样式
.img_list {
margin-top: 10px;
.img_item {
width: 150px;
height: 120px;
border: 1px dashed #ddd;
position: relative;
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
img {
width: 100%;
height: 100%;
display: block;
}
&.selected {
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.3) url(../assets/selected.png) no-repeat
center / 50px;
}
}
}
}
.el-tabs__content {
text-align: left;
}
- 分页效果
<!-- 分页区域 -->
<el-pagination
background
layout="prev, pager, next"
:total="total"
:current-page="filterParams.page"
:page-size="filterParams.per_page"
@current-change="changePager"
:hide-on-single-page="false"
></el-pagination>
// 处理页码改变
changePager (page) {
this.filterParams.page = page
this.loadImageList()
}
- 确定按钮布局
- 可以基于
el-dialog组件的center属性控制按钮的居中效果
- 可以基于
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
- 控制图片的选中高亮
- 点击图片时,给图片添加一个类名 selected (显示对勾)
- 控制图片的点击操作:记录点击图片的路径
<div class="img_list">
<div
class="img_item"
:class="{selected: item.url === selectedImageUrl}"
v-for="item in list"
:key="item.id"
>
<img @click='handleSelectImg(item.url)' :src="item.url" alt />
</div>
</div>
handleSelectImg (url) {
// 控制图片的选择
this.selectedImageUrl = url
}
# 上传图片
目标:点击 + 打开选择文件的窗口,选中文件后直接上传,上传成功后显示图片。
- 上传组件基本布局
- action 表示上传图片的地址
- headers 表示上传图片的请求头
- show-file-list 表示是否显示文件列表
- name 表示上传图片时请求参数名称(接口文档决定)
- on-success 上传成功的回调函数
<!-- 上传图片 -->
<el-upload
:action="uploadURL"
:headers='headers'
:show-file-list="false"
name='image'
:on-success="handleSuccess">
<img v-if="uploadImageUrl" :src="uploadImageUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
- 上传组件成功回调函数
// 上传成功
handleSuccess (res) {
this.uploadImageUrl = res.data.url
}
# 确定选中图片
目标:实现获取图片路径功能
getImgPath () {
// 获取图片的地址
// 判断图片是选择的还是上传的
if (this.activeName === 'image') {
// 选项卡选中了【选择素材】
if (!this.selectedImageUrl) {
return this.$message.error('请选择一张图片')
}
// 选中了一张图片,然后
this.defaultImage = this.selectedImageUrl
// 把子组件地址传递给父组件
this.$emit('get-cover', this.selectedImageUrl)
} else if (this.activeName === 'upload') {
// 选项卡选中了【上传图片】
if (!this.uploadImageUrl) {
return this.$message.error('请上传一张图片')
}
// 上传了一张图片
this.defaultImage = this.uploadImageUrl
// 把子组件地址传递给父组件
this.$emit('get-cover', this.uploadImageUrl)
}
// 关闭弹窗
this.dialogVisible = false
},
# 使用封面组件
目标:控制不同选项下封面图效果
- v-model的本质是属性绑定和事件绑定
- value
props: ['value'], - input
this.$emit('input', this.selectedImageUrl)
- value
<el-form-item label="封面:">
<!-- 3、使用局部组件 -->
<el-radio-group @change="articleForm.cover.images=[]" v-model="articleForm.cover.type">
<el-radio :label="1">单图</el-radio>
<el-radio :label="3">三图</el-radio>
<el-radio :label="0">无图</el-radio>
<el-radio :label="-1">自动</el-radio>
</el-radio-group>
<!-- 选择图片的组件 -->
<div v-if='articleForm.cover.type === 1'>
<my-image v-model='articleForm.cover.images[0]'></my-image>
</div>
<div v-else-if='articleForm.cover.type === 3'>
<my-image v-model='articleForm.cover.images[0]'></my-image>
<my-image v-model='articleForm.cover.images[1]'></my-image>
<my-image v-model='articleForm.cover.images[2]'></my-image>
</div>
</el-form-item>
# 发布文章提交表单
目标:实现发布文章发布功能
async handleSubmit (type) {
// 发表文章
try {
await publishArticle(this.articleForm, {
draft: type
})
// 跳转到文章列表页面
this.$router.push('/home/article')
} catch (e) {
console.log(e)
this.$message.error('发布文章失败')
}
}
# 内容管理-修改文章
编辑文章
1、点击文章列表的【编辑】按钮,跳转到【发布文章】的组件页面(发布文章和编辑文章组件重用),需要传递路由参数 /home/publish?id=123123
2、进入到【发布文章】组件页面后,获取文章的id(通用路由参数获取)$route.query.id
3、根据得到的id调用接口获取文章的详细数据,然后填充表单
# 点击编辑按钮动作
目标:实现页面的跳转操作
- 点击编辑按钮
<el-button @click="toEdit(scope.row.id)" plain type="primary" icon="el-icon-edit" circle></el-button>
- 编程式导航实现跳转
// 编辑文章
toEdit (id) {
// 编程式导航跳转页面
this.$router.push('/home/publish?id=' + id)
},
- 获取路由参数
this.$route.query.id
# 根据id获取文章信息
目标:能够根据文章的id获取文章详细数据
methods: {
resetData () {
// 重置表单
this.articleForm = {
// 文章的标题
title: '',
// 文章的内容
content: '',
// 文章的封面
cover: {
// 图片的张数
type: 1,
// 具体的图片地址
images: []
},
// 文章所属频道
channel_id: ''
}
},
}
methods: {
async getArticleInfo (id) {
// 根据id获取文章详情数据
try {
const ret = await getArticleInfo(id)
this.articleForm = ret.data
} catch (e) {
console.log(e)
this.$message.error('获取文章信息失败')
}
},
}
created () {
// 根据id获取文章的详细数据
this.getArticleInfo(this.$route.query.id)
}
- 获取接口数据
// 根据id获取文章详情数据
export const getArticleInfo = (id) => {
return request({
method: 'get',
url: 'articles/' + id
})
}
# 编辑文章信息显示
目标:实现文章表单数据填充
- 基于v-model绑定表单数据
- 定制组件的标题和提交按钮的文字
computed: {
// 动态生成表单的标题
title () {
return this.articleForm.id ? '编辑文章' : '发布文章'
}
},
<div slot="header" class="clearfix">
<span>{{title}}</span>
</div>
# 编辑文章提交表单
目标:实现编辑的表单提交动作
import { publishArticle, getArticleInfo, editArticle } from '@/api/article.js'
async handleSubmit (type) {
if (this.articleForm.id) {
// 编辑文章
try {
await editArticle(this.articleForm, {
draft: type
})
// 跳转到文章列表页面
this.$router.push('/home/article')
} catch (e) {
console.log(e)
this.$message.error('编辑文章失败')
}
} else {
// 发表文章
try {
await publishArticle(this.articleForm, {
draft: type
})
// 跳转到文章列表页面
this.$router.push('/home/article')
} catch (e) {
console.log(e)
this.$message.error('发布文章失败')
}
}
}
// 编辑文章
export const editArticle = (data, query) => {
const { id, ...other } = data
return request({
method: 'put',
url: 'articles/' + id,
data: other,
params: query
})
}
# 关于动态路由的问题
提醒一下,当使用路由参数时,例如从
/user/foo导航到/user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
- 为了解决这个问题,需要使用侦听器监听路由的变化
<template>
<div>
用户信息:{{id}}
</div>
</template>
<script>
export default {
name: 'UserInfo',
data () {
return {
id: null
}
},
created () {
console.log(this.$route.params.id)
this.id = this.$route.params.id
},
watch: {
$route (to, from) {
// to表示跳转到哪里
// from表示从哪里跳转过来
console.log(to.params.id)
this.id = to.params.id
}
}
}
</script>
# 完善添加和编辑动作
- 重置表单需要添加el-form标签的model属性; el-form-item需要添加prop属性
- 重置表单指的是恢复为表单原来的初始值,而不是清空表单
watch: {
$route (to) {
if (!to.query.id) {
// 发布文章
this.articleForm.id = null
// 重置表单
this.$refs.myform.resetFields()
}
}
},