useMessage.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /**
  2. * Copyright (c) 2013-Now https://jeesite.com All rights reserved.
  3. * No deletion without permission, or be held responsible to law.
  4. * @author Vben、ThinkGem
  5. */
  6. import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
  7. import { Modal, message as Message, notification } from 'ant-design-vue';
  8. import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons-vue';
  9. import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification';
  10. import { useI18n } from './useI18n';
  11. import { isString } from '@jeesite/core/utils/is';
  12. import { Icon } from '@jeesite/core/components/Icon';
  13. import type { ConfigOnClose, MessageType } from 'ant-design-vue/lib/message';
  14. import type { JointContent } from 'ant-design-vue/es/message/interface';
  15. export interface NotifyApi {
  16. info(config: NotificationArgsProps): void;
  17. success(config: NotificationArgsProps): void;
  18. error(config: NotificationArgsProps): void;
  19. warn(config: NotificationArgsProps): void;
  20. warning(config: NotificationArgsProps): void;
  21. open(args: NotificationArgsProps): void;
  22. close(key: string): void;
  23. config(options: ConfigProps): void;
  24. destroy(): void;
  25. }
  26. export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
  27. export declare type IconType = 'success' | 'info' | 'error' | 'warning';
  28. export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
  29. iconType: 'warning' | 'success' | 'error' | 'info';
  30. icon?: any;
  31. title?: any;
  32. content?: any;
  33. }
  34. export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
  35. interface ConfirmOptions {
  36. info: ModalFunc;
  37. success: ModalFunc;
  38. error: ModalFunc;
  39. warn: ModalFunc;
  40. warning: ModalFunc;
  41. }
  42. function getIcon(iconType: string) {
  43. if (iconType === 'warning') {
  44. return <InfoCircleFilled class="modal-icon-warning" />;
  45. } else if (iconType === 'success') {
  46. return <CheckCircleFilled class="modal-icon-success" />;
  47. } else if (iconType === 'info') {
  48. return <InfoCircleFilled class="modal-icon-info" />;
  49. } else {
  50. return <CloseCircleFilled class="modal-icon-error" />;
  51. }
  52. }
  53. function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
  54. if (isString(content)) {
  55. return <div innerHTML={`<div>${content as string}</div>`}></div>;
  56. } else {
  57. return content;
  58. }
  59. }
  60. /**
  61. * @description: Create confirmation box
  62. */
  63. function createConfirm(options: ModalOptionsEx) {
  64. const iconType = options.iconType || 'info';
  65. Reflect.deleteProperty(options, 'iconType');
  66. const opt: ModalFuncProps = {
  67. centered: true,
  68. icon: getIcon(iconType),
  69. ...options,
  70. content: renderContent(options),
  71. maskClosable: false,
  72. };
  73. return Modal.confirm(opt);
  74. }
  75. const getBaseOptions = () => {
  76. const { t } = useI18n();
  77. return {
  78. okText: t('common.okText'),
  79. centered: true,
  80. };
  81. };
  82. function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial | any {
  83. return {
  84. ...getBaseOptions(),
  85. ...options,
  86. content: renderContent(options),
  87. icon: getIcon(icon),
  88. maskClosable: false,
  89. };
  90. }
  91. function createSuccessModal(options: ModalOptionsPartial) {
  92. return Modal.success(createModalOptions(options, 'success'));
  93. }
  94. function createErrorModal(options: ModalOptionsPartial) {
  95. return Modal.error(createModalOptions(options, 'close'));
  96. }
  97. function createInfoModal(options: ModalOptionsPartial) {
  98. return Modal.info(createModalOptions(options, 'info'));
  99. }
  100. function createWarningModal(options: ModalOptionsPartial) {
  101. return Modal.warning(createModalOptions(options, 'warning'));
  102. }
  103. function contains(str, searchs) {
  104. if (typeof str === 'object' && str.content) {
  105. str = str.content;
  106. }
  107. if (typeof str === 'object' && str.props?.innerHTML) {
  108. str = str.props?.innerHTML;
  109. }
  110. if (typeof str === 'string' && searchs) {
  111. const ss = searchs.split(',');
  112. for (let i = 0; i < ss.length; i++) {
  113. if (str.indexOf(ss[i]) >= 0) {
  114. return true;
  115. }
  116. }
  117. }
  118. return false;
  119. }
  120. const MESSAGE_DEDUPE_INTERVAL = 1200;
  121. const GENERIC_SUCCESS_MESSAGE_KEY = '__jeesite_generic_success_message__';
  122. const recentMessageMap = new Map<string, number>();
  123. function getContentText(content: any): string {
  124. if (typeof content === 'string') return content;
  125. if (typeof content === 'number') return String(content);
  126. if (content?.content) return getContentText(content.content);
  127. if (content?.props?.innerHTML) return String(content.props.innerHTML);
  128. return '';
  129. }
  130. function normalizeMessageText(content: any) {
  131. return getContentText(content).trim();
  132. }
  133. function isGenericSuccessMessage(content: any) {
  134. return normalizeMessageText(content).replace(/[!!。.\s]/g, '') === '操作成功';
  135. }
  136. function shouldSkipDuplicateMessage(type: string, content: any) {
  137. const text = normalizeMessageText(content);
  138. if (!text) return false;
  139. const key = `${type}:${text}`;
  140. const now = Date.now();
  141. const lastTime = recentMessageMap.get(key) || 0;
  142. recentMessageMap.set(key, now);
  143. return now - lastTime < MESSAGE_DEDUPE_INTERVAL;
  144. }
  145. function buildGenericSuccessConfig(content: JointContent, duration?: number, onClose?: ConfigOnClose) {
  146. if (typeof content === 'object' && content && 'content' in content) {
  147. return {
  148. ...content,
  149. key: (content as any).key || GENERIC_SUCCESS_MESSAGE_KEY,
  150. duration: (content as any).duration ?? duration,
  151. onClose: (content as any).onClose ?? onClose,
  152. };
  153. }
  154. return {
  155. content,
  156. key: GENERIC_SUCCESS_MESSAGE_KEY,
  157. duration,
  158. onClose,
  159. };
  160. }
  161. function showDedupeMessage(type: string, content: JointContent, duration?: number, onClose?: ConfigOnClose) {
  162. if (shouldSkipDuplicateMessage(type, content)) {
  163. return () => {};
  164. }
  165. if (type === 'success') {
  166. if (isGenericSuccessMessage(content)) {
  167. return Message.success(buildGenericSuccessConfig(content, duration, onClose));
  168. }
  169. Message.destroy(GENERIC_SUCCESS_MESSAGE_KEY);
  170. }
  171. if (type === 'error') {
  172. return Message.error(content, duration, onClose);
  173. } else if (type === 'warning') {
  174. return Message.warning(content, duration, onClose);
  175. } else if (type === 'success') {
  176. return Message.success(content, duration, onClose);
  177. } else {
  178. return Message.info(content, duration, onClose);
  179. }
  180. }
  181. function createDedupeMessageApi() {
  182. return new Proxy(Message as any, {
  183. get(target: any, prop: string | symbol) {
  184. if (typeof prop === 'string' && ['success', 'error', 'warning', 'warn', 'info'].includes(prop)) {
  185. const type = prop === 'warn' ? 'warning' : prop;
  186. return (content: JointContent, duration?: number, onClose?: ConfigOnClose) =>
  187. showDedupeMessage(type, content, duration, onClose);
  188. }
  189. return target[prop];
  190. },
  191. }) as typeof Message;
  192. }
  193. function showMessageModal(options: ModalOptionsPartial | string, type?: string) {
  194. const { t } = useI18n();
  195. if (typeof options === 'string') options = { content: options };
  196. if (typeof options.content === 'string' && options.content.startsWith('posfull:')) {
  197. options.content = '<div class="modal-posfull-content">' + options.content.substring(8) + '</div>';
  198. options.width = '80%';
  199. }
  200. if (type === 'error' || contains(options.content, t('sys.message.error'))) {
  201. return Modal.error(createModalOptions(options, 'close'));
  202. } else if (type === 'warning' || contains(options.content, t('sys.message.warning'))) {
  203. return Modal.warning(createModalOptions(options, 'warning'));
  204. } else if (type === 'success' || contains(options.content, t('sys.message.success'))) {
  205. return Modal.success(createModalOptions(options, 'success'));
  206. } else {
  207. return Modal.info(createModalOptions(options, 'info'));
  208. }
  209. }
  210. function showMessage(content: JointContent, type?: string, duration?: number, onClose?: ConfigOnClose) {
  211. const { t } = useI18n();
  212. let messageRemove = () => {};
  213. if (typeof content === 'string' && content.startsWith('posfull:')) {
  214. content = {
  215. content: (
  216. <div style="position: relative;" onMouseout={() => setTimeout(messageRemove, 1000)}>
  217. <div style="position: absolute; right: -6px; top: -20px;" onClick={() => messageRemove()}>
  218. <Icon icon="i-ant-design:close-outlined" color="#555" class="cursor-pointer" />
  219. </div>
  220. <div class="text-left" innerHTML={content.substring(8)}></div>
  221. </div>
  222. ),
  223. duration,
  224. };
  225. }
  226. if (type === 'error' || contains(content, t('sys.message.error'))) {
  227. messageRemove = showDedupeMessage('error', content, duration, onClose);
  228. } else if (type === 'warning' || contains(content, t('sys.message.warning'))) {
  229. messageRemove = showDedupeMessage('warning', content, duration, onClose);
  230. } else if (type === 'success' || contains(content, t('sys.message.success')) || isGenericSuccessMessage(content)) {
  231. messageRemove = showDedupeMessage('success', content, duration, onClose);
  232. } else {
  233. messageRemove = showDedupeMessage('info', content, duration, onClose);
  234. }
  235. return messageRemove;
  236. }
  237. notification.config({
  238. placement: 'topRight',
  239. duration: 3,
  240. });
  241. /**
  242. * @description: message
  243. */
  244. export function useMessage() {
  245. return {
  246. createMessage: createDedupeMessageApi(),
  247. notification: notification as NotifyApi,
  248. createConfirm: createConfirm,
  249. createSuccessModal,
  250. createErrorModal,
  251. createInfoModal,
  252. createWarningModal,
  253. showMessageModal,
  254. showMessage,
  255. };
  256. }