diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts
index 3d3ba409..3f6e6ee3 100644
--- a/packages/components/src/components.ts
+++ b/packages/components/src/components.ts
@@ -18,3 +18,4 @@ export * from "./components/dialog";
export * from "./components/pagination";
export * from "./components/codemirror";
export * from "./components/empty";
+export * from "./components/status";
diff --git a/packages/components/src/components/status/StatusDot.story.vue b/packages/components/src/components/status/StatusDot.story.vue
new file mode 100644
index 00000000..e71d9054
--- /dev/null
+++ b/packages/components/src/components/status/StatusDot.story.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/components/src/components/status/StatusDot.vue b/packages/components/src/components/status/StatusDot.vue
new file mode 100644
index 00000000..6624bf69
--- /dev/null
+++ b/packages/components/src/components/status/StatusDot.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/packages/components/src/components/status/__tests__/StatusDot.spec.ts b/packages/components/src/components/status/__tests__/StatusDot.spec.ts
new file mode 100644
index 00000000..e1183c73
--- /dev/null
+++ b/packages/components/src/components/status/__tests__/StatusDot.spec.ts
@@ -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");
+ });
+});
diff --git a/packages/components/src/components/status/__tests__/__snapshots__/StatusDot.spec.ts.snap b/packages/components/src/components/status/__tests__/__snapshots__/StatusDot.spec.ts.snap
new file mode 100644
index 00000000..b5314c28
--- /dev/null
+++ b/packages/components/src/components/status/__tests__/__snapshots__/StatusDot.spec.ts.snap
@@ -0,0 +1,8 @@
+// Vitest Snapshot v1
+
+exports[`StatusDot > should match snapshot 1`] = `
+"
"
+`;
diff --git a/packages/components/src/components/status/index.ts b/packages/components/src/components/status/index.ts
new file mode 100644
index 00000000..755ac6a0
--- /dev/null
+++ b/packages/components/src/components/status/index.ts
@@ -0,0 +1 @@
+export { default as VStatusDot } from "./StatusDot.vue";
diff --git a/packages/components/src/components/status/interface.ts b/packages/components/src/components/status/interface.ts
new file mode 100644
index 00000000..cf0cfcdf
--- /dev/null
+++ b/packages/components/src/components/status/interface.ts
@@ -0,0 +1 @@
+export type State = "default" | "success" | "warning" | "error";
diff --git a/src/modules/contents/attachments/AttachmentList.vue b/src/modules/contents/attachments/AttachmentList.vue
index 30317bc4..cc24d754 100644
--- a/src/modules/contents/attachments/AttachmentList.vue
+++ b/src/modules/contents/attachments/AttachmentList.vue
@@ -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(() => {
-
-
-
+ state="warning"
+ animate
+ />
{
}"
class="flex items-center"
>
-
-
-
+
diff --git a/src/modules/contents/posts/PostList.vue b/src/modules/contents/posts/PostList.vue
index 03cb69bf..bafe1525 100644
--- a/src/modules/contents/posts/PostList.vue
+++ b/src/modules/contents/posts/PostList.vue
@@ -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"
>
-
-
-
+
-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) {
-
-
-
+
{
-
-
-
+
-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) {
-
-
-
+
diff --git a/src/modules/interface/menus/components/MenuList.vue b/src/modules/interface/menus/components/MenuList.vue
index 0ec3f64a..375b5381 100644
--- a/src/modules/interface/menus/components/MenuList.vue
+++ b/src/modules/interface/menus/components/MenuList.vue
@@ -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({
-
-
-
+
diff --git a/src/modules/system/plugins/components/PluginListItem.vue b/src/modules/system/plugins/components/PluginListItem.vue
index 2710468f..3f087191 100644
--- a/src/modules/system/plugins/components/PluginListItem.vue
+++ b/src/modules/system/plugins/components/PluginListItem.vue
@@ -1,5 +1,11 @@