Loading

Headline 项目总结中

目录

1.项目准备

技术栈:

  1. Vue
  2. vue-router
  3. vant
  4. axios,抽取api
  5. vue-cli脚手架
  6. Vuex状态管理

1.1 rem适配

头条项目使用rem适配

需求:浏览器尺寸改变之后,页面元素自动适配

步骤

下包导包

npm i amfe-flexible

main.js导入

通过modele的对应模块可以分隔屏幕

image-20210609093335754

在对应文件可以设置根文字的大小

image-20210609093530800

1.2 通用样式CSS

  1. 存放目录在 /src/styles/base.less

image-20210609093712805

  1. main.js中导入样式,顺序应在组件库之后,自己写的要覆盖其他的,在后面引入

image-20210609094151743

1.3删除测试代码

在创建项目的时候会自动创建一些测试代码

针对删除

  1. App.vue内容干掉
    1. 保留挂载点 #app 和 router-view渲染结构
  2. views目录内容,清空多余的组件,创建需要的组件
  3. /router/index.js
    1. 自定义路由规则
  4. components/HelloWorld.vue删掉

1.4Git托管

https 或 ssh托管

webstorm一键链接和管理

image-20210609094916522

2.login页面

2.1 页面布局和表单校验

布局

​ 1.导航栏

​ 2.表单

​ 3.提交按钮

校验

​ 1.添加rule对象规则

​ required必填项和提示信息

​ 正则表达式规定输入信息格式

vant-field组件

vant-form组件

  1. van-form
    1. @submit:表单验证成功之后触发的回调函数
      1. 参数是一个对象
      2. 内部的输入元素的name属性和value值,拼接为一个对象
  2. 输入元素
    1. rules:校验规则
      1. 数组
      2. 每一条规则是一个对象
      3. required:必填
      4. message:提示信息
      5. pattern:正则的规则
<template>
  <div class="login-container">
    <!-- 导航栏 -->
    <van-nav-bar title="登录" class="my-nav-bar" />
    <!-- 表单 -->
      //  内置的submit事件调用method的onSubmit自定义方法
    <van-form @submit="onSubmit">
      <van-field
	
        v-model="username"
		//name提交的参数名
		//value在input框由用户输入
        name="用户名"
        label="手机号"
		//必填项
        required
        placeholder="请输入手机号"
        :rules="[
		//校验规则
          { required: true, message: '请输入手机号' },
          {
            pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
            message: '手机号格式不对'
          }
        ]"
      />
      <van-field
        v-model="password"
        name="密码"
        label="验证码"
        required
        placeholder="请输入验证码"
        :rules="[
          { required: true, message: '请填写验证码' },
          { pattern: /\d{6}/, message: '验证码格式不对' }
        ]"
      />
      <div style="margin: 16px;">
        <van-button round block type="info" native-type="submit"
          >提交</van-button
        >
      </div>
    </van-form>
  </div>
</template>

<script>
export default {
//组件暴露的名称
  name: 'login',
//login组件的数据,以函数形式表达
  data () {
    return {
      username: '',
      password: ''
    }
  },
//对应表单的自定义方法
  methods: {
      //values是一个对象,name(代码定义)-value(用户输入)
    onSubmit (values) {
      console.log('submit', values)
    }
  }
}
</script>

<style lang="less">
  //设置登录条的样式
.login-container {
  .my-nav-bar {
    background-color: #3196fa;
    .van-nav-bar__title {
      color: white;
    }
  }
}
</style>

2.2login页的接口抽取

下包导包

  1. 下包:npm i axios

  2. 导入在/src/api/login.js

  3. create方法创建一个副本

    image-20210608105527869

4.可用信息

  1. 手机号很多个
  2. mobile:13912345678
  3. 验证码固定的
  4. code:246810

image-20210609113458157

image-20210609113541768

2.5.loading效果

避免用户频繁提交,为按钮增加loading效果,并且切换启用禁用状态

需求:

  1. 数据提交时,为按钮增加loading效果
  2. 切换启用/禁用状态,避免重复点击
  3. 通过button按钮的属性实现

Example

<template>
  <div>
    <button @click="isLoading = !isLoading">切换loading</button>
    <br />
    <van-button :loading="isLoading" type="primary" />
    <br />
    <van-button :loading="isLoading" type="primary" loading-type="spinner" />
    <br />
    <van-button
	//当加载中的时候,禁止提交按钮的点击,其状态为true是禁止
      :disabled="isLoading"
	//当加载中的时候,其状态时true,显示加载动画
      :loading="isLoading"
	//type =info,是信息按钮,为蓝色
      type="info"
	//加载中的文字
      loading-text="加载中..."
      >按钮</van-button
    >
  </div>
</template>

<script>
          
export default {
  data () {
    return {
      isLoading: false
    }
  }
}

</script>

<style></style>

2.6封装token方法

实现功能,把token保存到缓存中

步骤:

  1. sessionStorage刷新不在了
  2. localStorage刷新还在
  3. .then
    1. 保存起来
      1. 默认无法直接保存复杂类型
      2. 除非转为JSON格式的字符串
      3. JSON.stringify(复杂类型)-->字符串

token在多个地方都需要使用,比如登出,接口我们把它抽取一下,方便调用,同时避免出错,为了方便操作缓存,封装工具函数

  1. /src/utils/token.js

image-20210609114010247

  1. 提供3个方法并暴露出来
    1. saveToken
      1. 保存token
      2. 接收参数
    2. removeToken
      1. 删除token
      2. 无参数,无返回至
    3. getToken
      1. 返回token

实现token工具函数的封装

// 定义key
const TOKENKEY = 'top-line-token'

// 保存 token
const saveToken = tokenObj => {
  window.localStorage.setItem(TOKENKEY, JSON.stringify(tokenObj))
}
// 删除 token
const removeToken = () => {
  window.localStorage.removeItem(TOKENKEY)
}
// 获取 token
const getToken = () => {
   //getToken需要return
  // str-->obj
  return JSON.parse(window.localStorage.getItem(TOKENKEY))
}
//将定义的方法暴露出去
export { saveToken, removeToken, getToken }

2.7轻提示toast

在调用onsumit方法提交成功之后要打印数据和弹出轻提示

1.导入请求方法

2.在点击提交按钮时调用onsumit方法,提交请求之后通过then和catch判断是否请求成功

3.如果请求成功就弹出轻提示--陈工

4.如果请求失败就弹出轻提示--失败

image-20210608111934092

2.8整合三部分代码

loading-MV代码、token工具函数封装、、toast-MV代码、

1.在请求成功时除了改变toast的轻提示,还有改变loading动画的状态

2.在请求时时除了改变toast的轻提示,还有改变loading动画的状态

3.为了防止loading响应太快,出现闪顿,设置一个定时器给关闭loading的语句

4.在Model结构中控制改变 过渡动画的状态 以及 禁用的状态

实现代码如下

<template>
  <div class = "login-container">
    <!-- 导航栏 -->
    <van-nav-bar title = "登录" class = "my-nav-bar"/>
    <!-- 表单 -->
    <van-form @submit = "onSubmit">
      <van-field
		//绑定data中的mobile数据
        v-model = "mobile"
		//设置请求的键名
        name = "mobile"
		//绑定要显示的文字
        label = "手机号"
		//设置必选项
        required
        //设置占位符placeholder,同时给占位符文本内容
        placeholder = "请输入手机号"
		//对文本框的内容通过正则进行限制
        :rules = "[
          { required: true, message: '请输入手机号' },
          {
            pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
            message: '手机号格式不对'
          }
        ]"
      />
      <van-field
        v-model = "code"	
        name = "code"
        label = "验证码"
        required
        placeholder = "请输入验证码"
		//对文本框的内容通过正则进行限制
        :rules = "[
          { required: true, message: '请填写验证码' },
          { pattern: /\d{6}/, message: '验证码格式不对' }
        ]"
      />
      <div style = "margin: 16px;">
        <van-button
          :loading = "isLoading"
          :disabled = "isLoading"
          loading-text = "登录ing"
          round
          block
          type = "info"
          native-type = "submit"
        >提交
        </van-button
        >
      </div>
    </van-form>
  </div>
</template>

<script>
// 导入api的请求方法
import { userLogin } from '../../api/login'
// 导入 token工具函数
import { saveToken } from '../../utils/token'

