编辑用户资料
1.0 创建组件并配置路由
1、创建 views/user/index.vue
<template>
<div>
<van-nav-bar title="个人信息" left-arrow right-text="保存" />
<van-cell-group>
<van-cell title="头像" is-link>
<van-image
round
width="30"
height="30"
fit="cover"
src="http://toutiao.meiduo.site/FgSTA3msGyxp5-Oufnm5c0kjVgW7"
/>
</van-cell>
<van-cell title="昵称" value="abc" is-link />
<van-cell title="性别" value="男" is-link />
<van-cell title="生日" value="2019-9-27" is-link />
</van-cell-group>
</div>
</template>
<script>
export default {
name: "UserIndex"
};
</script>
2、将该页面配置到根路由
{
name: 'user-profile',
path: '/user/profile',
component: () => import('@/views/user-profile')
}
2.0 页面布局
<template>
<div class="user-profile">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
title="个人信息"
left-arrow
@click-left="$router.back()"
/>
<!-- /导航栏 -->
<!-- 个人信息 -->
<van-cell class="avatar-cell" title="头像" is-link center>
<van-image
class="avatar"
round
fit="cover"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
</van-cell>
<van-cell title="昵称" value="内容" is-link />
<van-cell title="性别" value="内容" is-link />
<van-cell title="生日" value="内容" is-link />
<!-- /个人信息 -->
</div>
</template>
<script>
export default {
name: 'UserProfile',
components: {},
props: {},
data () {
return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less">
.user-profile {
.avatar-cell {
.van-cell__value {
display: flex;
flex-direction: row-reverse;
}
.avatar {
width: 60px;
height: 60px;
}
}
}
</style>
3.0 展示用户信息
思路:
- 找到数据接口
- 封装请求方法
- 请求获取数据
- 模板绑定
1、在 api/user.js
中添加封装数据接口
/**
* 获取当前登录用户的个人资料
*/
export const getUserProfile = target => {
return request({
method: 'GET',
url: '/app/v1_0/user/profile'
})
}
2、在 views/user/index.vue
组件中请求获取数据
import { getUserProfile } from '@/api/user'
created () {
this.loadGetUserProfile()
},
methods: {
async loadGetUserProfile() {
try {
const { data } = await getUserProfile()
this.user = data.data
} catch (err) {
this.$toast('获取数据失败')
}
}
}
<van-cell class="avatar-cell" title="头像" is-link center>
<van-image
class="avatar"
round
fit="cover"
:src="user.photo"
/>
</van-cell>
<van-cell title="昵称" :value="user.name" is-link />
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link />
<van-cell title="生日" :value="user.birthday" is-link />
4.0 修改昵称
一、准备弹出层
<van-cell title="昵称" :value="user .name" is-link @click="isUpdateNameShow = true"/>
<!-- 修改名称弹出层 -->
<van-popup v-model="isUpdateNameShow" position="bottom" style="height: 100%">
<update-name
v-if="isUpdateNameShow"
@close="isUpdateNameShow = false"
v-model="user.name"
></update-name>
</van-popup>
二、封装组件
<template>
<div class="update-name">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
title="设置呢称"
left-text="取消"
right-text="完成"
@click-left="$emit('close')"
@click-right="loadUpdateUserProfile"
/>
<!-- /导航栏 -->
<!-- 输入栏 -->
<div class="field-wrap">
<van-field
v-model="localName"
rows="2"
autosize
type="textarea"
maxlength="7"
placeholder="请输入呢称"
show-word-limit
/>
</div>
</div>
</template>
<script>
import { updateUserProfile } from '@/api/user'
export default {
props: {
value: {
type: String,
required: true,
}
},
data() {
return {
localName: this.value,
}
},
created() {
this.loadUpdateUserProfile
},
methods: {
async loadUpdateUserProfile() {
this.$toast.loading({
duration: 0,
forbidClick: true, // 禁止背景点击
message: '保存中----'
})
try {
if(!this.localName.length) {
this.$toast('呢称不能为空')
return
}
await updateUserProfile({name: this.localName})
this.$emit('input', this.localName)
this.$emit('close')
this.$toast.success('更新名称成功')
} catch(err) {
this.$toast.fail('修改名称失败')
}
}
}
}
</script>
<style lang='less' scoped>
.update-name {
/deep/.van-nav-bar__text {
color: #fff;
}
.field-wrap {
padding: 20px;
}
}
</style>
5.0 修改性别
一、准备弹出层
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link @click="isUpdateGenderShow = true"/>
<!-- 编辑性别 -->
<van-popup v-model="isUpdateGenderShow" position="bottom">
<update-gender v-if="isUpdateGenderShow" v-model="user.gender" @close="isUpdateGenderShow = false">
</update-gender>
</van-popup>
二、封装组件
<template>
<div class="update-gender">
<van-picker
title="标题"
show-toolbar
:columns="columns"
:default-index="value"
@confirm="onConfirm"
@cancel="$emit('close')"
@change="onPickerChange"
/>
</div>
</template>
<script>
import { updateUserProfile } from '@/api/user'
export default {
props: {
value: {
type: Number,
required: true,
}
},
data() {
return {
columns:['男','女'],
localGender: this.value
}
},
methods: {
onPickerChange(picker, value, index) {
this.localGender = index
},
async onConfirm() {
this.$toast.loading({
duration: 0,
forbidClick: true, // 禁止背景点击
message: '保存中----'
})
try {
await updateUserProfile({gender: this.localGender})
this.$emit('input', this.localGender)
this.$emit('close')
this.$toast.success('更新名称成功')
} catch(err) {
this.$toast.fail('修改名称失败')
}
}
}
}
</script>
<style>
</style>
6.0 修改生日
一、准备弹出层
<van-cell title="生日" :value="user.birthday" is-link @click="isUpdateBirthdayShow = true" />
<!-- 编辑生日 -->
<van-popup v-model="isUpdateBirthdayShow" position="bottom">
<update-birthday v-if="isUpdateBirthdayShow" v-model="user.birthday" @close="isUpdateGenderShow = false">
</update-birthday>
</van-popup>
二、封装组件
<template>
<div class="update-birthday">
<van-datetime-picker
v-model="currentDate"
type="date"
title="选择年月日"
:min-date="minDate"
:max-date="maxDate"
@cancel="$emit('close')"
@confirm="onConfirm"
/>
</div>
</template>
<script>
import { updateUserProfile } from '@/api/user'
import dayjs from 'dayjs'
export default {
props: {
value: {
type: String,
required: true,
}
},
data() {
return {
minDate: new Date(2020, 0, 1),
maxDate: new Date(2025, 10, 1),
currentDate: new Date(this.value),
}
},
methods: {
async onConfirm() {
this.$toast.loading({
duration: 0,
forbidClick: true, // 禁止背景点击
message: '保存中----'
})
try {
const currentDate = dayjs(this.currentDate).format('YYYY-MM-DD')
await updateUserProfile({birthday: currentDate})
this.$emit('input', currentDate)
this.$emit('close')
this.$toast.success('更新日期成功')
} catch(err) {
this.$toast.fail('修改日期失败')
}
}
}
}
</script>
<style>
</style>
7.0 修改头像
一、准备弹出层
<input type="file" hidden ref="file" @change="onFileChange">
<van-cell class="avatar-cell" title="头像" is-link center @click="$refs.file.click()">
<van-image
class="avatar"
round
fit="cover"
:src="user.photo"
/>
</van-cell>
<!-- 编辑照片 -->
<van-popup v-model="isUpdatePhotoShow" position="bottom" style="height: 100%">
<update-photo v-if="isUpdatePhotoShow" :img="img" @close="isUpdatePhotoShow = false"
@update-photo="user.photo = $event">
</update-photo>
</van-popup>
import UpdatePhoto from './components/update-photo.vue'
isUpdatePhotoShow: false,
img: null,
onFileChange() {
// 获取文件对象
const file = this.$refs.file.files[0]
// 基于文章对象获取 blob 数据
this.img = window.URL.createObjectURL(file)
this.isUpdatePhotoShow = true
// file-inpit 如果选了同一个文件不会触发 change 事件
// 解决办法就是每次使用完毕, 把 它的 value 清空
this.$refs.file.value = ''
}
二、封装组件
这里要用到 cropperjs 这个第三方的库 https://github.com/fengyuanchen/cropperjs
1. npm install cropperjs
2. <link href="/path/to/cropper.css" rel="stylesheet">
<script src="/path/to/cropper.js"></script>
或者 vue 中导入方式
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
3. img 外面必须包裹一个 div容器
<div>
<img id="image" src="picture.jpg">
</div>
4.img { // img 必须配置这两个属性
display: block;
max-width: 100%;
}
5.// import 'cropperjs/dist/cropper.css';
import Cropper from 'cropperjs';
<img class="img" :src="img" alt="" ref="img"> // 添加一个 ref
mounted() { // 这里要用 mounted 操作 dom 元素
const image = this.$refs.img
const cropper = new Cropper(image, {
aspectRatio: 16 / 9,
crop(event) {
console.log(event.detail.x);
console.log(event.detail.y);
console.log(event.detail.width);
console.log(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
},
})
},
6. 删除原本的配置 , 自定义配置
data() {
retrue {
cropper: null
}
}
mounted() { // 这里要用 mounted 操作 dom 元素
const image = this.$refs.img
this.cropper = new Cropper(image, { // 这里设置为 this.
viewMode: 1, // 定义裁纸器的查看模式
dragMode: 'move', // 拖动模式
aspectRatio: 1, // 截图比例 默认是 16 | 9
autoCropArea: 1, // 自动截取区域,
cropBoxMovable: false, // 截图区域是否可以移动,改为false就是 使画布移动
cropBoxResizable: false, // 是否可以缩放
background: false, // 不需要背景
movable: true // 这个默认就是 true, 就是画布是否可以移动, 可以不写
})
},
7. 获取结果的两种方式
onConfirm() {
// 基于服务端的裁切使用 getData 方法获取裁切参数 , 这个没兼容问题,主要是后端来做
// console.log(this.cropper.getData())
// 纯客户端的裁切使用 getCroppedCanvas 获取裁切的文件对象 这个有兼容问题, 客户端慎用
this.cropper.getCroppedCanvas().toBlob(blob => {
this.updateUserPhoto(blob)
})
}
8. 封装保存更新的函数
<update-photo
v-if="isUpdatePhotoShow"
:img="img"
@close="isUpdatePhotoShow = false"
@update-photo="user.photo = $event">
</update-photo>
import { updateUserPhoto } from '@/api/user'
async updateUserPhoto(blob) {
this.$toast.loading({
message:'保存中----',
forbidClick: true,
duration:0
})
// 如果 Content-Type 要求是 application/json ,则 data 传普通对象 {}
// 如果 Content-Type 要求是 multipart/form-data ,则 data 传 FormData 对象
// 纵观所有数据接口,你会发现大多数的接口都要求 Content-Type 要求是 application/json
// 一般只有涉及到文件上传的数据接口才要求Content-Type 要求是 multipart/form-data
// 这个时候传递一个 FormData 对象
try {
const formData = new FormData()
formData.append('photo', blob)
const { data } = await updateUserPhoto(formData)
this.$emit('close')
this.$emit('update-photo', data.data.photo)
this.$toast.success('更新成功')
} catch (err) {
this.$toast.fail('保存失败')
}
}
全部代码
<template>
<div class="update-photo">
<img class="img" :src="img" alt="" ref="img">
<div class="toolbar">
<div class="cancel" @click="$emit('close')">取消</div>
<div class="confirm" @click="onConfirm">完成</div>
</div>
</div>
</template>
<script>
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { updateUserPhoto } from '@/api/user'
export default {
data() {
return {
cropper: null,
}
},
props: {
img: {
type: [String,Object],
required: true,
}
},
mounted() {
const image = this.$refs.img
this.cropper = new Cropper(image, {
viewMode: 1,
dragMode: 'move',
aspectRatio: 1,
autoCropArea: 1,
cropBoxMovable: false,
cropBoxResizable: false,
background: false,
movable: true
})
},
methods: {
onConfirm() {
// 基于服务端的裁切使用 getData 方法获取裁切参数 , 这个没兼容问题,主要是后端来做
// console.log(this.cropper.getData())
// 纯客户端的裁切使用 getCroppedCanvas 获取裁切的文件对象 这个有兼容问题, 客户端慎用
this.cropper.getCroppedCanvas().toBlob(blob => {
this.updateUserPhoto(blob)
})
},
async updateUserPhoto(blob) {
this.$toast.loading({
message:'保存中----',
forbidClick: true,
duration:0
})
// 如果 Content-Type 要求是 application/json ,则 data 传普通对象 {}
// 如果 Content-Type 要求是 multipart/form-data ,则 data 传 FormData 对象
// 纵观所有数据接口,你会发现大多数的接口都要求 Content-Type 要求是 application/json
// 一般只有涉及到文件上传的数据接口才要求Content-Type 要求是 multipart/form-data
// 这个时候传递一个 FormData 对象
try {
const formData = new FormData()
formData.append('photo', blob)
const { data } = await updateUserPhoto(formData)
this.$emit('close')
this.$emit('update-photo', data.data.photo)
this.$toast.success('更新成功')
} catch (err) {
this.$toast.fail('保存失败')
}
}
}
}
</script>
<style lang="less" scoped>
.update-photo {
background-color: #000;
height: 100%;
.img {
display: block;
max-width: 100%;
}
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
.cancel, .confirm {
width: 90px;
height: 90px;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
}
}
</style>
8.0 index全部内容
<template>
<div class="user-profile">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
title="个人信息"
left-arrow
@click-left="$router.back()"
/>
<!-- /导航栏 -->
<input type="file" hidden ref="file" @change="onFileChange">
<!-- 个人信息 -->
<van-cell class="avatar-cell" title="头像" is-link center @click="$refs.file.click()">
<van-image
class="avatar"
round
fit="cover"
:src="user.photo"
/>
</van-cell>
<van-cell title="昵称" :value="user .name" is-link @click="isUpdateNameShow = true"/>
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link @click="isUpdateGenderShow = true"/>
<van-cell title="生日" :value="user.birthday" is-link @click="isUpdateBirthdayShow = true" />
<!-- /个人信息 -->
<!-- 修改名称弹出层 -->
<van-popup v-model="isUpdateNameShow" position="bottom" style="height: 100%">
<update-name
v-if="isUpdateNameShow"
@close="isUpdateNameShow = false"
v-model="user.name"
></update-name>
</van-popup>
<!-- 编辑性别 -->
<van-popup v-model="isUpdateGenderShow" position="bottom">
<update-gender v-if="isUpdateGenderShow" v-model="user.gender" @close="isUpdateGenderShow = false">
</update-gender>
</van-popup>
<!-- 编辑生日 -->
<van-popup v-model="isUpdateBirthdayShow" position="bottom">
<update-birthday v-if="isUpdateBirthdayShow" v-model="user.birthday" @close="isUpdateGenderShow = false">
</update-birthday>
</van-popup>
<!-- 编辑照片 -->
<van-popup v-model="isUpdatePhotoShow" position="bottom" style="height: 100%">
<update-photo v-if="isUpdatePhotoShow" :img="img" @close="isUpdatePhotoShow = false"
@update-photo="user.photo = $event">
</update-photo>
</van-popup>
</div>
</template>
<script>
import { getUserProfile } from '@/api/user'
import UpdateName from './components/update-name.vue'
import UpdateGender from './components/update-gender.vue'
import UpdateBirthday from './components/update-birthday.vue'
import UpdatePhoto from './components/update-photo.vue'
export default {
name: 'UserProfile',
components: {
UpdateName,
UpdateGender,
UpdateBirthday,
UpdatePhoto,
},
props: {},
data () {
return {
user: {}, // 个人信息
isUpdateNameShow: false, // 名称弹出层
isUpdateGenderShow: false, // 年龄
isUpdateBirthdayShow: false, // 生日
isUpdatePhotoShow: false, // 照片
img: null,
}
},
computed: {},
watch: {},
created () {
this.loadGetUserProfile()
},
mounted () {},
methods: {
async loadGetUserProfile() {
try {
const { data } = await getUserProfile()
this.user = data.data
} catch (err) {
this.$toast('获取数据失败')
}
},
onFileChange() {
// 获取文件对象
const file = this.$refs.file.files[0]
// 基于文章对象获取 blob 数据
this.img = window.URL.createObjectURL(file)
this.isUpdatePhotoShow = true
// file-inpit 如果选了同一个文件不会触发 change 事件
// 解决办法就是每次使用完毕, 把 它的 value 清空
this.$refs.file.value = ''
}
}
}
</script>
<style scoped lang="less">
.user-profile {
.van-popup {
background-color: #f5f7f9;
}
.page-nav-bar {
/deep/.van-icon {
color: #fff;
}
}
.avatar-cell {
.van-cell__value {
display: flex;
flex-direction: row-reverse;
}
.avatar {
width: 60px;
height: 60px;
}
}
}
</style>