Feat(use): add useMessage useNotification (#6527)
* feat(Message): add useMessage hook * feat(Notification): add useNotification hook * feat(Message): add Hook demo * feat(Notification): add Hook demo * test(Message): update demo snap * test(Notification): update demo snap * docs(Message): update docs with FAQ * docs(Notification): update docs with FAQpull/6538/head
							parent
							
								
									b61c88e5df
								
							
						
					
					
						commit
						6eb4d8f5c5
					
				| 
						 | 
				
			
			@ -78,5 +78,24 @@ export function renderHelper<T = Record<string, any>>(
 | 
			
		|||
  }
 | 
			
		||||
  return v ?? defaultV;
 | 
			
		||||
}
 | 
			
		||||
export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
 | 
			
		||||
  let closeFn: VoidFunction;
 | 
			
		||||
 | 
			
		||||
  const closePromise = new Promise<boolean>(resolve => {
 | 
			
		||||
    closeFn = openFn(() => {
 | 
			
		||||
      resolve(true);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const result: any = () => {
 | 
			
		||||
    closeFn?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  result.then = (filled: VoidFunction, rejected: VoidFunction) =>
 | 
			
		||||
    closePromise.then(filled, rejected);
 | 
			
		||||
  result.promise = closePromise;
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
import Notice from '../vc-notification/Notice';
 | 
			
		||||
import type { NoticeProps } from '../vc-notification/Notice';
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
import type { NoticeType } from './interface';
 | 
			
		||||
import {
 | 
			
		||||
  CheckCircleFilled,
 | 
			
		||||
  CloseCircleFilled,
 | 
			
		||||
  ExclamationCircleFilled,
 | 
			
		||||
  InfoCircleFilled,
 | 
			
		||||
  LoadingOutlined,
 | 
			
		||||
} from '@ant-design/icons-vue';
 | 
			
		||||
import type { VueNode } from '../_util/type';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import { useConfigContextInject } from '../config-provider/context';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
 | 
			
		||||
export const TypeIcon = {
 | 
			
		||||
  info: <InfoCircleFilled />,
 | 
			
		||||
  success: <CheckCircleFilled />,
 | 
			
		||||
  error: <CloseCircleFilled />,
 | 
			
		||||
  warning: <ExclamationCircleFilled />,
 | 
			
		||||
  loading: <LoadingOutlined />,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface PureContentProps {
 | 
			
		||||
  prefixCls: string;
 | 
			
		||||
  type?: NoticeType;
 | 
			
		||||
  icon?: VueNode;
 | 
			
		||||
  children: VueNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const PureContent = defineComponent({
 | 
			
		||||
  name: 'PureContent',
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: ['prefixCls', 'type', 'icon'] as any,
 | 
			
		||||
 | 
			
		||||
  setup(props, { slots }) {
 | 
			
		||||
    return () => (
 | 
			
		||||
      <div
 | 
			
		||||
        class={classNames(`${props.prefixCls}-custom-content`, `${props.prefixCls}-${props.type}`)}
 | 
			
		||||
      >
 | 
			
		||||
        {props.icon || TypeIcon[props.type!]}
 | 
			
		||||
        <span>{slots.default?.()}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface PurePanelProps
 | 
			
		||||
  extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
 | 
			
		||||
    Omit<PureContentProps, 'prefixCls' | 'children'> {
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @private Internal Component. Do not use in your production. */
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'PurePanel',
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: ['prefixCls', 'class', 'type', 'icon', 'content'] as any,
 | 
			
		||||
  setup(props, { slots, attrs }) {
 | 
			
		||||
    const { getPrefixCls } = useConfigContextInject();
 | 
			
		||||
    const prefixCls = computed(() => props.staticPrefixCls || getPrefixCls('message'));
 | 
			
		||||
    const [, hashId] = useStyle(prefixCls);
 | 
			
		||||
    return (
 | 
			
		||||
      <Notice
 | 
			
		||||
        {...attrs}
 | 
			
		||||
        prefixCls={prefixCls.value}
 | 
			
		||||
        class={classNames(hashId, `${prefixCls.value}-notice-pure-panel`)}
 | 
			
		||||
        noticeKey="pure"
 | 
			
		||||
        duration={null}
 | 
			
		||||
      >
 | 
			
		||||
        <PureContent prefixCls={props.prefixCls} type={props.type} icon={props.icon}>
 | 
			
		||||
          {slots.default?.()}
 | 
			
		||||
        </PureContent>
 | 
			
		||||
      </Notice>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,14 @@ exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
 | 
			
		|||
</button>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`renders ./components/message/demo/hook.vue correctly 1`] = `
 | 
			
		||||
<!--teleport start-->
 | 
			
		||||
<!--teleport end-->
 | 
			
		||||
<button class="ant-btn ant-btn-primary" type="button">
 | 
			
		||||
  <!----><span>Display normal message</span>
 | 
			
		||||
</button>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`renders ./components/message/demo/info.vue correctly 1`] = `
 | 
			
		||||
<button class="ant-btn ant-btn-primary" type="button">
 | 
			
		||||
  <!----><span>Display normal message</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<docs>
 | 
			
		||||
---
 | 
			
		||||
order: 10
 | 
			
		||||
title:
 | 
			
		||||
  zh-CN: Hooks 调用(推荐)
 | 
			
		||||
  en-US: Hooks Usage (Recommend)
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## zh-CN
 | 
			
		||||
 | 
			
		||||
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
 | 
			
		||||
 | 
			
		||||
## en-US
 | 
			
		||||
 | 
			
		||||
Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work.
 | 
			
		||||
 | 
			
		||||
</docs>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
  <a-button type="primary" @click="info">Display normal message</a-button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { message } from 'ant-design-vue';
 | 
			
		||||
const [messageApi, contextHolder] = message.useMessage();
 | 
			
		||||
 | 
			
		||||
const info = () => {
 | 
			
		||||
  messageApi.info('Hello, Ant Design!');
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <demo-sort>
 | 
			
		||||
    <Hook />
 | 
			
		||||
    <info />
 | 
			
		||||
    <duration />
 | 
			
		||||
    <other />
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +21,7 @@ import customStyleVue from './custom-style.vue';
 | 
			
		|||
import CN from '../index.zh-CN.md';
 | 
			
		||||
import US from '../index.en-US.md';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import Hook from './hook.vue';
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  CN,
 | 
			
		||||
  US,
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +33,7 @@ export default defineComponent({
 | 
			
		|||
    Thenable,
 | 
			
		||||
    Update,
 | 
			
		||||
    customStyleVue,
 | 
			
		||||
    Hook,
 | 
			
		||||
  },
 | 
			
		||||
  setup() {
 | 
			
		||||
    return {};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,6 +64,7 @@ Methods for global configuration and destruction are also provided:
 | 
			
		|||
 | 
			
		||||
- `message.config(options)`
 | 
			
		||||
- `message.destroy()`
 | 
			
		||||
- `message.useMessage()`
 | 
			
		||||
 | 
			
		||||
#### message.config
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,3 +86,33 @@ message.config({
 | 
			
		|||
| 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` |  |
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
 | 
			
		||||
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in message?
 | 
			
		||||
 | 
			
		||||
antdv will dynamic create Vue instance by `Vue.render` when call message methods. Whose context is different with origin code located context.
 | 
			
		||||
 | 
			
		||||
When you need context info (like ConfigProvider context), you can use `message.useMessage` to get `api` instance and `contextHolder` node. And put it in your children:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<template>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
  <!-- <component :is='contextHolder'/> -->
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  import { message } from 'ant-design-vue';
 | 
			
		||||
  const [messageApi, contextHolder] = message.useMessage();
 | 
			
		||||
  messageApi.open({
 | 
			
		||||
    // ...
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
 | 
			
		||||
 | 
			
		||||
> [App Package Component](/components/app) can be used to simplify the problem of `useMessage` and other methods that need to manually implant contextHolder.
 | 
			
		||||
 | 
			
		||||
### How to set static methods prefixCls ?
 | 
			
		||||
 | 
			
		||||
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import type { Key, VueNode } from '../_util/type';
 | 
			
		|||
import type { NotificationInstance } from '../vc-notification/Notification';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
 | 
			
		||||
import useMessage from './useMessage';
 | 
			
		||||
let defaultDuration = 3;
 | 
			
		||||
let defaultTop: string;
 | 
			
		||||
let messageInstance: NotificationInstance;
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn
 | 
			
		|||
    callback(messageInstance);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Notification.newInstance(
 | 
			
		||||
    {
 | 
			
		||||
      appContext: args.appContext,
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +226,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
 | 
			
		|||
typeList.forEach(type => attachTypeApi(api, type));
 | 
			
		||||
 | 
			
		||||
api.warn = api.warning;
 | 
			
		||||
 | 
			
		||||
api.useMessage = useMessage;
 | 
			
		||||
export interface MessageInstance {
 | 
			
		||||
  info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
 | 
			
		||||
  success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
 | 
			
		||||
| 
						 | 
				
			
			@ -233,6 +234,7 @@ export interface MessageInstance {
 | 
			
		|||
  warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
 | 
			
		||||
  loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
 | 
			
		||||
  open(args: MessageArgsProps): MessageType;
 | 
			
		||||
  useMessage: typeof useMessage;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MessageApi extends MessageInstance {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*7qMTRoq3ZGkAAA
 | 
			
		|||
 | 
			
		||||
- `message.config(options)`
 | 
			
		||||
- `message.destroy()`
 | 
			
		||||
- `message.useMessage()`
 | 
			
		||||
 | 
			
		||||
#### message.config
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -88,3 +89,33 @@ message.config({
 | 
			
		|||
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 |  |
 | 
			
		||||
| rtl | 是否开启 RTL 模式 | boolean | false |  |  |
 | 
			
		||||
| top | 消息距离顶部的位置 | string | `8px` |  |  |
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
 | 
			
		||||
### 为什么 message 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
 | 
			
		||||
 | 
			
		||||
直接调用 message 方法,antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
 | 
			
		||||
 | 
			
		||||
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<template>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
  <!-- <component :is='contextHolder'/> -->
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  import { message } from 'ant-design-vue';
 | 
			
		||||
  const [messageApi, contextHolder] = message.useMessage();
 | 
			
		||||
  messageApi.open({
 | 
			
		||||
    // ...
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
 | 
			
		||||
 | 
			
		||||
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useMessage` 等方法需要手动植入 contextHolder 的问题。
 | 
			
		||||
 | 
			
		||||
### 静态方法如何设置 prefixCls ?
 | 
			
		||||
 | 
			
		||||
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import type { Key, VueNode } from '../_util/type';
 | 
			
		||||
 | 
			
		||||
export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
 | 
			
		||||
 | 
			
		||||
export interface ConfigOptions {
 | 
			
		||||
  top?: number;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  getContainer?: () => HTMLElement;
 | 
			
		||||
  transitionName?: string;
 | 
			
		||||
  maxCount?: number;
 | 
			
		||||
  rtl?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ArgsProps {
 | 
			
		||||
  content: VueNode;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
  type?: NoticeType;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
  icon?: VueNode;
 | 
			
		||||
  key?: string | number;
 | 
			
		||||
  style?: CSSProperties;
 | 
			
		||||
  className?: string;
 | 
			
		||||
  onClick?: (e: Event) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type JointContent = VueNode | ArgsProps;
 | 
			
		||||
 | 
			
		||||
export interface MessageType extends PromiseLike<boolean> {
 | 
			
		||||
  (): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TypeOpen = (
 | 
			
		||||
  content: JointContent,
 | 
			
		||||
  duration?: number | VoidFunction, // Also can use onClose directly
 | 
			
		||||
  onClose?: VoidFunction,
 | 
			
		||||
) => MessageType;
 | 
			
		||||
 | 
			
		||||
export interface MessageInstance {
 | 
			
		||||
  info: TypeOpen;
 | 
			
		||||
  success: TypeOpen;
 | 
			
		||||
  error: TypeOpen;
 | 
			
		||||
  warning: TypeOpen;
 | 
			
		||||
  loading: TypeOpen;
 | 
			
		||||
  open(args: ArgsProps): MessageType;
 | 
			
		||||
  destroy(key?: Key): void;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,228 @@
 | 
			
		|||
import { shallowRef, computed, defineComponent } from 'vue';
 | 
			
		||||
import { useNotification as useVcNotification } from '../vc-notification';
 | 
			
		||||
import type { NotificationAPI } from '../vc-notification';
 | 
			
		||||
import CloseOutlined from '@ant-design/icons-vue';
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
import type {
 | 
			
		||||
  MessageInstance,
 | 
			
		||||
  ArgsProps,
 | 
			
		||||
  MessageType,
 | 
			
		||||
  ConfigOptions,
 | 
			
		||||
  NoticeType,
 | 
			
		||||
  TypeOpen,
 | 
			
		||||
} from './interface';
 | 
			
		||||
 | 
			
		||||
import { PureContent } from './PurePanel';
 | 
			
		||||
import { getMotion } from '../vc-trigger/utils/motionUtil';
 | 
			
		||||
import type { Key } from '../_util/type';
 | 
			
		||||
import { wrapPromiseFn } from '../_util/util';
 | 
			
		||||
import type { VNode } from 'vue';
 | 
			
		||||
import useConfigInject from '../config-provider/hooks/useConfigInject';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
 | 
			
		||||
const DEFAULT_OFFSET = 8;
 | 
			
		||||
const DEFAULT_DURATION = 3;
 | 
			
		||||
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
// ==                                  Holder                                  ==
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
type HolderProps = ConfigOptions & {
 | 
			
		||||
  onAllRemoved?: VoidFunction;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface HolderRef extends NotificationAPI {
 | 
			
		||||
  prefixCls: string;
 | 
			
		||||
  hashId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Holder = defineComponent({
 | 
			
		||||
  name: 'Holder',
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: [
 | 
			
		||||
    'top',
 | 
			
		||||
    'prefixCls',
 | 
			
		||||
    'getContainer',
 | 
			
		||||
    'maxCount',
 | 
			
		||||
    'duration',
 | 
			
		||||
    'rtl',
 | 
			
		||||
    'transitionName',
 | 
			
		||||
    'onAllRemoved',
 | 
			
		||||
  ] as any,
 | 
			
		||||
  setup(props, { expose }) {
 | 
			
		||||
    const { getPrefixCls, getPopupContainer } = useConfigInject('message', props);
 | 
			
		||||
 | 
			
		||||
    const prefixCls = computed(() => getPrefixCls('message', props.prefixCls));
 | 
			
		||||
 | 
			
		||||
    const [, hashId] = useStyle(prefixCls);
 | 
			
		||||
 | 
			
		||||
    // =============================== Style ===============================
 | 
			
		||||
    const getStyles = () => ({
 | 
			
		||||
      left: '50%',
 | 
			
		||||
      transform: 'translateX(-50%)',
 | 
			
		||||
      top: top ?? DEFAULT_OFFSET,
 | 
			
		||||
    });
 | 
			
		||||
    const getClassName = () => classNames(hashId.value, props.rtl ? `${prefixCls.value}-rtl` : '');
 | 
			
		||||
 | 
			
		||||
    // ============================== Motion ===============================
 | 
			
		||||
    const getNotificationMotion = () =>
 | 
			
		||||
      getMotion({
 | 
			
		||||
        prefixCls: prefixCls.value,
 | 
			
		||||
        animation: props.animation ?? `move-up`,
 | 
			
		||||
        transitionName: props.transitionName,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    // ============================ Close Icon =============================
 | 
			
		||||
    const mergedCloseIcon = (
 | 
			
		||||
      <span class={`${prefixCls.value}-close-x`}>
 | 
			
		||||
        <CloseOutlined class={`${prefixCls.value}-close-icon`} />
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // ============================== Origin ===============================
 | 
			
		||||
    const [api, holder] = useVcNotification({
 | 
			
		||||
      //@ts-ignore
 | 
			
		||||
      getStyles,
 | 
			
		||||
      prefixCls: prefixCls.value,
 | 
			
		||||
      getClassName,
 | 
			
		||||
      motion: getNotificationMotion,
 | 
			
		||||
      closable: false,
 | 
			
		||||
      closeIcon: mergedCloseIcon,
 | 
			
		||||
      duration: props.duration ?? DEFAULT_DURATION,
 | 
			
		||||
      getContainer: () =>
 | 
			
		||||
        props.staticGetContainer?.() || getPopupContainer.value?.() || document.body,
 | 
			
		||||
      maxCount: props.maxCount,
 | 
			
		||||
      onAllRemoved: props.onAllRemoved,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // ================================ Ref ================================
 | 
			
		||||
    expose({
 | 
			
		||||
      ...api,
 | 
			
		||||
      prefixCls,
 | 
			
		||||
      hashId,
 | 
			
		||||
    });
 | 
			
		||||
    return holder;
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
// ==                                   Hook                                   ==
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
let keyIndex = 0;
 | 
			
		||||
 | 
			
		||||
export function useInternalMessage(
 | 
			
		||||
  messageConfig?: HolderProps,
 | 
			
		||||
): readonly [MessageInstance, () => VNode] {
 | 
			
		||||
  const holderRef = shallowRef<HolderRef>(null);
 | 
			
		||||
  // ================================ API ================================
 | 
			
		||||
  const wrapAPI = computed(() => {
 | 
			
		||||
    // Wrap with notification content
 | 
			
		||||
    // >>> close
 | 
			
		||||
    const close = (key: Key) => {
 | 
			
		||||
      holderRef.value?.close(key);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // >>> Open
 | 
			
		||||
    const open = (config: ArgsProps): MessageType => {
 | 
			
		||||
      if (!holderRef.value) {
 | 
			
		||||
        const fakeResult: any = () => {};
 | 
			
		||||
        fakeResult.then = () => {};
 | 
			
		||||
        return fakeResult;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { open: originOpen, prefixCls, hashId } = holderRef.value;
 | 
			
		||||
      const noticePrefixCls = `${prefixCls}-notice`;
 | 
			
		||||
      const { content, icon, type, key, className, onClose, ...restConfig } = config;
 | 
			
		||||
 | 
			
		||||
      let mergedKey: Key = key!;
 | 
			
		||||
      if (mergedKey === undefined || mergedKey === null) {
 | 
			
		||||
        keyIndex += 1;
 | 
			
		||||
        mergedKey = `antd-message-${keyIndex}`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return wrapPromiseFn(resolve => {
 | 
			
		||||
        originOpen({
 | 
			
		||||
          ...restConfig,
 | 
			
		||||
          key: mergedKey,
 | 
			
		||||
          content: (
 | 
			
		||||
            <PureContent prefixCls={prefixCls} type={type} icon={icon}>
 | 
			
		||||
              {content}
 | 
			
		||||
            </PureContent>
 | 
			
		||||
          ),
 | 
			
		||||
          placement: 'top',
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
 | 
			
		||||
          onClose: () => {
 | 
			
		||||
            onClose?.();
 | 
			
		||||
            resolve();
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Return close function
 | 
			
		||||
        return () => {
 | 
			
		||||
          close(mergedKey);
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // >>> destroy
 | 
			
		||||
    const destroy = (key?: Key) => {
 | 
			
		||||
      if (key !== undefined) {
 | 
			
		||||
        close(key);
 | 
			
		||||
      } else {
 | 
			
		||||
        holderRef.value?.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const clone = {
 | 
			
		||||
      open,
 | 
			
		||||
      destroy,
 | 
			
		||||
    } as MessageInstance;
 | 
			
		||||
 | 
			
		||||
    const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading'];
 | 
			
		||||
    keys.forEach(type => {
 | 
			
		||||
      const typeOpen: TypeOpen = (jointContent, duration, onClose) => {
 | 
			
		||||
        let config: ArgsProps;
 | 
			
		||||
        if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) {
 | 
			
		||||
          config = jointContent;
 | 
			
		||||
        } else {
 | 
			
		||||
          config = {
 | 
			
		||||
            content: jointContent as VNode,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Params
 | 
			
		||||
        let mergedDuration: number | undefined;
 | 
			
		||||
        let mergedOnClose: VoidFunction | undefined;
 | 
			
		||||
        if (typeof duration === 'function') {
 | 
			
		||||
          mergedOnClose = duration;
 | 
			
		||||
        } else {
 | 
			
		||||
          mergedDuration = duration;
 | 
			
		||||
          mergedOnClose = onClose;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const mergedConfig = {
 | 
			
		||||
          onClose: mergedOnClose,
 | 
			
		||||
          duration: mergedDuration,
 | 
			
		||||
          ...config,
 | 
			
		||||
          type,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return open(mergedConfig);
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      clone[type] = typeOpen;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return clone;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // ============================== Return ===============================
 | 
			
		||||
  return [
 | 
			
		||||
    wrapAPI.value,
 | 
			
		||||
    () => <Holder key="message-holder" {...messageConfig} ref={holderRef} />,
 | 
			
		||||
  ] as const;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useMessage(messageConfig?: ConfigOptions) {
 | 
			
		||||
  return useInternalMessage(messageConfig);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
import { computed } from 'vue';
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
import useConfigInject from '../config-provider/hooks/useConfigInject';
 | 
			
		||||
import type { IconType } from './interface';
 | 
			
		||||
import Notice from '../vc-notification/Notice';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import type { NoticeProps } from '../vc-notification/Notice';
 | 
			
		||||
import type { VueNode } from '../_util/type';
 | 
			
		||||
import {
 | 
			
		||||
  CheckCircleOutlined,
 | 
			
		||||
  CloseCircleOutlined,
 | 
			
		||||
  CloseOutlined,
 | 
			
		||||
  ExclamationCircleOutlined,
 | 
			
		||||
  InfoCircleOutlined,
 | 
			
		||||
} from '@ant-design/icons-vue';
 | 
			
		||||
import { renderHelper } from '../_util/util';
 | 
			
		||||
 | 
			
		||||
export function getCloseIcon(prefixCls: string, closeIcon?: VueNode) {
 | 
			
		||||
  return (
 | 
			
		||||
    closeIcon || (
 | 
			
		||||
      <span class={`${prefixCls}-close-x`}>
 | 
			
		||||
        <CloseOutlined class={`${prefixCls}-close-icon`} />
 | 
			
		||||
      </span>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PureContentProps {
 | 
			
		||||
  prefixCls: string;
 | 
			
		||||
  icon?: VueNode;
 | 
			
		||||
  message?: VueNode;
 | 
			
		||||
  description?: VueNode;
 | 
			
		||||
  btn?: VueNode;
 | 
			
		||||
  type?: IconType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const typeToIcon = {
 | 
			
		||||
  success: CheckCircleOutlined,
 | 
			
		||||
  info: InfoCircleOutlined,
 | 
			
		||||
  error: CloseCircleOutlined,
 | 
			
		||||
  warning: ExclamationCircleOutlined,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function PureContent({
 | 
			
		||||
  prefixCls,
 | 
			
		||||
  icon,
 | 
			
		||||
  type,
 | 
			
		||||
  message,
 | 
			
		||||
  description,
 | 
			
		||||
  btn,
 | 
			
		||||
}: PureContentProps) {
 | 
			
		||||
  let iconNode = null;
 | 
			
		||||
  if (icon) {
 | 
			
		||||
    iconNode = <span class={`${prefixCls}-icon`}>{renderHelper(icon)}</span>;
 | 
			
		||||
  } else if (type) {
 | 
			
		||||
    const Icon = typeToIcon[type];
 | 
			
		||||
    iconNode = <Icon class={`${prefixCls}-icon ${prefixCls}-icon-${type}`} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      class={classNames({
 | 
			
		||||
        [`${prefixCls}-with-icon`]: iconNode,
 | 
			
		||||
      })}
 | 
			
		||||
      role="alert"
 | 
			
		||||
    >
 | 
			
		||||
      {iconNode}
 | 
			
		||||
      <div class={`${prefixCls}-message`}>{message}</div>
 | 
			
		||||
      <div class={`${prefixCls}-description`}>{description}</div>
 | 
			
		||||
      {btn && <div class={`${prefixCls}-btn`}>{btn}</div>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PurePanelProps
 | 
			
		||||
  extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
 | 
			
		||||
    Omit<PureContentProps, 'prefixCls' | 'children'> {
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @private Internal Component. Do not use in your production. */
 | 
			
		||||
export default function PurePanel(props: PurePanelProps) {
 | 
			
		||||
  const { getPrefixCls } = useConfigInject('notification', props);
 | 
			
		||||
  const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
 | 
			
		||||
  const noticePrefixCls = `${prefixCls.value}-notice`;
 | 
			
		||||
 | 
			
		||||
  const [, hashId] = useStyle(prefixCls);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Notice
 | 
			
		||||
      {...props}
 | 
			
		||||
      prefixCls={prefixCls.value}
 | 
			
		||||
      class={classNames(hashId.value, `${noticePrefixCls}-pure-panel`)}
 | 
			
		||||
      noticeKey="pure"
 | 
			
		||||
      duration={null}
 | 
			
		||||
      closable={props.closable}
 | 
			
		||||
      closeIcon={getCloseIcon(prefixCls.value, props.closeIcon)}
 | 
			
		||||
    >
 | 
			
		||||
      <PureContent
 | 
			
		||||
        prefixCls={noticePrefixCls}
 | 
			
		||||
        icon={props.icon}
 | 
			
		||||
        type={props.type}
 | 
			
		||||
        message={props.message}
 | 
			
		||||
        description={props.description}
 | 
			
		||||
        btn={props.btn}
 | 
			
		||||
      />
 | 
			
		||||
    </Notice>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
<docs>
 | 
			
		||||
---
 | 
			
		||||
order: 10
 | 
			
		||||
title:
 | 
			
		||||
  zh-CN: Hooks 调用(推荐)
 | 
			
		||||
  en-US: Hooks Usage (Recommend)
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## zh-CN
 | 
			
		||||
 | 
			
		||||
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
 | 
			
		||||
 | 
			
		||||
## en-US
 | 
			
		||||
 | 
			
		||||
Use `notification.useNotification` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `notification` static method, because static method cannot consume context, and ConfigProvider data will not work.
 | 
			
		||||
 | 
			
		||||
</docs>
 | 
			
		||||
<template>
 | 
			
		||||
  <a-space>
 | 
			
		||||
    <a-button type="primary" @click="() => open('topLeft')">
 | 
			
		||||
      <RadiusUpleftOutlined />
 | 
			
		||||
      topLeft
 | 
			
		||||
    </a-button>
 | 
			
		||||
    <a-button type="primary" @click="() => open('topRight')">
 | 
			
		||||
      <RadiusUprightOutlined />
 | 
			
		||||
      topRight
 | 
			
		||||
    </a-button>
 | 
			
		||||
  </a-space>
 | 
			
		||||
  <a-divider />
 | 
			
		||||
  <a-space>
 | 
			
		||||
    <a-button type="primary" @click="() => open('bottomLeft')">
 | 
			
		||||
      <RadiusBottomleftOutlined />
 | 
			
		||||
      bottomLeft
 | 
			
		||||
    </a-button>
 | 
			
		||||
    <a-button type="primary" @click="() => open('bottomRight')">
 | 
			
		||||
      <RadiusBottomrightOutlined />
 | 
			
		||||
      bottomRight
 | 
			
		||||
    </a-button>
 | 
			
		||||
  </a-space>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import {
 | 
			
		||||
  RadiusBottomleftOutlined,
 | 
			
		||||
  RadiusBottomrightOutlined,
 | 
			
		||||
  RadiusUpleftOutlined,
 | 
			
		||||
  RadiusUprightOutlined,
 | 
			
		||||
} from '@ant-design/icons-vue';
 | 
			
		||||
import { NotificationPlacement, notification } from 'ant-design-vue';
 | 
			
		||||
const [api, contextHolder] = notification.useNotification();
 | 
			
		||||
const open = (placement: NotificationPlacement) => openNotification(placement);
 | 
			
		||||
const openNotification = (placement: NotificationPlacement) => {
 | 
			
		||||
  api.info({
 | 
			
		||||
    message: `Notification ${placement}`,
 | 
			
		||||
    description:
 | 
			
		||||
      'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
 | 
			
		||||
    placement,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <demo-sort>
 | 
			
		||||
    <hook />
 | 
			
		||||
    <basic />
 | 
			
		||||
    <duratioin />
 | 
			
		||||
    <with-icon />
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +13,7 @@
 | 
			
		|||
</template>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Basic from './basic.vue';
 | 
			
		||||
import Hook from './hook.vue';
 | 
			
		||||
import Duratioin from './duration.vue';
 | 
			
		||||
import WithIcon from './with-icon.vue';
 | 
			
		||||
import CustomIcon from './custom-icon.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +37,7 @@ export default defineComponent({
 | 
			
		|||
    CustomStyle,
 | 
			
		||||
    Placement,
 | 
			
		||||
    Update,
 | 
			
		||||
    Hook,
 | 
			
		||||
  },
 | 
			
		||||
  setup() {
 | 
			
		||||
    return {};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ To display a notification message at any of the four corners of the viewport. Ty
 | 
			
		|||
- `notification.open(config)`
 | 
			
		||||
- `notification.close(key: String)`
 | 
			
		||||
- `notification.destroy()`
 | 
			
		||||
- `notification.useNotification()`
 | 
			
		||||
 | 
			
		||||
The properties of config are as follows:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -74,3 +75,33 @@ notification.config({
 | 
			
		|||
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` |  |
 | 
			
		||||
| rtl | Whether to enable RTL mode | boolean | false |  |
 | 
			
		||||
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` |  |
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
 | 
			
		||||
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in notification?
 | 
			
		||||
 | 
			
		||||
antdv will dynamic create Vue instance by `Vue.render` when call notification methods. Whose context is different with origin code located context.
 | 
			
		||||
 | 
			
		||||
When you need context info (like ConfigProvider context), you can use `notification.useNotification` to get `api` instance and `contextHolder` node. And put it in your children:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<template>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
  <!-- <component :is='contextHolder'/> -->
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  import { notification } from 'ant-design-vue';
 | 
			
		||||
  const [notificationApi, contextHolder] = notification.useNotification();
 | 
			
		||||
  notificationApi.open({
 | 
			
		||||
    // ...
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
 | 
			
		||||
 | 
			
		||||
> [App Package Component](/components/app) can be used to simplify the problem of `useNotification` and other methods that need to manually implant contextHolder.
 | 
			
		||||
 | 
			
		||||
### How to set static methods prefixCls ?
 | 
			
		||||
 | 
			
		||||
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,8 @@ import { globalConfig } from '../config-provider';
 | 
			
		|||
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
import useNotification from './useNotification';
 | 
			
		||||
 | 
			
		||||
export type NotificationPlacement =
 | 
			
		||||
  | 'top'
 | 
			
		||||
  | 'topLeft'
 | 
			
		||||
| 
						 | 
				
			
			@ -284,6 +286,7 @@ iconTypes.forEach(type => {
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
api.warn = api.warning;
 | 
			
		||||
api.useNotification = useNotification;
 | 
			
		||||
 | 
			
		||||
export interface NotificationInstance {
 | 
			
		||||
  success(args: NotificationArgsProps): void;
 | 
			
		||||
| 
						 | 
				
			
			@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance {
 | 
			
		|||
  close(key: string): void;
 | 
			
		||||
  config(options: ConfigProps): void;
 | 
			
		||||
  destroy(): void;
 | 
			
		||||
  useNotification: typeof useNotification;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @private test Only function. Not work on production */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*9hTIToR-3YYAAA
 | 
			
		|||
- `notification.open(config)`
 | 
			
		||||
- `notification.close(key: String)`
 | 
			
		||||
- `notification.destroy()`
 | 
			
		||||
- `notification.useNotification()`
 | 
			
		||||
 | 
			
		||||
config 参数如下:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -75,3 +76,33 @@ notification.config({
 | 
			
		|||
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight |  |
 | 
			
		||||
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
 | 
			
		||||
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` |  |
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
 | 
			
		||||
### 为什么 notification 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
 | 
			
		||||
 | 
			
		||||
直接调用 notification 方法,antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
 | 
			
		||||
 | 
			
		||||
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `notification.useNotification` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<template>
 | 
			
		||||
  <contextHolder />
 | 
			
		||||
  <!-- <component :is='contextHolder'/> -->
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  import { notification } from 'ant-design-vue';
 | 
			
		||||
  const [notificationApi, contextHolder] = notification.useNotification();
 | 
			
		||||
  notificationApi.open({
 | 
			
		||||
    // ...
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
 | 
			
		||||
 | 
			
		||||
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useNotification` 等方法需要手动植入 contextHolder 的问题。
 | 
			
		||||
 | 
			
		||||
### 静态方法如何设置 prefixCls ?
 | 
			
		||||
 | 
			
		||||
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import type { Key, VueNode } from '../_util/type';
 | 
			
		||||
 | 
			
		||||
export type NotificationPlacement =
 | 
			
		||||
  | 'top'
 | 
			
		||||
  | 'topLeft'
 | 
			
		||||
  | 'topRight'
 | 
			
		||||
  | 'bottom'
 | 
			
		||||
  | 'bottomLeft'
 | 
			
		||||
  | 'bottomRight';
 | 
			
		||||
 | 
			
		||||
export type IconType = 'success' | 'info' | 'error' | 'warning';
 | 
			
		||||
 | 
			
		||||
export interface ArgsProps {
 | 
			
		||||
  message: VueNode;
 | 
			
		||||
  description?: VueNode;
 | 
			
		||||
  btn?: VueNode;
 | 
			
		||||
  key?: Key;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
  duration?: number | null;
 | 
			
		||||
  icon?: VueNode;
 | 
			
		||||
  placement?: NotificationPlacement;
 | 
			
		||||
  style?: CSSProperties;
 | 
			
		||||
  className?: string;
 | 
			
		||||
  readonly type?: IconType;
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
  closeIcon?: VueNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StaticFn = (args: ArgsProps) => void;
 | 
			
		||||
 | 
			
		||||
export interface NotificationInstance {
 | 
			
		||||
  success: StaticFn;
 | 
			
		||||
  error: StaticFn;
 | 
			
		||||
  info: StaticFn;
 | 
			
		||||
  warning: StaticFn;
 | 
			
		||||
  open: StaticFn;
 | 
			
		||||
  destroy(key?: Key): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GlobalConfigProps {
 | 
			
		||||
  top?: number;
 | 
			
		||||
  bottom?: number;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  getContainer?: () => HTMLElement;
 | 
			
		||||
  placement?: NotificationPlacement;
 | 
			
		||||
  closeIcon?: VueNode;
 | 
			
		||||
  rtl?: boolean;
 | 
			
		||||
  maxCount?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NotificationConfig {
 | 
			
		||||
  top?: number;
 | 
			
		||||
  bottom?: number;
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  getContainer?: () => HTMLElement;
 | 
			
		||||
  placement?: NotificationPlacement;
 | 
			
		||||
  maxCount?: number;
 | 
			
		||||
  rtl?: boolean;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,154 @@
 | 
			
		|||
import type { VNode } from 'vue';
 | 
			
		||||
import { shallowRef, computed, defineComponent } from 'vue';
 | 
			
		||||
import { useNotification as useVcNotification } from '../vc-notification';
 | 
			
		||||
import type { NotificationAPI } from '../vc-notification';
 | 
			
		||||
import type {
 | 
			
		||||
  NotificationInstance,
 | 
			
		||||
  ArgsProps,
 | 
			
		||||
  NotificationPlacement,
 | 
			
		||||
  NotificationConfig,
 | 
			
		||||
} from './interface';
 | 
			
		||||
 | 
			
		||||
import useStyle from './style';
 | 
			
		||||
import { getCloseIcon, PureContent } from './PurePanel';
 | 
			
		||||
import { getMotion, getPlacementStyle } from './util';
 | 
			
		||||
import useConfigInject from '../config-provider/hooks/useConfigInject';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import type { Key } from '../_util/type';
 | 
			
		||||
 | 
			
		||||
const DEFAULT_OFFSET = 24;
 | 
			
		||||
const DEFAULT_DURATION = 4.5;
 | 
			
		||||
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
// ==                                  Holder                                  ==
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
type HolderProps = NotificationConfig & {
 | 
			
		||||
  onAllRemoved?: VoidFunction;
 | 
			
		||||
  getPopupContainer?: () => HTMLElement;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface HolderRef extends NotificationAPI {
 | 
			
		||||
  prefixCls: string;
 | 
			
		||||
  hashId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Holder = defineComponent({
 | 
			
		||||
  name: 'Holder',
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: ['prefixCls', 'class', 'type', 'icon', 'content', 'onAllRemoved'],
 | 
			
		||||
  setup(props: HolderProps, { expose }) {
 | 
			
		||||
    const { getPrefixCls, getPopupContainer } = useConfigInject('notification', props);
 | 
			
		||||
    const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
 | 
			
		||||
    // =============================== Style ===============================
 | 
			
		||||
    const getStyles = (placement: NotificationPlacement) =>
 | 
			
		||||
      getPlacementStyle(placement, props.top ?? DEFAULT_OFFSET, props.bottom ?? DEFAULT_OFFSET);
 | 
			
		||||
 | 
			
		||||
    // Style
 | 
			
		||||
    const [, hashId] = useStyle(prefixCls);
 | 
			
		||||
 | 
			
		||||
    const getClassName = () => classNames(hashId.value, { [`${prefixCls.value}-rtl`]: props.rtl });
 | 
			
		||||
 | 
			
		||||
    // ============================== Motion ===============================
 | 
			
		||||
    const getNotificationMotion = () => getMotion(prefixCls.value);
 | 
			
		||||
 | 
			
		||||
    // ============================== Origin ===============================
 | 
			
		||||
    const [api, holder] = useVcNotification({
 | 
			
		||||
      prefixCls: prefixCls.value,
 | 
			
		||||
      getStyles,
 | 
			
		||||
      getClassName,
 | 
			
		||||
      motion: getNotificationMotion,
 | 
			
		||||
      closable: true,
 | 
			
		||||
      closeIcon: getCloseIcon(prefixCls.value),
 | 
			
		||||
      duration: DEFAULT_DURATION,
 | 
			
		||||
      getContainer: () =>
 | 
			
		||||
        props.getPopupContainer?.() || getPopupContainer.value?.() || document.body,
 | 
			
		||||
      maxCount: props.maxCount,
 | 
			
		||||
      hashId: hashId.value,
 | 
			
		||||
      onAllRemoved: props.onAllRemoved,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // ================================ Ref ================================
 | 
			
		||||
    expose({
 | 
			
		||||
      ...api,
 | 
			
		||||
      prefixCls: prefixCls.value,
 | 
			
		||||
      hashId,
 | 
			
		||||
    });
 | 
			
		||||
    return holder;
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
// ==                                   Hook                                   ==
 | 
			
		||||
// ==============================================================================
 | 
			
		||||
export function useInternalNotification(
 | 
			
		||||
  notificationConfig?: HolderProps,
 | 
			
		||||
): readonly [NotificationInstance, () => VNode] {
 | 
			
		||||
  const holderRef = shallowRef<HolderRef>(null);
 | 
			
		||||
 | 
			
		||||
  // ================================ API ================================
 | 
			
		||||
  const wrapAPI = computed(() => {
 | 
			
		||||
    // Wrap with notification content
 | 
			
		||||
 | 
			
		||||
    // >>> Open
 | 
			
		||||
    const open = (config: ArgsProps) => {
 | 
			
		||||
      if (!holderRef.value) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const { open: originOpen, prefixCls, hashId } = holderRef.value;
 | 
			
		||||
      const noticePrefixCls = `${prefixCls}-notice`;
 | 
			
		||||
 | 
			
		||||
      const { message, description, icon, type, btn, className, ...restConfig } = config;
 | 
			
		||||
      return originOpen({
 | 
			
		||||
        placement: 'topRight',
 | 
			
		||||
        ...restConfig,
 | 
			
		||||
        content: (
 | 
			
		||||
          <PureContent
 | 
			
		||||
            prefixCls={noticePrefixCls}
 | 
			
		||||
            icon={icon}
 | 
			
		||||
            type={type}
 | 
			
		||||
            message={message}
 | 
			
		||||
            description={description}
 | 
			
		||||
            btn={btn}
 | 
			
		||||
          />
 | 
			
		||||
        ),
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // >>> destroy
 | 
			
		||||
    const destroy = (key?: Key) => {
 | 
			
		||||
      if (key !== undefined) {
 | 
			
		||||
        holderRef.value?.close(key);
 | 
			
		||||
      } else {
 | 
			
		||||
        holderRef.value?.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const clone = {
 | 
			
		||||
      open,
 | 
			
		||||
      destroy,
 | 
			
		||||
    } as NotificationInstance;
 | 
			
		||||
 | 
			
		||||
    const keys = ['success', 'info', 'warning', 'error'] as const;
 | 
			
		||||
    keys.forEach(type => {
 | 
			
		||||
      clone[type] = config =>
 | 
			
		||||
        open({
 | 
			
		||||
          ...config,
 | 
			
		||||
          type,
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return clone;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // ============================== Return ===============================
 | 
			
		||||
  return [
 | 
			
		||||
    wrapAPI.value,
 | 
			
		||||
    () => <Holder key="notification-holder" {...notificationConfig} ref={holderRef} />,
 | 
			
		||||
  ] as const;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useNotification(notificationConfig?: NotificationConfig) {
 | 
			
		||||
  return useInternalNotification(notificationConfig);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import type { NotificationPlacement } from './interface';
 | 
			
		||||
import type { CSSMotionProps } from '../_util/transition';
 | 
			
		||||
 | 
			
		||||
export function getPlacementStyle(
 | 
			
		||||
  placement: NotificationPlacement,
 | 
			
		||||
  top: number | string,
 | 
			
		||||
  bottom: number | string,
 | 
			
		||||
) {
 | 
			
		||||
  let style: CSSProperties;
 | 
			
		||||
  top = typeof top === 'number' ? `${top}px` : top;
 | 
			
		||||
  bottom = typeof bottom === 'number' ? `${bottom}px` : bottom;
 | 
			
		||||
  switch (placement) {
 | 
			
		||||
    case 'top':
 | 
			
		||||
      style = {
 | 
			
		||||
        left: '50%',
 | 
			
		||||
        transform: 'translateX(-50%)',
 | 
			
		||||
        right: 'auto',
 | 
			
		||||
        top,
 | 
			
		||||
        bottom: 'auto',
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case 'topLeft':
 | 
			
		||||
      style = {
 | 
			
		||||
        left: 0,
 | 
			
		||||
        top,
 | 
			
		||||
        bottom: 'auto',
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case 'topRight':
 | 
			
		||||
      style = {
 | 
			
		||||
        right: 0,
 | 
			
		||||
        top,
 | 
			
		||||
        bottom: 'auto',
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case 'bottom':
 | 
			
		||||
      style = {
 | 
			
		||||
        left: '50%',
 | 
			
		||||
        transform: 'translateX(-50%)',
 | 
			
		||||
        right: 'auto',
 | 
			
		||||
        top: 'auto',
 | 
			
		||||
        bottom,
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case 'bottomLeft':
 | 
			
		||||
      style = {
 | 
			
		||||
        left: 0,
 | 
			
		||||
        top: 'auto',
 | 
			
		||||
        bottom,
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      style = {
 | 
			
		||||
        right: 0,
 | 
			
		||||
        top: 'auto',
 | 
			
		||||
        bottom,
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return style;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getMotion(prefixCls: string): CSSMotionProps {
 | 
			
		||||
  return {
 | 
			
		||||
    name: `${prefixCls}-fade`,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,230 @@
 | 
			
		|||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import { watch, computed, defineComponent, ref, TransitionGroup } from 'vue';
 | 
			
		||||
import type { NoticeProps } from './Notice';
 | 
			
		||||
import Notice from './Notice';
 | 
			
		||||
import type { CSSMotionProps } from '../_util/transition';
 | 
			
		||||
import { getTransitionGroupProps } from '../_util/transition';
 | 
			
		||||
import type { Key, VueNode } from '../_util/type';
 | 
			
		||||
import classNames from '../_util/classNames';
 | 
			
		||||
import Portal from '../_util/Portal';
 | 
			
		||||
 | 
			
		||||
let seed = 0;
 | 
			
		||||
const now = Date.now();
 | 
			
		||||
 | 
			
		||||
export function getUuid() {
 | 
			
		||||
  const id = seed;
 | 
			
		||||
  seed += 1;
 | 
			
		||||
  return `rcNotification_${now}_${id}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
 | 
			
		||||
 | 
			
		||||
export interface OpenConfig extends NoticeProps {
 | 
			
		||||
  key: Key;
 | 
			
		||||
  placement?: Placement;
 | 
			
		||||
  content?: VueNode;
 | 
			
		||||
  duration?: number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Placements = Partial<Record<Placement, OpenConfig[]>>;
 | 
			
		||||
 | 
			
		||||
export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  key?: Key;
 | 
			
		||||
  updateMark?: string;
 | 
			
		||||
  content?: any;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
  style?: CSSProperties;
 | 
			
		||||
  class?: String;
 | 
			
		||||
  placement?: Placement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type NoticeFunc = (noticeProps: NoticeContent) => void;
 | 
			
		||||
export type HolderReadyCallback = (
 | 
			
		||||
  div: HTMLDivElement,
 | 
			
		||||
  noticeProps: NoticeProps & { key: Key },
 | 
			
		||||
) => void;
 | 
			
		||||
 | 
			
		||||
export interface NotificationInstance {
 | 
			
		||||
  notice: NoticeFunc;
 | 
			
		||||
  removeNotice: (key: Key) => void;
 | 
			
		||||
  destroy: () => void;
 | 
			
		||||
  add: (noticeProps: NoticeContent) => void;
 | 
			
		||||
  component: Notification;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface HookNotificationProps {
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  transitionName?: string;
 | 
			
		||||
  animation?: string | CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
 | 
			
		||||
  maxCount?: number;
 | 
			
		||||
  closeIcon?: any;
 | 
			
		||||
  hashId?: string;
 | 
			
		||||
  // Hook Notification
 | 
			
		||||
  remove: (key: Key) => void;
 | 
			
		||||
  notices: NotificationState;
 | 
			
		||||
  getStyles?: (placement?: Placement) => CSSProperties;
 | 
			
		||||
  getClassName?: (placement?: Placement) => string;
 | 
			
		||||
  onAllRemoved?: VoidFunction;
 | 
			
		||||
  getContainer?: () => HTMLElement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationState = {
 | 
			
		||||
  notice: NoticeContent & {
 | 
			
		||||
    userPassKey?: Key;
 | 
			
		||||
  };
 | 
			
		||||
  holderCallback?: HolderReadyCallback;
 | 
			
		||||
}[];
 | 
			
		||||
 | 
			
		||||
const Notification = defineComponent<HookNotificationProps>({
 | 
			
		||||
  name: 'HookNotification',
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: [
 | 
			
		||||
    'prefixCls',
 | 
			
		||||
    'transitionName',
 | 
			
		||||
    'animation',
 | 
			
		||||
    'maxCount',
 | 
			
		||||
    'closeIcon',
 | 
			
		||||
    'hashId',
 | 
			
		||||
    'remove',
 | 
			
		||||
    'notices',
 | 
			
		||||
    'getStyles',
 | 
			
		||||
    'getClassName',
 | 
			
		||||
    'onAllRemoved',
 | 
			
		||||
    'getContainer',
 | 
			
		||||
  ] as any,
 | 
			
		||||
  setup(props, { attrs, slots }) {
 | 
			
		||||
    const hookRefs = new Map<Key, HTMLDivElement>();
 | 
			
		||||
    const notices = computed(() => props.notices);
 | 
			
		||||
    const transitionProps = computed(() => {
 | 
			
		||||
      let name = props.transitionName;
 | 
			
		||||
      if (!name && props.animation) {
 | 
			
		||||
        switch (typeof props.animation) {
 | 
			
		||||
          case 'string':
 | 
			
		||||
            name = props.animation;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'function':
 | 
			
		||||
            name = props.animation().name;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'object':
 | 
			
		||||
            name = props.animation.name;
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            name = `${props.prefixCls}-fade`;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return getTransitionGroupProps(name);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const remove = (key: Key) => props.remove(key);
 | 
			
		||||
    const placements = ref({} as Record<Placement, NotificationState>);
 | 
			
		||||
    watch(notices, () => {
 | 
			
		||||
      const nextPlacements = {} as any;
 | 
			
		||||
      // init placements with animation
 | 
			
		||||
      Object.keys(placements.value).forEach(placement => {
 | 
			
		||||
        nextPlacements[placement] = [];
 | 
			
		||||
      });
 | 
			
		||||
      props.notices.forEach(config => {
 | 
			
		||||
        const { placement = 'topRight' } = config.notice;
 | 
			
		||||
        if (placement) {
 | 
			
		||||
          nextPlacements[placement] = nextPlacements[placement] || [];
 | 
			
		||||
          nextPlacements[placement].push(config);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      placements.value = nextPlacements;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const placementList = computed(() => Object.keys(placements.value) as Placement[]);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
 | 
			
		||||
      const noticeNodes = placementList.value.map(placement => {
 | 
			
		||||
        const noticesForPlacement = placements.value[placement];
 | 
			
		||||
        const classes = props.getClassName?.(placement);
 | 
			
		||||
        const styles = props.getStyles?.(placement);
 | 
			
		||||
        const noticeNodesForPlacement = noticesForPlacement.map(
 | 
			
		||||
          ({ notice, holderCallback }, index) => {
 | 
			
		||||
            const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined;
 | 
			
		||||
            const { key, userPassKey } = notice;
 | 
			
		||||
            const { content } = notice;
 | 
			
		||||
            const noticeProps = {
 | 
			
		||||
              prefixCls,
 | 
			
		||||
              closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon,
 | 
			
		||||
              ...(notice as any),
 | 
			
		||||
              ...notice.props,
 | 
			
		||||
              key,
 | 
			
		||||
              noticeKey: userPassKey || key,
 | 
			
		||||
              updateMark,
 | 
			
		||||
              onClose: (noticeKey: Key) => {
 | 
			
		||||
                remove(noticeKey);
 | 
			
		||||
                notice.onClose?.();
 | 
			
		||||
              },
 | 
			
		||||
              onClick: notice.onClick,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (holderCallback) {
 | 
			
		||||
              return (
 | 
			
		||||
                <div
 | 
			
		||||
                  key={key}
 | 
			
		||||
                  class={`${prefixCls}-hook-holder`}
 | 
			
		||||
                  ref={(div: HTMLDivElement) => {
 | 
			
		||||
                    if (typeof key === 'undefined') {
 | 
			
		||||
                      return;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (div) {
 | 
			
		||||
                      hookRefs.set(key, div);
 | 
			
		||||
                      holderCallback(div, noticeProps);
 | 
			
		||||
                    } else {
 | 
			
		||||
                      hookRefs.delete(key);
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
              <Notice {...noticeProps} class={classNames(noticeProps.class, props.hashId)}>
 | 
			
		||||
                {typeof content === 'function' ? content({ prefixCls }) : content}
 | 
			
		||||
              </Notice>
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
        const className = {
 | 
			
		||||
          [prefixCls]: 1,
 | 
			
		||||
          [`${prefixCls}-${placement}`]: 1,
 | 
			
		||||
          [attrs.class as string]: !!attrs.class,
 | 
			
		||||
          [props.hashId]: true,
 | 
			
		||||
          [classes]: !!classes,
 | 
			
		||||
        };
 | 
			
		||||
        function onAfterLeave() {
 | 
			
		||||
          if (noticesForPlacement.length > 0) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          Reflect.deleteProperty(placements.value, placement);
 | 
			
		||||
          props.onAllRemoved?.();
 | 
			
		||||
        }
 | 
			
		||||
        return (
 | 
			
		||||
          <div
 | 
			
		||||
            key={placement}
 | 
			
		||||
            class={className}
 | 
			
		||||
            style={
 | 
			
		||||
              (attrs.style as CSSProperties) ||
 | 
			
		||||
              styles || {
 | 
			
		||||
                top: '65px',
 | 
			
		||||
                left: '50%',
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            <TransitionGroup tag="div" {...transitionProps.value} onAfterLeave={onAfterLeave}>
 | 
			
		||||
              {noticeNodesForPlacement}
 | 
			
		||||
            </TransitionGroup>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
      return <Portal getContainer={props.getContainer}>{noticeNodes}</Portal>;
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Notification;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
import { getTransitionGroupProps } from '../_util/transition';
 | 
			
		||||
import type { Key } from '../_util/type';
 | 
			
		||||
import type { Key, VueNode } from '../_util/type';
 | 
			
		||||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
  shallowRef,
 | 
			
		||||
  createVNode,
 | 
			
		||||
  computed,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +34,14 @@ export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKe
 | 
			
		|||
  style?: CSSProperties;
 | 
			
		||||
  class?: String;
 | 
			
		||||
}
 | 
			
		||||
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
 | 
			
		||||
 | 
			
		||||
export interface OpenConfig extends NoticeProps {
 | 
			
		||||
  key: Key;
 | 
			
		||||
  placement?: Placement;
 | 
			
		||||
  content?: VueNode;
 | 
			
		||||
  duration?: number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type NoticeFunc = (noticeProps: NoticeContent) => void;
 | 
			
		||||
export type HolderReadyCallback = (
 | 
			
		||||
| 
						 | 
				
			
			@ -220,7 +229,7 @@ Notification.newInstance = function newNotificationInstance(properties, callback
 | 
			
		|||
    compatConfig: { MODE: 3 },
 | 
			
		||||
    name: 'NotificationWrapper',
 | 
			
		||||
    setup(_props, { attrs }) {
 | 
			
		||||
      const notiRef = ref();
 | 
			
		||||
      const notiRef = shallowRef();
 | 
			
		||||
      const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls));
 | 
			
		||||
      const [, hashId] = useStyle(prefixCls);
 | 
			
		||||
      onMounted(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
// based on rc-notification 4.5.7
 | 
			
		||||
import Notification from './Notification';
 | 
			
		||||
 | 
			
		||||
import useNotification from './useNotification';
 | 
			
		||||
import type { NotificationAPI } from './useNotification';
 | 
			
		||||
export { useNotification, NotificationAPI };
 | 
			
		||||
export default Notification;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,184 @@
 | 
			
		|||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import { shallowRef, watch, ref, computed } from 'vue';
 | 
			
		||||
import HookNotification, { getUuid } from './HookNotification';
 | 
			
		||||
import type { NotificationInstance, OpenConfig, Placement } from './Notification';
 | 
			
		||||
import type { CSSMotionProps } from '../_util/transition';
 | 
			
		||||
import type { Key, VueNode } from '../_util/type';
 | 
			
		||||
import type { HolderReadyCallback, NoticeContent } from './HookNotification';
 | 
			
		||||
 | 
			
		||||
const defaultGetContainer = () => document.body;
 | 
			
		||||
 | 
			
		||||
type OptionalConfig = Partial<OpenConfig>;
 | 
			
		||||
 | 
			
		||||
export interface NotificationConfig {
 | 
			
		||||
  prefixCls?: string;
 | 
			
		||||
  /** Customize container. It will repeat call which means you should return same container element. */
 | 
			
		||||
  getContainer?: () => HTMLElement;
 | 
			
		||||
  motion?: CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
 | 
			
		||||
  closeIcon?: VueNode;
 | 
			
		||||
  closable?: boolean;
 | 
			
		||||
  maxCount?: number;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
  /** @private. Config for notification holder style. Safe to remove if refactor */
 | 
			
		||||
  getClassName?: (placement?: Placement) => string;
 | 
			
		||||
  /** @private. Config for notification holder style. Safe to remove if refactor */
 | 
			
		||||
  getStyles?: (placement?: Placement) => CSSProperties;
 | 
			
		||||
  /** @private Trigger when all the notification closed. */
 | 
			
		||||
  onAllRemoved?: VoidFunction;
 | 
			
		||||
  hashId?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NotificationAPI {
 | 
			
		||||
  open: (config: OptionalConfig) => void;
 | 
			
		||||
  close: (key: Key) => void;
 | 
			
		||||
  destroy: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface OpenTask {
 | 
			
		||||
  type: 'open';
 | 
			
		||||
  config: OpenConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CloseTask {
 | 
			
		||||
  type: 'close';
 | 
			
		||||
  key: Key;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DestroyTask {
 | 
			
		||||
  type: 'destroy';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Task = OpenTask | CloseTask | DestroyTask;
 | 
			
		||||
 | 
			
		||||
let uniqueKey = 0;
 | 
			
		||||
 | 
			
		||||
function mergeConfig<T>(...objList: Partial<T>[]): T {
 | 
			
		||||
  const clone: T = {} as T;
 | 
			
		||||
 | 
			
		||||
  objList.forEach(obj => {
 | 
			
		||||
    if (obj) {
 | 
			
		||||
      Object.keys(obj).forEach(key => {
 | 
			
		||||
        const val = obj[key];
 | 
			
		||||
 | 
			
		||||
        if (val !== undefined) {
 | 
			
		||||
          clone[key] = val;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return clone;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useNotification(rootConfig: NotificationConfig = {}) {
 | 
			
		||||
  const {
 | 
			
		||||
    getContainer = defaultGetContainer,
 | 
			
		||||
    motion,
 | 
			
		||||
    prefixCls,
 | 
			
		||||
    maxCount,
 | 
			
		||||
    getClassName,
 | 
			
		||||
    getStyles,
 | 
			
		||||
    onAllRemoved,
 | 
			
		||||
    ...shareConfig
 | 
			
		||||
  } = rootConfig;
 | 
			
		||||
 | 
			
		||||
  const notices = ref([]);
 | 
			
		||||
  const notificationsRef = shallowRef<NotificationInstance>();
 | 
			
		||||
  const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
 | 
			
		||||
    const key = originNotice.key || getUuid();
 | 
			
		||||
    const notice: NoticeContent & { key: Key; userPassKey?: Key } = {
 | 
			
		||||
      ...originNotice,
 | 
			
		||||
      key,
 | 
			
		||||
    };
 | 
			
		||||
    const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key);
 | 
			
		||||
    const updatedNotices = notices.value.concat();
 | 
			
		||||
    if (noticeIndex !== -1) {
 | 
			
		||||
      updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (maxCount && notices.value.length >= maxCount) {
 | 
			
		||||
        notice.key = updatedNotices[0].notice.key as Key;
 | 
			
		||||
        notice.updateMark = getUuid();
 | 
			
		||||
        notice.userPassKey = key;
 | 
			
		||||
        updatedNotices.shift();
 | 
			
		||||
      }
 | 
			
		||||
      updatedNotices.push({ notice, holderCallback } as any);
 | 
			
		||||
    }
 | 
			
		||||
    notices.value = updatedNotices;
 | 
			
		||||
  };
 | 
			
		||||
  const removeNotice = (removeKey: Key) => {
 | 
			
		||||
    notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => {
 | 
			
		||||
      const mergedKey = userPassKey || key;
 | 
			
		||||
      return mergedKey !== removeKey;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const destroy = () => {
 | 
			
		||||
    notices.value = [];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const contextHolder = computed(() => (
 | 
			
		||||
    <HookNotification
 | 
			
		||||
      ref={notificationsRef}
 | 
			
		||||
      prefixCls={prefixCls}
 | 
			
		||||
      maxCount={maxCount}
 | 
			
		||||
      notices={notices.value}
 | 
			
		||||
      remove={removeNotice}
 | 
			
		||||
      getClassName={getClassName}
 | 
			
		||||
      getStyles={getStyles}
 | 
			
		||||
      animation={motion}
 | 
			
		||||
      hashId={rootConfig.hashId}
 | 
			
		||||
      onAllRemoved={onAllRemoved}
 | 
			
		||||
      getContainer={getContainer}
 | 
			
		||||
    ></HookNotification>
 | 
			
		||||
  ));
 | 
			
		||||
 | 
			
		||||
  const taskQueue = ref([] as Task[]);
 | 
			
		||||
  // ========================= Refs =========================
 | 
			
		||||
  const api = computed(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      open: (config: OpenConfig) => {
 | 
			
		||||
        const mergedConfig = mergeConfig(shareConfig, config);
 | 
			
		||||
        //@ts-ignore
 | 
			
		||||
        if (mergedConfig.key === null || mergedConfig.key === undefined) {
 | 
			
		||||
          //@ts-ignore
 | 
			
		||||
          mergedConfig.key = `vc-notification-${uniqueKey}`;
 | 
			
		||||
          uniqueKey += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        taskQueue.value = [...taskQueue.value, { type: 'open', config: mergedConfig as any }];
 | 
			
		||||
      },
 | 
			
		||||
      close: key => {
 | 
			
		||||
        taskQueue.value = [...taskQueue.value, { type: 'close', key }];
 | 
			
		||||
      },
 | 
			
		||||
      destroy: () => {
 | 
			
		||||
        taskQueue.value = [...taskQueue.value, { type: 'destroy' }];
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // ======================== Effect ========================
 | 
			
		||||
  watch(taskQueue, () => {
 | 
			
		||||
    // Flush task when node ready
 | 
			
		||||
    if (taskQueue.value.length) {
 | 
			
		||||
      taskQueue.value.forEach(task => {
 | 
			
		||||
        switch (task.type) {
 | 
			
		||||
          case 'open':
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            add(task.config);
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          case 'close':
 | 
			
		||||
            removeNotice(task.key);
 | 
			
		||||
            break;
 | 
			
		||||
          case 'destroy':
 | 
			
		||||
            destroy();
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      taskQueue.value = [];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // ======================== Return ========================
 | 
			
		||||
  return [api.value, () => contextHolder.value] as const;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue