diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts
index 16adc53a..c89e95a1 100644
--- a/packages/components/src/components.ts
+++ b/packages/components/src/components.ts
@@ -20,3 +20,4 @@ export * from "./components/codemirror";
export * from "./components/empty";
export * from "./components/status";
export * from "./components/entity";
+export * from "./components/toast";
diff --git a/packages/components/src/components/toast/Toast.story.vue b/packages/components/src/components/toast/Toast.story.vue
new file mode 100644
index 00000000..6b14679c
--- /dev/null
+++ b/packages/components/src/components/toast/Toast.story.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+ 成功
+
+
+ 提示
+
+
+
+ 警告
+
+
+
+ 错误
+
+
+
+
diff --git a/packages/components/src/components/toast/Toast.vue b/packages/components/src/components/toast/Toast.vue
new file mode 100644
index 00000000..0f6b0eb0
--- /dev/null
+++ b/packages/components/src/components/toast/Toast.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
diff --git a/packages/components/src/components/toast/index.ts b/packages/components/src/components/toast/index.ts
new file mode 100644
index 00000000..461895dc
--- /dev/null
+++ b/packages/components/src/components/toast/index.ts
@@ -0,0 +1 @@
+export { Toast } from "./toast-manager";
diff --git a/packages/components/src/components/toast/interface.ts b/packages/components/src/components/toast/interface.ts
new file mode 100644
index 00000000..aa70bf01
--- /dev/null
+++ b/packages/components/src/components/toast/interface.ts
@@ -0,0 +1,11 @@
+export type Type = "success" | "info" | "warning" | "error";
+
+export interface ToastProps {
+ type?: Type;
+ content?: string;
+ duration?: number;
+ closable?: boolean;
+ frozenOnHover?: boolean;
+ count?: 0;
+ onClose?: () => void;
+}
diff --git a/packages/components/src/components/toast/toast-manager.ts b/packages/components/src/components/toast/toast-manager.ts
new file mode 100644
index 00000000..baa0e5b4
--- /dev/null
+++ b/packages/components/src/components/toast/toast-manager.ts
@@ -0,0 +1,131 @@
+import ToastComponent from "./Toast.vue";
+import { createVNode, render, type Component, type VNode } from "vue";
+import type { ToastProps } from "./interface";
+
+export type ToastApiProps = Omit;
+
+export interface ToastInstance {
+ id: string;
+ vnode: VNode;
+}
+
+export type ToastApi = (
+ content: string,
+ props?: ToastApiProps
+) => ToastInstance;
+
+export interface ToastEntry {
+ (props: ToastProps): ToastInstance;
+ info: ToastApi;
+ success: ToastApi;
+ error: ToastApi;
+ warning: ToastApi;
+}
+
+let index = 0;
+
+const instances: ToastInstance[] = [];
+
+const defaultProps: ToastProps = {
+ frozenOnHover: true,
+ duration: 3000,
+ count: 0,
+};
+
+const toast: ToastEntry = (userProps: ToastProps) => {
+ const id = "toast-" + index++;
+
+ const props = {
+ ...defaultProps,
+ ...userProps,
+ id,
+ };
+
+ let container = document.body.querySelector(".toast-container");
+ if (!container) {
+ container = document.createElement("div");
+ container.className = "toast-container";
+ document.body.appendChild(container);
+ }
+
+ // Grouping toasts
+ if (instances.length > 0) {
+ const instance = instances.find((item) => {
+ const { vnode } = item;
+ if (vnode?.props) {
+ return (
+ vnode.props.content === props.content &&
+ vnode.props.type === props.type
+ );
+ }
+ return undefined;
+ });
+
+ if (instance?.vnode.component?.props) {
+ (instance.vnode.component.props.count as number) += 1;
+ index = instances.length - 1;
+ return instance;
+ }
+ }
+
+ const { vnode, container: hostContainer } = createVNodeComponent(
+ ToastComponent,
+ props
+ );
+
+ if (hostContainer.firstElementChild) {
+ container.appendChild(hostContainer.firstElementChild);
+ }
+
+ if (vnode?.props) {
+ // close emit
+ vnode.props.onClose = () => {
+ removeInstance(id);
+ render(null, hostContainer);
+ };
+ }
+
+ const instance = {
+ id,
+ vnode,
+ close: () => {
+ vnode?.component?.exposed?.close();
+ },
+ };
+
+ instances.push(instance);
+ return instance;
+};
+
+function createVNodeComponent(
+ component: Component,
+ props: Record
+) {
+ const vnode = createVNode(component, props);
+ const container = document.createElement("div");
+ render(vnode, container);
+ return { vnode, container };
+}
+
+function removeInstance(id: string) {
+ const index = instances.findIndex((instance) => instance.id === id);
+ if (index >= 0) {
+ instances.splice(index, 1);
+
+ if (instances.length === 0) {
+ const container = document.body.querySelector(".toast-container");
+ container?.remove();
+ }
+ }
+}
+
+toast.success = (content: string, props?: ToastApiProps) =>
+ toast({ ...props, type: "success", content });
+toast.info = (content: string, props?: ToastApiProps) =>
+ toast({ ...props, type: "info", content });
+toast.warning = (content: string, props?: ToastApiProps) =>
+ toast({ ...props, type: "warning", content });
+toast.error = (content: string, props?: ToastApiProps) =>
+ toast({ ...props, type: "error", content });
+
+export { toast as Toast };