# 内容管理-文章列表

# 回顾

  • 主页
    • axios拦截器统一处理token
    • axios响应拦截器处理返回的数据
    • 导航守卫和拦截器的关系
    • 处理token失效的问题(token的续签)
    • 退出功能
      • 原生的事件是否可以直接绑定到组件的标签上?不可以
      • 如果确实想使用原生的事件,需要在添加事件修饰符native
      • 基于组件库提供的规则实现事件的绑定
  • 文章列表
    • 配置组件的基本路由
    • 控制左侧菜单的高亮 $route.path
    • 文章列表页面的基本布局
      • 顶部筛选条件
      • 底部的内容列表
    • 获取文章列表的数据并且填充表格
    • 插槽
      • 基本的插槽用法
      • 具名插槽的用法
      • 作用域插槽

# 列表页面功能

# 文章列表渲染完善

目标:通过作用域插槽定制相关列的布局样式

  • el-table-column组件之间通过插槽定制列的模板内容
  • 如何获取每一列的数据?<template v-slot='slotProps'>
    • slotProps.row可以获取每一行对应的表格数据
<el-table
  :data='list'>
  <el-table-column label="封面">
    <!-- 定制图片的效果 -->
    <!-- slotProps.row表示其中一条数据,来源于自组件 -->
    <template v-slot='slotProps'>
      <el-image class="cover-img" :src="slotProps.row.cover.images[0]">
        <!-- 如果图片的地址加载失败,就显示标签之间的内容 -->
        <div slot="error" class="image-slot">
          <i class="el-icon-picture-outline"></i>
        </div>
      </el-image>
    </template>
  </el-table-column>
  <el-table-column label="标题" prop="title"></el-table-column>
  <el-table-column label="状态">
    <!-- 默认显示数组 -->
    <!-- 这一列的内容由插槽决定 -->
    <template v-slot='slotProps'>
      <!-- status这个数据从哪里来的?子组件来的 -->
      <el-tag v-if="slotProps.row.status===0" type="info">草稿</el-tag>
      <el-tag v-else-if="slotProps.row.status===1">待审核</el-tag>
      <el-tag v-else-if="slotProps.row.status===2" type="success">审核通过</el-tag>
      <el-tag v-else-if="slotProps.row.status===3" type="warning">审核失败</el-tag>
      <el-tag v-else type="danger">已删除</el-tag>
    </template>
  </el-table-column>
  <el-table-column label="发布时间" prop="pubdate"></el-table-column>
  <el-table-column label="操作" width="120px">
    <!-- 这里是插槽内容 -->
    <template>
      <el-button plain type="primary" icon="el-icon-edit" circle></el-button>
      <el-button plain type="danger" icon="el-icon-delete" circle></el-button>
    </template>
  </el-table-column>
</el-table>
  • 模仿ElementUi内部实现
<!-- 父组件 -->
<el-table-column label="状态" >
  <!-- 定制这一列的结果 -->
  <!-- 需求:不同的状态值,显示不同的文本 -->
  <!-- 这里如何获取子组件的值 -->
  <template v-slot='abc'>
    <div v-if='abc.row.status===2'>审核通过</div>
  </template>
</el-table-column>
<!-- 子组件 -->
<!-- el-table-column组件的定义 -->
<template>
  <div>
    <td>
      <slot v-bind:row='row'>{{row.status}}</slot>
    </td>
  </div>
</template>
<script>
export default {
  data () {
    return {
      row: {
        status: 2
      }
    }
  }
}
</script>

# 频道条件动态渲染

目标:动态渲染下拉列表数据

  • 获取频道列表数据
import { loadArticleList, loadChannelList } from '@/api/article.js'

// 获取筛选条件的频道列表数据
async loadChannelList () {
  try {
    const ret = await loadChannelList()
    this.channels = ret.data.channels
  } catch (e) {
    console.log(e)
    this.$message.error('获取频道列表数据失败!')
  }
},
  • 动态渲染频道列表数据
<!-- 频道列表 -->
<el-form-item label="频道列表:">
  <el-select value='' placeholder="请选择频道列表">
    <el-option :key='item.id' v-for='item in channels' :label="item.name" :value="item.id"></el-option>
  </el-select>
</el-form-item>

# 筛选功能实现

  • 单选框数据绑定
  • 下拉列表数据绑定
  • 日期数据绑定
data () {
  return {
    filterParams: {
      // 文章的状态
      status: null,
      // 文章所属频道
      channel_id: null,
      // 开始时间
      begin_pubdate: null,
      // 结束时间
      end_pubdate: null,
      // 页码
      page: 1,
      // 每页的条数
      per_page: 10
    },
    // 文章发布时间范围
    rangeTime: '',
    // 文章列表数据
    list: [],
    // 文章列表总数
    total: 0,
    // 频道列表数据
    channels: []
  }
},
  • 绑定操作

都是用v-model方式绑定数据

<el-radio-group v-model='filterParams.status'>
<el-date-picker
  v-model='filterTime'
  @change='handleChange'
  type="daterange"
  range-separator=""
  start-placeholder="开始日期"
  end-placeholder="结束日期"
  value-format="yyyy-MM-dd"
></el-date-picker>
handleChange (date) {
    // 监控时间返回的选择动作
    this.filterParams.begin_pubdate = date[0]
    this.filterParams.end_pubdate = date[1]
},
  • 点击按钮触发接口调用
