写在前面

  • 本篇主要讲解个人信息和修改密码页面,补充篇

1. 个人信息

1.1 Person.vue

<template>
    <el-card style="width: 500px;">
      <el-form label-width="80px" size="small">
        <el-upload
            class="avatar-uploader"
            action="http://localhost:9000/file/upload"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
        >
          <img v-if="form.avatarUrl" :src="form.avatarUrl" class="avatar">
          <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
  
        <el-form-item label="用户名">
          <el-input v-model="form.username" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="昵称">
          <el-input v-model="form.nickname" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="form.email" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="电话">
          <el-input v-model="form.phone" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="地址">
          <el-input v-model="form.address" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="save">确 定</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </template>
  
  <script>
  export default {
    name: "Person",
    data() {
      return {
        form: {},
        user: localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")) : {}
      }
    },
    created() {
      this.load();
    },
    methods: {
      load(){
        const username = this.user.username
        if(!username){
          this.$message.error("当前无法获取用户信息!");
          return false
        }
        // 数据库做了唯一性处理
        this.request.get("/user/username/"+username).then(res => {
          this.form = res.data
        })
      },
      save() {
        this.request.post("/user", this.form).then(res => {
          if (res.code === '200') {
            this.$message.success("保存成功")
            //this.dialogFormVisible("保存成功")
            this.load()
            //向父组件传递值   触发方法
            this.$emit('refreshUser')
          } else {
            this.$message.error("保存失败")
          }
        })
      },

      handleAvatarSuccess(res) {
        console.log('===',res)
        //res就是文件的路径
        this.form.avatarUrl = res
      }
    }
  }
  </script>
  
  <style>
  .avatar-uploader {
    text-align: center;
    padding-bottom: 10px;
  }
  .avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    border-color: #409EFF;
  }
  .avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 138px;
    height: 138px;
    line-height: 138px;
    text-align: center;
  }
  .avatar {
    width: 138px;
    height: 138px;
    display: block;
  }
  </style>
  

1.2 设置路由并改动Header.vue

// 拼装动态路由
      const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", children: [
          { path: 'person', name: '个人信息', component: () => import('../views/Person.vue'),meta: { title: '个人信息' }},
          { path: 'password', name: '修改密码', component: () => import('../views/Password.vue'),meta: { title: '修改密码' }}
        ] }

在Header.vue中添加

<el-dropdown-item style="font-size: 14px; padding: 5px 0;">
  <router-link to="/person">个人信息</router-link>
</el-dropdown-item>

1.3 动态刷新头像

当更换头像时,点击确定时,实现头像的同步更新

1.3.1 在保存个人信息时,触发方法

save() {
    this.request.post("/user", this.form).then(res => {
      if (res.code === '200') {
        this.$message.success("保存成功")
        this.load()
        /*
        这段Vue模板代码定义了一个方法,当调用这个方法时,会触发名为refreshUser的自定义事件。
        这个事件可以被父组件监听到,从而在父组件中执行相应的逻辑操作。
        **/
        this.$emit('refreshUser')
      } else {
        this.$message.error("保存失败")
      }
    })
  },

1.3.2 父组件Manage.vue

// 触发@refreshUser自定义事件
<el-main>
    <!--  表示当前页面的子路由会在 <router-view/> 里面显示     -->
    <router-view @refreshUser="getUser"/>
</el-main>


// 获取用户的最新数据
getUser(){
      const username = localStorage.getItem('loginUser') ?         			 JSON.parse(localStorage.getItem('loginUser')).username : '';
      this.request.get('/user/username/' + username).then(res => {
        if(res.code == '200'){
          // 重新赋值后台的最新User数据
          this.user = res.data;
        }
      })
    }

1.3.3 再将user以prop方式传递给子组件Header.vue

<el-header style="border-bottom: 1px solid #ccc;">
    <Header :collapse-btn-class="collapseBtnClass" :collapse="isCollapse" :user="user"/>
</el-header>

完整的Manage.vue代码

<template>
  <el-container style="min-height: 100vh">

    <el-aside :width="sideWidth + 'px'" style="box-shadow: 2px 0 6px rgb(0 21 41 / 0.35);">
      <Aside :is-collapse="isCollapse" :logo-text-show="logoTextShow"/>
    </el-aside>

    <el-container>
      <el-header style="border-bottom: 1px solid #ccc;">
        <Header :collapse-btn-class="collapseBtnClass" :collapse="isCollapse" :user="user"/>
      </el-header>

      <el-main>
        <!--  表示当前页面的子路由会在 <router-view/> 里面显示     -->
        <router-view @refreshUser="getUser"/>
      </el-main>

    </el-container>
  </el-container>
