mirror of https://github.com/halo-dev/halo
				
				
				
			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
							parent
							
								
									e4cce918f7
								
							
						
					
					
						commit
						8302c21bb6
					
				| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
export { default as VAvatar } from "./Avatar.vue";
 | 
			
		||||
export { default as VAvatarGroup } from "./AvatarGroup.vue";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue