逻辑代码及思维过程
- 在项目中安装croppers
csharp
$ yarn add cropperjs
- 分析思路,首先需要在navbar组件中添加一个更新头像的下拉选项
添加代码-代码位置(src/layout/components/navbar.vue)-dropdown组件中
xml
<!-- 更新头像 -->
<a target="_blank" @click.prevent="updateStaffPhoto">
<el-dropdown-item>更新头像</el-dropdown-item>
</a>
- 添加一个变量控制弹层的显示
javascript
export default {
data() {
return {
...
showCropperDialog: false // 控制编辑图像的弹层
}
}
}
- 实现一个updateStaffPhoto方法-调用时,将弹层显示
javascript
updateStaffPhoto() {
this.showCropperDialog = true
},
- 放置一个弹层,底部使用插槽放置两个按钮
xml
<el-dialog title="更新头像" :visible.sync="showCropperDialog">
<!-- 底部按钮显示区域 -->
<template #footer>
<el-row type="flex" justify="end">
<el-button size="mini" >取消</el-button>
<el-button size="mini" type="primary">确认并上传</el-button>
</el-row>
</template>
</el-dialog>
- 设置裁剪图区域 和 预览图区域的页面结构和样式
对应代码
xml
<el-dialog title="更新头像" :visible.sync="showCropperDialog">
<!-- 图片显示区域 -->
<el-row class="img_box_row">
<el-col :span="14" class="img_box_left">
<img
ref="imgStaffPhoto"
class="image_staffphoto"
:src="selectImg"
alt=""
>
</el-col>
<el-col :span="10" class="img_box_right">
<div class="img-preview" />
</el-col>
</el-row>
...
</el-dialog>
布局样式和预览图样式
css
.img_box_row {
height: 300px;
.img_box_left {
height: 100%;
background: #eee;
background-size: 24px 24px;
background-position: 0 0,12px 12px;
background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 0,transparent 75%,rgba(0,0,0,.25) 0),linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 0,transparent 75%,rgba(0,0,0,.25) 0);
.image_staffphoto {
display: block;
max-width: 80%;
height: 300px;
}
}
.img_box_right {
height: 150px;
.img-preview {
background-color: #fff;
text-align: center;
width: 200px;
height: 200px;
border: 1px solid #ccc;
margin-left: 20px;
overflow: hidden;
}
}
}
上面css中设置背景图片主要是设置在没有图片情况下 Cropper是不会被初始化的,所以设置一个默认的背景,如下图
- 打开弹出层时,将头像给到一个图片变量,并初始化Cropper
弹出层时,我们需要将用户原来的头像设置给用于显示弹层图片的变量,如果该变量有值,则初始化Cropper
- 定义selectImg为响应式数据
javascript
export default {
data() {
return {
...
selectImg: ''
}
}
}
- 将selectImg绑定给裁剪区域的图片
csharp
<img ref="imgStaffPhoto" class="image_staffphoto" :src="selectImg" alt="" >
- 弹出层时,设置头像,并初始化Cropper
kotlin
updateStaffPhoto() {
this.showCropperDialog = true
this.selectImg = this.avatar
this.$nextTick(() => {
this.selectImg && this.initCropper()
})
},
initCropper() {
this.staffCropper = new Cropper(this.$refs.imgStaffPhoto, {
aspectRatio: 1,
viewMode: 0,
preview: '.img-preview'
})
},
我们从计算属性中取到了avatar头像赋值给selectImg,然后用了nextTick,因为更新完头像之后,要保证头像的值更新到dom必须在上一次更新完成后执行, 然后我们将Cropper的实例给到this.staffCropper,以后就可以使用它来进行各种的控制啦
完成以上步骤,如果用户有头像,就可以进入编辑模式啦
- 布局下方操作按钮
- 定义一个计算属性iconEnable-用来控制操作按钮的可用性
只有当selectImg有值时,才可以让按钮可用
javascript
computed: {
...
iconEnable() {
return !!this.selectImg
},
}
- 结构代码
ini
<el-row type="flex" style="height: 60px; width: 440px" align="middle">
<el-col :span="10">
<el-tooltip content="上传图片">
<el-upload
:show-file-list="false"
:auto-upload="false"
action=""
accept="image/png, image/jpeg"
>
<el-button
type="primary"
size="mini"
icon="el-icon-upload"
class="tool-icon"
circle
/>
</el-upload>
</el-tooltip>
</el-col>
<el-col>
<el-row type="flex" justify="space-around">
<el-tooltip content="重置">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="reset"
/>
</el-tooltip>
<el-tooltip content="逆时针旋转">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh-left"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="rotateRetroGrade"
/>
</el-tooltip>
<el-tooltip content="顺时针旋转">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh-right"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="rotateForward"
/>
</el-tooltip>
<el-tooltip content="水平翻转">
<el-button
type="primary"
size="mini"
icon="el-icon-sort"
class="tool-icon"
circle
:disabled="!iconEnable"
style="transform: rotate(90deg);"
@click="scaleX"
/>
</el-tooltip>
<el-tooltip content="垂直翻转">
<el-button
type="primary"
size="mini"
icon="el-icon-sort"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleY"
/>
</el-tooltip>
<el-tooltip content="放大">
<el-button
type="primary"
size="mini"
icon="el-icon-zoom-in"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleMax"
/>
</el-tooltip>
<el-tooltip content="缩小">
<el-button
type="primary"
size="mini"
icon="el-icon-zoom-out"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleMin"
/>
</el-tooltip>
</el-row>
</el-col>
</el-row>
实现重置-正向旋转-逆向旋转-水平镜像-垂直镜像-放大-缩小等功能(类似前面的demo)
kotlin
// 重置
reset() {
this.staffCropper?.reset()
},
// 正向旋转
rotateForward() {
this.staffCropper?.rotate(45)
},
// 逆向旋转
rotateRetroGrade() {
this.staffCropper?.rotate(-45)
},
// 水平翻转
scaleX() {
this.scaleXValue = this.scaleXValue === -1 ? 1 : -1
this.staffCropper?.scaleX(this.scaleXValue)
},
// 垂直翻转
scaleY() {
this.scaleYValue = this.scaleYValue === -1 ? 1 : -1
this.staffCropper?.scaleY(this.scaleYValue)
},
// 放大
scaleMax() {
this.staffCropper?.zoom(0.1)
},
// 缩小
scaleMin() {
this.zoom -= 0.1
this.staffCropper?.zoom(-0.1)
},
到现在我们就实现了头像的裁剪的基本功能啦,接下来我们处理下选择新图片的情况
- 绑定el-upload的on-change属性-⚠️: el-upload的on-change是属性不是事件,但是这个属性是函数类型
ini
<el-upload
:on-change="selectFile"
:show-file-list="false"
:auto-upload="false"
action=""
accept="image/png, image/jpeg"
>
<el-button
type="primary"
size="mini"
icon="el-icon-upload"
class="tool-icon"
circle
/>
</el-upload>
- 实现selectFile方法
kotlin
selectFile(file) {
if (file) {
if (file.raw.size > 1024 * 1024) {
this.$message.warning('头像不能大于1M')
return
}
this.staffCropper?.destroy() // 销毁图片
const reader = new FileReader()
reader.readAsDataURL(file.raw) // 读取图片
reader.onloadend = () => {
this.selectImg = reader.result
this.$nextTick(() => {
this.selectImg && this.initCropper() // 重新初始化Cropper
})
}
}
},
上述方法首先判断了文件的大小,然后销毁了原来的Cropper实例,然后将File读取为base64,最终将base64设置给selectImg,有了新的selectImg重新进行Cropper
- 实现更新头像的功能
此时要获取裁剪后的头像,将它转成base64(类似前面demo),然后调用接口更新到服务器,成功之后,再将新的头像同步更新到Vuex中的头像
- 封装更新头像的接口
php
/** *
*
* 更新用户头像
* ***/
export function updateStaffPhoto(data) {
return request({
url: '/sys/updateStaffPhoto',
method: 'put',
data
})
}
kotlin
import { updateStaffPhoto } from '@/api/user'
// 获取图片的canvas - 并转化成base64
async bentStaffSubmit() {
const canvas = this.staffCropper?.getCroppedCanvas()
const staffPhoto = canvas.toDataURL('image/png') // 转化成base64的png图片格式
const byteLength = (staffPhoto.length * 3) / 4 / 1024 / 1024 // 设置base64不能超过1M
if (byteLength > 1) {
return this.$message.warning('编辑过的图片大小不能超过1M')
}
await updateStaffPhoto({
staffPhoto
})
// 更新vuex数据
this.$store.commit('user/setUserInfo', { ...this.$store.state.user.userInfo, staffPhoto })
this.staffCropper?.destroy() // 销毁Cropper实例
this.showCropperDialog = false
},
- 关闭弹层的方法
javascript
btnCancelImg() {
this.staffCropper?.destroy() // 销毁Cropper实例
this.showCropperDialog = false
},
到现在为止,我们就实现了整个的裁剪头像的功能,现在附上所有代码
xml
<template>
<div class="navbar">
<hamburger
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb class="breadcrumb-container" />
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<!-- 头像 -->
<img v-if="avatar" :src="avatar" class="user-avatar">
<span v-else class="username">{{ name?.charAt(0) }}</span>
<!-- 用户名称 -->
<span class="name">{{ name }}</span>
<!-- 图标 -->
<!-- <i class="el-icon-setting" /> -->
</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://github.com/PanJiaChen/vue-admin-template/"
>
<el-dropdown-item>项目地址</el-dropdown-item>
</a>
<!-- prevent阻止默认事件 -->
<a target="_blank" @click.prevent="updatePassword">
<el-dropdown-item>修改密码</el-dropdown-item>
</a>
<!-- 更新头像 -->
<a target="_blank" @click.prevent="updateStaffPhoto">
<el-dropdown-item>更新头像</el-dropdown-item>
</a>
<!-- native事件修饰符 -->
<!-- 注册组件的根元素的原生事件 -->
<el-dropdown-item @click.native="logout">
<span style="display: block">登出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 放置dialog -->
<!-- sync- 可以接收子组件传过来的事件和值 -->
<el-dialog
width="500px"
title="修改密码"
:visible.sync="showDialog"
@close="btnCancel"
>
<!-- 放置表单 -->
<el-form
ref="passForm"
label-width="120px"
:model="passForm"
:rules="rules"
>
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="passForm.oldPassword" show-password size="small" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="passForm.newPassword" show-password size="small" />
</el-form-item>
<el-form-item label="重复密码" prop="confirmPassword">
<el-input
v-model="passForm.confirmPassword"
show-password
size="small"
/>
</el-form-item>
<el-form-item>
<el-button
size="mini"
type="primary"
@click="btnOK"
>确认修改</el-button>
<el-button size="mini" @click="btnCancel">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog title="更新头像" :visible.sync="showCropperDialog" @close="btnCancelImg">
<!-- 图片显示区域 -->
<el-row class="img_box_row">
<el-col :span="14" class="img_box_left">
<img
ref="imgStaffPhoto"
class="image_staffphoto"
:src="selectImg"
alt=""
>
</el-col>
<el-col :span="10" class="img_box_right">
<div class="img-preview" />
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row type="flex" style="height: 60px; width: 440px" align="middle">
<el-col :span="10">
<el-tooltip content="上传图片">
<el-upload
:on-change="selectFile"
:show-file-list="false"
:auto-upload="false"
action=""
accept="image/png, image/jpeg"
>
<el-button
type="primary"
size="mini"
icon="el-icon-upload"
class="tool-icon"
circle
/>
</el-upload>
</el-tooltip>
</el-col>
<el-col>
<el-row type="flex" justify="space-around">
<el-tooltip content="重置">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="reset"
/>
</el-tooltip>
<el-tooltip content="逆时针旋转">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh-left"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="rotateRetroGrade"
/>
</el-tooltip>
<el-tooltip content="顺时针旋转">
<el-button
type="primary"
size="mini"
icon="el-icon-refresh-right"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="rotateForward"
/>
</el-tooltip>
<el-tooltip content="水平翻转">
<el-button
type="primary"
size="mini"
icon="el-icon-sort"
class="tool-icon"
circle
:disabled="!iconEnable"
style="transform: rotate(90deg);"
@click="scaleX"
/>
</el-tooltip>
<el-tooltip content="垂直翻转">
<el-button
type="primary"
size="mini"
icon="el-icon-sort"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleY"
/>
</el-tooltip>
<el-tooltip content="放大">
<el-button
type="primary"
size="mini"
icon="el-icon-zoom-in"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleMax"
/>
</el-tooltip>
<el-tooltip content="缩小">
<el-button
type="primary"
size="mini"
icon="el-icon-zoom-out"
class="tool-icon"
circle
:disabled="!iconEnable"
@click="scaleMin"
/>
</el-tooltip>
</el-row>
</el-col>
</el-row>
<template #footer>
<el-row type="flex" justify="end">
<el-button size="mini" @click="btnCancelImg">取消</el-button>
<el-button size="mini" type="primary" @click="bentStaffSubmit">确认并上传</el-button>
</el-row>
</template>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import { updatePassword, updateStaffPhoto, getUserSelfMessage, delUserSelfMessage, updateMessageState } from '@/api/user'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
export default {
components: {
Breadcrumb,
Hamburger
},
data() {
return {
showDialog: false, // 控制弹层的显示和隐藏
showCropperDialog: false, // 控制编辑图像的弹层
selectImg: '', // 选择的图片
passForm: {
oldPassword: '', // 旧密码
newPassword: '', // 新密码
confirmPassword: '' // 确认密码字段
},
rules: {
oldPassword: [
{ required: true, message: '旧密码不能为空', trigger: 'blur' }
], // 旧密码
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{
trigger: 'blur',
min: 6,
max: 16,
message: '新密码的长度为6-16位之间'
}
], // 新密码
confirmPassword: [
{ required: true, message: '重复密码不能为空', trigger: 'blur' },
{
trigger: 'blur',
validator: (rule, value, callback) => {
// value
if (this.passForm.newPassword === value) {
callback()
} else {
callback(new Error('重复密码和新密码输入不一致'))
}
}
}
] // 确认密码字段
}
}
},
computed: {
// 引入头像和用户名称
...mapGetters(['sidebar', 'avatar', 'name']),
iconEnable() {
return !!this.selectImg
}
},
methods: {
btnCancelImg() {
this.staffCropper?.destroy()
this.showCropperDialog = false
},
selectFile(file) {
if (file) {
if (file.raw.size > 1024 * 1024) {
this.$message.warning('头像不能大于1M')
return
}
this.staffCropper?.destroy()
const reader = new FileReader()
reader.readAsDataURL(file.raw)
reader.onloadend = () => {
this.selectImg = reader.result
this.$nextTick(() => {
this.selectImg && this.initCropper()
})
}
}
},
updateStaffPhoto() {
this.showCropperDialog = true
this.selectImg = this.avatar
this.$nextTick(() => {
this.selectImg && this.initCropper()
})
},
initCropper() {
this.staffCropper = new Cropper(this.$refs.imgStaffPhoto, {
aspectRatio: 1,
viewMode: 0,
preview: '.img-preview'
})
},
// 获取图片的canvas - 并转化成base64
async bentStaffSubmit() {
const canvas = this.staffCropper?.getCroppedCanvas()
const staffPhoto = canvas.toDataURL('image/png') // 转化成base64的png图片格式
const byteLength = (staffPhoto.length * 3) / 4 / 1024 / 1024 // 设置base64不能超过1M
if (byteLength > 1) {
return this.$message.warning('编辑过的图片大小不能超过1M')
}
await updateStaffPhoto({
staffPhoto
})
// 更新vuex数据
this.$store.commit('user/setUserInfo', { ...this.$store.state.user.userInfo, staffPhoto })
this.staffCropper?.destroy()
this.showCropperDialog = false
},
// 重置
reset() {
this.staffCropper?.reset()
},
// 水平翻转
scaleX() {
this.scaleXValue = this.scaleXValue === -1 ? 1 : -1
this.staffCropper?.scaleX(this.scaleXValue)
},
// 垂直翻转
scaleY() {
this.scaleYValue = this.scaleYValue === -1 ? 1 : -1
this.staffCropper?.scaleY(this.scaleYValue)
},
// 放大
scaleMax() {
this.staffCropper?.zoom(0.1)
},
// 缩小
scaleMin() {
this.zoom -= 0.1
this.staffCropper?.zoom(-0.1)
},
// 正向旋转
rotateForward() {
this.staffCropper?.rotate(45)
},
// 逆向旋转
rotateRetroGrade() {
this.staffCropper?.rotate(-45)
},
updatePassword() {
// 弹出层
this.showDialog = true // 显示弹层
},
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
// 调用退出登录的action
await this.$store.dispatch('user/logout')
this.$router.push('/login')
},
// 确定
btnOK() {
this.$refs.passForm.validate(async(isOK) => {
if (isOK) {
// 调用接口
await updatePassword(this.passForm)
this.$message.success('修改密码成功')
this.btnCancel()
}
})
},
// 取消
btnCancel() {
this.$refs.passForm.resetFields() // 重置表单
// 关闭弹层
this.showDialog = false
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
display: flex;
align-items: center;
&:focus {
outline: none;
}
.user-message {
display: flex;
width: 20px;
font-size: 23px;
color: #1e1e1e;
margin-right: 15px;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
display: flex;
align-items: center;
.name {
// 用户名称距离右侧距离
margin-right: 40px;
margin-left: 20px;
font-size: 16px;
color: #383c4e;
}
.username {
width: 30px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 50%;
background: #04c9be;
color: #fff;
margin-right: 4px;
}
.el-icon-setting {
font-size: 20px;
}
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
.tool-icon {
font-size: 16px;
}
.img_box_row {
height: 300px;
.img_box_left {
height: 100%;
background: #eee;
background-size: 24px 24px;
background-position: 0 0,12px 12px;
background-image:linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 0,transparent 75%,rgba(0,0,0,.25) 0),linear-gradient(45deg,rgba(0,0,0,.25) 25%,transparent 0,transparent 75%,rgba(0,0,0,.25) 0);
.image_staffphoto {
display: block;
max-width: 80%;
height: 300px;
}
}
.img_box_right {
height: 150px;
.img-preview {
background-color: #fff;
text-align: center;
width: 200px;
height: 200px;
border: 1px solid #ccc;
margin-left: 20px;
overflow: hidden;
}
}
}
}
</style>