拉钩教育移动端项目实战 - 登录、用户功能

344 阅读1分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

登录用户功能

登录模块

未登录点击学习和我时会跳转到登录页面让用户登录之后才能访问

导航布局

顶部导航栏使用Vant中的导航栏组件

这里返回按钮可以使用this.$router.go(-1)方法处理

src/views/login/index.vue  添加导航栏模块,处理返回按钮

<template>
  <div class="login">
    <!-- 顶部导航栏 -->
    <van-nav-bar
      title="标题"
      left-text="返回"
      left-arrow
      @click-left="onClickLeft"
    />
  </div>
</template>

<script>
export default {
  name: 'login',
  methods: {
    // 导航栏返回事件函数
    onClickLeft () {
      // 使用 go 方法实现返回上一页
      this.$router.go(-1)
    }
  }
}
</script>

表单布局

登录表单可以使用Vant的表单组件

可以直接把校验规则写在结构内部

src/views/login/index.vue  添加登陆表单,设置校验规则

<template>
  <div class="login">
    <!-- 顶部导航栏 -->
    <van-nav-bar
      title="标题"
      left-text="返回"
      left-arrow
      @click-left="onClickLeft"
    />
    <!-- 登陆表单 rules为校验规则,只有全部规则满足才能触发 submit -->
    <van-form @submit="onSubmit">
      <!-- 手机号 -->
      <van-field
        v-model="form.username"
        name="手机号"
        label="手机号"
        placeholder="手机号"
        :rules="[
          { required: true, message: '请填写手机号码' },
          { pattern: /^1\d{10}$/, message: '手机号格式错误,请重新输入' }
        ]"
      />
      <!-- 密码框 -->
      <van-field
        v-model="form.password"
        type="password"
        name="密码"
        label="密码"
        placeholder="密码"
        :rules="[
          { required: true, message: '请填写密码' },
          { pattern: /^[0-9a-zA-Z]{6,12}$/, message: '密码格式错误,请输入6-12位密码' }
          ]"
      />
      <!-- 登陆按钮 -->
      <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',
  data () {
    return {
      // 表单数据
      form: {}
    }
  },
  methods: {
    // 导航栏返回事件函数
    onClickLeft () {
      // 使用 go 方法实现返回上一页
      this.$router.go(-1)
    },
    // 表单登陆事件函数
    onSubmit () {
      console.log('登陆啦')
    }
  }
}
</script>

接口封装

使用登录接口

移动端可以使用new URLSearchParams(data).toString()将对象参数转换为url格式

URLSearchParams可以处理URL字符串,新功能

添加提示信息提示是否登陆成功

// src/api/user.js  新建文件封装用户相关接口

// 引入axios实例
import axios from './axios'

// 用户登录
export const login = data => {
  return axios({
    method: 'post',
    url: '/front/user/login',
    data: new URLSearchParams(data).toString()
  })
}
src/views/login/index.vue  使用用户登陆接口

..................
<script>
// 引入接口
import { login } from '@/api/user'
export default {
  ...................
  methods: {
    ....................
    // 表单登陆事件函数
    async onSubmit () {
      // 调用接口
      const { data } = await login(this.form)
      if (data.state === 1) {
        // 如果接口请求成功弹出提示(暂时先不存储数据)
        this.$toast.success('登陆成功')
      } else {
        this.$toast.fail('登录失败')
      }
    }
  }
}
</script>

避免重复请求

利用按钮组件额的加载状态,避免短时间重复请求

src/views/login/index.vue  给登录按钮添加loading属性,绑定数据

<van-button :loading="loading" round block type="info" native-type="submit">提交</van-button>

登录请求接口之前设置为true,登录无论是否成功都设置回false

存储登录状态

使用VueX功能存储登录状态

登陆成功后将返回的数据利用vuex的功能添加进去(注意转换为对象)

将数据存在本地,避免刷新后丢失数据

  • 写入:window.localStorage.setItem(名字,数据)
  • 读取:window.localStorage.getItem('名字')
// src/views/login/index.vue  存储用户登录信息

// 表单登陆事件函数
    async onSubmit () {
      // 请求接口之前让登录按钮处于加载状态
      this.loading = true
      // 调用接口
      const { data } = await login(this.form)
      if (data.state === 1) {
        // 登录成功调用vuex方法存储登录信息!!!!!!!!!!!!!!!!!!
        this.$store.commit('setUser', data.content)
        // 如果接口请求成功弹出提示(暂时先不存储数据)
        this.$toast.success('登陆成功')
      } else {
        this.$toast.fail('登录失败')
      }
      // 请求完成后不管成功还是失败,都需要将按钮取消加载状态
      this.loading = false
    }
