uni-app黑马优购项目学习记录(十)(中篇)

206 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

10.3 用户信息

10.3.1 实现用户头像昵称区域的基本布局

  1. my-userinfo 组件中,定义如下的 UI 结构:

    <template>
      <view class="my-userinfo-container">
    
        <!-- 头像昵称区域 -->
        <view class="top-box">
          <image src="" class="avatar"></image>
          <view class="nickname">xxx</view>
        </view>
    
      </view>
    </template>
    
  2. 美化当前组件的样式:

    .my-userinfo-container {
      height: 100%;
      // 为整个组件的结构添加浅灰色的背景
      background-color: #f4f4f4;
    
      .top-box {
        height: 400rpx;
        background-color: #c00000;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    
        .avatar {
          display: block;
          width: 90px;
          height: 90px;
          border-radius: 45px;
          border: 2px solid white;
          box-shadow: 0 1px 5px black;
        }
    
        .nickname {
          color: white;
          font-weight: bold;
          font-size: 16px;
          margin-top: 10px;
        }
      }
    }
    
  3. my.vue 页面中,为最外层包裹性质的 view 容器,添加 class="my-container" 的类名,并美化样式如下:

    page,
    .my-container {
      height: 100%;
    }
    

10.3.2 渲染用户的头像和昵称

  1. my-userinfo 组件中,通过 mapState 辅助函数,将需要的成员映射到当前组件中使用:

    // 按需导入 mapState 辅助函数
    import { mapState } from 'vuex'
    
    export default {
      computed: {
        // 将 m_user 模块中的 userinfo 映射到当前页面中使用
        ...mapState('m_user', ['userinfo']),
      },
      data() {
        return {}
      },
    }
    
  2. 将用户的头像和昵称渲染到页面中:

    <!-- 头像昵称区域 -->
    <view class="top-box">
      <image :src="userinfo.avatarUrl" class="avatar"></image>
      <view class="nickname">{{userinfo.nickName}}</view>
    </view>
    

10.3.3 渲染第一个面板区域

  1. my-userinfo 组件中,定义如下的 UI 结构:

    <!-- 面板的列表区域 -->
    <view class="panel-list">
      <!-- 第一个面板 -->
      <view class="panel">
        <!-- panel 的主体区域 -->
        <view class="panel-body">
          <!-- panel 的 item 项 -->
          <view class="panel-item">
            <text>8</text>
            <text>收藏的店铺</text>
          </view>
          <view class="panel-item">
            <text>14</text>
            <text>收藏的商品</text>
          </view>
          <view class="panel-item">
            <text>18</text>
            <text>关注的商品</text>
          </view>
          <view class="panel-item">
            <text>84</text>
            <text>足迹</text>
          </view>
        </view>
      </view>
    
      <!-- 第二个面板 -->
    
      <!-- 第三个面板 -->
    </view>
    
  2. 美化第一个面板的样式:

    .panel-list {
      padding: 0 10px;
      position: relative;
      top: -10px;
    
      .panel {
        background-color: white;
        border-radius: 3px;
        margin-bottom: 8px;
    
        .panel-body {
          display: flex;
          justify-content: space-around;
    
          .panel-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-around;
            font-size: 13px;
            padding: 10px 0;
          }
        }
      }
    }
    

10.3.4 渲染第二个面板区域

  1. 定义第二个面板区域的 UI 结构:

    <!-- 第二个面板 -->
    <view class="panel">
      <!-- 面板的标题 -->
      <view class="panel-title">我的订单</view>
      <!-- 面板的主体 -->
      <view class="panel-body">
        <!-- 面板主体中的 item 项 -->
        <view class="panel-item">
          <image src="/static/my-icons/icon1.png" class="icon"></image>
          <text>待付款</text>
        </view>
        <view class="panel-item">
          <image src="/static/my-icons/icon2.png" class="icon"></image>
          <text>待收货</text>
        </view>
        <view class="panel-item">
          <image src="/static/my-icons/icon3.png" class="icon"></image>
          <text>退款/退货</text>
        </view>
        <view class="panel-item">
          <image src="/static/my-icons/icon4.png" class="icon"></image>
          <text>全部订单</text>
        </view>
      </view>
    </view>
    
  2. 对之前的 SCSS 样式进行改造,从而美化第二个面板的样式:

    .panel-list {
      padding: 0 10px;
      position: relative;
      top: -10px;
    
      .panel {
        background-color: white;
        border-radius: 3px;
        margin-bottom: 8px;
    
        .panel-title {
          line-height: 45px;
          padding-left: 10px;
          font-size: 15px;
          border-bottom: 1px solid #f4f4f4;
        }
    
        .panel-body {
          display: flex;
          justify-content: space-around;
    
          .panel-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-around;
            font-size: 13px;
            padding: 10px 0;
    
            .icon {
              width: 35px;
              height: 35px;
            }
          }
        }
      }
    }
    

