[TOC]

导航布局

  • src/views/login/index.vue ```vue

    
    <a name="2wGH0"></a>
    ## 表单布局与校验
    
    - `src/views/login/index.vue`
    ```vue
    <template>
      <div class="login">
        <!-- 顶部导航 -->
    
        <!-- 登陆表单 -->
        <van-form @submit="onSubmit">
          <van-field
          v-model="form.phone"
          name='phone'
          label='手机号'
          placeholder='请输入手机号'
          :rules="[{
            required: true,
            message: '请填写手机号'
          }, {
            validator: phoneCheck,
            message: '格式有误,请重新输入'
          }]"></van-field>
          <van-field
          v-model="form.password"
          type='password'
          name='密码'
          label='密码'
          palceholder='请输入密码'
          :rules="[{
            required: true,
            message: '请填写密码'
          }, {
            validator: passwordCheck,
            message: '格式有误,请重新输入'
          }]"></van-field>
          <div style="margin:16px">
            <van-button
             round 
             block
             type='info'
             native-type='submit'
             :loading='isLoading'>登陆</van-button>
          </div>
        </van-form>
      </div>
    </template>
    <script>
    export default {
      name: 'Login',
      data () {
        return {
          // 登陆表单数据
          form: {
            phone: '17201234567',
            password: 'qsryja'
          },
          // 登陆按钮加载中状态
          isLoading: false
        }
      },
      methods: {
        // 手机号校验
        phoneCheck (value) {
          return /^1\d{10}$/.test(value)
        },
        // 密码校验
        passwordCheck (value) {
          return /^[a-zA-Z0-9]{6,12}$/.test(value)
        },
        // 提交
        onSubmit () {
          this.$toast('登陆成功')
        },
    
      }
    }
    </script>
    <style lang="scss" scoped>
    
    </style>
    

    接口封装

    • src/services/user.js ```javascript import request from ‘@/utils/request’

    // 用户登陆 export const login = data => { return request({ method: ‘POST’, url: ‘/front/user/login’, data: new URLSearchParams(data).toString() //转换参数格式 {key:val} => ‘key=val’ }) }

    <a name="5Twu0"></a>
    ## 存储登陆状态
    
    - `src/store/index.js`
    ```javascript
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        // 存储登陆用户数据,尝试读取本地存储数据
        user: JSON.parse(window.localStorage.getItem('lagouedu-mobile-user') || null)
      },
      mutations: {
        // 用于修改 user
        setUser (state, payload) {
          // payload 请求到的用户数据,JSON 格式
          state.user = JSON.parse(payload)
          // 将数据设置到本地存储,
          window.localStorage.setItem('lagouedu-mobile-user', payload)
        }
      },
      actions: {
      },
      modules: {
      }
    })
    
    • src/views/login/index.vue

      <script>
      import { login } from '@/services/user'
      export default {
      
      methods: {
      
        // 提交
        async onSubmit () {
          this.isLoading = true
          const { data } = await login(this.form)
          // 登陆成功
          if (data.state === 1) {
            // 将数据保存到本地存储中
            this.$store.commit('setUser', data.content)
            // 提示,$toast 也可以通过内部方法触发不同提示效果,使用方式类似 element-ui
            this.$toast.success('登陆成功')
          } else {
            this.$toast.fail('登陆失败')
          }
          this.isLoading = false
        },
      
      }
      }
      </script>
      

      登陆状态检测

    • src/router/index.js ```javascript

    const routes = [

    // 学习页面 { path: ‘/learn’, name: ‘learn’, component: () => import(/ webpackChunkName: ‘learn’ / ‘@/views/learn’), meta: { requiresAuth: true } // 标志需要进行登陆验证 }, // 用户页 { path: ‘/user’, name: ‘user’, component: () => import(/ webpackChunkName: ‘login’ / ‘@/views/user’), meta: { requiresAuth: true } },

    ]

    const router = new VueRouter({ routes })

    // 导航守卫进行登陆检测与跳转 router.beforeEach((to, form, next) => { // 验证 to 路由是否需要进行身份认证 if (to.matched.some(record => record.meta.requiresAuth)) { // 验证 Vuex 的 store 中的用户登陆信息是否存储 if (!store.state.user) { // 未登陆, 跳转到登陆页 return next({ name: ‘login’, query: { // 将本次路由的 fullPath 传递给login 页面 redirect: to.fullPath } }) } // 已登陆 next() } else { next() } }) export default router

    
    - `src/views/login/index.vue`
    ```vue
    <script>
      // 提交
        async onSubmit () {
    
          // 登陆成功
          if (data.state === 1) {
    
            // 跳转
            this.$router.push(this.$route.query.redirect || '/')
          } else {
    
          }
    
        },
    </script>
    

    通过请求拦截器设置 Token

    • src/utils/requrest.js ```javascript

    import store from ‘@/store’

    // 设置请求拦截器进行接口鉴权 request.interceptors.request.use(config => { // 读取 store 中存储的 user 数据 const { user } = store.state // 检测 user 是否存在数据,如果有,则进行 token 设置 if (user && user.access_token) { config.headers.Authorization = user.access_token }

    return config })

    export default request

    
    <a name="1qjmy"></a>
    ## 刷新 Token
    
    - `src/utils/request.js`
    ```javascript
    import axios from 'axios'
    import store from '@/store'
    import router from '@/router'
    
    // 设置请求拦截器进行接口鉴权
    
    // 封装函数,用于跳转登陆页
    function redirectLogin () {
      router.push({
        name: 'login',
        query: {
          redirect: router.currentRoute.fullPath
        }
      })
    }
    // 存储是否正在更新 Token 的状态
    let isRefreshing = false
    // 存储因为等待 Token 刷新而挂起的请求
    let requests = []
    // 响应拦截器
    request.interceptors.response.use(function (response) {
      // 成功
      return response
    }, function (error) {
      if (error.response) {
        const { status } = error.response
        if (status === 400) {
    
        } else if (status === 401) {
          if (!store.state.user) {
            // 没有登陆信息
            redirectLogin()
            return Promise.reject(error)
          }
          // 检测是否已经存在了正在刷新 Token 的请求
          if (isRefreshing) {
            // 将当前失败的请求,存储到请求列表中
            return requests.push(() => {
              // 当前函数调用后,会自动发送本次失败的请求
              request(error.config)
            })
          }
          isRefreshing = true
          // 发送请求,获取新的 access_token
          return request({
            method: 'POST',
            url: '/front/user/refresh_token',
            data: new window.URLSearchParams({
              refreshtoken: store.state.user.refresh_token
            }).toString()
          }).then(res => {
            // 刷新token 失败
            if (res.data.state !== 1) {
              // 清除无效的用户信息
              store.commit('setUser', null)
              redirectLogin()
              return Promise.reject(error)
            }
            // 刷新 token 成功
            // 存储新的 token
            store.commit('setUser', res.data.content)
            // 重新发送失败的请求 (根据 requests 发送所有失败的请求)
            requests.forEach(callback => callback())
            // 发送完毕,清除 requests 内容即可
            requests = []
            // 将本次请求发送
            return requests(error.config)
          }).finally(() => {
            // 请求发送完毕,响应处理完毕,将刷新状态更改为 false
            isRefreshing = false
          })
        }
      }
      // 将本次请求的错误对象继续向后抛出,让接收响应的处理函数进行操作
      return Promise.reject(error)
    })
    
    export default request
    

    用户信息处理

    • src/views/user/index.vue ```vue

      
      <a name="yBhkm"></a>
      ## 账号信息处理
      
      - `src/views/user/index.vue`
      ```vue
      <template>
        <div class="user">
          <van-cell-group>
            <!-- 用户信息区域 -->
      
            <!-- 账号信息 -->
            <van-cell class="account-info">
              <van-grid :border='false'>
                <van-grid-item>
                  <span class="grid-item-value">99.99</span>
                  <span class="grid-item-title">学习时长</span>
                </van-grid-item>
                <van-grid-item>
                  <span class="grid-item-value">99.99</span>
                  <span class="grid-item-title">钱包/勾豆</span>
                </van-grid-item>
                <van-grid-item>
                  <span class="grid-item-value">99</span>
                  <span class="grid-item-title">优惠券</span>
                </van-grid-item>
                <van-grid-item>
                  <span class="grid-item-value">999</span>
                  <span class="grid-item-title">学分</span>
                </van-grid-item>
              </van-grid>
            </van-cell>
          </van-cell-group>
          <layout-footer></layout-footer>
        </div>
      </template>
      
      <style lang="scss" scoped>
      // 用户信息
      
      
      // 账户信息
      .account-info {
        background-color: rgb(248, 150, 3)
      }
      
      .account-info .van-cell__value{
        border-radius: 15px;
      }
      
      .account-info .grid-item-value{
        font-size: 22px;
        font-weight: 700;
      }
      </style>
      

      底部菜单列表

      • src/views/user/index.vue ```vue

      
      <a name="rwMxL"></a>
      ## 封装接口与数据绑定
      
      - `src/services/user.js`
      ```javascript
      // 用户基本信息
      export const getInfo = () => {
        return request({
          method: 'GET',
          url: '/front/user/getInfo'
        })
      }
      
      • src/views/user/index.vue ```vue ```