</template>

<script>

import Aside from "@/components/Aside";
import Header from "@/components/Header";

export default {
  name: 'HomeView',
  components: {
    Aside,
    Header
  },
  data() {
    return {
      collapseBtnClass: 'el-icon-s-fold',
      isCollapse: false,
      sideWidth: 200,
      logoTextShow: true,
      headerBg: 'headerBg',
      user: {}
    }
  },
  created() {
    this.getUser()
  },
  methods: {
    collapse() {  // 点击收缩按钮触发
      this.isCollapse = !this.isCollapse
      if (this.isCollapse) {  // 收缩
        this.sideWidth = 64
        this.collapseBtnClass = 'el-icon-s-unfold'
        this.logoTextShow = false
      } else {   // 展开
        this.sideWidth = 200
        this.collapseBtnClass = 'el-icon-s-fold'
        this.logoTextShow = true
      }
    },
    // 获取用户的最新数据
    getUser(){
      const username = localStorage.getItem('loginUser') ? JSON.parse(localStorage.getItem('loginUser')).username : '';
      this.request.get('/user/username/' + username).then(res => {
        if(res.code == '200'){
          // 重新赋值后台的最新User数据
          this.user = res.data;
        }
      })
    }
  }
}
</script>

<style>
.headerBg {
  background: #eee!important;
}
</style>

1.3.4 Header.vue使用user

// 接收数据
props: {
    collapseBtnClass: String,
    collapse: Boolean,
    // 定义一个user属性接受从Manage.vue传进来的user对象
    user: Object
  },
  
// 使用数据
<div style="display: inline-block">
    <img :src="user.avatarUrl" alt=""
         style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px;overflow: hidden;">
    <span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
</div>

完整的Header.vue代码

<template>
  <div style="line-height: 60px; display: flex">
    <div style="flex: 1;">
      <span :class="collapseBtnClass" style="cursor: pointer; font-size: 18px" @click="collapse"></span>

      <el-breadcrumb separator=">" style="display: inline-block; margin-left: 10px">
        <el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
<!--        <el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>-->
        <el-breadcrumb-item v-for="(item, index) in breadCrumbs" :key="item.path">
          <router-link :to="item.path">{{ item.meta.title }}</router-link>
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>

    <el-dropdown style="width: 100px; cursor: pointer">
      <div style="display: inline-block">
        <img :src="user.avatarUrl" alt=""
             style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px;overflow: hidden;">
        <span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
      </div>
      <el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
        <el-dropdown-item style="font-size: 14px; padding: 5px 0;">
          <router-link to="/person">个人信息</router-link>
        </el-dropdown-item>
        <el-dropdown-item style="font-size: 14px; padding: 5px 0;">
          <router-link to="/password">修改密码</router-link>
        </el-dropdown-item>
        <el-dropdown-item style="font-size: 14px; padding: 5px 0">
          <span style="text-decoration: none" @click="logout">退出</span>
        </el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  name: "Header",
  props: {
    collapseBtnClass: String,
    collapse: Boolean,
    // 定义一个user属性接受从Manage.vue传进来的user对象
    user: Object
  },
  // 当当前路由发生变化时,调用getBreadcrumb方法来更新面包屑导航的数据
  watch: {
    $route() {
      this.getBreadcrumb();
    }
  },
  data(){
    return {
      breadCrumbs: [],
      //user: localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")) : ""
    }
  },
  created() {
    this.getBreadcrumb()
  },
  methods: {
    getBreadcrumb(){
      // 从当前路由的匹配记录中过滤出具有meta属性且包含title属性的路由记录
      this.breadCrumbs = this.$route.matched.filter(item => item.meta && item.meta.title);
    },
    // 退出登录
    logout(){
      this.$router.push("/login");
      localStorage.removeItem("loginUser")
      this.$message.success("退出成功")
    }
  }
  // computed: {
  //   currentPathName () {
  //     return this.$store.state.currentPathName;  //需要监听的数据
  //   }
  // },

}
</script>

<style scoped>

</style>

1.4 效果图

在这里插入图片描述
在这里插入图片描述
注意:上传的头像最好是正方形的大小,形成的头像才会是规整的

2. 修改密码

2.1 前端页面编写(Password.vue)

<template>
  <el-card style="width: 500px;">
    <el-form label-width="120px" size="small" :model="form" :rules="rules" ref="pass">

      <el-form-item label="原密码" prop="password">
        <el-input v-model="form.password" autocomplete="off" show-password></el-input>
      </el-form-item>
      <el-form-item label="新密码" prop="newPassword">
        <el-input v-model="form.newPassword" autocomplete="off" show-password></el-input>
      </el-form-item>
      <el-form-item label="确认新密码" prop="confirmPassword">
        <el-input v-model="form.confirmPassword" autocomplete="off" show-password></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="save">确 定</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