export default {
  name: 'login',

  data () {
    return {
      // 在页面进入的时候加载一个手机号和验证吗,方便测试。
      // 逻辑上说,应该由用户输入和验证
      mobile: '13912345678', // 手机号
      code: '246810', // 验证码
      isLoading: false// 是否正在加载中
    }
  },
  methods: {
    onSubmit (values) {
      //键名是name,键值是用户在表单输入的,为了方便测试设置了一个默认的值
      // values是一个对象{mobile:'xxx',code:'xxx'}
      // 开启loading
      this.isLoading = true
      //userlogin是封装的请求方法,传入请求的对象{ mobile: '13912345678', code: '246810' }
      userLogin(values)
        .then(res => {
          console.log('res:', res)
          // 保存token
          saveToken(res.data.data)
          //设置定时器的目的是为了在呈现效果的时候不会闪顿,更加可控和顺滑
          //请求成功之后关闭轻提示,关闭动画
          setTimeout(() => {
            this.$toast.success('登录成功!')
            this.isLoading = false
          }, 500)
        })
        .catch(errRes => {
          console.log('errRes:', errRes)
           //设置定时器的目的是为了在呈现效果的时候不会闪顿,更加可控和顺滑
           //请求失败之后关闭轻提示,关闭动画
          setTimeout(() => {
            this.$toast.fail('登录失败!')
            this.isLoading = false
          }, 500)
        })
    }
  }
}
</script>

//设置导航栏nav的样式
<style lang = "less">
.login-container {
  .my-nav-bar {
    background-color: #3196fa;
    .van-nav-bar__title {
      color: white;
    }
  }
}
</style>

2.9使用token的保存方法

image-20210609151121252

2.10跳转到home