// src/store/index.js  vuex添加用户信息属性,设置方法作本地数据持久化

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 用户登录信息,读取本地持久化数据,并且转换为对象格式
    user: JSON.parse(window.localStorage.getItem('lagou-edu-mobile'))
  },
  mutations: {
    // 存储用户登录信息
    setUser (state, data) {
      // 设置user,否则user不刷新不会自动读取localStorage
      state.user = JSON.parse(data)
      // 将登录心心做本地持久化
      window.localStorage.setItem('lagou-edu-mobile', data)
    }
  },
  actions: {
  },
  modules: {
  }
})

登录状态监测

访问需要登录的页面需要校验是否登录,使用导航守卫,添加路由元信息meta: {requiresAuth: true}

判断访问页面是否需要要验证to.marched.some(record => record.meta.requiresAuth)判断元数据属性是否有这个属性

需要验证判断是否已经登陆,否则跳转login

在跳转登陆之前记录一下信息,登录又跳转回指定页面

src/router/index.js  给路由增加元信息,并且设置导航守卫

................
{ // 学习页面
    name: 'study',
    path: '/study',
    component: () => import(/* webpackChunkName: 'study' */'@/views/study'),
    // 添加路由元信息,设置需要判断是否登录的标记
    meta: { login: true }
  },
  { // 用户页面
    name: 'user',
    path: '/user',
    component: () => import(/* webpackChunkName: 'user' */'@/views/user'),
    // 添加路由元信息,设置需要判断是否登录的标记
    meta: { login: true }
  },
 ...........  
// 导航守卫
router.beforeEach((to, from, next) => {
  // 判断路由的元信息中是否有需要校验是否登录的标记
  if (to.matched.some(record => record.meta.login)) {
    // 如果需要判断再判断是否登录
    if (!Store.state.user) {
      // 未登录跳转到登陆页,并且把原本要访问的路由带上
      next({
        path: '/login',
        query: { path: to.fullPath }
      })
    } else {
      // 已经登陆直接放行
      next()
    }
  } else {
    // 不需要判断的直接放行
    next()
  }
})

通过请求拦截器设置token

在发送请求之前添加token,使用vuex数据

如果有user并且有token则将token设置给请求头

// src/api/axios.js  设置请求拦截器,再请求之前添加请求头

// 引入axios插件
import axiosPlug from 'axios'
// 引入vuex
import Store from '@/store'

// 创建axios实例
const axios = axiosPlug.create({
  // 设置url前缀
  baseURL: 'http://edufront.lagou.com'
})

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 设置请求头,判断是否登录,登录就带上请求头
  if (Store.state.user) config.headers.Authorization = Store.state.user.access_token || ''
  // 返回新的配置信息
  return config
}, function (error) {
  // 抛出错误
  return Promise.reject(error)
})

// 导出实例
export default axios

刷新token

判断当前token是否已经过期,所以我们需要刷新token

设置相应拦截器,成功不需要处理,所以我们需要设置失败的处理函数

判断是否返回了401,如果返回401说明未授权,而未授权又分为两种情况

  • 根本没有登录:直接抛出错误
  • token过期:更新token - 使用刷新token接口,判断是否能刷新,刷新成功把新数据更新,如果请求失败直接清除本地存储

存在多个请求的情况需要(添加刷新标记)将多个请求存储起来,刷新token后一起发送,请求信息存在error.config中,别忘了触发token刷新的请求

未登录和无法刷新token可以直接跳转回login,记得带参数

// src/api/axios.js  设置请求拦截器,刷新token或者返回登录

// 引入axios插件
import axiosPlug from 'axios'
// 引入vuex
import Store from '@/store'
// 引入router,后面会有跳转
import router from '@/router'

// 创建axios实例
const axios = axiosPlug.create({
  // 设置url前缀
  baseURL: 'http://edufront.lagou.com'
})

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 设置请求头,判断是否登录,登录就带上请求头
  if (Store.state.user) config.headers.Authorization = Store.state.user.access_token || ''
  // 返回新的配置信息
  return config
}, function (error) {
  // 抛出错误
  return Promise.reject(error)
})

