仿今日头条项目——首页(展示文章列表)

1.头部导航栏组件

1、使用导航栏组件

2、在导航栏组件中插入按钮

<template>
  <div class="home-container">
    <!-- 导航栏 -->
    <van-nav-bar class="page-nav-bar">
      <van-button
        class="search-btn"
        slot="title"
        type="info"
        size="small"
        round
        icon="search"
      >搜索</van-button>
    </van-nav-bar>
    <!-- /导航栏 -->
  </div>
</template>

<style scoped lang="less">
.home-container {
  .van-nav-bar__title {
    max-width: unset;
  }
  .search-btn {
    width: 555px;
    height: 64px;
    background-color: #5babfb;
    border: none;
    font-size: 28px;
    .van-icon {
      font-size: 32px;
    }
  }
}
</style>

3.插槽:

 使用插槽,将原本只能插入文本的位置(title),用button替代

4.debug:为搜索框设置宽度时,发现宽度受限,经审查元素发现,其父元素设置了max-width,因此 

.van-nav-bar__title {
    max-width: unset;
  }


2.频道列表 

1.使用 Tab 标签页组件

2.样式调整

2.1bug:vue组件中,在style设置为scoped的时候,里面在写样式对子组件是不生效的,如果想让某些样式对所以子组件都生效,可以使用 /deep/ 深度选择器

/deep/ .channel-tabs {
    .van-tabs__wrap {
      position: fixed;
      top: 92px;
      z-index: 1;
      left: 0;
      right: 0;
      height: 82px;
    }
}

2.2处理汉堡按钮

1、使用插槽插入内容

      <div slot="nav-right" class="hamburger-btn">
        <i class="iconfont icon-gengduo"></i>
      </div>

2、样式调整

3、使用伪元素设置渐变边框

4、添加占位符充当内容区域

<div slot="nav-right" class="placeholder"></div>
      <div slot="nav-right" class="hamburger-btn">
        <i class="iconfont icon-gengduo"></i>
      </div>

bug:当指定view为flex布局后,给子元素定义width是不起效果的。 用  flex-shrink: 0;表示该盒子不缩小适应

.placeholder {
  flex-shrink: 0;
  width: 66px;
  height: 82px;
}

.hamburger-btn {
  position: fixed;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 66px;
  height: 82px;
  background-color: #fff;
  opacity: 0.902;
  i.toutiao {
    font-size: 33px;
  }
  &:before {
    content: "";
    position: absolute;
    left: 0;
    width: 1px;
    height: 100%;
    background-image: url(~@/assets/gradient-gray-line.png);
    background-size: contain;
  }
}

3.展示频道列表

1. 找数据接口


2. 把接口封装为请求方法(api/index.js)

/**
 * 获取用户自己的信息
 */
export const getUserChannels = () => {
  return request({
    method: 'GET',
    url: '/app/v1_0/user/channels'
  })
}

3. 在组件中请求获取数据

import { getUserChannels } from '@/api/user'
export default {
  name: 'HomeIndex',
  data() {
    return {
      active: 0,
      channels: [] //频道列表
    }
  },
  created() {
    this.loadChannels()
  },
  methods: {
    async loadChannels() {
      try {
        const { data } = await getUserChannels()
        this.channels = data.data.channels
      } catch (err) {
        this.$toast('获取频道数据失败')
      }
    }
  }
}
</script>

4. 模板绑定

      <van-tab v-for="obj in channels" :key="obj.id" :title="obj.name"
        >{{ obj.name }}的内容
      </van-tab>

4.文章列表(渲染思路)

根据不同的频道加载不同的文章列表:

- 有一个 `list` 数组,用来存储文章列表
- 查看 `a` 频道:请求获取数据,让 `list = a` 频道文章
- 查看 `b` 频道:请求获取数据,让 `list = b` 频道文章
- 查看 `c` 频道:请求获取数据,让 `list = c` 频道文章
- ...

 但是我们想要加载过的数据列表不要重新加载!!

因此我们准备多个 list 数组,每个频道对应一个,查看哪个频道就把数据往哪个频道的列表数组中存放,这样的话就不会导致覆盖问题

 但是一个一个声明的话会非常麻烦,所以这里利用组件来处理。

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

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

 

 1、创建文章列表子组件 `src/views/home/components/article-list.vue`

props: {
    channel: {
      type: Object,
      required: true
    }
  },

2、在 `home/index.vue` 中注册使用

      <van-tab v-for="obj in channels" :key="obj.id" :title="obj.name">
        <!-- 文章列表 -->
        <article-list :channel="obj"></article-list>
        <!-- 文章列表 -->
      </van-tab>
import ArticleList from './components/article-list.vue'
export default {
  name: 'HomeIndex',
  components: {
    ArticleList
  },
  data() {
    return {
      active: 0,
      channels: [] //频道列表
    }
  },
}

4.1使用 List 列表组件 

 List 组件通过 loading 和 finished 两个变量控制加载状态,
当组件初始化或滚动到到底部时,会触发 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 事件


5. 文章列表——加载数据