userLogin(values).then(res => {
        /* 进入到then就表示已经成功请求到了 */
        console.log('res', res)
        saveToken(res.data.data)

        /* 防止太快看不到loading动画,设一个定时器 */
        setTimeout(() => {
          this.$toast.success('登录成功!')
          this.isLoading = false
          this.$router.push({
            path: '/home'
          })
        }, 300)

2.11 重定向

在没有输入url时,打开项目看到的是白色界面,最后咱们通过重定向来解决这个问题 '/'跳转到home

  routes: [
    {	
        // 默认地址
        path: '/a', 
        // 重定向的地址 需要被注册
        redirect: '/b' 
    }
  ]

image-20210610095105648

3.layout页面

3.1整合底部layout结构

<template>
  <div class = "layout-container">
    layout
    <van-tabbar v-model = "active" route>
        //根据样式找到对应的图标类名
      <van-tabbar-item icon = "home-o">首页</van-tabbar-item>
      <van-tabbar-item icon = "chat-o">问答</van-tabbar-item>
      <van-tabbar-item icon = "video-o">视频</van-tabbar-item>
      <van-tabbar-item icon = "user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<script>
export default {
  name: 'layout',
  data () {
    return {
        //高亮第一个图标
      active: 0
    }
  }
}
</script>

<style></style>

<van-tabbar v-model = "active" route> route的作用是开启路由模式,vant的Tabbar组件的参数

3.2layout嵌套路由

1.文件结构

image-20210610091129197

2.路由导入

import home from '../views/layout/home'
import question from '../views/layout/question'
import movie from '../views/layout/movie'
import user from '../views/layout/user'

3.渲染结构的出口在layout的index.vue中的vue-router

	<!--嵌套路由的出口-->
    <router-view></router-view>

4.嵌套路由出口的渲染(router-view)

image-20210610094447574

5.children在定义的时候也决定了它的路由出口在父组件上,所以组件的router-view最终渲染的位置在layout组件上,通过chrome插件可以查看 children: [{},{},{}]

4.user页面

4.1.整合路由

SSR直连,解决timeout

4.2顶部区域

<template>
  <div class = "user-container">
    <div class = "info-box">
        
      <van-image
        class = "my-image"
        round
        src = "https://img01.yzcdn.cn/vant/cat.jpeg"
      />
      <h2 class = "name">
        起飞
        <br>
        <van-tag color = "#fff" text-color = "#3296fa" type = "primary">2021-6-10</van-tag>
      </h2>
    </div>
  </div>
</template>

<script>
            
export default {
  name: 'user'
}

</script>

<style lang = "less">
.user-container {
  .info-box {
    height: 100px;
    background-color: #3296fa;
    display: flex;
    padding-left: 18px;
    // 从 弹性布局 左边开始
    justify-content: flex-start;
    align-items: center;

    .my-image {
      width: 60px;
      height: 60px;
      margin-right: 5px;
    }

    .name {
      margin-left: 5px;
      color: white;
      font-size: 15px;
      font-weight: normal;
    }
  }
}
</style>

拓--justify-content:的样式展示

justify-content:flex-start

4.3操作按钮

less语法有&操作符 用法:&符号有2中用法,其一:父选择符;其二:且的意思,在这里使用的

设置操作链接的布局和样式

image-20210610115646777

实现结构

<van-row class = "my-control-box">
      <van-col class = "my-col" span = "8"
      >
        <van-icon class = "my-icon my" name = "newspaper-o"/>
        我的作品
      </van-col
      >
      <van-col class = "my-col " span = "8"
      >
        <van-icon class = "my-icon star" name = "star-o"/>
        我的收藏
      </van-col
      >
      <van-col class = "my-col " span = "8"
      >
        <van-icon class = "my-icon history" name = "tosend"/>
        阅读历史
      </van-col
      >
    </van-row>

实现样式


  //设置每一列的字体大小,并且居中
  .my-col {
    font-size: 12px;
    text-align: center;
  }

  //单独给icon字体图标设置大小,并且转为块级元素
  .my-icon {
    font-size: 28px;
    display: block;

    //且的意思,my-icon且my
    &.my {
      color: #77aaff;
    }

    &.star {
      color: #ff0000;
    }

    &.history {
      color: #ffaa00;
    }
  }
}

4.4底部区域

1.icon:左侧图标

2.title:左侧文本

3.is-link:右侧箭头

image-20210610121347328

实现结构

<template>
  <div>
    <van-cell-group>
      <van-cell title="编辑资料" icon="edit" is-link />
      <van-cell title="小智同学" icon="chat-o" is-link />
      <van-cell title="系统设置" icon="setting-o" is-link />
      <van-cell title="退出登录" icon="warning-o" is-link />
    </van-cell-group>
  </div>
</template>

<script>
export default {}
</script>

<style></style>

实现样式

.my-control-box {
  //设置上下内边距,把三个小图标挤进去
  padding-top: 20px;
  padding-bottom: 20px;

  //设置每一列的字体大小,并且居中
  .my-col {
    font-size: 12px;
    text-align: center;
  }

  //单独给icon字体图标设置大小,并且转为块级元素
  .my-icon {
    font-size: 28px;
    display: block;

    //且的意思,my-icon且my
    &.my {
      color: #77aaff;
    }

    &.star {
      color: #ff0000;
    }

    &.history {
      color: #ffaa00;
    }
  }
}

4.5用户信息

1.测试接口

image-20210610144411928

image-20210610144532828

2.api抽取

在src/api文件夹封装一个user.js用来保存user页面用到的api

//导入请求组件axios
import axios from 'axios'

// 导入获取token的工具函数
import { getToken } from '@/utils/token'

//设置基地址
const request = axios.create({
  baseURL: 'http://toutiao-app.itheima.net'
})

//封装获取用户信息的方法
const getUserInfo = () => {
  return request({
      //剩余的请求地址
    url: '/v1_0/user/profile',
      //请求的方式
    method: 'get',
      //请求头的设置
    headers: {
        
      Authorization: `Bearer ${getToken().token}`
    }
  })
}

//暴露获取用户信息的方法
export { getUserInfo }


//保存的token

//{token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M…HNlfQ.4XdqWBCJ-Q_IGxNY5jEekiqrmzOg6zZIYLQkbK5WIWE", refresh_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M…ydWV9.UycupnAiENN4AZROq7n8LbxkNEAuUYZHcXe52SFfVgs"}

////获得请求头的授权token,通过调用token工具函数,因为在saveToken的时候是一个对象,所以调用函数得到的是一个对象 setItem(TOKENKEY, JSON.stringify(tokenObj))

3.使用添加数据

实现逻辑

//导入默认的图片
import defaultImg from '../../../assets/logo.png'
// 导入 请求的 api方法
import { getUserInfo } from '../../../../src/api/user'

//暴露接口
export default {
  name: 'user',
  data () {
    return {
      defaultImg,
      // 设置一个空对象保存用户信息
      userInfo: {}
    }
  },
  created () {
    // 在钩子添加请求到的信息
    getUserInfo().then(res => {
      console.log('res:', res)
        //如果获取成功用户信息,添加到  userInfo: {}
      this.userInfo = res.data.data
    })
  }
}

实现结构

<div class = "info-box">
      <van-image
        class = "my-image"
        round
        //设置返回数据的头像
        :src = "userInfo.photo"
      />
      <h2 class = "name">
          //设置返回数据的名称
        {{ userInfo.name }}
        <br>
            //设置返回数据的生日
        <van-tag color = "#fff" text-color = "#3296fa" type = "primary"> {{ userInfo.birthday }}</van-tag>
      </h2>
   </div>

4.6登出功能

通过dialog模态框实现弹出

清空信息的语句在后面有改善,通过Vuex在登出方法中做出优化

this.$store.commit('setUserInfo', {})

实现逻辑

methods: {
    logout () {
        //
      this.$dialog
        .confirm({})
        .then(() => {
          // 登出后删除token数据
          removeToken()
          // 登出后将用户信息清空,之前保存的用户信息是一个对象
          this.userInfo = {}
          // 登出后通过router组件跳转页面到home,因为不是后台管理系统所以不需要跳转到login登录页面
          this.$router.push({
            path: '/home'
          })
          // 登出失败返回一个catch
        }).catch(() => {
          console.log('catch')
        })
    }
  }

实现功能

在登出的标签注册一个点击事件

<van-cell title = "退出登录" icon = "warning-o" is-link @click = "logout"/>

4.7登录判断token--前置守卫

限制在user页面进行判断,其他页面不登陆也可以访问

image-20210611093740951

image-20210611095014458

文件目录 --src/router/index.js

实现逻辑

导航守卫的三个参数必须按顺序书写


//routes参数的设置
{
        path: 'user',
        name: 'user',
        component: user,
        meta: {
          //自定义的规则字段
          needLogin: true
        }
}

// 1.在导航守卫中先获取meta是否需要登录
router.beforeEach((to, from, next) => {
    //判断是否需要
  if (to.meta.needLogin !== true) {
      //如果不需要元信息,直接返回next()
    return next()
  }

  // 拿到令牌
  const tokenObj = getToken()
//2.判断token令牌是否存在
  if (tokenObj === null) {
      //如果令牌为空,弹出轻提示
    Toast.fail('请先登录')
       //如果令牌为空,跳转到login页面
    return next({ path: '/login' })
  }
    //token既不是空,而且需要元信息
  next()
    
    
//3.判断token令牌是否正确  
     // 拿到个人的信息
     //getUserInfo根据已有的token发起请求,并且返回用户的数据
  getUserInfo()
    // 请求成功之后携带数据直接到下一站
    .then(res => {
      console.log(res)
      next()
    })
    // 请求失败,根据返回的状态码401判断token不正确(后端完成)
    .catch(errRes => {
      if (errRes.response.status === 401) {
        removeToken()
        // 如果token给出提示请先登录
        Toast.fail('请先登录')
        // 如果token不对跳转登录页面
        next({ path: '/login' })
      }
    })
})

console.dir()可以解析属性的值

解决Vuerouter的Promise报错(吞掉)

// 吞掉 没有处理的 promise的错误
const originalPush = VueRouter.prototype.push


VueRouter.prototype.push = function push (location, onResolve, onReject) {
  if (onResolve || onReject) {
    // 执行之后 阻断后续代码
    return originalPush.call(this, location, onResolve, onReject)
  }
  return originalPush.call(this, location).catch(err => err)
}

4.8 共享用户信息-vuex

1.添加vuex组件

vue add vuex

在mian.js文件上

  // 将Vuex仓库注册Vue构造函数上
  store,

2.修改用户信息

src/router/index.js

// 将获取的用户信息共享到Vuex的仓库的state中,通过定义的 mutations的setUserInfo方法修改预设置的信息信息
//第一个参数是调用的修改数据的方法,第二个数据是修改后的数据
store.commit('setUserInfo', res.data.data)

3.设置路由元信息

export default new Vuex.Store({
    //设置原始数据
  state: {
    userInfo: {}
  },
  
  mutations: {
        //定义修改数据的方法
      	//这个方法接收一个原始的数据,第二个参数是传入的修改后的数据
    setUserInfo (state, newUserInfo) {
      state.userInfo = newUserInfo
    }
},

4.查看共享的数据

image-20210611112813047

4.9用户页面调整

1.导航守卫中请求了一次

导航守卫属于router,在router文件夹的index.js当中

​ 目的有三

​ 为了判断是否需要判断needLogin

​ 为了判断请求的数据是否存在token

​ 为了判断token的值是否正确,401?

这里拿到了用户的数据只进行了判断,没有进行使用

2.加载user页面请求一次

这里通过调用 getUserInfo()方法获取,用来渲染页面。

请求用户信息的方法getUserInfo()定义在src/api/user中,因为也算是请求,放在api文件当中

3.解决多次请求

Vuex本质是共享数据,统一放在新建的src/store的文件中的index.js

用户信息保存到Vuex中,调整用户页面信息的数据来源(目的是为了只请求一次放在仓库中,减少请求次数),之前是两次里请求

image-20210611120628898

在登出方法logout(),修改清空数据的语句

// 登出后将用户信息清空,之前保存的用户信息是一个对象
          // this.userInfo = {}
          // 优化登出信息清空,现在拿到的信息是从Vuex中来的
this.$store.commit('setUserInfo', {})

4.10登陆成功返回访问页

1.正常访问login页面之后区home页面

2.因为没有登录被转到login,在登录成功之后要返回

​ 1.login/index.vue登陆成功之后进行判断

​ 1.有redirect参数

​ 2.没有redirect参数

实现逻辑

在router文件的index.js中实现redirect参数的携带

1.没有token携带redirect参数

 if (tokenObj === null) {
    Toast.fail('请先登录')
    // 如果没有token令牌跳转到登陆页面,并且携带redirect参数
    return next({
      path: '/login',
      // 定义重新定向的到哪去的路径参数,这个路径参数指向当前没登陆的页面
      query: { redirect: to.path }
    })
  }

2.token令牌错误携带redirect参数

.catch(errRes => {
      if (errRes.response.status === 401) {
        removeToken()
        // 如果token给出提示请先登录
        Toast.fail('请先登录')
        // 如果token不对跳转登录页面
        next({
          path: '/login',
          // 如果请求的token令牌是错误的也要重新定向
          query: { redirect: to.path }
        })
      }
    })

3.在login文件中的index.vue对redirect参数进行判断

该逻辑写在表单提交成功的then之中

 const redirect = this.$route.query.redirect
          if (redirect) {
            return this.$router.push({
              path: redirect
            })
          }
          // 没有redirect参数
          this.$router.push({
            path: '/home'
})

5.edit页面

5.1整合路由

1.创建edit页面在src/views/layout/edit/index.vue

<template>
  <div class = "edit-container">
    <!-- 导航条 -->
    <van-nav-bar left-arrow title = "编辑资料"></van-nav-bar>
    <!-- 头像部分 -->
    <div class = "avatar">
      <van-image fit = "cover" round src = "https://img.yzcdn.cn/vant/cat.jpeg"/>
    </div>
    <!-- 信息展示 -->
    <van-cell-group>
      <van-cell is-link title = "名称" value = "昵称"/>
      <van-cell is-link title = "性别" value = "男"/>
      <van-cell is-link title = "生日" value = "2020-1-1"/>
    </van-cell-group>
  </div>
</template>

<script>
export default {
  name: 'editUser'
}
</script>

<style lang = "less">
.edit-container {
  // 导航条部分
  .van-nav-bar {
    background-color: #3196fa;

    .van-nav-bar__title {
      color: #fff;
    }

    .van-icon-arrow-left {
      color: #fff;
    }
  }

  // 头像部分
  .avatar {
    padding: 20px 0;
    text-align: center;

    .van-image {
      width: 120px;
      height: 120px;
    }
  }
}
</style>

2.在src/router/index.js创建对应的edit路由

路由安放的位置在layout路由的children序列当中

layout是所有chidren路由渲染的出口<router-view></router-view>

{
        path: 'edit',
        name: 'edit',
        component: edit
},

5.2user跳转edit

1.添加跳转属性

操作目录:src/views/layout/user/index.vue

<van-cell title = "编辑资料" icon = "edit" is-link to = "/edit"/>

2.从edit返回user的两种方法

操作目录src/views/layout/edit/index.vue

go方法

onClickleft () {
      this.$router.go(-1)
}

back方法

onClickleft () {
      this.$router.back()
}

5.3 edit登录判断

需求:

  1. 编辑页面需要登录才可以访问

步骤:

  1. 给任意希望登录才可以访问的路由(页面)
  2. 添加元信息即可
    1. meta:{needLogin:true}

5.4编辑隐藏Tabbar

需求:

  1. 访问/edit页面时隐藏tabbar
  2. 结合路由元信息实现

步骤:

  1. 配置路由元信息 ,src/router/index.js
{
        path: 'edit',
        name: 'edit',
        component: edit,
        meta: {
          needLogin: true,
          // 给他设置单独的路由元,为false,其他没有true默认显示,在V-show取反时直接v-show:false隐藏
          hideTabbar: true
        }
}

2.在layout页面隐藏底部导航条,src/views/layout/index.vue

​ 获取元路由信息

​ 对预设的hideTabbar取反

 <van-tabbar v-model = "active" route v-show = "!$route.meta.showTabbar">

3.不隐藏的效果:

image-20210611172812623

4.隐藏的实现效果:

image-20210611172840519

5.5渲染edit页面

需求:

  1. 进入编辑页面,把用户数据渲染到页面上

步骤:

  1. 路由中对token进行了判断,并且将数据保存到了Vuex的仓库当中 store.commit('setUserInfo', res.data.data)

  2. 在edit页面获取仓库中的数据

    ​ 在计算属性中定义一个用户信息的方法,并且返回用户的信息

    computed: {
        // 在计算属性内部定义一个方法,调用时不需要加括号
     userInfo () {
     return  this.$store.state.userInfo
    }
    

3.将获得数据添加到页面, src/views/layout/edit/index.vue

<div class = "avatar">
      <van-image fit = "cover" round :src = "userInfo.photo"/>
</div>
    <!-- 信息展示 -->
    <van-cell-group>
      <van-cell is-link title = "名称" :value = "userInfo.name"/>
      <van-cell is-link title = "性别" :value = "userInfo.gender === 0?'男':'女'"/>
      <van-cell is-link title = "生日" :value = "userInfo.birthday"/>
    </van-cell-group>

在渲染性别的时候要对绑定的数据的值进行判断

因为在行内可以通过三元表达式

<van-cell is-link title = "性别" :value = "userInfo.gender === 0?'男':'女'"/>

5.6 mapState整合

1.导入mapstate函数

import { mapState } from 'vuex'

2.整合computed

computed: mapState(['userInfo']),
  //因为只有一个数据所以不用使用对象和拓展运算符直接进行复制操作
    
  // 之前的定义
  //computed: {
  //   // 在计算属性内部定义一个方法,调用时不需要加括号
  //   userInfo () {
  //     return this.$store.state.userInfo
  //   }
  // },

5.7用户信息请求请求优化

(2到1)-- 登陆判断

在router的index.js对用户信息进行判断

一次是在user页面,一次是在编辑页面

1.user页面的请求

computed: {
    userInfo () {
      return this.$store.state.userInfo
    }
},

2.编辑页面的请求

 computed: {
    // 在计算属性内部定义一个方法,调用时不需要加括号
    userInfo () {
      return this.$store.state.userInfo
    }
 },

3.优化之后


// 为了防止重复的对用户信息发起请求,
  if (store.state.userInfo.name) {
    return next
  }
// 不能通过 store.state.userInfo 来判断,因为是个空对象,空对象判断之后也是true
//return可以打断代码不再往下执行,而且如果有用户信息证明一定有正确的token
//console.log({}===true)//true

5.8 编辑用户名

1.dialog弹出模态框

实现结构

​ 1.绑定点击事件

<van-cell is-link title = "名称" :value = "userInfo.name" @click = "showEditName"/>

​ 2.整合用户名编辑框

<!--姓名编辑框-->
<van-dialog v-model = "showName" title = "修改姓名" show-cancel-button>
  <van-field ref = "nameField" v-model = "name" placeholder = "请输入用户名"/>
</van-dialog>

2.cell绑定点击事件

1.弹框

2.用户的信息填入输入框

3.输入框获取焦点

​ 设置filed框设置ref

​ this.$refs.属性名获取标签添加点击事件

实现逻辑

data () {
    return {
      showName: false,
      name: ''
    }
  },


methods : {
showEditName () {
    //点击模态框,修改showName的属性为true
      this.showName = true
    //获取用户信息的name赋值给定义的name数据
      this.name = this.userInfo.name
      this.$nextTick(() => {
        this.$refs.nameField.focus()
      })
 }
}

4.修改模态框的样式

​ 边框

​ 父盒子设置内边距

.van-dialog__content {
    padding: 10px;
  }

  .van-field {
    border: 1px solid #ccc;
  }

3.注意事项

vue更新数据和更新dom是异步的

vue数据更新--->dom更新是异步的

可以通过$nextTick注册一个回调函数

dom更新之后执行

5.9 抽取edit的api

方便发起请求和维护

1.设置请求拦截器的信息

2.封装编辑用户信息的请求方法

// 导入
import axios from 'axios'
// 导入token工具函数
import { getToken } from '../utils/token'
// create方法设置基地址的请求request
const request = axios.create({
  baseURL: 'http://toutiao-app.itheima.net'
})


// 注册拦截器
// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    console.log('请求拦截器执行啦')
      //设置请求拦截器的信息token令牌!!!
    config.headers.Authorization = `Bearer ${getToken().token}`
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)


// 抽取方法 - 编辑用户信息
const editUserInfo = data => {
  return request({
    url: '/v1_0/user/profile',
      //后台文档定义的请求方式
    method: 'patch',
      // data:{...data}不缩写的参数
    data:data
  })
}


// 暴露出去
export { editUserInfo }

5.10 保存用户名数据

实现结构

添加修改昵称的事件

//显示编辑的用户名
<van-cell is-link title = "名称" :value = "userInfo.name" @click = "showEditName"/>

//保存修改后的用户名
 <van-dialog v-model = "showName" title = "修改姓名" show-cancel-button @confirm = "saveNameEdit">

实现逻辑

  
//显示编辑的用户名
showEditName () {
      this.showName = true
      this.name = this.userInfo.name
      this.$nextTick(() => {
        this.$refs.nameField.focus()
      })
}





// 定义一个保存用户名的方法
  saveNameEdit () {
       // 调用编辑用户信息的函数,并且传入一个参数{gender:0/1}
      editUserInfo({ name: this.name })
        // 请求成功之后返回数据,在then回调中处理
        .then(res => {
          // 在仓库中更新用户的信息
          this.$store.commit('setUserInfo', {
            // 展开所有的用户信息
            ...this.userInfo,
            // 覆盖用户的昵称信息
            name: this.name
          })
        })
    },

5.11 编辑性别信息

实现结构

<van-cell is-link title = "性别" :value = "userInfo.gender === 0?'男':'女'" @click = "showGender = true"/>





//popup编辑框
<van-popup v-model = "showGender" position = "bottom">
      <van-nav-bar title = "修改性别" left-text = "取消"/>
      <van-cell-group>
        <van-cell title = "男" is-link/>
        <van-cell title = "女" is-link/>
      </van-cell-group>
</van-popup>

实现逻辑

//在data中定义
 showGender: false,

点击切换cell组件,修改popub的布尔值,切换弹出的状态

跳转不了看一下路由router

5.12保存性别信息

1.修改数据库中的数据

2.关闭显示的模态框(点击取消,男,女)

实现结构

   1.<!--在渲染性别的时候要对绑定的数据的值进行判断 因为在行内可以通过三元表达式,在点击性别的时候显示性别框-->
      <van-cell is-link title = "性别" :value = "userInfo.gender === 0?'男':'女'" @click = "showGender = true"/>



 //2.
<van-nav-bar title = "修改性别" left-text = "取消" @click-left = "showGender = false"/>


//3.
<van-cell-group>
        <van-cell title = "男" is-link @click = "saveGenderEdit(0)"/>
        <van-cell title = "女" is-link @click = "saveGenderEdit(1)"/>
      </van-cell-group>

实现逻辑

 saveGenderEdit (gender) {
      editUserInfo({ gender }).then(res => {
        this.showGender = false
        this.$store.commit('setUserInfo', {
          // 展开所有的用户信息
          ...this.userInfo,
          // 覆盖用户的性别信息
          gender
        })
      })
    },

5.13编辑日期

data(){
birthday: '',
minDate: new Date(1970, 0, 1)//设置最小值,月份从0开始,看文档
}



showBirthdayPop () {
      this.showBirthday = true
      this.birthday = this.userInfo.birthday
    }
   <van-cell is-link title = "生日" :value = "userInfo.birthday" @click="showBirthdayPop"/>//为生日模态框绑定点击事件
   
   
   <!--生日编辑框-->
    <van-popup v-model = "birthday" position = "bottom">
      <van-datetime-picker
        v-model = "birthday"
        type = "date"
        title = "选择年月日"
        :min-date = "minDate"
      />
    </van-popup>

5.14保存编辑日期

实现结构

<van-datetime-picker
        v-model = "birthday"
        type = "date"
        title = "选择年月日"
        :min-date = "minDate"
        @cancel = "showBirthday = false"
        @confirm = "saveBirthday"
/>

实现逻辑

saveBirthday () {
      // 处理生日的格式,符合要求
      const birthday = moment(this.birthday).format('YYYY-MM-DD')
      editUserInfo({ birthday }).then(res => {
        this.showBirthday = false
        this.$store.commit('setUserInfo', {
          ...this.userInfo,
          birthday
        })
      })
    }

5.14编辑头像

实现结构

<!-- 头像部分 -->
    <div class = "avatar">
        //绑定一个回调
      <van-image fit = "cover" round :src = "userInfo.photo"/>
      <van-uploader :after-read = "afterRead"/>
    </div>

实现逻辑

 // 用户选择头像之后的回调函数
    afterRead (file) {
      console.log('file:', file)
    },

实现样式

// 上传组件布局
  .avatar {
    position: relative;

    .van-uploader {
      position: absolute;
      opacity: 0;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);

      width: 120px;
      height: 120px;

      .van-uploader__upload {
        width: 120px;
        height: 120px;
      }
    }
  }

5.15保存编辑头像,上传组件uploader

after-read 文件读取完成后的回调函数 Function

上传组件在Vant中直接使用

1.引入文件上传组件,绑定点回调函数afterRead

<van-uploader :after-read = "afterRead"/>

2.再上传头像

实现逻辑

 afterRead (file) {
     //打印file数据
      console.log('file', file)
     //表单数据类型
      const formData = new FormData()
      //添加数据
      formData.append('photo', file.file)
     //将邮箱数据作为参数传递给uploaderPhoto方法,在vuex的仓库更新头像数据
      uploaderPhoto(formData).then(res => {
        this.$store.commit('setUserInfo', {
          ...this.userInfo,
            //新的头像的数据
          photo: res.data.data.photo
        })
      })
    }

5.16 裁切头像组件 Cropper

安装和导入、注册

npm i vue-cropper

//导入
import { VueCropper } from 'vue-cropper'

 // 注册组件
 components: {
    VueCropper
  },

实现结构

    <!-- 截图容器 -->
    <div class="cropper-container" v-show="showCropper">
      <VueCropper
               //下面要调用这个组件的方法,给个ref,方便找到DOM元素
        ref="cropper"
			//修改后的img的值
        :img="img"
			//生成截图框,默认是false,直接写上就是tru
        autoCrop
        	//默认截图的高度
        autoCropHeight="120"
			//默认截图的宽度
        autoCropWidth="120"
      ></VueCropper>
			//设置截图框的左下角按钮
      <van-button type="primary" class="confirm-btn">确认</van-button>
			//设置截图框的右下角按钮
      <van-button type="primary" class="cancel-btn">取消</van-button>
    </div>

在回调afterRead中更改逻辑

this.showCropper = true
this.img = file.content

实现逻辑

afterRead (file) {
      console.log('file:', file)
      // 显示截图组件
      this.showCropper = true
      this.img = file.content
}

实现样式

// 截图组件样式
  .cropper-container {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    .van-button {
        //以浏览器为相对定位
      position: absolute;
      bottom: 0;
    }
    .confirm-btn {
      left: 0;
    }
    .cancel-btn {
      right: 0;
    }
  }

5.17裁切头像上传

实现结构

给添加和取消添加事件,都要隐藏裁切组件,但是语句的位置不同

<van-button type = "primary" class = "confirm-btn" @click = "confirmUpload">确认</van-button>
      //点击取消的时候,隐藏裁切组件,将状态改为false
      <van-button type = "primary" class = "cancel-btn" @click = "showCropper = false">取消</van-button>

实现逻辑

formData传入的data截图数据

Blob {size: 61212, type: "image/jpeg"}

formData在接受data数据后,

FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。如果表单enctype属性设为multipart/form-data ,则会使用表单的submit()方法来发送数据,从而,发送数据具有同样形式。

注意:字段 "userfile" 和 "webmasterfile" 都包含一个文件. 字段 "accountnum" 是数字类型,它将被FormData.append()方法转换成字符串类型(FormData 对象的字段类型可以是 Blob, File, 或者 string: 如果它的字段类型不是Blob也不是File,则会被转换成字符串类)。

示例

var fd = new FormData(document.querySelector("form"));
fd.append("CustomField", "This is some extra data");
$.ajax({
  url: "stash.php",
  type: "POST",
  data: fd,
  processData: false,  // 不处理数据
  contentType: false   // 不设置内容类型
});
confirmUpload () {
    // 获取cropper的组件使用组件的方法getCropBlob,传入一个回调函数,获取截图的blob数据data
      this.$refs.cropper.getCropBlob((data) => {
        // do something
        console.log(data)
		
        const formData = new FormData()
        //将获取的截图数据添加到formData
        formData.append('photo', data)
        //调用封装的更新用户头像的方法,将封装好的formData数据作为参数传入方法当中
        //如果formData的数据请求成功
        uploaderPhoto(formData).then(res => {
        // 请求成功之后修改仓库中的用户数据
        //调用vuex中定义修改数据的方法,传入要修改的信息
          this.$store.commit('setUserInfo', {
          //展开当前的用户的数据
            ...this.userInfo,
          //原始的数据就是photo:值,不能通过ES6的简写方法缩写
          //通过以下语句覆盖原始的数据
            photo: res.data.data.photo
          })
        })
      })
   		//点击确认,上传截图数据之后,将裁切组件隐藏,v-show = false
      this.showCropper = false
    }

6.home页面

6.1基础结构

css 的src根目录, ~@

background: url('~@/assets/logo.png') no-repeat;

<template>
  <div class = "home-container">
    <!-- 顶部导航栏 -->
    <van-nav-bar fixed>
      <template #left>
        <div class = "logo"></div>
      </template>
      <template #right>
        <van-button round class = "search-btn" size = "small" icon = "search"
        >搜索
        </van-button
        >
      </template>
    </van-nav-bar>
    <!-- 横向滑动条 -->
    <!-- 内容 -->
    <van-tabs v-model = "active">
      <van-tab v-for = "item in 10" :key = "item" :title = "'标签' + item">
        <div v-for = "v in 50" :key = "v">内容 {{ item }}---{{ v }}</div>
      </van-tab>
    </van-tabs>
  </div>
</template>

<script>
export default {
  name: 'home',
  data () {
    return {
      active: 0
    }
  }
}
</script>

<style lang = "less">
.home-container {
  height: 100vh;
  padding-top: 46px;
  padding-bottom: 50px;
  overflow: hidden;

  .van-nav-bar {
    background-color: #3296fa;
  }

  .van-tabs {
    height: 100%;
    padding-top: 44px;
    // tab栏标题
    .van-tabs__wrap {
      position: fixed;
      left: 0;
      right: 30px;
      top: 46px;
    }

    .van-tabs__content {
      overflow-y: scroll;
      height: 100%;
    }
  }

  .van-tab {
    padding: 0 16px;
  }

  .van-tabs__line {
    background-color: #3196fa;
  }

  // logo区域
  .logo {
    background: url('~@/assets/logo.png') no-repeat;
    background-size: cover;
    width: 100px;
    height: 30px;
  }

  // 搜索按钮
  .search-btn {
    background-color: #5babfb;
    width: 100px;
    font-size: 14px;
    border: none;

    .van-icon {
      color: #fff;
    }

    .van-button__text {
      color: #fff;
    }
  }
}
</style>

6.2接口抽取

和之前一样先把api抽取一下

步骤:

  1. 导入utils/request.js
  2. 根据接口文档抽取方法并暴露出去

封装请求的方法在src/api/home.js

// 导入request
import request from '../utils/request'

// 抽取方法并暴露
const getChannels = () => {
  return request({
    url: '/v1_0/user/channels',
    method: 'get'
  })
}

// 暴露出去
export { getChannels }

6.3获取数据

结合上一步抽取的api完成数据渲染并获取

步骤:

  1. 导入接口并调用
  2. created

实现逻辑

data () {
    return {
      active: 0,
      // 频道信息
      channels: []
    }
  },
created () {
    getChannels().then(res => {
      console.log('res:', res)
        //将返回数据的channels的数据赋值给data中定义的channel
      this.channels = res.data.data.channels
    })
  }

6.4引入vant-list组件01

image-20210616094209348

文章请求的逻辑

请求的channel数据,一进来就请求所有的channel数据。

然后点击对应的标签获得channel id再发起对应的请求,返回对应的文章信息

image-20210616114359447

返回的频道信息

实现结构


实现逻辑


6.5刷新数据

请求的文章数据 通过展开运算符添加到list数组中,this.list.push(...res.data.data.results)

image-20210616112334098

实现结构


实现逻辑


6.6文章图片数据的处理

数据图片

image-20210616115529642

1.在具名插槽 <template #label> 中的嵌套循环组件van-grid-item 中渲染图片,根据返回的type(对应图片的个数)

<!-- 具名插槽 标题的底部 -->
          <template #label>
            <!-- 主体内容区域 -->
            <van-grid :border = "false" :column-num = "3">

2.在外层循环中,渲染文章的作者,评论和日期

嵌套循环

<van-grid :border = "false" :column-num = "3">
  //
  <van-grid-item v-for = "(it,index) in item.cover.images" :key = "index">
    <van-image
      fit = "cover"
      :src = "it"
    />
  </van-grid-item>

外层循环

<div class = "info-box">
  <span>{{ item.aut_name }}</span>
  <span>{{ item.comm_count }}</span>
  <span>{{ item.pubdate }}</span>
</div>

6.7图片懒加载

引入Lazyload组件在utils/vant.js中

在图片渲染的位置添加lazy-load

image-20210616143105690

6.8引入moment组件

格式化翻译日期的时间

// 配置moment组件的语言环境
moment.locale('zh-cn')

<span>{{ item.pubdate | formatTime }}</span>

filters: {
    formatTime (value) {
      return moment(value).fromNow()
    }
}

6.9文章的x更多操作

实现数据

 data:
 	  // 是否显示更多弹框
      isShowAction: false,
      // 是否显示二级菜单
      isShowTwo: false,
      // 当前执行更多操作的文章id
      target: undefined,
      // 举报选项
      reportList: [
        { type: 0, title: '其他问题' },
        { type: 1, title: '标题夸张' },
        { type: 2, title: '低俗涩情' },
        { type: 3, title: '错别字多' },
        { type: 4, title: '旧闻重复' },
        { type: 5, title: '广告软文' },
        { type: 6, title: '内容不是' },
        { type: 7, title: '涉嫌违法犯罪' },
        { type: 8, title: '侵权' }
      ]

1.登录可以看到x,不登陆无法看到``

 <!-- 右侧 -->
              <van-icon
                v-show="$store.state.userInfo.name"
                @click="showAction(item.art_id)"
                name="cross"
              ></van-icon>

2.弹框整合

实现结构

如果一级界面进不去就去加载 二级界面在

 <!-- 弹框 -->
    <van-popup v-model = "isShowAction" :style = "{ width: '60%' }">
      <!-- 默认界面 -->
      <van-cell-group v-if = "!isShowTwo">
        <van-cell title = "不感兴趣" @click = "setDislike"/>
        <van-cell title = "反馈垃圾内容" is-link @click = "isShowTwo = true"/>
        <van-cell title = "拉黑作者"/>
      </van-cell-group>
      <!-- 二级界面 -->
      <van-cell-group v-else>
        <van-cell icon = "arrow-left" @click = "isShowTwo = false">返回</van-cell>
        <van-cell
          v-for = "item in reportList"
          :key = "item.type"
          :title = "item.title"
          @click = "reportArt(item.type)"
        />
      </van-cell-group>
    </van-popup>

3.反馈垃圾内容

        <van-cell title = "反馈垃圾内容" is-link @click = "isShowTwo = true"/>

将二级菜单的显示属性改为true,一级菜单的v-show绑定了取反的值,所以一级菜单自动隐藏<van-cell-group v-if = "!isShowTwo">

弹框中并不是一次性显示所有内容,会有显示的切换效果

需求:

  1. 点击反馈垃圾内容显示二级界面
  2. 点击返回,显示一级界面

步骤:

  1. data中定义布尔值
    1. isShowTwo:false
  2. 点击反馈垃圾
    1. 布尔值->true
  3. 点击返回
    1. 布尔值->false

结构显示的时机,点x调用方法

 <!-- 右侧 -->
              <van-icon v-show = "$store.state.userInfo.name"
                        @click = "showAction(item.art_id)"
                        name = "cross"></van-icon>

定义方法和保存文章id

// 显示 更多操作 弹框
    showAction (id) {
      // 弹框
      this.isShowAction = true
      // 保存文章id
      this.target = id
    },

弹框结构、逻辑

 <!-- 弹框 -->
    <van-popup v-model = "isShowAction" :style = "{ width: '60%' }">
      <!-- 默认界面 -->
      <van-cell-group v-if = "!isShowTwo">
        <van-cell title = "不感兴趣" @click = "setDislike"/>
        <van-cell title = "反馈垃圾内容" is-link @click = "isShowTwo = true"/>
        <van-cell title = "拉黑作者"/>
      </van-cell-group>
      <!-- 二级界面 -->
      <van-cell-group v-else>
        <van-cell icon = "arrow-left" @click = "isShowTwo = false">返回</van-cell>
        <van-cell
          v-for = "item in reportList"
          :key = "item.type"
          :title = "item.title"
          @click = "reportArt(item.type)"
        />
      </van-cell-group>
    </van-popup>

4.一级界面--不感兴趣功能

执行时机

<van-cell title = "不感兴趣" @click = "setDislike"/>

实现逻辑

// 设置不喜欢某篇文章
    setDislike () {
      disLikeArticle({ target: this.target }).then(res => {
        // console.log('res:', res)
        // 关闭弹框
        this.isShowAction = false
        // 删除本地的文章 art_id和target相等的那个 不需要
        this.list = this.list.filter(v => {
          if (v.art_id === res.data.data.target) {
            // 相等 就不需要 删除
            return false
          } else {
            return true
          }
        })
      })
    },

5.二级界面--垃圾内容的提交类型

实现结构

<van-cell
          v-for = "item in reportList"
          :key = "item.type"
          :title = "item.title"
          @click = "reportArt(item.type)"
 />

实现逻辑

// 举报文章
    reportArt (type) {
      // console.log('type:',type)
      reportArticle({
        target: this.target,
        type
      }).then(res => {
        // console.log('res:', res)
        // 提示用户
        this.$toast.success('举报成功!')
        // 关闭弹框
        this.isShowAction = false
        // 关闭二级页面显示
        this.isShowTwo = false
      })
    },

6.优化x在的点击

在不点user页面时加载x,获取用户的信息

7.detail页面

7.1文章跳转详情页

1.跳转详情页

实现结构

src/views/layout/home/articleList.vue

跳转的时候要携带文章的id

<van-cell
          v-for="item in list"
          :key="item.art_id"
          :title="item.title"
          @click="toDetail(item.art_id)"
 >

实现逻辑

 // 去看详情
    toDetail (id) {
      this.$router.push({
        path: '/article',
        query: {
          id
        }
      })
    },
2.修复BUG--跳转

在设置跳转详情页之后就没法弹出更多操作的框,不论点击什么地方都是跳转详情

BUG动画

3.复习v-on

通过添加stop阻止X的冒泡行为到父元素上

代码实现

@click.stop = "showAction(item.art_id)"

7.2详情页构建

在路由router中的layout的子组件中注册详情页组件

	//导入组件
	import article from '@/views/layout/article'

	//注册组件
	{
        path: 'article', // /article
        name: 'article',
        component: article
      }

抽取详情页api

src/api/article.js

// 导入request
import request from '../utils/request'

const getArticleDetail = ({ id }) => {
  return request({
    url: '/v1_0/articles/' + id,
    method: 'get'
  })
}

// 暴露出去
export { getArticleDetail }

详情页结构

<template>
  <div class = "article-container">
    <!-- 导航栏 -->
    <van-nav-bar
      fixed
      left-arrow
      @click-left = "$router.back()"
      title = "文章详情"
    ></van-nav-bar>
    <!-- /导航栏 -->

    <!-- 加载中 loading -->
    <van-loading class = "article-loading"/>
    <!-- /加载中 loading -->

    <!-- 文章详情 -->
    <div class = "detail">
      <h3 class = "title">标题</h3>
      <div class = "author">
        <van-image round width = "1rem" height = "1rem" fit = "fill"/>
        <div class = "text">
          <p class = "name">作者</p>
          <p class = "time">4天前</p>
        </div>
        <van-button round size = "small" type = "info">+ 关注</van-button>
      </div>
      <div class = "content">
        <!-- 直接解析html结构并渲染 -->
        <div v-html = "article.content">正文</div>
      </div>
      <van-divider>END</van-divider>
      <div class = "zan">
        <van-button
          round
          size = "small"
          hairline
          type = "primary"
          plain
          icon = "good-job-o"
        >点赞
        </van-button
        >
        &nbsp;&nbsp;&nbsp;&nbsp;
        <van-button
          round
          size = "small"
          hairline
          type = "danger"
          plain
          icon = "delete"
        >不喜欢
        </van-button
        >
      </div>
    </div>
    <!-- /文章详情 -->
  </div>
</template>

<script>
import { getArticleDetail } from '../../../api/article'

export default {
  name: 'article',
  data () {
    return {
      loading: true, // 控制加载中的 loading 状态
      article: {}
    }
  },
  created () {
    const id = this.$route.query.id
    console.log('id:', id)
    getArticleDetail({ id }).then(res => {
      console.log('res:', res)
      this.article = res.data.data
    })
  }
}
</script>

<style lang = "less">
.article-container {
  position: absolute;
  left: 0;
  top: 0;
  overflow-y: scroll;
  width: 100%;
  height: 100%;

  .article-loading {
    padding-top: 100px;
    text-align: center;
  }

  .van-nav-bar {
    background-color: #3296fa;

    .van-nav-bar__title {
      color: white;
    }
  }

  .error {
    padding-top: 100px;
    text-align: center;
  }

  .detail {
    padding: 50px 10px;

    .title {
      font-size: 16px;
    }

    .zan {
      text-align: center;
    }

    .author {
      padding: 10px 0;
      display: flex;

      .text {
        flex: 1;
        padding-left: 10px;
        line-height: 1.3;

        .name {
          font-size: 14px;
          margin: 0;
        }

        .time {
          margin: 0;
          font-size: 12px;
          color: #999;
        }
      }
    }

    .content {
      font-size: 14px;
      overflow: hidden;
      white-space: pre-wrap;
      word-break: break-all;

      ::v-deep img {
        max-width: 100%;
        background: #f9f9f9;
      }
    }
  }
}
</style>

7.3渲染详情页

1.loading效果的切换

实现结构

<van-loding class = "article-loading" v-if = "loading">

实现逻辑

​ 在data()中定义loading来控制加载中的状态

loading:true

​ 在渲染页面时在钩子函数created中定义

loding: false

2.文章作者信息的渲染

在钩子函数created中就返回的数据添加到article对象中,然后依次添加到对应的位置

<h3 class = "title">{{ article.title }}</h3>

 <p class = "name">{{ article.aut_name }}</p>
          <p class = "time">{{ article.pubdate | formatTime }}</p>
          
<van-button @click = "toggleFollow" round size = "small" type = "info">{{
            article.is_followed ? '已关注' : '+ 关注'
                                                                               }}
        </van-button>       

 <div v-html = "article.content">正文</div>

细节:根据返回的is_followed的布尔值来判断是否已经关注,通过三元表达式来显示对应的文字

3.修改关注的状态

​ 首先给关注按钮添加切换关注事件

<van-button @click = "toggleFollow"> </van-button>

​ 其次定义切换关注事件

toggleFollow(){
if(getToken()===null){
this.$toast.fail('请先登录')
return this.$router.push({path:'/login',query:{redirect:this.$route.fullPath}})
}

//有token的情况下判断是否订阅,未订阅的情况进入第一个if修改为订阅成功
if(this.article.is_followed === false){     			followUser({target:this.article.aut_id}).then(res=>{
          consoloe.log('res',res)
          this.$toast.success('关注成功')
          this.article.is_followed = !this.article.is_followed})}else {
    //有token并且已经关注,再点击就是取关
    //如果要调用取关的方法就要轻提示并且修改article.is_followed的状态
    									              unfollowUser({target:this.article.aut_id}).then(res=>{
        this.$toast.fail('取消关注成功')
        this.article.is_followed = !this.article.id_followed
                                                       
                                                        console.log('res',res)
    })
}
}
}
}                  
//调用关注方法的参数键值对是target:this.article.aut_id     


