使用Vue开发项目(黑马头条项目)--第八天

605 阅读3分钟

需要实现的主要功能如下:

资讯列表、标签页切换,文章举报,频道管理、文章详情、阅读记忆,关注功能、点赞功能、评论功能、回复评论、搜索功能、登录功能、个人中心、编辑资料、小智同学 ...

今天要实现的功能主要是:权限处理,401页面的处理,个人中心,编辑资料

1. 路由守卫

在router/index.js中补充一个前置路由守卫。业务逻辑有两条是:

  1. 如果没有登陆就不能访问某些敏感页面
  2. 如果登陆了,就不能再访问login页面
import store from '@/store/index.js'
const vipList = ['/setting']
router.beforeEach((to, from, next) => {
  const token = store.state.tokenInfo.token
  console.log(token)
  if (!token && vipList.includes(to.path)) {
    console.log('没有权限,不能访问vip页面')
    next('/login?backto=' + encodeURIComponent(to.fullPath))
  } else if (token && to.path === '/login') {
    console.log('已经登录了,就不能访问login了')
    next('/')
  } else {
    next()
  }
})
export default router

测试效果

在没有token的情况,直接在地址栏中访问setting将无法访问,而会跳回login页面

2 页面跳转-登陆成功之后回到初始页面

在src\utils\request.js中,补充响应拦截器。

import router from '../router/auth.js'


// 响应拦截器
request.interceptors.response.use(function (response) {
  console.log('响应拦截器', response)
  return response
}, async function (error) {
  // 如果发生了错误,判断是否是401
  console.dir(error)
  if (error.response.status === 401) {
    // 开始处理
    console.log('响应拦截器-错误-401')
    const refreshToken = store.state.tokenInfo.refresh_token
    // if (有refresh_token) {
    if (refreshToken) {
      // 1. 请求新token
      try {
        const res = await axios({
          url: 'http://localhost:8000/v1_0/authorizations',
          method: 'PUT',
          headers: {
            Authorization: `Bearer ${refreshToken}`
          }
        })
        console.log('请求新token', res.data.data.token)
        // 2. 保存到vuex
        store.commit('mSetToken', {
          refresh_token: refreshToken,
          token: res.data.data.token
        })
        // 3. 重发请求
        //    request是上面创建的axios的实例,它会自动从vuex取出token带上
        return request(error.config)
      } catch (error) {
        // 清除token
        store.commit('mSetToken', {})
        // 去到登录页(如果有token值,就不能到login)
        const backtoUrl = encodeURIComponent(router.currentRoute.fullPath)
        router.push('/login?backto=' + backtoUrl)
        return Promise.reject(error)
      }
    } else {
      // 去到登录页
      // 清除token
      store.commit('mSetToken', {})
      const backtoUrl = encodeURIComponent(router.currentRoute.fullPath)
      router.push('/login?backto=' + backtoUrl)
      return Promise.reject(error)
    }
  } else {
    return Promise.reject(error)
  }
})

3 个人中心

3.1 布局

src/views/setting/setting.vue

<template>
  <div class="user">
    <!-- 用户个人资料 -->
    <div class="user-profile">
      <div class="info">
        <van-image round src="https://img.yzcdn.cn/vant/cat.jpeg" />
        <h3 class="name">
          用户名
          <br />
          <van-tag size="mini">申请认证</van-tag>
        </h3>
      </div>
      <van-row>
        <van-col span="8">
          <p>0</p>
          <p>动态</p>
        </van-col>
        <van-col span="8">
          <p>0</p>
          <p>关注</p>
        </van-col>
        <van-col span="8">
          <p>0</p>
          <p>粉丝</p>
        </van-col>
      </van-row>
    </div>

    <!-- 操作链接 -->
    <van-row class="user-links">
      <van-col span="8">
        <van-icon name="newspaper-o" color="#7af" />我的作品
      </van-col>
      <van-col span="8">
        <van-icon name="star-o" color="#f00" />我的收藏
      </van-col>
      <van-col span="8">
        <van-icon name="tosend" color="#fa0" />阅读历史
      </van-col>
    </van-row>

    <!-- 编辑入口
      is-link: 产生一个向右的箭头
      to: 用来做路由的跳转
    -->
    <van-cell-group class="user-group">
      <van-cell icon="edit" title="编辑资料" to="/setting/profile" is-link />
      <van-cell icon="chat-o" title="小智同学" to="/setting/chat" is-link />
      <van-cell icon="setting-o" title="系统设置"  is-link />
      <van-cell icon="warning-o" title="退出登录" to="/login" is-link />
    </van-cell-group>
  </div>
