UserAvatar.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <template>
  2. <div class="user-info-head" @click="editCropper()">
  3. <img :src="props.img" title="点击上传头像" class="img-circle img-lg" alt="" />
  4. </div>
  5. <el-dialog
  6. v-model="dialogVisible"
  7. title="编辑头像"
  8. :mask-closable="false"
  9. width="800px"
  10. append-to-body
  11. @opened="cropperVisible = true"
  12. >
  13. <el-row>
  14. <el-col :xs="24" :md="12" :style="{ height: '350px' }">
  15. <VueCropper
  16. ref="cropper"
  17. v-if="cropperVisible"
  18. :img="options.img"
  19. :info="true"
  20. :infoTrue="options.infoTrue"
  21. :autoCrop="options.autoCrop"
  22. :autoCropWidth="options.autoCropWidth"
  23. :autoCropHeight="options.autoCropHeight"
  24. :fixedNumber="options.fixedNumber"
  25. :fixedBox="options.fixedBox"
  26. :centerBox="options.centerBox"
  27. @realTime="realTime"
  28. />
  29. </el-col>
  30. <el-col :xs="24" :md="12" :style="{ height: '350px' }">
  31. <div
  32. class="avatar-upload-preview"
  33. :style="{
  34. width: previews.w + 'px',
  35. height: previews.h + 'px',
  36. overflow: 'hidden',
  37. margin: '5px'
  38. }"
  39. >
  40. <div :style="previews.div">
  41. <img :src="previews.url" :style="previews.img" style="!max-width: 100%" alt="" />
  42. </div>
  43. </div>
  44. </el-col>
  45. </el-row>
  46. <template #footer>
  47. <el-row>
  48. <el-col :lg="2" :md="2">
  49. <el-upload
  50. action="#"
  51. :http-request="requestUpload"
  52. :show-file-list="false"
  53. :before-upload="beforeUpload"
  54. >
  55. <el-button size="small">
  56. <Icon icon="ep:upload-filled" class="mr-5px" />
  57. 选择
  58. </el-button>
  59. </el-upload>
  60. </el-col>
  61. <el-col :lg="{ span: 1, offset: 2 }" :md="2">
  62. <el-button size="small" @click="changeScale(1)">
  63. <Icon icon="ep:zoom-in" class="mr-5px" />
  64. </el-button>
  65. </el-col>
  66. <el-col :lg="{ span: 1, offset: 1 }" :md="2">
  67. <el-button size="small" @click="changeScale(-1)">
  68. <Icon icon="ep:zoom-out" class="mr-5px" />
  69. </el-button>
  70. </el-col>
  71. <el-col :lg="{ span: 1, offset: 1 }" :md="2">
  72. <el-button size="small" @click="rotateLeft()">
  73. <Icon icon="ep:arrow-left-bold" class="mr-5px" />
  74. </el-button>
  75. </el-col>
  76. <el-col :lg="{ span: 1, offset: 1 }" :md="2">
  77. <el-button size="small" @click="rotateRight()">
  78. <Icon icon="ep:arrow-right-bold" class="mr-5px" />
  79. </el-button>
  80. </el-col>
  81. <el-col :lg="{ span: 2, offset: 6 }" :md="2">
  82. <el-button size="small" type="primary" @click="uploadImg()">提 交</el-button>
  83. </el-col>
  84. </el-row>
  85. </template>
  86. </el-dialog>
  87. </template>
  88. <script setup lang="ts">
  89. import { ref, reactive, watch, Ref, UnwrapNestedRefs } from 'vue'
  90. import 'vue-cropper/dist/index.css'
  91. import VueCropper from 'vue-cropper/lib/vue-cropper.vue'
  92. import { ElRow, ElCol, ElUpload, ElMessage, ElDialog } from 'element-plus'
  93. import { propTypes } from '@/utils/propTypes'
  94. import { uploadAvatarApi } from '@/api/system/user/profile'
  95. const cropper = ref()
  96. const dialogVisible = ref(false)
  97. const cropperVisible = ref(false)
  98. const props = defineProps({
  99. img: propTypes.string.def('')
  100. })
  101. interface Options {
  102. img: string | ArrayBuffer | null // 裁剪图片的地址
  103. info: true // 裁剪框的大小信息
  104. outputSize: number // 裁剪生成图片的质量 [1至0.1]
  105. outputType: 'jpeg' // 裁剪生成图片的格式
  106. canScale: boolean // 图片是否允许滚轮缩放
  107. autoCrop: boolean // 是否默认生成截图框
  108. autoCropWidth: number // 默认生成截图框宽度
  109. autoCropHeight: number // 默认生成截图框高度
  110. fixedBox: boolean // 固定截图框大小 不允许改变
  111. fixed: boolean // 是否开启截图框宽高固定比例
  112. fixedNumber: Array<number> // 截图框的宽高比例 需要配合centerBox一起使用才能生效
  113. full: boolean // 是否输出原图比例的截图
  114. canMoveBox: boolean // 截图框能否拖动
  115. original: boolean // 上传图片按照原始比例渲染
  116. centerBox: boolean // 截图框是否被限制在图片里面
  117. infoTrue: boolean // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
  118. }
  119. const options: UnwrapNestedRefs<Options> = reactive({
  120. img: '', // 需要剪裁的图片
  121. autoCrop: true, // 是否默认生成截图框
  122. autoCropWidth: 200, // 默认生成截图框的宽度
  123. autoCropHeight: 200, // 默认生成截图框的长度
  124. fixedBox: false, // 是否固定截图框的大小 不允许改变
  125. info: true, // 裁剪框的大小信息
  126. outputSize: 1, // 裁剪生成图片的质量 [1至0.1]
  127. outputType: 'jpeg', // 裁剪生成图片的格式
  128. canScale: false, // 图片是否允许滚轮缩放
  129. fixed: true, // 是否开启截图框宽高固定比例
  130. fixedNumber: [1, 1], // 截图框的宽高比例 需要配合centerBox一起使用才能生效
  131. full: true, // 是否输出原图比例的截图
  132. canMoveBox: false, // 截图框能否拖动
  133. original: false, // 上传图片按照原始比例渲染
  134. centerBox: true, // 截图框是否被限制在图片里面
  135. infoTrue: true // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
  136. })
  137. const previews: Ref<any> = ref({})
  138. /** 编辑头像 */
  139. const editCropper = () => {
  140. dialogVisible.value = true
  141. }
  142. /** 向左旋转 */
  143. const rotateLeft = () => {
  144. cropper.value.rotateLeft()
  145. }
  146. /** 向右旋转 */
  147. const rotateRight = () => {
  148. cropper.value.rotateRight()
  149. }
  150. /** 图片缩放 */
  151. const changeScale = (num: number) => {
  152. num = num || 1
  153. cropper.value.changeScale(num)
  154. }
  155. // 覆盖默认的上传行为
  156. const requestUpload: any = () => {}
  157. /** 上传预处理 */
  158. const beforeUpload = (file: Blob) => {
  159. if (file.type.indexOf('image/') == -1) {
  160. ElMessage('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
  161. } else {
  162. const reader = new FileReader()
  163. // 转化为base64
  164. reader.readAsDataURL(file)
  165. reader.onload = () => {
  166. if (reader.result) {
  167. // 获取到需要剪裁的图片 展示到剪裁框中
  168. options.img = reader.result as string
  169. }
  170. return false
  171. }
  172. }
  173. }
  174. /** 上传图片 */
  175. const uploadImg = () => {
  176. cropper.value.getCropBlob((data: any) => {
  177. let formData = new FormData()
  178. formData.append('avatarFile', data)
  179. uploadAvatarApi(formData).then((res) => {
  180. options.img = res
  181. window.location.reload()
  182. })
  183. dialogVisible.value = false
  184. cropperVisible.value = false
  185. })
  186. }
  187. /** 实时预览 */
  188. const realTime = (data: any) => {
  189. previews.value = data
  190. }
  191. watch(
  192. () => props.img,
  193. () => {
  194. if (props.img) {
  195. options.img = props.img
  196. previews.value.img = props.img
  197. previews.value.url = props.img
  198. }
  199. }
  200. )
  201. </script>
  202. <style scoped>
  203. .user-info-head {
  204. position: relative;
  205. display: inline-block;
  206. }
  207. .img-circle {
  208. border-radius: 50%;
  209. }
  210. .img-lg {
  211. width: 120px;
  212. height: 120px;
  213. }
  214. .avatar-upload-preview {
  215. position: absolute;
  216. top: 50%;
  217. -webkit-transform: translate(50%, -50%);
  218. transform: translate(50%, -50%);
  219. width: 200px;
  220. height: 200px;
  221. border-radius: 50%;
  222. -webkit-box-shadow: 0 0 4px #ccc;
  223. box-shadow: 0 0 4px #ccc;
  224. overflow: hidden;
  225. }
  226. .user-info-head:hover:after {
  227. content: '+';
  228. position: absolute;
  229. left: 0;
  230. right: 0;
  231. top: 0;
  232. bottom: 0;
  233. color: #eee;
  234. background: rgba(0, 0, 0, 0.5);
  235. font-size: 24px;
  236. font-style: normal;
  237. -webkit-font-smoothing: antialiased;
  238. -moz-osx-font-smoothing: grayscale;
  239. cursor: pointer;
  240. line-height: 110px;
  241. border-radius: 50%;
  242. }
  243. </style>