一、我的页面布局
1.1未登录头部状态
![](https://i-blog.csdnimg.cn/blog_migrate/15ef0ca4d8af95ddc9ab5f9e1245cd92.png)
<!-- 视图层: html -->
<template>
<div class="my-container">
<!-- 未登录 -->
<div v-else class="header not-login">
<div class="login-btn">
<div class="img">
<span class="iconfont icon-shoujihaoma"></span>
</div>
<span class="text">登录 / 注册</span>
</div>
</div>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
</script>
<style lang="less" scoped>
.my-container {
margin-bottom: 200px;
.header {
height: 461px;
background-color: rgb(197, 66, 34);
}
.not-login {
display: flex;
justify-content: center;
align-items: center;
.login-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.img {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 132px;
height: 132px;
background-color: #fff;
border-radius: 50%;
.icon-shoujihaoma {
position: absolute;
font-size: 70px;
color: rgb(197, 66, 34);
}
}
.text {
color: #fff;
font-size: 32px;
margin-top: 18px;
}
}
}
}
</style>
1.2已登录头部
![](https://i-blog.csdnimg.cn/blog_migrate/461487284baf33e63ffde6daad0efa99.png)
<template>
<div class="my-container">
<!-- 已登录:用户信息 -->
<div class="header user-info">
<div class="base-info">
<div class="left">
<van-image
round
fit="cover"
class="avatar"
:src=""
/>
<span class="name">黑马先锋</span>
</div>
<div class="right">
<van-button size="mini" round>编辑资料</van-button>
</div>
</div>
<div class="data-stats">
<div class="data-item">
<span class="count">10</span>
<span class="text">头条</span>
</div>
<div class="data-item">
<span class="count">10</span>
<span class="text">关注</span>
</div>
<div class="data-item">
<span class="count">10</span>
<span class="text">粉丝</span>
</div>
<div class="data-item">
<span class="count">10</span>
<span class="text">获赞</span>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.my-container {
margin-bottom: 200px;
.header {
height: 461px;
background-color: rgb(197, 66, 34);
}
.user-info {
.base-info {
height: 331px;
padding: 76px 32px 23px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: center;
.avatar {
width: 132px;
height: 132px;
margin-right: 23px;
border: 1px solid #fff;
}
.name {
font-size: 32px;
color: #fff;
}
}
}
.data-stats {
display: flex;
.data-item {
height: 130px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
.count {
font-size: 36px;
}
.text {
font-size: 23px;
}
}
}
}
}
</style>
1.3宫格导航
![](https://i-blog.csdnimg.cn/blog_migrate/2d1486d104926cf17cba936a0c62021d.png)
<!-- 导航 -->
<van-grid clickable :column-num="2" class="grid-nav">
<van-grid-item class="grid-item">
<i slot="icon" class="news news-shoucang"></i>
<span slot="text" class="text">收藏</span>
</van-grid-item>
<van-grid-item class="grid-item">
<i slot="icon" class="news news-lishi"></i>
<span slot="text" class="text">历史</span>
</van-grid-item>
</van-grid>
<style lang="less" scoped>
.grid-nav {
margin-bottom: 15px;
.grid-item {
height: 141px;
i.news {
font-size: 45px;
color: rgb(197, 66, 34);
}
span.text {
font-size: 28px;
margin-top: 10px;
}
}
}
</style>
1.4单元格导航
![](https://i-blog.csdnimg.cn/blog_migrate/6d4223e8d7fb64747f485ee1572b66cb.png)
<van-cell title="消息通知" is-link to="/home" class="cell" />
<van-cell title="小智同学" is-link to="/" class="cell" />
<van-cell
clickable
@click="onLogout"
v-if="$store.state.user"
title="退出登录"
class="exit"
/>
<style lang="less" scoped>
.cell {
font-size: 30px;
}
.exit {
text-align: center;
margin-top: 15px;
font-size: 30px;
color: rgb(197, 66, 34);
}
</style>
二、处理已登录和未登录的页面展示
未登录,展示登录按钮
已登录,展示登录用户信息
<!-- 已登录:用户信息 -->
<div v-if="$store.state.user" class="header user-info">
</div>
<!-- 未登录 -->
<div v-else class="header not-login">
</div>
<!-- 退出 -->
<van-cell
clickable
@click="onLogout"
v-if="$store.state.user"
title="退出登录"
class="exit"
/>
三、用户登录
<div class="login-btn" @click="$router.push('/login')">
<div class="img">
<span class="iconfont icon-shoujihaoma"></span>
</div>
四、用户退出
给退出按钮注册点击事件
退出处理
<script setup>
import { showConfirmDialog } from "vant";
import "vant/es/dialog/style";
import "vant/es/toast/style";
import { useStore } from "vuex";
const store = useStore();
function onLogout() {
showConfirmDialog({
title: "确认退出吗?",
})
.then(() => {
// on confirm
// 确认退出:清除登录状态(容器中的 user + 本地存储中的 user)
store.commit("setUser", null);
})
.catch(() => {
// on cancel
console.log("取消");
});
}
</script>
五、展示登录用户信息
步骤:
封装接口
请求获取数据
模板绑定
1、在 api/user.js 中添加封装数据接口
![](https://i-blog.csdnimg.cn/blog_migrate/ce70aa8b73915fe30620a144a996ce90.png)
// 获取用户自己信息
export const getUserInfo = () => {
return request({
method: 'GET',
url: '/v1_0/user'
headers:{
// 注意:该接口需要授权才能访问
Authorization:`Bearer ${store.state.user.token}`
}
})
}
2、在src/store/index.js获取当前用户的信息
import { createStore } from 'vuex'
import { getUserInfo } from '~/api/user.js'
export default createStore({
state: {
// 用户信息
userInfo: {},
},
mutations: {
// 设置用户信息
setUserInfo(state, users) {
state.userInfo = users
},
},
actions: {
getInfo({ commit }) {
return new Promise((resolve, reject) => {
// 获取当前用户的信息
getUserInfo().then((res) => {
// 获取成功后,将数据存储在vuex 的setUserInfo对象中
commit("setUserInfo", res.data.data)
console.log("用户信息",res.data.data);
resolve(res)
}).catch((err) => {
// 失败回调
reject(err)
});
})
}
},
modules: {
}
})
3、在views/me.vue请求加载数据
![](https://i-blog.csdnimg.cn/blog_migrate/901bd3ef2b109c2232a8d225ed37d4e8.png)
<script setup>
import { useStore } from "vuex";
const store = useStore();
if (store.state.user) {
store.dispatch("getInfo");
}
</script>
4、在views/me.vue进行模板绑定
<!-- 已登录:用户信息 -->
<div v-if="$store.state.user" class="header user-info">
<div class="base-info">
<div class="left">
<van-image
round
fit="cover"
class="avatar"
:src="$store.state.userInfo.photo"
/>
<span class="name">{{ $store.state.userInfo.name }}</span>
</div>
<div class="right">
<van-button size="mini" round>编辑资料</van-button>
</div>
</div>
<div class="data-stats">
<div class="data-item">
<span class="count">{{ $store.state.userInfo.art_count }}</span>
<span class="text">头条</span>
</div>
<div class="data-item">
<span class="count">{{ $store.state.userInfo.follow_count }}</span>
<span class="text">关注</span>
</div>
<div class="data-item">
<span class="count">{{ $store.state.userInfo.fans_count }}</span>
<span class="text">粉丝</span>
</div>
<div class="data-item">
<span class="count">{{ $store.state.userInfo.like_count }}</span>
<span class="text">获赞</span>
</div>
</div>
</div>
六、优化设置Token
项目中的接口除了登录之外大多数接口都需要提供token才有访问权限。
通过接口文档可以看到,后端接口要求我们将token放到请求头Header中并以以下格式发送。
![](https://i-blog.csdnimg.cn/blog_migrate/b330da8025093b77654fa5b755b3541f.png)
字段名:Authorization
字段值:Bearer token,注意Bearer 和token之间有一个空格
方式一:在每次请求的时候手动添加(麻烦)。
return request({
method: 'GET',
url: '/v1_0/user'
headers:{
// 注意:该接口需要授权才能访问
Authorization:`Bearer token`
}
})
方式二:使用请求拦截器统一添加(推荐,更方便)。
![](https://i-blog.csdnimg.cn/blog_migrate/55b90f8a475a5b3e93ae520d41ea53de.png)
在 src/utils/request.js 中添加拦截器统一设置 token:
import axios from "axios";
import store from '~/store'
const request= axios.create({
baseURL:'http://toutiao.itheima.net'
})
// 添加请求拦截器
request.interceptors.request.use(function (config) {
const {user}=store.state
// 如果用户已登录,统一给接口设置token信息
if(user){
config.headers. Authorization=`Bearer ${user.token}`
}
// 处理完之后一定要把config返回,否则请求就会停在这里
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
//导出
export default request
七、首页--文章列表页面
![](https://i-blog.csdnimg.cn/blog_migrate/b710fec20bc860f53084d89935eaa897.png)
头部导航栏
https://vant-contrib.gitee.io/vant/#/zh-CN/search
show-action:是否在搜索框右侧显示取消按钮
disabled:是否禁用输入框
shape:搜索框形状,可选值为 round
src/views/home.vue
<van-search
disabled
background="rgb(197, 66, 34)"
show-action
shape="round"
placeholder="请输入搜索关键词"
>
<template #action>
<div class="seachico">
<van-icon name="add-o" />
</div>
</template>
</van-search>
<style lang="less" scoped>
.seachico {
color: white;
font-size: 50px;
}
</style>
频道列表
使用Tab标签页组件
![](https://i-blog.csdnimg.cn/blog_migrate/3ead1018c8cae7d5a6bb7c20786331b4.png)
animated:是否开启切换标签内容时的转场动画
swipeable:是否开启手势左右滑动切换
title-active-color:标题选中态颜色
color:标签主题色
v-model:active:绑定当前选中标签的标识符
<van-tabs
sticky
v-model:active="active"
animated="true"
swipeable="true"
title-active-color="rgb(197, 66, 34)"
color="rgb(197, 66, 34)"
class="channel-tabs"
>
<van-tab title="关注">
关注
</van-tab>
<van-tab title="推荐">
推荐
</van-tab>
<van-tab title="热榜">
热榜
</van-tab>
<van-tab title="视频">
视频
</van-tab>
<van-tab title="科技">
科技
</van-tab>
<van-tab title="图片">
图片
</van-tab>
<template #nav-right>
<i class="placeholder"></i>
<van-icon name="wap-nav" size="20" class="hamburger-btn"></van-icon>
</template>
</van-tabs>
<style lang="less" scoped>
.channel-tabs .hamburger-btn {
/* 固定定位 */
position: fixed;
/* 最右侧 */
right: 0;
display: flex;
justify-content: center;
align-items: center;
width: 66px;
height: 80px;
background-color: #fff;
/* 设置透明度 */
opacity: 0.902;
}
.channel-tabs .placeholder {
flex-shrink: 0;
width: 66px;
height: 80px;
}
/deep/ .van-tabs__line {
width: 40px !important;
height: 6px !important;
}
</style>
展示频道列表
![](https://i-blog.csdnimg.cn/blog_migrate/e2e465809b877117eeedf535602a08cb.png)
思路:
找数据接口
把接口封装为请求方法
在组件中请求获取数据
模板绑定
1、在src/api/user.js封装数据请求接口
// 获取用户的频道
export const getUserChannels = () => {
return request({
method: 'GET',
url: '/v1_0/user/channels'
})
}
2、在src/store/index.js请求获取数据
import { createStore } from 'vuex'
import { getUserInfo,getUserChannels } from '~/api/user.js'
export default createStore({
state: {
// 频道列表
channels:[]
},
mutations: {
// 设置频道列表
setChannels(state, channelsList) {
state.channels = channelsList
},
},
actions: {
getChannels({ commit }) {
return new Promise((resolve, reject) => {
// 获取当前频道列表信息
getUserChannels().then((res) => {
// 获取成功后,将数据存储在vuex 的setUserInfo对象中
commit("setChannels", res.data.data.channels)
console.log("频道列表信息",res.data.data.channels
);
resolve(res)
}).catch((err) => {
// 失败回调
reject(err)
});
})
}
},
modules: {
}
})
3、模板绑定
<van-tab
v-for="channel in $store.state.channels"
:key="channel.id"
:title="channel.name"
>
{{ channel.name }}
</van-tab>
import { useStore } from "vuex";
const store = useStore();
store.dispatch("getChannels");
文章列表
思路:
找到数据接口
封装请求方法
在组件中请求获取数据,将数据存储到data中
模板绑定展示
根据不同的频道加载不同的文章列表,你的思路可能是这样的:
有一个list数组,用来存储文章列表
查看a频道:请求获取数据,让list=a频道文章
查看b频道:请求获取数据,让list=b频道文章
查看c频道:请求获取数据,让list=c频道文章
![](https://i-blog.csdnimg.cn/blog_migrate/fb75d7b173c558dbd0c4d009585b6d7a.png)
思路没有问题,但是并不是我们想要的效果。
我们想要的效果是:加载过的数据列表不要重新加载。
实现思路也非常简单,就是我们准备多个list数组,每个频道对应一个,查看哪个频道就把数据往哪个频道的列表数据中存放,这样的话就不会导致覆盖问题。
![](https://i-blog.csdnimg.cn/blog_migrate/b53a463375ff5fbd8e0aa293dd0a962a.png)
可是有多少频道就得有多少频道文章数组,我们都一个一个声明的话会非常麻烦,所以这里的建议是利用组件来处理。
具体做法:
封装一个文章列表组件
然后在频道列表中把文章列表遍历出来
因为文章列表组件中请求获取文章列表数据需要频道id,所以频道id应该作为props参数传递给文章列表组件,为了方便,我们直接把频道对象传给文章列表组件就可以了。
![](https://i-blog.csdnimg.cn/blog_migrate/0c2962aedbbe7df168577557985ce551.png)
在文章列表中请求获取对应的列表数据,展示到列表中。最后把组件在频道列表中遍历出来。
![](https://i-blog.csdnimg.cn/blog_migrate/7e9164d7161e469133a25f91aab4013e.png)
为什么标签内容是懒渲染的?
因为这是Tab标签页组件本身支持的默认功能,如果不需要可以通过配置:lazy-render="false"来关闭这个效果。
1、创建 src/components/home/article-list.vue
<template>
<div class="article-list">文章列表</div>
</template>
<!-- 逻辑层:js -->
<script setup>
const props = defineProps({
channel: {
type: Object,
required: true,
},
});
</script>
2、在src/views/home.vue中注册使用
<ArticleList :channel="channel"></ArticleList>
使用List列表组件
List列表组件:瀑布流滚动加载,用于展示长列表。
https://vant-contrib.gitee.io/vant/#/zh-CN/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="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import { ref } from "vue";
const props = defineProps({
channel: {
type: Object,
required: true,
},
});
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const onLoad = () => {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push(list.value.length + 1);
}
// 加载状态结束
loading.value = false;
// 数据全部加载完成
if (list.value.length >= 40) {
finished.value = true;
}
}, 1000);
};
</script>
让头部固定定位和van-tabs粘性定位
<div class="search">
<van-search
fixed
disabled
background="rgb(197, 66, 34)"
show-action
shape="round"
placeholder="请输入搜索关键词"
>
<template #action>
<div class="seachico">
<van-icon name="add-o" />
</div>
</template>
</van-search>
</div>
<van-tabs
v-model:active="active"
animated="true"
swipeable="true"
title-active-color="rgb(197, 66, 34)"
color="rgb(197, 66, 34)"
class="channel-tabs"
sticky
offset-top="46px"
>
......
</van-tabs>
......
.search {
width: 100%;
transform: none;
position: fixed;
z-index: 9999;
}
加载文章列表数据
实现思路:
找到数据接口
封装请求方法
请求获取数据
模板绑定
1、创建 src/api/article.js 封装获取文章列表数据的接口
import request from '~/utils/request'
// 获取文章新闻推荐
export const getArticles= params =>{
return request({
method:'GET',
url:'/v1_0/articles',
params
})
}
2、然后在首页文章列表组件的 onload中请求加载文章列表
<!-- 视图层: html -->
<template>
<div class="article-list">
<van-list
v-model:loading="loading"
v-model:error="error"
error-text="请求失败,点击重新加载"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-for="(item, index) in articles"
:key="index"
:title="item.title"
/>
</van-list>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import { getArticles } from "~/api/article.js";
import { ref } from "vue";
const props = defineProps({
channel: {
type: Object,
required: true,
},
});
const articles = ref([]);
const loading = ref(false);
const error = ref(false);
const finished = ref(false);
const timestamp = ref(null); //获取下一页的时间戳
async function onLoad() {
try {
// 1.请求获取数据
const { data } = await getArticles({
channel_id: props.channel.id, //频道id
timestamp: timestamp.value || Date.now(), //时间戳,请求新的推荐数据传当前的时间戳,请求历史推送传指定的时间戳
with_top: 1, //是否包含置顶,进入页面第一次请求时要包含置顶文章,1-包含置顶,0-不包含
});
console.log(data);
// 2.把数据放到list数组中
const { results } = data.data;
articles.value.push(...results); //es6展开运算符
// 3.设置本次加载状态结束,它才可以判断是否需要加载下一次,否则就会永远的停在这里
loading.value = false;
// 4.数据全部加载完成
if (results.length) {
// 更新获取下一页数据的页码
timestamp.value = data.data.pre_timestamp;
} else {
// 没有数据了,把加载状态设置结束,不再触发加载更多
finished.value = true;
}
} catch (err) {
loading.value = false;
error.value = true;
}
}
</script>
下拉刷新
这里主要用到vant中的PullRefresh 下拉刷新。
https://vant-contrib.gitee.io/vant/#/zh-CN/pull-refresh
思路:
注册下拉刷新事件的处理函数
发送请求获取文章列表数据
把获取到的数据添加到当前频道的文章列表的顶部
提示用户刷新成功
下拉刷新时会触发 refresh 事件,在事件的回调函数中可以进行同步或异步操作,操作完成后将 v-model 设置为 false,表示加载完成。
success-duration:刷新成功提示展示时长(ms)
success-text:刷新成功提示文案
async function onRefresh() {
try {
// 下拉刷新,组件自己就会展示loading状态
// 1.请求获取数据
const { data } = await getArticles({
channel_id: props.channel.id, //频道id
timestamp: Date.now(), //时间戳,请求新的推荐数据传当前的时间戳,请求历史推送传指定的时间戳
with_top: 1, //是否包含置顶,进入页面第一次请求时要包含置顶文章,1-包含置顶,0-不包含
});
// 2.把数据放到数据列表中(往顶部追加)
const { results } = data.data;
articles.value.unshift(...results);
// 3.关闭刷新的状态loading
isRefreshLoading.value = false;
refreshSuccessText.value = `更新了${results.length}条数据`;
} catch (err) {
isRefreshLoading.value = false;
showToast("刷新失败");
}
}
文章列表项
在我们项目中有好几个页面中都有这个文章列表项内容,如果我们在每个页面中都写一次的话不仅效率低而且维护起来也麻烦。所以最好的办法就是我们把它封装为一个组件,然后在需要使用的组件中加载使用即可。
1、创建 src/components/article-item/index.vue 组件
<template>
<div>文章列表项</div>
</template>
<!-- 逻辑层:js -->
<script setup>
const props = defineProps({
article: {
type: Object,
required: true,
},
});
</script>
2、在文章列表组件article-list.vue中注册使用文章列表项组件
<ArticleItem
v-for="(article, index) in articles"
:key="index"
:article="article"
></ArticleItem>
展示列表项内容
使用cell单元格组件
展示标题
展示底部信息
https://vant-ui.github.io/vant/#/zh-CN/cell#cell-slots
title:自定义左侧标题
label:自定义标题下方的描述信息
default:默认插槽
src/components/article-item/index.vue 组件
文章标题
字号、颜色、多行文字省略
单图封面
封面容器
去除flex:1,固定宽高
左内边距
封面图
宽高
填充模式:cover
底部文本信息
字号、颜色、间距
多图封面
外出容器
flex容器
上下外边距
图片容器
平均分配容器空间:flex:1
固定高度
容器项间距
图片
宽高
填充模式
<!-- 视图层: html -->
<template>
<van-cell class="article-item">
<template #title>
<span class="title van-ellipsis">{{ article.title }}</span>
</template>
<template #label>
<div v-if="article.cover.type === 3" class="cover-wrap">
<div
class="cover-wrap-item"
v-for="(img, index) in article.cover.images"
:key="index"
>
<van-image fit="cover" :src="img" class="cover-item" />
</div>
</div>
<div class="label-wrap">
<span>{{ article.aut_name }}</span>
<span>{{ article.comm_count }}评论</span>
<span>{{ article.pubdate }}</span>
</div>
</template>
<template #default v-if="article.cover.type === 1">
<van-image
class="right-cover"
fit="cover"
:src="article.cover.images[0]"
/>
</template>
</van-cell>
</template>
<!-- 逻辑层:js -->
<script setup>
const props = defineProps({
article: {
type: Object,
required: true,
},
});
</script>
<style lang="less" scoped>
.article-item {
/deep/ .title {
display: inline-block;
width: 409px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16px;
color: #3a3a3a;
}
/deep/ .van-cell__value {
flex: unset !important;
width: 256px;
height: 161px;
margin-left: 12px;
}
.right-cover {
width: 246px;
height: 154px;
}
.cover-wrap {
padding: 15px 0;
display: flex;
flex-wrap: nowrap;
.cover-wrap-item {
flex: 1;
height: 154px;
&:not(:last-child) {
padding-right: 15px;
}
.cover-item {
width: 100%;
height: 154px;
}
}
}
.label-wrap {
width: 409px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 70px;
span {
margin-right: 10px;
}
}
}
</style>
关于第三方图片资源403问题
为什么文章列表数据中的好多图片资源请求失败返回403?
这是因为我们项目的接口数据是后端通过爬虫抓取的第三方平台内容,而第三方平台对图片资源做了防盗链保护处理。
第三方平台怎么处理图片资源保护的?
服务端一般使用Referer请求头识别访问来源,然后处理资源访问。
![](https://i-blog.csdnimg.cn/blog_migrate/5b4ac8262a976f0a17c1f9df895d5749.png)
Referer是什么东西?
HTTP Referer 教程 - 阮一峰的网络日志 (ruanyifeng.com)
Referer是HTTP请求头的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,它包含了当前请求资源的来源页面的地址。服务端一般使用Referer请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
怎么解决
不要发送Referer,对方服务端就不知道你从哪里来的了,姑且认为你是自己人吧。
如何设置不发送Referer?
用<a>、<area>、<img>、<iframe>、<script> 或者 <link> 元素上的 referrerpolicy 属性为其设置独立的请求策略,例如:
<img src="http://……" referrerPolicy="no-referrer">
或者直接在Html页面头中通过meta属性全局配置:
<meta name="referrer" content="no-referrer" />
index.html
![](https://i-blog.csdnimg.cn/blog_migrate/312e933f63581f9d7b70cbe74ac638ef.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ab7ec7d6a099f674eb53dee1ee0d3675.png)
处理相对时间
推荐第三方库:
1、打开Day.js 官网
2、点击开始使用
![](https://i-blog.csdnimg.cn/blog_migrate/4665e0575080184f27fb16c2392bebfd.png)
3、安装day.js并修改main.js代码
![](https://i-blog.csdnimg.cn/blog_migrate/a1147fae82e867472a25e8f78389804c.png)
4、使用
![](https://i-blog.csdnimg.cn/blog_migrate/48aa2d2949970281f606653f61ac6ffb.png)
八、首页--频道编辑
8.1处理页面弹出层
https://vant-ui.github.io/vant/#/zh-CN/popup
v-model:show 是否显示弹出层
position 弹出位置,可选值为 top bottom right left
closeable 是否显示关闭图标
close-icon-position
关闭图标位置,可选值为 top-left、bottom-left、 bottom-right
弹出层默认挂载到组件标签所在位置,可以通过 teleport 属性指定挂载位置。
在src/views/home.vue中
<van-popup
v-model:show="isChannelEditShow"
position="bottom"
closeable
close-icon-position="top-left"
:style="{ height: '100%' }"
teleport="body"
z-index="9999"
class="channel-edit-popup"
/>
const isChannelEditShow = ref(false); //控制弹出层是否展示
8.2创建频道编辑组件
1、创建src/components/home/article-list.vue
<!-- 视图层: html -->
<template>
<div class="channel-edit">频道编辑</div>
</template>
<!-- 逻辑层:js -->
<script setup>
</script>
<style lang="less" scoped>
</style>
2、在home.vue中加载注册
import ChannelEdit from "~/components/home/channel-edit.vue";
3、在弹出层中使用频道编辑组件
<!-- 底部弹出 -->
<van-popup
v-model:show="isChannelEditShow"
position="bottom"
closeable
close-icon-position="top-left"
:style="{ height: '100%' }"
teleport="body"
z-index="9999"
class="channel-edit-popup"
>
<ChannelEdit></ChannelEdit>
</van-popup>
8.3页面布局
<!-- 视图层: html -->
<template>
<div class="channel-edit">
<van-cell center :border="false">
<template #title>
<div class="title">我的频道</div>
</template>
<van-button type="danger" plain size="mini">编辑</van-button>
</van-cell>
<van-grid :gutter="10">
<van-grid-item v-for="value in 8" :key="value" text="文字" />
</van-grid>
<van-cell center :border="false">
<template #title>
<div class="title">频道推荐</div>
</template>
</van-cell>
<van-grid :gutter="10">
<van-grid-item
class="grid-item"
v-for="value in 8"
:key="value"
text="文字"
/>
</van-grid>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
</script>
<style lang="less" scoped>
.channel-edit {
padding-top: 94px;
.title {
font-size: 30px;
color: #333;
}
/deep/ .van-grid-item__content {
background-color: #f4f5f6;
.van-grid-item__text {
font-size: 30px;
color: #222;
}
}
}
</style>
8.4展示我的频道
1、在父组件中把channels传递给频道编辑组件
![](https://i-blog.csdnimg.cn/blog_migrate/90da2869c95af58d610fd274ad14f994.png)
2、在频道编辑组件中声明接收父组件的userChannel频道列表数据并遍历展示
![](https://i-blog.csdnimg.cn/blog_migrate/a9e6b093b978f348c6aca14682981dc5.png)
![](https://i-blog.csdnimg.cn/blog_migrate/cb328b38a6bb408ec7c094b632400238.png)
8.5展示推荐频道列表
没有用来获取推荐频道的数据接口,但是我们有获取所有频道列表的数据接口。
所以:所有频道列表-我的频道=剩余推荐频道
实现过程一共分为两大步:
获取所有频道
基于所以频道和我的频道计算获取剩余的推荐频道
获取所有频道
1、src/api/channel.js封装数据接口
import request from '~/utils/request'
//获取所有频道列表
export const getAllChannels= () =>{
return request({
method:'GET',
url:'/v1_0/channels',
})
}
2、在src/store/index.js中请求获取所以频道数据
import { getAllChannels } from '~/api/channel.js'
......
state: {
// 所有频道列表
allChannels:[]
},
mutations: {
// 设置所有频道列表
setAllChannels(state, allChannelsList) {
state.allChannels = allChannelsList
},
},
actions: {
getAllChnels({ commit }) {
return new Promise((resolve, reject) => {
// 获取所有频道列表
getAllChannels().then((res) => {
// 获取成功后,将数据存储在vuex 的setUserInfo对象中
commit("setAllChannels", res.data.data.channels)
console.log("所有频道列表信息",res.data.data.channels);
resolve(res)
}).catch((err) => {
// 失败回调
reject(err)
});
})
},
}
处理展示推荐频道
思路:所有频道 - 用户频道 = 推荐频道
1、封装计算属性筛选数据
遍历所有频道
对每一个频道都判断:该频道是否属于我的频道
如果不属于我的频道,则收集起来
直到遍历结束,剩下来就是推荐频道
// 计算属性会观测内部依赖数据的变化而变化
const recommendChannels = computed(() => {
// 所有频道列表-我的频道=剩余推荐频道
// filter方法:过滤数据,根据方法返回的布尔值true来收集数据
// filter方法查找满足条件的所有元素
return store.state.allChannels.filter((channel) => {
// 判断channel是否属于用户频道
// find方法查找满足条件的单个元素
return !store.state.channels.find((userChannel) => {
// 找到满足该条件的元素
return userChannel.id === channel.id;
});
});
});
2、模板绑定
<van-grid :gutter="10">
<van-grid-item
v-for="(channel, index) in recommendChannels"
:key="index"
class="grid-item"
:text="channel.name"
@click="onAdd(channel)"
/>
</van-grid>
8.6添加频道
思路:
给推荐频道列表中每一项注册点击事件
获取点击的频道项
将频道项添加到我的频道中
不需要删除,因为我们获取数据使用的是计算属性,当我的频道发生改变,计算属性重新求值了
1、给推荐频道中的频道注册点击事件
![](https://i-blog.csdnimg.cn/blog_migrate/0210d31b30ef1572b1a475d3e0a7b63d.png)
2、在添加频道事件处理函数中
![](https://i-blog.csdnimg.cn/blog_migrate/f031bbe7e5cc288592d43ab1dd4da5f1.png)
然后你会神奇的发现点击的那个推荐频道跑到我的频道中了,我们并没有去手动的删除点击的这个推荐频道,但是它没了!主要是因为推荐频道是通过一个计算属性获取的,计算属性中使用了 channels(我的频道)数据,所以只要我的频道中的数据发生变化,那么计算属性就会重新运算获取最新的数据。
8.7编辑频道
思路:
给我的频道中的频道项注册点击事件
在事件处理函数中
如果是编辑状态,则执行删除频道操作
如果是非编辑状态,则执行切换频道操作
处理编辑状态
1、添加数据用来控制编辑状态的显示
![](https://i-blog.csdnimg.cn/blog_migrate/24a2a1bcb7fe25c48aa3f2bb18cbdae7.png)
2、在我的频道项中添加删除图标
![](https://i-blog.csdnimg.cn/blog_migrate/218aa631ec6f9b244b401f79ce82579d.png)
3、处理点击编辑按钮
![](https://i-blog.csdnimg.cn/blog_migrate/09d06248f973b507844965f56b7aa7ee.png)
切换频道
功能需求:在非编辑器状态下切换频道。
1、给我的频道项注册点击事件
![](https://i-blog.csdnimg.cn/blog_migrate/65c57afb38c51532680956eb615b9d5c.png)
2、处理函数
![](https://i-blog.csdnimg.cn/blog_migrate/a8fdbe70ba149552835c3559fd5ac844.png)
3、在父组件中监听处理自定义事件
![](https://i-blog.csdnimg.cn/blog_migrate/cd1c91f578b536191eb9a18497fec568.png)
让激活活频道高亮
思路
将首页中的激活的标签索引传递给频道编辑组件
在频道编辑组件中遍历我的频道列表的时候判断遍历项的索引是否等于激活的频道标签索引,如果一样则作用一个高亮的css类名
1、将首页组件中的 active 传递到频道编辑组件中
![](https://i-blog.csdnimg.cn/blog_migrate/3de08c0239568a1d2b67bef7d0a49fe6.png)
2、在频道编辑组件中声明 props 接收
![](https://i-blog.csdnimg.cn/blog_migrate/46da1410858b8ead4b930b1f1a457d20.png)
3、判断遍历项,如果 遍历项索引 === active,则给这个频道项设置高亮样式
![](https://i-blog.csdnimg.cn/blog_migrate/38b5caad1ea019d14a1891fef9c46715.png)
删除频道
功能需求:在编辑状态下删除频道。
![](https://i-blog.csdnimg.cn/blog_migrate/f2e9a122d542c394d10bbe21b065bff0.png)
8.8频道数据持久化
业务分析
频道编辑这个功能,无论用户是否登录,用户都可以使用。
不登录也能使用
数据存储在本地
不支持同步功能
登录也能使用
数据存储在线上后台服务器
更换不同的设备可以同步数据
添加频道
1、封装添加频道的请求方法
//设置用户的频道(部分覆盖)
export const addUserChannels= (data) =>{
return request({
method:'PATCH',
url:'/v1_0/user/channels',
data
})
}
2、修改添加频道的处理逻辑
async function onAdd(channel) {
try {
// console.log(channel);
store.state.channels.push(channel);
// 数据持久化
if (store.state.user) {
// 登录了,数据存储到线上
await addUserChannels({
channels: [
{
id: channel.id,
seq: store.state.channels.length,
},
],
});
} else {
// 没有登录,数据存储到本地
setItem("user-channels", store.state.channels);
}
} catch (err) {
console.log(err);
showToast("添加频道失败");
}
}
删除频道
思路:
如果未登录,则存储到本地
如果已登录,则存储到线上
找到数据接口
封装请求方法
请求调用
1、封装删除用户频道请求方法
//删除指定用户频道
export const deleteUserChannel= (channelId) =>{
return request({
method:'DELETE',
url:`/v1_0/user/channels/${channelId}`,
})
}
2、修改删除频道的处理逻辑
async function deleteChannel(channel, index) {
console.log("删除频道");
// 如果删除的是当前激活频道之前的频道
if (index <= props.active) {
// 更新激活频道的索引
emit("update-active", props.active - 1);
}
store.state.channels.splice(index, 1);
// 数据持久化
if (store.state.user) {
// 登录了,数据存储到线上
await deleteUserChannel(channel.id);
} else {
// 没有登录,数据存储到本地
setItem("user-channels", store.state.channels);
}
}
8.9正确的获取首页频道列表数据
![](https://i-blog.csdnimg.cn/blog_migrate/930b04445a50ec6c53dd3a3af4d4bf34.png)
获取登录用户的频道列表和获取默认推荐的频道列表是同一个数据接口。后端会根据接口中的token来判断返回数据。
![](https://i-blog.csdnimg.cn/blog_migrate/400e2ad29530772f2e2c15bd69b70700.png)