From 9a66660e4d810ac643bb8b64e3481d25e372739c Mon Sep 17 00:00:00 2001 From: leihaohao Date: Sun, 8 Aug 2021 00:41:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20toast=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/toast/toast-icon-close.tsx | 41 ++++ devui/toast/toast-image.tsx | 94 +++++++++ devui/toast/toast-service.ts | 46 +++++ devui/toast/toast.scss | 134 +++++++++++++ devui/toast/toast.tsx | 314 ++++++++++++++++++++++++++++++- devui/toast/toast.type.ts | 99 ++++++++++ 6 files changed, 720 insertions(+), 8 deletions(-) create mode 100644 devui/toast/toast-icon-close.tsx create mode 100644 devui/toast/toast-image.tsx create mode 100644 devui/toast/toast-service.ts create mode 100644 devui/toast/toast.scss create mode 100644 devui/toast/toast.type.ts diff --git a/devui/toast/toast-icon-close.tsx b/devui/toast/toast-icon-close.tsx new file mode 100644 index 00000000..404f3ac0 --- /dev/null +++ b/devui/toast/toast-icon-close.tsx @@ -0,0 +1,41 @@ +import { defineComponent, PropType } from 'vue' + +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/toast-image.tsx b/devui/toast/toast-image.tsx new file mode 100644 index 00000000..9fe611a1 --- /dev/null +++ b/devui/toast/toast-image.tsx @@ -0,0 +1,94 @@ +import { defineComponent, PropType } from 'vue' +import { IToastSeverity } from './toast.type' + +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 svgInnerVNode = () => { + switch (severity) { + case 'info': + return ( + + + + ) + case 'success': + return ( + <> + + + + + + + + + + + ) + case 'warn': + return ( + + + + + ) + case 'error': + return ( + <> + + + + + + + + + + + ) + } + } + + return ( + + + {svgInnerVNode()} + + + ) + } +}) diff --git a/devui/toast/toast-service.ts b/devui/toast/toast-service.ts new file mode 100644 index 00000000..a9f942a3 --- /dev/null +++ b/devui/toast/toast-service.ts @@ -0,0 +1,46 @@ +import { App, createApp, onUnmounted } from 'vue' +import { ToastProps } from './toast.type' +import DToast from './toast' +import { uniqueId } from 'lodash-es' + +function _id() { + return uniqueId('d-toast-service') +} + +function createToastApp(props: Record) { + return createApp(DToast, props) +} + +export function useToastService(props: Partial & Pick) { + let $body: HTMLElement | null = document.body + let $div: HTMLDivElement | null = document.createElement('div') + + $div.dataset.id = _id() + $body!.appendChild($div) + + let app: App | null = createToastApp({ + ...(props ?? {}), + onHidden: () => app?.unmount() + }) + const toastInstance = app.mount($div) + + onUnmounted(() => { + $body!.removeChild($div!) + + $body = null + $div = null + app = null + }, toastInstance.$) + + return { + toastInstance, + close: () => (toastInstance as any)?.removeAll?.() + } +} + +export default function installToastService(app: App) { + if ((installToastService as any).installed) return + + app.config.globalProperties.$toastService = useToastService + ;(installToastService as any).installed = true +} diff --git a/devui/toast/toast.scss b/devui/toast/toast.scss new file mode 100644 index 00000000..babf5770 --- /dev/null +++ b/devui/toast/toast.scss @@ -0,0 +1,134 @@ +@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; + + .devui-toast-close-icon { + fill: $devui-light-text; + } +} + +.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; + + &.devui-toast-image-warn { + path.devui-icon-warning-outer { + fill: $devui-warning-line; + } + + path.devui-icon-warning-inner { + fill: $devui-light-text; + stroke: $devui-light-text; + } + } + + &.devui-toast-image-info { + background-color: $devui-info; + } + + &.devui-toast-image-error { + background-color: $devui-danger; + } + + &.devui-toast-image-success { + background-color: $devui-success; + } + + .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/toast.tsx b/devui/toast/toast.tsx index 65f6ca82..309054eb 100644 --- a/devui/toast/toast.tsx +++ b/devui/toast/toast.tsx @@ -1,12 +1,310 @@ -import { defineComponent } from 'vue' +import { computed, defineComponent, onUnmounted, ref, Teleport, watch } from 'vue' +import { Message, ToastProps, toastProps } from './toast.type' +import DToastIconClose from './toast-icon-close' +import DToastImage from './toast-image' +import { cloneDeep, defaults, isEqual, throttle } from 'lodash-es' + +import './toast.scss' + +const ANIMATION_TIME = 300 +const ANIMATION_NAME = 'slide-in' +const ID_PREFIX = 'toast-message' export default defineComponent({ - name: 'd-toast', - props: { - }, - setup(props, ctx) { - return () => { - return
devui-toast
+ name: 'DToast', + inheritAttrs: false, + props: toastProps, + emits: ['closeEvent', 'valueChange'], + setup(props: ToastProps, ctx) { + 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 + + initValue() + 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 severityDelay(msg: Message) { + switch (msg.severity) { + case 'warn': + case 'error': + return 10e3 + default: + return 5e3 + } + } + + function initValue() { + const cloneValue = cloneDeep(props.value) + messages.value = cloneValue.map((v, i) => defaults(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(() => { + ctx.emit('closeEvent', msg) + + if (hasMsgAnimation()) { + messages.value.splice(i, 1) + ctx.emit('valueChange', messages.value) + } else { + messages.value = [] + ctx.emit('valueChange', 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(() => { + ctx.emit('closeEvent', messages.value[i]) + + messages.value.splice(i, 1) + + ctx.emit('valueChange', messages.value) + + if (props.lifeMode === 'global') { + removeReset() + } + }, ANIMATION_TIME) + } + + function removeAll() { + if (messages.value.length > 0) { + msgAnimations.value = [] + + setTimeout(() => { + messages.value.forEach((msg) => ctx.emit('closeEvent', msg)) + + messages.value = [] + + ctx.emit('valueChange', 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 index = messages.value.findIndex((_msg) => isEqual(_msg, msg)) + removeIndexThrottle(index) + } + + function removeMsgAnimation(msg: Message) { + msgAnimations.value = msgAnimations.value.filter((_msg) => _msg !== msg) + } + + function msgItemRef(i: number) { + return msgItemRefs.value[i] as HTMLDivElement + } + + function hasMsgAnimation() { + return msgAnimations.value.length > 0 + } + + function onHidden() { + setTimeout(() => (ctx.attrs.onHidden as () => void)?.(), ANIMATION_TIME) } + + return { + messages, + msgAnimations, + zIndex, + containerRef, + msgItemRefs, + interrupt, + remove, + removeReset, + removeThrottle, + removeMsgThrottle, + removeAll, + 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) => (msg.content ? $slots[msg.content]?.(msg) ?? msg.content : null) + + 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} +
+
+
+ ))} +
+ ) } -}) \ No newline at end of file +}) diff --git a/devui/toast/toast.type.ts b/devui/toast/toast.type.ts new file mode 100644 index 00000000..339285ed --- /dev/null +++ b/devui/toast/toast.type.ts @@ -0,0 +1,99 @@ +import type { CSSProperties, ExtractPropTypes, PropType } 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 + /** + * 单个消息超时时间,需设置 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 -- Gitee From 77fd78056e8366b603dd07e4bb21c434c867fa74 Mon Sep 17 00:00:00 2001 From: leihaohao Date: Mon, 9 Aug 2021 19:16:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20Toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整 Toast 目录结构 - 修改 Toast 图标为 DIcon - 修改 Toast 样式颜色 - 修改 Toast Message content 属性 slot 使用方式 - Toast Message content 支持 render 函数 - 增加 DevuiApiTable 组件 - 修改 Toast Api 文档 - 修改 Toast install 方式 --- .gitignore | 1 + .../devui-api-table/devui-api-table.tsx | 39 ++ .../devui-api-table/devui-api-table.type.ts | 23 + devui/shared/devui-api-table/index.ts | 1 + devui/toast/demo/toast-demo.tsx | 12 - devui/toast/demo/toast.route.ts | 15 - devui/toast/doc/api-cn.md | 48 -- devui/toast/doc/api-en.md | 48 -- devui/toast/hooks/use-toast-constant.ts | 11 + devui/toast/hooks/use-toast-event.ts | 23 + devui/toast/hooks/use-toast-helper.ts | 15 + devui/toast/index.ts | 12 + devui/toast/{ => src}/toast-icon-close.tsx | 23 +- devui/toast/src/toast-image.tsx | 27 + devui/toast/src/toast-service.ts | 35 + devui/toast/{ => src}/toast.scss | 41 +- devui/toast/{ => src}/toast.tsx | 108 ++-- devui/toast/{ => src}/toast.type.ts | 4 +- devui/toast/toast-image.tsx | 94 --- devui/toast/toast-service.ts | 46 -- devui/vue-devui.ts | 74 ++- sites/.vitepress/config/sidebar.ts | 17 +- sites/components/toast/index.md | 611 ++++++++++++++++++ 23 files changed, 949 insertions(+), 379 deletions(-) create mode 100644 devui/shared/devui-api-table/devui-api-table.tsx create mode 100644 devui/shared/devui-api-table/devui-api-table.type.ts create mode 100644 devui/shared/devui-api-table/index.ts delete mode 100644 devui/toast/demo/toast-demo.tsx delete mode 100644 devui/toast/demo/toast.route.ts delete mode 100644 devui/toast/doc/api-cn.md delete mode 100644 devui/toast/doc/api-en.md create mode 100644 devui/toast/hooks/use-toast-constant.ts create mode 100644 devui/toast/hooks/use-toast-event.ts create mode 100644 devui/toast/hooks/use-toast-helper.ts create mode 100644 devui/toast/index.ts rename devui/toast/{ => src}/toast-icon-close.tsx (31%) create mode 100644 devui/toast/src/toast-image.tsx create mode 100644 devui/toast/src/toast-service.ts rename devui/toast/{ => src}/toast.scss (72%) rename devui/toast/{ => src}/toast.tsx (76%) rename devui/toast/{ => src}/toast.type.ts (94%) delete mode 100644 devui/toast/toast-image.tsx delete mode 100644 devui/toast/toast-service.ts create mode 100644 sites/components/toast/index.md diff --git a/.gitignore b/.gitignore index 9c5f67e6..63647070 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 00000000..a1076a67 --- /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) => ( + + ))} + + {data.map((row) => ( + + {columns.map((col) => ( + + ))} + + ))} +
{col.title}
{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 00000000..359f59df --- /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 00000000..fb6d8d0b --- /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 2781b035..00000000 --- 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 4000ac66..00000000 --- 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 434c8da3..00000000 --- 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 8cf9d150..00000000 --- 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 00000000..af0627de --- /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 00000000..26ff6e03 --- /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 00000000..fc12f581 --- /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 00000000..f6773433 --- /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/toast-icon-close.tsx b/devui/toast/src/toast-icon-close.tsx similarity index 31% rename from devui/toast/toast-icon-close.tsx rename to devui/toast/src/toast-icon-close.tsx index 404f3ac0..f1aa5493 100644 --- a/devui/toast/toast-icon-close.tsx +++ b/devui/toast/src/toast-icon-close.tsx @@ -1,4 +1,5 @@ import { defineComponent, PropType } from 'vue' +import DIcon from '../../icon' export default defineComponent({ name: 'DToastIconClose', @@ -14,27 +15,7 @@ export default defineComponent({ 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 00000000..d009677d --- /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 00000000..e1813e55 --- /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/toast.scss b/devui/toast/src/toast.scss similarity index 72% rename from devui/toast/toast.scss rename to devui/toast/src/toast.scss index babf5770..82a86a44 100644 --- a/devui/toast/toast.scss +++ b/devui/toast/src/toast.scss @@ -1,9 +1,9 @@ -@import '../style/mixins/index'; -@import '../style/theme/color'; -@import '../style/theme/shadow'; -@import '../style/theme/corner'; -@import '../style/core/_font'; -@import '../style/core/animation'; +@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; @@ -60,8 +60,8 @@ right: 10px; cursor: pointer; - .devui-toast-close-icon { - fill: $devui-light-text; + & i.icon { + color: $devui-light-text !important; } } @@ -82,27 +82,24 @@ top: 14px; padding: 0; - &.devui-toast-image-warn { - path.devui-icon-warning-outer { - fill: $devui-warning-line; - } + & i.icon { + vertical-align: 0; + } - path.devui-icon-warning-inner { - fill: $devui-light-text; - stroke: $devui-light-text; - } + &.devui-toast-image-warn i.icon { + color: $devui-warning !important; } - &.devui-toast-image-info { - background-color: $devui-info; + &.devui-toast-image-info i.icon { + color: $devui-info !important; } - &.devui-toast-image-error { - background-color: $devui-danger; + &.devui-toast-image-error i.icon { + color: $devui-danger !important; } - &.devui-toast-image-success { - background-color: $devui-success; + &.devui-toast-image-success i.icon { + color: $devui-success !important; } .devui-toast-image-info-path, diff --git a/devui/toast/toast.tsx b/devui/toast/src/toast.tsx similarity index 76% rename from devui/toast/toast.tsx rename to devui/toast/src/toast.tsx index 309054eb..11d0c886 100644 --- a/devui/toast/toast.tsx +++ b/devui/toast/src/toast.tsx @@ -1,14 +1,15 @@ -import { computed, defineComponent, onUnmounted, ref, Teleport, watch } from 'vue' +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, defaults, isEqual, throttle } from 'lodash-es' - -import './toast.scss' +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_TIME = 300 -const ANIMATION_NAME = 'slide-in' -const ID_PREFIX = 'toast-message' +const { ANIMATION_NAME, ANIMATION_TIME, ID_PREFIX } = useToastConstant() export default defineComponent({ name: 'DToast', @@ -16,6 +17,9 @@ export default defineComponent({ 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([]) @@ -42,8 +46,14 @@ export default defineComponent({ (value) => { if (value.length === 0) return - initValue() - handleValueChange() + if (hasMsgAnimation()) { + initValue() + } + + nextTick(() => { + initValue(value) + handleValueChange() + }) }, { deep: true, immediate: true } ) @@ -68,19 +78,9 @@ export default defineComponent({ } }) - function severityDelay(msg: Message) { - switch (msg.severity) { - case 'warn': - case 'error': - return 10e3 - default: - return 5e3 - } - } - - function initValue() { - const cloneValue = cloneDeep(props.value) - messages.value = cloneValue.map((v, i) => defaults(v, { id: `${ID_PREFIX}-${i}` })) + function initValue(value: Message[] = []) { + const cloneValue = cloneDeep(value) + messages.value = cloneValue.map((v, i) => merge(v, { id: `${ID_PREFIX}-${i}` })) msgAnimations.value = [] } @@ -117,15 +117,15 @@ export default defineComponent({ function singleModeRemove(msg: Message, i: number) { removeMsgAnimation(msg) setTimeout(() => { - ctx.emit('closeEvent', msg) + onCloseEvent(msg) if (hasMsgAnimation()) { messages.value.splice(i, 1) - ctx.emit('valueChange', messages.value) } else { messages.value = [] - ctx.emit('valueChange', messages.value) } + + onValueChange(messages.value) }, ANIMATION_TIME) } @@ -162,11 +162,11 @@ export default defineComponent({ removeMsgAnimation(messages.value[i]) setTimeout(() => { - ctx.emit('closeEvent', messages.value[i]) + onCloseEvent(messages.value[i]) messages.value.splice(i, 1) - ctx.emit('valueChange', messages.value) + onValueChange(messages.value) if (props.lifeMode === 'global') { removeReset() @@ -179,11 +179,11 @@ export default defineComponent({ msgAnimations.value = [] setTimeout(() => { - messages.value.forEach((msg) => ctx.emit('closeEvent', msg)) + messages.value.forEach((msg) => onCloseEvent(msg)) messages.value = [] - ctx.emit('valueChange', messages.value) + onValueChange(messages.value) }, ANIMATION_TIME) } } @@ -212,7 +212,8 @@ export default defineComponent({ } function removeMsgThrottle(msg: Message) { - const index = messages.value.findIndex((_msg) => isEqual(_msg, msg)) + const ignoreDiffKeys = ['id'] + const index = messages.value.findIndex((_msg) => isEqual(omit(_msg, ignoreDiffKeys), omit(msg, ignoreDiffKeys))) removeIndexThrottle(index) } @@ -220,6 +221,18 @@ export default defineComponent({ 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 } @@ -228,10 +241,6 @@ export default defineComponent({ return msgAnimations.value.length > 0 } - function onHidden() { - setTimeout(() => (ctx.attrs.onHidden as () => void)?.(), ANIMATION_TIME) - } - return { messages, msgAnimations, @@ -239,11 +248,9 @@ export default defineComponent({ containerRef, msgItemRefs, interrupt, - remove, removeReset, removeThrottle, - removeMsgThrottle, - removeAll, + close, msgItemRef } }, @@ -280,24 +287,41 @@ export default defineComponent({ const showContent = (msg: Message) => !!msg.content const showDetail = (msg: Message) => !showContent(msg) && !!msg.detail - const msgContent = (msg: Message) => (msg.content ? $slots[msg.content]?.(msg) ?? msg.content : null) + 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' + aria-live="polite" onMouseenter={() => interrupt(i)} onMouseleave={() => removeReset(i, msg)} >
{showClose(msg) ? removeThrottle(i)} /> : null} {showImage(msg) ? : null} -
- {showSummary(msg) ? {msg.summary} : null} +
+ {showSummary(msg) ? {msg.summary} : null} {showContent(msg) ? msgContent(msg) : null} {showDetail(msg) ?

: null}
diff --git a/devui/toast/toast.type.ts b/devui/toast/src/toast.type.ts similarity index 94% rename from devui/toast/toast.type.ts rename to devui/toast/src/toast.type.ts index 339285ed..458161ec 100644 --- a/devui/toast/toast.type.ts +++ b/devui/toast/src/toast.type.ts @@ -1,4 +1,4 @@ -import type { CSSProperties, ExtractPropTypes, PropType } from 'vue' +import type { CSSProperties, ExtractPropTypes, PropType, h } from 'vue' export type IToastLifeMode = 'single' | 'global' export type IToastSeverity = 'common' | 'success' | 'error' | 'warn' | 'info' | string @@ -23,7 +23,7 @@ export interface Message { /** * 消息内容,支持纯文本和插槽,推荐使用。 */ - content?: string + content?: string | `slot:${string}` | ((message: Message) => ReturnType) /** * 单个消息超时时间,需设置 lifeMode 为 single 。 * 每个消息使用自己的超时时间,开启该模式却未设置时按 severity 判断超时时间。 diff --git a/devui/toast/toast-image.tsx b/devui/toast/toast-image.tsx deleted file mode 100644 index 9fe611a1..00000000 --- a/devui/toast/toast-image.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { defineComponent, PropType } from 'vue' -import { IToastSeverity } from './toast.type' - -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 svgInnerVNode = () => { - switch (severity) { - case 'info': - return ( - - - - ) - case 'success': - return ( - <> - - - - - - - - - - - ) - case 'warn': - return ( - - - - - ) - case 'error': - return ( - <> - - - - - - - - - - - ) - } - } - - return ( - - - {svgInnerVNode()} - - - ) - } -}) diff --git a/devui/toast/toast-service.ts b/devui/toast/toast-service.ts deleted file mode 100644 index a9f942a3..00000000 --- a/devui/toast/toast-service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { App, createApp, onUnmounted } from 'vue' -import { ToastProps } from './toast.type' -import DToast from './toast' -import { uniqueId } from 'lodash-es' - -function _id() { - return uniqueId('d-toast-service') -} - -function createToastApp(props: Record) { - return createApp(DToast, props) -} - -export function useToastService(props: Partial & Pick) { - let $body: HTMLElement | null = document.body - let $div: HTMLDivElement | null = document.createElement('div') - - $div.dataset.id = _id() - $body!.appendChild($div) - - let app: App | null = createToastApp({ - ...(props ?? {}), - onHidden: () => app?.unmount() - }) - const toastInstance = app.mount($div) - - onUnmounted(() => { - $body!.removeChild($div!) - - $body = null - $div = null - app = null - }, toastInstance.$) - - return { - toastInstance, - close: () => (toastInstance as any)?.removeAll?.() - } -} - -export default function installToastService(app: App) { - if ((installToastService as any).installed) return - - app.config.globalProperties.$toastService = useToastService - ;(installToastService as any).installed = true -} diff --git a/devui/vue-devui.ts b/devui/vue-devui.ts index 2a0a2d82..4cc3741e 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 c49dde11..e9854522 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 00000000..2218666a --- /dev/null +++ b/sites/components/toast/index.md @@ -0,0 +1,611 @@ +# Toast 全局通知 + +全局信息提示组件。 + +### 何时使用 + +当需要向用户全局展示提示信息时使用,显示数秒后消失。 + +### 基本用法 + +common 时不展示图标。 + +
+ + + + Success + Warn + Error + Multiple + link + pure text + common + no title +
+ +```html +
+ + + + Success + Warn + Error + Multiple + link + pure text + common + no title +
+``` + +```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 + } + } +}) +``` + +### 自定义样式 + +
+ +
+ + Custom Style +
+ +```html +
+ + Custom Style +
+``` + +```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 毫秒。 + +
+ + Single +
+ +```html +
+ + Single +
+``` + +```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 + + + + + + -- Gitee