您好,欢迎访问代理记账网站
移动应用 微信公众号 联系我们

咨询热线 -

电话 15988168888

联系客服
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

Vue 移动端 实战九 编辑用户资料、修改呢称、生日、性别、头像、以及头像的剪裁器依赖使用、Content-Type 要求的 multipart/form-data 格式

编辑用户资料

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>



分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进