06发布文章
01 发布文章-页面组件和路由
目标
准备发表文章的页面组件并配置路由显示


讲解
在 src/views/article/artList.vue组件, ==直接复制标签==
:inline="true" 不加: 是字符串, 加: 是布尔值
条件查询用【行内表单】去实现
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>文章列表</span>
</div>
<!-- 搜索区域 -->
<div class="search-box">
<el-form :inline="true" :model="q">
<el-form-item label="文章分类">
<el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发布状态" style="margin-left: 15px;">
<el-select v-model="q.state" placeholder="请选择状态" size="small">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small">筛选</el-button>
<el-button type="info" size="small">重置</el-button>
</el-form-item>
</el-form>
<!-- 发表文章的按钮 -->
<el-button type="primary" size="small" class="btn-pub">发表文章</el-button>
</div>
<!-- 文章表格区域 -->
<!-- 分页区域 -->
</el-card>
</div>
</template>
<script>
export default {
name: 'ArtList',
data() {
return {
// 查询参数对象
q: {
pagenum: 1,
pagesize: 2,
cate_id: '',
state: ''
}
}
}
}
</script>
<style lang="less" scoped>
.search-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
.btn-pub {
margin-top: 5px;
}
}
</style>
在 src/router/index.js路由模块中,导入并组件并声明路由规则如下
{
path: 'art-list', // 文章列表
component: () => import('@/views/article/artList')
}

02发表文章-对话框组件
目标
准备发表文章的对话框组件
讲解
在 artList.vue组件中,声明发表文章的对话框
<!-- 发表文章的 Dialog 对话框 -->
<el-dialog title="发表文章" :visible.sync="pubDialogVisible" fullscreen :before-close="handleClose">
<span>这是一段信息</span>
</el-dialog>
在 data 中定义布尔值 pubDialogVisible,用来控制对话框的显示与隐藏
data() {
return {
// ...其他
pubDialogVisible: false // 控制发表文章对话框的显示与隐藏
}
}
点击发布按钮,展示对话框
定义事件处理函数如下
methods: {
// 发表文章按钮->点击事件->让添加文章对话框出现
showPubDialogFn () {
this.pubDialogVisible = true
}
}
在对话框将要关闭时,询问用户是否确认关闭对话框


// 对话框关闭前的回调
async handleClose (done) {
// 询问用户是否确认关闭对话框
const confirmResult = await this.$confirm('此操作将导致文章信息丢失, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
// 取消了关闭-阻止住, 什么都不干
if (confirmResult === 'cancel') return
// 确认关闭
done()
}
小结
this.$confirm的返回值是Promise对象, 如何提取成功结果?
答案
await后面的catch又是怎么回事?
答案
03发表文章-对话框内表单
目标
准备好, 对话框内,发表文章用的表单, 以及数据绑定和规则对象
并渲染文章分类下拉菜单数据
讲解
初步定义表单的 UI 结构
<!-- 发布文章的对话框 -->
<el-form :model="pubForm" :rules="pubFormRules" ref="pubFormRef" label-width="100px">
<el-form-item label="文章标题" prop="title">
<el-input v-model="pubForm.title" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="文章分类" prop="cate_id">
<el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%;">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
在 data 中定义数据对象和验证规则对象:
data() {
return {
// ...其他
pubForm: { // 表单的数据对象
title: '',
cate_id: ''
},
pubFormRules: { // 表单的验证规则对象
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 1, max: 30, message: '文章标题的长度为1-30个字符', trigger: 'blur' }
],
cate_id: [{ required: true, message: '请选择文章标题', trigger: 'blur' }]
}
}
}
在 methods 中,声明初始化文章分类列表数据的方法
// 初始化文章分类的列表数据
async initCateList() {
const { data: res } = await this.$http.get('/my/cate/list')
if (res.code === 0) {
this.cateList = res.data
}
}
在 data 中声明 cateList 数组
data() {
return {
// ...其他
cateList: [] // 文章分类
}
}
在 created 生命周期函数中,调用步骤1声明的方法
created() {
this.initCateList()
},
循环渲染文章分类的可选项
<el-form-item label="文章分类" prop="cate_id">
<el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%;">
<!-- 循环渲染分类的可选项 -->
<el-option :label="item.cate_name" :value="item.id" v-for="item in cateList" :key="item.id">
</el-option>
</el-select>
</el-form-item>