4.日期的渲染

​ 创建过滤器的组件再/filter/index.js中,

​ 在main.js中导入全局过滤器 import './filters'

​ 使用全局过滤器完成日期的处理

核心逻辑

//导入Vue框架
import Vue from 'vue'
//导入moment组件
import moment from 'moment'
//设置语言环境
moment.locale('zh-cn')
//注册全局的过滤器
Vue.filter('formatTime',value=>{
    //计算距今有多少时间
    return moment(value).fromNow})

image-20210618095952605

image-20210618100006394

image-20210618100012983

image-20210618100020145

  1. search页面

8.1 整合搜索页

search页面结构

<template>
  <div class="search-container">
    <!-- 搜索组件一级路由   $router.back()返回上一个页面-->
    <van-nav-bar
      left-arrow
      title="搜索中心"
      @click-left="$router.back()">
    </van-nav-bar>

    <!-- https://youzan.github.io/vant/#/zh-CN/search -->
    <van-search
      placeholder="请输入搜索关键词"
      shape="round"
      v-model.trim="keyword"
    >
      <template #action>
        <div>搜索</div>
      </template>
    </van-search>

    <!-- 联想建议 -->
    <van-cell-group>
      <van-cell title="js" icon="search" />
      <van-cell title="jsa" icon="search" />
    </van-cell-group>
    <!-- /联想建议 -->
  </div>
