Bootstrap

2021-09-01 黑马移动端头条项目-day04

目录

首页-文章列表

一、页面布局

二、频道列表

三、文章列表

四、文章列表项


首页-文章列表

一、页面布局

1.导航栏  NavBar 导航栏

2.文章频道列表 Tab标签页

二、频道列表

1.联调接口-获取文章频道列表数据,在utils/user.js中封装好请求方法

// 获取文章频道
export const getUserChannels = () => {
  return request({
    method: 'GET',
    url: '/app/v1_0/user/channels'
  })
}

2.在home/index.vue中引入该接口的方法,同时在页面加载时created调用方法

import { getUserChannels } from "@/api/user";
created() {
    this.loadChannels();
  },
methods: {
    // 请求获取频道数据
    async loadChannels() {
      const { data } = await getUserChannels();
      this.channels = data.data.channels;
    }
  }

3.遍历数据列表

三、文章列表

目标:实现加载和未加载

                对于未加载过的文章列表会重新加载,加载过的无需加载,点击列表按钮时直接显示文章列表内容,效果如下:

           实现菜单下滑出现:下拉刷新

           实现菜单上滑出现:加载中

           实现点击不同频道加载不同的文章内容,如下的思路会导致的一个问题:每次发请求都会对数组进行重置,有一个list数组,用来存储文章列表

                a. 查看a频道,请求获取数据,让list = a 频道文章

                b. 查看b频道,请求获取数据,让list = b 频道文章

                c. 查看c频道,请求获取数据,让list = c 频道文章

实际要想的效果是:加载过的数据列表不要重新加载 

实现:准备多个list数组,每个频道对应一个,查看哪个频道就把数据往哪个频道的列表数组中存放,即不会导致覆盖问题

 但是有多少频道就得有多少频道文章数组,一个个声明很麻烦,因此利用组件来处理,具体实现:

  • 封装一个文章列表组件
  • 然后在频道列表中把文章列表遍历出来

 因为文章列表组件中请求获取文章列表数据需要频道id,所以频道id应该作为props参数传递给文章列表数组,为了方便,可直接把频道对象传递给文章列表组件

 在文章列表中请求获取对应的列表数据,展示到列表中,最后把组件在频道列表中遍历出来,如下:

 编码:

1.封装文章组件列表,新建home/components/article-list组件,props限定channel为对象

<template>
  <div class="article-list">
    1111
  </div>
</template>
<script>
export default {
  name: "ArticleList",
  props: {
    channel: {
      type: Object,
      required: true
    }
  },
  data() {
    return {};
  }
};
</script>
<style></style>

在home/index.vue中使用

引入组件 import ArticleList from "./components/article-list.vue";

注册组件 

components: {

    ArticleList

  },

使用article-list组件

    <!-- 标签页组件有一个功能,只有第一次查看标签页的时候才会渲染里面的内容 -->
    <van-tabs v-model:active="active">
      <van-tab
        v-for="channel in channels"
        :title="channel.name"
        :key="channel.id"
      >
        <!-- 文章列表 -->
        <article-list :channel="channel" />
      </van-tab>
    </van-tabs>

补充:

父子传值:如果没有v-bind,则值里面的任何内容都是字符串;如果需要动态绑定数据,则使用v-bind

2.使用List列表组件:瀑布流滚动加载,用于展示长列表

List组件通过loadingfinished两个变量控制加载状态,

组件初始化或滚动到底部时,会触发load事件并将loading设置成true,此时可以发起异步操作,并更新数据,数据更新完毕后,将loading设置成false即可

数据已全部加载完毕,则直接将finished设置成true即可。

  • load事件
    • List初始化后会触发一次load事件,用于加载第一屏的数据
    • 如果一次请求加载的数据条数较少,导致列表内容无法铺满当前屏幕,List会继续触发load事件,直到内容铺满屏幕或数据全部加载完成 
  • loading属性:控制加载中的loading状态
    • 非加载中,loading为false,此时会根据列表滚动位置判断是否触发load事件(内容列表不足一屏幕时,会直接触发)
    • 加载中,loading为true,表示正在发送异步请求,此时不会触发load事件
  • finished属性:控制加载结束的状态
    • 在每次请求完毕后,需要手动将loading设置为false,表示本次加载结束
    • 所有数据加载结束,finished为true,此时不会触发load事件
<template>
  <div class="article-list">
    <van-list
      v-model="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <van-cell v-for="item in list" :key="item" :title="item" />
    </van-list>
  </div>
</template>
<script>
export default {
  name: "ArticleList",
  props: {
    channel: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      list: [], // 数据列表
      loading: false, // 控制加载中的loading状态
      finished: false // 控制加载结束的状态,当加载结束,不再触发加载更多
    };
  },
  methods: {
    onLoad() {
      // 异步更新数据
      // setTimeout 仅做示例,真实场景中一般为 ajax 请求

      // 1.请求获取数据
      setTimeout(() => {
        // 2.把数据放到list数组中
        for (let i = 0; i < 10; i++) {
          this.list.push(this.list.length + 1);
        }
        // 3.设置本次加载状态结束,它才可以判断是否需要加载下一次,否则就会永远的停在这里
        this.loading = false;
        // 4.数据全部加载完成
        if (this.list.length >= 40) {
          // 数据全部加载完成,不再触发加载更多
          this.finished = true;
        }
      }, 1000);
    }
  }
};
</script>
<style scoped lang="less">
.article-list {
  position: fixed;
  left: 0;
  right: 0;
  top: 90px;
  bottom: 50px;
  overflow-y: auto;
}
</style>

