Skip to content

Commit

Permalink
Merge pull request #116 from KIRAKIRA-DOUGA/feature-2023112601-Avatar…
Browse files Browse the repository at this point in the history
…-cfdxkk

✨ 完善用户头像更新流程,现在可以上传用户本地的头像了
  • Loading branch information
otomad authored Jan 8, 2024
2 parents 2cc88b0 + a420939 commit 691777e
Showing 1 changed file with 95 additions and 25 deletions.
120 changes: 95 additions & 25 deletions pages/settings/profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,99 @@
tags: selfUserInfoStore.tags?.map(tag => tag.labelName),
}));
const userAvatarFileInput = ref<HTMLInputElement>();
/**
* 点击头像事件,模拟点击文件上传并唤起文件资源管理器
*/
function handleUploadAvatarImage() {
userAvatarFileInput.value?.click();
}
const userUploadFile = ref<string | undefined>();
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<InstanceType<typeof ImageCropper>>();
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.
*/
Expand All @@ -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); // 释放内存
</script>

<template>
<Modal v-model="avatarCropperIsOpen">
<div style="width: 20vw; height: 20vw;">
<ImageCropper ref="cropper" :fixed="true" :fixedNumber="[1, 1]" />
<!-- // TODO 使用多语言 -->
<Modal v-model="avatarCropperIsOpen" title="更新头像">
<div class="avatar-cropper">
<ImageCropper ref="cropper" :image="userUploadFile" :fixed="true" :fixedNumber="[1, 1]" />
</div>
<template #footer-right>
<!-- TODO 使用多语言 -->
<!-- // TODO 使用多语言 -->
<Button class="secondary" @click="avatarCropperIsOpen = false">取消</Button>
<!-- TODO 使用多语言 -->
<!-- // TODO 使用多语言 -->
<Button :loading="isUploadingUserAvatar" @click="handleSubmitAvatarImage">更新头像</Button>
</template>
</Modal>
Expand All @@ -91,9 +145,15 @@
<span>{{ t.profile.edit_banner }}</span>
</div>

<div class="change-avatar" @click="handleOpenAvatarCropper">
<div class="change-avatar" @click="handleUploadAvatarImage">
<UserAvatar :avatar="selfUserInfoStore.userAvatar" />
<span>{{ t.profile.edit_avatar }}</span>
<input
ref="userAvatarFileInput"
type="file"
accept="image/*"
hidden
/>
</div>

<div class="items">
Expand Down Expand Up @@ -176,4 +236,14 @@
flex-direction: column;
gap: 1rem;
}
.avatar-cropper {
--avatar-cropper-side-length: 350px;
width: var(--avatar-cropper-side-length);
height: var(--avatar-cropper-side-length);
@media (width <= 450px) {
--avatar-cropper-side-length: 80dvw; /// 对于图片切割器,不建议使用响应式,因为切割器内部被切割的图片不会随之改变尺寸,但考虑到极端小尺寸的适配问题,且只有极少数场景会改变浏览器宽度
}
}
</style>

0 comments on commit 691777e

Please sign in to comment.