【前端Vue】社交信息头条项目完整笔记第3篇:三、个人中心,TabBar 处理【附代码文档】

社交媒体-信息头条项目完整开发笔记完整教程(附代码资料)主要内容讲述:一、项目初始化使用 Vue CLI 创建项目,加入 Git 版本管理,调整初始目录结构,导入图标素材,引入 Vant 组件库,移动端 REM 适配。二、登录注册准备,实现基本登录功能,登录状态提示,表单验证,验证码处理,处理用户 Token。三、个人中心,四、首页—文章列表TabBar 处理,页面布局,处理已登录和未登录的页面展示,用户退出,展示登录用户信息,优化设置 Token。五、首页—频道编辑处理页面弹出层,创建频道编辑组件,页面布局,展示我的频道,展示推荐频道列表,添加频道。六、文章搜索创建组件并配置路由,页面布局,处理页面显示状态,搜索联想建议,搜索结果,搜索历史记录。七、文章详情创建组件并配置路由,页面布局,关于后端返回数据中的大数字问题,展示文章详情,处理内容加载状态,关于文章正文的样式。八、文章评论展示文章评论列表,评论点赞,发布文章评论,评论回复。九、用户页面创建组件并配置路由,页面布局,展示用户信息,用户关注,展示用户文章列表。十二、编辑用户资料创建组件并配置路由,页面布局,展示用户信息,修改昵称,修改性别,修改生日。

全套笔记资料代码移步: 前往gitee仓库查看

感兴趣的小伙伴可以自取哦,欢迎大家点赞转发~


全套教程部分目录:


部分文件图片:

三、个人中心

TabBar 处理

通过分析页面,我们可以看到,首页、问答、视频、我的 都使用的是同一个底部标签栏,我们没必要在每个页面中都写一个,所以为了通用方便,我们可以使用 Vue Router 的嵌套路由来处理。

  • 父路由:一个空页面,包含一个 tabbar,中间留子路由出口
  • 子路由
  • 首页
  • 问答
  • 视频
  • 我的

一、创建 tabbar 组件并配置路由

image-20200109153050432