</template>

<script>
export default {
  name: 'setting',
  data () {
    return {

    }
  }
}
</script>

<style lang="less" scoped>
.user {
  .user-profile {
    width: 100%;
    height: 200px;
    display: block;
    background: #3296fa;
    color: #fff;
    .info {
      display: flex;
      padding: 20px;
      align-items: center;
      .van-image{
        width: 64px;
        height: 64px;
      }
      .name {
        font-size: 16px;
        font-weight: normal;
        margin-left: 10px;
      }
      .van-tag {
        background: #fff;
        color: #3296fa;
      }
    }
    p{
      margin: 0;
      text-align: center;
    }
  }
  .user-group {
    margin-bottom: 15px;
  }
  .user-links {
    padding: 15px 0;
    font-size: 12px;
    text-align: center;
    background-color: #fff;
    .van-icon {
      display: block;
      font-size: 24px;
      padding-bottom: 5px;
    }
  }
}
</style>

3.2 隐藏顶部导航

修改layout.vue中的nav-bar的显示 : 如果在/setting页面中, 则隐藏顶部的搜索导航区域

<!-- 顶部logo搜索导航区域
    如果当前是在"我的" 页面,则不要出现搜索导航区域
    只有当前页面不是“我的”,它才显示出来
    -->
    <van-nav-bar
      fixed
      v-show="cIsVisiable"
     >
computed: {
    cIsVisiable () {
      // 只有在个人中心页才不可见
      return this.$route.path !== '/setting'
    },
    ...mapGetters(['isLogin'])
  }

测试效果

image.png

3.3 个人中心-数据渲染

src/api/user.js 中定义接口文档

/**
 * 获取用户自已的信息
 */
export const getInfo = () => {
  return request({
    method: 'GET',
    url: 'v1_0/user'
  })
}

src\views\setting\setting.vue调用

import { getInfo } from '@/api/user.js'

data () {
    return {
      userInfo: {}
    }
  },
  created () {
    this.loadUserInfo()
  },
  methods: {
    async loadUserInfo () {
      const res = await getInfo()
      console.log(res)
      this.userInfo = res.data.data
    }
  }
<!-- 用户个人资料 -->
    <div class="user-profile">
      <div class="info">
        <van-image round
        :src="userInfo.photo" />
        <h3 class="name">
          {{userInfo.name}}
          <br />
          <van-tag size="mini">申请认证</van-tag>
        </h3>
      </div>
      <van-row>
        <van-col span="8">
          <p>{{userInfo.art_count}}</p>
          <p>动态</p>
        </van-col>
        <van-col span="8">
          <p>{{userInfo.follow_count}}</p>
          <p>关注</p>
        </van-col>
        <van-col span="8">
          <p>{{userInfo.fans_count}}</p>
          <p>粉丝</p>
        </van-col>
      </van-row>
    </div>

查看效果

image.png

3.3 个人中心-退出登录

用户点击退出时,弹出确认框,确认退出之后:

  • 回到登陆页
  • 清空本地登陆信息 在组件view/settging /setting.vue中, 绑定事件
 <!-- 用户退出 -->
<van-cell icon="warning-o" title="退出登录" 
+  @click="hQuit" 
is-link />
// 用户退出
    hQuit () {
      // 1. 是否确认要退出
      // 2. 清空token,userInfo
      // 3. 回去login
      this.$dialog.confirm({
        title: '系统提示',
        message: '再多玩一会嘛'
      }).then(() => {
        this.$store.commit('getToken', {})
        localStorage.removeItem('token')
        this.$router.push('/login')
      }).catch(() => {
        // on cancel
      })
    }

4 编辑个人资料-创建组件并配置路由

4.1 结构

创建src/views/setting/profile.vue

<template>
  <div class="container">
    <!-- 导航条 -->
    <van-nav-bar
      left-arrow
      @click-left="$router.back()"
      title="编辑资料">
    </van-nav-bar>

    <!-- 编辑区 -->
    <van-cell-group>
      <van-cell is-link title="头像"  center>
        <van-image
          slot="default"
          width="1.5rem"
          height="1.5rem"
          fit="cover"
          round
          :src="userInfo.photo"
        />
      </van-cell>
        <!-- value: 设置右侧显示的文字 -->
      <van-cell is-link title="姓名" :value="userInfo.name" @click="isShowName=true"/>

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

      <van-cell is-link title="生日" :value="userInfo.birthday" @click="isShowBirthday=true"/>
    </van-cell-group>
  </div>
