303 lines
7.7 KiB
TypeScript
303 lines
7.7 KiB
TypeScript
/**
|
|
* 全局提示模态框组件
|
|
*
|
|
* 用于显示成功、错误、警告和信息提示消息
|
|
* 支持自动关闭、手动关闭、自定义图标和操作按钮
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import messageModalStyles from '~/styles/components/message-modal.css?url';
|
|
|
|
// 消息类型
|
|
export type MessageType = 'success' | 'error' | 'warning' | 'info';
|
|
|
|
// 组件属性
|
|
interface MessageModalProps {
|
|
// 是否显示模态框
|
|
isOpen: boolean;
|
|
// 关闭模态框的回调
|
|
onClose: () => void;
|
|
// 模态框标题
|
|
title?: string;
|
|
// 模态框消息内容
|
|
message: string;
|
|
// 消息类型
|
|
type?: MessageType;
|
|
// 是否自动关闭
|
|
autoClose?: boolean;
|
|
// 自动关闭的延迟时间(毫秒)
|
|
autoCloseDelay?: number;
|
|
// 确认按钮文本
|
|
confirmText?: string;
|
|
// 确认按钮回调
|
|
onConfirm?: () => void;
|
|
// 取消按钮文本
|
|
cancelText?: string;
|
|
// 是否显示关闭按钮
|
|
showCloseButton?: boolean;
|
|
// 自定义图标
|
|
customIcon?: React.ReactNode;
|
|
// 自定义内容
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
// 默认自动关闭延迟
|
|
const DEFAULT_AUTO_CLOSE_DELAY = 3000;
|
|
|
|
// 导出样式
|
|
export function links() {
|
|
return [{ rel: 'stylesheet', href: messageModalStyles }];
|
|
}
|
|
|
|
export function MessageModal({
|
|
isOpen,
|
|
onClose,
|
|
title,
|
|
message,
|
|
type = 'info',
|
|
autoClose = false,
|
|
autoCloseDelay = DEFAULT_AUTO_CLOSE_DELAY,
|
|
confirmText = '确定',
|
|
onConfirm,
|
|
cancelText = '取消',
|
|
showCloseButton = true,
|
|
customIcon,
|
|
children
|
|
}: MessageModalProps) {
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
|
|
|
// 在客户端渲染时获取 portal 容器
|
|
useEffect(() => {
|
|
if (typeof document !== 'undefined') {
|
|
let element = document.getElementById('message-modal-portal');
|
|
if (!element) {
|
|
element = document.createElement('div');
|
|
element.id = 'message-modal-portal';
|
|
document.body.appendChild(element);
|
|
}
|
|
setPortalElement(element);
|
|
}
|
|
}, []);
|
|
|
|
// 处理关闭动画
|
|
const handleClose = useCallback(() => {
|
|
setIsClosing(true);
|
|
setTimeout(() => {
|
|
setIsClosing(false);
|
|
onClose();
|
|
}, 300); // 动画持续时间
|
|
}, [onClose]);
|
|
|
|
// 处理确认
|
|
const handleConfirm = useCallback(() => {
|
|
if (onConfirm) {
|
|
onConfirm();
|
|
}
|
|
handleClose();
|
|
}, [onConfirm, handleClose]);
|
|
|
|
// 自动关闭
|
|
useEffect(() => {
|
|
if (isOpen && autoClose) {
|
|
const timer = setTimeout(() => {
|
|
handleClose();
|
|
}, autoCloseDelay);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [isOpen, autoClose, autoCloseDelay, handleClose]);
|
|
|
|
// 关闭按钮键盘交互
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
handleClose();
|
|
}
|
|
}, [handleClose]);
|
|
|
|
// 渲染图标
|
|
const renderIcon = () => {
|
|
if (customIcon) {
|
|
return customIcon;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'success':
|
|
return <i className="ri-check-line message-modal-icon success"></i>;
|
|
case 'error':
|
|
return <i className="ri-close-circle-line message-modal-icon error"></i>;
|
|
case 'warning':
|
|
return <i className="ri-alert-line message-modal-icon warning"></i>;
|
|
case 'info':
|
|
default:
|
|
return <i className="ri-information-line message-modal-icon info"></i>;
|
|
}
|
|
};
|
|
|
|
// 如果模态框未打开,不渲染
|
|
if (!isOpen || !portalElement) {
|
|
return null;
|
|
}
|
|
|
|
// 使用 Portal 渲染模态框
|
|
return createPortal(
|
|
<div
|
|
className={`message-modal-overlay ${isClosing ? 'closing' : ''}`}
|
|
onClick={handleClose}
|
|
onKeyDown={handleKeyDown}
|
|
tabIndex={0}
|
|
role="button"
|
|
aria-label="关闭对话框"
|
|
>
|
|
<div
|
|
className={`message-modal message-modal-${type} ${isClosing ? 'closing' : ''}`}
|
|
onClick={(e) => e.stopPropagation()}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="message-modal-title"
|
|
aria-describedby="message-modal-content"
|
|
>
|
|
{showCloseButton && (
|
|
<button
|
|
className="message-modal-close"
|
|
onClick={handleClose}
|
|
aria-label="关闭"
|
|
>
|
|
<i className="ri-close-line"></i>
|
|
</button>
|
|
)}
|
|
|
|
<div className="message-modal-icon-wrapper">
|
|
{renderIcon()}
|
|
</div>
|
|
|
|
<div className="message-modal-content">
|
|
{title && (
|
|
<h3 id="message-modal-title" className="message-modal-title">
|
|
{title}
|
|
</h3>
|
|
)}
|
|
|
|
<div id="message-modal-content" className="message-modal-message">
|
|
{message}
|
|
</div>
|
|
|
|
{children && (
|
|
<div className="message-modal-custom-content">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="message-modal-actions">
|
|
{onConfirm && (
|
|
<>
|
|
<button
|
|
className="message-modal-button primary"
|
|
onClick={handleConfirm}
|
|
>
|
|
{confirmText}
|
|
</button>
|
|
<button
|
|
className="message-modal-button"
|
|
onClick={handleClose}
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{!onConfirm && (
|
|
<button
|
|
className="message-modal-button primary"
|
|
onClick={handleClose}
|
|
>
|
|
{confirmText}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
portalElement
|
|
);
|
|
}
|
|
|
|
// 创建全局消息服务
|
|
type ShowMessageOptions = Omit<MessageModalProps, 'isOpen' | 'onClose'>;
|
|
|
|
class MessageService {
|
|
private static instance: MessageService;
|
|
private showModal: ((options: ShowMessageOptions) => void) | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): MessageService {
|
|
if (!MessageService.instance) {
|
|
MessageService.instance = new MessageService();
|
|
}
|
|
return MessageService.instance;
|
|
}
|
|
|
|
registerShowModal(showFn: (options: ShowMessageOptions) => void): void {
|
|
this.showModal = showFn;
|
|
}
|
|
|
|
success(message: string, options?: Partial<ShowMessageOptions>): void {
|
|
this.show({ ...options, message, type: 'success' });
|
|
}
|
|
|
|
error(message: string, options?: Partial<ShowMessageOptions>): void {
|
|
this.show({ ...options, message, type: 'error' });
|
|
}
|
|
|
|
warning(message: string, options?: Partial<ShowMessageOptions>): void {
|
|
this.show({ ...options, message, type: 'warning' });
|
|
}
|
|
|
|
info(message: string, options?: Partial<ShowMessageOptions>): void {
|
|
this.show({ ...options, message, type: 'info' });
|
|
}
|
|
|
|
show(options: ShowMessageOptions): void {
|
|
if (this.showModal) {
|
|
this.showModal(options);
|
|
} else {
|
|
console.error('MessageService: showModal is not registered');
|
|
}
|
|
}
|
|
}
|
|
|
|
export const messageService = MessageService.getInstance();
|
|
|
|
/**
|
|
* 全局消息模态框容器
|
|
* 用于在应用根组件中挂载消息模态框服务
|
|
*/
|
|
export function MessageModalProvider({ children }: { children: React.ReactNode }) {
|
|
const [messageOptions, setMessageOptions] = useState<ShowMessageOptions | null>(null);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
messageService.registerShowModal((options) => {
|
|
setMessageOptions(options);
|
|
setIsOpen(true);
|
|
});
|
|
}, []);
|
|
|
|
const handleClose = () => {
|
|
setIsOpen(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{children}
|
|
{messageOptions && (
|
|
<MessageModal
|
|
{...messageOptions}
|
|
isOpen={isOpen}
|
|
onClose={handleClose}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|