diff --git a/pages/settings/profile.vue b/pages/settings/profile.vue index f027baee..8655fbdb 100755 --- a/pages/settings/profile.vue +++ b/pages/settings/profile.vue @@ -12,51 +12,99 @@ tags: selfUserInfoStore.tags?.map(tag => tag.labelName), })); + const userAvatarFileInput = ref(); + /** + * 点击头像事件,模拟点击文件上传并唤起文件资源管理器 + */ + function handleUploadAvatarImage() { + userAvatarFileInput.value?.click(); + } + + const userUploadFile = ref(); const avatarCropperIsOpen = ref(false); - const handleOpenAvatarCropper = () => { - avatarCropperIsOpen.value = true; - }; - const cropper = ref(); + /** + * 如果有上传图片,则开启图片裁切器(即:用户选择了本地文件的事件) + * @param e 应为用户上传文件的 input 元素的 change 事件 + */ + function handleOpenAvatarCropper(e?: Event) { + const fileInput = e?.target as HTMLInputElement | undefined; + if (fileInput?.files?.[0]) { + const image = fileInput.files[0]; + + if (!/\.(a?png|gif|jpe?g|webp|svg)$/i.test(fileInput.value)) { + useToast("只能上传图片文件!", "error"); // TODO 使用多语言 + console.error("ERROR", "上传的头像文件格式不合法!"); + return; + } + + if (image) { + userUploadFile.value = fileToBlob(image); + avatarCropperIsOpen.value = true; + fileInput.value = ""; // 读取完用户上传的文件后,需要清空 input,以免用户在下次上传同一个文件时无法触发 change 事件 + } + } + } + + const cropper = ref>(); const isUploadingUserAvatar = ref(false); - const handleSubmitAvatarImage = async () => { + + /** + * 修改头像事件,向服务器提交新的图片 + */ + async function handleSubmitAvatarImage() { try { isUploadingUserAvatar.value = true; - const blobImageData = await cropper.value.getCropBlobData() as Blob; - const userAvatarUploadSignedUrlResult = await api.user.getUserAvatarUploadSignedUrl(); - const userAvatarUploadSignedUrl = userAvatarUploadSignedUrlResult.userAvatarUploadSignedUrl; - if (userAvatarUploadSignedUrlResult.success && userAvatarUploadSignedUrl) { - const uploadResult = await api.user.uploadUserAvatar(blobImageData, userAvatarUploadSignedUrl); - if (uploadResult) { - await api.user.getSelfUserInfo(); - avatarCropperIsOpen.value = false; + const blobImageData = await cropper.value?.getCropBlobData(); + if (blobImageData) { + const userAvatarUploadSignedUrlResult = await api.user.getUserAvatarUploadSignedUrl(); + const userAvatarUploadSignedUrl = userAvatarUploadSignedUrlResult.userAvatarUploadSignedUrl; + if (userAvatarUploadSignedUrlResult.success && userAvatarUploadSignedUrl) { + const uploadResult = await api.user.uploadUserAvatar(blobImageData, userAvatarUploadSignedUrl); + if (uploadResult) { + await api.user.getSelfUserInfo(); + avatarCropperIsOpen.value = false; + clearBlobUrl(); // 释放内存 + } + isUploadingUserAvatar.value = false; } - isUploadingUserAvatar.value = false; + } else { + useToast("无法获取裁切后的图片", "error"); // TODO 使用多语言 + console.error("ERROR", "无法获取裁切后的图片"); } } catch (error) { + useToast("头像上传失败", "error"); // TODO 使用多语言 console.error("ERROR", "在上传用户头像时出错"); isUploadingUserAvatar.value = false; } - }; + } /** * 根据 cookie 中的 uid 和 token 来获取用户信息(同时具有验证用户 token 的功能) */ - const getUserInfo = async () => { + async function getUserInfo() { try { await api.user.getSelfUserInfo(); } catch (error) { console.error("无法获取用户信息,请尝试重新登录", error); } - }; + } + + /** + * 清除已经上传完成的图片,释放内存 + */ + function clearBlobUrl() { + if (userUploadFile.value) { + URL.revokeObjectURL(userUploadFile.value); + userUploadFile.value = undefined; + } + } useListen("user:login", async loginStatus => { if (loginStatus) await getUserInfo(); }); - onMounted(async () => { await getUserInfo(); }); - /** * Update the user profile. */ @@ -72,17 +120,23 @@ // await api?.updateProfile(encodedName, encodedGender, profile.birthday.toString(), encodedBio); // } catch (error) { handleError(error); } } + + // 监听头像文件变化事件 + useEventListener(userAvatarFileInput, "change", handleOpenAvatarCropper); + onMounted(async () => { await getUserInfo(); }); + onBeforeUnmount(clearBlobUrl); // 释放内存