5.1.找到数据接口

5.2.封装请求方法

创建 src/api/article.js 封装获取文章列表数据的接口

import request from '@/utils/request'

/**
 * 获取频道的文章列表
 */
export const getArticles = params => {
  return request({
    method: 'GET',
    url: '/app/v1_1/articles',
    params
  })
}

5.3.请求获取数据

在首页文章列表组件 onload 的时候请求加载文章列表

要获取reuslts中的数据:         const { results } = data.data

再将获取到的数据放入list中:   this.list = results   (错误写法!!!因为页面滚动加载,会覆盖原始数据!!)

                                                  this.list.push(...results)      (正确写法!!!!)

请求失败的处理:

 

<template>
  <div class="article-list">
      <van-list
        v-model="loading"
        :finished="finished"
        finished-text="没有更多了"
        :error.sync="error"
        error-text="请求失败,点击重新加载"
        @load="onLoad"
      >
        <van-cell
          v-for="(article, index) in list"
          :key="index"
          :title="article.title"
        />
      </van-list>
  </div>
</template>

<script>
import { getArticles } from '@/api/article'

export default {
  name: 'ArticleList',
  components: {},
  props: {
    channel: {
      type: Object,
      required: true
    }
  },
  data () {
    return {
      list: [], // 文章列表数据
      loading: false, // 上拉加载更多的 loading 状态
      finished: false, // 是否加载结束
      error: false, // 是否加载失败
      timestamp: null // 请求下一页数据的时间戳
    }
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {
    // 当触发上拉加载更多的时候调用该函数
    async onLoad () {
      try {
        // 1. 请求获取数据
        const { data } = await getArticles({
          channel_id: this.channel.id, // 频道 id
          timestamp: this.timestamp || Date.now(), // 时间戳,请求新的推荐数据传当前的时间戳,请求历史推荐传指定的时间戳
          with_top: 1 // 是否包含置顶,进入页面第一次请求时要包含置顶文章,1-包含置顶,0-不包含
        })

        // 2. 把数据添加到 list 数组中
        const { results } = data.data
        this.list.push(...results)

        // 3. 设置本次加载中 loading 状态结束
        this.loading = false

        // 4. 判断数据是否加载结束
        if (results.length) {
          // 更新获取下一页数据的时间戳
          this.timestamp = data.data.pre_timestamp
        } else {
          // 没有数据了,设置加载状态结束,不再触发上拉加载更多了
          this.finished = true
        }
      } catch (err) {
        console.log(err)
        this.loading = false // 关闭 loading 效果
        this.error = true // 开启错误提示
      }
    }
  }
}
</script>

<style scoped lang="less"></style>

5.4模板绑定 

bug:头部导航栏需要固定定位


6.文章列表——下拉刷新

<van-pull-refresh>标签包住要刷新的区域 

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

 

<van-pull-refresh
      v-model="isreFreshLoading"
      :success-text="refreshSuccessText"
      :success-duration="1500"
      @refresh="onRefresh"
    >
...
</van-pull-refresh>
// 当触发下拉刷新的时候调用该函数
async onRefresh () {
  try {
    // 1. 请求获取数据
    const { data } = await getArticles({
      channel_id: this.channel.id, // 频道 id
      timestamp: Date.now(), // 下拉刷新每次都应该获取最新数据
      with_top: 1 // 是否包含置顶,进入页面第一次请求时要包含置顶文章,1-包含置顶,0-不包含
    })

    // 2. 将数据追加到列表的顶部
    const { results } = data.data
    this.list.unshift(...results)

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

    // 提示成功
    this.refreshSuccessText = `刷新成功,更新了${results.length}条数据`
  } catch (err) {
    console.log(err)
    this.isRefreshLoading = false // 关闭下拉刷新的 loading 状态
    this.$toast('刷新失败')
  }
}

bug:切换不同文章列表,返回原列表的滚动会受到影响(不在原位置)? 

因为他们并不是在自己内部滚动,而是整个body页面在滚动,无论在a频道还是b频道,其滚动都是body元素

解决:让每一个标签内容文章列表产生自己的滚动容器,这样就不会互相影响了

固定高度:height:xxx

溢出滚动:overflow-y:auto

<template>
  <div class="article-list">
   ...
  </div>
</template>
.article-list {
  // 百分比单位是相对于父元素的
  // height: 100%;

  // 视口(在移动端是布局视口)单位:vw 和 vh,不受父元素影响
  // 1vw = 视口宽度的百分之一
  // 1vh = 视口高度的百分之一
  height: 79vh;
  overflow-y: auto;
}

bug:设置height=100%无效因为百分比单位是相对于父元素的,为解决可用css3中的视口单位vw/vh,他们根据浏览器窗口大小,不受父元素影响


7.文章列表项——准备组件

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

1、创建 src/components/article-item/index.vue 组件

需要把文章列表获取到的数据传入文章列表项,因此用props接收数据:

<template>
  <div class="article-item">文章列表项</div>
</template>

<script>
export default {
  name: 'ArticleItem',
  components: {},
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less"></style>

2、在文章列表组件中注册使用文章列表项组件


8.文章列表项——展示列表项内容 

1.使用 Cell 单元格组件


2.展示标题(利用插槽)
3.展示底部信息(利用插槽)

针对图片有一张或者多张的区别,一张则放在最右,多张放最下的中心

(type属性表示有多少张图片)

 

<template>
  <van-cell
    class="article-item"
  >
    <div slot="title" class="title">{{ article.title }}</div>
    <div slot="label">
      <div v-if="article.cover.type === 3" class="cover-wrap">
        <div
          class="cover-item"
          v-for="(img, index) in article.cover.images"
          :key="index"
        >
          <van-image
            width="100"
            height="100"
            :src="img"
          />
        </div>
      </div>
      <div>
        <span>{{ article.aut_name }}</span>
        <span>{{ article.comm_count }}评论</span>
        <span>{{ article.pubdate }}</span>
      </div>
    </div>
    <van-image
      v-if="article.cover.type === 1"
      slot="default"
      width="100"
      height="100"
      :src="article.cover.images[0]"
    />
  </van-cell>
</template>

 9.文章列表项——样式调整

- 文章标题:

  + 字号
  + 颜色
  + 多行文字省略


- 单图封面
  + 封面容器:去除 flex: 1,(flex: unset;)固定宽高、左内边距
  + 封面图:宽高、填充模式:cover
- 底部文本信息
  + 字号
  + 颜色
  + 间距
- 多图封面
  + 外层容器:flex 容器、上下外边距
  + 图片容器:平均分配容器空间:flex: 1;  、固定高度、容器项间距
  + 图片:宽高、填充模式 

<template>
  <van-cell
    class="article-item"
  >
    <div slot="title" class="title van-multi-ellipsis--l2">{{ article.title }}</div>
    <div slot="label">
      <div v-if="article.cover.type === 3" class="cover-wrap">
        <div
          class="cover-item"
          v-for="(img, index) in article.cover.images"
          :key="index"
        >
          <van-image
            class="cover-item-img"
            fit="cover"
            :src="img"
          />
        </div>
      </div>
      <div class="label-info-wrap">
        <span>{{ article.aut_name }}</span>
        <span>{{ article.comm_count }}评论</span>
        <span>{{ article.pubdate }}</span>
      </div>
    </div>
    <van-image
      v-if="article.cover.type === 1"
      slot="default"
      class="right-cover"
      fit="cover"
      :src="article.cover.images[0]"
    />
  </van-cell>
</template>

<script>
export default {
  name: 'ArticleItem',
  components: {},
  props: {
    article: {
      type: Object,
      required: true
    }
  },
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.article-item {
  .title {
    font-size: 32px;
    color: #3a3a3a;
  }

  .van-cell__value {
    flex: unset;
    width: 232px;
    height: 146px;
    padding-left: 25px;
  }

  .right-cover {
    width: 232px;
    height: 146px;
  }

  .label-info-wrap span {
    font-size: 22px;
    color: #b4b4b4;
    margin-right: 25px;
  }

  .cover-wrap {
    display: flex;
    padding: 30px 0;
    .cover-item {
      flex: 1;
      height: 146px;
      &:not(:last-child) {
        padding-right: 4px;
      }
      .cover-item-img {
        width: 100%;
        height: 146px;
      }
    }
  }
}
</style>

 10.文章列表项——关于第三方图片资源403问题

文章列表数据中的好多图片资源请求失败返回 403?

因为项目的接口数据是后端通过爬虫抓取的第三方平台内容,而第三方平台对图片资源做了防盗链保护处理。

第三方平台一般使用 Referer 请求头识别访问来源,然后处理资源访问。

 (Referer 是 HTTP 请求头的一部分,当浏览器向 Web 服务器发送请求的时候,一般会带上 Referer,它包含了当前请求资源的来源页面的地址。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。)

解决:不要发送 referrer 

直接在 HTMl 页面头中通过 meta 属性全局配置(public/index.html):

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

 11.文章列表项——处理相对时间

Moment.js
Day.js
(https://day.js.org/)

两者都是专门用于处理时间的 JavaScript 库,功能差不多,因为 Day.js 的设计就是参考的 Moment.js。但是 Day.js 相比 Moment.js 的包体积要更小一些,因为它采用了插件化的处理方式。

Day.js 可以运行在浏览器和 Node.js 中。

- 🕒 和 Moment.js 相同的 API 和用法
- 💪 不可变数据 (Immutable)
- 🔥 支持链式操作 (Chainable)
- 🌐 国际化 I18n
- 📦 仅 2kb 大小的微型库
- 👫 全浏览器兼容

1、安装

2、创建 (utils/dayjs.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')

// 全局过滤器:处理相对时间
Vue.filter('relativeTime', value => {
  return dayjs().to(dayjs(value))
})

3、在 main.js 中加载初始化: import './utils/dayjs'

4、使用(过滤器)

<p>{{ 日期数据 | relativeTime }}</p>

 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值