3.获取文章列表数据

创建src/api/artice.js文件编写接口请求

src/views/home/componenets/article-list.vue中加载和编写获取数据的方法

    async onLoad() {
      // 1.请求获取数据
      const { data } = await getArticles({
        channel_id: this.channel.id, // 频道ID
        timestamp: this.timestamp || Date.now(), // 时间戳,请求新的推荐数据传当前的时间戳,请求历史推荐传指定的时间戳,timestamp相当于页码,请求最新数据使用当前最新时间戳,下一页数据使用上一次返回的数据中的时间戳
        with_top: 1 // 是否包含置顶,进入页面第一次请求时发包含置顶文章,1包含置顶,0不包含
      });
      console.log(data);
      // 2.把数据放到list数组中,利用了数组合并
      const { results } = data.data;
      this.articles.push(...results);
 }

把获取到的数据存入articles数组中,这里要用到数组合并的方法并push到articles数组中,不能直接写this.articles = data.data.results,会把articles数组中的值覆盖掉;同时在页面上进行数据渲染

<van-cell
        v-for="(article, index) in articles"
        :key="index"
        :title="article.title"
      />

 设置本次加载状态结束;判断数据是否加载完成,若未完成,则更新获取下一页数据的页码,若完成,则把加载状态设置结束

  async onLoad() {
      // 1.请求获取数据
      const { data } = await getArticles({
        channel_id: this.channel.id, // 频道ID
        timestamp: this.timestamp || Date.now(), // 时间戳,请求新的推荐数据传当前的时间戳,请求历史推荐传指定的时间戳,timestamp相当于页码,请求最新数据使用当前最新时间戳,下一页数据使用上一次返回的数据中的时间戳
        with_top: 1 // 是否包含置顶,进入页面第一次请求时发包含置顶文章,1包含置顶,0不包含
      });
      console.log(data);
      // 2.把数据放到list数组中,利用了数组合并
      const { results } = data.data;
      this.articles.push(...results);
      // 3.设置本次加载状态结束,它才可以判断是否需要加载下一次,否则就会永远的停在这里
      this.loading = false;
      // 4.数据全部加载完成
      if (results.length) {
        // 若还有下一页数据,更新获取下一页数据的页码
        this.timestamp = data.data.pre_timestamp;
      } else {
        // 没有数据了,把加载状态设置结束,不再触发加载更多
        this.finished = true;
      }
    }

4.实现下拉刷新功能 - PullRefresh 下拉刷新组件

思路:

  • 注册下拉刷新事件(组件)的处理函数
  • 发送请求获取文章列表数据 
  • 把获取到的数据添加到当前频道的文章列表的顶部 unshift
  • 提示用户刷新成功

使用下拉组件将文章列表<van-list></van-list>包裹住

<van-pull-refresh
      v-model="isRefreshLoading"
      @refresh="onRefresh"
      :success-text="refreshSuccessText"
      :success-duration="1500"
    >
<van-list>.......</van-list>
</van-pull-refresh>

将下拉刷新组件需要的数据初始化到data中

      isRefreshLoading: false, //默认下拉刷新loading状态

      refreshSuccessText: "" // 下拉刷新成功的提示

下拉刷新时会触发组件的refresh事件,在事件的回调函数中可以进行同步或异步操作,操作完成后将v-model设置为false,表示加载完成

 // 下拉刷新
    async onRefresh() {
      // 下拉刷新,组件会自己展示loading状态
      // 1.请求获取数据
      const { data } = await getArticles({
        channel_id: this.channel.id,
        timestamp: Date.now(),
        with_top: 1
      });
      // 2.把数据放到数据列表中,往顶部追加 unshift方法
      const { results } = data.data;
      this.articles.unshift(...results);

      // 3.关闭刷新的状态loading
      this.isRefreshLoading = false;

      this.refreshSuccessText = `更新了${results.length}条数据`;
    }

四、文章列表项

文章列表项的展示通过有三种排版:

第一种:

 第二种:

 第三种:

准备组件

        在该项目中有好几个页面中都有这个文章列表项内容,如果我们在每个页面中都写一次的话不仅效率低而且维护麻烦,所以把它封装为一个组件,然后在需要使用的组件中加载使用即可。

1.封装文章列表项组件,新建src/components/article-item/index.vue组件,props限定article为对象

<!-- 文章列表项展示 -->
<template>
  <div>2432</div>
</template>
<script>
export default {
  name: "index",
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  data() {
    return {};
  }
};
</script>
<style></style>

在home/components/article-list.vue中:

在文章列表组件中注册使用文章列表项组件import ArticleItem from "@/components/article-item",同时要注册组件 

components: {

    ArticleItem

  },

使用article-item组件