</template>

<script>
export default {
  name: 'userProfile',
  data () {
    return {
      // 控制弹层
      isShowName: false,
      isShowGender: false,
      isShowBirthday: false,

      // 当前用户的信息
      userInfo: { name: '张三' },

      // 修改后的新名字
      newName: '',
      // 修改后新生日
      newDate: new Date(),
      minDate: new Date(1965, 0, 10), // dateTime-picker中最小时间
      maxDate: new Date() // 当前时间
    }
  }
}
</script>

4.2 添加路由

{
    path: '/setting/profile',
    name: 'settingProfile',
    component: () => import('../views/setting/profile.vue')
  }

4.3 测试效果

image.png

4.4 编辑个人资料-数据渲染-从vuex中获取数据

import { mapState } from 'vuex'
export default {
  name: 'userProfile',
  data () {
    return {
      // 控制弹层
      isShowName: false,
      isShowGender: false,
      isShowBirthday: false,

      // 修改后的新名字
      newName: '',
      // 修改后新生日
      newDate: new Date(),
      minDate: new Date(1965, 0, 10), // dateTime-picker中最小时间
      maxDate: new Date() // 当前时间
    }
  },
  created () {
    // 调用带命名空间的modules中的action
    this.$store.dispatch('user/getProfile')
  },
  computed: {
    // ...mapState(命名空间名, ['state名字'])
    ...mapState('user', ['userInfo'])
  }
}

模板

<van-cell-group>
      <van-cell is-link title="头像"  center>
        <van-image
          slot="default"
          width="1.5rem"
          height="1.5rem"
          fit="cover"
          round
          :src="userInfo.photo"
        />
      </van-cell>

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

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

      <van-cell is-link title="生日" :value="userInfo.birthday" @click="isShowBirthday=true"/>
    </van-cell-group>

5 编辑个人资料-姓名

5.1 添加弹层结构

<!-- 修改名字  -->
      <van-dialog
        v-model="isShowName"
        title="修改名字"
        show-cancel-button
        @confirm="hSaveName">
        <van-field v-model.trim="newName" />
      </van-dialog>

5.2 封装接口api

/**
 * 编辑用户信息
 * data: {
 *  name,
 *  gender,
 *  birthday
 * }
 */
export const updateUserInfo = (data) => {
  return request({
    method: 'PATCH',
    url: 'v1_0/user/profile',
    data
  })
}

5.3 准备muation

mUpdateUserName (state, newName) {
    state.userInfo.name = newName
},

5.4 准备action

// 修改用户名
    async updateUserName (context, newName) {
      try {
        await updateUserInfo({
          name: newName
        })
        context.commit('mUpdateUserName', newName)
      } catch (err) {
        // console.log(err)
        throw new Error(err)
      }
    },

5.5 调用

view/setting/profile.vue中,调用上面的接口,实现功能

// 修改名字
    async hSaveName () {
      if (this.newName === '') {
        return
      }
      try {
        await this.$store.dispatch('user/updateUserName', this.newName)
        this.$toast.success('修改成功')
      } catch (err) {
        this.$toast.fail('修改失败')
        console.log(err)
      }
    },

6 编辑个人资料-性别

6.1 加入弹层结构

<!-- 修改性别 -->
      <van-popup v-model="showGender" position="bottom">
        <van-nav-bar title="修改性别" left-text="取消" @click-left="showGender=false">
        </van-nav-bar>
        <van-cell title="女" @click="hSaveGender(0)" is-link></van-cell>
        <van-cell title="男" @click="hSaveGender(1)" is-link></van-cell>
      </van-popup>

6.2 准备muation

mUpdateGender (state, newGender) {
      state.userInfo.gender = newGender
    }

6.3 调用并实现功能

// 修改性别
    async hSaveGender (gender) {
      try {
        await updateUserInfo({ gender })
        // 在本地更新vuex
        this.$store.commit('user/mUpdateGender', gender)
        // 关闭弹层
        this.isShowGender = false
        this.$toast.success('操作成功')
      } catch (err) {
        console.log(err)
        this.$toast.fail('操作失败')
      }
    }

7 编辑个人资料-生日

