主页模块
目标
:实现主页基本布局效果主页的布局组件位置
**src/layout**
主页布局-左侧菜单
目标
:实现左侧菜单效果
- 左侧导航样式处理
styles/siderbar.scss
// 设置背景渐变色
.sidebar-container {
background: -webkit-linear-gradient(bottom, #3d6df8, #5b8cff);
}
// 设置左侧导航背景图片
.scrollbar-wrapper {
background: url('~@/assets/common/leftnavBg.png') no-repeat 0 100%;
}
// 设置菜单选中颜色
.el-menu {
border: none;
height: 100%;
width: 100% !important;
a{
li{
.svg-icon{
color: #fff;
font-size: 18px;
vertical-align: middle;
.icon{
color:#fff;
}
}
span{
color: #fff;
}
&:hover{
.svg-icon{
color: #43a7fe
}
span{
color: #43a7fe;
}
}
}
}
}
因为我们后期没有二级菜单,所以这里暂时不对二级菜单的样式进行控制。
主页布局-左侧图标
需求:定制SideBar菜单的Logo:
@/src/layout/components/Logo.vue
- 左侧菜单展开和折叠的控制逻辑
- 左侧菜单的展开和折叠由什么决定?el-menu组件的collapse属性决定
- 谁会影响到这个属性的值?点击汉堡图标会影响到
- 汉堡图标是如何影响这个collapse的值的?app这个vuex模块来管理菜单展开和折叠状态的
-
app提供菜单的默认展开状态数据给菜单组件
-
点击汉堡图标时,通过action影响app模块的里面的状态数据
-
// sidebar一共有三个地方需要用到
// 1、值的初始化操作
// 2、他的值是以全局getters的方式提供给组件
// 3、点击汉堡图标,触发action修改这个值即可
- 左侧logo图片显示效果调整
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link key="collapse" class="sidebar-logo-link" to="/">
<img src="@/assets/common/logo.png" class="sidebar-logo ">
</router-link>
</transition>
</div>
- 设置大图和小图的样式
// 大图样式
& .sidebar-logo {
width: 140px;
vertical-align: middle;
margin-right: 12px;
}
// 小图样式
&.collapse {
.sidebar-logo {
margin-right: 0px;
width: 50px;
height: 24px;
}
}
总结:&和类名之间的空格问题
- 如果有空格,就是父子关系
- 如果没有空格就是兄弟关系(并列关系)
主页布局-头部导航
**目标**
设置头部内容的布局和样式
- 头部组件效果
**layout/components/Navbar.vue**
<div class="app-breadcrumb">
HR人力资源管理平台
<span class="breadBtn">体验版</span>
</div>
<!-- <breadcrumb class="breadcrumb-container" /> -->
- 布局样式
.navbar {
background-image: -webkit-linear-gradient(left, #3d6df8, #5b8cff);
.app-breadcrumb {
display: inline-block;
font-size: 18px;
line-height: 50px;
margin-left: 10px;
color: #ffffff;
cursor: text;
.breadBtn {
background: #84a9fe;
font-size: 14px;
padding: 0 10px;
display: inline-block;
height: 30px;
line-height: 30px;
border-radius: 10px;
margin-left: 15px;
}
}
}
- 汉堡组件图标颜色
**src/components/Hamburger/index.vue**
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
fill="#fff"
>
**注意**
这里的图标我们使用了**svg**
,设置颜色需要使用svg标签的**fill属性**
- 右侧下拉菜单设置
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img src="@/assets/common/bigUserHeader.png" class="user-avatar">
<span class="name">管理员</span>
<i class="el-icon-caret-bottom" style="color:#fff" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<router-link to="/">
<el-dropdown-item>
首页
</el-dropdown-item>
</router-link>
<a target="_blank" href="https://xxx.com">
<el-dropdown-item>项目地址</el-dropdown-item>
</a>
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
- 头像和下拉菜单样式
.avatar-wrapper {
position: relative;
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 15px;
vertical-align: middle;
}
.name {
cursor: pointer;
color: #fff;
vertical-align: middle;
margin-left:5px;
}
.user-dropdown {
color: #fff;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 20px;
font-size: 12px;
}
}
总结:汉堡菜单;面包屑导航;右侧下拉菜单(基于ElementUI组件dropdown实现)
注意:svg的样式控制使用fill属性填充颜色
获取用户信息
**目标**
封装获取用户资料的资料信息上小节中,我们完成了头部菜单的基本布局,但是头像和名称没有,需要通过接口调用的方式获取当前用户的资料信息
- 获取用户资料接口
**src/api/user.js**
export function reqGetUserInfo() {
return request({
url: '/sys/profile',
method: 'post'
})
}
这个接口, 需要配置 headers 请求头, 配置 token, 而我们在请求任何带安全权限的接口时都需要
**令牌(token)**
,每次在接口中携带**令牌(token)**
很麻烦,所以我们可以在axios拦截器中统一添加token。**src/utils/request.js**
// 请求拦截器
instance.interceptors.request.use(function(config) {
// 在发送请求之前做些什么
if (store.getters.token) {
// 如果token存在 注入token
config.headers = {
Authorization: `Bearer ${store.getters.token}`
}
}
return config
}, function(error) {
// 对请求错误做些什么
return Promise.reject(error)
})
- 前端解决跨域问题
// 在vue.config.js文件中配置代理
devServer: {
port: port,
// 自动打开浏览器
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
// 所有的请求路径以api开始的地址都会被代理
// 发送的请求 http://localhost:9528/api/login
// 代理的目标 http://ihrm-java.itheima.net/api/login
'^/api': {
// 代理的目标地址
target: 'http://ihrm-java.itheima.net'
}
}
// before: require('./mock/mock-server.js')
},
// 实际发送的请求基准路径调整为本地地址 .env.development
VUE_APP_BASE_API = 'http://localhost:9528/api/'
总结:
- 网页网址http://localhost:9528/#/dashboard
- 浏览器监控的接口网址http://localhost:9528/api/sys/profile
- 实际的发送的接口网址ihrm-java.itheima.net/api/sys/pro…
vuex存储用户资料
**目标**
: 在用户的vuex模块中封装获取用户资料的action,并存储相关状态到vuex中用户状态会在后续的开发中,频繁用到,所以我们将用户状态同样的封装到action中
- 封装获取用户资料action
**action**
**src/store/modules/user.js**
import { login, getInfo } from '@/api/user.js'
mutations: {
updateUserInfo (state, payload) {
// 初始化用户信息
state.userInfo = payload
}
},
actions: {
// 获取用户基本信息
async getInfo (context) {
// 调用接口获取数据
const ret = await getInfo()
// 初始化用户信息
context.commit('updateUserInfo', ret.data)
}
}
- NavBar 组件中调用
import { mapActions } from 'vuex'
created() {
// 原始写法
// this.$store.dispatch('user/getInfo')
this.getInfo()
},
methods: {
...mapActions('user', ['getInfo']),
}
- 页面中使用
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
uname: state => state.user.userInfo.username // 建立用户名称的映射
}
export default getters
computed: {
...mapGetters([
'sidebar',
'avatar',
'uname'
])
},
<span class="name">{{uname}}</span>
总结:
- 通过action调用接口获取用户信息
- 把获取的数据更新到state里面
- Layout组件中触发action
- 通过getters解析用户信息中的uname
- 模板中显示用户信息
获取用户的头像
目标
:获取用户头像信息我们发现头像并不在接口的返回体中(接口原因),我们可以通过另一个接口来获取头像,并把头像合并到当前的资料中
- 封装获取用户信息接口
**src/api/user.js**
// 获取用户头像信息
export function getDetailInfo (id) {
// 参数ID表示当前登录系统的用户id
return request({
method: 'get',
url: '/sys/user/' + id
})
}
import { login, getInfo, getDetailInfo } from '@/api/user.js'
updateUserInfo (state, payload) {
// 初始化用户信息
if (state.userInfo) {
// 原来有值(合并两个对象的所有属性到一块)
state.userInfo = {
...state.userInfo,
...payload
}
} else {
// 第一次给他初始化
state.userInfo = payload
}
}
- 为了页面中更好地获取头像,同样可以把头像放于getters中
avatar: state => state.user.userInfo.staffPhoto // 建立用户头像的映射
- 展示头像
**layout/components/Navbar.vue**
computed: {
...mapGetters([
'sidebar',
'avatar',
'uname'
])
},
<img :src="avatar" class="user-avatar">
<span class="name">{{ name }}</span>
总结:基于Action获取用户的详细信息
- 基于延展运算符合并对象的写法
- 上一个接口调用的结果作为下一个接口调用的参数(注意第一次调用的await是必要的)
- 优化功能代码
- 封装一个独立的action:处理两个接口调用
// 获取用户基本信息
async getInfo (context) {
// 调用接口获取用户基本数据
const info = await getInfo()
// 调用接口获取用户详细数据
const detail = await getDetailInfo(info.data.userId)
// 更新用户数据
context.commit('updateUserInfo', {
...info.data,
...detail.data
})
}
- 处理mutation初始化操作
updateUserInfo (state, payload) {
state.userInfo = payload
}
- 组件中触发action
methods: {
...mapActions('user', ['getInfo']),
}
async created () {
this.getInfo()
}
总结:action中可以处理多个异步接口调用(基于async函数)
处理头像失效问题
目标
:处理图片加载失败时的默认显示效果头像的图片如果加载失败了,就显示一张默认的图片(基于自定义指令实现)
- 关于Vue插件用法补充
// 定义插件
export default {
// Vue.use(MyPlugins, 'defaultImg.png')
// Vue.use的参数二传递给install方法的第二个参数options
install (Vue, options) {
console.log(options)
}
}
// 导入并配置插件
import MyPlugins from '@/utils/plugins.js'
Vue.use(MyPlugins, 'defaultImg.png')
总结:配置和定义插件时,支持配置选项
- 注册自定义指令基本用法
Vue.directive('指令名称', {
// 会在当前指令作用的dom元素 插入之后执行
// el 指令所在dom元素
// bindings 里面是指令的参数信息对象
inserted(el, bindings) {
}
})
- 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror
/*
封装Vue插件
*/
export default {
install (Vue, options) {
// 扩展一个自定义指令,处理图片加载失败的情况
// <img v-imgerror='default.png' src="a.png" alt=""/>
Vue.directive('imgerror', {
// bindings包含指令相关的参数信息
inserted (el, bindings) {
console.dir(bindings)
// 如何知道img标签图片加载失败了?
el.onerror = () => {
// 加载失败后触发该函数
el.src = bindings.value || options
}
}
})
}
}
// main.js
// 导入插件
import MyPlugins from '@/utils/plugins.js'
// 配置插件
Vue.use(MyPlugins, 'default.png' )
- 使用指令, 这里图片如果是用本地图片, 需要导入, 如果是完整地址的网图, 直接赋值即可
<img v-imgerror="defaultImg" :src="avatar" class="user-avatar">
// 基于ES6导入单独的图片也是可以的
import Img from '@/assets/common/head.jpg'
data() {
return {
defaultImg: Img
}
},
// 或者直接完整地址的网图赋值
data() {
return {
defaultImg: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'
}
},
总结:
- 自定义指令的基本规则
- 插件基本使用规则:先定义,再导入并配置(支持选项)
- 配置插件时,可以传递options选项
- 扩展图片加载的自定义指令(原生dom事件 img.onerror 表示图片加载失败)
- 使用自定义指令
退出功能
**目标**
:实现用户的退出操作
- 基于.native事件修饰符绑定组件的原生事件
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">退出登录</span>
</el-dropdown-item>
总结:在组件的标签上绑定事件时,如果添加.native修饰符,表示把事件绑定到组件的根元素上
- 退出提示效果
// 点击退出时需要提示是否退出
this.$confirm('确认要退出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 点击确定,执行then方法
console.log('ok')
}).catch(() => {
// 点击取消,执行catch方法
console.log('cancel')
})
总结:退出API支持Promise方法,点击确定触发then,点击取消触发catch
- 退出action
**src/store/modules/user.js**
import { getToken, setToken, removeToken } from '@/utils/auth'
// mutations重要的原则: 只能是同步的
mutations: {
// 删除缓存的token
removeToken (state) {
state.token = ''
removeToken()
}
},
- 头部菜单调用action
**src/layout/components/Navbar.vue**
async logout () {
// 点击退出时需要提示是否退出
this.$confirm('确认要退出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 点击确定,执行then方法
// 删除token,删除用户信息
this.removeToken()
this.updateUserInfo(null)
// 跳转到登录页面
this.$router.push('/login')
}).catch(() => {
// 点击取消,执行catch方法
console.log('cancel')
})
}
总结:
- 绑定事件
- 确认删除
- 清除token
- 清除用户信息
- 跳转到登录页面
处理token失效问题
**目标**
: 实现token失效的处理token超时的错误码是
**10002**
- 拦截器处理token失效
**src/utils/request.js**
// 响应拦截器
instance.interceptors.response.use(function(response) {
// 对响应数据做点什么
return response.data
}, function(error) {
// 判断token是否失效
if (error.response.status === 401 && error.response.data.code === 10002) {
// token已经失效,删除用户信息,跳转到登录页面
store.commit('user/removeToken')
store.commit('user/updateUserInfo', {})
router.push('/login')
} else {
// 不是401,也不是200,那么说明是其他错误,直接进行提示
Message.error(error.response.message)
}
return Promise.reject(error)
})
总结:
- 判断token过期的情况
- 判断服务器失败的其他情况
- 在组件中触发mutation没有添加user前缀,因为映射时已经添加
- 在非组件环境触发mutation需要添加模块的前缀