mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +08:00 
			
		
		
		
	feat: 新增头像上传前裁剪功能
This commit is contained in:
		| @@ -61,8 +61,8 @@ public class MessageServiceImpl | ||||
|     @Override | ||||
|     public PageDataVO<MessageVO> page(MessageQuery query, PageQuery pageQuery) { | ||||
|         QueryWrapper<MessageDO> queryWrapper = QueryHelper.build(query); | ||||
|         queryWrapper.apply(null != query.getUid(), "msgUser.user_id={0}", query.getUid()); | ||||
|         queryWrapper.apply(null != query.getReadStatus(), "msgUser.read_status={0}", query.getReadStatus()); | ||||
|         queryWrapper.apply(null != query.getUid(), "msgUser.user_id={0}", query.getUid()) | ||||
|             .apply(null != query.getReadStatus(), "msgUser.read_status={0}", query.getReadStatus()); | ||||
|         IPage<MessageVO> page = baseMapper.selectVoPage(pageQuery.toPage(), queryWrapper); | ||||
|         page.getRecords().forEach(this::fill); | ||||
|         return PageDataVO.build(page); | ||||
| @@ -71,8 +71,8 @@ public class MessageServiceImpl | ||||
|     @Override | ||||
|     public List<MessageVO> list(MessageQuery query, SortQuery sortQuery) { | ||||
|         QueryWrapper<MessageDO> queryWrapper = QueryHelper.build(query); | ||||
|         queryWrapper.apply("msgUser.user_id={0}", LoginHelper.getUserId()); | ||||
|         queryWrapper.apply(null != query.getReadStatus(), "msgUser.read_status={0}", query.getReadStatus()); | ||||
|         queryWrapper.apply("msgUser.user_id={0}", LoginHelper.getUserId()).apply(null != query.getReadStatus(), | ||||
|             "msgUser.read_status={0}", query.getReadStatus()); | ||||
|         // 设置排序 | ||||
|         this.sort(queryWrapper, sortQuery); | ||||
|         return baseMapper.selectVoList(queryWrapper); | ||||
| @@ -99,8 +99,8 @@ public class MessageServiceImpl | ||||
|         messageUserService.add(messageId, userIdList); | ||||
|     } | ||||
|  | ||||
|     @Transactional(rollbackFor = Exception.class) | ||||
|     @Override | ||||
|     @Transactional(rollbackFor = Exception.class) | ||||
|     public void delete(List<Long> ids) { | ||||
|         super.delete(ids); | ||||
|         messageUserService.delete(ids); | ||||
|   | ||||
| @@ -46,6 +46,7 @@ | ||||
|     "query-string": "^8.1.0", | ||||
|     "sortablejs": "^1.15.0", | ||||
|     "vue": "^3.3.4", | ||||
|     "vue-cropper": "^1.0.9", | ||||
|     "vue-echarts": "^6.6.1", | ||||
|     "vue-i18n": "^9.5.0", | ||||
|     "vue-json-pretty": "^2.2.4", | ||||
|   | ||||
							
								
								
									
										3
									
								
								continew-admin-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								continew-admin-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -58,6 +58,9 @@ dependencies: | ||||
|   vue: | ||||
|     specifier: ^3.3.4 | ||||
|     version: 3.3.4 | ||||
|   vue-cropper: | ||||
|     specifier: ^1.0.9 | ||||
|     version: 1.0.9 | ||||
|   vue-echarts: | ||||
|     specifier: ^6.6.1 | ||||
|     version: 6.6.1(echarts@5.4.3)(vue@3.3.4) | ||||
|   | ||||
| @@ -12,6 +12,20 @@ export interface AvatarRes { | ||||
|   avatar: string; | ||||
| } | ||||
|  | ||||
| export interface cropperOptions { | ||||
|   autoCrop: boolean; // 是否默认生成截图框 | ||||
|   autoCropWidth: number; // 默认生成截图框宽度 | ||||
|   autoCropHeight: number; // 默认生成截图框高度 | ||||
|   canMove: boolean; // 上传图片是否可以移动  (默认:true) | ||||
|   centerBox: boolean; // 截图框是否被限制在图片里面  (默认:false) | ||||
|   full: boolean; // 是否输出原图比例的截图 选true生成的图片会非常大  (默认:false) | ||||
|   fixed: boolean; // 是否开启截图框宽高固定比例  (默认:false) | ||||
|   fixedBox: boolean; // 固定截图框大小 不允许改变 | ||||
|   img: string | ArrayBuffer | null; // 裁剪图片的地址 | ||||
|   outputSize: number; // 裁剪生成图片的质量  (默认:1) | ||||
|   outputType: string; // 默认生成截图为PNG格式 | ||||
| } | ||||
|  | ||||
| export function uploadAvatar(data: FormData) { | ||||
|   return axios.post<AvatarRes>(`${BASE_URL}/avatar`, data); | ||||
| } | ||||
|   | ||||
| @@ -7,8 +7,7 @@ | ||||
|         :show-file-list="false" | ||||
|         list-type="picture-card" | ||||
|         :show-upload-button="true" | ||||
|         :custom-request="handleUpload" | ||||
|         @change="handleAvatarChange" | ||||
|         :on-before-upload="handleBeforeUpload" | ||||
|       > | ||||
|         <template #upload-button> | ||||
|           <a-avatar :size="100" class="info-avatar"> | ||||
| @@ -22,6 +21,56 @@ | ||||
|         </template> | ||||
|       </a-upload> | ||||
|  | ||||
|       <div class="main"> | ||||
|         <a-modal | ||||
|           :visible="cropperVisible" | ||||
|           width="40%" | ||||
|           :footer="false" | ||||
|           unmount-on-close | ||||
|           render-to-body | ||||
|           @cancel="handleCropperCancel" | ||||
|         > | ||||
|           <a-row> | ||||
|             <a-col :span="14"> | ||||
|               <div style="width: 370px; height: 370px"> | ||||
|                 <!-- 头像裁剪框 --> | ||||
|                 <vue-cropper | ||||
|                   ref="cropper" | ||||
|                   :info="true" | ||||
|                   :img="options.img" | ||||
|                   :full="options.full" | ||||
|                   :fixed="options.fixed" | ||||
|                   :fixed-box="options.fixedBox" | ||||
|                   :can-move="options.canMove" | ||||
|                   :center-box="options.centerBox" | ||||
|                   :auto-crop="options.autoCrop" | ||||
|                   :auto-crop-width="options.autoCropWidth" | ||||
|                   :auto-crop-height="options.autoCropHeight" | ||||
|                   :output-type="options.outputType" | ||||
|                   :output-size="options.outputSize" | ||||
|                   @realTime="realTime" | ||||
|                 /> | ||||
|               </div> | ||||
|             </a-col> | ||||
|             <a-col :span="6"> | ||||
|               <!-- 实时预览 --> | ||||
|               <div :style="previewStyle"> | ||||
|                 <div :style="previews.div"> | ||||
|                   <img :src="previews.url" :style="previews.img" alt="" /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </a-col> | ||||
|           </a-row> | ||||
|           <br /> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="handleUpload">提交</a-button> | ||||
|             <a-button type="outline" @click="handleCropperCancel" | ||||
|               >取消</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-modal> | ||||
|       </div> | ||||
|  | ||||
|       <a-descriptions | ||||
|         :column="2" | ||||
|         :label-style="{ | ||||
| @@ -70,15 +119,22 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { getCurrentInstance, ref } from 'vue'; | ||||
|   import { FileItem, RequestOption } from '@arco-design/web-vue'; | ||||
|   import { uploadAvatar } from '@/api/system/user-center'; | ||||
|   import { useUserStore } from '@/store'; | ||||
|   import { reactive, ref, getCurrentInstance } from 'vue'; | ||||
|   import { FileItem } from '@arco-design/web-vue'; | ||||
|   import { uploadAvatar, cropperOptions } from '@/api/system/user-center'; | ||||
|   import getAvatar from '@/utils/avatar'; | ||||
|   import { useUserStore } from '@/store'; | ||||
|   import { VueCropper } from 'vue-cropper'; | ||||
|   import 'vue-cropper/dist/index.css'; | ||||
|  | ||||
|   const fileRef = ref(reactive({ name: 'avatar.png' })); | ||||
|   const previews: any = ref({}); | ||||
|   const previewStyle: any = ref({}); | ||||
|   const cropperVisible = ref(false); | ||||
|   const cropper = ref(); | ||||
|   const { proxy } = getCurrentInstance() as any; | ||||
|  | ||||
|   const userStore = useUserStore(); | ||||
|  | ||||
|   const avatar = { | ||||
|     uid: '-2', | ||||
|     name: 'avatar.png', | ||||
| @@ -86,52 +142,75 @@ | ||||
|   }; | ||||
|   const avatarList = ref<FileItem[]>([avatar]); | ||||
|  | ||||
|   const options: cropperOptions = reactive({ | ||||
|     autoCrop: true, | ||||
|     autoCropWidth: 200, | ||||
|     autoCropHeight: 200, | ||||
|     canMove: true, | ||||
|     centerBox: true, | ||||
|     full: false, | ||||
|     fixed: false, | ||||
|     fixedBox: false, | ||||
|     img: '', | ||||
|     outputSize: 1, | ||||
|     outputType: 'png', | ||||
|   }); | ||||
|  | ||||
|   /** | ||||
|    * 上传头像 | ||||
|    * 上传前弹出裁剪框 | ||||
|    * | ||||
|    * @param options 选项 | ||||
|    * @param file 头像 | ||||
|    */ | ||||
|   const handleUpload = (options: RequestOption) => { | ||||
|     const controller = new AbortController(); | ||||
|     (async function requestWrap() { | ||||
|       const { | ||||
|         onProgress, | ||||
|         onError, | ||||
|         onSuccess, | ||||
|         fileItem, | ||||
|         name = 'avatarFile', | ||||
|       } = options; | ||||
|       onProgress(20); | ||||
|       const formData = new FormData(); | ||||
|       formData.append(name as string, fileItem.file as Blob); | ||||
|       uploadAvatar(formData) | ||||
|         .then((res) => { | ||||
|           onSuccess(res); | ||||
|           userStore.avatar = res.data.avatar; | ||||
|           proxy.$message.success(res.msg); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           onError(error); | ||||
|         }); | ||||
|     })(); | ||||
|     return { | ||||
|       abort() { | ||||
|         controller.abort(); | ||||
|       }, | ||||
|   const handleBeforeUpload = (file: File): boolean => { | ||||
|     fileRef.value = file; | ||||
|     const reader = new FileReader(); | ||||
|     reader.readAsDataURL(file); | ||||
|     reader.onload = () => { | ||||
|       options.img = reader.result; | ||||
|     }; | ||||
|     cropperVisible.value = true; | ||||
|     return false; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 切换头像 | ||||
|    * | ||||
|    * @param fileItemList 文件列表 | ||||
|    * @param currentFile 当前文件 | ||||
|    * 关闭裁剪框 | ||||
|    */ | ||||
|   const handleAvatarChange = ( | ||||
|     fileItemList: FileItem[], | ||||
|     currentFile: FileItem | ||||
|   ) => { | ||||
|     avatarList.value = [currentFile]; | ||||
|   const handleCropperCancel = () => { | ||||
|     fileRef.value = { name: '' }; | ||||
|     options.img = ''; | ||||
|     cropperVisible.value = false; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 上传头像 | ||||
|    */ | ||||
|   const handleUpload = () => { | ||||
|     cropper.value.getCropBlob((data: string | Blob) => { | ||||
|       const formData = new FormData(); | ||||
|       formData.append('avatarFile', data, fileRef.value?.name); | ||||
|       uploadAvatar(formData).then((res) => { | ||||
|         userStore.avatar = res.data.avatar; | ||||
|         avatarList.value[0].url = getAvatar(res.data.avatar, undefined); | ||||
|         proxy.$message.success(res.msg); | ||||
|         handleCropperCancel(); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * 实时预览 | ||||
|    * @param data data | ||||
|    */ | ||||
|   const realTime = (data: any) => { | ||||
|     previewStyle.value = { | ||||
|       width: `${data.w}px`, | ||||
|       height: `${data.h}px`, | ||||
|       overflow: 'hidden', | ||||
|       margin: '0', | ||||
|       zoom: 0.8, | ||||
|       borderRadius: '50%', | ||||
|     }; | ||||
|     previews.value = data; | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| @@ -152,4 +231,8 @@ | ||||
|       font-size: 14px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .main { | ||||
|     position: relative; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Bull-BCLS
					Bull-BCLS