mirror of https://github.com/halo-dev/halo
fix: it cannot be re-uploaded when the user's avatar cannot be obtained (#5270)
#### What type of PR is this? /area console /kind bug /milestone 2.12.x #### What this PR does / why we need it: 修复当头像因为后端服务无法正常变更时,前端因为一直处于 refetch 状态导致无法重新上传的问题。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/4852 #### Special notes for your reviewer: 1. 使用 https://github.com/AirboZH/plugin-uposs 存储服务创建一个存储策略。 2. 用户设置中的头像存储位置改为此存储策略。 3. 上传头像,观测是否一直处于重新获取的状态。 #### Does this PR introduce a user-facing change? ```release-note 优化头像上传,防止因为后端服务异常导致无法重新上传。 ```pull/5276/head
parent
cf72cbccbd
commit
192b238e37
|
@ -5,7 +5,6 @@ import {
|
||||||
VTabbar,
|
VTabbar,
|
||||||
VDropdown,
|
VDropdown,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VLoading,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { computed, provide, ref, type Ref } from "vue";
|
import { computed, provide, ref, type Ref } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
@ -15,7 +14,6 @@ import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
|
||||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
||||||
import type { Raw } from "vue";
|
import type { Raw } from "vue";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
|
@ -45,7 +43,6 @@ const { params } = useRoute();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
isFetching,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
|
@ -56,13 +53,6 @@ const {
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
refetchInterval: (data) => {
|
|
||||||
const annotations = data?.user.metadata.annotations;
|
|
||||||
return annotations?.[rbacAnnotations.AVATAR_ATTACHMENT_NAME] !==
|
|
||||||
annotations?.[rbacAnnotations.LAST_AVATAR_ATTACHMENT_NAME]
|
|
||||||
? 1000
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
enabled: computed(() => !!params.name),
|
enabled: computed(() => !!params.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,8 +95,7 @@ function handleRouteToUC() {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex flex-row items-center gap-5">
|
<div class="flex flex-row items-center gap-5">
|
||||||
<div class="group relative h-20 w-20">
|
<div class="group relative h-20 w-20">
|
||||||
<VLoading v-if="isFetching" class="h-full w-full" />
|
<UserAvatar :name="user?.user.metadata.name" />
|
||||||
<UserAvatar v-else />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="truncate text-lg font-bold text-gray-900">
|
<h1 class="truncate text-lg font-bold text-gray-900">
|
||||||
|
|
|
@ -11,21 +11,23 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
Toast,
|
Toast,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
VLoading,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { ref, defineAsyncComponent, type Ref } from "vue";
|
import { ref, defineAsyncComponent, type Ref } from "vue";
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useFileDialog } from "@vueuse/core";
|
import { useFileDialog } from "@vueuse/core";
|
||||||
import { inject } from "vue";
|
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
name?: string;
|
||||||
isCurrentUser?: boolean;
|
isCurrentUser?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
name: "-",
|
||||||
isCurrentUser: false,
|
isCurrentUser: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -34,6 +36,31 @@ const queryClient = useQueryClient();
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const { data: avatar, isFetching } = useQuery({
|
||||||
|
queryKey: ["user-avatar", props.name, props.isCurrentUser],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = props.isCurrentUser
|
||||||
|
? await apiClient.user.getCurrentUserDetail()
|
||||||
|
: await apiClient.user.getUserDetail({
|
||||||
|
name: props.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const annotations = data?.user.metadata.annotations;
|
||||||
|
|
||||||
|
// Check avatar has been updated. if not, we need retry.
|
||||||
|
if (
|
||||||
|
annotations?.[rbacAnnotations.AVATAR_ATTACHMENT_NAME] !==
|
||||||
|
annotations?.[rbacAnnotations.LAST_AVATAR_ATTACHMENT_NAME]
|
||||||
|
) {
|
||||||
|
throw new Error("Avatar is not updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.user.spec.avatar || "";
|
||||||
|
},
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
const UserAvatarCropper = defineAsyncComponent(
|
const UserAvatarCropper = defineAsyncComponent(
|
||||||
() => import("./UserAvatarCropper.vue")
|
() => import("./UserAvatarCropper.vue")
|
||||||
);
|
);
|
||||||
|
@ -48,8 +75,6 @@ const { open, reset, onChange } = useFileDialog({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
|
||||||
|
|
||||||
const userAvatarCropper = ref<IUserAvatarCropperType>();
|
const userAvatarCropper = ref<IUserAvatarCropperType>();
|
||||||
const visibleCropperModal = ref(false);
|
const visibleCropperModal = ref(false);
|
||||||
const originalFile = ref<File>() as Ref<File>;
|
const originalFile = ref<File>() as Ref<File>;
|
||||||
|
@ -67,18 +92,15 @@ onChange((files) => {
|
||||||
const uploadSaving = ref(false);
|
const uploadSaving = ref(false);
|
||||||
const handleUploadAvatar = () => {
|
const handleUploadAvatar = () => {
|
||||||
userAvatarCropper.value?.getCropperFile().then((file) => {
|
userAvatarCropper.value?.getCropperFile().then((file) => {
|
||||||
if (!user?.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadSaving.value = true;
|
uploadSaving.value = true;
|
||||||
|
|
||||||
apiClient.user
|
apiClient.user
|
||||||
.uploadUserAvatar({
|
.uploadUserAvatar({
|
||||||
name: props.isCurrentUser ? "-" : user.value.user.metadata.name,
|
name: props.isCurrentUser ? "-" : props.name,
|
||||||
file: file,
|
file: file,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["user-avatar"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
||||||
handleCloseCropperModal();
|
handleCloseCropperModal();
|
||||||
})
|
})
|
||||||
|
@ -99,15 +121,12 @@ const handleRemoveCurrentAvatar = () => {
|
||||||
confirmText: t("core.common.buttons.confirm"),
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
if (!user?.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient.user
|
apiClient.user
|
||||||
.deleteUserAvatar({
|
.deleteUserAvatar({
|
||||||
name: props.isCurrentUser ? "-" : user.value.user.metadata.name,
|
name: props.isCurrentUser ? "-" : props.name,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["user-avatar"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -128,15 +147,16 @@ const changeUploadAvatar = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAvatar = computed(() => {
|
const hasAvatar = computed(() => {
|
||||||
return !!user?.value?.user.spec.avatar;
|
return !!avatar.value;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="group h-full w-full">
|
<VLoading v-if="isFetching" class="h-full w-full" />
|
||||||
|
<div v-else class="group h-full w-full">
|
||||||
<VAvatar
|
<VAvatar
|
||||||
:src="user?.user.spec.avatar"
|
:src="avatar"
|
||||||
:alt="user?.user.spec.displayName"
|
:alt="props.name"
|
||||||
circle
|
circle
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
VTabbar,
|
VTabbar,
|
||||||
VDropdown,
|
VDropdown,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VLoading,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { computed, provide, ref, type Ref } from "vue";
|
import { computed, provide, ref, type Ref } from "vue";
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
import type { DetailedUser } from "@halo-dev/api-client";
|
||||||
|
@ -13,7 +12,6 @@ import ProfileEditingModal from "./components/ProfileEditingModal.vue";
|
||||||
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
|
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
|
||||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
||||||
import type { Raw } from "vue";
|
import type { Raw } from "vue";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
|
@ -41,7 +39,6 @@ const passwordChangeModal = ref(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
isFetching,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
|
@ -50,13 +47,6 @@ const {
|
||||||
const { data } = await apiClient.user.getCurrentUserDetail();
|
const { data } = await apiClient.user.getCurrentUserDetail();
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
refetchInterval: (data) => {
|
|
||||||
const annotations = data?.user.metadata.annotations;
|
|
||||||
return annotations?.[rbacAnnotations.AVATAR_ATTACHMENT_NAME] !==
|
|
||||||
annotations?.[rbacAnnotations.LAST_AVATAR_ATTACHMENT_NAME]
|
|
||||||
? 1000
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
provide<Ref<DetailedUser | undefined>>("user", user);
|
provide<Ref<DetailedUser | undefined>>("user", user);
|
||||||
|
@ -110,8 +100,7 @@ const activeTab = useRouteQuery<string>("tab", tabs[0].id, {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex flex-row items-center gap-5">
|
<div class="flex flex-row items-center gap-5">
|
||||||
<div class="group relative h-20 w-20">
|
<div class="group relative h-20 w-20">
|
||||||
<VLoading v-if="isFetching" class="h-full w-full" />
|
<UserAvatar :name="user?.user.metadata.name" is-current-user />
|
||||||
<UserAvatar v-else is-current-user />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="truncate text-lg font-bold text-gray-900">
|
<h1 class="truncate text-lg font-bold text-gray-900">
|
||||||
|
|
Loading…
Reference in New Issue