解决系统弹窗阻塞问题:实现非阻塞用户交互

- 创建自定义ConfirmDialog组件替代系统confirm
- 删除功能使用自定义确认对话框,避免界面阻塞
- 添加/编辑模态框使用内联错误提示替代alert
- 优化用户体验:更详细的确认信息和统一的视觉风格
This commit is contained in:
farion1231
2025-08-06 16:29:52 +08:00
parent 8a9ec05d33
commit dbafab57cf
6 changed files with 209 additions and 6 deletions

View File

@@ -3,6 +3,7 @@ import { Provider } from '../shared/types'
import ProviderList from './components/ProviderList' import ProviderList from './components/ProviderList'
import AddProviderModal from './components/AddProviderModal' import AddProviderModal from './components/AddProviderModal'
import EditProviderModal from './components/EditProviderModal' import EditProviderModal from './components/EditProviderModal'
import { ConfirmDialog } from './components/ConfirmDialog'
import './App.css' import './App.css'
function App() { function App() {
@@ -13,6 +14,7 @@ function App() {
const [editingProviderId, setEditingProviderId] = useState<string | null>(null) const [editingProviderId, setEditingProviderId] = useState<string | null>(null)
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' } | null>(null) const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
const [isNotificationVisible, setIsNotificationVisible] = useState(false) const [isNotificationVisible, setIsNotificationVisible] = useState(false)
const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void } | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null) const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// 设置通知的辅助函数 // 设置通知的辅助函数
@@ -82,10 +84,18 @@ function App() {
} }
const handleDeleteProvider = async (id: string) => { const handleDeleteProvider = async (id: string) => {
if (confirm('确定要删除这个供应商吗?')) { const provider = providers[id]
setConfirmDialog({
isOpen: true,
title: '删除供应商',
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
onConfirm: async () => {
await window.electronAPI.deleteProvider(id) await window.electronAPI.deleteProvider(id)
await loadProviders() await loadProviders()
setConfirmDialog(null)
showNotification('供应商删除成功', 'success')
} }
})
} }
const handleSwitchProvider = async (id: string) => { const handleSwitchProvider = async (id: string) => {
@@ -180,6 +190,16 @@ function App() {
onClose={() => setEditingProviderId(null)} onClose={() => setEditingProviderId(null)}
/> />
)} )}
{confirmDialog && (
<ConfirmDialog
isOpen={confirmDialog.isOpen}
title={confirmDialog.title}
message={confirmDialog.message}
onConfirm={confirmDialog.onConfirm}
onCancel={() => setConfirmDialog(null)}
/>
)}
</div> </div>
) )
} }

View File

@@ -29,6 +29,16 @@
color: #2c3e50; color: #2c3e50;
} }
.error-message {
background: #fee;
color: #c33;
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
border: 1px solid #fcc;
font-size: 0.9rem;
}
.presets { .presets {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;

View File

@@ -16,12 +16,14 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
websiteUrl: '' websiteUrl: ''
}) })
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError('')
if (!formData.name || !formData.apiUrl || !formData.apiKey) { if (!formData.name || !formData.apiUrl || !formData.apiKey) {
alert('请填写所有必填字段') setError('请填写所有必填字段')
return return
} }
@@ -89,6 +91,12 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2></h2> <h2></h2>
{error && (
<div className="error-message">
{error}
</div>
)}
<div className="presets"> <div className="presets">
<label></label> <label></label>
<div className="preset-buttons"> <div className="preset-buttons">

View File

@@ -0,0 +1,105 @@
.confirm-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.confirm-dialog {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
min-width: 300px;
max-width: 400px;
animation: confirmSlideIn 0.2s ease-out;
}
@keyframes confirmSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.confirm-header {
padding: 1.5rem 1.5rem 1rem;
border-bottom: 1px solid #eee;
}
.confirm-header h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
font-weight: 600;
}
.confirm-content {
padding: 1rem 1.5rem;
}
.confirm-content p {
margin: 0;
color: #666;
line-height: 1.5;
}
.confirm-actions {
display: flex;
gap: 0.75rem;
padding: 1rem 1.5rem 1.5rem;
justify-content: flex-end;
}
.confirm-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s, transform 0.1s;
min-width: 70px;
}
.confirm-btn:hover {
transform: translateY(-1px);
}
.confirm-btn:active {
transform: translateY(0);
}
.cancel-btn {
background: #f8f9fa;
color: #6c757d;
border: 1px solid #dee2e6;
}
.cancel-btn:hover {
background: #e9ecef;
}
.confirm-btn-primary {
background: #dc3545;
color: white;
}
.confirm-btn-primary:hover {
background: #c82333;
}
.confirm-btn:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}

View File

@@ -0,0 +1,52 @@
import React from 'react';
import './ConfirmDialog.css';
interface ConfirmDialogProps {
isOpen: boolean;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
onCancel: () => void;
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
confirmText = '确定',
cancelText = '取消',
onConfirm,
onCancel
}) => {
if (!isOpen) return null;
return (
<div className="confirm-overlay">
<div className="confirm-dialog">
<div className="confirm-header">
<h3>{title}</h3>
</div>
<div className="confirm-content">
<p>{message}</p>
</div>
<div className="confirm-actions">
<button
className="confirm-btn cancel-btn"
onClick={onCancel}
autoFocus
>
{cancelText}
</button>
<button
className="confirm-btn confirm-btn-primary"
onClick={onConfirm}
>
{confirmText}
</button>
</div>
</div>
</div>
);
};

View File

@@ -17,6 +17,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
websiteUrl: provider.websiteUrl || '' websiteUrl: provider.websiteUrl || ''
}) })
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
@@ -29,9 +30,10 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError('')
if (!formData.name || !formData.apiUrl || !formData.apiKey) { if (!formData.name || !formData.apiUrl || !formData.apiKey) {
alert('请填写所有必填字段') setError('请填写所有必填字段')
return return
} }
@@ -79,6 +81,12 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2></h2> <h2></h2>
{error && (
<div className="error-message">
{error}
</div>
)}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label htmlFor="name"> *</label> <label htmlFor="name"> *</label>