项目实战第十七记
- 写在前面
- 1. 个人信息
- 1.1 Person.vue
- 1.2 设置路由并改动Header.vue
- 1.3 动态刷新头像
- 1.3.1 在保存个人信息时,触发方法
- 1.3.2 父组件Manage.vue
- 1.3.3 再将user以prop方式传递给子组件Header.vue
- 1.3.4 Header.vue使用user
- 1.4 效果图
- 2. 修改密码
- 2.1 前端页面编写(Password.vue)
- 2.2 修改密码后退出系统
- 2.3 路由设置并改动Header.vue
- 2.4 后端接口编写
- 2.4.1 UserController
- 2.4.2 UserServiceImpl
- 2.4.3 UserMapper
- 2.5 页面效果
- 总结
- 写在最后
写在前面
- 本篇主要讲解个人信息和修改密码页面,补充篇
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 页面效果
总结
- 这篇主要的难点是头像上传后的同步刷新;
- 修改密码其实是更新用户,通过用户名和旧密码确定唯一用户,然后才进行密码更改。
写在最后
如果此文对您有所帮助,请帅戈靓女们务必不要吝啬你们的Zan,感谢!!不懂的可以在评论区评论,有空会及时回复。
文章会一直更新