refactor: message

refactor-notification
tangjinzhou 2021-12-30 10:57:15 +08:00
parent f6760bff27
commit f92e75a1d9
10 changed files with 249 additions and 101 deletions

View File

@ -0,0 +1,46 @@
<docs>
---
order: 6
title:
zh-CN: 自定义样式
en-US: Customized style
---
## zh-CN
使用 `style` `class` 来定义样式
## en-US
The `style` and `class` are available to customize Message.
</docs>
<template>
<a-button @click="success">Customized style</a-button>
</template>
<script lang="ts">
import { message } from 'ant-design-vue';
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
const success = () => {
message.success({
content: () => 'This is a prompt message with custom className and style',
class: 'custom-class',
style: {
marginTop: '20vh',
},
});
};
return {
success,
};
},
});
</script>
<style>
.custom-class {
color: red;
}
</style>

View File

@ -6,6 +6,7 @@
<loading /> <loading />
<thenable /> <thenable />
<update /> <update />
<customStyleVue />
</demo-sort> </demo-sort>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -15,6 +16,7 @@ import Duration from './duration.vue';
import Loading from './loading.vue'; import Loading from './loading.vue';
import Thenable from './thenable.vue'; import Thenable from './thenable.vue';
import Update from './update.vue'; import Update from './update.vue';
import customStyleVue from './custom-style.vue';
import CN from '../index.zh-CN.md'; import CN from '../index.zh-CN.md';
import US from '../index.en-US.md'; import US from '../index.en-US.md';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -28,6 +30,7 @@ export default defineComponent({
Loading, Loading,
Thenable, Thenable,
Update, Update,
customStyleVue,
}, },
setup() { setup() {
return {}; return {};

View File

@ -8,20 +8,25 @@ title:
## zh-CN ## zh-CN
可以通过唯一的 `key` 来更新内容 可以通过唯一的 `key` 来更新内容或者响应式数据
## en-US ## en-US
Update message content with unique `key`. Update message content with unique `key`or use reactive data.
</docs> </docs>
<template> <template>
<a-button type="primary" @click="openMessage">Open the message box</a-button> <a-button type="primary" @click="openMessage">Open the message box (update by key)</a-button>
<br />
<br />
<a-button type="primary" @click="openMessage2">
Open the message box (update by reactive)
</a-button>
</template> </template>
<script lang="ts"> <script lang="ts">
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { defineComponent } from 'vue'; import { defineComponent, ref } from 'vue';
const key = 'updatable'; const key = 'updatable';
export default defineComponent({ export default defineComponent({
setup() { setup() {
@ -31,8 +36,17 @@ export default defineComponent({
message.success({ content: 'Loaded!', key, duration: 2 }); message.success({ content: 'Loaded!', key, duration: 2 });
}, 1000); }, 1000);
}; };
const content = ref('Loading...');
const openMessage2 = () => {
// content must use function
message.loading({ content: () => content.value });
setTimeout(() => {
content.value = 'Loaded!';
}, 1000);
};
return { return {
openMessage, openMessage,
openMessage2,
}; };
}, },
}); });

View File

@ -25,7 +25,7 @@ This components provides some static methods, with usage and arguments as follow
| Argument | Description | Type | Default | | Argument | Description | Type | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| content | content of the message | string\| VNode | - | | content | content of the message | string\| VNode \| () => VNode | - |
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 | | duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 |
| onClose | Specify a function that will be called when the message is closed | Function | - | | onClose | Specify a function that will be called when the message is closed | Function | - |
@ -48,11 +48,15 @@ The properties of config are as follows:
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| content | content of the message | string\| VNode | - | | | class | Customized CSS class | string | - |
| content | content of the message | string\| VNode \| () => VNode | - | |
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | | | duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | |
| onClose | Specify a function that will be called when the message is closed | function | - | | | onClose | Specify a function that will be called when the message is closed | function | - | |
| icon | Customized Icon | VNode | - | | | icon | Customized Icon | VNode \| ()=> VNode | - | |
| key | The unique identifier of the Message | string\|number | - | | | key | The unique identifier of the Message | string\|number | - | |
| style | Customized inline style | CSSProperties | - | |
| onClick | Specify a function that will be called when the message is clicked | function | - | |
| onClose | Specify a function that will be called when the message is closed | function | - | |
### Global static methods ### Global static methods
@ -68,12 +72,16 @@ message.config({
top: '100px', top: '100px',
duration: 2, duration: 2,
maxCount: 3, maxCount: 3,
rtl: true,
prefixCls: 'my-message',
}); });
``` ```
| Argument | Description | Type | Default | | Argument | Description | Type | Default | Version |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| duration | time before auto-dismiss, in seconds | number | 1.5 | | duration | time before auto-dismiss, in seconds | number | 1.5 | |
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | | getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | |
| maxCount | max message show, drop oldest if exceed limit | number | - | | maxCount | max message show, drop oldest if exceed limit | number | - | |
| top | distance from top | string | `24px` | | prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
| top | distance from top | string | `8px` |

