# 五、文章发布## 创建组件并配置路由1、创建 `src/views/publish/index.vue` 组件```html
<template><div class="publish-container">发布文章</div>
</template><script>
export default {name: 'PublishIndex',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less"></style>```2、配置页面路由<img src="assets/image-20200425162406736.png" alt="image-20200425162406736" style="zoom:50%;" />3、访问测试## 页面布局```html
<template><div class="publish-container"><el-card class="box-card"><div slot="header" class="clearfix"><!-- 面包屑路径导航 --><el-breadcrumb separator-class="el-icon-arrow-right"><el-breadcrumb-item to="/">首页</el-breadcrumb-item><el-breadcrumb-item>发布文章</el-breadcrumb-item></el-breadcrumb><!-- /面包屑路径导航 --></div><el-form ref="form" :model="form" label-width="40px"><el-form-item label="标题"><el-input v-model="form.name"></el-input></el-form-item><el-form-item label="内容"><el-input type="textarea" v-model="form.desc"></el-input></el-form-item><el-form-item label="封面"><el-radio-group v-model="form.resource"><el-radio label="单图"></el-radio><el-radio label="三图"></el-radio><el-radio label="无图"></el-radio><el-radio label="自动"></el-radio></el-radio-group></el-form-item><el-form-item label="频道"><el-select v-model="form.region" placeholder="请选择频道"><el-option label="区域一" value="shanghai"></el-option><el-option label="区域二" value="beijing"></el-option></el-select></el-form-item><el-form-item><el-button type="primary" @click="onSubmit">发表</el-button><el-button>存入草稿</el-button></el-form-item></el-form></el-card></div>
</template><script>
export default {name: 'PublishIndex',components: {},props: {},data () {return {form: {name: '',region: '',date1: '',date2: '',delivery: false,type: [],resource: '',desc: ''}}},computed: {},watch: {},created () {},mounted () {},methods: {onSubmit () {console.log('submit!')}}
}
</script><style scoped lang="less"></style>```## 处理表单数据绑定> 思路:根据后端接口的要求处理绑定数据绑定```html
<template><div class="publish-container"><el-card class="box-card"><div slot="header" class="clearfix"><!-- 面包屑路径导航 --><el-breadcrumb separator-class="el-icon-arrow-right"><el-breadcrumb-item to="/">首页</el-breadcrumb-item><el-breadcrumb-item>发布文章</el-breadcrumb-item></el-breadcrumb><!-- /面包屑路径导航 --></div><el-form ref="form" :model="form" label-width="40px"><el-form-item label="标题"><el-input v-model="article.title"></el-input></el-form-item><el-form-item label="内容"><el-input type="textarea" v-model="article.content"></el-input></el-form-item><el-form-item label="封面"><el-radio-group v-model="article.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></el-form-item><el-form-item label="频道"><el-select v-model="form.region" placeholder="请选择频道"><el-option label="区域一" value="shanghai"></el-option><el-option label="区域二" value="beijing"></el-option></el-select></el-form-item><el-form-item><el-button type="primary" @click="onSubmit">发表</el-button><el-button>存入草稿</el-button></el-form-item></el-form></el-card></div>
</template><script>
export default {name: 'PublishIndex',components: {},props: {},data () {return {form: {name: '',region: '',date1: '',date2: '',delivery: false,type: [],resource: '',desc: ''},article: {title: '', // 文章标题content: '', // 文章内容cover: { // 文章封面type: 0, // 封面类型 -1:自动,0-无图,1-1张,3-3张images: [] // 封面图片的地址}}}},computed: {},watch: {},created () {},mounted () {},methods: {onSubmit () {console.log('submit!')}}
}
</script><style scoped lang="less"></style>```## 处理文章频道数据一、展示频道列表1、请求获取频道列表数据<img src="assets/image-20200425163038862.png" alt="image-20200425163038862" style="zoom:50%;" />2、绑定到模板中展示<img src="assets/image-20200425163137597.png" alt="image-20200425163137597" style="zoom:50%;" />二、将频道绑定到表单元素选择器1、在 article 中添加频道数据字段<img src="assets/image-20200425163209293.png" alt="image-20200425163209293" style="zoom:50%;" />2、将数据绑定到频道选择器中<img src="assets/image-20200425163242141.png" alt="image-20200425163242141" style="zoom:50%;" />## 发布文章一、封装数据接口```js
/*** 新建文章*/
export const addArticle = (data, draft = false) => {return request({method: 'POST',url: '/mp/v1_0/articles',params: {draft // 是否存为草稿(true 为草稿)},data})
}
```二、在发布文章组件中请求调用1、加载请求方法<img src="assets/image-20200425163618795.png" alt="image-20200425163618795" style="zoom:50%;" />2、给发布和存入草稿绑定事件处理函数<img src="assets/image-20200425163526654.png" alt="image-20200425163526654" style="zoom:50%;" />3、处理函数如下```js
onPublish (draft = false) {// 找到数据接口// 封装请求方法// 请求提交表单addArticle(this.article, draft).then(res => {// 处理响应结果// console.log(res)this.$message({message: '发布成功',type: 'success'})})
}
```## 编辑文章### 展示编辑文章内容1、封装获取文章的数据接口```js
/*** 获取指定文章*/
export const getArticle = articleId => {return request({method: 'GET',url: `/mp/v1_0/articles/${articleId}`})
}
```2、点击编辑,导航到发布文章页面<img src="assets/image-20200425163826162.png" alt="image-20200425163826162" style="zoom:50%;" />3、在发布文章页面组件中,处理加载编辑文章内容<img src="assets/image-20200425164127166.png" alt="image-20200425164127166" style="zoom:50%;" /><img src="assets/image-20200425164215467.png" alt="image-20200425164215467" style="zoom:50%;" />### 提交更新1、封装更新文章的数据接口```js
/*** 编辑文章*/
export const updateArticle = (articleId, data, draft = false) => {return request({method: 'PUT',url: `/mp/v1_0/articles/${articleId}`,params: {draft // 是否存为草稿(true 为草稿)},data})
}
```2、加载请求方法<img src="assets/image-20200425164349629.png" alt="image-20200425164349629" style="zoom:50%;" />3、修改发布文章的处理逻辑```js
onPublish (draft = false) {// 找到数据接口// 封装请求方法// 请求提交表单// 如果是修改文章,则执行修改操作,否则执行添加操作const articleId = this.$route.query.idif (articleId) {// 执行修改操作updateArticle(articleId, this.article, draft).then(res => {console.log(res)this.$message({message: `${draft ? '存入草稿' : '发布'}成功`,type: 'success'})// 跳转到内容管理页面this.$router.push('/article')})} else {addArticle(this.article, draft).then(res => {// 处理响应结果// console.log(res)this.$message({message: `${draft ? '存入草稿' : '发布'}成功`,type: 'success'})// 跳转到内容管理页面this.$router.push('/article')})}
},
```## 使用富文本编辑器基于 Vue 的富文本编辑器有很多,例如官方就收录推荐了一些: https://github.com/vuejs/awesome-vue#rich-text-editing 。这里我们以 element-tiptap 为例。- GitHub 仓库:https://github.com/Leecason/element-tiptap
- 在线示例:https://leecason.github.io/element-tiptap
- 中文文档:https://github.com/Leecason/element-tiptap/blob/master/README_ZH.md1、安装```shell
npm i element-tiptap
```2、初始配置```html
<template><el-tiptap v-model="content" :extensions="extensions"></el-tiptap>
</template><script>import {ElementTiptap,Doc,Text,Paragraph,Heading,Bold,Underline,Italic,Image,Strike,ListItem,BulletList,OrderedList,TodoItem,TodoList,HorizontalRule,Fullscreen,Preview,CodeBlock
} from 'element-tiptap'
import 'element-tiptap/lib/index.css'export default {components: {'el-tiptap': ElementTiptap},data () {return {content: 'hello world',extensions: [new Doc(),new Text(),new Paragraph(),new Heading({ level: 3 }),new Bold({ bubble: true }), // 在气泡菜单中渲染菜单按钮new Image(),new Underline(), // 下划线new Italic(), // 斜体new Strike(), // 删除线new HorizontalRule(), // 华丽的分割线new ListItem(),new BulletList(), // 无序列表new OrderedList(), // 有序列表new TodoItem(),new TodoList(),new Fullscreen(),new Preview(),new CodeBlock()]}}
}
</script>```## 处理富文本编辑器中的图片<img src="assets/image-20200426121121725.png" alt="image-20200426121121725" style="zoom:50%;" />1、创建 `src/api/image.js` 封装数据接口```js
/*** 素材请求相关模块*/import request from '@/utils/request'/*** 上传图片素材*/
export const uploadImage = data => {return request({method: 'POST',url: '/mp/v1_0/user/images',// 一般文件上传的接口都要求把请求头中的 Content-Type 设置为 multipart/form-data,但是我们使用 axios 上传文件的话不需要手动设置,你只要给 data 一个 FormData 对象即可。// new FormData()data})
}```2、自定义图片上传到服务器<img src="assets/image-20200426173740077.png" alt="image-20200426173740077" style="zoom:50%;" />## 表单验证处理<img src="assets/image-20200426173930626.png" alt="image-20200426173930626" style="zoom:50%;" />验证规则配置对象:```js
formRules: {title: [{ required: true, message: '请输入文章标题', trigger: 'blur' },{ min: 5, max: 30, message: '长度在 5 到 30 个字符', trigger: 'blur' }],content: [// { required: true, message: '请输入文章内容', trigger: 'change' }{validator (rule, value, callback) {console.log('content validator')if (value === '<p></p>') {// 验证失败callback(new Error('请输入文章内容'))} else {// 验证通过callback()}}},{ required: true, message: '请输入文章内容', trigger: 'blur' }],channel_id: [{ required: true, message: '请选择文章频道' }]
}
```## 文章封面> 该业务功能比较复杂,需要自定义封装组件,所以放到项目最后讲解。1、创建 `src/views/publish/components/upload-image.vue` 组件并写入```html
<template><div>上传图片组件</div>
</template><script>export default {name: "UploadImage",components: {},props: {},data() {return {};},computed: {},watch: {},created() {},methods: {}};
</script><style scoped></style>
```2、在文章发布中加载使用> 注册> 在模板中使用### 使用对话框```html
<template><div class="upload-image"><div class="preview" @click="centerDialogVisible=true"><!-- <img src="" class="avatar"> --><i class="el-icon-plus avatar-uploader-icon"></i></div><!--visible 控制对话框的显示和隐藏--><el-dialogtitle="请选择文章封面图片":visible.sync="centerDialogVisible"width="30%"center><span slot="footer" class="dialog-footer"><el-button @click="centerDialogVisible = false">取 消</el-button><el-button type="primary" @click="centerDialogVisible = false">确 定</el-button></span></el-dialog></div>
</template><script>export default {name: "UploadImage",components: {},props: {},data() {return {centerDialogVisible: false};},computed: {},watch: {},created() {},methods: {}};
</script><style scoped>.upload-image {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;}.upload-image .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>
```### 展示素材库```html
<template><div class="upload-image"><div class="preview" @click="onUploadShow"><!-- <img src="" class="avatar"> --><i class="el-icon-plus avatar-uploader-icon"></i></div><!--visible 控制对话框的显示和隐藏--><el-dialogtitle="请选择文章封面图片":visible.sync="centerDialogVisible"width="50%"center><!-- 标签导航 --><!--el-tabs 组件v-model 双向绑定数据驱动视图:当绑定数据发生改变,激活的标签页受影响视图影响数据:当标签改变的时候,标签的 name 会同步到数据中label 标签的标题name 相当于标签的 id--><el-tabs v-model="activeName"><el-tab-pane label="素材库" name="first"><!-- 标签内容写到里面来 --><!--radio 有个 change 事件当选择的 radio 改变的时候会触发--><el-radio-group v-model="activeImage" @change="loadImages"><el-radio label="all">全部</el-radio><el-radio label="collect">收藏</el-radio></el-radio-group><el-row :gutter="20"><el-col :span="6" v-for="item in images" :key="item.id"><img height="100" :src="item.url" /></el-col></el-row></el-tab-pane><el-tab-pane label="上传图片" name="second">配置管理</el-tab-pane></el-tabs><!-- /标签导航 --><span slot="footer" class="dialog-footer"><el-button @click="centerDialogVisible = false">取 消</el-button><el-button type="primary" @click="centerDialogVisible = false">确 定</el-button></span></el-dialog></div>
</template><script>export default {name: "UploadImage",components: {},props: {},data() {return {centerDialogVisible: false, // 对话框的显示状态activeName: "first", // 激活的标签页activeImage: "all", // 激活的图片筛选类型images: []};},computed: {},watch: {},created() {console.log(123);},methods: {onUploadShow() {// 请求加载数据this.loadImages();// 显示弹窗this.centerDialogVisible = true;},loadImages() {this.$axios({method: "GET",url: "/user/images",params: {// this.activeImage 双向绑定了 radio 选择框组// 所以获取 this.activeImage 也就是在获取选中的那个 radio 的状态collect: this.activeImage === "collect" // 是否获取收藏图片}}).then(res => {this.images = res.data.data.results;}).catch(err => {console.log(err);});}}};
</script><style scoped>.upload-image {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;}.upload-image .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>
```### 处理选择图片1、添加一个数据字段用来存储当前点击的图片项的索引2、模板绑定### 数据绑定1、在父组件中绑定数组元素给图片上传组件2、在上传图片组件中> 声明 value 接收数据> 点击对话框确认触发:将所选的图片路径发送给父组件### 上传图片1、添加上传图片组件2、记录选择的上传图片3、对话框确定的时候## 禁用路由缓存我们发现一个小问题,从编辑文章导航到发布文章,表单内容并没有被清空,这是因为两个路由共用的同一个组件,两者之间相互跳转的时候,原来的组件实例会被复用。 因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。**不过,这也意味着组件的生命周期钩子不会再被调用**。路由默认提供的这个功能的用意是好的,但是有时候却会带来问题,解决方案就是:**禁用缓存**。在路由出口 `router-view` 上添加一个唯一的 `key` 即可。> 详细内容请查阅官方文档:[响应路由参数的变化](<[https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E5%93%8D%E5%BA%94%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0%E7%9A%84%E5%8F%98%E5%8C%96](https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#响应路由参数的变化)>)。