4 发表文章-富文本
目标
准备富文本编辑器
富文本: 就是可以带标签和样式的字符串, 一般用于设置一个产品/文章的主体内容
文档可以在npm的网页中搜索



讲解
基于 vue-quill-editor 实现富文本编辑器:https://www.npmjs.com/package/vue-quill-editor
运行如下的命令,在项目中安装富文本编辑器:
yarn add vue-quill-editor
在项目入口文件 main.js中导入并全局注册富文本编辑器
当然, 你也可以单独封装模块写这些代码后, 引入到main.js即可
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
// 全局注册富文本编辑器
Vue.use(VueQuillEditor)
在 views/article/artList.vue组件的 data 中,定义富文本编辑器对应的数据项, 也绑定到表单的数据对象属性中
data() {
return {
// ...其他
// 表单的数据对象
pubForm: {
title: '',
cate_id: '',
+ content: '' // 文章的内容
},
// ...其他
}
}
在 artList.vue组件的模板结构中,添加富文本编辑器的 DOM 结构, 给el-form内新增一块表单域
<el-form-item label="文章内容">
<!-- 使用 v-model 进行双向的数据绑定 -->
<quill-editor v-model="pubForm.content"></quill-editor>
</el-form-item>
美化富文本编辑器的样式:
// 设置富文本编辑器的默认最小高度
// ::v-deep作用: 穿透选择, 正常style上加了scope的话, 会给.ql-editor[data-v-hash]属性, 只能选择当前页面标签或者组件的根标签
// 如果想要选择组件内的标签(那些标签没有data-v-hash值)所以正常选择选不中, 加了::v-deep空格前置的话, 选择器就会变成如下形式
// [data-v-hash] .ql-editor 这样就能选中组件内的标签的class类名了
::v-deep .ql-editor {
min-height: 300px;
}
再给文章内容部分, 绑定el-form的表单校验, 在el-form-item加上同名的prop属性
<el-form-item label="文章内容" prop="content">
<!-- 使用 v-model 进行双向的数据绑定 -->
<quill-editor v-model="pubForm.content"></quill-editor>
</el-form-item>
在rules对应的规则对象中, 添加校验规则
content: [
{ required: true, message: '请输入文章内容', trigger: 'blur' }
]

05 发表文章-封面标签
目标
渲染文章封面区域标签
实现用户选择封面功能, 拿到用户选择的图片文件
讲解
在 artList.vue组件的模板结构中,添加文章封面对应的 DOM 结构:
<el-form-item label="文章封面">
<!-- 用来显示封面的图片 -->
<img src="../../assets/images/cover.jpg" alt="" class="cover-img" ref="imgRef" />
<br />
<!-- 文件选择框,默认被隐藏 -->
<input type="file" style="display: none;" accept="image/*" ref="iptFileRef" />
<!-- 选择封面的按钮 -->
<el-button type="text">+ 选择封面</el-button>
</el-form-item>
美化封面图片的样式:
// 设置图片封面的宽高
.cover-img {
width: 400px;
height: 280px;
object-fit: cover;
}
为选择封面的按钮绑定点击事件处理函数
模拟文件选择框的点击事件
chooseImgFn() {
this.$refs.iptFileRef.click()
}
监听文件选择框的 change 事件
<!-- 文件选择框,默认被隐藏 -->
<input type="file" style="display: none;" ref="iptFile" accept="image/*" @change="onCoverChangeFn" />
定义 onCoverChange 处理函数如下
// 封面选择改变的事件
onCoverChangeFn (e) {
// 获取用户选择的文件列表
const files = e.target.files
if (files.length === 0) {
// 用户没有选择封面
this.pubForm.cover_img = null
} else {
// 用户选择了封面
this.pubForm.cover_img = files[0]
}
}
在 data 中的 pubForm对象上,声明 cover_img属性,用来存储用户选择的封面
data() {
return {
// 表单的数据对象
pubForm: {
title: '',
cate_id: '',
content: '',
+ cover_img: null // 用户选择的封面图片(null 表示没有选择任何封面)
},
}
}
小结
封面标签选择图片的思路是?
答案
发表文章-封面图片
目标
给封面图片标签做预览效果
如果用户未选择图片, 给一张默认图片
讲解
在 artList.vue组件中,导入默认的封面图片
// 导入默认的封面图片
import defaultImg from '@/assets/images/cover.jpg'
在文件选择框的 change事件处理函数中,根据用户是否选择了封面,动态设置封面图片的 src 地址:
// 监听文件选择框的 change 事件
onCoverChange(e) {
// 获取到用户选择的封面
const files = e.target.files
if (files.length === 0) {
// 用户没有选择封面
this.pubForm.cover_img = null
+ this.$refs.imgRef.setAttribute('src', defaultImg)
} else {
// 用户选择了封面
this.pubForm.cover_img = files[0]
+ const url = URL.createObjectURL(files[0])
+ this.$refs.imgRef.setAttribute('src', url)
}
}