</template>

<script>
export default {
  name: 'search',
  data () {
    return {
      keyword: '' // 搜索关键字
    }
  }
}
</script>

<style lang="less">
.search-container {
  .van-nav-bar {
    background-color: #3296fa;
    .van-nav-bar__title {
      color: white;
    }
    .van-icon {
      color: white;
    }
  }
}
</style>

注册search组件

image-20210618112515199

从home页面跳转到search页面

绑定点击事件,设置路由的路径

image-20210618112520827

8.2 搜索推荐词--基本

vant-search组件

1.抽取search请求的api组件在src/api/search.js

// 导入请求对象
import request from '../utils/request'
// 抽取api - 获取搜索建议
const getSearchSuggestion = ({ q }) => {
  return request({
    url: '/v1_0/suggestion',
    method: 'get',
    params: {
      q // q:q
    }
  })
}
// 暴露
export { getSearchSuggestion }

2.实现逻辑

//导入请求推荐词的方法
import {getSearchSuggestion} from '../../../api/search'
//增加推荐词数组
options:[]
//注册方法
inputHandler (){
console.log('keywords',this.keyword)
    if(this.keyword === ''){
        //搜索的词是空,就清空推荐词数组
         this.option = []
   		 return} 
		}
	//如果搜索框的内容发生了改变并且内容不是空就调用获取搜索热词的方法