7.1 加入弹层结构

 <!-- 修改生日 -->
    <van-popup v-model="isShowBirthday" position="bottom">
      <van-nav-bar title="修改生日"> </van-nav-bar>
      <van-datetime-picker
        v-model="newDate"
        type="date"
        title="选择年月日"
        @cancel="isShowBirthday = false"
        @confirm="hSaveBirthday"
        :min-date="minDate"
        :max-date="maxDate"
      />
    </van-popup>

7.2 准备muation

// 修改生日
    mUpdateUserBirthday (state, newBirthday) {
      state.userInfo.birthday = newBirthday
    },

7.3 封装时间过滤器函数

utils/formateDate.js中加入

export const formatDate = (dateTime) => {
  const date = new Date(dateTime) // 转换成Data();
  var y = date.getFullYear()
  var m = date.getMonth() + 1
  m = m < 10 ? '0' + m : m
  var d = date.getDate()
  d = d < 10 ? ('0' + d) : d
  return y + '-' + m + '-' + d
}

7.4 引入并实现代码

// 修改生日
    async hSaveBirthday () {
      // 组件中提供的时间是标准时间,后端接口中需要是年-月-日的格式,
      // 要进行转换
      const birthday = formatDate(this.newDate)
      try {
        await updateUserInfo({ birthday })
        // 1. 更新到vuex中
        this.$store.commit('user/mUpdateUserBirthday', birthday)
        // 2. 关闭弹层
        this.isShowBirthday = false
        this.$toast.success('修改成功')
      } catch (err) {
        console.log(err)
        this.$toast.fail('修改失败')
      }
    },

8 编辑个人资料-头像

编辑个人头像,本质就是从本地上传一张新的图片到服务器,以替换之前头像文件。

8.1 在前面加入上传文件input

 <van-cell-group>
+     <input type="file" @change="hFileChange" />
      <van-cell is-link title="头像"  center>
        <van-image
          slot="default"
          width="1.5rem"
          height="1.5rem"
          fit="cover"
          round
          :src="userInfo.photo"
        />
      </van-cell>

8.2 封装接口api

/**
 * 修改用户头像
 *
 * obj: 以formData格式保存参数
 */
/**
 * 更新用户头像
 * @param {*} fd 头像
 */
export const updateUserPhoto = fd => {
  return request({
    method: 'PATCH',
    url: 'v1_0/user/photo',
    data: fd
  })
}

8.3 补充mutation和action

import { getProfile, updateUserInfo, updateUserPhoto } from '@/api/user.js'


mutations: {
    mUpdateUserPhoto (state, newPhoto) {
      state.userInfo.photo = newPhoto
    }
  },

actions: {
  // 修改头像
    async updateUserPhoto (context, fd) {
      try {
        const res = await updateUserPhoto(fd)
        console.log('updateUserPhoto', res)
        context.commit('mUpdateUserPhoto', res.data.data.photo)
      } catch (err) {
        console.log(err)
        throw new Error(err)
      }
    }
}

8.4在回调中去上传头像

// 文件域选中文件之后,自动会传递事件对象给change的回调
    // e.target: input对
    // e.target.files // 选中的文件的集合
    // e.target.files[0] // 选中的第一个文件
    async  hFileChange (e) {
      const file = e.target.files[0]
      if (!file) {
        // 用户没有选中
        return
      }

      // 做文件上传
      console.dir(file)
      // ajax文件上传
      // 1. 封装FormData对象
      // 2. append参数
      const fd = new FormData()
      fd.append('photo', file)
      try {
        await this.$store.dispatch('user/updateUserPhoto', fd)
        this.$toast.success('操作成功')
      } catch (err) {
        this.$toast.fail('操作失败')
      }
    }

8.5 优化:隐藏文件域

<!-- 当用户在文件域中进行选择时,会触发change事件
       hidden: 隐藏。它用来隐藏input元素。元素将不可见,也不占用页面的空间
       ref="file": 添加ref为了通过引用来访问这个dom元素  -->

      <input type="file" 
+ hidden 
 @change="hFileChange" 
+ref="file"/>
<van-cell is-link title="头像"  center @click="hClickImage">
// 用户在头像上点击,希望也弹出文件选择框
// 移花接木
// 用户点击了头像: 理解他要更新头像,此时 弹出input type="file"
    hClickImage () {
      // 找到这个引用,并直接调用click()
      this.$refs.file.click()
    }