解决系统弹窗阻塞问题:实现非阻塞用户交互
- 创建自定义ConfirmDialog组件替代系统confirm - 删除功能使用自定义确认对话框,避免界面阻塞 - 添加/编辑模态框使用内联错误提示替代alert - 优化用户体验:更详细的确认信息和统一的视觉风格
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
105
src/renderer/components/ConfirmDialog.css
Normal file
105
src/renderer/components/ConfirmDialog.css
Normal 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;
|
||||||
|
}
|
||||||
52
src/renderer/components/ConfirmDialog.tsx
Normal file
52
src/renderer/components/ConfirmDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user