// 信号值,是否正在刷新token
let uploading = false
// 除刷新token的请求外,其他请求全部暂时存储起来
let ajax = []

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 响应成功什么都不做
  return response
}, async error => {
  // 如果有响应
  if (error.response) {
    // 判断返回状态码是否是401
    if (error.response.status === 401) {
      // 判断用户是否登录
      if (!Store.state.user) {
        // 用户未登录,直接带参数返回登录
        router.push({
          name: 'login',
          query: {
            path: router.currentRoute.fullPath
          }
        })
        console.log('用户未登录,返回登陆')
        return Promise.reject(error)
      }
      // 判断是否正在刷新token
      if (uploading) {
        // 正在刷新token将剩下的请求全部挂起保存起来
        return ajax.push(() => {
          axios(error.config)
        })
      }
      // 没刷新token则更改状态为刷新
      uploading = true
      // 尝试刷新token
      const { data } = await axios({
        method: 'post',
        url: '/front/user/refresh_token',
        data: new window.URLSearchParams({
          refreshtoken: Store.state.user.refresh_token
        }).toString()
      })
      if (data.state !== 1) {
        // 刷新token失败,清空用户信息
        Store.commit('setUser', null)
        // 带参数返回登录
        router.push({
          name: 'login',
          query: {
            path: router.currentRoute.fullPath
          }
        })
        console.log('更新失败')
        return Promise.reject(error)
      }
      // 刷新token成功
      console.log('更新成功')
      // 保存新的登录信息
      Store.commit('setUser', data.content)
      // 将之前挂起的请求重新发送
      ajax.forEach(fn => fn())
      // 清空请求
      ajax = []
      // 修改状态为未刷新
      uploading = false
      // 最后重新发送本次请求
      return axios(error.config)
    }
  }
  return Promise.reject(error)
})

// 导出实例
export default axios

用户模块

用户信息处理

使用公共导航栏组建

顶部整体可以 使用Vant的单元格组件,只需要修改内部内容即可

// src/api/user.js 添加获取用户信息接口

// 获取用户基本信息
export const getInfo = () => {
  return axios({
    method: 'get',
    url: '/front/user/getInfo'
  })
}
src/views/user/index.vue 使用公共组件,设置顶部用户信息

<template>
  <div>
    <van-cell-group>
      <!-- 顶部用户信息单元格,绑定数据 -->
      <van-cell class="user-info">
        <!-- 头像 -->
        <van-image
          round
          width="50px"
          height="50px"
          :src="info.portrait"
        />
        <!-- 文字信息 -->
        <div class="user-info-text">
          <h3>{{info.userName}}</h3>
          <p><van-icon name="edit"/> 编辑个人资料</p>
        </div>
      </van-cell>
    </van-cell-group>
    <!-- 使用子组件 -->
    <foot-bar></foot-bar>
  </div>
</template>

<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
  name: 'User',
  data () {
    return {
      // 用户信息数据
      info: {}
    }
  },
  created () {
    // 初始化数据
    this.getUserInfo()
  },
  methods: {
    // 调用接口获取用户数据
    async getUserInfo () {
      const { data } = await getInfo()
      // 把获取到的数据写进data
      this.info = data.content
    }
  },
  // 组件注册
  components: {
    FootBar
  }
}
</script>

<style lang="scss" scoped>
// 顶部用户信息
.user-info{
  padding: 0;
  .van-cell__value {
    display: flex;
    padding: 30px 20px 0;
    background: #f89704;
    .user-info-text {
      flex: 1;
      margin-left: 15px;
      h3 {
        margin: 5px;
        font-size: 18px;
      }
      p {
        margin: 5px;
      }
    }
  }
}
</style>

账户信息处理

使用Vant的宫格组件,依然写在vant单元格里面

src/views/user/index.vue  添加账户信息

<template>
  <div>
    <van-cell-group>
      <!-- 顶部用户信息单元格,绑定数据 -->
      <van-cell class="user-info">
        <!-- 头像 -->
        <van-image
          round
          width="50px"
          height="50px"
          :src="info.portrait"
        />
        <!-- 文字信息 -->
        <div class="user-info-text">
          <h3>{{info.userName}}</h3>
          <p><van-icon name="edit"/> 编辑个人资料</p>
        </div>
      </van-cell>
      <van-cell  class="user-account">
        <!-- 账户信息 - 宫格组件 -->
        <van-grid>
          <!-- 使用v-for创建结构 -->
          <van-grid-item v-for="item in account" :key="item.id">
            <p class="num">{{item.num}}</p>
            <p>{{item.title}}</p>
          </van-grid-item>
        </van-grid>
      </van-cell>
    </van-cell-group>
    <!-- 使用子组件 -->
    <foot-bar></foot-bar>
  </div>
</template>