10.3.5 渲染第三个面板区域

  1. 定义第三个面板区域的 UI 结构:

    <!-- 第三个面板 -->
    <view class="panel">
      <view class="panel-list-item">
        <text>收货地址</text>
        <uni-icons type="arrowright" size="15"></uni-icons>
      </view>
      <view class="panel-list-item">
        <text>联系客服</text>
        <uni-icons type="arrowright" size="15"></uni-icons>
      </view>
      <view class="panel-list-item">
        <text>退出登录</text>
        <uni-icons type="arrowright" size="15"></uni-icons>
      </view>
    </view>
    
  2. 美化第三个面板区域的样式:

    .panel-list-item {
      height: 45px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 15px;
      padding: 0 10px;
    }
    

10.3.6 实现退出登录的功能

  1. 为第三个面板区域中的 退出登录 项绑定 click 点击事件处理函数:

    <view class="panel-list-item" @click="logout">
      <text>退出登录</text>
      <uni-icons type="arrowright" size="15"></uni-icons>
    </view>
    
  2. my-userinfo 组件的 methods 节点中定义 logout 事件处理函数:

    // 退出登录
    async logout() {
      // 询问用户是否退出登录
      const [err, succ] = await uni.showModal({
        title: '提示',
        content: '确认退出登录吗?'
      }).catch(err => err)
    
      if (succ && succ.confirm) {
         // 用户确认了退出登录的操作
         // 需要清空 vuex 中的 userinfo、token 和 address
         this.updateUserInfo({})
         this.updateToken('')
         this.updateAddress({})
      }
    }
    
  3. 使用 mapMutations 辅助方法,将需要用到的 mutations 方法映射到当前组件中:

    // 按需导入辅助函数
    import { mapState, mapMutations } from 'vuex'
    
    export default {
      methods: {
        ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateAddress']),
      },
    }
    

10.4 三秒后自动跳转

10.4.1 三秒后自动跳转到登录页面

需求描述:在购物车页面,当用户点击 “结算” 按钮时,如果用户没有登录则 3 秒后自动跳转到登录页面

  1. my-settle 组件的 methods 节点中,声明一个叫做 showTips 的方法,专门用来展示倒计时的提示消息:

    // 展示倒计时的提示消息
    showTips(n) {
      // 调用 uni.showToast() 方法,展示提示消息
      uni.showToast({
        // 不展示任何图标
        icon: 'none',
        // 提示的消息
        title: '请登录后再结算!' + n + ' 秒后自动跳转到登录页',
        // 为页面添加透明遮罩,防止点击穿透
        mask: true,
        // 1.5 秒后自动消失
        duration: 1500
      })
    }
    
  2. data 节点中声明倒计时的秒数:

    data() {
      return {
        // 倒计时的秒数
        seconds: 3
      }
    }
    
  3. 改造 结算 按钮的 click 事件处理函数,如果用户没有登录,则预调用一个叫做 delayNavigate 的方法,进行倒计时的导航跳转:

    // 点击了结算按钮
    settlement() {
      // 1. 先判断是否勾选了要结算的商品
      if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')
    
      // 2. 再判断用户是否选择了收货地址
      if (!this.addstr) return uni.$showMsg('请选择收货地址!')
    
      // 3. 最后判断用户是否登录了,如果没有登录,则调用 delayNavigate() 进行倒计时的导航跳转
      // if (!this.token) return uni.$showMsg('请先登录!')
      if (!this.token) return this.delayNavigate()
    },
    
  4. 定义 delayNavigate 方法,初步实现倒计时的提示功能

    // 延迟导航到 my 页面
    delayNavigate() {
      // 1. 展示提示消息,此时 seconds 的值等于 3
      this.showTips(this.seconds)
    
      // 2. 创建定时器,每隔 1 秒执行一次
      setInterval(() => {
        // 2.1 先让秒数自减 1
        this.seconds--
        // 2.2 再根据最新的秒数,进行消息提示
        this.showTips(this.seconds)
      }, 1000)
    },
    

    上述代码的问题:定时器不会自动停止,此时秒数会出现等于 0 或小于 0 的情况!

  5. data 节点中声明定时器的 Id 如下:

    data() {
      return {
        // 倒计时的秒数
        seconds: 3,
        // 定时器的 Id
        timer: null
      }
    }
    
  6. 改造 delayNavigate 方法如下:

    // 延迟导航到 my 页面
    delayNavigate() {
      this.showTips(this.seconds)
    
      // 1. 将定时器的 Id 存储到 timer 中
      this.timer = setInterval(() => {
        this.seconds--
    
        // 2. 判断秒数是否 <= 0
        if (this.seconds <= 0) {
          // 2.1 清除定时器
          clearInterval(this.timer)
    
          // 2.2 跳转到 my 页面
          uni.switchTab({
            url: '/pages/my/my'
          })
    
          // 2.3 终止后续代码的运行(当秒数为 0 时,不再展示 toast 提示消息)
          return
        }
    
        this.showTips(this.seconds)
      }, 1000)
    },
    