<article-item
          v-for="(article, index) in articles"
          :key="index"
          :article="article"
        >
</article-item>

展示列表项内容

  • 使用cell单元格组件
  • 展示标题
  • 展示底部信息
<!-- 文章列表项展示 -->
<template>
  <van-cell class="article-item">
    <div slot="title">{{ article.title }}</div>
    <div slot="label">
      <!-- 如果有3张图片,则显示3张 -->
      <div class="cover-wrap" v-if="article.cover.type === 3">
        <div class="cover-wrap-item">
          <van-image
            width="116"
            heigth="73"
            fit="cover"
            v-for="(img, index) in article.cover.images"
            :key="index"
            :src="img"
          >
          </van-image>
        </div>
      </div>
      <div class="label-wrap">
        <span>{{ article.aut_name }}</span>
        <span>{{ article.comm_count }}评论</span>
        <span>{{ article.pubdate }}</span>
      </div>
    </div>
    <!-- 如果有1张图片,则显示1张 -->
    <van-image
      v-if="article.cover.type === 1"
      width="116"
      heigth="73"
      fit="cover"
      :src="article.cover.images[0]"
    >
    </van-image>
  </van-cell>
</template>
<script>
export default {
  name: "index",
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  data() {
    return {};
  }
};
</script>
<style></style>

样式调整后的版本:

<!-- 文章列表项展示 -->
<template>
  <van-cell class="article-item">
    <div slot="title" class="title van-multi-ellipsis--13">
      {{ article.title }}
    </div>
    <div slot="label">
      <!-- 图片包裹 -->
      <!-- 如果有3张图片,则显示3张 -->
      <div class="cover-wrap" v-if="article.cover.type === 3">
        <div
          class="cover-wrap-item"
          v-for="(img, index) in article.cover.images"
          :key="index"
        >
          <van-image class="cover-item" heigth="73" fit="cover" :src="img">
          </van-image>
        </div>
      </div>
      <!-- 底部信息包裹 -->
      <div class="label-wrap">
        <span>{{ article.aut_name }}</span>
        <span>{{ article.comm_count }}评论</span>
        <span>{{ article.pubdate }}</span>
      </div>
    </div>
    <!-- 如果有1张图片,则显示1张 -->
    <van-image
      v-if="article.cover.type === 1"
      class="right-cover"
      fit="cover"
      :src="article.cover.images[0]"
    >
    </van-image>
  </van-cell>
</template>
<script>
export default {
  name: "index",
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  data() {
    return {};
  }
};
</script>
<style scoped lang="less">
.article-item {
  .title {
    font-size: 16px;
    color: #3a3a3a;
  }
  /deep/.van-cell__value {
    flex: unset; /* 去掉flex效果 */
    width: 116px;
    height: 73px;
    margin-left: 12px;
  }
  .right-cover {
    width: 116px;
    height: 73px;
  }
  .cover-wrap {
    display: flex;
    padding: 15px 0;
    .cover-wrap-item {
      flex: 1;
      height: 73px;
      &:not(:last-child) {
        padding-right: 4px;
      }
      .cover-item {
        width: 100%;
        height: 73px;
      }
    }
  }
}
</style>

然后会发现一个问题,在控制台中报错:

 若文章列表数据中的图片资源请求失败报错404,则说明资源没有了,并不是我们的问题;报错403,是因为我们项目的接口数据是后端通过爬虫抓取的第三方平台内容,而第三方平台对图片资源做了防盗链保护处理

 如何处理第三方平台的图片资源保护???

服务端一般使用Referer 请求头识别访问来源,然后处理资源访问

 补充:Referer 

 可以在控制台中的请求头里看到这个字段:

 如何解决?

不要发送referrer,对方服务器就不知道你从哪来的了,姑且认为是你自己的

如何设置不发送referrer?

 在这里我们是统一在public/index.html下设置的,因为图片较多

 <meta name="referrer" content="no-referrer" />

 2.处理相对时间

如以下形式:

推荐两个第三方库:Moment.js ,Day.js;这里以Day.js为例,官网:Day.js · 中文文档 - 2kB 大小的 JavaScript 时间日期库

  • 安装 npm install dayjs --save
  • 创建src/utils/dayjs.js
// 初始化day.js相关配置
import Vue from 'vue'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import relativeTime from 'dayjs/plugin/relativeTime'
// 配置使用处理相对时间
dayjs.extend(relativeTime)
// 配置使用中文语言包
dayjs.locale('zh-cn')

//
console.log(dayjs('2020-05-13').to(dayjs()))

// 把处理相对时间的格式包装为全局过滤器
// 然后就可以在任何组件的模板中使用了
// 所谓的过滤器其实是一个可以在模板中调用的函数而已
// 在组件的模板中使用过滤器,{{ xxx | relativeTime }}
// 管道符前面的内容会作为参数传递给过滤器函数
// 过滤器的返回值会渲染到使用过滤器的模板中
Vue.filter('relativeTime', value => {
  return dayjs(value).from(dayjs())
})
  • main.js文件引入:import './utils/dayjs'
  • 使用:<span>{{ article.pubdate | relativeTime }}</span>

;