防今日头条实现过程

512 阅读10分钟

防今日头条实现过程

image.png

移动端项目

一、项目搭建

1、使用脚手架项目搭建

vue create hm-project

2、下载其他依赖

npm i vant@2
npm i less less-loader@5.0.0 -D
npm i axios

3、配置vant组件库按需引入

3-1、下载依赖包

npm i babel-plugin-import -D

3-2、配置自动按需引入( babel.config.js中进行配置 )

module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};

4、删除脚手架项目的欢迎界面等项目无关的文件

1、assets/所有文件
2、components/所有文件
3、views/所有文件
4App.vue中的代码(注意:需保留template标签,以及template里面根标签)

5、关闭eslint语法检测

在项目根目录下创建vue.config.js文件

module.exports = {
	lintOnSave: false
}

**注意:**配置完成后,需要重启服务器,也就是需要重新执行npm run serve我们的配置才生效。

6、全局引入重置样式文件(reset.less)

reset.less文件在预习资料中,第一天文件资料中

main.js

import "./style/reset.less"

二、登录页面

1、创建login.vue组件

创建目录:src/views/login.vue

2、配置登录组件的路由

​ 2-1、下载路由

npm i vue-router@3

​ 2-2、配置路由

单独创建一个文件进行配置

  • 创建src/router/index.js文件(目的:方便后期维护)

  • 在index.js中进行如下配置

    // 配置路由 
    import Vue from "vue"
    // 1、引入路由对象
    import VueRouter from "vue-router"
    // 2、使用Vue.use注册相关组件等
    Vue.use(VueRouter)
    // 3、创建路由规则
    const routes = [
        {
            path: "/login",
            // @是src这个目录的别名
            component: ()=>import("@/views/login.vue") // 按需引入
        }
    ]
    // 4、使用路由规则生成路由对象
    const router = new VueRouter({
        // routes: routes, 简写如下
        routes
    })
    // 5、导出路由对象
    export default router
    
  • 在main.js中引入,并注入到new Vue实例中

    import router from "./router"
    
    // 6、将路由对象注入到new vue实例中
    new Vue({
      router,
      render: h => h(App),
    }).$mount('#app')
    
  • 在App.vue中设置路由页面的挂载点

    <template>
      <div>
        <!-- 设置挂载点 -->
        <router-view></router-view>
      </div>
    </template>
    
    

3、布局

  • 在App.vue中引入在线字体图标库

    <style>
    @import "http://at.alicdn.com/t/font_1426139_h6vn3jbl5q.css";
    </style>
    
  • 按钮效果的实现,直接使用vant组件库中的按钮组件

    vant-contrib.gitee.io/vant/#/zh-C…

    <van-button icon="star-o" round type="danger" block>登录按钮</van-button>
    
  • 文本框:使用的时候vant组件库中的输入框

4、实现登录功能

4-1、封装登录接口的请求函数

​ 4-1-1、封装请求地址的基础路径,src/utils/request.js

// 封装请求基地址
import axios from "axios";

// axios.defaults.baseURL = "http://localhost:3000" 	// 本地服务器
axios.defaults.baseURL = "http://157.122.54.189:6002" 	// 远程服务器

export default axios

​ 4-1-2、封装请求函数,src/api/user.js

// 封装用户相关的接口
import axios from "../utils/request"
// 登录接口 post /login
// post请求必须使用data传参(除非后台特殊说明),get请求必须使用params传参(除非后台特殊说明)
export function login(data){
    return axios({
        url: "/login",
        method: "post",
        data
    })
}

4-2、给登录添加点击事件,实现登录功能

​ 1、引入自己封装的登录接口的请求函数

import { login } from "@/api/user"

​ 2、给登录按钮添加点击事件

<van-button icon="star-o" 
        round type="danger" block @click="loginFn">登录按钮</van-button>

​ 3、在事件处理函数中通过引入的请求函数进行登录。

        // 登录函数
        loginFn(){
            console.log(this.user);
            login(this.user).then(res=>{
                console.log(51,"请求成功",res);
                if(res.data.message === "用户不存在"){
                    this.$toast.fail(res.data.message)
                }else{
                    // 登录成功,跳转到首页

                }
            })
        }

三、注册页面

1、创建register.vue注册组件

2、配置路由

3、布局

​ 直接复用登录页面的结构和样式

​ 文本框:使用的时候vant组件库中的输入框

​ 注册按钮:使用的时候vant组件库中的按钮

4、实现注册功能

​ 4-1、封装注册接口的函数