getSearchSuggestion({q:this.keyword}).then(res=>{
        console.log('res',res)
 		this.options = res.data.data.options})
}

//如果没有输入内容就清空推荐词数组
//调用导入的方法,获取推荐词数据

3.实现结构

//注册事件,input事件一直触发,onchange事件触发频率相比较而言会低一些,只有在失去焦点的时候才会触发
@input = "inputHandler"

//循环搜索的结果现在van-cell组件中
v-for = "(item,index) in options"
:key = "index"//v-for必须设置唯一的数值用数组的下标记录
:title = "item"//数组元素的内容设置给显示的标题
icon = "search"//设置搜索的图标

8.3 搜索推荐词--高亮

8.4 搜索推荐优化--防抖

8.4 搜索推荐优化--节流

10.相关知识点

1.具名插槽

2.axios-create创建实例

​ 通过axios-create创建实例来抽取api

this.$axios的方式调用接口会有两个问题

​ 1.没有办法在审查代码时立即定位

​ 2.修改参数、地址比较麻烦,不利于后期的维护

抽取案例

image-20210607110708642

image-20210607110712757

image-20210607110717877

3.轻提示toast

轻提示

4.vuex的基本使用

Vuex是什么:

​ Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

image-20210609153404883

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用

基础使用

​ 1.在main.js中声明以store仓库,通过Vue的store方法,传入参数对象count,这样就可以在main.js之外的页面使用count

  <div class = "info-box">
      <van-image
        class = "my-image"
        round
        :src = "userInfo.photo"
      />
      <h2 class = "name">
        {{ userInfo.name }}
        <br>
        <van-tag color = "#fff" text-color = "#3296fa" type = "primary"> {{ userInfo.birthday }}</van-tag>
      </h2>
    </div>