06发表文章-状态
目标
设置发布文章状态的表单
讲解
在 data 中的 pubForm 对象上,定义 state属性,用来存储文章的发布状态
data() {
return {
// 表单的数据对象
pubForm: {
title: '',
cate_id: '',
content: '',
cover_img: null,
+ state: '' // 文章的发布状态,可选值有两个:草稿、已发布
},
}
}
在el-form最后, 再准备发布和存为草稿按钮,并绑定点击事件处理函数
<el-form-item>
<el-button type="primary" @click="pubArticleFn('已发布')">发布</el-button>
<el-button type="info" @click="pubArticleFn('草稿')">存为草稿</el-button>
</el-form-item>
在 methods 中声明 pubArticle处理函数如下
// 发布文章或草稿-按钮点击事件
pubArticleFn (state) {
// 1. 设置发布状态
this.pubForm.state = state
// 2. 表单预校验
this.$refs.pubFormRef.validate(valid => {
if (!valid) return this.$message.error('请完善文章信息!')
// 3. 判断是否提供了文章封面
if (!this.pubForm.cover_img) return this.$message.error('请选择文章封面!')
// 4. TODO:发布文章
console.log(this.pubForm)
})
}
两个按钮用了一个方法,通过什么来区分是哪个按钮那?
传递的参数 this.pubForm.state = state
3.1 后台好多都是必填的,所以点击按钮时候要进行校验!!!!


按以前的elemntui 的form表单校验,只有input是好使的,其他都不好使
2 下拉菜单不能失去焦点, 下拉菜单应该是change。

3 富文本:

修改:


4封面的校验


// 发布文章或草稿-按钮点击事件
pubArticleFn (state) {
// 1. 设置发布状态
this.pubForm.state = state
// 2. 表单预校验
this.$refs.pubFormRef.validate(valid => {
if (!valid) return this.$message.error('请完善文章信息!')
// 3. 判断是否提供了文章封面
if (!this.pubForm.cover_img) return this.$message.error('请选择文章封面!')
// 4. TODO:发布文章
console.log(this.pubForm)
})
}
在对话框完全关闭之后,清空表单的关键数据:
<!-- 发表文章的 Dialog 对话框 -->
<el-dialog
title="发表文章"
:visible.sync="pubDialogVisible"
fullscreen
:before-close="handleClose"
@closed="onDialogClosedFn"
>
<!-- 省略其它代码 -->
</el-dialog>
在 methods 中声明 onDialogClosed函数如下:
// 对话框完全关闭之后的处理函数
onDialogClosedFn () {
// 清空关键数据
this.$refs.pubFormRef.resetFields()
// 因为这2个变量对应的标签不是表单绑定的, 所以需要单独控制
this.pubForm.content = ''
this.$refs.imgRef.setAttribute('src', defaultImg)
}
07发表文章-完成功能
目标
把前面准备好的数据对象, 调用接口保存在后台
讲解
查看接口文档, 封装发布文章的接口方法
/**
* 发布文章
* @param {*} fd 表单对象
* @returns Promise对象
*/
export const uploadArticleAPI = (fd) => {
return request({
url: '/my/article/add',
method: 'POST',
data: fd // 参数要的是表单对象, 不能写普通对象, axios内部会判断, 如果是表单对象, 传递的请求体会设置Content-Type: form-data与后端对应
})
}
在发布/存草稿按钮, 点击事件中调用接口, 组织好参数传递
// 创建 FormData 对象
const fd = new FormData()
// 向 FormData 中追加数据
Object.keys(this.pubForm).forEach((key) => {
fd.append(key, this.pubForm[key])
})
// 发起请求
const { data: res } = await uploadArticleAPI(fd)
if (res.code !== 0) return this.$message.error('发布文章失败!')
this.$message.success('发布文章成功!')
// 关闭对话框
this.pubDialogVisible = false
// TODO:刷新文章列表数据