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