// 查询操作
handleQuery () {
  // 点击查询后应该发生什么?调用接口查询文章列表数据并且传递参数
  // 查询数据时,需要把页码重置为1,保证能查询到数据
  this.filterParams.page = 1
  this.loadArticleList()
},

# 文章列表分页

目标:熟悉分页组件的基本用法

  • 分页基本布局
    • layout 控制分页条的布局
    • total 控制整个列表的总数
    • current-page 表示当前的页码
    • page-size 每页显示的条数
    • current-change 表示页码切换时触发的事件
  • 页面切换操作
<el-pagination
    style="margin-top:20px;text-align:right"
    background
    layout="prev, pager, next"
    @current-change="changePage"
    :total="total"
    :current-page="filterParams.page"
    :page-size="filterParams.per_page"
></el-pagination>
// 改变分页
changePage (page) {
    this.filterParams.page = page
    this.loadArticleList()
}

# 删除文章

目标:能够实现删除文章的功能

步骤:

1、绑定删除按钮的点击事件并且获取id

2、调用接口删除文章

3、重新渲染文章列表

  • 绑定删除文章事件
<el-button @click="handleDelete(scopeProps.row.id)" plain type="danger" icon="el-icon-delete" circle></el-button>
  • 实现删除文章功能
// 删除文章
handleDelete (id) {
  this.$confirm('确认要删除吗?')
    .then(async () => {
      await deleteArticle(id)
      this.loadArticleList()
    })
    .catch((e) => {
      // 这个catch有两个作用:1、点击取消触发;2、有异常触发
      // 如果点击取消,那么参数e的值就是cancel
      if (e === 'cancel') {
        console.log('取消删除')
      } else {
        this.$message.error('删除文章失败!')
      }
    })
},

# 处理删除文章id超范围问题

js表示的整数最大值 Number.MAX_SAFE_INTEGER = 9007199254740991

超过这个范围后,js无法表示这个数

  1. 安装依赖包
npm i json-bigint
  1. 导入包并使用
// json字符串
// 原生js把json字符串转换为对象时,不支持这么大的数
var obj = '{ "value" : 1367686301318381568, "v2": 123 }'
var info = JSON.parse(obj)
// 所以,你非要用原生js转换,就会导致数据失真
console.log(info.value) // 1367686301318381600

// 使用第三方包 json-bigint 提供的api可以正常处理这么大的数据
var JSONbig = require('json-bigint');
var info1 = JSONbig.parse(obj)
console.log(info1.value.toString()) // 1367686301318381568
  • axios统一处理返回的数据
// 专门处理返回的数据格式
axios.defaults.transformResponse = [(data) => {
  // 参数data表示后端返回的数据
  // 删除成功后,后端有返回数据吗?没有
  try {
    // 把后端返回的超大整数转换为正确的数据
    return JSONbig.parse(data)
  } catch (e) {
    console.log(e)
    // 如果返回的数据无法转换为正常的数据,就返回原始内容
    return data
  }
}]

# 频道通用组件封装

目标:能够完成频道通用组件的封装

  • 为什么要封装频道组件?多个地方都需要用到这个功能
  • 封装组件的步骤

1、封装组件代码

  • value 决定下拉列表默认选中谁(从父组件传递过来)
  • input事件监听选中值的变化
  • 值发生变化后,把最新的选中的值传递回父组件 this.$emit('select-channel', id)
<el-select :value='channel_id' @input='handleSelect' placeholder="请选择频道列表:">
    <el-option v-for='item in channels' :key='item.id' :label="item.name" :value="item.id"></el-option>
</el-select>

2、使用组件

  • :channel_id='filterParams.channel_id' 把值传递给子组件的value属性
  • @select-channel='updateChannelId' 用于接收子组件传递回来的最新选中的项目id
  • :channels='channels'把频道列表数据传递给子组件用于遍历下拉列表
<my-channel
@select-channel='updateChannelId'
:channel_id='filterParams.channel_id'
:channels='channels'></my-channel>
// 更新频道id
updateChannelId (id) {
  // 接收子组件传递过来的id值
  this.filterParams.channel_id = id
},

# 另一种实现


  1. 先封装组件功能 (单文件组件 MyChannel.vue)
<template>
  <el-select :value='value' @input='handleSelect' placeholder="请选择频道列表:">
    <el-option v-for='item in channels' :key='item.id' :label="item.name" :value="item.id"></el-option>
  </el-select>
</template>
<script>
import { loadChannelList } from '@/api/article.js'

export default {
  name: 'MyChannel',
  data () {
    return {
      channels: []
    }
  },
  props: {
    // channels: {
    //   // 配置类型的目的是进行类型验证,如果传递的数据不是这个类型,就会警告
    //   type: Array
    // },
    value: {
      type: Number
    }
  },
  methods: {
    async loadChannelList () {
      // 加载频道列表数据
      try {
        const ret = await loadChannelList()
        this.channels = ret.data.channels
      } catch (e) {
        console.log(e)
        this.$message.error('获取频道列表数据失败!')
      }
    },
    handleSelect (id) {
      // 这里的input事件函数中获取到的直接就是选项的id
      // 这个规则是Element规定的
      // 这里选中的id,需要传递给父组件
      this.$emit('input', id)
    }
  },
  created () {
    this.loadChannelList()
  }
}
</script>

  1. 再导入并使用组件
// 1、导入组件
import MyChannel from '@/components/MyChannel.vue'
export default {
    // 2、配置组件
    components: {
        MyChannel
    }
}
<!-- 3、使用组件 -->
<my-channel v-model='filterParams.channel_id'></my-channel>