上述代码的问题:seconds 秒数不会被重置,导致第 2 次,3 次,n 次 的倒计时跳转功能无法正常工作

  1. 进一步改造 delayNavigate 方法,在执行此方法时,立即将 seconds 秒数重置为 3 即可:

    // 延迟导航到 my 页面
    delayNavigate() {
      // 把 data 中的秒数重置成 3 秒
      this.seconds = 3
      this.showTips(this.seconds)
    
      this.timer = setInterval(() => {
        this.seconds--
    
        if (this.seconds <= 0) {
          clearInterval(this.timer)
          uni.switchTab({
            url: '/pages/my/my'
          })
          return
        }
    
        this.showTips(this.seconds)
      }, 1000)
    }
    

10.4.2 登录成功之后再返回之前的页面

核心实现思路:在自动跳转到登录页面成功之后,把返回页面的信息存储到 vuex 中,从而方便登录成功之后,根据返回页面的信息重新跳转回去。

返回页面的信息对象,主要包含 { openType, from } 两个属性,其中 openType 表示以哪种方式导航回之前的页面from 表示之前页面的 url 地址

  1. store/user.js 模块的 state 节点中,声明一个叫做 redirectInfo 的对象如下:

    // state 数据
    state: () => ({
      // 收货地址
      address: JSON.parse(uni.getStorageSync('address') || '{}'),
      // 登录成功之后的 token 字符串
      token: uni.getStorageSync('token') || '',
      // 用户的基本信息
      userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}'),
      // 重定向的 object 对象 { openType, from }
      redirectInfo: null
    }),
    
  2. store/user.js 模块的 mutations 节点中,声明一个叫做 updateRedirectInfo 的方法:

    mutations: {
      // 更新重定向的信息对象
      updateRedirectInfo(state, info) {
        state.redirectInfo = info
      }
    }
    
  3. my-settle 组件中,通过 mapMutations 辅助方法,把 m_user 模块中的 updateRedirectInfo 方法映射到当前页面中使用:

    methods: {
      // 把 m_user 模块中的 updateRedirectInfo 方法映射到当前页面中使用
      ...mapMutations('m_user', ['updateRedirectInfo']),
    }
    
  4. 改造 my-settle 组件 methods 节点中的 delayNavigate 方法,当成功跳转到 my 页面 之后,将重定向的信息对象存储到 vuex 中:

    // 延迟导航到 my 页面
    delayNavigate() {
      // 把 data 中的秒数重置成 3 秒
      this.seconds = 3
      this.showTips(this.seconds)
    
      this.timer = setInterval(() => {
        this.seconds--
    
        if (this.seconds <= 0) {
          // 清除定时器
          clearInterval(this.timer)
          // 跳转到 my 页面
          uni.switchTab({
            url: '/pages/my/my',
            // 页面跳转成功之后的回调函数
            success: () => {
              // 调用 vuex 的 updateRedirectInfo 方法,把跳转信息存储到 Store 中
              this.updateRedirectInfo({
                // 跳转的方式
                openType: 'switchTab',
                // 从哪个页面跳转过去的
                from: '/pages/cart/cart'
              })
            }
          })
    
          return
        }
    
        this.showTips(this.seconds)
      }, 1000)
    }
    
  5. my-login 组件中,通过 mapStatemapMutations 辅助方法,将 vuex 中需要的数据和方法,映射到当前页面中使用:

    // 按需导入辅助函数
    import { mapMutations, mapState } from 'vuex'
    
    export default {
      computed: {
        // 调用 mapState 辅助方法,把 m_user 模块中的数据映射到当前用组件中使用
        ...mapState('m_user', ['redirectInfo']),
      },
      methods: {
        // 调用 mapMutations 辅助方法,把 m_user 模块中的方法映射到当前组件中使用
        ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateRedirectInfo']),
      },
    }
    
  6. 改造 my-login 组件中的 getToken 方法,当登录成功之后,预调用 this.navigateBack() 方法返回登录之前的页面:

    // 调用登录接口,换取永久的 token
    async getToken(info) {
      // 省略其它代码...
    
      // 判断 vuex 中的 redirectInfo 是否为 null
      // 如果不为 null,则登录成功之后,需要重新导航到对应的页面
      this.navigateBack()
    }
    
  7. my-login 组件中,声明 navigateBack 方法如下:

    // 返回登录之前的页面
    navigateBack() {
      // redirectInfo 不为 null,并且导航方式为 switchTab
      if (this.redirectInfo && this.redirectInfo.openType === 'switchTab') {
        // 调用小程序提供的 uni.switchTab() API 进行页面的导航
        uni.switchTab({
          // 要导航到的页面地址
          url: this.redirectInfo.from,
          // 导航成功之后,把 vuex 中的 redirectInfo 对象重置为 null
          complete: () => {
            this.updateRedirectInfo(null)
          }
        })
      }
    }