perf: refine the fallback of the avatar component (#649)

#### What type of PR is this?

/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

优化 Avatar 组件在图片加载失败的状态,以及添加加载状态的动画。目前的策略为,如果图片加载异常,那么会取 `alt` 属性生成占位样式。如果没有设置 `alt`,会显示失败的图标。

#### Screenshots:

<img width="421" alt="image" src="https://user-images.githubusercontent.com/21301288/196194978-229cf58d-19e1-4492-b77a-8b1a6b41e3a0.png">

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试方式:

1. 需要 `pnpm build:packages`
2. 测试修改个人资料的头像,观测不同效果。
3. 访问 https://halo-admin-ui-git-fork-ruibaby-perf-avatar-fallback-halo-dev.vercel.app/story/src-components-avatar-avatar-story-vue?variantId=_default 测试不同参数的效果。

#### Does this PR introduce a user-facing change?

```release-note
优化 Avatar 组件在图片加载失败的状态
```
pull/645/head^2
Ryan Wang 2022-10-18 00:45:38 +08:00 committed by GitHub
parent 2ae2cfad00
commit 6d8a2ddd75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 11 deletions

View File

@ -1,8 +1,49 @@
<script lang="ts" setup>
import { VAvatar } from "./index";
import { VSpace } from "../space";
</script>
<template>
<Story title="Avatar">
<VAvatar src="https://ryanc.cc/avatar" size="lg" />
<VSpace direction="column">
<VSpace>
<VAvatar size="md" alt="Halo" src="https://halo.run/logo" />
<VAvatar size="md" alt="Ryan Wang" src="https://ryanc.cc/avatar" />
<VAvatar size="md" alt="John Niang" src="https://johnniang.me/avatar" />
<VAvatar size="md" alt="guqing" src="https://guqing.xyz/avatar" />
</VSpace>
<VSpace>
<VAvatar size="md" alt="Halo" />
<VAvatar size="md" alt="Ryan Wang" />
<VAvatar size="md" alt="John Niang" />
<VAvatar size="md" alt="guqing" />
</VSpace>
<VSpace>
<VAvatar size="md" circle alt="Halo" />
<VAvatar size="md" circle alt="Ryan Wang" />
<VAvatar size="md" circle alt="John Niang" />
<VAvatar size="md" circle alt="guqing" />
</VSpace>
<VSpace>
<VAvatar size="xs" circle alt="Halo" src="https://halo.run/logo" />
<VAvatar
size="sm"
circle
alt="Ryan Wang"
src="https://ryanc.cc/avatar"
/>
<VAvatar
size="md"
circle
alt="John Niang"
src="https://johnniang.me/avatar"
/>
<VAvatar
size="lg"
circle
alt="guqing"
src="https://guqing.xyz/avatar"
/>
</VSpace>
</VSpace>
</Story>
</template>

View File

@ -1,8 +1,7 @@
<script lang="ts" setup>
import { computed } from "vue";
import { computed, onMounted, ref } from "vue";
import { IconErrorWarning } from "../../icons/icons";
import type { Size } from "./interface";
import { useImage } from "@vueuse/core";
import { IconUserLine } from "@/icons/icons";
const props = withDefaults(
defineProps<{
@ -16,13 +15,41 @@ const props = withDefaults(
{
src: undefined,
alt: undefined,
size: undefined,
size: "md",
width: undefined,
height: undefined,
circle: false,
}
);
const isLoading = ref(false);
const error = ref(false);
const loadImage = async () => {
const image = new Image();
image.src = props.src;
return new Promise((resolve, reject) => {
image.onload = () => resolve(image);
image.onerror = (err) => reject(err);
});
};
onMounted(async () => {
if (!props.src) {
error.value = true;
return;
}
isLoading.value = true;
try {
await loadImage();
} catch (e) {
error.value = true;
} finally {
isLoading.value = false;
}
});
const classes = computed(() => {
const result = [`avatar-${props.circle ? "circle" : "square"}`];
if (props.size) {
@ -42,13 +69,49 @@ const styles = computed(() => {
return result;
});
const { isLoading, error } = useImage({ src: props.src });
const placeholderText = computed(() => {
if (!props.alt) {
return undefined;
}
const words = props.alt.split(" ");
if (words.length === 1) {
return words[0].charAt(0).toUpperCase();
}
if (words.length > 1) {
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();
}
return undefined;
});
</script>
<template>
<div class="avatar-wrapper" :class="classes" :style="styles">
<div v-if="isLoading || error" class="w-full h-full">
<IconUserLine class="w-full h-full" />
<div v-if="isLoading || error" class="avatar-fallback">
<svg
v-if="isLoading"
class="avatar-loading"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg>
<span v-else-if="placeholderText" class="avatar-placeholder">
{{ placeholderText }}
</span>
<IconErrorWarning v-else class="avatar-error" />
</div>
<img v-else :src="src" :alt="alt" />
</div>
@ -56,12 +119,28 @@ const { isLoading, error } = useImage({ src: props.src });
<style lang="scss">
.avatar-wrapper {
@apply inline-flex items-center justify-center overflow-hidden bg-white;
@apply inline-flex items-center justify-center overflow-hidden bg-gray-100;
img {
@apply w-full h-full object-cover;
}
.avatar-fallback {
@apply w-full h-full flex items-center justify-center;
}
.avatar-loading {
@apply animate-spin w-5 h-5;
}
.avatar-placeholder {
@apply text-sm text-gray-800 font-medium;
}
.avatar-error {
@apply w-5 h-5 text-red-500;
}
&.avatar-circle {
@apply rounded-full;
}
@ -72,10 +151,18 @@ const { isLoading, error } = useImage({ src: props.src });
&.avatar-xs {
@apply w-6 h-6;
.avatar-placeholder {
@apply text-xs;
}
}
&.avatar-sm {
@apply w-8 h-8;
.avatar-placeholder {
@apply text-xs;
}
}
&.avatar-md {

View File

@ -259,7 +259,12 @@ const subjectRefResult = computed(() => {
<template #start>
<VEntityField>
<template #description>
<VAvatar circle :src="comment?.owner.avatar" size="md"></VAvatar>
<VAvatar
circle
:src="comment?.owner.avatar"
:alt="comment?.owner.displayName"
size="md"
></VAvatar>
</template>
</VEntityField>
<VEntityField

View File

@ -104,7 +104,12 @@ const isHoveredReply = computed(() => {
<template #start>
<VEntityField>
<template #description>
<VAvatar circle :src="reply?.owner.avatar" size="md"></VAvatar>
<VAvatar
circle
:src="reply?.owner.avatar"
:alt="reply?.owner.displayName"
size="md"
></VAvatar>
</template>
</VEntityField>
<VEntityField

View File

@ -93,6 +93,7 @@ const handleTabChange = (id: string) => {
<div class="h-24 w-24 sm:h-32 sm:w-32">
<VAvatar
:src="user?.spec?.avatar"
:alt="user?.spec?.displayName"
circle
width="100%"
height="100%"