在其他页面通过语句

this.$store.state.xxx修改和获取数据

5.嵌套路由

目的:为了搭建更为复杂的项目

语法:

  1. /login登录页
  2. /home首页
    1. /home/index
    2. /home/news
    3. /home/vip
    4. /home/hots
      1. router-view
// 嵌套路由的规则
import index from '../views/home/index/index.vue'
import news from '../views/home/news'
import vip from '../views/home/vip'
import hots from '../views/home/hots'

Vue.use(VueRouter)

const routes = [
  {
    path: '/login',
    name: 'login',
    component: login
  },
  {
    path: '/home',
    name: 'home',
    component: home,
      //通过children对象来设置嵌套路由的地址
    children: [
      {
        path: 'index', // /home/index
        component: index
      },
      {
        path: 'news', // /home/news
        component: news
      },
      {
        path: 'vip', // /home/vip
        component: vip
      },
      {
        path: 'hots', // /home/hots
        component: hots
      }
    ]
  }
]

const router = new VueRouter({
  routes
})

export default router

6.路由元信息

Router中的meta

是一个对象

与path、name、component同级

image-20210610160343617

image-20210610160322106

7.导航守卫

Router中的,

不止一个导航守卫

三个参数:

​ 1.to去的路由信息

​ 2.from来的路由信息

​ 3.next()是否继续执行

语法

const router = new VueRouter({
  routes
})

