实现完美的浮动通知系统
- 添加自定义通知组件替代阻塞式alert - 浮动定位不影响页面布局,宽度自适应内容 - 支持成功/错误两种样式,渐变背景+阴影效果 - 实现完整的淡入淡出动画,原地显示隐藏 - 重启提示显示4秒,普通操作反馈2-3秒 - 智能定时器管理,支持动画完成后清理 用户体验:切换供应商后优雅提示"请重启Claude Code终端以生效"
This commit is contained in:
@@ -89,3 +89,63 @@
|
|||||||
.browse-btn:hover {
|
.browse-btn:hover {
|
||||||
background: #2980b9;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { Provider } from '../shared/types'
|
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'
|
||||||
@@ -11,6 +11,31 @@ function App() {
|
|||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||||
const [configPath, setConfigPath] = useState<string>('')
|
const [configPath, setConfigPath] = useState<string>('')
|
||||||
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 [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(() => {
|
useEffect(() => {
|
||||||
@@ -18,6 +43,15 @@ function App() {
|
|||||||
loadConfigPath()
|
loadConfigPath()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
const loadedProviders = await window.electronAPI.getProviders()
|
const loadedProviders = await window.electronAPI.getProviders()
|
||||||
@@ -58,10 +92,10 @@ function App() {
|
|||||||
const success = await window.electronAPI.switchProvider(id)
|
const success = await window.electronAPI.switchProvider(id)
|
||||||
if (success) {
|
if (success) {
|
||||||
setCurrentProviderId(id)
|
setCurrentProviderId(id)
|
||||||
// 移除阻塞式alert
|
// 显示重启提示,时间更长
|
||||||
console.log('供应商切换成功')
|
showNotification('切换成功!请重启 Claude Code 终端以生效', 'success', 4000)
|
||||||
} else {
|
} else {
|
||||||
console.error('切换失败,请检查配置')
|
showNotification('切换失败,请检查配置', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,17 +104,12 @@ function App() {
|
|||||||
await window.electronAPI.updateProvider(provider)
|
await window.electronAPI.updateProvider(provider)
|
||||||
await loadProviders()
|
await loadProviders()
|
||||||
setEditingProviderId(null)
|
setEditingProviderId(null)
|
||||||
// 移除阻塞式alert,避免焦点管理问题
|
// 显示编辑成功提示,时间较短
|
||||||
setTimeout(() => {
|
showNotification('供应商配置已保存', 'success', 2000)
|
||||||
console.log('供应商更新成功')
|
|
||||||
}, 100)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新供应商失败:', error)
|
console.error('更新供应商失败:', error)
|
||||||
setEditingProviderId(null)
|
setEditingProviderId(null)
|
||||||
// 错误情况下也避免alert
|
showNotification('保存失败,请重试', 'error')
|
||||||
setTimeout(() => {
|
|
||||||
console.error('保存失败,请重试')
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +135,14 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
|
<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
|
<ProviderList
|
||||||
providers={providers}
|
providers={providers}
|
||||||
currentProviderId={currentProviderId}
|
currentProviderId={currentProviderId}
|
||||||
@@ -113,6 +150,7 @@ function App() {
|
|||||||
onDelete={handleDeleteProvider}
|
onDelete={handleDeleteProvider}
|
||||||
onEdit={setEditingProviderId}
|
onEdit={setEditingProviderId}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{configPath && (
|
{configPath && (
|
||||||
<div className="config-path">
|
<div className="config-path">
|
||||||
|
|||||||
Reference in New Issue
Block a user