实现完美的浮动通知系统

- 添加自定义通知组件替代阻塞式alert
- 浮动定位不影响页面布局,宽度自适应内容
- 支持成功/错误两种样式,渐变背景+阴影效果
- 实现完整的淡入淡出动画,原地显示隐藏
- 重启提示显示4秒,普通操作反馈2-3秒
- 智能定时器管理,支持动画完成后清理

用户体验:切换供应商后优雅提示"请重启Claude Code终端以生效"
This commit is contained in:
farion1231
2025-08-06 16:16:09 +08:00
parent 6c7d4c158f
commit e87f206905
2 changed files with 111 additions and 13 deletions

View File

@@ -89,3 +89,63 @@
.browse-btn:hover {
background: #2980b9;
}
/* 供应商列表区域 - 相对定位容器 */
.provider-section {
position: relative;
}
/* 浮动通知 - 绝对定位,不占据空间 */
.notification-floating {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
width: fit-content;
white-space: nowrap;
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
.fade-out {
animation: fadeOut 0.3s ease-out;
}
.notification-success {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: white;
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3);
}
.notification-error {
background: linear-gradient(135deg, #e74c3c 0%, #ec7063 100%);
color: white;
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Provider } from '../shared/types'
import ProviderList from './components/ProviderList'
import AddProviderModal from './components/AddProviderModal'
@@ -11,6 +11,31 @@ function App() {
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [configPath, setConfigPath] = useState<string>('')
const [editingProviderId, setEditingProviderId] = useState<string | null>(null)
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
const [isNotificationVisible, setIsNotificationVisible] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// 设置通知的辅助函数
const showNotification = (message: string, type: 'success' | 'error', duration = 3000) => {
// 清除之前的定时器
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// 立即显示通知
setNotification({ message, type })
setIsNotificationVisible(true)
// 设置淡出定时器
timeoutRef.current = setTimeout(() => {
setIsNotificationVisible(false)
// 等待淡出动画完成后清除通知
setTimeout(() => {
setNotification(null)
timeoutRef.current = null
}, 300) // 与CSS动画时间匹配
}, duration)
}
// 加载供应商列表
useEffect(() => {
@@ -18,6 +43,15 @@ function App() {
loadConfigPath()
}, [])
// 清理定时器
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
const loadProviders = async () => {
const loadedProviders = await window.electronAPI.getProviders()
@@ -58,10 +92,10 @@ function App() {
const success = await window.electronAPI.switchProvider(id)
if (success) {
setCurrentProviderId(id)
// 移除阻塞式alert
console.log('供应商切换成功')
// 显示重启提示,时间更长
showNotification('切换成功!请重启 Claude Code 终端以生效', 'success', 4000)
} else {
console.error('切换失败,请检查配置')
showNotification('切换失败,请检查配置', 'error')
}
}
@@ -70,17 +104,12 @@ function App() {
await window.electronAPI.updateProvider(provider)
await loadProviders()
setEditingProviderId(null)
// 移除阻塞式alert避免焦点管理问题
setTimeout(() => {
console.log('供应商更新成功')
}, 100)
// 显示编辑成功提示,时间较短
showNotification('供应商配置已保存', 'success', 2000)
} catch (error) {
console.error('更新供应商失败:', error)
setEditingProviderId(null)
// 错误情况下也避免alert
setTimeout(() => {
console.error('保存失败,请重试')
}, 100)
showNotification('保存失败,请重试', 'error')
}
}
@@ -106,13 +135,22 @@ function App() {
</header>
<main className="app-main">
<ProviderList
<div className="provider-section">
{/* 浮动通知组件 */}
{notification && (
<div className={`notification-floating ${notification.type === 'error' ? 'notification-error' : 'notification-success'} ${isNotificationVisible ? 'fade-in' : 'fade-out'}`}>
{notification.message}
</div>
)}
<ProviderList
providers={providers}
currentProviderId={currentProviderId}
onSwitch={handleSwitchProvider}
onDelete={handleDeleteProvider}
onEdit={setEditingProviderId}
/>
</div>
{configPath && (
<div className="config-path">