// 添加全局前置守卫
router.beforeEach((to, from, next) => {
  // to 即将到达的路由信息 this.$route获取到的是一致的
  console.log('to:', to)
  // from 离开的路由信息 this.$route获取到的是一致的
  console.log('from:', from)
  // console.log('next:', next)
  // 不执行next 卡在这个地方 路由的切换就有问题啦!
  // next(),类似于node中的中间件,如果没有next到这就停了
  // 如果你要去的就是 404 直接放走
  if (to.path === '/404') {
    // 直接放走
    next()
  } else {
    // 除他之外 去404
    // 直接跳转到指定页面
    next({ path: '/404' })
  }
})

导航守卫有多个,不止是beforeEach()

8.vuex的mutations

主要作用是修改数据

this.$store.state.xxx取值/赋值

image-20210611090156488

这种方法可以修改,但是插件无法捕捉,不方便调试和修改

2.组件中调用这个方法

this.$store.commit('mutations',参数)取值/赋值

实现原理

image-20210611090536460

setFood要和mutation中一样,state是被修改的数据,setFood方法内将传进的数据参数对原始state数据进行赋值修改

类似于

​ this.$emit()

9.Vuex的mapState辅助函数

意义:方便计算属性的定义取值

作用:

​ 1.自动生成计算属性:this.$store.state.userInfo

​ 2.直接通过mapState({'userInfo'})获取仓库的数据

​ 返回值:

​ 4.返回的数据结构:

​ userInfo:function(){

},函数

​ {userIfo:function(){return this.$store.state.userInfo}}

展开了对象{food:f}

food:function(){}

image-20210611181546529

辅助函数的两种方法

1.有自己的数据要绑定,通过展开运算符

computed: {
    //本地的计算属性
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

2.没有自己的数据直接使用

computed: mapState([
  // 映射 this.count 为 store.state.count
  'state中保存的名字'
])

10.axios的拦截器

逻辑关系

image-20210611162930413

实现代码

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

11.点击user无效

在晚上点击user按钮之后没有任何的反应(一段时间之后会报错),首先是服务器的响应失效了。

没有任何反应的原因是,在单击我的之后。user的vue页面一直在发送请求但是没有请求到数据,

根本原因页面有四种方法到指定页面但是都不符合

​ 1.不要登陆(不符合)

​ 2.有请求的用户信息名称(不符合)

​ 3.没有token next跳转到login(不符合)

​ 4.定义的方法没有调用所以不饿能next(不符合)

12.axios上传文件

上传组件并不会自己上传文件,需要我们自己编写逻辑,那么axios,如何上传文件呢?

传送门:axios请求配置

传送门:mdn-FormData

传送门:mdn-FormData.append

语法:

  1. 直接把FormData设置给data即可

  2. FormData使用

    1. const formData = new FormData(可选的form表单)
      1. 可选:传入form表单
    2. formData.append('key名',key值)
    3. 直接提交到服务器即可
  3. axios中直接把formData设置给data即可

13.vue-cropper

头像裁切组件

npm i vue-cropper

<vueCropper
  ref="cropper"
  :img="option.img"
  :outputSize="option.size"
  :outputType="option.outputType"
></vueCropper>

14.FormData的使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      form {
        border: 1px solid #000;
        width: 200px;
      }
    </style>
  </head>
  <body>
    <form class="form">
      <input type="text" name="username" />
      <br />
      <input type="password" name="password" />
      <br />
      <input type="file" name="icon" />
      <br />
      <button class="getValue">获取value</button>
    </form>
    <script>
      document.querySelector('.getValue').onclick = function (e) {
        e.preventDefault()
        // FormData 可以自动获取form中的所有有 name属性的表单元素的值
        // FormData 带着键值对,name:值
        const formData1 = new FormData(document.querySelector('.form'))
        console.log('formData1:', formData1)
        // 无法直接看到 需要通过get方法看到
        console.log(formData1.get('username'))
        console.log(formData1.get('password'))
        console.log(formData1.get('icon'))
        // 结合jQ使用的注意点
        /*
          contentType不设置任何额外的请求头
          processData 不处理数据
          必须去官方文档,才可以看到这个说明
        */
        $.ajax({
          url: 'xxx',
          method: 'post',
          contentType: false,
          processData: false,
          data: formData1
        })
      }
    </script>
  </body>
</html>

Ajax发起FormData,两个false对应

不设置请求头

不不处理数据

image-20210615151010880

15.Vant--list组件

1.基础用法

<template>
  <div>
    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
      <!-- 列表组件 -->
      <van-list
        v-model="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onLoad"
      >
        <!-- 结合数组 循环成列表 -->
        <van-cell v-for="item in list" :key="item" :title="item" />
      </van-list>
    </van-pull-refresh>
  </div>
</template>

<script>
export default {
  data () {
    return {
      // 列表数据
      list: [],
      // 是否正在加载
      loading: false,
      // 是否加载完毕
      finished: false,
      // 是否正在下拉刷新
      refreshing: false
    }
  },
  methods: {
    onRefresh () {
      // 清空列表数据
      this.list = []
      this.finished = false

      // 重新加载数据
      // 将 loading 设置为 true,表示处于加载状态
      this.loading = true
      this.onLoad()
    },
    /*
      1. 通过执行onLoad让数据填满页面
      2. 填满之后不加载了,直到触底再去加载 onload-->true
      3. 加载完成之后,认为的变为false,直到再次触底加载
    */

    onLoad () {
      console.log('onload触发啦!')
      // 异步更新数据
      // setTimeout 仅做示例,真实场景中一般为 ajax 请求
      setTimeout(() => {
        for (let i = 0; i < 10; i++) {
          this.list.push(this.list.length + 1)
        }

        // 加载状态结束
        this.loading = false

        // 数据全部加载完成
        if (this.list.length >= 40) {
          this.finished = true
        }
        // 下拉的动画变为false
        this.refreshing = false
      }, 1000)
    }
  }
}
</script>

<style></style>

2.list示例笑话

<template>
  <div>
    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
      <van-list
        v-model="loading"
        :finished="finished"
        finished-text="没
      有更多了"
        @load="onLoad"
      >
        <van-cell v-for="item in list" :key="item" :title="item" />
      </van-list>
    </van-pull-refresh>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data () {
    return {
      // 列表
      list: [],
      // 是否正在加载中
      loading: false,
      // 是否加载完毕
      finished: false,
      // 是否下拉刷新
      refreshing: false
    }
  },
  methods: {
    onRefresh () {
      // 清空列表数据
      this.finished = false

      // 重新加载数据
      // 将 loading 设置为 true,表示处于加载状态
      this.loading = true
      this.onLoad()
    },
    onLoad () {
      // 异步更新数据
      axios({
        url: 'https://autumnfish.cn/api/joke/list',
        method: 'get',
        params: {
          num: 5
        }
      }).then(res => {
        console.log('res:', res)
        // 如果是下拉刷新 清空数据
        if (this.refreshing === true) {
          // 清空
          this.list = []
          // 还原下拉状态
          this.refreshing = false
        }
        // 还原加载状态
        this.loading = false
        this.list.push(...res.data.jokes)
      })
    }
  }
}
</script>

<style></style>

16代码片段和ES

image-20210616110732827

这个文件可以ES的规则,一般是leader规定

image-20210616110718232

17.moment组件配合vue过滤器

vue过滤器

{{ message | filterA | filterB }}

将信息通过函数A和函数B依次进行了过滤

示例图片

image-20210616144320906

实现结构

<!-- //管道符-->
                <span>{{ item.pubdate | formatTime }}</span>

定义过滤器函数,与methods同级

filters: {
    formatTime (value) {
      return moment(value).fromNow()
    }
  }

拓展

过滤器是 JavaScript 函数,因此可以接收参数:

{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

1.格式化日期时间

2.国际化转译中文

3.拓展i8n

internationalation是i8n的缩写单词

moment.locale('zh-cn')

image-20210616150423114

image-20210616150428770

18.懒加载

图片每看到的时候时并没有加载的必要,可以使用懒加载功能来搞定他哦

Vue官方懒加载

传送门:Vant组件的lazy-load

src/utils/vant.js,整合懒加载组件

import Vue from 'vue'
import Vant, { Lazyload } from 'vant'
import 'vant/lib/index.css'

Vue.use(Lazyload)
// vant和vue关联起来
Vue.use(Vant)

在结构处使用

<van-image
                  lazy-load
                  fit = "cover"
                  :src = "it"
/>

19.全局过滤器

将filter定义在 src/filters/index.js

实现代码

// 导入Vue
import Vue from 'vue'
// 导入moment
import moment from 'moment'
// 设置语言环境
moment.locale('zh-cn')
// 注册全局过滤器
Vue.filter('formatTime', value => {
  // 计算日期距今多久
  return moment(value).fromNow()
})

.websocket

.soket.io

posted @ 2021-06-14 12:11  Mzs-Qsy  阅读(118)  评论(0编辑  收藏  举报