<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
  name: 'User',
  data () {
    return {
      // 用户信息数据
      info: {},
      // 账户信息数据
      account: [
        { id: 0, num: 14.05, title: '学习时长' },
        { id: 1, num: 200, title: '钱包/勾豆' },
        { id: 2, num: 1, title: '优惠券' },
        { id: 3, num: 213, title: '学分' }
      ]
    }
  },
  created () {
    // 初始化数据
    this.getUserInfo()
  },
  methods: {
    // 调用接口获取用户数据
    async getUserInfo () {
      const { data } = await getInfo()
      // const { data2 } = await getInfo()
      // const { data3 } = await getInfo()
      // const { data4 } = await getInfo()
      // return data + data2 + data3 + data4
      // 把获取到的数据写进data
      this.info = data.content
    }
  },
  // 组件注册
  components: {
    FootBar
  }
}
</script>

<style lang="scss" scoped>
// 顶部用户信息
.user-info{
  padding: 0;
  .van-cell__value {
    display: flex;
    padding: 30px 20px 0;
    background: #f89704;
    .user-info-text {
      flex: 1;
      margin-left: 15px;
      h3 {
        margin: 5px;
        font-size: 18px;
      }
      p {
        margin: 5px;
      }
    }
  }
}
// 账户信息样式
.user-account {
  margin-top: -1px;
  padding: 10px 16px;
  background: #f89704;
  .van-grid {
    border-radius: 10px;
    overflow: hidden;
    p {
      margin: 0;
    }
    .num {
      font-size: 22px;
      font-weight: 700;
    }
  }
}
</style>

底部菜单列表

直接使用单元格组件罗列起来即可

src/views/user/index.vue 书写底部列表结构,使用v-for

<template>
  <div>
    <van-cell-group>
      <!-- 顶部用户信息单元格,绑定数据 -->
      <van-cell class="user-info">
        <!-- 头像 -->
        <van-image
          round
          width="50px"
          height="50px"
          :src="info.portrait"
        />
        <!-- 文字信息 -->
        <div class="user-info-text">
          <h3>{{info.userName}}</h3>
          <p><van-icon name="edit"/> 编辑个人资料</p>
        </div>
      </van-cell>
      <van-cell  class="user-account">
        <!-- 账户信息 - 宫格组件 -->
        <van-grid>
          <!-- 使用v-for创建结构 -->
          <van-grid-item v-for="item in account" :key="item.id">
            <p class="num">{{item.num}}</p>
            <p>{{item.title}}</p>
          </van-grid-item>
        </van-grid>
      </van-cell>
      <!-- 底部列表,使用v-for创建结构 -->
      <van-cell v-for="item in list" :key="item.id" :icon="item.icon" :value="item.value" :title="item.title" is-link />
    </van-cell-group>
    <!-- 使用子组件 -->
    <foot-bar></foot-bar>
  </div>
</template>

<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
  name: 'User',
  data () {
    return {
      // 用户信息数据
      info: {},
      // 账户信息数据
      account: [
        { id: 0, num: 14.05, title: '学习时长' },
        { id: 1, num: 200, title: '钱包/勾豆' },
        { id: 2, num: 1, title: '优惠券' },
        { id: 3, num: 213, title: '学分' }
      ],
      // 底部列表数据
      list: [
        { id: 0, icon: 'location-o', value: '收益200元', title: '分销中心' },
        { id: 1, icon: 'location-o', value: '', title: '个性化设置' },
        { id: 2, icon: 'location-o', value: '', title: '我的下载' },
        { id: 3, icon: 'location-o', value: '', title: '帮助与反馈' },
        { id: 4, icon: 'location-o', value: 'v2.0.0', title: '关于拉勾教育' }
      ]
    }
  },
  created () {
    // 初始化数据
    this.getUserInfo()
  },
  methods: {
    // 调用接口获取用户数据
    async getUserInfo () {
      const { data } = await getInfo()
      // const { data2 } = await getInfo()
      // const { data3 } = await getInfo()
      // const { data4 } = await getInfo()
      // return data + data2 + data3 + data4
      // 把获取到的数据写进data
      this.info = data.content
    }
  },
  // 组件注册
  components: {
    FootBar
  }
}
</script>

<style lang="scss" scoped>
// 顶部用户信息
.user-info{
  padding: 0;
  .van-cell__value {
    display: flex;
    padding: 30px 20px 0;
    background: #f89704;
    .user-info-text {
      flex: 1;
      margin-left: 15px;
      h3 {
        margin: 5px;
        font-size: 18px;
      }
      p {
        margin: 5px;
      }
    }
  }
}
// 账户信息样式
.user-account {
  margin-top: -1px;
  padding: 10px 16px;
  background: #f89704;
  .van-grid {
    border-radius: 10px;
    overflow: hidden;
    p {
      margin: 0;
    }
    .num {
      font-size: 22px;
      font-weight: 700;
    }
  }
}
</style>

封装接口与数据绑定

使用用户基本信息接口,获取然后绑定数据

上面已经进行了绑定,这里不用再写了