From 252e3f139230ed8a9fbc5cc5605d0aa3132fe5fc Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 17 Oct 2022 14:09:38 +0800 Subject: [PATCH] feat: add toast component (#644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /milestone 2.0 #### What this PR does / why we need it: 添加 Toast 组件。 特性: 1. 支持相同消息合并。 2. 支持鼠标悬停。 使用方式: ```vue import { Toast } from '@halo-dev/components' Toast.success("Hello", { //props }) Toast.info("Hello", { //props }) Toast.warning("Hello", { //props }) Toast.error("Hello", { //props }) ``` props: ```ts export interface ToastProps { type?: Type; content?: string; duration?: number; closable?: boolean; frozenOnHover?: boolean; count?: 0; onClose?: () => void; } ``` Toast 方法不仅可以在 Vue 单组件中使用,理论上在任何地方均可使用。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2534 #### Screenshots: image #### Special notes for your reviewer: /cc @halo-dev/sig-halo-console 测试方式: https://halo-admin-ui-git-fork-ruibaby-feat-toast-component-halo-dev.vercel.app/story/src-components-toast-toast-story-vue?variantId=_default 测试功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note 添加 Toast 组件 ``` --- packages/components/src/components.ts | 1 + .../src/components/toast/Toast.story.vue | 39 ++++ .../components/src/components/toast/Toast.vue | 167 ++++++++++++++++++ .../components/src/components/toast/index.ts | 1 + .../src/components/toast/interface.ts | 11 ++ .../src/components/toast/toast-manager.ts | 131 ++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 packages/components/src/components/toast/Toast.story.vue create mode 100644 packages/components/src/components/toast/Toast.vue create mode 100644 packages/components/src/components/toast/index.ts create mode 100644 packages/components/src/components/toast/interface.ts create mode 100644 packages/components/src/components/toast/toast-manager.ts 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 };