From e32edfeec3d2e9dbffefe45c84fbf679202ea562 Mon Sep 17 00:00:00 2001 From: cfdxkk Date: Fri, 17 May 2024 18:58:36 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E7=94=A8=E6=88=B7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=20+=20=E6=AC=A2?= =?UTF-8?q?=E8=BF=8E=E9=A1=B5=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E5=A1=AB?= =?UTF-8?q?=E5=86=99=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :sparkles: 1. 设置页用户信息更新功能 2. 欢迎页用户信息填写 / 重置功能 3. 更新全新头像上传流程,点击提交后头像不会直接更新而是最终确认更新才会更新。 --- components/Settings/SettingsUserProfile.vue | 29 +++- composables/api/User/UserController.ts | 34 ++-- composables/api/User/UserControllerDto.d.ts | 24 ++- pages/settings/profile.vue | 129 ++++++++++----- pages/welcome.vue | 170 ++++++++++++++++---- stores/user-info.ts | 6 +- types/backend.d.ts | 6 +- 7 files changed, 298 insertions(+), 100 deletions(-) diff --git a/components/Settings/SettingsUserProfile.vue b/components/Settings/SettingsUserProfile.vue index 27f50ddf..c056ca88 100644 --- a/components/Settings/SettingsUserProfile.vue +++ b/components/Settings/SettingsUserProfile.vue @@ -9,6 +9,7 @@ const validChar = makeUsername(); const profile = defineModel<{ name: string; + nickname: string; nameValid?: boolean; bio: string; gender: string; @@ -48,16 +49,33 @@ @@ -110,8 +129,8 @@ .text-box:not(.normal) { --size: large; } - - .name { + + .name, .nickname { display: flex; flex-direction: column; gap: 8px; @@ -125,11 +144,11 @@ .gender { display: flex; + row-gap: 1rem; align-items: center; min-height: 36px; padding: 0 12px; color: c(icon-color); - row-gap: 1rem; &, * { diff --git a/composables/api/User/UserController.ts b/composables/api/User/UserController.ts index 75a61690..5c2c56d6 100644 --- a/composables/api/User/UserController.ts +++ b/composables/api/User/UserController.ts @@ -1,6 +1,6 @@ import { GET, POST, uploadFile2CloudflareImages } from "api/Common"; import getCorrectUri from "api/Common/getCorrectUri"; -import type { CheckUserTokenResponseDto, GetSelfUserInfoRequestDto, GetSelfUserInfoResponseDto, GetUserAvatarUploadSignedUrlResponseDto, GetUserInfoByUidRequestDto, GetUserInfoByUidResponseDto, GetUserSettingsRequestDto, GetUserSettingsResponseDto, UpdateOrCreateUserSettingsRequestDto, UpdateOrCreateUserSettingsResponseDto, UpdateUserEmailRequestDto, UserExistsCheckRequestDto, UserExistsCheckResponseDto, UserLoginRequestDto, UserLoginResponseDto, UserRegistrationRequestDto, UserRegistrationResponseDto } from "./UserControllerDto"; +import type { CheckUserTokenResponseDto, GetSelfUserInfoRequestDto, GetSelfUserInfoResponseDto, GetUserAvatarUploadSignedUrlResponseDto, GetUserInfoByUidRequestDto, GetUserInfoByUidResponseDto, GetUserSettingsRequestDto, GetUserSettingsResponseDto, UpdateOrCreateUserInfoResponseDto, UpdateOrCreateUserSettingsRequestDto, UpdateOrCreateUserSettingsResponseDto, UpdateUserEmailRequestDto, UpdateUserEmailResponseDto, UserExistsCheckRequestDto, UserExistsCheckResponseDto, UserLoginRequestDto, UserLoginResponseDto, UserRegistrationRequestDto, UserRegistrationResponseDto } from "./UserControllerDto"; const BACK_END_URL = getCorrectUri(); const USER_API_URI = `${BACK_END_URL}/user`; @@ -36,11 +36,20 @@ export const userExistsCheck = async (userExistsCheckRequest: UserExistsCheckReq /** * 用户更改邮箱 - * @param updateUserEmailRequest 用户更改邮箱的请求的参数 + * @param updateUserEmailRequest 用户更改邮箱的请求的请求载荷 * @returns 用户更改邮箱返回的参数 */ -export const updateUserEmail = async (updateUserEmailRequest: UpdateUserEmailRequestDto): Promise => { - return await POST(`${USER_API_URI}/update/email`, updateUserEmailRequest) as UserLoginResponseDto; +export const updateUserEmail = async (updateUserEmailRequest: UpdateUserEmailRequestDto): Promise => { + return await POST(`${USER_API_URI}/update/email`, updateUserEmailRequest, { credentials: "include" }) as UpdateUserEmailResponseDto; +}; + +/** + * 创建或更新用户信息 + * @param updateOrCreateUserInfoRequest 创建或更新用户信息的请求载荷 + * @returns 创建或更新用户信息的响应结果 + */ +export const updateOrCreateUserInfo = async (updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto): Promise => { + return await POST(`${USER_API_URI}/update/info`, updateOrCreateUserInfoRequest, { credentials: "include" }) as UpdateOrCreateUserInfoResponseDto; }; /** @@ -52,15 +61,17 @@ export const updateUserEmail = async (updateUserEmailRequest: UpdateUserEmailReq export const getSelfUserInfo = async (getSelfUserInfoRequest?: GetSelfUserInfoRequestDto): Promise => { // TODO: use { credentials: "include" } to allow save/read cookies from cross-origin domains. Maybe we should remove it before deployment to production env. const selfUserInfo = await POST(`${USER_API_URI}/self`, getSelfUserInfoRequest, { credentials: "include" }) as GetSelfUserInfoResponseDto; - if (selfUserInfo.success) { + const selfUserInfoResult = selfUserInfo.result; + if (selfUserInfo.success && selfUserInfoResult) { const selfUserInfoStore = useSelfUserInfoStore(); selfUserInfoStore.isLogined = true; - selfUserInfoStore.uid = selfUserInfo.result?.uid; - selfUserInfoStore.userAvatar = selfUserInfo.result?.avatar || ""; - selfUserInfoStore.username = selfUserInfo.result?.username || "User"; // TODO: 使用多语言,为未设置用户名的用户提供国际化的缺省用户名 - selfUserInfoStore.gender = selfUserInfo.result?.gender || ""; - selfUserInfoStore.signature = selfUserInfo.result?.signature || ""; - selfUserInfoStore.tags = selfUserInfo.result?.label || []; + selfUserInfoStore.uid = selfUserInfoResult.uid; + selfUserInfoStore.userAvatar = selfUserInfoResult.avatar || ""; + selfUserInfoStore.username = selfUserInfoResult.username || "Anonymous"; // TODO: 使用多语言,为未设置用户名的用户提供国际化的缺省用户名 + selfUserInfoStore.userNickname = selfUserInfoResult.userNickname || "Anonymous"; // TODO: 使用多语言,为未设置用户昵称的用户提供国际化的缺省用户昵称 + selfUserInfoStore.gender = selfUserInfoResult.gender || ""; + selfUserInfoStore.signature = selfUserInfoResult.signature || ""; + selfUserInfoStore.tags = selfUserInfoResult.label?.map(label => label.labelName) || []; } return selfUserInfo; }; @@ -95,6 +106,7 @@ export const userLogout = async (): Promise => { selfUserInfoStore.uid = undefined; selfUserInfoStore.userAvatar = ""; selfUserInfoStore.username = ""; + selfUserInfoStore.userNickname = ""; selfUserInfoStore.gender = ""; selfUserInfoStore.signature = ""; selfUserInfoStore.tags = []; diff --git a/composables/api/User/UserControllerDto.d.ts b/composables/api/User/UserControllerDto.d.ts index 48860ac4..fed7cf0f 100644 --- a/composables/api/User/UserControllerDto.d.ts +++ b/composables/api/User/UserControllerDto.d.ts @@ -110,8 +110,6 @@ export type BeforeHashPasswordDataType = { passwordHash: string; }; - - /** * 用户的个人标签 */ @@ -170,7 +168,6 @@ export type UpdateOrCreateUserInfoRequestDto = { userWebsite?: UserWebsite; }; - /** * 更新或创建用户信息的请求结果 */ @@ -183,8 +180,6 @@ export type UpdateOrCreateUserInfoResponseDto = { result?: {} & UpdateOrCreateUserInfoRequestDto; }; - - /** * 获取当前登录的用户信息的请求参数 */ @@ -282,8 +277,8 @@ export type GetUserAvatarUploadSignedUrlResponseDto = { type UserLinkAccountsPrivacySettingDto = { /** 关联账户类型 - 非空 - 例:"X" */ accountType: string; - /** 显示方式 - 非空 - 允许的值有:{public: 公开, following: 仅关注, private: 隐藏}; */ - privacyType: 'public' | 'following' | 'private'; + /** 显示方式 - 非空 - 允许的值有:{public: 公开, following: 仅关注, private: 隐藏} */ + privacyType: "public" | "following" | "private"; }; /** @@ -292,8 +287,8 @@ type UserLinkAccountsPrivacySettingDto = { export type BasicUserSettingsDto = { /** 是否启用 Cookie - 布尔 */ enableCookie?: boolean; - /** 主题外观设置(主题类型) - 可选的值:{light: 浅色, dark: 深色, system: 跟随系统}; */ - themeType?: 'light' | 'dark' | 'system'; + /** 主题外观设置(主题类型) - 可选的值:{light: 浅色, dark: 深色, system: 跟随系统} */ + themeType?: "light" | "dark" | "system"; /** 主题颜色 - 字符串,颜色字符串 */ themeColor?: string; /** 用户自定义主题颜色 - 字符串,HAX 颜色字符串,不包含井号 */ @@ -303,7 +298,7 @@ export type BasicUserSettingsDto = { /** 是否启用彩色导航栏 - 布尔 */ coloredSideBar?: boolean; /** 流量使用偏好 - 字符串,{standard: 标准, limit: 节省网络流量模式, preview: 超前加载} */ - dataSaverMode?: 'standard' | 'limit' | 'preview'; + dataSaverMode?: "standard" | "limit" | "preview"; /** 禁用搜索推荐 - 布尔 */ noSearchRecommendations?: boolean; /** 禁用相关视频推荐 - 布尔 */ @@ -328,8 +323,8 @@ export type BasicUserSettingsDto = { sharpAppearanceMode?: boolean; /** 实验性:启用扁平模式 - 布尔 */ flatAppearanceMode?: boolean; - /** 用户关联网站的隐私设置 - 允许的值有:{public: 公开, following: 仅关注, private: 隐藏}; */ - userWebsitePrivacySetting?: 'public' | 'following' | 'private'; + /** 用户关联网站的隐私设置 - 允许的值有:{public: 公开, following: 仅关注, private: 隐藏} */ + userWebsitePrivacySetting?: "public" | "following" | "private"; /** 用户关联账户的隐私设置 */ userLinkAccountsPrivacySetting?: UserLinkAccountsPrivacySettingDto[]; }; @@ -337,7 +332,7 @@ export type BasicUserSettingsDto = { /** * 获取用于渲染页面的用户设定的请求参数 */ -export type GetUserSettingsRequestDto = {} & GetSelfUserInfoRequestDto +export type GetUserSettingsRequestDto = {} & GetSelfUserInfoRequestDto; /** * 获取用于渲染页面的用户设定的请求响应 @@ -351,11 +346,10 @@ export type GetUserSettingsResponseDto = { message?: string; }; - /** * 更新或创建用户设定的请求参数 */ -export type UpdateOrCreateUserSettingsRequestDto = {} & BasicUserSettingsDto +export type UpdateOrCreateUserSettingsRequestDto = {} & BasicUserSettingsDto; /** * 更新或创建用户设定的请求响应 diff --git a/pages/settings/profile.vue b/pages/settings/profile.vue index c76316ce..dce2584a 100755 --- a/pages/settings/profile.vue +++ b/pages/settings/profile.vue @@ -4,15 +4,24 @@ // const avatar = "/static/images/avatars/aira.webp"; const selfUserInfoStore = useSelfUserInfoStore(); - const profile = computed(() => ({ + const newAvatar = ref(); // 新上传的头像 + const correctAvatar = computed(() => newAvatar.value ?? selfUserInfoStore.userAvatar); // 正确显示的头像(如果用户没有新上传头像,则使用全局变量中的旧头像) + const userAvatarUploadFile = ref(); // 用户上传的头像文件 Blob + const isAvatarCropperOpen = ref(false); // 用户头像图片裁剪器是否开启 + const userAvatarFileInput = ref(); // 隐藏的图片上传 Input 元素 + const isUpdateUserInfo = ref(false); // 是否正在上传用户信息 + const isResetUserInfo = ref(false); // 是否正在重置用户信息 + const profile = reactive({ name: selfUserInfoStore.username, + nickname: selfUserInfoStore.userNickname, bio: selfUserInfoStore.signature, gender: selfUserInfoStore.gender, - birthday: new Date(), // FIXME: 注意:这个值是静态的、非响应式的,不会随时间变化 - tags: selfUserInfoStore.tags?.map(tag => tag.labelName), - })); + birthday: new Date(), // TODO: 日期选择器 // FIXME: 注意:这个值是静态的、非响应式的,不会随时间变化 + tags: selfUserInfoStore.tags, + }); + const cropper = ref>(); // 图片裁剪器实例 + const isUploadingUserAvatar = ref(false); // 是否正在上传头像 - const userAvatarFileInput = ref(); /** * 点击头像事件,模拟点击文件上传并唤起文件资源管理器 */ @@ -20,9 +29,6 @@ userAvatarFileInput.value?.click(); } - const userUploadFile = ref(); - const isAvatarCropperOpen = ref(false); - /** * 如果有上传图片,则开启图片裁切器。 * @@ -30,6 +36,7 @@ * @param e - 应为用户上传文件的 `` 元素的 change 事件。 */ function handleOpenAvatarCropper(e?: Event) { + e?.stopPropagation(); const fileInput = e?.target as HTMLInputElement | undefined; const image = fileInput?.files?.[0]; @@ -40,15 +47,12 @@ return; } - userUploadFile.value = fileToBlob(image); + userAvatarUploadFile.value = fileToBlob(image); isAvatarCropperOpen.value = true; fileInput.value = ""; // 读取完用户上传的文件后,需要清空 input,以免用户在下次上传同一个文件时无法触发 change 事件。 } } - const cropper = ref>(); - const isUploadingUserAvatar = ref(false); - /** * 修改头像事件,向服务器提交新的图片。 */ @@ -63,7 +67,7 @@ if (userAvatarUploadSignedUrlResult.success && userAvatarUploadSignedUrl && userAvatarUploadFilename) { const uploadResult = await api.user.uploadUserAvatar(userAvatarUploadFilename, blobImageData, userAvatarUploadSignedUrl); if (uploadResult) { - await api.user.getSelfUserInfo(); + newAvatar.value = userAvatarUploadFilename; isAvatarCropperOpen.value = false; clearBlobUrl(); // 释放内存 } @@ -97,37 +101,87 @@ * 清除已经上传完成的图片,释放内存。 */ function clearBlobUrl() { - if (userUploadFile.value) { - URL.revokeObjectURL(userUploadFile.value); - userUploadFile.value = undefined; + if (userAvatarUploadFile.value) { + URL.revokeObjectURL(userAvatarUploadFile.value); + userAvatarUploadFile.value = undefined; } } - useListen("user:login", async loginStatus => { - if (loginStatus) - await getUserInfo(); - }); - /** * Update the user profile. */ async function updateProfile() { - // const api = useApi(); + isUpdateUserInfo.value = true; + const updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto = { + avatar: correctAvatar.value, + username: profile.name, + userNickname: profile.nickname, + signature: profile.bio, + gender: profile.gender, + userBirthday: new Date().getTime(), // TODO: 日期选择器 // FIXME: 注意:这个值是静态的、非响应式的,不会随时间变化 + label: profile.tags.map((tag, index) => ({ id: index, labelName: tag })), + }; + try { + const updateOrCreateUserInfoResult = await api.user.updateOrCreateUserInfo(updateOrCreateUserInfoRequest); + if (updateOrCreateUserInfoResult.success) { + await api.user.getSelfUserInfo(); + isUpdateUserInfo.value = false; + } + } catch (error) { + isUpdateUserInfo.value = false; + useToast("用户信息更新失败!", "error"); // TODO: 使用多语言 + console.error("用户信息更新失败!", error); + } + } - // const encodedName = encodeUtf8(profile.name); - // const encodedGender = encodeUtf8(profile.gender); - // const encodedBio = encodeUtf8(profile.bio); - // const handleError = (error: unknown) => error && console.error(error); + /** + * reset all user info. + * 重置用户设置 + * 请求旧用户信息,并修改 Pinia 中的用户数据,然后触发上方的监听 + */ + async function reset() { + isResetUserInfo.value = true; + const updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto = { + avatar: "", + username: "", + userNickname: "", + signature: "", + gender: "", + userBirthday: undefined, + label: [], + }; + try { + const updateOrCreateUserInfoResult = await api.user.updateOrCreateUserInfo(updateOrCreateUserInfoRequest); + if (updateOrCreateUserInfoResult.success) { + await api.user.getSelfUserInfo(); + isResetUserInfo.value = false; + } + } catch (error) { + isResetUserInfo.value = false; + useToast("未能清空用户信息!", "error"); // TODO: 使用多语言 + console.error("未能清空用户信息!", error); + } + } - // try { - // await api?.updateProfile(encodedName, encodedGender, profile.birthday.toString(), encodedBio); - // } catch (error) { handleError(error); } + /** + * 将 Pinia 中的用户数据拷贝到当前组件的响应式变量 "profile" 中 + */ + function copyPiniaUserInfo2Profile() { + profile.name = selfUserInfoStore.username; + profile.nickname = selfUserInfoStore.userNickname; + profile.bio = selfUserInfoStore.signature; + profile.gender = selfUserInfoStore.gender; + profile.tags = selfUserInfoStore.tags; } - // 监听头像文件变化事件 - useEventListener(userAvatarFileInput, "change", handleOpenAvatarCropper); - onMounted(async () => { await getUserInfo(); }); + useEventListener(userAvatarFileInput, "change", handleOpenAvatarCropper); // 监听头像文件变化事件 + onMounted(async () => { await api.user.getSelfUserInfo(); }); onBeforeUnmount(clearBlobUrl); // 释放内存 + watch(selfUserInfoStore, copyPiniaUserInfo2Profile); // 监听 Pinia 中的用户数据,一定发生改变,则拷贝到当前组件的响应式变量 "profile" 中 + useListen("user:login", async loginStatus => { // 发生用户登录事件,请求最新用户信息,并修改 Pinia 中的用户数据,然后触发上方的监听 + if (loginStatus) + await api.user.getSelfUserInfo(); + }); +
- + {{ t.profile.edit_avatar }}
@@ -168,8 +223,8 @@
- - + +
@@ -188,8 +243,8 @@ cursor: pointer; &:any-hover { - filter: brightness(0.75) blur(2px); scale: 105%; + filter: brightness(0.75) blur(2px); &+span { opacity: 1; diff --git a/pages/welcome.vue b/pages/welcome.vue index 9cb3f724..203e2bbd 100644 --- a/pages/welcome.vue +++ b/pages/welcome.vue @@ -10,21 +10,49 @@ const profile = reactive({ name: "", nameValid: false, + nickname: "", bio: "", gender: "", birthday: new Date(), tags: [] as string[], }); const isRead = ref(false); - const validData = computed(() => isRead.value && profile.nameValid && profile.gender); const showWelcome = ref(false); - const avatarBlob = ref(); - const avatarInput = ref(); + const avatarBlob = ref(); // 头像文件 + const cropper = ref>(); // 图片裁剪器实例 + const isAvatarCropperOpen = ref(false); // 用户头像图片裁剪器是否开启 + const isUploadingUserAvatar = ref(false); // 是否正在上传头像 + const userAvatarUploadFile = ref(); // 用户上传的头像文件 Blob + const userAvatarFileInput = ref(); // 隐藏的图片上传 Input 元素 + const isUpdateUserInfo = ref(false); // 是否正在上传用户信息 + const validData = computed(() => isRead.value && profile.nameValid && profile.gender); /** * 完成注册。 */ async function finish() { + isUpdateUserInfo.value = true; + const updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto = { + avatar: avatarBlob.value, + username: profile.name, + userNickname: profile.nickname, + signature: profile.bio, + gender: profile.gender, + userBirthday: new Date().getTime(), // TODO: 日期选择器 // FIXME: 注意:这个值是静态的、非响应式的,不会随时间变化 + label: profile.tags.map((tag, index) => ({ id: index, labelName: tag })), + }; + try { + const updateOrCreateUserInfoResult = await api.user.updateOrCreateUserInfo(updateOrCreateUserInfoRequest); + if (updateOrCreateUserInfoResult.success) { + await api.user.getSelfUserInfo(); + isUpdateUserInfo.value = false; + } + } catch (error) { + isUpdateUserInfo.value = false; + useToast("用户信息更新失败!", "error"); // TODO: 使用多语言 + console.error("用户信息更新失败!", error); + } + const main = container.value?.parentElement; if (main) { main.scrollTo({ top: 0, left: 0, behavior: "smooth" }); @@ -36,18 +64,103 @@ } /** - * 更换头像。 - * @param e - 普通事件。 + * 修改头像事件,向服务器提交新的图片。 + */ + async function handleSubmitAvatarImage() { + try { + isUploadingUserAvatar.value = true; + const blobImageData = await cropper.value?.getCropBlobData(); + if (blobImageData) { + const userAvatarUploadSignedUrlResult = await api.user.getUserAvatarUploadSignedUrl(); + const userAvatarUploadSignedUrl = userAvatarUploadSignedUrlResult.userAvatarUploadSignedUrl; + const userAvatarUploadFilename = userAvatarUploadSignedUrlResult.userAvatarFilename; + if (userAvatarUploadSignedUrlResult.success && userAvatarUploadSignedUrl && userAvatarUploadFilename) { + const uploadResult = await api.user.uploadUserAvatar(userAvatarUploadFilename, blobImageData, userAvatarUploadSignedUrl); + if (uploadResult) { + avatarBlob.value = userAvatarUploadFilename; + isAvatarCropperOpen.value = false; + clearBlobUrl(); // 释放内存 + } + isUploadingUserAvatar.value = false; + } + } else { + useToast("无法获取裁切后的图片!", "error"); // TODO: 使用多语言 + console.error("ERROR", "无法获取裁切后的图片"); + } + } catch (error) { + useToast("头像上传失败!", "error"); // TODO: 使用多语言 + console.error("ERROR", "在上传用户头像时出错", error); + isUploadingUserAvatar.value = false; + } + } + + /** + * 点击头像事件,模拟点击文件上传并唤起文件资源管理器 + */ + function handleUploadAvatarImage() { + userAvatarFileInput.value?.click(); + } + + /** + * 如果有上传图片,则开启图片裁切器。 + * + * 即:用户选择了本地文件的事件。 + * @param e - 应为用户上传文件的 `` 元素的 change 事件。 */ - function onChangeAvatar(e: Event) { - const input = e.target as HTMLInputElement; - const file = input.files?.[0]; - if (!file) return; - avatarBlob.value = fileToBlob(file); + function handleOpenAvatarCropper(e?: Event) { + e?.stopPropagation(); + const fileInput = e?.target as HTMLInputElement | undefined; + const image = fileInput?.files?.[0]; + + if (image) { + if (!/\.(a?png|jpe?g|jfif|pjp(eg)?|gif|svg|webp)$/i.test(fileInput.value)) { + useToast("只能上传图片文件!", "error"); // TODO: 使用多语言 + console.error("ERROR", "不支持所选头像图片格式!"); + return; + } + + userAvatarUploadFile.value = fileToBlob(image); + isAvatarCropperOpen.value = true; + fileInput.value = ""; // 读取完用户上传的文件后,需要清空 input,以免用户在下次上传同一个文件时无法触发 change 事件。 + } } + + /** + * 清除已经上传完成的图片,释放内存。 + */ + function clearBlobUrl() { + if (userAvatarUploadFile.value) { + URL.revokeObjectURL(userAvatarUploadFile.value); + userAvatarUploadFile.value = undefined; + } + } + + useEventListener(userAvatarFileInput, "change", handleOpenAvatarCropper); // 监听头像文件变化事件