目录
首页-文章列表
一、页面布局
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组件通过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事件
<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>