View File

@ -5,18 +5,65 @@ import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFill
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled'; import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled';
import type { VueNode } from '../_util/type'; import type { Key, VueNode } from '../_util/type';
import type { NotificationInstance } from '../vc-notification/Notification';
import classNames from '../_util/classNames';
let defaultDuration = 3; let defaultDuration = 3;
let defaultTop: string; let defaultTop: string;
let messageInstance: any; let messageInstance: NotificationInstance;
let key = 1; let key = 1;
let localPrefixCls = ''; let localPrefixCls = '';
let transitionName = 'move-up'; let transitionName = 'move-up';
let hasTransitionName = false;
let getContainer = () => document.body; let getContainer = () => document.body;
let maxCount: number; let maxCount: number;
let rtl = false;
function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void) { export function getKeyThenIncreaseKey() {
return key++;
}
export interface ConfigOptions {
top?: string;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
rtl?: boolean;
}
function setMessageConfig(options: ConfigOptions) {
if (options.top !== undefined) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
localPrefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
hasTransitionName = true;
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
if (options.rtl !== undefined) {
rtl = options.rtl;
}
}
function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationInstance) => void) {
if (messageInstance) { if (messageInstance) {
callback(messageInstance); callback(messageInstance);
return; return;
@ -27,6 +74,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void)
prefixCls: args.prefixCls || localPrefixCls, prefixCls: args.prefixCls || localPrefixCls,
rootPrefixCls: args.rootPrefixCls, rootPrefixCls: args.rootPrefixCls,
transitionName, transitionName,
hasTransitionName,
style: { top: defaultTop }, // style: { top: defaultTop }, //
getContainer, getContainer,
maxCount, maxCount,
@ -49,7 +97,7 @@ export interface ThenableArgument {
(val: any): void; (val: any): void;
} }
const iconMap = { const typeToIcon = {
info: InfoCircleFilled, info: InfoCircleFilled,
success: CheckCircleFilled, success: CheckCircleFilled,
error: CloseCircleFilled, error: CloseCircleFilled,
@ -57,16 +105,14 @@ const iconMap = {
loading: LoadingOutlined, loading: LoadingOutlined,
}; };
export interface MessageType { export interface MessageType extends PromiseLike<any> {
(): void; (): void;
then: (fill: ThenableArgument, reject: ThenableArgument) => Promise<void>;
promise: Promise<void>;
} }
export interface MessageArgsProps { export interface MessageArgsProps {
content: string | (() => VueNode) | VueNode; content: string | (() => VueNode) | VueNode;
duration: number | null; duration?: number;
type: NoticeType; type?: NoticeType;
prefixCls?: string; prefixCls?: string;
rootPrefixCls?: string; rootPrefixCls?: string;
onClose?: () => void; onClose?: () => void;
@ -75,12 +121,13 @@ export interface MessageArgsProps {
style?: CSSProperties; style?: CSSProperties;
class?: string; class?: string;
appContext?: any; appContext?: any;
onClick?: (e: MouseEvent) => void;
} }
function notice(args: MessageArgsProps): MessageType { function notice(args: MessageArgsProps): MessageType {
const duration = args.duration !== undefined ? args.duration : defaultDuration; const duration = args.duration !== undefined ? args.duration : defaultDuration;
const target = args.key || key++; const target = args.key || getKeyThenIncreaseKey();
const closePromise = new Promise(resolve => { const closePromise = new Promise(resolve => {
const callback = () => { const callback = () => {
if (typeof args.onClose === 'function') { if (typeof args.onClose === 'function') {
@ -95,18 +142,21 @@ function notice(args: MessageArgsProps): MessageType {
style: args.style || {}, style: args.style || {},
class: args.class, class: args.class,
content: ({ prefixCls }) => { content: ({ prefixCls }) => {
const Icon = iconMap[args.type]; const Icon = typeToIcon[args.type];
const iconNode = Icon ? <Icon /> : ''; const iconNode = Icon ? <Icon /> : '';
const messageClass = classNames(`${prefixCls}-custom-content`, {
[`${prefixCls}-${args.type}`]: args.type,
[`${prefixCls}-rtl`]: rtl === true,
});
return ( return (
<div <div class={messageClass}>
class={`${prefixCls}-custom-content${args.type ? ` ${prefixCls}-${args.type}` : ''}`} {typeof args.icon === 'function' ? args.icon() : args.icon || iconNode}
>
{typeof args.icon === 'function' ? args.icon : args.icon || iconNode}
<span>{typeof args.content === 'function' ? args.content() : args.content}</span> <span>{typeof args.content === 'function' ? args.content() : args.content}</span>
</div> </div>
); );
}, },
onClose: callback, onClose: callback,
onClick: args.onClick,
}); });
}); });
}); });
@ -121,7 +171,7 @@ function notice(args: MessageArgsProps): MessageType {
return result; return result;
} }
type ConfigDuration = number | (() => void); type ConfigDuration = number;
type JointContent = VueNode | MessageArgsProps; type JointContent = VueNode | MessageArgsProps;
export type ConfigOnClose = () => void; export type ConfigOnClose = () => void;
@ -132,73 +182,64 @@ function isArgsProps(content: JointContent): content is MessageArgsProps {
); );
} }
export interface ConfigOptions {
top?: string;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
}
const api: any = { const api: any = {
open: notice, open: notice,
config(options: ConfigOptions) { config: setMessageConfig,
if (options.top !== undefined) { destroy(messageKey?: Key) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
localPrefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
},
destroy() {
if (messageInstance) { if (messageInstance) {
messageInstance.destroy(); if (messageKey) {
const { removeNotice } = messageInstance;
removeNotice(messageKey);
} else {
const { destroy } = messageInstance;
destroy();
messageInstance = null; messageInstance = null;
} }
}
}, },
}; };
['success', 'info', 'warning', 'error', 'loading'].forEach(type => { export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
api[type] = (content: JointContent, duration: ConfigDuration, onClose?: ConfigOnClose) => { originalApi[type] = (
content: JointContent,
duration?: ConfigDuration,
onClose?: ConfigOnClose,
) => {
if (isArgsProps(content)) { if (isArgsProps(content)) {
return api.open({ ...content, type }); return originalApi.open({ ...content, type });
} }
if (typeof duration === 'function') { if (typeof duration === 'function') {
onClose = duration; onClose = duration;
duration = undefined; duration = undefined;
} }
return api.open({ content, duration, type, onClose });
return originalApi.open({ content, duration, type, onClose });
}; };
}); }
(['success', 'info', 'warning', 'error', 'loading'] as NoticeType[]).forEach(type =>
attachTypeApi(api, type),
);
api.warn = api.warning; api.warn = api.warning;
export interface MessageApi { export interface MessageInstance {
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
open(args: MessageArgsProps): MessageType; open(args: MessageArgsProps): MessageType;
config(options: ConfigOptions): void;
destroy(): void;
} }
export interface MessageApi extends MessageInstance {
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
config(options: ConfigOptions): void;
destroy(messageKey?: Key): void;
}
/** @private test Only function. Not work on production */
export const getInstance = () => (process.env.NODE_ENV === 'test' ? messageInstance : null);
export default api as MessageApi; export default api as MessageApi;

