# 五、文章发布

## 创建组件并配置路由

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.id
  if (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.md



1、安装

```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、在文章发布中加载使用

![image-20191121151753691](assets/image-20191121151753691.png)

> 注册

![image-20191121154827697](assets/image-20191121154827697.png)

> 在模板中使用

### 使用对话框

```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-dialog
      title="请选择文章封面图片"
      :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-dialog
      title="请选择文章封面图片"
      :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、添加一个数据字段用来存储当前点击的图片项的索引

![image-20191121171951851](assets/image-20191121171951851.png)

2、模板绑定

![image-20191121172033149](assets/image-20191121172033149.png)

### 数据绑定

1、在父组件中绑定数组元素给图片上传组件

![image-20191121171755076](assets/image-20191121171755076.png)

2、在上传图片组件中

![image-20191121171811156](assets/image-20191121171811156.png)

> 声明 value 接收数据

![image-20191121171832121](assets/image-20191121171832121.png)

> 点击对话框确认触发:将所选的图片路径发送给父组件

### 上传图片

1、添加上传图片组件

![image-20191121174046912](assets/image-20191121174046912.png)

2、记录选择的上传图片

![image-20191121174107207](assets/image-20191121174107207.png)

3、对话框确定的时候

![image-20191121174133611](assets/image-20191121174133611.png)



## 禁用路由缓存

我们发现一个小问题,从编辑文章导航到发布文章,表单内容并没有被清空,这是因为两个路由共用的同一个组件,两者之间相互跳转的时候,原来的组件实例会被复用。 因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。**不过,这也意味着组件的生命周期钩子不会再被调用**。

路由默认提供的这个功能的用意是好的,但是有时候却会带来问题,解决方案就是:**禁用缓存**。

在路由出口 `router-view` 上添加一个唯一的 `key` 即可。

![image-20191118155038989](assets/image-20191118155038989.png)

> 详细内容请查阅官方文档:[响应路由参数的变化](<[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#响应路由参数的变化)>)。