# 内容管理-添加与编辑文章

# 回顾

  • 文章列表频道组件的封装
    • v-model在组件标签上的用法
      • :value
      • @input
    • 频道数据通过子组件进行加载
  • 素材管理
    • 加载图片列表的数据
    • 动态渲染图片列表
    • 添加分页功能
      • 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>
  • 上述样式编译的结果如下

image-20210308095456166

图示说明:深度选择器会强制使选择器之间形成父子关系

# 文章频道

目标:基于封装好的组件实现频道选择功能

<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)
<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()
    }
  }
},