View File

@ -25,8 +25,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
- `message.loading(content, [duration], onClose)` - `message.loading(content, [duration], onClose)`
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
| -------- | --------------------------------------------- | -------------- | ------ | | --- | --- | --- | --- |
| content | 提示内容 | string\| VNode | - | | content | 提示内容 | string\| VNode \| () => VNode | - |
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | | duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 |
| onClose | 关闭时触发的回调函数 | Function | - | | onClose | 关闭时触发的回调函数 | Function | - |
@ -47,13 +47,19 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
- `message.warn(config)` // alias of warning - `message.warn(config)` // alias of warning
- `message.loading(config)` - `message.loading(config)`
`config` 对象属性如下:
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| -------- | --------------------------------------------- | -------------- | ------ | ---- | | --- | --- | --- | --- | --- |
| content | 提示内容 | string\| VNode | - | | | class | 自定义 CSS class | string | - | |
| content | 提示内容 | string\| VNode \| ()=> VNode | - | |
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | | | duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | |
| onClose | 关闭时触发的回调函数 | Function | - | | | onClose | 关闭时触发的回调函数 | Function | - | |
| icon | 自定义图标 | VNode | - | | | icon | 自定义图标 | VNode \| () => VNode | - | |
| key | 当前提示的唯一标志 | string \| number | - | | | key | 当前提示的唯一标志 | string \| number | - | |
| style | 自定义内联样式 | CSSProperties | - | |
| onClick | 点击 message 时触发的回调函数 | function | - | |
| onClose | 关闭时触发的回调函数 | function | - | |
### 全局方法 ### 全局方法
@ -69,12 +75,16 @@ message.config({
top: `100px`, top: `100px`,
duration: 2, duration: 2,
maxCount: 3, maxCount: 3,
rtl: true,
prefixCls: 'my-message',
}); });
``` ```
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| duration | 默认自动关闭延时,单位秒 | number | 3 | | duration | 默认自动关闭延时,单位秒 | number | 3 | |
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | | getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | | maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
| top | 消息距离顶部的位置 | string | `24px` | | prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 |
| rtl | 是否开启 RTL 模式 | boolean | false | | |
| top | 消息距离顶部的位置 | string | `8px` | |

