From c4a6b8ee3af4065c8215e85d2d9d782c57a4fe4e Mon Sep 17 00:00:00 2001 From: wangyupei <2311595895@qq.com> Date: Wed, 9 Mar 2022 15:04:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(badge):=20=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E5=81=8F=E7=A7=BB=E9=87=8F=E5=8F=82=E6=95=B0=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/devui-vue/devui/badge/index.ts | 17 +- .../devui-vue/devui/badge/src/badge-types.ts | 40 ++-- packages/devui-vue/devui/badge/src/badge.tsx | 65 +++---- .../devui-vue/docs/components/badge/index.md | 172 +++++++++--------- 4 files changed, 148 insertions(+), 146 deletions(-) diff --git a/packages/devui-vue/devui/badge/index.ts b/packages/devui-vue/devui/badge/index.ts index 8657a733b7..d6a4e971d7 100644 --- a/packages/devui-vue/devui/badge/index.ts +++ b/packages/devui-vue/devui/badge/index.ts @@ -1,17 +1,14 @@ -import type { App } from 'vue' -import Badge from './src/badge' +import type { App } from 'vue'; +import Badge from './src/badge'; +export * from './src/badge-types'; -Badge.install = function (app: App) { - app.component(Badge.name, Badge) -} - -export { Badge } +export { Badge }; export default { title: 'Badge 徽标', category: '数据展示', status: '100%', install(app: App): void { - app.use(Badge as any) - } -} + app.component(Badge.name, Badge); + }, +}; diff --git a/packages/devui-vue/devui/badge/src/badge-types.ts b/packages/devui-vue/devui/badge/src/badge-types.ts index b0a5ba2fdf..ee212e5311 100644 --- a/packages/devui-vue/devui/badge/src/badge-types.ts +++ b/packages/devui-vue/devui/badge/src/badge-types.ts @@ -1,41 +1,41 @@ -import type { PropType, ExtractPropTypes } from 'vue' +import type { PropType, ExtractPropTypes } from 'vue'; -type BadgeStatusType = PropType<'danger' | 'warning' | 'waiting' | 'success' | 'info'> -type BadgePositionType = PropType<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'> +export type BadgeStatusType = 'danger' | 'warning' | 'waiting' | 'success' | 'info'; +export type BadgePositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; -const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info'] -const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] +const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info']; +const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; export const badgeProps = { count: { - type: [Number, String] + type: [Number, String], }, maxCount: { type: Number, - default: 99 + default: 99, }, showDot: { type: Boolean, - default: false + default: false, }, status: { - type: String as BadgeStatusType, - validator: (val: string) => badgeStatusType.includes(val) + type: String as PropType, + validator: (val: string): boolean => badgeStatusType.includes(val), }, - badgePos: { - type: String as BadgePositionType, + position: { + type: String as PropType, default: 'top-right', - validator: (val: string) => badgePositionType.includes(val) + validator: (val: string): boolean => badgePositionType.includes(val), }, - offsetXY: { - type: Array + offset: { + type: Array as PropType>, }, bgColor: { - type: String + type: String, }, textColor: { - type: String - } -} + type: String, + }, +}; -export type BadgeProps = ExtractPropTypes +export type BadgeProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/badge/src/badge.tsx b/packages/devui-vue/devui/badge/src/badge.tsx index ad6024bd19..a4b428eca6 100644 --- a/packages/devui-vue/devui/badge/src/badge.tsx +++ b/packages/devui-vue/devui/badge/src/badge.tsx @@ -1,65 +1,60 @@ -import './badge.scss' - -import { defineComponent, computed } from 'vue' -import { badgeProps, BadgeProps } from './badge-types' +import { defineComponent, computed } from 'vue'; +import { badgeProps, BadgeProps } from './badge-types'; +import './badge.scss'; export default defineComponent({ name: 'DBadge', props: badgeProps, - emits: [], setup(props: BadgeProps, ctx) { const className = computed(() => { - const base = 'devui-badge-content' + const base = 'devui-badge-content'; return [ base, props.showDot ? `${base}-dot` : `${base}-count`, props.status && `${base}-${props.status}`, - ctx.slots.default && props.badgePos && `${base}-${props.badgePos}`, - ctx.slots.default && `${base}-fixed` - ].join(' ') - }) + ctx.slots.default && props.position && `${base}-${props.position}`, + ctx.slots.default && `${base}-fixed`, + ].join(' '); + }); const style = computed(() => { const styleMap = { bgColor: 'background', - textColor: 'color' - } - const ret = Object.keys(styleMap).reduce((ret, key) => { - if (props[key]) { - ret[styleMap[key]] = props[key] - } - return ret - }, {}) - // 偏移量 - if (ctx.slots.default && props.offsetXY) { - const [x, y]: Array = props.offsetXY as Array - const [yName, xName] = (props.badgePos as string).split('-') - ret[yName] = y + 'px' - ret[xName] = x + 'px' + textColor: 'color', + }; + const ret = Object.keys(styleMap).reduce((result, key) => { + props[key] && (result[styleMap[key]] = props[key]); + return result; + }, {}); + if (ctx.slots.default && props.offset) { + const [x, y]: Array = props.offset; + const [yName, xName] = props.position.split('-'); + ret[yName] = y + 'px'; + ret[xName] = x + 'px'; } - return ret - }) + return ret; + }); const text = computed(() => { if (props.showDot) { - return + return; } if (typeof props.count === 'number' && typeof props.maxCount === 'number') { - return props.count > props.maxCount ? `${props.maxCount}+` : props.count + return props.count > props.maxCount ? `${props.maxCount}+` : props.count; } - return props.count - }) + return props.count; + }); return () => { return ( -
+
{ctx.slots.default?.()}
{text.value}
- ) - } - } -}) + ); + }; + }, +}); diff --git a/packages/devui-vue/docs/components/badge/index.md b/packages/devui-vue/docs/components/badge/index.md index 41f8007189..5597e7fe30 100644 --- a/packages/devui-vue/docs/components/badge/index.md +++ b/packages/devui-vue/docs/components/badge/index.md @@ -2,7 +2,7 @@ 图标右上角的圆形徽标数字。 -### 何时使用 +#### 何时使用 出现在图标右上角或列表项右方,通过不同的状态色加数字提示用户有消息需要处理时。 @@ -12,34 +12,51 @@ ```vue + + ``` ::: ### 点状徽章 -:::demo 点状徽章类型,当有包裹元素且 showDot 参数为 true 时为点状徽章,默认在右上角展示小点不显示数目。 +:::demo 点状徽章类型,当有包裹元素且 `show-dot` 参数为 true 时为点状徽章,默认在右上角展示小点不显示数目。 ```vue + + ``` ::: @@ -50,41 +67,57 @@ ```vue + + ``` ::: ### 状态徽章 -:::demo 当徽章独立使用、不包裹任何元素且 showDot 参数为 true 时为状态徽章,不同状态展示不同色点。 +:::demo 当徽章独立使用、不包裹任何元素且 `show-dot` 参数为 true 时为状态徽章,不同状态展示不同色点。 ```vue ``` @@ -93,20 +126,16 @@ ### 徽章位置 -:::demo 通过 badgePos 参数设置徽章位置。 +:::demo 通过 `position` 参数设置徽章位置。 ```vue @@ -116,64 +145,45 @@ ### 自定义 -:::demo 通过 bgColor 参数设置徽章展示状态色(此时 status 参数设置的徽章状态色失效),通过 offsetXY 参数可设置相对于 badgePos 的徽章偏移量。通过 textColor、bgColor 自定义文字、背景颜色。 +:::demo 通过 `bg-color` 参数设置徽章展示状态色(此时 `status` 参数设置的徽章状态色失效),通过 `offset` 参数可设置相对于 position 的徽章偏移量。通过 ` text-color``、bgColor ` 自定义文字、背景颜色。 ```vue ``` ::: -### API - -| 参数 | 类型 | 默认 | 说明 | -| :-------: | :-----------------: | :---------: | :--------------------------------------------------------------------------------------------------------------------------- | -| count | `Number` | -- | 可选,设置基本徽章和计数徽章中显示的数目 | -| maxCount | `Number` | 99 | 可选,设置基本徽章和计数徽章最大可显示数目,当 count > maxCount 时显示 maxCount+ | -| showDot | `Boolean` | false | 可选,true 时为点状徽章(有包裹)或状态徽章(无包裹),false 时为基本徽章(有包裹)或计数徽章(无包裹) | -| status | `BadgeStatusType` | -- | 可选,状态色 danger\| warning \| waiting \| success \| info | -| badgePos | `BadgePositionType` | 'top-right' | 可选,徽标位置 top-left\| top-right \| bottom-left \| bottom-right | -| bgColor | `String` | -- | 可选,自定义徽标色,此时 status 参数设置的徽章状态色失效 | -| textColor | `String` | -- | 可选, 可自定义徽标文字颜色 | -| offsetXY | `[number, number] ` | -- | 可选,可选,有包裹时徽标位置偏移量,格式为[x,y],单位为 px。x 为相对 right 或 left 的偏移量,y 为相对 top 或 bottom 的偏移量 | - - +### d-badge 参数 + +| 参数 | 类型 | 默认 | 说明 | +| ---------- | ------------------- | ----------- | :--------------------------------------------------------------------------------------------------------------------- | +| count | `Number` | -- | 可选,设置基本徽章和计数徽章中显示的数目 | +| max-count | `Number` | 99 | 可选,设置基本徽章和计数徽章最大可显示数目,当 count > `max-count` 时显示 `max-count+` | +| show-dot | `Boolean` | false | 可选,true 时为点状徽章(有包裹)或状态徽章(无包裹),false 时为基本徽章(有包裹)或计数徽章(无包裹) | +| status | `BadgeStatusType` | -- | 可选,状态色 danger\| warning \| waiting \| success \| info | +| position | `BadgePositionType` | 'top-right' | 可选,徽标位置 top-left\| top-right \| bottom-left \| bottom-right | +| bg-color | `String` | -- | 可选,自定义徽标色,此时 status 参数设置的徽章状态色失效 | +| text-color | `String` | -- | 可选, 可自定义徽标文字颜色 | +| offset | `[number, number]` | -- | 可选,有包裹时徽标位置偏移量,格式为[x,y],单位为 px。x 为相对 right 或 left 的偏移量,y 为相对 top 或 bottom 的偏移量 | + +### BadgeStatusType 类型 + +```typescript +type BadgeStatusType = 'danger' | 'warning' | 'waiting' | 'success' | 'info'; +``` + +### BadgePositionType 类型 + +```typescript +type BadgePositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +``` From 66f8ce6efb7d2b19a373aa8d1ad906d2cde68e28 Mon Sep 17 00:00:00 2001 From: wangyupei <2311595895@qq.com> Date: Thu, 10 Mar 2022 10:31:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(Toast):=20=E9=87=8D=E6=9E=84Toast?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA?= =?UTF-8?q?Notification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui-vue/devui/notification/index.ts | 16 + .../src/notification-icon-close.tsx | 13 + .../notification/src/notification-image.tsx | 28 + .../notification/src/notification-service.tsx | 60 +++ .../notification/src/notification-types.ts | 40 ++ .../src/notification.scss} | 78 ++- .../devui/notification/src/notification.tsx | 36 ++ .../notification/src/use-notification.ts | 59 ++ .../devui/toast/__tests__/toast.spec.ts | 295 ---------- packages/devui-vue/devui/toast/index.ts | 19 - .../toast/src/hooks/use-toast-constant.ts | 11 - .../devui/toast/src/hooks/use-toast-event.ts | 23 - .../devui/toast/src/hooks/use-toast-helper.ts | 15 - .../toast/src/hooks/use-toast-z-index.ts | 5 - .../devui/toast/src/toast-icon-close.tsx | 22 - .../devui-vue/devui/toast/src/toast-image.tsx | 27 - .../devui/toast/src/toast-service.ts | 35 -- .../devui-vue/devui/toast/src/toast-types.ts | 99 ---- packages/devui-vue/devui/toast/src/toast.tsx | 336 ------------ .../devui-vue/devui/upload/src/upload.tsx | 292 +++++----- .../docs/components/notification/index.md | 212 ++++++++ .../devui-vue/docs/components/toast/index.md | 502 ------------------ 22 files changed, 625 insertions(+), 1598 deletions(-) create mode 100644 packages/devui-vue/devui/notification/index.ts create mode 100644 packages/devui-vue/devui/notification/src/notification-icon-close.tsx create mode 100644 packages/devui-vue/devui/notification/src/notification-image.tsx create mode 100644 packages/devui-vue/devui/notification/src/notification-service.tsx create mode 100644 packages/devui-vue/devui/notification/src/notification-types.ts rename packages/devui-vue/devui/{toast/src/toast.scss => notification/src/notification.scss} (56%) create mode 100644 packages/devui-vue/devui/notification/src/notification.tsx create mode 100644 packages/devui-vue/devui/notification/src/use-notification.ts delete mode 100644 packages/devui-vue/devui/toast/__tests__/toast.spec.ts delete mode 100644 packages/devui-vue/devui/toast/index.ts delete mode 100644 packages/devui-vue/devui/toast/src/hooks/use-toast-constant.ts delete mode 100644 packages/devui-vue/devui/toast/src/hooks/use-toast-event.ts delete mode 100644 packages/devui-vue/devui/toast/src/hooks/use-toast-helper.ts delete mode 100644 packages/devui-vue/devui/toast/src/hooks/use-toast-z-index.ts delete mode 100644 packages/devui-vue/devui/toast/src/toast-icon-close.tsx delete mode 100644 packages/devui-vue/devui/toast/src/toast-image.tsx delete mode 100644 packages/devui-vue/devui/toast/src/toast-service.ts delete mode 100644 packages/devui-vue/devui/toast/src/toast-types.ts delete mode 100644 packages/devui-vue/devui/toast/src/toast.tsx create mode 100644 packages/devui-vue/docs/components/notification/index.md delete mode 100644 packages/devui-vue/docs/components/toast/index.md diff --git a/packages/devui-vue/devui/notification/index.ts b/packages/devui-vue/devui/notification/index.ts new file mode 100644 index 0000000000..b39bd22007 --- /dev/null +++ b/packages/devui-vue/devui/notification/index.ts @@ -0,0 +1,16 @@ +import type { App } from 'vue'; +import Notification from './src/notification'; +import NotificationService from './src/notification-service'; +export * from './src/notification-types'; + +export { Notification, NotificationService }; + +export default { + title: 'Notification 全局通知', + category: '反馈', + status: '100%', + install(app: App): void { + app.component(Notification.name, Notification); + app.config.globalProperties.$notificationService = NotificationService; + }, +}; diff --git a/packages/devui-vue/devui/notification/src/notification-icon-close.tsx b/packages/devui-vue/devui/notification/src/notification-icon-close.tsx new file mode 100644 index 0000000000..9c316db469 --- /dev/null +++ b/packages/devui-vue/devui/notification/src/notification-icon-close.tsx @@ -0,0 +1,13 @@ +import { defineComponent } from 'vue'; +import { Icon } from '../../icon'; + +export default defineComponent({ + emits: ['click'], + setup(props, { emit }) { + return () => ( +
emit('click', e)}> + +
+ ); + }, +}); diff --git a/packages/devui-vue/devui/notification/src/notification-image.tsx b/packages/devui-vue/devui/notification/src/notification-image.tsx new file mode 100644 index 0000000000..ee44c231e1 --- /dev/null +++ b/packages/devui-vue/devui/notification/src/notification-image.tsx @@ -0,0 +1,28 @@ +import { computed, defineComponent, toRefs } from 'vue'; +import type { PropType } from 'vue'; +import { NotificationType } from './notification-types'; +import { Icon } from '../../icon'; + +export default defineComponent({ + props: { + type: { + type: String as PropType, + default: 'normal', + }, + }, + setup(props) { + const { type } = toRefs(props); + const classes = computed(() => ({ + 'devui-notification-image': true, + [`devui-notification-image-${type.value}`]: true, + })); + const severityIconMap = { + info: 'info-o', + success: 'right-o', + warning: 'warning-o', + error: 'error-o', + }; + + return () => {type.value !== 'normal' && }; + }, +}); diff --git a/packages/devui-vue/devui/notification/src/notification-service.tsx b/packages/devui-vue/devui/notification/src/notification-service.tsx new file mode 100644 index 0000000000..4f6cf88826 --- /dev/null +++ b/packages/devui-vue/devui/notification/src/notification-service.tsx @@ -0,0 +1,60 @@ +import { reactive, createApp, onUnmounted } from 'vue'; +import type { App } from 'vue'; +import { NotificationOption, VoidFn } from './notification-types'; +import Notification from './notification'; + +const defaultOptions: NotificationOption = { + modelValue: false, + duration: 3000, + type: 'normal', +}; + +function initInstance(props: NotificationOption, content: string): App { + const container = document.createElement('div'); + const app: App = createApp({ + setup() { + onUnmounted(() => { + document.body.removeChild(container); + }); + + return () => ( + + {content} + + ); + }, + }); + document.body.appendChild(container); + app.mount(container); + return app; +} + +function close(props: NotificationOption, originOnClose: VoidFn | null): void { + props.modelValue = false; + originOnClose?.(); +} + +export default class NotificationService { + static open(options: NotificationOption): void { + const originOnClose: VoidFn | null = options.onClose || null; + const content = options.content; + let timer; + delete options.content; + + const props: NotificationOption = reactive({ + ...defaultOptions, + ...options, + onClose: () => { + close(props, originOnClose); + }, + }); + + initInstance(props, content); + props.modelValue = true; + + clearTimeout(timer); + if (options.duration) { + timer = setTimeout(props.onClose, options.duration); + } + } +} diff --git a/packages/devui-vue/devui/notification/src/notification-types.ts b/packages/devui-vue/devui/notification/src/notification-types.ts new file mode 100644 index 0000000000..ada3a3a31f --- /dev/null +++ b/packages/devui-vue/devui/notification/src/notification-types.ts @@ -0,0 +1,40 @@ +import type { ExtractPropTypes, PropType, h } from 'vue'; + +export type NotificationType = 'normal' | 'success' | 'error' | 'warning' | 'info'; + +export interface Message { + type?: NotificationType; + title?: string; + content?: string | ((message: Message) => ReturnType); + duration?: number; +} + +export const notificationProps = { + modelValue: { + type: Boolean, + default: false, + }, + title: { + type: String, + default: '', + }, + type: { + type: String as PropType, + default: 'normal', + }, + duration: { + type: Number, + default: 3000, + }, + onClose: { + type: Function as PropType<() => void>, + }, +}; + +export type EmitEventFn = (event: 'update:modelValue' | 'destroy', result?: unknown) => void; + +export type VoidFn = () => void; + +export type NotificationProps = ExtractPropTypes; + +export type NotificationOption = Partial & { content?: string }; diff --git a/packages/devui-vue/devui/toast/src/toast.scss b/packages/devui-vue/devui/notification/src/notification.scss similarity index 56% rename from packages/devui-vue/devui/toast/src/toast.scss rename to packages/devui-vue/devui/notification/src/notification.scss index 081257afca..dbea8ca23e 100644 --- a/packages/devui-vue/devui/toast/src/toast.scss +++ b/packages/devui-vue/devui/notification/src/notification.scss @@ -1,17 +1,13 @@ -@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 { +@import '../../styles-var/devui-var.scss'; + +.devui-notification { position: fixed; top: 50px; right: 20px; width: 20em; word-break: normal; word-wrap: break-word; + z-index: 1060; a { &:link, @@ -26,35 +22,24 @@ } } -.devui-toast-item-container { +.devui-notification-item-container { position: relative; - transform: translateX(100%); - margin: 0 0 10px 0; + margin: 0 0 8px 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-out; background-color: $devui-feedback-overlay-bg; - - &.slide-in { - transform: translateX(0); - } } -.devui-toast-item { +.devui-notification-item { position: relative; display: block; padding: 12px 16px; } -.devui-toast-item p { - padding: 0; - margin: 0; -} - -.devui-toast-icon-close { +.devui-notification-icon-close { position: absolute; top: 7px; right: 10px; @@ -65,14 +50,14 @@ } } -.devui-toast-title { +.devui-notification-title { font-size: $devui-font-size-card-title; padding: 0 0 calc(0.5em - 2px) 0; display: block; font-weight: 700; } -.devui-toast-image { +.devui-notification-image { position: absolute; display: inline-block; width: 16px; @@ -83,46 +68,55 @@ padding: 0; line-height: 1; - &.devui-toast-image-warn i.icon { + &.devui-notification-image-warn i.icon { color: $devui-warning !important; } - &.devui-toast-image-info i.icon { + &.devui-notification-image-info i.icon { color: $devui-info !important; } - &.devui-toast-image-error i.icon { + &.devui-notification-image-error i.icon { color: $devui-danger !important; } - &.devui-toast-image-success i.icon { + &.devui-notification-image-success i.icon { color: $devui-success !important; } - .devui-toast-image-info-path, - .devui-toast-image-error-path, - .devui-toast-image-success-path { + .devui-notification-image-info-path, + .devui-notification-image-error-path, + .devui-notification-image-success-path { fill: $devui-light-text; } } -.devui-toast-message { +.devui-notification-message { margin-left: 20px; - p { - padding: 0 8px 0 4px; - } - - span.devui-toast-title + p { - padding: 0; + .devui-notification-content { + font-size: $devui-font-size; + margin-top: 4px; } } -.devui-toast-message-common .devui-toast-message { +.devui-notification-message-common .devui-notification-message { margin-left: 0; } -.devui-toast-message p { +.devui-notification-message p { font-size: $devui-font-size; - margin-top: 2px; + margin-top: 4px; +} + +.notification-fade { + &-enter-active, + &-leave-active { + transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out; + } + + &-enter-from, + &-leave-to { + transform: translateX(100%); + } } diff --git a/packages/devui-vue/devui/notification/src/notification.tsx b/packages/devui-vue/devui/notification/src/notification.tsx new file mode 100644 index 0000000000..e9c96a9708 --- /dev/null +++ b/packages/devui-vue/devui/notification/src/notification.tsx @@ -0,0 +1,36 @@ +import { defineComponent, toRefs, Transition } from 'vue'; +import { notificationProps, NotificationProps } from './notification-types'; +import Close from './notification-icon-close'; +import TypeIcon from './notification-image'; +import { useNotification, useEvent } from './use-notification'; +import './notification.scss'; + +export default defineComponent({ + name: 'DNotification', + props: notificationProps, + emits: ['update:modelValue', 'destroy'], + setup(props: NotificationProps, { emit, slots }) { + const { modelValue, title, type } = toRefs(props); + const { classes } = useNotification(props); + const { interrupt, removeReset, close, handleDestroy } = useEvent(props, emit); + + return () => ( + + {modelValue.value && ( +
+
+
+ + {title.value && } +
+ {title.value} + {slots.default?.()} +
+
+
+
+ )} +
+ ); + }, +}); diff --git a/packages/devui-vue/devui/notification/src/use-notification.ts b/packages/devui-vue/devui/notification/src/use-notification.ts new file mode 100644 index 0000000000..238ad1893f --- /dev/null +++ b/packages/devui-vue/devui/notification/src/use-notification.ts @@ -0,0 +1,59 @@ +import { computed, watch } from 'vue'; +import type { ComputedRef } from 'vue'; +import { NotificationProps, EmitEventFn, VoidFn } from './notification-types'; + +export function useNotification(props: NotificationProps): { classes: ComputedRef> } { + const classes = computed(() => ({ + 'devui-notification-item-container': true, + [`devui-notification-message-${props.type}`]: true, + })); + + return { classes }; +} + +export function useEvent( + props: NotificationProps, + emit: EmitEventFn +): { interrupt: VoidFn; removeReset: VoidFn; close: VoidFn; handleDestroy: VoidFn } { + let timer = null; + let timestamp: number; + + const close = () => { + clearTimeout(timer); + timer = null; + props.onClose?.(); + emit('update:modelValue', false); + }; + + const interrupt = () => { + if (timer) { + clearTimeout(timer); + timer = null; + } + }; + + const removeReset = () => { + if (!props.modelValue) { + const remainTime = props.duration - (Date.now() - timestamp); + timer = setTimeout(close, remainTime); + } + }; + + const handleDestroy = () => { + emit('destroy'); + }; + + watch( + () => props.modelValue, + (val) => { + if (val) { + timestamp = Date.now(); + if (props.duration) { + timer = setTimeout(close, props.duration); + } + } + } + ); + + return { interrupt, removeReset, close, handleDestroy }; +} diff --git a/packages/devui-vue/devui/toast/__tests__/toast.spec.ts b/packages/devui-vue/devui/toast/__tests__/toast.spec.ts deleted file mode 100644 index 7d7f30f836..0000000000 --- a/packages/devui-vue/devui/toast/__tests__/toast.spec.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import Toast from '../src/toast'; - -describe('Toast', () => { - describe('toast basic', () => { - it('should create toast component correctly', () => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success' - } - ] - } - }); - expect(wrapper.find('.devui-toast').exists()).toBe(true); - }) - it('toast has summary', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success', - summary: 'Summary' - } - ] - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-title').text()).toBe('Summary'); - }) - it('toast has content', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success', - content: 'content' - } - ] - } - }) - await nextTick() - expect(wrapper.find('.devui-toast-message').text()).toBe('content'); - }) - it('toast has content of solt', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success', - content: 'slot:customTemplate', - info: 'info' - } as any - ] - }, - slots: { - customTemplate: (msg) => { - return `

${msg.info}

` - } - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-message').text()).toBe('

info

'); - }) - it('toast has detail', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success', - detail : 'detail' - } - ] - } - }); - await nextTick(); - expect(wrapper.find('.devui-toast-message').text()).toBe('detail'); - }) - }) - - describe('toast type', () => { - it('toast should be success', async () => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'success' - } - ] - } - }); - await nextTick(); - expect(wrapper.find('.devui-toast-message-success').exists()).toBe(true); - }); - it('toast should be common', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'common' - } - ] - } - }); - await nextTick(); - expect(wrapper.find('.devui-toast-message-common').exists()).toBe(true); - }) - it('toast should be info', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'info' - } - ] - } - }); - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(true); - }) - it('toast should be error', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'error' - } - ] - } - }); - await nextTick(); - expect(wrapper.find('.devui-toast-message-error').exists()).toBe(true); - }) - it('toast should be warning', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { - severity: 'warning' - } - ] - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-message-warning').exists()).toBe(true); - }) - }) - - describe('toast life and sticky', () => { - const value = [ - { - severity: 'success', - summary: 'summary' - } - ]; - beforeEach(() => { - jest.useFakeTimers(); - }) - it('has life, should close after 5000ms', async() => { - const wrapper = mount(Toast, { - props: { - value: value, - life: 5000 - } - }) - jest.advanceTimersToNextTimer(3000); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container')).toBeTruthy(); - jest.advanceTimersToNextTimer(2000); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container').exists()).toBeFalsy(); - }) - it('has life and sticky, not dismiss should close', async() => { - const wrapper = mount(Toast, { - props:{ - value: value, - life: 5000, - sticky: true - } - }) - jest.advanceTimersByTime(3000); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container').exists()).toBe(true); - jest.advanceTimersByTime(2000); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container').exists()).toBe(true); - await wrapper.find('.devui-toast-icon-close').trigger('click'); - jest.advanceTimersByTime(300); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container').exists()).toBe(false); - }) - }) - - describe('toast single and global life mode', () => { - beforeEach(() => { - jest.useFakeTimers(); - }) - it('dismiss by global life mode', async() => { - const wrapper = mount(Toast,{ - props: { - value: [ - { severity: 'success', life: 3000, summary: 'success' }, - { severity: 'info', life: 5000, summary: 'info'}, - { severity: 'error',summary: 'error' } - ], - lifeMode: 'global' - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-message-success')).toBeTruthy(); - expect(wrapper.find('.devui-toast-message-info')).toBeTruthy(); - expect(wrapper.find('.devui-toast-message-error')).toBeTruthy(); - jest.advanceTimersByTime(5300); - await nextTick(); - expect(wrapper.find('.devui-toast-message-success').exists()).toBeFalsy(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBeFalsy(); - expect(wrapper.find('.devui-toast-message-error').exists()).toBeFalsy(); - expect(wrapper.find('.devui-toast').text()).toBe(''); - }) - it('dismiss by singel life mode', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { life: 3000, severity: 'info', summary: 'info', detail: 'info content' }, - { life: 6000, severity: 'success', summary: 'success', detail: 'success content' } - ], - lifeMode: 'single' - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(true); - expect(wrapper.find('.devui-toast-message-success').exists()).toBe(true); - jest.advanceTimersByTime(3300); - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(false); - expect(wrapper.find('.devui-toast-message-success').exists()).toBe(true); - jest.advanceTimersByTime(3000); - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(false); - expect(wrapper.find('.devui-toast-message-success').exists()).toBe(false); - jest.runAllTimers(); - await nextTick(); - expect(wrapper.find('.devui-toast').text()).toBe(''); - }) - //TODO: mouseenter 没起作用 - describe('toast multiple', () => { - beforeEach(() => { - jest.useFakeTimers(); - }) - it('mouse over not dismiss, mouse out dismiss', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { severity: 'info', summary: 'info', detail: 'info content' }, - { severity: 'success', summary: 'success', detail: 'success content' } - ], - life: 5000 - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(true); - expect(wrapper.find('.devui-toast-message-success').exists()).toBe(true); - await wrapper.findAll('.devui-toast-item-container')[0].trigger('mouseenter'); - jest.advanceTimersByTime(5000); - await nextTick(); - expect(wrapper.find('.devui-toast-message-info').exists()).toBe(true); - await wrapper.find('.devui-toast-message-info').trigger('mouseleave'); - jest.runAllTimers(); - await nextTick(); - expect(wrapper.find('.devui-toast-item-container').exists()).toBe(false); - }) - }) - - describe('toast styleClass and style', () => { - it('add styleclass and style', async() => { - const wrapper = mount(Toast, { - props: { - value: [ - { severity: 'info', summary: 'info', detail: 'info content' } - ], - styleClass: 'myClass', - style: { - color: 'rgb(255, 255, 255)' - } - } - }) - await nextTick(); - expect(wrapper.find('.devui-toast').classes()).toContain('myClass'); - expect(wrapper.find('.devui-toast').attributes('style')).toBe('z-index: 1076; color: rgb(255, 255, 255);'); - }) - }) - }) -}) \ No newline at end of file diff --git a/packages/devui-vue/devui/toast/index.ts b/packages/devui-vue/devui/toast/index.ts deleted file mode 100644 index 705ee20d4a..0000000000 --- a/packages/devui-vue/devui/toast/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -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) -} - -export { Toast, ToastService } - -export default { - title: 'Toast 全局提示', - category: '反馈', - status: '100%', - install(app: App): void { - app.use(Toast as any) - app.config.globalProperties.$toastService = ToastService - } -} diff --git a/packages/devui-vue/devui/toast/src/hooks/use-toast-constant.ts b/packages/devui-vue/devui/toast/src/hooks/use-toast-constant.ts deleted file mode 100644 index af0627dec1..0000000000 --- a/packages/devui-vue/devui/toast/src/hooks/use-toast-constant.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/packages/devui-vue/devui/toast/src/hooks/use-toast-event.ts b/packages/devui-vue/devui/toast/src/hooks/use-toast-event.ts deleted file mode 100644 index 45c7f16fa9..0000000000 --- a/packages/devui-vue/devui/toast/src/hooks/use-toast-event.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getCurrentInstance } from 'vue' -import { Message } from '../toast-types' -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/packages/devui-vue/devui/toast/src/hooks/use-toast-helper.ts b/packages/devui-vue/devui/toast/src/hooks/use-toast-helper.ts deleted file mode 100644 index a6cd71720b..0000000000 --- a/packages/devui-vue/devui/toast/src/hooks/use-toast-helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Message } from '../toast-types' - -export function useToastHelper() { - function severityDelay(msg: Message) { - switch (msg.severity) { - case 'warning': - case 'error': - return 10e3 - default: - return 5e3 - } - } - - return { severityDelay } -} diff --git a/packages/devui-vue/devui/toast/src/hooks/use-toast-z-index.ts b/packages/devui-vue/devui/toast/src/hooks/use-toast-z-index.ts deleted file mode 100644 index 22e2519012..0000000000 --- a/packages/devui-vue/devui/toast/src/hooks/use-toast-z-index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export let toastZIndex = 1060 - -export function toastIncrease() { - toastZIndex++ -} diff --git a/packages/devui-vue/devui/toast/src/toast-icon-close.tsx b/packages/devui-vue/devui/toast/src/toast-icon-close.tsx deleted file mode 100644 index 6ded5f19fb..0000000000 --- a/packages/devui-vue/devui/toast/src/toast-icon-close.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineComponent, PropType } from 'vue' -import { Icon } 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/packages/devui-vue/devui/toast/src/toast-image.tsx b/packages/devui-vue/devui/toast/src/toast-image.tsx deleted file mode 100644 index 9bf97f2f39..0000000000 --- a/packages/devui-vue/devui/toast/src/toast-image.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { defineComponent, PropType } from 'vue' -import { IToastSeverity } from './toast-types' -import { Icon } 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', - warning: 'warning-o', - error: 'error-o' - } - - const showIcon = () => severity !== 'common' - - return {showIcon() ? : null} - } -}) diff --git a/packages/devui-vue/devui/toast/src/toast-service.ts b/packages/devui-vue/devui/toast/src/toast-service.ts deleted file mode 100644 index bb72303cc2..0000000000 --- a/packages/devui-vue/devui/toast/src/toast-service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createApp, onUnmounted } from 'vue' -import { ToastProps } from './toast-types' -import Toast from './toast' - -function createToastApp(props: Record) { - return createApp(Toast, 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/packages/devui-vue/devui/toast/src/toast-types.ts b/packages/devui-vue/devui/toast/src/toast-types.ts deleted file mode 100644 index c77e05dcff..0000000000 --- a/packages/devui-vue/devui/toast/src/toast-types.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { CSSProperties, ExtractPropTypes, PropType, h } from 'vue' - -export type IToastLifeMode = 'single' | 'global' -export type IToastSeverity = 'common' | 'success' | 'error' | 'warning' | '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/packages/devui-vue/devui/toast/src/toast.tsx b/packages/devui-vue/devui/toast/src/toast.tsx deleted file mode 100644 index c6600dd7b4..0000000000 --- a/packages/devui-vue/devui/toast/src/toast.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import './toast.scss' - -import { computed, defineComponent, nextTick, onUnmounted, ref, watch } from 'vue' -import { Message, ToastProps, toastProps } from './toast-types' -import ToastIconClose from './toast-icon-close' -import ToastImage from './toast-image' -import { cloneDeep, isEqual, merge, omit, throttle } from 'lodash' -import { useToastEvent } from './hooks/use-toast-event' -import { useToastHelper } from './hooks/use-toast-helper' -import { useToastConstant } from './hooks/use-toast-constant' -import { toastZIndex, toastIncrease } from './hooks/use-toast-z-index' - -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 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() { - toastIncrease() - - 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), msg.life || severityDelay(msg)) - }) - }) - } else { - timeout = setTimeout(() => removeAll(), defaultLife.value) - } - } - - function singleModeRemove(msg: Message) { - removeMsgAnimation(msg) - setTimeout(() => { - onCloseEvent(msg) - - if (hasMsgAnimation()) { - // avoid index confusion in settimeout - const index = messages.value.indexOf(msg) - if (index !== -1) { - messages.value.splice(index, 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!), 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, - containerRef, - msgItemRefs, - interrupt, - removeReset, - removeThrottle, - close, - msgItemRef - } - }, - render() { - const { - style: extraStyle, - styleClass: extraClass, - messages, - msgAnimations, - msgItemRefs, - life, - interrupt, - removeReset, - removeThrottle, - $attrs, - $slots - } = this - - const prefixCls = 'devui-toast' - - const wrapperStyles = [`z-index: ${toastZIndex}`, 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/packages/devui-vue/devui/upload/src/upload.tsx b/packages/devui-vue/devui/upload/src/upload.tsx index 3cea263bf3..e0315671be 100644 --- a/packages/devui-vue/devui/upload/src/upload.tsx +++ b/packages/devui-vue/devui/upload/src/upload.tsx @@ -1,16 +1,11 @@ -import { defineComponent, toRefs, ref } from 'vue' -import { ToastService } from '../../toast' -import { UploadStatus, UploadProps, uploadProps } from './upload-types' -import { useSelectFiles } from './use-select-files' -import { useUpload } from './use-upload' -import { - getFailedFilesCount, - getSelectedFilesCount, - getUploadingFilesCount, - getExistSameNameFilesMsg -} from './i18n-upload' -import { FileUploader } from './file-uploader' -import './upload.scss' +import { defineComponent, toRefs, ref } from 'vue'; +import { NotificationService } from '../../notification'; +import { UploadStatus, UploadProps, uploadProps } from './upload-types'; +import { useSelectFiles } from './use-select-files'; +import { useUpload } from './use-upload'; +import { getFailedFilesCount, getSelectedFilesCount, getUploadingFilesCount, getExistSameNameFilesMsg } from './i18n-upload'; +import { FileUploader } from './file-uploader'; +import './upload.scss'; export default defineComponent({ name: 'DUpload', @@ -30,202 +25,190 @@ export default defineComponent({ uploadedFiles, multiple, accept, - webkitdirectory - } = toRefs(props) - const { triggerSelectFiles, _validateFiles, triggerDropFiles, checkAllFilesSize } = - useSelectFiles() - const { - fileUploaders, - addFile, - getFullFiles, - deleteFile, - upload, - resetSameNameFiles, - removeFiles, - _oneTimeUpload, - getSameNameFiles - } = useUpload() - const isDropOVer = ref(false) - const uploadTips = ref('') + webkitdirectory, + } = toRefs(props); + const { triggerSelectFiles, _validateFiles, triggerDropFiles, checkAllFilesSize } = useSelectFiles(); + const { fileUploaders, addFile, getFullFiles, deleteFile, upload, resetSameNameFiles, removeFiles, _oneTimeUpload, getSameNameFiles } = + useUpload(); + const isDropOVer = ref(false); + const uploadTips = ref(''); const alertMsg = (errorMsg: string) => { - ToastService.open({ - value: [{ severity: 'warn', content: errorMsg }] - }) - } + NotificationService.open({ + type: 'warning', + content: errorMsg, + //value: [{ severity: 'warn', content: errorMsg }] + }); + }; const checkValid = () => { - let totalFileSize = 0 + let totalFileSize = 0; fileUploaders.value.forEach((fileUploader) => { - totalFileSize += fileUploader.file.size + totalFileSize += fileUploader.file.size; - const checkResult = _validateFiles( - fileUploader.file, - accept.value, - fileUploader.uploadOptions - ) + const checkResult = _validateFiles(fileUploader.file, accept.value, fileUploader.uploadOptions); if (checkResult && checkResult.checkError) { - deleteFile(fileUploader.file) - alertMsg(checkResult.errorMsg) - return + deleteFile(fileUploader.file); + alertMsg(checkResult.errorMsg); + return; } - }) + }); if (oneTimeUpload.value) { - const checkResult = checkAllFilesSize(totalFileSize, uploadOptions.value.maximumSize) + const checkResult = checkAllFilesSize(totalFileSize, uploadOptions.value.maximumSize); if (checkResult && checkResult.checkError) { - removeFiles() - alertMsg(checkResult.errorMsg) + removeFiles(); + alertMsg(checkResult.errorMsg); } } - } + }; const _dealFiles = (promise: Promise) => { - resetSameNameFiles() + resetSameNameFiles(); promise .then((files) => { files.forEach((file) => { // 单文件上传前先清空数组 if (!multiple.value) { - removeFiles() + removeFiles(); } - addFile(file, uploadOptions.value) + addFile(file, uploadOptions.value); // debounceTime(100) - }) - checkValid() - const sameNameFiles = getSameNameFiles() + }); + checkValid(); + const sameNameFiles = getSameNameFiles(); if (uploadOptions.value.checkSameName && sameNameFiles.length) { - alertMsg(getExistSameNameFilesMsg(sameNameFiles)) + alertMsg(getExistSameNameFilesMsg(sameNameFiles)); } const selectedFiles = fileUploaders.value .filter((fileUploader) => fileUploader.status === UploadStatus.preLoad) - .map((fileUploader) => fileUploader.file) - ctx.emit('fileSelect', selectedFiles) + .map((fileUploader) => fileUploader.file); + ctx.emit('fileSelect', selectedFiles); if (autoUpload.value) { - fileUpload() + fileUpload(); } }) .catch((error: Error) => { - alertMsg(error.message) - }) - } + alertMsg(error.message); + }); + }; const handleClick = () => { if (disabled.value) { - return + return; } _dealFiles( triggerSelectFiles({ accept: accept.value, multiple: multiple.value, - webkitdirectory: webkitdirectory.value + webkitdirectory: webkitdirectory.value, }) - ) - } + ); + }; const onFileDrop = (files: File[]) => { - isDropOVer.value = false - _dealFiles(triggerDropFiles(files)) - ctx.emit('fileDrop', files) - } + isDropOVer.value = false; + _dealFiles(triggerDropFiles(files)); + ctx.emit('fileDrop', files); + }; const onFileOver = (event: boolean) => { - isDropOVer.value = event - ctx.emit('fileOver', event) - } + isDropOVer.value = event; + ctx.emit('fileOver', event); + }; // 删除已上传文件 const deleteUploadedFile = (file: File) => { const newUploadedFiles = uploadedFiles.value.filter((uploadedFile) => { - return uploadedFile.name !== file.name - }) - ctx.emit('deleteUploadedFile', file) - ctx.emit('update:uploadedFiles', newUploadedFiles) - } + return uploadedFile.name !== file.name; + }); + ctx.emit('deleteUploadedFile', file); + ctx.emit('update:uploadedFiles', newUploadedFiles); + }; const onDeleteFile = (event: Event, file: File, status: UploadStatus) => { - event.stopPropagation() + event.stopPropagation(); if (status === UploadStatus.uploaded) { - deleteUploadedFile(file) + deleteUploadedFile(file); } - deleteFile(file) - } + deleteFile(file); + }; const canUpload = () => { - let uploadResult = Promise.resolve(true) + let uploadResult = Promise.resolve(true); if (beforeUpload.value) { - const result: any = beforeUpload.value(getFullFiles()) + const result: any = beforeUpload.value(getFullFiles()); if (typeof result !== 'undefined') { if (result.then) { - uploadResult = result + uploadResult = result; } else { - uploadResult = Promise.resolve(result) + uploadResult = Promise.resolve(result); } } } - return uploadResult - } + return uploadResult; + }; const fileUpload = (event: Event, fileUploader?: FileUploader) => { if (event) { - event.stopPropagation() + event.stopPropagation(); } canUpload().then((_canUpload) => { if (!_canUpload) { - removeFiles() - return + removeFiles(); + return; } - const uploadObservable = oneTimeUpload.value ? _oneTimeUpload() : upload(fileUploader) + const uploadObservable = oneTimeUpload.value ? _oneTimeUpload() : upload(fileUploader); uploadObservable - .then((results: Array<{ file: File; response: any; }>) => { - props['on-success'] && props['on-success'](results) - const newFiles = results.map((result) => result.file) - const newUploadedFiles = [...newFiles, ...uploadedFiles.value] - ctx.emit('update:uploadedFiles', newUploadedFiles) + .then((results: Array<{ file: File; response: any }>) => { + props['on-success'] && props['on-success'](results); + const newFiles = results.map((result) => result.file); + const newUploadedFiles = [...newFiles, ...uploadedFiles.value]; + ctx.emit('update:uploadedFiles', newUploadedFiles); }) .catch((error) => { - props['on-error'] && props['on-error'](error) - }) - }) - } + props['on-error'] && props['on-error'](error); + }); + }); + }; const getStatus = () => { - let uploadingCount = 0 - let uploadedCount = 0 - let failedCount = 0 - const filesCount = fileUploaders.value.length + let uploadingCount = 0; + let uploadedCount = 0; + let failedCount = 0; + const filesCount = fileUploaders.value.length; fileUploaders.value.forEach((fileUploader) => { if (fileUploader.status === UploadStatus.uploading) { - uploadingCount++ + uploadingCount++; } else if (fileUploader.status === UploadStatus.uploaded) { - uploadedCount++ + uploadedCount++; } else if (fileUploader.status === UploadStatus.failed) { - failedCount++ + failedCount++; } - }) + }); if (failedCount > 0) { - uploadTips.value = getFailedFilesCount(failedCount) - return 'failed' + uploadTips.value = getFailedFilesCount(failedCount); + return 'failed'; } if (uploadingCount > 0) { - uploadTips.value = getUploadingFilesCount(uploadingCount, filesCount) - return 'uploading' + uploadTips.value = getUploadingFilesCount(uploadingCount, filesCount); + return 'uploading'; } if (uploadedCount === filesCount && uploadedCount !== 0) { - return 'uploaded' + return 'uploaded'; } if (filesCount !== 0) { - uploadTips.value = getSelectedFilesCount(filesCount) - return 'selected' + uploadTips.value = getSelectedFilesCount(filesCount); + return 'selected'; } - } + }; // 取消上传 const cancelUpload = () => { fileUploaders.value = fileUploaders.value.map((fileUploader) => { if (fileUploader.status === UploadStatus.uploading) { // 取消上传请求 - fileUploader.cancel() - fileUploader.status = UploadStatus.failed + fileUploader.cancel(); + fileUploader.status = UploadStatus.failed; } - return fileUploader - }) - } + return fileUploader; + }); + }; return { uploadOptions, @@ -247,8 +230,8 @@ export default defineComponent({ uploadTips, cancelUpload, deleteUploadedFile, - multiple - } + multiple, + }; }, render() { const { @@ -267,23 +250,20 @@ export default defineComponent({ fileUpload, uploadedFiles, deleteUploadedFile, - multiple - } = this + multiple, + } = this; return (
+ style={`border: ${isDropOVer ? '1px solid #15bf15' : '0'}`}> {this.$slots.default?.() ? (
{this.$slots.default()}
) : (
- {fileUploaders.length === 0 && ( -
{placeholderText}
- )} + {fileUploaders.length === 0 &&
{placeholderText}
} {fileUploaders.length > 0 && (
    {fileUploaders.map((fileUploader, index) => ( @@ -291,30 +271,18 @@ export default defineComponent({ key={index} class='devui-file-item devui-file-tag' style='display: inline-block; margin: 0 2px 2px 0' - title={fileUploader.file.name} - > - + title={fileUploader.file.name}> + {fileUploader.file.name} - onDeleteFile(event, fileUploader.file, fileUploader.status) - } + onClick={(event) => onDeleteFile(event, fileUploader.file, fileUploader.status)} /> {fileUploader.status === UploadStatus.uploading && (
    @@ -323,16 +291,11 @@ export default defineComponent({ percentage={fileUploader.percentage} barbgcolor='#50D4AB' strokeWidth={8} - showContent={false} - > + showContent={false}>
    )} - {fileUploader.status === UploadStatus.failed && ( - - )} - {fileUploader.status === UploadStatus.uploaded && ( - - )} + {fileUploader.status === UploadStatus.failed && } + {fileUploader.status === UploadStatus.uploaded && } ))}
@@ -343,12 +306,7 @@ export default defineComponent({
)} {!autoUpload && !withoutBtn && ( - + {uploadText} )} @@ -357,16 +315,16 @@ export default defineComponent({
{this.$slots.preloadFiles?.({ fileUploaders, - deleteFile: onDeleteFile + deleteFile: onDeleteFile, })}
{this.$slots.uploadedFiles?.({ uploadedFiles, - deleteFile: deleteUploadedFile + deleteFile: deleteUploadedFile, })}
- ) - } -}) + ); + }, +}); diff --git a/packages/devui-vue/docs/components/notification/index.md b/packages/devui-vue/docs/components/notification/index.md new file mode 100644 index 0000000000..5315fa491c --- /dev/null +++ b/packages/devui-vue/docs/components/notification/index.md @@ -0,0 +1,212 @@ +# Notification 全局通知 + +全局信息提示组件。 + +#### 何时使用 + +当需要向用户全局展示提示信息时使用,显示数秒后消失。 + +### 基本用法 + +:::demo 推荐使用服务方式调用,默认情况只展示消息内容和关闭按钮。 + +```vue + + + +``` + +::: + +### 消息标题 + +:::demo 通过`title`参数设置消息标题,默认为空,不显示标题。 + +```vue + + + +``` + +::: + +### 消息类型 + +:::demo 通过`type`参数设置消息类型,目前支持`normal`、`info`、`success`、`warning`、`danger`五种类型,默认`normal`类型,不显示类型图标。 + +```vue + + + +``` + +::: + +### 超时时间 + +:::demo 通过`duration`参数设置超时时间,单位`ms`,默认`3000 ms`后自动关闭,设置为`0`则不会自动关闭。 + +```vue + + + +``` + +::: + +### 关闭回调 + +:::demo 通过`onClose`参数设置消息关闭时的回调。 + +```vue + + + +``` + +::: + +### 组件方式调用 + +:::demo 除服务方式外,还提供组件方式调用,组件方式的默认插槽与服务方式的`content`参数作用一致,其他参数与服务方式保持同名。 + +```vue + + + +``` + +::: + +### Service 使用 + +```typescript +// 方式1,局部引入 NotificationService +import { NotificationService } from '@devui/vue-devui/notification'; +NotificationService.open({ xxx }); + +// 方式2,全局属性 +this.$notificationService.open({ xxx }); +``` + +### d-notification 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 | +| -------- | ------------------ | -------- | -------------------------- | ----------------------------- | +| v-model | `boolean` | 'false' | 组件调用必选,控制是否显示 | [组件方式调用](#组件方式调用) | +| content | `string` | '' | 可选,设置消息内容 | [基本用法](#基本用法) | +| title | `string` | '' | 可选,设置消息标题 | [消息标题](#消息标题) | +| type | `NotificationType` | 'normal' | 可选,设置消息类型 | [消息类型](#消息类型) | +| duration | `number` | '3000' | 可选,设置超时时间 | [超时时间](#超时时间) | +| on-close | `() => void` | '' | 可选,设置消息关闭时的回调 | [关闭回调](#关闭回调) | + +### d-notification 插槽 + +| 名称 | 说明 | +| ------- | ---------------------------- | +| default | 默认插槽,组件方式使用时有效 | diff --git a/packages/devui-vue/docs/components/toast/index.md b/packages/devui-vue/docs/components/toast/index.md deleted file mode 100644 index 79f858e151..0000000000 --- a/packages/devui-vue/docs/components/toast/index.md +++ /dev/null @@ -1,502 +0,0 @@ -# Toast 全局通知 - -全局信息提示组件。 - -### 何时使用 - -当需要向用户全局展示提示信息时使用,显示数秒后消失。 - -### 基本用法 - -common 时不展示图标。 -:::demo - -```vue - - - -``` - -::: - -### 超时时间 - -当设置超时时间、没有标题时,则不展示标题和关闭按钮。 -:::demo - -```vue - - - -``` - -::: - -### 自定义样式 - -:::demo - -```vue - - - -``` - -::: - -### 每个消息使用单独的超时时间 - -当设置超时时间模式为 single 时,每个消息使用自身的 life 作为超时时间,如果未设置则按 severity 判断,severity 也未设置时默认超时时间为 5000 毫秒。 - -:::demo - -```vue - - -``` - -::: - -### 服务方式调用 - -使用服务的方式创建 toast 全局通知。 - -:::demo - -```vue - - - -``` - -::: - -### Toast Api - -| 参数 | 类型 | 默认 | 说明 | 跳转 | -| :--------- | :--------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | -| value | `Array` | -- | 必选,消息内容数组,Message 对象定义见下文 | [基本用法](#基本用法) | -| life | `number` | 5000 | 可选,超时时间,超时后自动消失,鼠标悬停可以阻止消失,单位毫秒。普通、成功、提示类默认为 5000 毫秒,错误、警告类默认为 10000 毫秒 | [超时时间](#超时时间) | -| lifeMode | `string` | global | 可选,超时时间模式,预设值为 global 和 single 。默认为 global,所有消息使用 life 或群组第一个消息的预设超时时间; 设置为 single 时, 每个消息使用自身的超时时间,参见 Message 中的 life 定义 | [每个消息使用单独的超时时间](#每个消息使用单独的超时时间) | -| sticky | `boolean` | false | 可选,是否常驻,默认自动关闭 | | -| style | `object` | - | 可选,样式 | [自定义样式](#自定义样式) | -| styleClass | `string` | - | 可选,类名 | [自定义样式](#自定义样式) | - -### Toast Event - -| 参数 | 类型 | 说明 | 跳转 | -| :---------- | :------------------------------ | :--------------------- | :---------------------------- | -| closeEvent | `(message: Message) => void` | 消息关闭回调 | [服务方式调用](#服务方式调用) | -| valueChange | `(messages: Message[]) => void` | 消息关闭后剩余消息回调 | [服务方式调用](#服务方式调用) | - -### 接口 & 类型定义 - -```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 }) -```