feat: add status dot component (#611)

#### What type of PR is this?

/kind feature
/milestone 2.0

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

添加状态指示器组件。

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


#### Screenshots:

#### Special notes for your reviewer:

https://halo-admin-ui-git-fork-ruibaby-feat-status-dot-ec0d53-halo-dev.vercel.app/story/src-components-status-statusdot-story-vue?variantId=src-components-status-statusdot-story-vue-1

/cc @halo-dev/sig-halo-admin 

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

```release-note
None
```
pull/613/head
Ryan Wang 2022-09-13 21:30:11 +08:00 committed by GitHub
parent f1bf0333aa
commit e65fd8f0c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 199 additions and 71 deletions

View File

@ -18,3 +18,4 @@ export * from "./components/dialog";
export * from "./components/pagination";
export * from "./components/codemirror";
export * from "./components/empty";
export * from "./components/status";

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import StatusDot from "./StatusDot.vue";
import { VSpace } from "../space";
</script>
<template>
<Story title="StatusDot">
<Variant title="State">
<template #default>
<VSpace direction="column">
<StatusDot state="default" text="默认" />
<StatusDot state="success" text="成功" />
<StatusDot state="warning" text="警告" />
<StatusDot state="error" text="错误" />
</VSpace>
</template>
</Variant>
<Variant title="Animate">
<template #default>
<VSpace direction="column">
<StatusDot state="default" text="默认" animate />
<StatusDot state="success" text="成功" animate />
<StatusDot state="warning" text="警告" animate />
<StatusDot state="error" text="错误" animate />
</VSpace>
</template>
</Variant>
</Story>
</template>

View File

@ -0,0 +1,92 @@
<script lang="ts" setup>
import { computed } from "vue";
import type { State } from "./interface";
const props = withDefaults(
defineProps<{
state?: State;
animate?: boolean;
text?: string;
}>(),
{ state: "success", animate: false, text: undefined }
);
const classes = computed(() => {
return [`status-dot-${props.state}`, { "status-dot-animate": props.animate }];
});
</script>
<template>
<div class="status-dot-wrapper" :class="classes">
<div class="status-dot-body">
<span class="status-dot-inner"></span>
</div>
<slot v-if="$slots.text || text" name="text">
<span class="status-dot-text">{{ text }}</span>
</slot>
</div>
</template>
<style lang="scss">
.status-dot-wrapper {
@apply flex items-center gap-2;
.status-dot-body {
@apply inline-flex h-2 w-2 items-center justify-center rounded-full;
}
.status-dot-inner {
@apply inline-block h-1.5 w-1.5 rounded-full;
}
.status-dot-text {
@apply text-gray-500;
}
&.status-dot-animate {
.status-dot-inner {
@apply animate-ping;
}
}
&.status-dot-default {
.status-dot-body {
@apply bg-gray-300;
}
.status-dot-inner {
@apply bg-gray-300;
}
}
&.status-dot-success {
.status-dot-body {
@apply bg-green-600;
}
.status-dot-inner {
@apply bg-green-600;
}
}
&.status-dot-warning {
.status-dot-body {
@apply bg-yellow-600;
}
.status-dot-inner {
@apply bg-yellow-600;
}
}
&.status-dot-error {
.status-dot-body {
@apply bg-red-600;
}
.status-dot-inner {
@apply bg-red-600;
}
}
}
</style>

View File

@ -0,0 +1,31 @@
import { mount } from "@vue/test-utils";
import { describe, expect, it } from "vitest";
import { VStatusDot } from "../index";
describe("StatusDot", () => {
it("should render", () => {
expect(mount(VStatusDot)).toBeDefined();
});
it("should match snapshot", () => {
const wrapper = mount(VStatusDot);
expect(wrapper.html()).toMatchSnapshot();
});
it("should work with state prop", () => {
["default", "success", "warning", "error"].forEach((state) => {
const wrapper = mount(VStatusDot, { props: { state } });
expect(wrapper.classes()).toContain(`status-dot-${state}`);
});
});
it("should work with animate prop", () => {
const wrapper = mount(VStatusDot, { props: { animate: true } });
expect(wrapper.classes()).toContain("status-dot-animate");
});
it("should work with text prop", () => {
const wrapper = mount(VStatusDot, { props: { text: "text" } });
expect(wrapper.find(".status-dot-text").text()).toBe("text");
});
});

View File

@ -0,0 +1,8 @@
// Vitest Snapshot v1
exports[`StatusDot > should match snapshot 1`] = `
"<div class=\\"status-dot-wrapper status-dot-success\\">
<div class=\\"status-dot-body\\"><span class=\\"status-dot-inner\\"></span></div>
<!--v-if-->
</div>"
`;

View File

@ -0,0 +1 @@
export { default as VStatusDot } from "./StatusDot.vue";

View File

@ -0,0 +1 @@
export type State = "default" | "success" | "warning" | "error";

View File

@ -16,6 +16,7 @@ import {
VEmpty,
IconCloseCircle,
IconFolder,
VStatusDot,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -617,14 +618,11 @@ onMounted(() => {
</EntityField>
<EntityField v-if="attachment.metadata.deletionTimestamp">
<template #description>
<div
<VStatusDot
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
state="warning"
animate
/>
</template>
</EntityField>
<EntityField

View File

@ -15,6 +15,7 @@ import {
useDialog,
VEmpty,
VAvatar,
VStatusDot,
} from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -360,13 +361,7 @@ const handleSelectUser = (user?: User) => {
}"
class="flex items-center"
>
<div
class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600"
></span>
</div>
<VStatusDot state="success" animate />
</RouterLink>
</template>
</EntityField>

View File

@ -17,6 +17,7 @@ import {
VPagination,
VSpace,
VAvatar,
VStatusDot,
} from "@halo-dev/components";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue";
@ -768,13 +769,7 @@ function handleContributorFilterItemChange(user?: User) {
}"
class="flex items-center"
>
<div
class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600"
></span>
</div>
<VStatusDot state="success" animate />
</RouterLink>
<PostTag
v-for="(tag, tagIndex) in post.tags"

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { IconList, VButton } from "@halo-dev/components";
import { IconList, VButton, VStatusDot } from "@halo-dev/components";
import Draggable from "vuedraggable";
import type { CategoryTree } from "../utils";
import { ref } from "vue";
@ -68,14 +68,7 @@ function onDelete(category: CategoryTree) {
<template #end>
<EntityField v-if="category.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</EntityField>
<EntityField

View File

@ -13,6 +13,7 @@ import {
VEmpty,
VPageHeader,
VSpace,
VStatusDot,
} from "@halo-dev/components";
import TagEditingModal from "./components/TagEditingModal.vue";
import PostTag from "./components/PostTag.vue";
@ -187,14 +188,7 @@ onMounted(async () => {
<template #end>
<EntityField v-if="tag.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</EntityField>
<EntityField

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { IconList, VButton, VTag } from "@halo-dev/components";
import { IconList, VButton, VTag, VStatusDot } from "@halo-dev/components";
import Draggable from "vuedraggable";
import { ref } from "vue";
import type { MenuTreeItem } from "@/modules/interface/menus/utils";
@ -89,14 +89,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
<template #end>
<EntityField v-if="menuItem.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</EntityField>
</template>

View File

@ -5,6 +5,7 @@ import {
VCard,
VEmpty,
VSpace,
VStatusDot,
} from "@halo-dev/components";
import MenuEditingModal from "./MenuEditingModal.vue";
import { defineExpose, onMounted, ref } from "vue";
@ -156,14 +157,7 @@ defineExpose({
<template #end>
<EntityField v-if="menu.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</EntityField>
</template>

View File

@ -1,5 +1,11 @@
<script lang="ts" setup>
import { VButton, VSpace, VSwitch, VTag } from "@halo-dev/components";
import {
VButton,
VSpace,
VSwitch,
VTag,
VStatusDot,
} from "@halo-dev/components";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
import { toRefs } from "vue";
@ -54,14 +60,11 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
<template #end>
<EntityField v-if="plugin?.status?.phase === 'FAILED'">
<template #description>
<div
<VStatusDot
v-tooltip="`${plugin?.status?.reason}:${plugin?.status?.message}`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
state="error"
animate
/>
</template>
</EntityField>
<EntityField>

View File

@ -15,6 +15,7 @@ import {
VPagination,
VSpace,
VTag,
VStatusDot,
} from "@halo-dev/components";
import RoleEditingModal from "./components/RoleEditingModal.vue";
import Entity from "@/components/entity/Entity.vue";
@ -227,14 +228,7 @@ const handleDelete = async (role: Role) => {
<template #end>
<EntityField v-if="role.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</EntityField>
<EntityField description="0 个用户" />