export default {
  name: "Password",
  data() {
    return {
      form: {},
      user: localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")) : {},
      rules: {
        password: [
          { required: true, message: '请输入原密码', trigger: 'blur' },
          { min: 3, message: '长度不少于3位', trigger: 'blur' }
        ],
        newPassword: [
          { required: true, message: '请输入新密码', trigger: 'blur' },
          { min: 3, message: '长度不少于3位', trigger: 'blur' }
        ],
        confirmPassword: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 3, message: '长度不少于3位', trigger: 'blur' }
        ],
      }
    }
  },
  created() {
    // 通过用户名和旧密码来唯一标识用户,然后再修改密码
    this.form.username = this.user.username
  },
  methods: {
    save() {
      this.$refs.pass.validate((valid) => {
        if (valid) {
          if (this.form.newPassword !== this.form.confirmPassword) {
            this.$message.error("2次输入的新密码不相同")
            return false
          }
          this.request.post("/user/password", this.form).then(res => {
            if (res.code === '200') {
              this.$message.success("修改成功")
              this.$store.commit("logout")
            } else {
              this.$message.error(res.msg)
            }
          })
        }
      })
    },
  }
}
</script>

<style>
.avatar-uploader {
  text-align: center;
  padding-bottom: 10px;
}
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 138px;
  height: 138px;
  line-height: 138px;
  text-align: center;
}
.avatar {
  width: 138px;
  height: 138px;
  display: block;
}
</style>

2.2 修改密码后退出系统

// 退出系统
this.$store.commit("logout")

store文件下index.js

import Vue from 'vue'
import Vuex from 'vuex'
import router from "@/router";


Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    currentPathName: ''
  },
  getters: {
  },
  mutations: {
    // setPath(state){
    //   state.currentPathName = localStorage.getItem('currentPathName')
    // },
    logout() {
      // 清空缓存
      localStorage.removeItem("loginUser")
      localStorage.removeItem("menus")
      router.push("/login")

      // 重置路由
      //resetRouter()
    }
  },
  actions: {
  },
  modules: {
  }
})

2.3 路由设置并改动Header.vue

// 拼装动态路由
const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", children: [
          { path: 'person', name: '个人信息', component: () => import('../views/Person.vue'),meta: { title: '个人信息' }},
          { path: 'password', name: '修改密码', component: () => import('../views/Password.vue'),meta: { title: '修改密码' }}
        ] }

完整的index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Manage from '../views/Manage.vue'
import store from "@/store";

Vue.use(VueRouter)
//定义一个路由对象数组
const routes = [
  {
    path: '/login',
    name: '登录',
    component: () => import('../views/Login.vue')
  },
  {
    path: '/register',
    name: '注册',
    component: () => import('../views/Register.vue')
  },
  {
    path: '/404',
    name: '404',
    component: () => import('../views/404.vue')
  }

]

//使用路由对象数组创建路由实例,供main.js引用
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 注意:刷新页面会导致页面路由重置
export const setRoutes = () => {
  const storeMenus = localStorage.getItem("menus");
  if (storeMenus) {

    // 获取当前的路由对象名称数组
    const currentRouteNames = router.getRoutes().map(v => v.name)
    if (!currentRouteNames.includes('Manage')) {
      // 拼装动态路由
      const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", children: [
          { path: 'person', name: '个人信息', component: () => import('../views/Person.vue'),meta: { title: '个人信息' }},
          { path: 'password', name: '修改密码', component: () => import('../views/Password.vue'),meta: { title: '修改密码' }}
        ] }
      const menus = JSON.parse(storeMenus)
      menus.forEach(item => {
        if (item.path) {  // 当且仅当path不为空的时候才去设置路由
          let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue'),meta: { title: item.name }}
          manageRoute.children.push(itemMenu)
        } else if(item.children.length) {
          item.children.forEach(item => {
            if (item.path) {
              let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue'),meta: { title: item.name }}
              manageRoute.children.push(itemMenu)
            }
          })
        }
      })
      // 动态添加到现在的路由对象中去
      router.addRoute(manageRoute)
    }

  }
}

// 重置我就再set一次路由
setRoutes()


// 路由守卫
router.beforeEach((to, from, next) => {
  // localStorage.setItem('currentPathName',to.name);   // 设置当前的路由名称,为了在Header组件中去使用
  // store.commit('setPath')    // 触发store的数据更新


  // 未找到路由情况
  if(!to.matched.length){
    const storeMenus = localStorage.getItem("menus");
    if(storeMenus){   // 有菜单没有找到路由,跳转至 404页面
      next("/404")
    }else {    // // 没有菜单,直接跳转至登录页
      next("/login")
    }
  }

  next()   // 放行路由
})

