From d6b95036b523cb8cec408a1681ef102edba8d058 Mon Sep 17 00:00:00 2001
From: cc <98377878+hicccc77@users.noreply.github.com>
Date: Sun, 15 Mar 2026 11:42:41 +0800
Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=AE=80=E5=8D=95=E7=9A=84?=
=?UTF-8?q?=E5=AE=89=E5=8D=93=E5=B2=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/services/config.ts | 2 +-
electron/windows/notificationWindow.ts | 8 +++++--
src/components/GlobalSessionMonitor.tsx | 4 ++--
src/components/NotificationToast.scss | 19 +++++++++++++++
src/components/NotificationToast.tsx | 2 +-
src/pages/ChatPage.tsx | 14 ++++-------
src/pages/NotificationWindow.scss | 32 +++++++++++++++++++++++++
src/pages/NotificationWindow.tsx | 14 ++++++++---
src/pages/SettingsPage.scss | 2 +-
src/pages/SettingsPage.tsx | 8 ++++---
10 files changed, 83 insertions(+), 22 deletions(-)
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)