feat(ui): 添加 ConfirmDialog 确认对话框组件
- 支持默认和危险两种样式变体 - 支持 ESC 键关闭和点击外部关闭 - 支持 loading 状态和自定义按钮文案 - 支持自定义内容区域
This commit is contained in:
parent
199772a95d
commit
749247affa
175
src/components/ui/ConfirmDialog.tsx
Normal file
175
src/components/ui/ConfirmDialog.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { X, AlertTriangle } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ConfirmDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
|
loading?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
confirmText = '确认',
|
||||||
|
cancelText = '取消',
|
||||||
|
variant = 'default',
|
||||||
|
loading = false,
|
||||||
|
children,
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// ESC 关闭
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isOpen && !loading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [isOpen, onClose, loading]);
|
||||||
|
|
||||||
|
// 点击外部关闭
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dialogRef.current &&
|
||||||
|
!dialogRef.current.contains(e.target as Node) &&
|
||||||
|
!loading
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [isOpen, onClose, loading]);
|
||||||
|
|
||||||
|
// 禁止背景滚动
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* 背景遮罩 */}
|
||||||
|
<div className="absolute inset-0 bg-black/50 animate-fade-in" />
|
||||||
|
|
||||||
|
{/* 对话框 */}
|
||||||
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
|
className={cn(
|
||||||
|
'relative z-10 w-full max-w-md mx-4',
|
||||||
|
'bg-[var(--color-bg-primary)] rounded-lg shadow-xl',
|
||||||
|
'animate-pop-up'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--color-border-light)]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{variant === 'danger' && (
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-red-500/10">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
className="p-1 rounded-md text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="px-5 py-4">
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div className="flex justify-end gap-3 px-5 py-4 border-t border-[var(--color-border-light)] bg-[var(--color-bg-secondary)]">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md hover:bg-[var(--color-bg-hover)] transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={loading}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50',
|
||||||
|
variant === 'danger'
|
||||||
|
? 'bg-red-600 text-white hover:bg-red-700'
|
||||||
|
: 'bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-dark)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-4 w-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
处理中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
confirmText
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user