View File

@ -7,7 +7,7 @@
.reset-component(); .reset-component();
position: fixed; position: fixed;
top: 16px; top: 8px;
left: 0; left: 0;
z-index: @zindex-message; z-index: @zindex-message;
width: 100%; width: 100%;
@ -16,9 +16,6 @@
&-notice { &-notice {
padding: 8px; padding: 8px;
text-align: center; text-align: center;
&:first-child {
margin-top: -8px;
}
} }
&-notice-content { &-notice-content {
@ -54,8 +51,7 @@
font-size: @font-size-lg; font-size: @font-size-lg;
} }
&-notice.move-up-leave.move-up-leave-active { &-notice.@{ant-prefix}-move-up-leave.@{ant-prefix}-move-up-leave-active {
overflow: hidden;
animation-name: MessageMoveOut; animation-name: MessageMoveOut;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
@ -67,9 +63,12 @@
padding: 8px; padding: 8px;
opacity: 1; opacity: 1;
} }
100% { 100% {
max-height: 0; max-height: 0;
padding: 0; padding: 0;
opacity: 0; opacity: 0;
} }
} }
@import './rtl';

View File

@ -0,0 +1,17 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@message-prefix-cls: ~'@{ant-prefix}-message';
.@{message-prefix-cls}-rtl {
direction: rtl;
span {
direction: rtl;
}
.@{iconfont-css-prefix} {
margin-right: 0;
margin-left: 8px;
}
}

View File

@ -1,5 +1,5 @@
import { getTransitionGroupProps } from 'ant-design-vue/es/_util/transition'; import { getTransitionGroupProps } from '../_util/transition';
import type { Key } from 'ant-design-vue/es/_util/type'; import type { Key } from '../_util/type';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { import {
createVNode, createVNode,
@ -125,7 +125,7 @@ const Notification = defineComponent<NotificationProps>({
remove, remove,
}); });
return () => { return () => {
const { prefixCls, closeIcon = slots.closeIcon?.() } = props; const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
const noticeNodes = notices.value.map(({ notice, holderCallback }, index) => { const noticeNodes = notices.value.map(({ notice, holderCallback }, index) => {
const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined; const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined;
const { key, userPassKey } = notice; const { key, userPassKey } = notice;
@ -201,6 +201,8 @@ Notification.newInstance = function newNotificationInstance(properties, callback
appContext, appContext,
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
rootPrefixCls: customRootPrefixCls, rootPrefixCls: customRootPrefixCls,
transitionName: customTransitionName,
hasTransitionName,
...props ...props
} = properties || {}; } = properties || {};
const div = document.createElement('div'); const div = document.createElement('div');
@ -234,9 +236,17 @@ Notification.newInstance = function newNotificationInstance(properties, callback
const global = globalConfigForApi; const global = globalConfigForApi;
const prefixCls = global.getPrefixCls(name, customizePrefixCls); const prefixCls = global.getPrefixCls(name, customizePrefixCls);
const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls); const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls);
const transitionName = hasTransitionName
? customTransitionName
: `${rootPrefixCls}-${customTransitionName}`;
return ( return (
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}> <ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
<Notification ref={notiRef} {...attrs} prefixCls={prefixCls} /> <Notification
ref={notiRef}
{...attrs}
prefixCls={prefixCls}
transitionName={transitionName}
/>
</ConfigProvider> </ConfigProvider>
); );
}; };