feat: add avatar group component (#6128)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.17.x

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

添加 AvatarGroup 组件,用于堆叠多个 Avatar 组件。

<img width="243" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/2d202e95-e735-4635-b16e-cdcf1f94f69a">
<img width="352" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/da4a293d-eb3f-40b4-94b2-10dcf54d3305">

#### Which issue(s) this PR fixes:

Fixes #6079 

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

```release-note
添加 AvatarGroup 组件,用于堆叠多个头像。
```
pull/6148/head
Ryan Wang 2024-06-26 10:24:48 +08:00 committed by GitHub
parent e4cce918f7
commit 8302c21bb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 139 additions and 47 deletions

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { usePermission } from "@/utils/permission";
import type { Contributor } from "@halo-dev/api-client";
import { VAvatar } from "@halo-dev/components";
import { VAvatar, VAvatarGroup } from "@halo-dev/components";
import { useRouter } from "vue-router";
withDefaults(
@ -26,14 +26,14 @@ function handleRouteToUserDetail(contributor: Contributor) {
</script>
<template>
<VAvatar
v-for="(contributor, contributorIndex) in contributors"
:key="contributorIndex"
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
@click="handleRouteToUserDetail(contributor)"
></VAvatar>
<VAvatarGroup size="xs" circle>
<VAvatar
v-for="contributor in contributors"
:key="contributor.name"
v-tooltip="contributor.displayName"
:src="contributor.avatar"
:alt="contributor.displayName"
@click="handleRouteToUserDetail(contributor)"
></VAvatar>
</VAvatarGroup>
</template>

View File

@ -1,3 +1,4 @@
/* eslint-disable storybook/no-uninstalled-addons */
import type { StorybookConfig } from "@storybook/vue3-vite";
const config: StorybookConfig = {
@ -15,5 +16,12 @@ const config: StorybookConfig = {
docs: {
autodocs: "tag",
},
async viteFinal(config) {
const { mergeConfig } = await import("vite");
return mergeConfig(config, {
assetsInclude: ["/sb-preview/runtime.js"],
});
},
};
export default config;

View File

@ -1,26 +1,21 @@
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from "vue";
import { computed, inject, onMounted, ref, watch } from "vue";
import { IconErrorWarning } from "../../icons/icons";
import type { Size } from "./interface";
import { AvatarGroupContextInjectionKey, type AvatarProps } from "./interface";
const props = withDefaults(
defineProps<{
src?: string;
alt?: string;
size?: Size;
width?: string;
height?: string;
circle?: boolean;
}>(),
{
src: undefined,
alt: undefined,
size: "md",
width: undefined,
height: undefined,
circle: false,
}
);
const props = withDefaults(defineProps<AvatarProps>(), {
size: "md",
circle: false,
});
const groupProps = inject(AvatarGroupContextInjectionKey);
const size = computed(() => groupProps?.size || props.size);
const circle = computed(() => groupProps?.circle || props.circle);
const width = computed(() => groupProps?.width || props.width);
const height = computed(() => groupProps?.height || props.height);
console.log(groupProps);
const isLoading = ref(false);
const error = ref(false);
@ -65,20 +60,20 @@ onMounted(async () => {
});
const classes = computed(() => {
const result = [`avatar-${props.circle ? "circle" : "square"}`];
if (props.size) {
result.push(`avatar-${props.size}`);
const result = [`avatar-${circle.value ? "circle" : "square"}`];
if (size.value) {
result.push(`avatar-${size.value}`);
}
return result;
});
const styles = computed(() => {
const result: Record<string, string> = {};
if (props.width) {
result.width = props.width;
if (width.value) {
result.width = width.value;
}
if (props.height) {
result.height = props.height;
if (height.value) {
result.height = height.value;
}
return result;
});

View File

@ -0,0 +1,41 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import { VAvatar, VAvatarGroup } from ".";
const meta: Meta<typeof VAvatarGroup> = {
title: "AvatarGroup",
component: VAvatarGroup,
tags: ["autodocs"],
render: (args) => ({
components: { VAvatarGroup, VAvatar },
setup() {
return { args };
},
template: `<VAvatarGroup v-bind="args">
<VAvatar src="https://avatar.iran.liara.run/public?id=1" />
<VAvatar src="https://avatar.iran.liara.run/public?id=2" />
<VAvatar src="https://avatar.iran.liara.run/public?id=3" />
<VAvatar src="https://avatar.iran.liara.run/public?id=4" />
</VAvatarGroup>`,
}),
argTypes: {
size: {
control: { type: "select" },
options: ["lg", "md", "sm", "xs"],
defaultValue: "md",
},
},
};
export default meta;
type Story = StoryObj<typeof VAvatarGroup>;
export const Default: Story = {
args: {},
};
export const Circle: Story = {
args: {
circle: true,
},
};

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import { provide } from "vue";
import {
AvatarGroupContextInjectionKey,
type AvatarGroupProps,
} from "./interface";
const props = withDefaults(defineProps<AvatarGroupProps>(), {
size: "md",
circle: false,
});
provide(AvatarGroupContextInjectionKey, props);
</script>
<template>
<div class="avatar-group-wrapper">
<slot />
</div>
</template>
<style lang="scss">
.avatar-group-wrapper {
@apply -space-x-2.5 inline-flex;
> * {
@apply hover:z-10 ring-2 ring-white transition-all;
}
}
</style>

View File

@ -1 +1,2 @@
export { default as VAvatar } from "./Avatar.vue";
export { default as VAvatarGroup } from "./AvatarGroup.vue";

View File

@ -1 +1,17 @@
import type { InjectionKey } from "vue";
export type Size = "lg" | "md" | "sm" | "xs";
export interface AvatarProps {
src?: string;
alt?: string;
size?: Size;
width?: string;
height?: string;
circle?: boolean;
}
export type AvatarGroupProps = Omit<AvatarProps, "src" | "alt">;
export const AvatarGroupContextInjectionKey: InjectionKey<AvatarGroupProps> =
Symbol("avatar-group-context");

View File

@ -7,6 +7,7 @@ import {
IconTimerLine,
Toast,
VAvatar,
VAvatarGroup,
VDropdownDivider,
VDropdownItem,
VEntity,
@ -179,15 +180,15 @@ function handleUnpublish() {
<template #end>
<VEntityField>
<template #description>
<VAvatar
v-for="{ name, avatar, displayName } in post.contributors"
:key="name"
v-tooltip="displayName"
size="xs"
:src="avatar"
:alt="displayName"
circle
></VAvatar>
<VAvatarGroup size="xs" circle>
<VAvatar
v-for="{ name, avatar, displayName } in post.contributors"
:key="name"
v-tooltip="displayName"
:src="avatar"
:alt="displayName"
></VAvatar>
</VAvatarGroup>
</template>
</VEntityField>
<VEntityField :description="publishStatus">