export default router

Header.vue改动

<el-dropdown-item style="font-size: 14px; padding: 5px 0;">
 <router-link to="/password">修改密码</router-link>
</el-dropdown-item>

完整的Header.vue

<template>
  <div style="line-height: 60px; display: flex">
    <div style="flex: 1;">
      <span :class="collapseBtnClass" style="cursor: pointer; font-size: 18px" @click="collapse"></span>

      <el-breadcrumb separator=">" style="display: inline-block; margin-left: 10px">
        <el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
<!--        <el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>-->
        <el-breadcrumb-item v-for="(item, index) in breadCrumbs" :key="item.path">
          <router-link :to="item.path">{{ item.meta.title }}</router-link>
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>

    <el-dropdown style="width: 100px; cursor: pointer">
      <div style="display: inline-block">
        <img :src="user.avatarUrl" alt=""
             style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px;overflow: hidden;">
        <span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
      </div>
      <el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
        <el-dropdown-item style="font-size: 14px; padding: 5px 0;">
          <router-link to="/person">个人信息</router-link>
        </el-dropdown-item>
        <el-dropdown-item style="font-size: 14px; padding: 5px 0;">
          <router-link to="/password">修改密码</router-link>
        </el-dropdown-item>
        <el-dropdown-item style="font-size: 14px; padding: 5px 0">
          <span style="text-decoration: none" @click="logout">退出</span>
        </el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  name: "Header",
  props: {
    collapseBtnClass: String,
    collapse: Boolean,
    // 定义一个user属性接受从Manage.vue传进来的user对象
    user: Object
  },
  // 当当前路由发生变化时,调用getBreadcrumb方法来更新面包屑导航的数据
  watch: {
    $route() {
      this.getBreadcrumb();
    }
  },
  data(){
    return {
      breadCrumbs: [],
      //user: localStorage.getItem("loginUser") ? JSON.parse(localStorage.getItem("loginUser")) : ""
    }
  },
  created() {
    this.getBreadcrumb()
  },
  methods: {
    getBreadcrumb(){
      // 从当前路由的匹配记录中过滤出具有meta属性且包含title属性的路由记录
      this.breadCrumbs = this.$route.matched.filter(item => item.meta && item.meta.title);
    },
    // 退出登录
    logout(){
      this.$router.push("/login");
      localStorage.removeItem("loginUser")
      this.$message.success("退出成功")
    }
  }
  // computed: {
  //   currentPathName () {
  //     return this.$store.state.currentPathName;  //需要监听的数据
  //   }
  // },

}
</script>

<style scoped>

</style>

2.4 后端接口编写

2.4.1 UserController

@PostMapping("/password")
public Result updatePassword(@RequestBody UserPasswordDTO userPasswordDTO){
        if(StrUtil.isBlank(userPasswordDTO.getUsername()) || StrUtil.isBlank(userPasswordDTO.getPassword())){
            return Result.error(Constants.CODE_400,"参数错误");
        }
        userService.updatePassword(userPasswordDTO);
        return Result.success();
    }

用UserPasswordDTO接收前端传过来的表单参数

package com.ppj.entity.dto;

import lombok.Data;

@Data
public class UserPasswordDTO {
    private String username;
    private String password;
    private String newPassword;
}

2.4.2 UserServiceImpl

@Override
public void updatePassword(UserPasswordDTO userPasswordDTO) {
    int res = userMapper.updatePassword(userPasswordDTO);
    if(res<1){
        throw new ServiceException(Constants.CODE_600,"密码修改失败");
    }
}

2.4.3 UserMapper

package com.ppj.mapper;

import com.ppj.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ppj.entity.dto.UserPasswordDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author ppj
 * @since 2024-04-20
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {

    int updatePassword(UserPasswordDTO userPasswordDTO);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ppj.mapper.UserMapper">

    <update id="updatePassword" parameterType="com.ppj.entity.dto.UserPasswordDTO">
        update sys_user
        set password = #{newPassword}
        where username = #{username} and password = #{password}
    </update>

</mapper>

2.5 页面效果

在这里插入图片描述

总结

  1. 这篇主要的难点是头像上传后的同步刷新;
  2. 修改密码其实是更新用户,通过用户名和旧密码确定唯一用户,然后才进行密码更改。

写在最后

如果此文对您有所帮助,请帅戈靓女们务必不要吝啬你们的Zan,感谢!!不懂的可以在评论区评论,有空会及时回复。
文章会一直更新

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