diff --git a/electron/services/config.ts b/electron/services/config.ts index 6ec8270..689521b 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -47,7 +47,7 @@ interface ConfigSchema { // 通知 notificationEnabled: boolean - notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' + notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' notificationFilterMode: 'all' | 'whitelist' | 'blacklist' notificationFilterList: string[] wordCloudExcludeWords: string[] diff --git a/electron/windows/notificationWindow.ts b/electron/windows/notificationWindow.ts index 1642924..fc31ccc 100644 --- a/electron/windows/notificationWindow.ts +++ b/electron/windows/notificationWindow.ts @@ -132,7 +132,7 @@ async function showAndSend(win: BrowserWindow, data: any) { // 更新位置 const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize - const winWidth = 344 + const winWidth = position === 'top-center' ? 280 : 344 const winHeight = 114 const padding = 20 @@ -140,6 +140,10 @@ async function showAndSend(win: BrowserWindow, data: any) { let y = 0 switch (position) { + case 'top-center': + x = (screenWidth - winWidth) / 2 + y = padding + break case 'top-right': x = screenWidth - winWidth - padding y = padding @@ -166,7 +170,7 @@ async function showAndSend(win: BrowserWindow, data: any) { win.showInactive() // 显示但不聚焦 win.setAlwaysOnTop(true, 'screen-saver') // 最高层级 - win.webContents.send('notification:show', data) + win.webContents.send('notification:show', { ...data, position }) // 自动关闭计时器通常由渲染进程管理 // 渲染进程发送 'notification:close' 来隐藏窗口 diff --git a/src/components/GlobalSessionMonitor.tsx b/src/components/GlobalSessionMonitor.tsx index a1abf71..93c9a47 100644 --- a/src/components/GlobalSessionMonitor.tsx +++ b/src/components/GlobalSessionMonitor.tsx @@ -96,8 +96,8 @@ export function GlobalSessionMonitor() { if (!isCurrentSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) { // 这是新消息事件 - // 免打扰、折叠群、折叠入口不弹通知 - if (newSession.isMuted || newSession.isFolded) continue + // 折叠群、折叠入口不弹通知 + if (newSession.isFolded) continue if (newSession.username.toLowerCase().includes('placeholder_foldgroup')) continue // 1. 群聊过滤自己发送的消息 diff --git a/src/components/NotificationToast.scss b/src/components/NotificationToast.scss index a01ab73..57dc558 100644 --- a/src/components/NotificationToast.scss +++ b/src/components/NotificationToast.scss @@ -134,6 +134,25 @@ } } + &.top-center { + top: 24px; + left: 50%; + transform: translate(-50%, -20px) scale(0.95); + + &.visible { + transform: translate(-50%, 0) scale(1); + } + + // 灵动岛样式 + border-radius: 40px !important; + padding: 12px 16px; + box-shadow: 0 12px 48px rgba(0, 0, 0, 0.2); + + &.static { + border-radius: 40px !important; + } + } + &:hover { box-shadow: 0 12px 48px rgba(0, 0, 0, 0.16) !important; } diff --git a/src/components/NotificationToast.tsx b/src/components/NotificationToast.tsx index 886a878..f394f6c 100644 --- a/src/components/NotificationToast.tsx +++ b/src/components/NotificationToast.tsx @@ -18,7 +18,7 @@ interface NotificationToastProps { onClose: () => void onClick: (sessionId: string) => void duration?: number - position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' isStatic?: boolean initialVisible?: boolean } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 50e1534..871f415 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react' -import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture } from 'lucide-react' +import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, Users, FolderClosed, UserCheck, Crown, Aperture } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { createPortal } from 'react-dom' import { useChatStore } from '../stores/chatStore' @@ -377,7 +377,7 @@ const SessionItem = React.memo(function SessionItem({ return (
onSelect(session)} > {session.summary || '暂无消息'}
- {session.isMuted && } {session.unreadCount > 0 && ( - + {session.unreadCount > 99 ? '99+' : session.unreadCount} )} @@ -414,7 +413,6 @@ const SessionItem = React.memo(function SessionItem({ prevProps.session.unreadCount === nextProps.session.unreadCount && prevProps.session.lastTimestamp === nextProps.session.lastTimestamp && prevProps.session.sortTimestamp === nextProps.session.sortTimestamp && - prevProps.session.isMuted === nextProps.session.isMuted && prevProps.isActive === nextProps.isActive ) }) @@ -1898,16 +1896,14 @@ function ChatPage(props: ChatPageProps) { if (!status) return session const nextIsFolded = status.isFolded ?? session.isFolded - const nextIsMuted = status.isMuted ?? session.isMuted - if (nextIsFolded === session.isFolded && nextIsMuted === session.isMuted) { + if (nextIsFolded === session.isFolded) { return session } hasChanges = true return { ...session, - isFolded: nextIsFolded, - isMuted: nextIsMuted + isFolded: nextIsFolded } }) diff --git a/src/pages/NotificationWindow.scss b/src/pages/NotificationWindow.scss index 3e1515d..5c92a13 100644 --- a/src/pages/NotificationWindow.scss +++ b/src/pages/NotificationWindow.scss @@ -10,6 +10,18 @@ } } +@keyframes noti-enter-center { + 0% { + opacity: 0; + transform: translateY(-50px) scale(0.7); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + @keyframes noti-exit { 0% { opacity: 1; @@ -24,6 +36,18 @@ } } +@keyframes noti-exit-center { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + + 100% { + opacity: 0; + transform: translateY(-50px) scale(0.7); + } +} + body { // Ensure the body background is transparent to let the rounded corners show background: transparent; @@ -41,6 +65,10 @@ body { // New notification slides in animation: noti-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; will-change: transform, opacity; + + &.anim-center { + animation: noti-enter-center 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } } #notification-prev { @@ -51,4 +79,8 @@ body { // Ensure it stays behind z-index: 0 !important; + + &.anim-center { + animation: noti-exit-center 0.5s cubic-bezier(0.33, 1, 0.68, 1) forwards; + } } \ No newline at end of file diff --git a/src/pages/NotificationWindow.tsx b/src/pages/NotificationWindow.tsx index deb6616..62cdb72 100644 --- a/src/pages/NotificationWindow.tsx +++ b/src/pages/NotificationWindow.tsx @@ -6,8 +6,9 @@ import './NotificationWindow.scss' export default function NotificationWindow() { const [notification, setNotification] = useState(null) const [prevNotification, setPrevNotification] = useState(null) + const [position, setPosition] = useState('top-right') - // We need a ref to access the current notification inside the callback + // We need a ref to access the current notification inside the callback // without satisfying the dependency array which would recreate the listener // Actually, setNotification(prev => ...) pattern is better, but we need the VALUE of current to set as prev. // So we use setNotification callback: setNotification(current => { ... return newNode }) @@ -34,6 +35,11 @@ export default function NotificationWindow() { avatarUrl: data.avatarUrl } + // 获取位置配置 + if (data.position) { + setPosition(data.position) + } + // Set previous to current (ref) if (notificationRef.current) { setPrevNotification(notificationRef.current) @@ -117,6 +123,7 @@ export default function NotificationWindow() {
{ }} // No-op for background item onClick={() => { }} - position="top-right" + position={position as any} isStatic={true} initialVisible={true} /> @@ -143,6 +150,7 @@ export default function NotificationWindow() {
diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 04a5751..10b6730 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1,6 +1,6 @@ .settings-modal-overlay { position: fixed; - top: 41px; + top: 0; left: 0; right: 0; bottom: 0; diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index d963b14..db817e2 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -102,7 +102,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) const [notificationEnabled, setNotificationEnabled] = useState(true) - const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'>('top-right') + const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right') const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all') const [notificationFilterList, setNotificationFilterList] = useState([]) const [filterSearchKeyword, setFilterSearchKeyword] = useState('') @@ -1102,12 +1102,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {notificationPosition === 'top-right' ? '右上角' : notificationPosition === 'bottom-right' ? '右下角' : - notificationPosition === 'top-left' ? '左上角' : '左下角'} + notificationPosition === 'top-left' ? '左上角' : + notificationPosition === 'top-center' ? '中间上方' : '左下角'}
{[ + { value: 'top-center', label: '中间上方' }, { value: 'top-right', label: '右上角' }, { value: 'bottom-right', label: '右下角' }, { value: 'top-left', label: '左上角' }, @@ -1117,7 +1119,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { key={option.value} className={`custom-select-option ${notificationPosition === option.value ? 'selected' : ''}`} onClick={async () => { - const val = option.value as 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' + const val = option.value as 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' setNotificationPosition(val) setPositionDropdownOpen(false) await configService.setNotificationPosition(val)