​ 4-2、收集用户输入的内容,通过v-model绑定输入框

​ 4-3、引入封装的接口函数

​ 4-4、给注册按钮添加点击事件

​ 4-5、在事件处理函数中调用接口函数,进行注册。

四、个人中心页面

1、创建个人中心组件personal.vue

2、配置路由

3、布局

​ 头部:直接使用预习资料中的结构和样式

​ 单元格:使用vant组件库的单元格(vant-contrib.gitee.io/vant/#/zh-C…

​ 按钮:使用vant组件库的按钮(vant-contrib.gitee.io/vant/#/zh-C…

4、功能实现

1、请求个人信息进行渲染

​ 1-1、封装用户详情请求函数

​ 分析:这个接口需要用到登录后的token值,因此我们在封装函数之前先在登录功能中把token保存起来,然后再封装请求函数

login.vue组件

 // 登录请求
         login(this.user).then(res=>{
                if(res.data.message === "用户不存在"){
                    this.$toast.fail(res.data.message)
                }else{
                    // 保存用户的token和id
                    localStorage.setItem("token-68", res.data.data.token);
                    localStorage.setItem("userid-68", res.data.data.user.id);
                    // 登录成功,跳转到首页
                }
            })
        }

user.js中进行封装,注意需要通过请求头进行token的传递(请求头指的是headers属性)

// 用户详情 /user/:id get   Authorization 
// 接口文档上的:id表示动态参数,真正传值的时候id前面不需要添加冒号
export const user = id=>axios({
	url: "user/"+id,
	method: "get",
	headers:{
		Authorization: localStorage.getItem("token-68")
	}
})

​ 1-2、在created钩子函数中请求并保存用户详情信息

​ 1-2-1、引入请求函数

​ 1-2-2、在created钩子函数中请求并保存用户详情接口返回的数据

​ 1-2-3、根据请求回来的数据进行渲染

​ 注意:头像需要拼接基础路径(这个路径就是在axios里面封装的基础路径,因此只需要引入过来使用即可)

​ 1-3、用保存的数据进行渲染

2、退出功能

#### 1、给退出按钮添加点击事件
<van-button round block color="#eb6112" type="default" @click="outgin">退出</van-button>

2、在点击事件里面移除token和用户id,然后跳转到登录页面

  methods: {
      // 退出
    outgin(){
      localStorage.removeItem("token-68");
      localStorage.removeItem("userid-68");
      // this.$router.push({
      //   path: "/login"
      // })
      // 不传参的时候可以使用如下写法
      this.$router.push("/login")
    }
  }

五、个人信息编辑页面

1、创建组件edit_profile.vue

2、配置路由

3、布局

​ 头部:使用vant组件库中的nav-bar组件以及van-icon组件(vant-contrib.gitee.io/vant/#/zh-C…

头像:自己完成布局

单元格:vant组件库中的单元格(vant-contrib.gitee.io/vant/#/zh-C…

4、实现功能

1、个人信息数据渲染

​ 1-1、直接在个人信息编辑页面引入并使用user接口函数,请求并保存数据。

​ 1-2、拿到数据进行渲染

2、修改个人信息

​ 2-1、修改头像

​ 2-1-1、封装修改个人信息的请求函数

​ 利用请求拦截器统一设置了token值

在utils/request.js中通过请求拦截器统一设置token值

// 添加请求拦截器
// 请求拦截,就是当我们发起ajax请求的时候,会触发(请求发送出去之前)的函数
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    // 添加token值,传递给后端
    console.log(11,config);
    let token = localStorage.getItem("token-68");
    if(token){
        config.headers.Authorization = token;
    }
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

在api/user.js中封装请求函数,注意此时已经不需要写token

// 上传文件接口 /upload post file
export const upload = data=>axios({
        url: "/upload",
        method: "post",
        data
})

// 修改个人信息接口 /user_update/:id post 
export const user_update = (id, data)=>axios({
        url: "/user_update/"+ id,
        method: "post",
        data
})

​ 2-1-2、点击头像弹出选择图片的弹窗(vant组件库中的Uploader 上传组件:vant-contrib.gitee.io/vant/#/zh-C…

​ 2-1-3、上传图片,得到图片的URL地址

​ 2-1-4、修改头像,把上一步得到的URL地址通过修改接口传递给后端

​ 2-2、修改昵称

​ 2-2-1、给昵称单元格添加点击事件,弹出一个弹窗(使用vant组件库的Dialog 弹出框)

​ 2-2-2、收集用户输入的昵称(v-model绑定)

​ 2-2-3、点击弹窗的确认按钮,进行ajax请求修改昵称

​ 点击弹窗的确认按钮会触发confirm事件,然后我们就能够在confirm事件处理函数中进行请求。

​ 2-2-4、修改昵称请求之前做判断(昵称是否为空)

        changeNickname(){
            // 判断用户输入昵称是否为空
            if(!this.nickname){
                this.$toast.fail("昵称不能为空!")
                return
            }
            user_update(localStorage.getItem("userid-68"),{
                nickname: this.nickname
            }).then(res=>{
                console.log(64,res);
                // 修改成功后,更新昵称
                this.userDetail.nickname = res.data.data.nickname;
            })
        },

​ 2-2-5、判断到用户输入的昵称为空的时候,阻止弹窗关闭。

​ 组件弹窗关闭,我们可以使用vant组件库中的beforeclose属性,属性的值是一个回调函数,函数中有2个参数,action和done

<van-dialog v-model="nicknameShow" :beforeClose="nicknameBeforeClose" @confirm="changeNickname" title="标题" show-cancel-button>
                .....
</van-cell-group>        
// 昵称为空的时候,阻止弹窗关闭
        // 弹窗关闭前会触发的函数
        nicknameBeforeClose(action, done){
            console.log(77,action);
            // done方法一定要调用,否则不会关闭弹窗
            if(!this.nickname&&action=="confirm"){
                // 传入false表示 组件关闭弹窗
                done(false)
            }else{
                done()
            }
        },

​ 2-3、修改密码

​ 2-3-1、点击修改密码单元格,弹窗dialog组件(vant-contrib.gitee.io/vant/#/zh-C…

​ 2-3-2、收集用户输入的密码(v-model)

​ 2-3-3、绑定确认按钮的事件,然后再事件处理函数中进行相关判断

​ 1、判断原密码输入是否正确,如果不正确组件修改密码

​ 2、判断原密码和新密码是否相同,如果相同阻止修改,或新密码为空也阻止修改

​ 3、如果判断都通过了,则进行修改密码请求

​ 2-3-4、判断不通过,阻止弹窗关闭

​ before-close属性

​ 2-4、修改性别

​ 2-4-1、点击修改性别单元格,弹出(ActionSheet 动作面板:vant-contrib.gitee.io/vant/#/zh-C…

​ 2-4-2、收集用户选择的性别

​ 2-4-3、绑定选择成功后的事件,然后再事件处理函数中进行修改性别的请求

<van-cell @click="showGenderFn" title="性别" is-link :value="user.gender==1?'男':'女'" />   
<!-- 
                actions: 属性是一个由对象构成的数组,就是动作面板中的选项集合
                select: 点击选项时触发的事件
             -->
            <van-action-sheet v-model="showGender" :actions="actions" @select="onSelect" />
<script>
export default {
    data(){
        return {
            showGender: false,          // 控制性别弹窗显示隐藏
            actions: [ { name: "男",gender: 1 },{ name: "女",gender: 0 } ], // 注意:name这个属性名不能修改

        }
    },
    methods: {
                // 修改性别
        // 选中选项时触发
        onSelect(item){
            console.log(item);
            let { id } = JSON.parse(localStorage.getItem("user-69"))
            user_update(id,{
                gender: item.gender
            }).then(res=>{
                console.log(71,"性别修改成功",res);
                this.showGender = false;
                // 更新页面性别
                if(res.data.message=="修改成功"){
                    this.$toast.success(res.data.message)
                    this.user.gender = item.gender;
                }else{
                    this.$toast.fail(res.data.message)
                }
            })
        },
        // 显示修改性别弹窗
        showGenderFn(){
            this.showGender = true;
        },
    }
}
</script>

六、首页

1、创建index.vue首页组件

2、配置路由

3、布局

​ 头部:vant组件库的导航组件nav-bar(vant-contrib.gitee.io/vant/#/zh-C…

​ 导航列表:使用vant组件库中的tab标签页组件(vant-contrib.gitee.io/vant/#/zh-C…

​ 新闻列表:3种结构,封装在一个组件中实现(components/newsItem.vue)

​ 1、左右结构,type==1,且cover数组的长度小于3

​ 2、上下结构,type==1,且cover数组的长度大于等于3

​ 3、上下结构(视频),type==2

​ 布局直接使用预习资料中的结构和样式

newsItem.vue组件结构如下

<template>
  <div>
      <!-- 1、左右结构 -->
    <div class="single">
      <div class="left">
        <p class="content"> 标题 </p>
        <p class="info">
          <span> 昵称 </span>
          <span>100跟帖</span>
        </p>
      </div>
      <img :src="../封面.jpg" alt />
    </div>
    <!-- 2、视频  -->
    <div class="single2">
      <p class="content">标题</p>
      <div class="playarea">
        <img src="../封面.jpg" alt />
        <div class="playicon">
          <van-icon name="play" />
        </div>
      </div>
      <p class="info">
        <span>昵称</span>
        <span>123 跟帖</span>
      </p>
    </div>
    <!-- 3、上下结构,显示3张图片的 -->
    <div class="single3">
      <p class="content">标题</p>
      <div class="imgs">
        <img
          :src="item"
          alt
          v-for="item in 3"
          :key="item"
        />
      </div>
      <p class="info">
        <span> 昵称 </span>
        <span>132 跟帖</span>
      </p>
    </div>
  </div>
</template>

<script>
export default {

};
</script>

<style lang="less" scoped>
.info {
  font-size: 12px;
  padding-left: 5px;
  color: #999;
  > span:nth-of-type(1) {
    padding-right: 15px;
  }
}
.content {
  font-size: 14px;
  padding: 0px 5px;
  line-height: 24px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
}

.single {
  padding: 15px 0px;
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  border-bottom: 1px solid #ccc;
  flex-wrap: wrap;
  .left {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    overflow: hidden;
  }

  img {
    width: (120/360) * 100vw;
    height: (70/360) * 100vw;
    object-fit: cover;
    padding-right: 5px;
  }
}
.single2 {
  border-bottom: 1px solid #ccc;
  padding: 8px 0;
  .playarea {
    width: 100%;
    position: relative;
    margin-bottom: 10px;
    img {
      width: 100%;
      display: block;
    }
    .playicon {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      background-color: rgba(0, 0, 0, 0.4);
      box-shadow: 0px 0px 15px #fff;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: flex;
      justify-content: center;
      .van-icon {
        font-size: 40px;
        color: #fff;
        line-height: 60px;
      }
    }
  }
}
.single3 {
  width: 100vw;
  .imgs {
    width: 100%;
    display: flex;
    padding: 10px 0 0 0;
    img {
      width: (120/360) * 100vw;
      height: (70/360) * 100vw;
      // 让图片自动的调整大小,根据父容器的大小自动调整
      object-fit: cover;
      padding-right: 5px;
    }
  }
}
</style>

4、实现功能

​ 4-1、栏目列表渲染

​ 1、封装栏目列表接口函数

​ 2、在created钩子函数中请求栏目列表数据并保存

​ 3、根据保存的数据进行栏目渲染

​ 布局的时候已经完成了

​ 4-2、新闻列表渲染

​ 1、封装新闻列表的接口函数

​ 2、在created钩子函数中请求新闻列表数据并保存

​ 3、根据保存的数据进行新闻列表渲染

​ 此时会报错入下图,为什么呢?

答:因为数据请求是异步的,不会阻塞DOM渲染,因此数据请求回来之前会先渲染一次,那么就会报错了。

如何解决?

答:可以让DOM渲染在数据请求回来之前再进行渲染,具体做法:“使用短路运算解决”,例如:

<img :src="baseUrl+(obj.cover&&obj.cover.length>0&&obj.cover[0].url)" />

4-3、根据用户是否登录,请求默认的新闻列表

​ 需求:无论是否登录,首页都应该展示“头条”内容

​ 1、登录情况下,有关注栏目,所以请求下标值为1的栏目数据(即为头条数据)

​ 2、未登录情况下,没有关注,所以请求下标值为0的栏目数据(即为头条数据)

    data(){
        return {
            categoryList: [ // 栏目列表
                ],
            newsArr: [], // 新闻列表
            // 根据用户是否登录判断“头条”栏目的下标值
            curIndex: localStorage.getItem("token-68")?1:0, // 当前栏的下标值
        }
    },
        .....
        .....
            // 根据是否登录,请求“头条”数据
            newsList({
                category: this.categoryList[this.curIndex].id
            }).then(res=>{
                console.log(67,res);
                this.newsArr = res.data.data;
            })

4-4、设置栏目的选中效果(默认选中头条)

​ 通过v-model属性绑定选中栏目的下标值即可

<van-tabs v-model="curIndex">
    ....
</van-tabs>

4-5、点击栏目,请求对应的数据

​ 1、在tabs标签上绑定change事件

<van-tabs v-model="curIndex" @change="categoryChange" ></van-tabs>

​ 2、在change事件中可以通过形参接收两个参数,第一个参数表示点击栏目的下标值,第二个参数是栏目标题,那么我点击栏目的时候,更新当前栏目(curIndex)的下标值即可.

    methods:{
        // 点击栏目(点击栏目会触发的函数),切换新闻列表
        categoryChange(name,title){
            // name属性就是栏目的下标值
            // title:栏目标题
            console.log(name,title);
            this.curIndex = name;
            // 获取点击栏目的新闻列表
            newsList({
                category: this.categoryList[this.curIndex].id
            }).then(res=>{
                console.log(67,res);
                this.newsArr = res.data.data;
            })
        }
    }

4-6、优化代码----把请求新闻列表的代码封装到一个函数中,需要用到这个函数的地方直接调用即可

        // 封装新闻列表请求
        getNews(){
            newsList({
                category: this.categoryList[this.curIndex].id
            }).then(res=>{
                console.log(67,res);
                this.newsArr = res.data.data;
            })
        },

4-7、下拉刷新页面数据功能

​ 1、使用vant组件库中的PullRefresh 下拉刷新组件完成(vant-contrib.gitee.io/vant/#/zh-C…

<van-pull-refresh v-model="isLoading" @refresh="onRefresh">
    <news v-for="item in newsArr" :key="item.id" :obj="item"></news>
</van-pull-refresh>
<script>
export default {
    data(){ return { isLoading: false } },
    methods:{
          // 封装新闻列表请求
        getNews(){
            newsList({
                category: this.categoryList[this.curIndex].id
            }).then(res=>{
                // console.log(67,res);
                // 请求成功后,需要将控制下拉属性的变量变为false
                this.isLoading = false;
                // 保存数据
                ...
            })
        },
         // 下拉刷新
        onRefresh(){
            // 重新请求数据
            this.getNews();
        },
    }
}
</script>

4-8、上拉加载更多数据功能

​ 1、直接使用vant组件库的list组件来实现上拉加载更多数据功能(vant-contrib.gitee.io/vant/#/zh-C…

​ 2、添加immediate-check属性(是否在初始化时立即执行滚动位置检查)

<van-list
          v-model="loading"
          :finished="finished"
          finished-text="没有更多了"
          @load="onLoad"
          :immediate-check="false"
          >
                            <news v-for="item in newsArr" :key="item.id" :obj="item"></news>
</van-list>

​ 3、改造了获取新闻列表的请求函数(添加了2个参数)

        // 封装新闻列表请求
        getNews(){
            newsList({
                category: this.categoryList[this.curIndex].id,
                pageIndex: this.pageIndex, // 表示请求的页数
                pageSize: this.pageSize	   // 表示每页请求的数据数量
            }).then(res=>{
                // 请求成功后,需要将控制下拉属性的变量变为false
                this.isLoading = false;
                // 保存数据
                this.newsArr = res.data.data;
            })
        },

​ 3、在load事件函数中实现数据请求

        // 上拉加载,默认会执行一次
        onLoad(){
            this.pageIndex++;
            this.getNews();
        },

4、改造getNews方法,修改上拉加载相关变量的值

        // 封装新闻列表请求
        getNews(){
            newsList({
                category: this.categoryList[this.curIndex].id,
                pageIndex: this.pageIndex,
                pageSize: this.pageSize
            }).then(res=>{
                ....
                
                // 请求成功后,需要将控制上拉加载的属性变量变为false
                this.loading = false;
                // 没有更多数据的时候需要把finished变为true
                // 当请求回来的数据少于请求的数量的时候,说明已经没有更多数据了
                if(res.data.data.length < this.pageSize){
                    this.finished = true;
                }
                // 保存数据
                // 需要将最新页的数据和老数据合并在一起,可以使用扩展运算符合并或者使用数组合并方法concat
                this.newsArr = [...this.newsArr,...res.data.data];
            })
        },

5、重置新闻列表数据等等

需要在下拉属性和点击栏目的时候重置数据

        // 重置数据
        resetData(){
            this.newsArr = []; // 新闻列表
            this.pageIndex = 1;// 当前请求的页数
            this.finished = false;// 上拉加载完成后需要用到的变量
        },
         // 下拉刷新
        onRefresh(){
            // 重置数据
            this.resetData();
            // 重新请求数据
            ...
        },
        // 点击栏目(点击栏目会触发的函数),切换新闻列表
        categoryChange(name,title){
            // 重置数据
            this.resetData()
            // name属性就是栏目的下标值
            // title:栏目标题
            ...
        }

6、栏目吸顶效果

给tab组件添加属性sticky

<van-tabs sticky v-model="curIndex" @change="categoryChange" ></van-tabs>

七、新闻详情

1、创建详情组件articleDetail.vue

2、配置路由

3、布局

​ 1、头部和文章详情布局:直接从预习资料中获取

​ 2、底部:需要封装成功一组

4、实现功能

​ 4-1、渲染新闻详情数据

​ 1、封装接口函数

​ 2、实现从点击首页新闻列表项,跳转到新闻详情页,并传新闻id过去。

​ 3、在created钩子函数中请求数据并保存数据

​ 4、拿到保存的数据进行渲染

​ 5、调节样式

​ 当vue解析v-html内容的时候,是把内容当成组件进行解析,因此设置内部样式的时候需要用到样式穿透

​ 6、视频的渲染

        <!-- 
            文章内容,文章内容包含结构,所以需要使用v-html来渲染
         -->
        <div v-if="article.type==1" class="content" v-html="article.content"></div>
        <div v-if="article.type==2" class="content">
            <!-- 视频 -->
            <video :src="article.content" controls poster="../assets/高木同学.jpg"></video>
        </div>

​ 4-2、关注用户功能(关注的是人)

​ 1、根据文章详情数据的has_follow字段来判断,是否已关注此用户,并动态设置样式和文字

<span :class="{ follow: article.has_follow }">{{ article.has_follow?'已关注':'关注' }}</span>

    &.follow{ // 已关注的样式
      color: rgb(32, 32, 32);
      background-color: #fff;
      border: 1px solid rgb(32, 32, 32);
    }

​ 2、发起关注用户的请求

​ 看文档封装关注用户的接口函数

​ 需要封装2个函数,关注用户接口,取消关注的接口

​ 在详情页面引入关注函数,然后通过点击事件发起请求

      // 关注、取消用户
      followFn(){
        let api = this.article.has_follow?user_unfollow:user_follows;
        api(this.article.user.id).then(res=>{
            this.$toast.success(res.data.message);
            this.article.has_follow = !this.article.has_follow;
        })
      }

​ 4-3、点赞文章功能

​ 1、根据用户是否登录做点击按钮的效果,

​ 如果未登录按钮显示“点赞”两个字,

​ 如果登录了且没有对文章进行点赞,那么按钮显示数字(也就是点赞的数量)并且显示黑色字体

​ 如果登录了且对文章已经点赞,那么按钮显示数字,并显示共色字体。

​ 2、实现点赞功能

​ 看文档封装点赞接口函数

// 点赞 /post_like/:id get
export const post_like = id=>axios({
    url: "/post_like/"+id
})

​ 在文章详情页面引入函数,然后在点赞按钮的点击事件中发起请求

​ 封装响应拦截,未登录的情况下不允许点赞,并且跳转到登录页面。

// src/utils/request.js中进行封装响应拦截
import Vue from "vue";
import { Toast } from 'vant';
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  // 判断用户是否已登录,如果未登录跳转到登录页面
  if(response.data.message == "用户信息验证失败"){
    Toast("请求登录!");
    location.href = "#/login";
  }
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});
...

​ 如果登录了,进行点击请求。

      // 点赞(文章)
      likeFn(){
        post_like(this.$route.query.id).then(res=>{
          console.log(85,res);
            this.$toast(res.data.message);
            // 修改判断是否点赞的布尔值
            this.article.has_like = !this.article.has_like;
            // 修改点赞的数量
            this.article.has_like?this.article.like_length++:this.article.like_length--;
        })
      },

八、我的关注

1、创建myFollow.vue组件

2、配置路由

3、布局

4、实现功能

​ 4-1、实现我的关注列表渲染

​ 1、看文档封装我的关注接口函数

​ 2、在“我的关注”页面的created钩子函数中请求并保存数据

​ 3、用保存起来的数据进行渲染

​ 4-2、取消关注

​ 直接引入之前封装的接口函数,然后取消关注按钮的点击事件中进行调用即可,取消关注后需要将该数据从页面中删除

九、文章详情页面底部收藏功能

​ 1、封装一个组件来实现底部模块comment.vue

​ 2、布局

​ 3、引入到详情页面中使用

​ 4、收藏功能

​ 动态设置样式,可以通过has_star子段判断,true表示已收藏(红色星星),false未收藏(黑色星星)

​ 封装收藏的接口函数

​ 点击收藏按钮,在这个点击事件的事件处理函数中进行收藏请求即可

十、我的收藏

1、创建组件myStar.vue

2、配置路由

3、布局

4、实现功能

​ 1、渲染文章收藏列表

​ 1-1、封装“我的收藏”接口函数

​ 1-2、引入函数,并在created钩子函数中请求列表数据并保存

​ 1-3、循环数据渲染列表

​ 2、点击文章,跳转到文章详情页面

​ 3、取消收藏

​ 3-1、使用“SwipeCell 滑动单元格”组件来实现往左滑动效果(vant-contrib.gitee.io/vant/#/zh-C…

​ 3-2、点击删除按钮,调用取消收藏接口即可(取消收藏接口已经在之前封好了直接引入使用即可)

十一、文章评论页面

1、创建组件commentList.vue

2、配置路由

3、布局

​ 头部:直接使用vant组件库nav-bar组件实现

​ 底部评论块:自己封装在组件comment.vue,这个组件需要传递参数才能显示,所以先要做从文章详情页面跳转到评论页面的功能,并把文章id传递到评论页面

​ 评论列表的布局:

​ 1、直接发表评论的结构

​ 2、有一层回复的评论的结构

​ 3、有多层回复评论的结构 --- 用到递归组件

4、实现功能

​ 1、渲染评论列表

​ 1-1、封装评论列表的接口函数

​ 1-2、在评论列表的页面中的created钩子函数请求评论列表数据并保存

​ 1-3、拿到这些数据进行渲染即可

​ 2、点击文本框,弹窗多行文本域,隐藏文本框。

​ 文本框和多行文本域的显示隐藏是通过isFocus变量来控制的,所以我们只要在文本框获取焦点的时候把isFocus变量的值改为true即可。

​ 3、发表评论

​ 情况一:直接发表评论

​ 情况二:点击最外层的回复按钮,进行评论

​ 情况三:点击嵌套组件中的回复按钮,进行评论

​ 情况四:点击递归组件生成的回复按钮,进行评论

情况一:直接发表评论

1、封装评论接口函数

2、收集用户输入的评论内容

3、在底部评论块组件中引入评论接口函数,然后再事件处理函数中 进行发表评论

​ 注意:发表成功后需要更新评论列表数据,因此可以在发表评论成功后“子传父”通知父组件更新列表数据

4、多行文本域自动获取焦点

      // 显示多行文本域,用于编辑发表表内容
      editInput(){
        this.isFocus = true;
          //自动获取焦点
        this.$nextTick(()=>{
          this.$refs.commtext.focus();
        })
      },

情况二:点击最外层的回复按钮,进行评论

​ 1、点击回复按钮,弹出多行文本域。

​ 通知子组件弹出多行文本域,并且传回复的评论id。

​ 2、在发表评论的事件处理函数中,添加id传递给后台

情况三:点击嵌套组件的回复按钮,进行评论

​ 1、点击回复按钮,弹出多行文本域。

​ 实现:点击回复按钮,“子传父”通知父组件并传评论id,弹出多行文本域。然后进行“父传子”,通知子组件弹出多行文本域,并且传回复的评论id。

(.\img\评论列表-嵌套组件回复功能的实现.png)

情况四:点击递归组件生成的回复按钮,进行评论

分析:直接在递归组件上绑定自定义事件replyEvent事件即可

<commentItem @replyEvent="replyFn" v-if="obj.parent" :obj="obj.parent"></commentItem>

4、取消发表评论

​ 给“取消按钮”添加点击事件,然后在事件处理函数中做一下事情:

​ 1、isFocus = false; 隐藏多行文本域

​ 2、content = ""; 清空文本内容

​ 3、parant_id = ""; 清空回复评论的id

十二、栏目管理页面

1、创建category.vue组件

2、配置路由

3、布局

​ 1、头部,使用vant组件库中的nav-bar组件

​ 2、列表,使用了vant组件库的Grid 宫格组件

4、实现功能

#### 1、栏目列表的渲染

​ 引入接口category函数,在created中请求并保存数据

​ 使用数据进行渲染v-for

​ “头条”和“关注” 不能被删除,所以要在频道管理列表中删除掉。

#### 2、实现删除和添加栏目

​ 点击栏目删除数据,把数据添加到别删除的列表中

        // 删除栏目
        delFn(id){
            // 删除栏目
            let index = this.category.findIndex(item=>item.id==id);
            let res = this.category.splice(index,1);
            // 保存被删除的栏目
            this.delCategory  = [...this.delCategory,...res];
        },
        // 添加栏目
        addFn(id){
            let index = this.delCategory.findIndex(item=>item.id==id);
            let res = this.delCategory.splice(index,1);
            // 把数据添加到category列表中
            this.category = [ ...this.category, ...res ];
        }

3、缓存数据

​ 3-1、数据发生变化的时候进行换,可以使用侦听器watch来完成这个功能

​ 3-2、在created钩子函数判断栏目是否存在缓存,如果有缓存则从缓存中获取初始数据,没有则请求数据。

4、首页的栏目数据根据频道管理的栏目列表进行渲染

// index.vue
    created(){
        // 判断栏目是否存在缓存,有缓存直接从缓存中获取
        let defaultCategory = JSON.parse(localStorage.getItem("defaultCategory"));
        if(defaultCategory){
            this.categoryList = [...defaultCategory,...JSON.parse(localStorage.getItem("category"))];
            // 新闻列表
            this.getNews()
            return
        }
        // 没有缓存 直接调用接口获取栏目列表
        ....
    },

5、给首页栏目的最右边添加一个加号,用于跳转到频道管理页面

1、使用伪对象after来实现这个加号

2、然后通过事件委托完成点击加号跳转页面功能

十三、搜索页面

1、创建search.vue组件

2、配置路由

3、布局

4、实现功能

​ 4-1、实现搜索功能

​ 1、封装搜索接口函数

​ 2、收集用户输入的内容

​ 3、点击搜索按钮,根据输入内容调用接口函数进行搜索

​ 4、渲染搜索结果

​ 5、在搜索结果中搜索的关键词显示红色

​ 使用replaceAll方法查找关键词,并替换成具有红色样式的内容

​ 4-2、历史记录功能

​ 1、每次搜索的时候保存搜索关键词(注意查重)

​ 2、渲染历史记录

​ 3、缓存历史记录(数据持久化)

​ 4、点击历史记录进行搜索,并且需要将关键词显示在输入框中

​ 4-3、点击搜索结果,跳转到文章详情页面

十四、路由权限设置(导航守卫)

分析:有些页面是必须登录后才能访问的,因此需要做导航守卫

// main.js 路由权限判断 个人中心和个人信息编辑页面是必须登录后才能访问的,因此需要做导航守卫
let arr = ["/personal","/edit_profile"];
router.beforeEach((to,from,next)=>{
  let token = localStorage.getItem("token-68")
  if(arr.indexOf(to.path) != -1 && !token){
    next("/login")
  }else{
    next()
  }
})

十五、完善项目

所有的路由跳转添加上去

问题一:当跳转到到当前路由的是会报错(这个报错是路由规定不能重复跳转到同一个路由地址)

解决:在src/router/index.js中添加如下代码即可

// 解决重复跳转同一个路由报错问题
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
    return originalPush.call(this, location).catch(err => err)
}

问题二:当从首页的非“头条”栏目中跳转到详情页面再返回到首页的时候自动跳转到了栏目这个到菜单。

解决:通过路由元信息保存当前栏目的下标值,然后在首页的栏目下标值则需要从路由元信息上获取即可

// 路由
const routes = [
    { // 首页
        path: "/index",
        component: ()=>import("@/views/index.vue"),
        meta: { // 元信息,作用:用于保存你想保存的任何信息
            curIndex: localStorage.getItem("token-68")?1:0
        }
    }
]
// 首页
data(){
    return {
        curIndex: this.$route.meta.curIndex, // 当前栏目的下标值从路由元信息中获取
    }
}
// 点击栏目(点击栏目会触发的函数),切换新闻列表
categoryChange(name,title){
    。。。
    // 切换栏目的时候,把栏目的下标值保存到路由元信息上
    this.$route.meta.curIndex = name;
    // 获取点击栏目的新闻列表
    。。。
}

十六、优化

1、所有的请求都添加loading效果

2、所有的请求都应该添加请求是否成功判断

十七、项目打包上线

1、为什么要打包?

答:因为源码体积比较大,文件数量多。

如何打包?答:在项目根目录下执行以下命令即可

npm run build

下图为项目打包后的大小

2、如何上线?

其实就是将前端打包好的代码放到远程服务器上?

前端只需要根据后台说明进行代码上传。

配置项目打包后的文件引入路径

在vue.config.js中进行配置即可

module.exports = {
    publicPath: "./"  // 配置打包后的文件引入路径,默认是绝对路径,./表示相对路径
}