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>
|
<script lang="ts" setup>
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import type { Contributor } from "@halo-dev/api-client";
|
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";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
|
@ -26,14 +26,14 @@ function handleRouteToUserDetail(contributor: Contributor) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VAvatar
|
<VAvatarGroup size="xs" circle>
|
||||||
v-for="(contributor, contributorIndex) in contributors"
|
<VAvatar
|
||||||
:key="contributorIndex"
|
v-for="contributor in contributors"
|
||||||
v-tooltip="contributor.displayName"
|
:key="contributor.name"
|
||||||
size="xs"
|
v-tooltip="contributor.displayName"
|
||||||
:src="contributor.avatar"
|
:src="contributor.avatar"
|
||||||
:alt="contributor.displayName"
|
:alt="contributor.displayName"
|
||||||
circle
|
@click="handleRouteToUserDetail(contributor)"
|
||||||
@click="handleRouteToUserDetail(contributor)"
|
></VAvatar>
|
||||||
></VAvatar>
|
</VAvatarGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable storybook/no-uninstalled-addons */
|
||||||
import type { StorybookConfig } from "@storybook/vue3-vite";
|
import type { StorybookConfig } from "@storybook/vue3-vite";
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
|
@ -15,5 +16,12 @@ const config: StorybookConfig = {
|
||||||
docs: {
|
docs: {
|
||||||
autodocs: "tag",
|
autodocs: "tag",
|
||||||
},
|
},
|
||||||
|
async viteFinal(config) {
|
||||||
|
const { mergeConfig } = await import("vite");
|
||||||
|
|
||||||
|
return mergeConfig(config, {
|
||||||
|
assetsInclude: ["/sb-preview/runtime.js"],
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
<script lang="ts" setup>
|
<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 { IconErrorWarning } from "../../icons/icons";
|
||||||
import type { Size } from "./interface";
|
import { AvatarGroupContextInjectionKey, type AvatarProps } from "./interface";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(defineProps<AvatarProps>(), {
|
||||||
defineProps<{
|
size: "md",
|
||||||
src?: string;
|
circle: false,
|
||||||
alt?: string;
|
});
|
||||||
size?: Size;
|
|
||||||
width?: string;
|
const groupProps = inject(AvatarGroupContextInjectionKey);
|
||||||
height?: string;
|
|
||||||
circle?: boolean;
|
const size = computed(() => groupProps?.size || props.size);
|
||||||
}>(),
|
const circle = computed(() => groupProps?.circle || props.circle);
|
||||||
{
|
const width = computed(() => groupProps?.width || props.width);
|
||||||
src: undefined,
|
const height = computed(() => groupProps?.height || props.height);
|
||||||
alt: undefined,
|
|
||||||
size: "md",
|
console.log(groupProps);
|
||||||
width: undefined,
|
|
||||||
height: undefined,
|
|
||||||
circle: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
@ -65,20 +60,20 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
const result = [`avatar-${props.circle ? "circle" : "square"}`];
|
const result = [`avatar-${circle.value ? "circle" : "square"}`];
|
||||||
if (props.size) {
|
if (size.value) {
|
||||||
result.push(`avatar-${props.size}`);
|
result.push(`avatar-${size.value}`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const styles = computed(() => {
|
const styles = computed(() => {
|
||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
if (props.width) {
|
if (width.value) {
|
||||||
result.width = props.width;
|
result.width = width.value;
|
||||||
}
|
}
|
||||||
if (props.height) {
|
if (height.value) {
|
||||||
result.height = props.height;
|
result.height = height.value;
|
||||||
}
|
}
|
||||||
return result;
|
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 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 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,
|
IconTimerLine,
|
||||||
Toast,
|
Toast,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
|
VAvatarGroup,
|
||||||
VDropdownDivider,
|
VDropdownDivider,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VEntity,
|
VEntity,
|
||||||
|
@ -179,15 +180,15 @@ function handleUnpublish() {
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<VAvatar
|
<VAvatarGroup size="xs" circle>
|
||||||
v-for="{ name, avatar, displayName } in post.contributors"
|
<VAvatar
|
||||||
:key="name"
|
v-for="{ name, avatar, displayName } in post.contributors"
|
||||||
v-tooltip="displayName"
|
:key="name"
|
||||||
size="xs"
|
v-tooltip="displayName"
|
||||||
:src="avatar"
|
:src="avatar"
|
||||||
:alt="displayName"
|
:alt="displayName"
|
||||||
circle
|
></VAvatar>
|
||||||
></VAvatar>
|
</VAvatarGroup>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField :description="publishStatus">
|
<VEntityField :description="publishStatus">
|
||||||
|
|
Loading…
Reference in New Issue