这里主要使用到的 Vant 组件:

  • [Tabbar 标签栏](

1、创建 src/views/layout/index.vue

<template>
  <div class="layout-container">
    <!-- 子路由出口 -->
    <router-view />
    <!-- /子路由出口 -->

    <!-- 标签导航栏 -->
    <!--
      route: 开启路由模式
     -->
    <van-tabbar class="layout-tabbar" route>
      <van-tabbar-item to="/">
        <i slot="icon" class="toutiao toutiao-shouye"></i>
        <span class="text">首页</span>
      </van-tabbar-item>
      <van-tabbar-item to="/qa">
        <i slot="icon" class="toutiao toutiao-wenda"></i>
        <span class="text">问答</span>
      </van-tabbar-item>
      <van-tabbar-item to="/video">
        <i slot="icon" class="toutiao toutiao-shipin"></i>
        <span class="text">视频</span>
      </van-tabbar-item>
      <van-tabbar-item to="/my">
        <i slot="icon" class="toutiao toutiao-wode"></i>
        <span class="text">我的</span>
      </van-tabbar-item>
    </van-tabbar>
    <!-- /标签导航栏 -->
  </div>
</template>

<script>
export default {
  name: 'LayoutIndex',
  components: {},
  props: {},
  data () {
    return {
    }
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.layout-container {
  .layout-tabbar {
    i.toutiao {
      font-size: 40px;
    }
    span.text {
      font-size: 20px;
    }
  }
}
</style>

2、然后将 layout 组件配置到一级路由

{
  path: '/',
  component: () => import('@/views/layout')
}

访问 / 测试。

二、分别创建首页、问答、视频、我的页面组件

首页组件:

<template>
  <div class="home-container">首页</div>
</template>

<script>
export default {
  name: 'HomePage',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped></style>

问答组件:

<template>
  <div class="qa-container">问答</div>
</template>

<script>
export default {
  name: 'QaPage',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped></style>

视频组件:

<template>
  <div class="video-container">首页</div>
</template>

<script>
export default {
  name: 'VideoPage',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped></style>

我的组件:

<template>
  <div class="my-container">首页</div>
</template>

<script>
export default {
  name: 'MyPage',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped></style>

二、将四个主页面配置为 tab-bar 的子路由

{
  path: '/',
  name: 'tab-bar',
  component: () => import('@/views/tab-bar'),
  children: [
    {
      path: '', // 默认子路由
      name: 'home',
      component: () => import('@/views/home')
    },
    {
      path: 'qa',
      name: 'qa',
      component: () => import('@/views/qa')
    },
    {
      path: 'video',
      name: 'video',
      component: () => import('@/views/video')
    },
    {
      path: 'my',
      name: 'my',
      component: () => import('@/views/my')
    }
  ]
}

最后测试。

页面布局

未登录头部状态

<template>
  <div class="my-container">
    <div class="header">
      <img
        class="mobile-img"
        src="~@/assets/mobile.png"
        @click="$router.push('/login')"
      >
    </div>
    <div class="grid-nav"></div>
    <van-cell title="消息通知" is-link url="" />
    <van-cell title="实名认证" is-link url="" />
    <van-cell title="用户反馈" is-link url="" />
    <van-cell title="小智同学" is-link url="" />
    <van-cell title="系统设置" is-link url="" />
  </div>
</template>

<script>
export default {
  name: 'MyIndex',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<style scoped lang="less">
.my-container {
  > .header {
    height: 361px;
    background: url("~@/assets/banner.png") no-repeat;
    background-size: cover;
    display: flex;
    justify-content: center;
    align-items: center;
    .mobile-img {
      width: 132px;
      height: 132px;
    }
  }
}
</style>

已登录头部

宫格导航

单元格导航

处理已登录和未登录的页面展示

  • 未登录,展示登录按钮
  • 已登录,展示登录用户信息
<!-- 已登录:用户信息 -->
<div v-if="$store.state.user" class="user-info-wrap">
  ...
</div>
<!-- /已登录:用户信息 -->

<!-- 未登录 -->
<div v-else class="not-login" @click="$router.push('/login')">
  ...
</div>
<!-- /未登录 -->

<!-- 退出 -->
<van-cell-group v-if="$store.state.user">
  ...
</van-cell-group>
<!-- /退出 -->

用户退出

用户退出

1、给退出按钮注册点击事件

2、退出处理

onLogout () {
  // 退出提示
  // 在组件中需要使用 this.$dialog 来调用弹框组件
  this.$dialog.confirm({
    title: '确认退出吗?'
  }).then(() => {
    // on confirm
    // 确认退出:清除登录状态(容器中的 user + 本地存储中的 user)
    this.$store.commit('setUser', null)
  }).catch(() => {
    // on cancel
    console.log('取消执行这里')
  })
}

最后测试。

展示登录用户信息

image-20200109133717775

步骤:

  • 封装接口
  • 请求获取数据
  • 模板绑定

1、在 api/user.js 中添加封装数据接口

/**
 * 获取用户自己的信息
 */
export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/app/v1_0/user',
    // 发送请求头数据
    headers: {
      // 注意:该接口需要授权才能访问
      //       token的数据格式:Bearer token数据,注意 Bearer 后面有个空格
      Authorization: `Bearer ${store.state.user.token}`
    }
  })
}

2、在 views/my/index.vue 请求加载数据

+ import { getUserInfo } from '@/api/user'

export default {
  name: 'MyPage',
  components: {},
  props: {},
  data () {
    return {
+      userInfo: {} // 用户信息
    }
  },
  computed: {},
  watch: {},
+++  created () {
    // 初始化的时候,如果用户登录了,我才请求获取当前登录用户的信息
    if (this.$store.state.user) {
      this.loadUser()
    }
  },
  mounted () {},
  methods: {
+++    async loadUser () {
      try {
        const { data } = await getUserInfo()
        this.user = data.data
      } catch (err) {
        console.log(err)
        this.$toast('获取数据失败')
      }
    }
  }
}

3、模板绑定

优化设置 Token

项目中的接口除了登录之外大多数都需要提供 token 才有访问权限。

通过接口文档可以看到,后端接口要求我们将 token 放到请求头 Header 中并以下面的格式发送。

image-20200301214857543

字段名称:Authorization

字段值:Bearer token,注意 Bearertoken 之间有一个空格

方式一:在每次请求的时候手动添加(麻烦)。

axios({
  method: "",
  url: "",
  headers: {
    Authorization: "Bearer token"
  }
})

方式二:使用请求拦截器统一添加(推荐,更方便)。

sequenceDiagram
    participant A as 发起请求
    participant B as 请求拦截器
    participant C as 服务端
  A-->>B: 
  Note right of B: 设置 token
  B->>C: 请求发出

src/utils/request.js 中添加拦截器统一设置 token:

/**
 * 请求模块
 */
import axios from 'axios'
import store from '@/store'

const request = axios.create({
  baseURL: ' // 接口的基准路径
})

// 请求拦截器
// Add a request interceptor
request.interceptors.request.use(function (config) {
  // Do something before request is sent
  // config :本次请求的配置对象
  // config 里面有一个属性:headers
  const { user } = store.state
  if (user && user.token) {
    config.headers.Authorization = `Bearer ${user.token}`
  }
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})

// 响应拦截器

export default request

四、首页—文章列表

页面布局

头部导航栏

1、使用导航栏组件

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

  • 文本
  • 图标

3、样式调整

  • 宽高
  • 背景色
  • 边框
  • 文本大小
  • 图标大小
<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>

<script>
export default {
  name: 'HomeIndex',
  components: {},
  props: {},
  data () {
    return {}
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {}
}
</script>

<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>

频道列表

image-20200305195458197

使用 Tab 标签页组件

参考:[Tab 标签页组件](

样式调整

(1)基础样式调整

  • 标签项
  • 右边框
  • 下边框
  • 宽高
  • 文字大小
  • 文字颜色
  • 底部条
  • 宽高
  • 颜色
  • 位置

(2)处理汉堡按钮

1、使用插槽插入内容

2、样式调整

  • 定位
  • 内容居中
  • 宽高
  • 背景色、透明度
  • 字体图标大小

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

  • 定位
  • 宽高
  • 背景图
  • 背景图填充模式

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

image-20200402215736215

CSS 样式:

.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;
  }
}

展示频道列表

思路:

  1. 找数据接口
  2. 把接口封装为请求方法
  3. 在组件中请求获取数据
  4. 模板绑定

1、封装数据请求接口

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

2、请求获取数据

image-20200401004107079

3、模板绑定

image-20200401004138232

文章列表

image-20200311221143472

思路分析

你的思路可能是这样的:

1、找到数据接口

2、封装请求方法

3、在组件中请求获取数据,将数据存储到 data 中

4、模板绑定展示

根据不同的频道加载不同的文章列表,你的思路可能是这样的:

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

image-20200111124415227

思路没有问题,但是并不是我们想要的效果。

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

实现思路也非常简单,就是我们准备多个 list 数组,每个频道对应一个,查看哪个频道就把数据往哪个频道的列表数组中存放,这样的话就不会导致覆盖问题

image-20200111124530323

可是有多少频道就得有多少频道文章数组,我们都一个一个声明的话会非常麻烦,所以这里的建议是利用组件来处理。

具体做法就是:

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

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

image-20200306155328108

在文章列表中请求获取对应的列表数据,展示到列表中。

最后把组件在频道列表中遍历出来,就像下面这样。

image-20200306154805522

1、创建 src/views/home/components/article-list.vue

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

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

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

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

image-20200305205441485

3、最后测试。

答疑:

  • 为什么标签内容是懒渲染的?
  • 因为这是 Tab 标签页组件本身支持的默认功能,如果不需要可以通过配置 :lazy-render="false" 来关闭这个效果。

使用 List 列表组件

[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">
    <!--
      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 事件
     -->
    <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',
  components: {},
  props: {
    channel: {
      type: Object,
      required: true
    }
  },
  data () {
    return {
      list: [], // 存储列表数据的数组
      loading: false, // 控制加载中 loading 状态
      finished: false // 控制数据加载结束的状态
    }
  },
  computed: {},
  watch: {},
  created () {},
  mounted () {},
  methods: {
    // 初始化或滚动到底部的时候会触发调用 onLoad
    onLoad () {
      console.log('onLoad')
      // 1. 请求获取数据
      // setTimeout 仅做示例,真实场景中一般为 ajax 请求
      setTimeout(() => {
        // 2. 把请求结果数据放到 list 数组中
        for (let i = 0; i < 10; i++) {
          // 0 + 1 = 1
          // 1 + 1 = 2
          // 2 + 1 = 3
          this.list.push(this.list.length + 1)
        }

        // 3. 本次数据加载结束之后要把加载状态设置为结束
        //     loading 关闭以后才能触发下一次的加载更多
        this.loading = false

        // 4. 判断数据是否全部加载完成
        if (this.list.length >= 40) {
          // 如果没有数据了,把 finished 设置为 true,之后不再触发加载更多
          this.finished = true
        }
      }, 1000)
    }
  }
}
</script>

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

让列表固定定位

.article-list {
  position: fixed;
  top: 180px;
  bottom: 100px;
  right: 0;
  left: 0;
    overflow-y: auto;
}

加载文章列表数据

实现思路:

  • 找到数据接口
  • 封装请求方法
  • 请求获取数据
  • 模板绑定

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

/**
 * 文章接口模块
 */
import request from '@/utils/request'

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

注意:使用接口文档中最下面的 频道新闻推荐_V1.1

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

<template>
  <div class="article-list">
      <!--
        loading 控制上拉加载更多的 loading 状态
        finished 控制数据是否加载结束
        load 事件:当触发上拉加载更多的时候会触发调用 load 事件

        List 初始化后会触发一次 load 事件,用于加载第一屏的数据
        如果一次请求加载的数据条数较少,导致列表内容无法铺满当前屏幕,List 会继续触发 load 事件,直到内容铺满屏幕或数据全部加载完成
      -->
      <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>

最后测试。

下拉刷新

展示文章列表-下拉刷新

这里主要使用到 Vant 中的 [PullRefresh 下拉刷新]( 组件。

思路:

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

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

// 当触发下拉刷新的时候调用该函数
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('刷新失败')
  }
}

文章列表项

准备组件

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

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

<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、在文章列表组件中注册使用文章列表项组件

image-20200404224728524

展示列表项内容

  • 使用 Cell 单元格组件
  • 展示标题
  • 展示底部信息
<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>

<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>

样式调整

  • 文章标题
  • 字号
  • 颜色
  • 多行文字省略
  • 单图封面
  • 封面容器
    • 去除 flex: 1,固定宽高
    • 左内边距
  • 封面图
    • 宽高
    • 填充模式: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>

关于第三方图片资源403问题

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

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

第三方平台怎么处理图片资源保护的?

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

image-20200405134347749

Referer 是什么东西?

扩展参考:

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

需要注意的是 referer 实际上是 "referrer" 误拼写。参见 [HTTP referer on Wikipedia]( (HTTP referer 在维基百科上的条目)来获取更详细的信息。

怎么解决?

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

如何设置不发送 referrer?

<a><area><img><iframe><script> 或者 <link> 元素上的 referrerpolicy 属性为其设置独立的请求策略,例如:

<img src=" referrerPolicy="no-referrer">

或者直接在 HTMl 页面头中通过 meta 属性全局配置:

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

处理相对时间

推荐两个第三方库:

  • [Moment.js](
  • [Day.js](

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

[Day.js]( 是一个轻量的处理时间和日期的 JavaScript 库,和 [Moment.js]( 的 API 设计保持完全一样,如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js 。

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

  • 🕒 和 Moment.js 相同的 API 和用法

  • 💪 不可变数据 (Immutable)
  • 🔥 支持链式操作 (Chainable)
  • 🌐 国际化 I18n
  • 📦 仅 2kb 大小的微型库
  • 👫 全浏览器兼容

1、安装

npm i dayjs

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>

未完待续, 同学们请等待下一期

全套笔记资料代码移步: 前往gitee仓库查看

感兴趣的小伙伴可以自取哦,欢迎大家点赞转发~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/513927.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ObjectiveC-08-OOP面向对象程序设计-类的分离与组合

本节用一简短的文章来说下是ObjectiveC中的类。类其实是OOP中的一个概念&#xff0c;概念上简单来讲类是它是一组关系密切属性的集合&#xff0c;所谓的关系就是对现实事物的抽象。 上面提到的关系包括很多种&#xff0c;比如has a&#xff0c; is a&#xff0c;has some等&…

交互设计师、UI设计师、视觉设计师面试作品集包装模板figma源文件

页面数量&#xff1a;19页 页面尺寸&#xff1a;1920*1080PX 交付格式&#xff1a;figma 赠送文件&#xff1a;24款高质量样机 交付文件&#xff1a;作品集模板源文件、作品集包装psd源文件、作品集所用字体文件 该作品集虽然只有19页&#xff0c;但可根据需求复制作品集里已有…

offsetof宏定义的使用和模拟实现详解

目录 一.offsetof宏定义的使用二.offsetof宏的模拟实现三.总结 一.offsetof宏定义的使用 offsetof的第一个参数是数据类型(结构体或联合体)&#xff0c;第二个参数是偏移量 &#xff08;offsetof用于计算结构体和联合体的偏移量&#xff09; 返回值的类型是size_t&#xff08;无…

Spring Cloud微服务入门(二)

微服务的技术栈 服务治理&#xff1a; 服务注册、发现、调用。 负载均衡&#xff1a; 高可用、集群部署。 容错&#xff1a; 避免雪崩、削峰、服务降级。 消息总线&#xff1a; 消息队列、异步通信&#xff0c;数据一致性。 网关&#xff1a; 校验路径、请求转发、服务集成…

Golang 开发实战day07 - Functions

Golang 教程07 - Functions 1. Functions 1.1 什么是函数&#xff1f; 在 Golang 中&#xff0c;函数就像是代码的超级组合体&#xff0c;可以将一段代码封装成一个独立的单元&#xff0c;以便重复使用。 1.2 函数声明 func funcName(parameter1 type1, parameter2 type2)…

利用sqoop实现sql表数据导入到Hadoop

1.在开发这创建好sql表后&#xff0c;开始执行下面步骤 2.sqoop的安装路径&#xff0c;我这里放在以下位置 3. 进入到option2脚本中&#xff0c;下面是脚本里的内容 下面四点要根据情况随时更改&#xff1a; 1>jdbc:mysql://node00:3306/数据库名 2>sid,sname->前…

网站如何运用百度文心一言API进行AI内容创作?

网站如何运用百度文心一言API进行AI内容创作&#xff1f; 当我们做好一个网站的时候会因为创作内容而发愁&#xff0c;随着chatgpt的出现&#xff0c;内容创作已经不再是什么困难的事情&#xff0c;但是由于gpt是国外的&#xff0c;在国内使用有诸多不便&#xff0c;因此我们今…

数据结构—堆

什么是堆 堆是一种特殊的树形结构&#xff0c;其中每个节点都有一个值。堆可以分为两种类型&#xff1a;最大堆和最小堆。在最大堆中&#xff0c;每个节点的值都大于等于其子节点的值&#xff1b;而在最小堆中&#xff0c;每个节点的值都小于等于其子节点的值。这种特性使得堆…

基于FPGA的SPI_FLASH程序设计

SPI_FLASH简介 spi_flash是一种通用存储器&#xff0c;也称为SPI NOR Flash或SPI Flash。它使用SPI&#xff08;Serial Peripheral Interface&#xff09;接口进行通信&#xff0c;可以通过串行方式读写数据。spi_flash的特点是工作电压低&#xff0c;体积小&#xff0c;读写速…

使用SpringBoot实现的登录注册后端功能

1、系统演示视频&#xff08;演示视频&#xff09; 2、需要交流和学习请联系

高级IO/多路转接-select/poll(1)

概念背景 IO的本质就是输入输出 刚开始学网络的时候&#xff0c;我们简单的写过一些网络服务&#xff0c;其中用到了read&#xff0c;write这样的接口&#xff0c;当时我们用的就是基础IO&#xff0c;高级IO主要就是效率问题。 我们在应用层调用read&&write的时候&…

elementui 实现一个固定位置的Pagination(分页)组件

系列文章目录 一、elementui 导航菜单栏和Breadcrumb 面包屑关联 二、elementui 左侧导航菜单栏与main区域联动 三、elementui 中设置图片的高度并支持PC和手机自适应 四、 elementui 实现一个固定位置的Pagination&#xff08;分页&#xff09;组件 文章目录 系列文章目录…

【css】使用display:inline-block后,元素间存在4px的间隔

问题&#xff1a;在本地项目中使用【display: inline-block】&#xff0c;元素间存在4px间隔。打包后发布到外网又不存在这个问题了。 归根结底这是一个西文排版的问题&#xff0c;英文有空格作为词分界&#xff0c;而中文则没有。 此时的元素具有文本属性&#xff0c;只要标签…

CNAS软件测试公司有什么好处?如何选择靠谱的软件测试公司?

CNAS认可是中国合格评定国家认可委员会的英文缩写&#xff0c;由国家认证认可监督管理委员会批准设立并授权的国家认可机构&#xff0c;统一负责对认证机构、实验室和检验机构等相关机构的认可工作。 在软件测试行业&#xff0c;CNAS认可具有重要意义。它标志着一个软件测试公…

uniapp android 原生插件封装--入门篇

本文将介绍uniapp 插件封装官方例子的搭建&#xff0c;运行。和注意事项&#xff0c;避坑 1、文档地址&#xff1a;简介 | uni小程序SDK 开发者须知 | uni小程序SDK Android平台第三方插件开发指导 - DCloud问答 这个介绍插件与原生的架构 2、下载官方示例&#xff1a;百度…

JAX深度学习库入门

JAX简介 https://www.bilibili.com/video/BV1Sb4y1b7rK/?spm_id_from333.999.0.0&vd_sourceb2549fdee562c700f2b1f3f49065201b JAX is NumPy wiht Autograd , XLA and Composable (function) transformations, brought together for high-performance machine learning …

hive词频统计---文件始终上传不来

目录 准备工作&#xff1a; 文件内容&#xff1a; 创建数据库及表 将文件上传到&#xff1a;上传到/user/hive/warehouse/db1.db/t_word目录下 hive里面查询&#xff0c;始终报错&#xff1a;&#xff08;直接查询也是不行&#xff09; 解决方案&#xff1a; 准备工作&am…

【Django学习笔记(三)】BootStrap介绍

BootStrap介绍 前言正文1、BootStrap 快速了解2、初识BootStrap2.1 下载地址2.2 创建目录2.3 引入BootStrap2.4 使用BootStrap 3、BootStrap 组件&样式3.1 导航条3.2 栅格系统3.3 container3.3.1 container3.3.2 container-fluid 3.4 面板3.5 媒体对象3.6 分页3.7 图标3.7.…

【协议篇:Http与Https】

1. Http 1.1 Http的定义 超文本传输协议&#xff08;Hypertext Transfer Protocol&#xff0c;HTTP&#xff09;是用于分布式、协作式和超媒体信息系统的应用层协议。它是互联网上最广泛应用的数据通信协议之一&#xff0c;尤其对于万维网&#xff08;WWW&#xff09;服务而言…

SQLite下一代查询规划器(十)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLite 查询优化器概述&#xff08;九&#xff09; 下一篇&#xff1a;SQLite—系列文章目录 1. 引言 “查询规划器”的任务是弄清楚 找出完成 SQL 语句的最佳算法或“查询计划”。 从 SQLite 版本 3.8.0 &am…