diff --git a/.gitignore b/.gitignore
index 9c5f67e6d7e03aa92e9f114abf5840437d00b36d..6364707073716036f0a76c8e4db36dd6dde12a53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ dist-ssr
*.local
package-lock.json
.history
+.vscode
diff --git a/devui/shared/devui-api-table/devui-api-table.tsx b/devui/shared/devui-api-table/devui-api-table.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a1076a67dff32294f5d4cdbce9efeb7dab6b9339
--- /dev/null
+++ b/devui/shared/devui-api-table/devui-api-table.tsx
@@ -0,0 +1,39 @@
+import { defineComponent } from 'vue'
+import { apiTableProps, ITableColumn, ITableDataRow } from './devui-api-table.type'
+
+export default defineComponent({
+ name: 'DevuiApiTable',
+ props: apiTableProps,
+ render() {
+ const { columns, data } = this
+
+ const renderTd = (params: { col: ITableColumn; row: ITableDataRow; }) => {
+ const { col, row } = params
+
+ const value = row[col.key]
+
+ if ('type' in col) {
+ return {value}
+ }
+
+ return value
+ }
+
+ return (
+
+
+ {columns.map((col) => (
+ {col.title} |
+ ))}
+
+ {data.map((row) => (
+
+ {columns.map((col) => (
+ {renderTd({ col, row })} |
+ ))}
+
+ ))}
+
+ )
+ }
+})
diff --git a/devui/shared/devui-api-table/devui-api-table.type.ts b/devui/shared/devui-api-table/devui-api-table.type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..359f59df2bc155d009f895c10bad5aa9089d534e
--- /dev/null
+++ b/devui/shared/devui-api-table/devui-api-table.type.ts
@@ -0,0 +1,23 @@
+import type { ExtractPropTypes, PropType } from 'vue'
+
+export type ITableColumn = {
+ key: string
+ title: string
+ type?: 'turn'
+}
+
+export type ITableDataRow = Record
+
+export const apiTableProps = {
+ columns: {
+ type: Array as PropType,
+ required: true,
+ default: () => []
+ },
+ data: {
+ type: Array as PropType,
+ default: () => []
+ }
+} as const
+
+export type ApiTableProps = ExtractPropTypes
diff --git a/devui/shared/devui-api-table/index.ts b/devui/shared/devui-api-table/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb6d8d0b45dc852cf1d8399881ee635fb21327cb
--- /dev/null
+++ b/devui/shared/devui-api-table/index.ts
@@ -0,0 +1 @@
+export { default } from './devui-api-table'
diff --git a/devui/toast/demo/toast-demo.tsx b/devui/toast/demo/toast-demo.tsx
deleted file mode 100644
index 2781b03502dc2a43fcfe0ac946007fc2f6c2dd0d..0000000000000000000000000000000000000000
--- a/devui/toast/demo/toast-demo.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineComponent } from 'vue'
-
-export default defineComponent({
- name: 'd-toast-demo',
- props: {
- },
- setup(props, ctx) {
- return () => {
- return devui-toast-demo
- }
- }
-})
\ No newline at end of file
diff --git a/devui/toast/demo/toast.route.ts b/devui/toast/demo/toast.route.ts
deleted file mode 100644
index 4000ac6682580a18ad3be8ad61b9643744ae70d8..0000000000000000000000000000000000000000
--- a/devui/toast/demo/toast.route.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import ToastDemoComponent from './toast-demo'
-import DevUIApiComponent from '../../shared/devui-api/devui-api'
-
-import ApiCn from '../doc/api-cn.md'
-import ApiEn from '../doc/api-en.md'
-const routes = [
- { path: '', redirectTo: 'demo' },
- { path: 'demo', component: ToastDemoComponent},
- { path: 'api', component: DevUIApiComponent, meta: {
- 'zh-cn': ApiCn,
- 'en-us': ApiEn
- }}
-]
-
-export default routes
diff --git a/devui/toast/doc/api-cn.md b/devui/toast/doc/api-cn.md
deleted file mode 100644
index 434c8da3c72e084c879b3c42ca542362065cbea8..0000000000000000000000000000000000000000
--- a/devui/toast/doc/api-cn.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# 如何使用
-
-在 module 中引入:
-
-```ts
-import { ToastModule } from 'ng-devui/toast';
-```
-
-在页面中使用:
-
-```xml
-
-```
-
-# d-toast
-
-## d-toast 参数
-
-| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |
-| :--------: | :--------------------------: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
-| value | [`Array`](#message) | -- | 必选,消息内容数组,Message 对象定义见下文 | [基本用法](demo#basic-usage) |
-| life | `number` | 5000 | 可选,超时时间,超时后自动消失,鼠标悬停可以阻止消失,单位毫秒。普通、成功、提示类默认为 5000 毫秒,错误、警告类默认为 10000 毫秒 | [超时时间](demo#life) |
-| lifeMode | `string` | global | 可选,超时时间模式,预设值为 global 和 single 。默认为 global,所有消息使用 life 或群组第一个消息的预设超时时间; 设置为 single 时, 每个消息使用自身的超时时间,参见 Message 中的 life 定义 | [每个消息使用单独的超时时间](demo#single) |
-| sticky | `boolean` | false | 可选,是否常驻,默认自动关闭 |
-| style | `string` | -- | 可选,样式 |
-| styleClass | `string` | -- | 可选,类名 |
-
-## d-toast 事件
-
-| 参数 | 类型 | 说明 |
-| :---------: | :-----------------------: | :----------------------------------------------------------------------------- |
-| closeEvent | `EventEmitter` | 可选,返回被手动关闭或自动消失的单条消息内容 |
-| valueChange | `EventEmitter` | 可选,返回变化(手动关闭或自动消失)后剩余消息内容数组,Message 对象定义见下文 |
-
-# 接口 & 类型定义
-
-### Message
-
-```ts
-export interface Message {
- severity?: string; // 预设值有 common、success、error、warn、info,超时时间参见 life 说明,未设置或非预设值时超时时间为 5000 毫秒,warn 和 error 为 10000 毫秒
- summary?: string; // 消息标题。当设置超时时间,未设置标题时,不展示标题和关闭按钮
- detail?: string; // 消息内容,推荐使用content替换
- content?: string | TemplateRef; // 消息内容,支持纯文本和模板,推荐使用
- life?: number; // 单个消息超时时间,需设置 lifeMode 为 single 。每个消息使用自己的超时时间,开启该模式却未设置时按 severity 判断超时时间
- id?: any; // 消息ID
-}
-```
diff --git a/devui/toast/doc/api-en.md b/devui/toast/doc/api-en.md
deleted file mode 100644
index 8cf9d1504ea169ba4c6341bfb98a197454ea5861..0000000000000000000000000000000000000000
--- a/devui/toast/doc/api-en.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# How to use
-
-Import into module:
-
-```ts
-import { ToastModule } from 'ng-devui/toast';
-```
-
-In the page:
-
-```xml
-
-```
-
-# d-toast
-
-## d-toast Parameter
-
-| Parameter | Type | Default | Description | Jump to Demo |
-| :--------: | :--------------------------: | :-----: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
-| value | [`Array`](#message) | -- | Required. Message content array. For details about the message object definition, see the following description. | [Basic usage](demo#basic-usage) |
-| life | `number` | 5000 | Optional. Timeout interval, in milliseconds. The timeout interval disappears automatically. You can move the mouse to stop the timeout interval. The default value is 5000 milliseconds for common, success, and info , and 10000 milliseconds for error and warn. | [Timeout interval](demo#life) |
-| lifeMode | `string` | global | Optional. The default value is global or single. The default value is global, indicating that all messages use the preset timeout interval of life or the first message in a group. If this parameter is set to single, each message uses its own timeout interval. For details, see the definition of life in Message. | [Each message uses a separate timeout interval.](demo#single) |
-| sticky | `boolean` | false | Optional. Indicating whether the database is permanently configured. This parameter is automatically disabled by default. |
-| style | `string` | -- | Optional. Style |
-| styleClass | `string` | -- | Optional. Class name |
-
-## d-toast event
-
-| Parameter | Type | Description |
-| :---------: | :-----------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| closeEvent | `EventEmitter` | Optional. Indicates the content of a message that is manually closed or disappears automatically. This parameter is optional. |
-| valueChange | `EventEmitter` | Optional. Indicates the array of remaining message content after the change (manually closed or automatically disappears). For details about the Message object definition, see the following description. |
-
-# 接口 & 类型定义
-
-### Message
-
-```ts
-export interface Message {
- severity?: string; // The preset values include common, success, error, warn, and info. For details about the timeout interval, see the life description. If the timeout interval is not set or is not set, the timeout interval is 5000 ms, and the warn and error are 10000 ms.
- summary?: string; // Message title. If the timeout interval is set but no title is set, the title and close button are not displayed.
- detail?: string; // Message content, content replacement is recommended.
- content?: string | TemplateRef; // Message content. Plain text and template are supported. Recommended.
- life?: number; // Timeout interval of a single message. Set lifeMode to single. Each message uses its own timeout interval. If this mode is enabled but is not set, the timeout interval is determined based on severity.
- id?: any; // Message ID.
-}
-```
diff --git a/devui/toast/hooks/use-toast-constant.ts b/devui/toast/hooks/use-toast-constant.ts
new file mode 100644
index 0000000000000000000000000000000000000000..af0627dec114e01d01bff9a73f00d74cc54eb93b
--- /dev/null
+++ b/devui/toast/hooks/use-toast-constant.ts
@@ -0,0 +1,11 @@
+export function useToastConstant() {
+ const ANIMATION_NAME = 'slide-in'
+ const ANIMATION_TIME = 300
+ const ID_PREFIX = 'toast-message'
+
+ return {
+ ANIMATION_TIME,
+ ANIMATION_NAME,
+ ID_PREFIX
+ } as const
+}
diff --git a/devui/toast/hooks/use-toast-event.ts b/devui/toast/hooks/use-toast-event.ts
new file mode 100644
index 0000000000000000000000000000000000000000..26ff6e036b15bb26aeca38d2c2729bab95f82915
--- /dev/null
+++ b/devui/toast/hooks/use-toast-event.ts
@@ -0,0 +1,23 @@
+import { getCurrentInstance } from 'vue'
+import { Message } from '../src/toast.type'
+import { useToastConstant } from './use-toast-constant'
+
+const { ANIMATION_TIME } = useToastConstant()
+
+export function useToastEvent() {
+ const ctx = getCurrentInstance()
+
+ function onCloseEvent(msg: Message) {
+ ctx.emit('closeEvent', msg)
+ }
+
+ function onValueChange(msgs: Message[]) {
+ ctx.emit('valueChange', msgs)
+ }
+
+ function onHidden() {
+ setTimeout(() => (ctx.attrs.onHidden as () => void)?.(), ANIMATION_TIME)
+ }
+
+ return { onCloseEvent, onValueChange, onHidden }
+}
diff --git a/devui/toast/hooks/use-toast-helper.ts b/devui/toast/hooks/use-toast-helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc12f581c1fa139657ba6d2e072c7f2db01e23e0
--- /dev/null
+++ b/devui/toast/hooks/use-toast-helper.ts
@@ -0,0 +1,15 @@
+import { Message } from '../src/toast.type'
+
+export function useToastHelper() {
+ function severityDelay(msg: Message) {
+ switch (msg.severity) {
+ case 'warn':
+ case 'error':
+ return 10e3
+ default:
+ return 5e3
+ }
+ }
+
+ return { severityDelay }
+}
diff --git a/devui/toast/index.ts b/devui/toast/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f67734333521b6e1c24edde008a595be9ed4f04c
--- /dev/null
+++ b/devui/toast/index.ts
@@ -0,0 +1,12 @@
+import type { App } from 'vue'
+import Toast from './src/toast'
+import ToastService from './src/toast-service'
+
+Toast.install = function (app: App) {
+ app.component(Toast.name, Toast)
+ app.config.globalProperties.$toastService = ToastService
+}
+
+export { ToastService }
+
+export default Toast
diff --git a/devui/toast/src/toast-icon-close.tsx b/devui/toast/src/toast-icon-close.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f1aa549353e6a7ca4641adb3c695628672ff333b
--- /dev/null
+++ b/devui/toast/src/toast-icon-close.tsx
@@ -0,0 +1,22 @@
+import { defineComponent, PropType } from 'vue'
+import DIcon from '../../icon'
+
+export default defineComponent({
+ name: 'DToastIconClose',
+ props: {
+ prefixCls: String,
+ onClick: Function as PropType<(e: MouseEvent) => void>
+ },
+ emits: ['click'],
+ render() {
+ const { prefixCls, $emit } = this
+
+ const wrapperCls = `${prefixCls}-icon-close`
+
+ return (
+ $emit('click', e)}>
+
+
+ )
+ }
+})
diff --git a/devui/toast/src/toast-image.tsx b/devui/toast/src/toast-image.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d009677d287d7804e5ea3393b66d114ea62fb047
--- /dev/null
+++ b/devui/toast/src/toast-image.tsx
@@ -0,0 +1,27 @@
+import { defineComponent, PropType } from 'vue'
+import { IToastSeverity } from './toast.type'
+import DIcon from '../../icon'
+
+export default defineComponent({
+ name: 'DToastImage',
+ props: {
+ prefixCls: String,
+ severity: String as PropType
+ },
+ render() {
+ const { prefixCls, severity } = this
+
+ const wrapperCls = [`${prefixCls}-image`, `${prefixCls}-image-${severity || 'common'}`]
+
+ const severityIconMap = {
+ info: 'info-o',
+ success: 'right-o',
+ warn: 'warning-o',
+ error: 'error-o'
+ }
+
+ const showIcon = () => severity !== 'common'
+
+ return {showIcon() ? : null}
+ }
+})
diff --git a/devui/toast/src/toast-service.ts b/devui/toast/src/toast-service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e1813e55496cc4271e99e2903cd9caabce9fe120
--- /dev/null
+++ b/devui/toast/src/toast-service.ts
@@ -0,0 +1,35 @@
+import { App, ComponentPublicInstance, createApp, onUnmounted } from 'vue'
+import { ToastProps } from './toast.type'
+import DToast from './toast'
+
+function createToastApp(props: Record) {
+ return createApp(DToast, props)
+}
+
+class ToastService {
+ static open(props: Partial & Pick) {
+ let $body: HTMLElement | null = document.body
+ let $div: HTMLDivElement | null = document.createElement('div')
+
+ $body.appendChild($div)
+
+ let app = createToastApp({ ...(props ?? {}), onHidden: () => app?.unmount() })
+ let toastInstance = app.mount($div)
+
+ onUnmounted(() => {
+ $body.removeChild($div)
+
+ $body = null
+ $div = null
+
+ app = null
+ toastInstance = null
+ }, toastInstance.$)
+
+ return {
+ toastInstance
+ }
+ }
+}
+
+export default ToastService
diff --git a/devui/toast/src/toast.scss b/devui/toast/src/toast.scss
new file mode 100644
index 0000000000000000000000000000000000000000..82a86a44131742418051cadd809f352b481507e2
--- /dev/null
+++ b/devui/toast/src/toast.scss
@@ -0,0 +1,131 @@
+@import '../../style/mixins/index';
+@import '../../style/theme/color';
+@import '../../style/theme/shadow';
+@import '../../style/theme/corner';
+@import '../../style/core/_font';
+@import '../../style/core/animation';
+
+.devui-toast {
+ position: fixed;
+ top: 50px;
+ right: 20px;
+ width: 20em;
+ word-break: normal;
+ word-wrap: break-word;
+
+ a {
+ &:link,
+ &:visited {
+ color: $devui-link-light;
+ }
+
+ &:hover,
+ &:active {
+ color: $devui-link-light-active;
+ }
+ }
+}
+
+.devui-toast-item-container {
+ position: relative;
+ left: 150%;
+ margin: 0 0 10px 0;
+ opacity: 0.95;
+ filter: alpha(opacity=95);
+ box-shadow: $devui-shadow-length-feedback-overlay $devui-shadow;
+ border-radius: $devui-border-radius-feedback;
+ color: $devui-feedback-overlay-text;
+ transition: all $devui-animation-duration-slow $devui-animation-ease-in-smooth;
+ background-color: $devui-feedback-overlay-bg;
+
+ &.slide-in {
+ left: 0;
+ }
+}
+
+.devui-toast-item {
+ position: relative;
+ display: block;
+ padding: 12px 16px;
+}
+
+.devui-toast-item p {
+ padding: 0;
+ margin: 0;
+}
+
+.devui-toast-icon-close {
+ position: absolute;
+ top: 7px;
+ right: 10px;
+ cursor: pointer;
+
+ & i.icon {
+ color: $devui-light-text !important;
+ }
+}
+
+.devui-toast-title {
+ font-size: $devui-font-size-card-title;
+ padding: 0 0 calc(0.5em - 2px) 0;
+ display: block;
+ font-weight: 700;
+}
+
+.devui-toast-image {
+ position: absolute;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ left: 16px;
+ top: 14px;
+ padding: 0;
+
+ & i.icon {
+ vertical-align: 0;
+ }
+
+ &.devui-toast-image-warn i.icon {
+ color: $devui-warning !important;
+ }
+
+ &.devui-toast-image-info i.icon {
+ color: $devui-info !important;
+ }
+
+ &.devui-toast-image-error i.icon {
+ color: $devui-danger !important;
+ }
+
+ &.devui-toast-image-success i.icon {
+ color: $devui-success !important;
+ }
+
+ .devui-toast-image-info-path,
+ .devui-toast-image-error-path,
+ .devui-toast-image-success-path {
+ fill: $devui-light-text;
+ }
+}
+
+.devui-toast-message {
+ margin-left: 20px;
+
+ p {
+ padding: 0 8px 0 4px;
+ }
+
+ span.devui-toast-title + p {
+ padding: 0;
+ }
+}
+
+.devui-toast-message-common .devui-toast-message {
+ margin-left: 0;
+}
+
+.devui-toast-message p {
+ font-size: $devui-font-size;
+ margin-top: 2px;
+}
diff --git a/devui/toast/src/toast.tsx b/devui/toast/src/toast.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..11d0c88691591f78c1a054af79578289cd489daf
--- /dev/null
+++ b/devui/toast/src/toast.tsx
@@ -0,0 +1,334 @@
+import './toast.scss'
+
+import { computed, defineComponent, nextTick, onUnmounted, ref, watch } from 'vue'
+import { Message, ToastProps, toastProps } from './toast.type'
+import DToastIconClose from './toast-icon-close'
+import DToastImage from './toast-image'
+import { cloneDeep, isEqual, merge, omit, throttle } from 'lodash-es'
+import { useToastEvent } from '../hooks/use-toast-event'
+import { useToastHelper } from '../hooks/use-toast-helper'
+import { useToastConstant } from '../hooks/use-toast-constant'
+
+const { ANIMATION_NAME, ANIMATION_TIME, ID_PREFIX } = useToastConstant()
+
+export default defineComponent({
+ name: 'DToast',
+ inheritAttrs: false,
+ props: toastProps,
+ emits: ['closeEvent', 'valueChange'],
+ setup(props: ToastProps, ctx) {
+ const { onCloseEvent, onHidden, onValueChange } = useToastEvent()
+ const { severityDelay } = useToastHelper()
+
+ const removeThrottle = throttle(remove, ANIMATION_TIME)
+
+ const messages = ref([])
+ const msgAnimations = ref([])
+ const zIndex = ref(1060)
+
+ const containerRef = ref()
+ const msgItemRefs = ref([])
+
+ let timestamp: number = Date.now()
+ let timeout: number | undefined
+ const timeoutArr: typeof timeout[] = []
+
+ const defaultLife = computed(() => {
+ if (props.life !== null) return props.life
+
+ if (messages.value.length > 0) return severityDelay(messages.value[0])
+
+ return 5e3
+ })
+
+ watch(
+ () => props.value,
+ (value) => {
+ if (value.length === 0) return
+
+ if (hasMsgAnimation()) {
+ initValue()
+ }
+
+ nextTick(() => {
+ initValue(value)
+ handleValueChange()
+ })
+ },
+ { deep: true, immediate: true }
+ )
+
+ watch(messages, (value) => {
+ value.length === 0 && msgAnimations.value.length > 0 && (msgAnimations.value = [])
+ })
+
+ watch(msgAnimations, (value, oldValue) => {
+ oldValue.length > 0 && value.length === 0 && onHidden()
+ })
+
+ onUnmounted(() => {
+ if (props.sticky) {
+ return
+ }
+
+ if (props.lifeMode === 'single') {
+ timeoutArr.forEach((t) => t && clearTimeout(t))
+ } else {
+ clearTimeout(timeout)
+ }
+ })
+
+ function initValue(value: Message[] = []) {
+ const cloneValue = cloneDeep(value)
+ messages.value = cloneValue.map((v, i) => merge(v, { id: `${ID_PREFIX}-${i}` }))
+ msgAnimations.value = []
+ }
+
+ function handleValueChange() {
+ zIndex.value++
+
+ setTimeout(() => {
+ messages.value.forEach((msg) => msgAnimations.value.push(msg))
+ }, 0)
+
+ if (props.sticky) return
+
+ if (timeout) {
+ timeout = clearTimeout(timeout) as undefined
+ }
+
+ if (timeoutArr.length > 0) {
+ timeoutArr.splice(0).forEach((t) => clearTimeout(t))
+ }
+
+ timestamp = Date.now()
+
+ if (props.lifeMode === 'single') {
+ setTimeout(() => {
+ messages.value.forEach((msg, i) => {
+ timeoutArr[i] = setTimeout(() => singleModeRemove(msg, i), msg.life || severityDelay(msg))
+ })
+ })
+ } else {
+ timeout = setTimeout(() => removeAll(), defaultLife.value)
+ }
+ }
+
+ function singleModeRemove(msg: Message, i: number) {
+ removeMsgAnimation(msg)
+ setTimeout(() => {
+ onCloseEvent(msg)
+
+ if (hasMsgAnimation()) {
+ messages.value.splice(i, 1)
+ } else {
+ messages.value = []
+ }
+
+ onValueChange(messages.value)
+ }, ANIMATION_TIME)
+ }
+
+ function interrupt(i: number) {
+ // 避免正在动画中的 toast 触发方法
+ if (!msgAnimations.value.includes(messages.value[i])) return
+
+ if (props.lifeMode === 'single') {
+ if (timeoutArr[i]) {
+ timeoutArr[i] = clearTimeout(timeoutArr[i]) as undefined
+ }
+ } else {
+ resetDelay(() => {
+ messages.value.forEach((msg, _i) => i !== _i && removeMsgAnimation(msg))
+ })
+ }
+ }
+
+ function resetDelay(fn: () => void) {
+ if (!props.sticky && timeout) {
+ timeout = clearTimeout(timeout) as undefined
+
+ const remainTime = defaultLife.value - (Date.now() - timestamp)
+ timeout = setTimeout(() => fn(), remainTime)
+ }
+ }
+
+ function remove(i: number) {
+ if (props.lifeMode === 'single' && timeoutArr[i]) {
+ timeoutArr[i] = clearTimeout(timeoutArr[i]) as undefined
+ timeoutArr.splice(i, 1)
+ }
+
+ removeMsgAnimation(messages.value[i])
+
+ setTimeout(() => {
+ onCloseEvent(messages.value[i])
+
+ messages.value.splice(i, 1)
+
+ onValueChange(messages.value)
+
+ if (props.lifeMode === 'global') {
+ removeReset()
+ }
+ }, ANIMATION_TIME)
+ }
+
+ function removeAll() {
+ if (messages.value.length > 0) {
+ msgAnimations.value = []
+
+ setTimeout(() => {
+ messages.value.forEach((msg) => onCloseEvent(msg))
+
+ messages.value = []
+
+ onValueChange(messages.value)
+ }, ANIMATION_TIME)
+ }
+ }
+
+ function removeReset(i?: number, msg?: Message) {
+ // 避免点击关闭但正在动画中或自动消失正在动画中的 toast 触发重置方法
+ const removed = messages.value.findIndex((_msg) => _msg === msg) === -1
+
+ if (removed || (msg !== undefined && !msgAnimations.value.includes(msg))) {
+ return
+ }
+
+ if (props.lifeMode === 'single') {
+ const msgLife = msg!.life || severityDelay(msg!)
+ const remainTime = msgLife - (Date.now() - timestamp)
+ timeoutArr[i!] = setTimeout(() => singleModeRemove(msg!, i!), remainTime)
+ } else {
+ resetDelay(() => removeAll())
+ }
+ }
+
+ function removeIndexThrottle(i: number) {
+ if (i < msgItemRefs.value.length && i > -1) {
+ removeThrottle(i)
+ }
+ }
+
+ function removeMsgThrottle(msg: Message) {
+ const ignoreDiffKeys = ['id']
+ const index = messages.value.findIndex((_msg) => isEqual(omit(_msg, ignoreDiffKeys), omit(msg, ignoreDiffKeys)))
+ removeIndexThrottle(index)
+ }
+
+ function removeMsgAnimation(msg: Message) {
+ msgAnimations.value = msgAnimations.value.filter((_msg) => _msg !== msg)
+ }
+
+ function close(params?: number | Message): void {
+ if (params === undefined) {
+ return removeAll()
+ }
+
+ if (typeof params === 'number') {
+ removeIndexThrottle(params)
+ } else {
+ removeMsgThrottle(params)
+ }
+ }
+
+ function msgItemRef(i: number) {
+ return msgItemRefs.value[i] as HTMLDivElement
+ }
+
+ function hasMsgAnimation() {
+ return msgAnimations.value.length > 0
+ }
+
+ return {
+ messages,
+ msgAnimations,
+ zIndex,
+ containerRef,
+ msgItemRefs,
+ interrupt,
+ removeReset,
+ removeThrottle,
+ close,
+ msgItemRef
+ }
+ },
+ render() {
+ const {
+ style: extraStyle,
+ styleClass: extraClass,
+ zIndex,
+ messages,
+ msgAnimations,
+ msgItemRefs,
+ life,
+ interrupt,
+ removeReset,
+ removeThrottle,
+ $attrs,
+ $slots
+ } = this
+
+ const prefixCls = 'devui-toast'
+
+ const wrapperStyles = [`z-index: ${zIndex}`, extraStyle]
+ const wrapperCls = [prefixCls, extraClass]
+
+ const msgCls = (msg: Message) => [
+ `${prefixCls}-item-container`,
+ `${prefixCls}-message-${msg.severity}`,
+ { [ANIMATION_NAME]: msgAnimations.includes(msg) }
+ ]
+
+ const showClose = (msg: Message) => !(!msg.summary && life !== null)
+ const showImage = (msg: Message) => msg.severity !== 'common'
+ const showSummary = (msg: Message) => !!msg.summary
+ const showContent = (msg: Message) => !!msg.content
+ const showDetail = (msg: Message) => !showContent(msg) && !!msg.detail
+
+ const msgContent = (msg: Message) => {
+ if (typeof msg.content === 'function') {
+ return msg.content(msg)
+ }
+
+ if ([null, undefined].includes(msg.content)) {
+ return null
+ }
+
+ const slotPrefix = 'slot:'
+ const isSlot = String(msg.content).startsWith(slotPrefix)
+
+ if (isSlot) {
+ return $slots[msg.content.slice(slotPrefix.length)]?.(msg)
+ }
+
+ return msg.content
+ }
+
+ return (
+
+ {messages.map((msg, i) => (
+
(msgItemRefs[i] = el)}
+ key={msg.id}
+ class={msgCls(msg)}
+ aria-live="polite"
+ onMouseenter={() => interrupt(i)}
+ onMouseleave={() => removeReset(i, msg)}
+ >
+
+ {showClose(msg) ?
removeThrottle(i)} /> : null}
+ {showImage(msg) ? : null}
+
+ {showSummary(msg) ?
{msg.summary} : null}
+ {showContent(msg) ? msgContent(msg) : null}
+ {showDetail(msg) ?
: null}
+
+
+
+ ))}
+
+ )
+ }
+})
diff --git a/devui/toast/src/toast.type.ts b/devui/toast/src/toast.type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..458161ec242941d01f5a0d5a0fa7ccfb2bd86c53
--- /dev/null
+++ b/devui/toast/src/toast.type.ts
@@ -0,0 +1,99 @@
+import type { CSSProperties, ExtractPropTypes, PropType, h } from 'vue'
+
+export type IToastLifeMode = 'single' | 'global'
+export type IToastSeverity = 'common' | 'success' | 'error' | 'warn' | 'info' | string
+export type IToastSeverityConfig = { color: string; icon: string; }
+
+export interface Message {
+ /**
+ * 消息级别。
+ * 预设值有 common、success、error、warn、info,超时时间参见 life 说明,
+ * 未设置或非预设值时超时时间为 5000 毫秒,warn 和 error 为 10000 毫秒。
+ */
+ severity?: IToastSeverity
+ /**
+ * 消息标题。
+ * 当设置超时时间,未设置标题时,不展示标题和关闭按钮。
+ */
+ summary?: string
+ /**
+ * 消息内容,推荐使用content替换。
+ */
+ detail?: string
+ /**
+ * 消息内容,支持纯文本和插槽,推荐使用。
+ */
+ content?: string | `slot:${string}` | ((message: Message) => ReturnType)
+ /**
+ * 单个消息超时时间,需设置 lifeMode 为 single 。
+ * 每个消息使用自己的超时时间,开启该模式却未设置时按 severity 判断超时时间。
+ */
+ life?: number
+ /**
+ * 消息 ID。
+ */
+ id?: any
+}
+
+export const toastProps = {
+ /**
+ * 必选,消息内容数组,Message 对象定义见下文。
+ */
+ value: {
+ type: Array as PropType,
+ required: true,
+ default: () => []
+ },
+ /**
+ * 可选,超时时间,超时后自动消失,鼠标悬停可以阻止消失,单位毫秒。
+ *
+ * @description 普通、成功、提示类默认为 5000 毫秒,错误、警告类默认为 10000 毫秒。
+ */
+ life: {
+ type: Number,
+ default: null
+ },
+ /**
+ * 可选,超时时间模式,预设值为 global 和 single 。
+ *
+ * @description
+ * 默认为 global,所有消息使用 life 或群组第一个消息的预设超时时间;
+ * 设置为 single 时,每个消息使用自身的超时时间,参见 Message 中的 life 定义。
+ *
+ * @default 'global'
+ */
+ lifeMode: {
+ type: String as PropType,
+ default: 'global'
+ },
+ /**
+ * 可选,是否常驻,默认自动关闭。
+ *
+ * @default false
+ */
+ sticky: {
+ type: Boolean,
+ default: false
+ },
+ /**
+ * 可选,样式。
+ */
+ style: {
+ type: Object as PropType,
+ default: () => ({})
+ },
+ /**
+ * 可选,类名。
+ */
+ styleClass: {
+ type: String
+ },
+ onCloseEvent: {
+ type: Function as PropType<(message: Message) => void>
+ },
+ onValueChange: {
+ type: Function as PropType<(restMessages: Message[]) => void>
+ }
+} as const
+
+export type ToastProps = ExtractPropTypes
diff --git a/devui/toast/toast.tsx b/devui/toast/toast.tsx
deleted file mode 100644
index 65f6ca822b15672cc621d07f31edab911b4bcdc1..0000000000000000000000000000000000000000
--- a/devui/toast/toast.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineComponent } from 'vue'
-
-export default defineComponent({
- name: 'd-toast',
- props: {
- },
- setup(props, ctx) {
- return () => {
- return devui-toast
- }
- }
-})
\ No newline at end of file
diff --git a/devui/vue-devui.ts b/devui/vue-devui.ts
index 2a0a2d82e5fdcb3e24f3903733d4ed1491650413..4cc3741e80ed1015a63dd87da13d6d202b1eed74 100644
--- a/devui/vue-devui.ts
+++ b/devui/vue-devui.ts
@@ -1,38 +1,72 @@
-import { App } from 'vue';
+import { App } from 'vue'
// 通用
-import Button from './button';
-import Icon from './icon';
-import Panel from './panel';
+import Button from './button'
+import Icon from './icon'
+import Panel from './panel'
// 导航
-import Tabs from './tabs';
+import Tabs from './tabs'
// 反馈
-import Alert from './alert/alert';
-import DLoading, { LoadingService, Loading } from './loading';
+import Alert from './alert/alert'
+import Toast, { ToastService } from './toast'
+import DLoading, { LoadingService, Loading } from './loading'
// 数据录入
-import Checkbox from './checkbox';
-import Radio from './radio';
-import Switch from './switch';
-import TagsInput from './tags-input';
-import TextInput from './text-input';
+import Checkbox from './checkbox'
+import Radio from './radio'
+import Switch from './switch'
+import TagsInput from './tags-input'
+import TextInput from './text-input'
// 数据展示
-import Avatar from './avatar';
-import Carousel from './carousel';
+import Avatar from './avatar'
+import Carousel from './carousel'
function install(app: App): void {
- const packages = [ Button, Icon, Panel, Tabs, Alert, DLoading, Checkbox, Radio, Switch, TagsInput, TextInput, Avatar, Carousel ];
+ const packages = [
+ Button,
+ Icon,
+ Panel,
+ Tabs,
+ Alert,
+ Toast,
+ ToastService,
+ DLoading,
+ Checkbox,
+ Radio,
+ Switch,
+ TagsInput,
+ TextInput,
+ Avatar,
+ Carousel
+ ]
packages.forEach((item: any) => {
if (item.install) {
- app.use(item);
+ app.use(item)
} else if (item.name) {
- app.component(item.name, item);
+ app.component(item.name, item)
}
- });
+ })
}
-export { Button, Icon, Panel, Tabs, Alert, LoadingService, Loading, Checkbox, Radio, Switch, TagsInput, TextInput, Avatar, Carousel };
-export default { install, version: '0.0.1' };
+export {
+ Button,
+ Icon,
+ Panel,
+ Tabs,
+ Alert,
+ Toast,
+ ToastService,
+ LoadingService,
+ Loading,
+ Checkbox,
+ Radio,
+ Switch,
+ TagsInput,
+ TextInput,
+ Avatar,
+ Carousel
+}
+export default { install, version: '0.0.1' }
diff --git a/sites/.vitepress/config/sidebar.ts b/sites/.vitepress/config/sidebar.ts
index c49dde11376d8e7494751e682b8cc726a8e13356..e98545224338c6552904b293921b37e8debdb97e 100644
--- a/sites/.vitepress/config/sidebar.ts
+++ b/sites/.vitepress/config/sidebar.ts
@@ -6,20 +6,19 @@ const sidebar = {
children: [
{ text: 'Button 按钮', link: '/components/button/' },
{ text: 'Icon 图标', link: '/components/icon/' },
- { text: 'Panel 面板', link: '/components/panel/' },
+ { text: 'Panel 面板', link: '/components/panel/' }
]
},
{
text: '导航',
- children: [
- { text: 'Tabs 选项卡切换', link: '/components/tabs/' },
- ]
+ children: [{ text: 'Tabs 选项卡切换', link: '/components/tabs/' }]
},
{
text: '反馈',
children: [
{ text: 'Alert 警告', link: '/components/alert/' },
{ text: 'Loading 加载提示', link: '/components/loading/' },
+ { text: 'Toast 全局通知', link: '/components/toast/' }
]
},
{
@@ -29,17 +28,17 @@ const sidebar = {
{ text: 'Radio 单选框', link: '/components/radio/' },
{ text: 'Switch 开关', link: '/components/switch/' },
{ text: 'TagsInput 标签输入', link: '/components/tags-input/' },
- { text: 'TextInput 文本框', link: '/components/text-input/' },
+ { text: 'TextInput 文本框', link: '/components/text-input/' }
]
},
{
text: '数据展示',
children: [
{ text: 'Avatar 头像', link: '/components/avatar/' },
- { text: 'Carousel 走马灯', link: '/components/carousel/' },
+ { text: 'Carousel 走马灯', link: '/components/carousel/' }
]
- },
- ],
+ }
+ ]
}
-export default sidebar
\ No newline at end of file
+export default sidebar
diff --git a/sites/components/toast/index.md b/sites/components/toast/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..2218666a7c7d57731ec5fd533f2052446343b728
--- /dev/null
+++ b/sites/components/toast/index.md
@@ -0,0 +1,611 @@
+# Toast 全局通知
+
+全局信息提示组件。
+
+### 何时使用
+
+当需要向用户全局展示提示信息时使用,显示数秒后消失。
+
+### 基本用法
+
+common 时不展示图标。
+
+
+
+```html
+
+```
+
+```ts
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+ setup() {
+ const msgs = ref([])
+
+ function showToast(type: any) {
+ switch (type) {
+ case 'link':
+ msgs.value = [
+ { severity: 'info', summary: 'Relative', detail: `Back to Home Page` },
+ { severity: 'info', summary: 'Absolute', content: 'slot:customTemplate', myInfo: 'Devui' }
+ ]
+ break
+ case 'multiple':
+ msgs.value = [
+ {
+ severity: 'info',
+ summary: 'Summary',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ },
+ {
+ severity: 'info',
+ summary: 'Summary',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ }
+ ]
+ break
+ case 'noTitle':
+ msgs.value = [{ severity: 'warn', content: 'This is a test text. This is a test text. This is a test text.' }]
+ break
+ case 'plainText':
+ msgs.value = [{ severity: 'info', content: 'data:' }]
+ break
+ default:
+ msgs.value = [
+ {
+ severity: type,
+ summary: 'Summary',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ }
+ ]
+ }
+ }
+
+ return {
+ msgs,
+ showToast
+ }
+ }
+})
+```
+
+### 超时时间
+
+当设置超时时间、没有标题时,则不展示标题和关闭按钮。
+
+
+
+ Success
+ Warn
+ Error
+ common
+
+
+```html
+
+
+ Success
+ Warn
+ Error
+ common
+
+```
+
+```ts
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+ setup() {
+ const msgs = ref([])
+
+ function showToast(type: any) {
+ switch (type) {
+ case 'error':
+ msgs.value = [{ severity: type, content: 'This is a test text. This is a test text. This is a test text.' }]
+ break
+ case 'common':
+ msgs.value = [{ severity: type, content: 'This is a test text. This is a test text. This is a test text.' }]
+ break
+ default:
+ msgs.value = [
+ {
+ severity: type,
+ summary: 'Summary',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ }
+ ]
+ }
+ }
+
+ return {
+ msgs,
+ showToast
+ }
+ }
+})
+```
+
+### 自定义样式
+
+
+
+
+
+```html
+
+```
+
+```scss
+.custom-class {
+ .devui-toast-item-container {
+ color: #252b3a;
+ background-color: #ffffff;
+
+ .devui-toast-icon-close {
+ top: 10px;
+ right: 13px;
+
+ & i.icon {
+ color: #252b3a !important;
+ }
+ }
+
+ .devui-toast-image {
+ top: 15px;
+ }
+
+ .devui-toast-message {
+ line-height: 23px;
+
+ .devui-toast-title {
+ font-size: 16px;
+ }
+
+ p {
+ font-size: 14px;
+ }
+ }
+ }
+}
+```
+
+```ts
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+ setup() {
+ const msgs = ref([])
+
+ function showToast() {
+ msgs.value = [
+ {
+ severity: 'success',
+ summary: 'Success',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ }
+ ]
+ }
+
+ return {
+ msgs,
+ showToast
+ }
+ }
+})
+```
+
+### 每个消息使用单独的超时时间
+
+当设置超时时间模式为 single 时,每个消息使用自身的 life 作为超时时间,如果未设置则按 severity 判断,severity 也未设置时默认超时时间为 5000 毫秒。
+
+
+
+```html
+
+```
+
+```ts
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+ setup() {
+ const msgs = ref([])
+
+ function showToast() {
+ msgs.value = [
+ { life: 3000, summary: 'Summary', content: 'This is a test text. This is a test text. This is a test text.' },
+ {
+ life: 6000,
+ severity: 'info',
+ summary: 'Summary',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ },
+ {
+ severity: 'success',
+ summary: 'Success',
+ content: 'This is a test text. This is a test text. This is a test text.'
+ },
+ { severity: 'warn', summary: 'Warn', content: 'This is a test text. This is a test text. This is a test text.' }
+ ]
+ }
+
+ return {
+ msgs,
+ showToast
+ }
+ }
+})
+```
+
+### 服务方式调用
+
+使用服务的方式创建 toast 全局通知。
+
+click me show simplest toast!
+click me show customer toast!
+click me close customer toast!
+only close first customer toast!
+
+```html
+click me show simplest toast!
+click me show customer toast!
+click me close customer toast!
+only close first customer toast!
+```
+
+```ts
+import { defineComponent, ref } from 'vue'
+import { ToastService } from 'devui/toast'
+
+export default defineComponent({
+ setup() {
+ const results = ref()
+
+ function openToast2() {
+ results.value = ToastService.open({
+ value: [
+ { severity: 'info', summary: 'summary', content: '1. I am content' },
+ { severity: 'error', summary: 'summary', content: '2. I am content' },
+ { severity: 'error', summary: 'summary', content: '3. I am content' }
+ ],
+ sticky: true,
+ style: { width: '600px', color: 'red' },
+ styleClass: 'myCustom-toast',
+ life: 5000,
+ lifeMode: 'global',
+ /*
+ 接收发射过来的数据
+ */
+ onCloseEvent(value: any) {
+ console.log('closeEvent', value)
+ },
+ onValueChange(value: any) {
+ console.log('valueChange', value)
+ }
+ })
+
+ console.log('results', results.value)
+
+ isShow.value = true
+ }
+
+ function closeToast2() {
+ results.value.toastInstance.close()
+ isShow.value = false
+ }
+
+ function closeToast3() {
+ /*
+ 1.可以根据指定下标关闭 results.value.toastInstance.close(0);
+ 2.可以根据value对象去关闭,作用跟1是等同的,如下所示:
+ */
+ results.value.toastInstance.close({ severity: 'info', summary: 'summary', content: '1. I am content' })
+ }
+
+ return {
+ isShow,
+ openToast,
+ openToast2,
+ closeToast2,
+ closeToast3
+ }
+ }
+})
+```
+
+### Toast Api
+
+
+
+### Toast Event
+
+
+
+### 接口 & 类型定义
+
+Message
+
+```ts
+export interface Message {
+ severity?: string // 预设值有 common、success、error、warn、info,超时时间参见 life 说明,未设置或非预设值时超时时间为 5000 毫秒,warn 和 error 为 10000 毫秒
+ summary?: string // 消息标题。当设置超时时间,未设置标题时,不展示标题和关闭按钮
+ detail?: string // 消息内容,推荐使用content替换
+ content?: string | `slot:${string}` | (message: Message) => ReturnType // 消息内容,支持纯文本和插槽,推荐使用
+ life?: number // 单个消息超时时间,需设置 lifeMode 为 single 。每个消息使用自己的超时时间,开启该模式却未设置时按 severity 判断超时时间
+ id?: any // 消息ID
+}
+```
+
+### Service 引入方式
+
+```ts
+import { ToastService } from 'devui'
+```
+
+### Service 使用
+
+```ts
+// 方式 1,局部引入 ToastService
+ToastService.open({ xxx })
+
+// 方式2,全局属性
+app.config.globalProperties.$toastService.open({ xxx })
+```
+
+### Service Api
+
+
+
+
+
+