diff --git a/electron/main.ts b/electron/main.ts index a13ab2b..a8ceee0 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -85,6 +85,7 @@ let agreementWindow: BrowserWindow | null = null let onboardingWindow: BrowserWindow | null = null // Splash 启动窗口 let splashWindow: BrowserWindow | null = null +const sessionChatWindows = new Map() const keyService = new KeyService() let mainWindowReady = false @@ -683,6 +684,87 @@ function createChatHistoryWindow(sessionId: string, messageId: number) { return win } +/** + * 创建独立的会话聊天窗口(单会话,复用聊天页右侧消息区域) + */ +function createSessionChatWindow(sessionId: string) { + const normalizedSessionId = String(sessionId || '').trim() + if (!normalizedSessionId) return null + + const existing = sessionChatWindows.get(normalizedSessionId) + if (existing && !existing.isDestroyed()) { + if (existing.isMinimized()) { + existing.restore() + } + existing.focus() + return existing + } + + const isDev = !!process.env.VITE_DEV_SERVER_URL + const iconPath = isDev + ? join(__dirname, '../public/icon.ico') + : join(process.resourcesPath, 'icon.ico') + + const isDark = nativeTheme.shouldUseDarkColors + + const win = new BrowserWindow({ + width: 980, + height: 820, + minWidth: 560, + minHeight: 560, + icon: iconPath, + webPreferences: { + preload: join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + }, + titleBarStyle: 'hidden', + titleBarOverlay: { + color: '#00000000', + symbolColor: isDark ? '#ffffff' : '#1a1a1a', + height: 40 + }, + show: false, + backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0', + autoHideMenuBar: true + }) + + const sessionParam = `sessionId=${encodeURIComponent(normalizedSessionId)}` + if (process.env.VITE_DEV_SERVER_URL) { + win.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/chat-window?${sessionParam}`) + + win.webContents.on('before-input-event', (event, input) => { + if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) { + if (win.webContents.isDevToolsOpened()) { + win.webContents.closeDevTools() + } else { + win.webContents.openDevTools() + } + event.preventDefault() + } + }) + } else { + win.loadFile(join(__dirname, '../dist/index.html'), { + hash: `/chat-window?${sessionParam}` + }) + } + + win.once('ready-to-show', () => { + win.show() + win.focus() + }) + + win.on('closed', () => { + const tracked = sessionChatWindows.get(normalizedSessionId) + if (tracked === win) { + sessionChatWindows.delete(normalizedSessionId) + } + }) + + sessionChatWindows.set(normalizedSessionId, win) + return win +} + function showMainWindow() { shouldShowMain = true if (mainWindowReady) { @@ -915,6 +997,12 @@ function registerIpcHandlers() { return true }) + // 打开会话聊天窗口(同会话仅保留一个窗口并聚焦) + ipcMain.handle('window:openSessionChatWindow', (_, sessionId: string) => { + const win = createSessionChatWindow(sessionId) + return Boolean(win) + }) + // 根据视频尺寸调整窗口大小 ipcMain.handle('window:resizeToFitVideo', (event, videoWidth: number, videoHeight: number) => { const win = BrowserWindow.fromWebContents(event.sender) diff --git a/electron/preload.ts b/electron/preload.ts index 5aa8731..60b5a9f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -98,7 +98,9 @@ contextBridge.exposeInMainWorld('electronAPI', { openImageViewerWindow: (imagePath: string, liveVideoPath?: string) => ipcRenderer.invoke('window:openImageViewerWindow', imagePath, liveVideoPath), openChatHistoryWindow: (sessionId: string, messageId: number) => - ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId) + ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId), + openSessionChatWindow: (sessionId: string) => + ipcRenderer.invoke('window:openSessionChatWindow', sessionId) }, // 数据库路径 diff --git a/src/App.tsx b/src/App.tsx index e9f634c..4bbd1e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -60,6 +60,7 @@ function App() { const isOnboardingWindow = location.pathname === '/onboarding-window' const isVideoPlayerWindow = location.pathname === '/video-player-window' const isChatHistoryWindow = location.pathname.startsWith('/chat-history/') + const isStandaloneChatWindow = location.pathname === '/chat-window' const isNotificationWindow = location.pathname === '/notification-window' const isExportRoute = location.pathname === '/export' const [themeHydrated, setThemeHydrated] = useState(false) @@ -361,6 +362,12 @@ function App() { return } + // 独立会话聊天窗口(仅显示聊天内容区域) + if (isStandaloneChatWindow) { + const sessionId = new URLSearchParams(location.search).get('sessionId') || '' + return + } + // 独立通知窗口 if (isNotificationWindow) { return diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index ed5de26..d3da281 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -202,7 +202,8 @@ function formatYmdHmDateTime(timestamp?: number): string { } interface ChatPageProps { - // 保留接口以备将来扩展 + standaloneSessionWindow?: boolean + initialSessionId?: string | null } @@ -403,7 +404,9 @@ const SessionItem = React.memo(function SessionItem({ -function ChatPage(_props: ChatPageProps) { +function ChatPage(props: ChatPageProps) { + const { standaloneSessionWindow = false, initialSessionId = null } = props + const normalizedInitialSessionId = useMemo(() => String(initialSessionId || '').trim(), [initialSessionId]) const navigate = useNavigate() const { @@ -2223,34 +2226,30 @@ function ChatPage(_props: ChatPageProps) { }, [appendMessages, getMessageKey]) // 选择会话 - const handleSelectSession = (session: ChatSession) => { - // 点击折叠群入口,切换到折叠群视图 - if (session.username.toLowerCase().includes('placeholder_foldgroup')) { - setFoldedView(true) - return - } - if (session.username === currentSessionId) return + const selectSessionById = useCallback((sessionId: string) => { + const normalizedSessionId = String(sessionId || '').trim() + if (!normalizedSessionId || normalizedSessionId === currentSessionId) return const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1 sessionSwitchRequestSeqRef.current = switchRequestSeq - setCurrentSession(session.username, { preserveMessages: false }) + setCurrentSession(normalizedSessionId, { preserveMessages: false }) setNoMessageTable(false) - const restoredFromWindowCache = restoreSessionWindowCache(session.username) + const restoredFromWindowCache = restoreSessionWindowCache(normalizedSessionId) if (restoredFromWindowCache) { pendingSessionLoadRef.current = null initialLoadRequestedSessionRef.current = null setIsSessionSwitching(false) - void refreshSessionIncrementally(session.username, switchRequestSeq) + void refreshSessionIncrementally(normalizedSessionId, switchRequestSeq) } else { - pendingSessionLoadRef.current = session.username - initialLoadRequestedSessionRef.current = session.username + pendingSessionLoadRef.current = normalizedSessionId + initialLoadRequestedSessionRef.current = normalizedSessionId setIsSessionSwitching(true) - void hydrateSessionPreview(session.username) + void hydrateSessionPreview(normalizedSessionId) setCurrentOffset(0) setJumpStartTime(0) setJumpEndTime(0) - void loadMessages(session.username, 0, 0, 0, false, { + void loadMessages(normalizedSessionId, 0, 0, 0, false, { preferLatestPath: true, deferGroupSenderWarmup: true, forceInitialLimit: 30, @@ -2269,6 +2268,23 @@ function ChatPage(_props: ChatPageProps) { setSessionDetail(null) setIsRefreshingDetailStats(false) setIsLoadingRelationStats(false) + }, [ + currentSessionId, + setCurrentSession, + restoreSessionWindowCache, + refreshSessionIncrementally, + hydrateSessionPreview, + loadMessages + ]) + + // 选择会话 + const handleSelectSession = (session: ChatSession) => { + // 点击折叠群入口,切换到折叠群视图 + if (session.username.toLowerCase().includes('placeholder_foldgroup')) { + setFoldedView(true) + return + } + selectSessionById(session.username) } // 搜索过滤 @@ -2698,6 +2714,21 @@ function ChatPage(_props: ChatPageProps) { const isCurrentSessionExporting = Boolean(currentSessionId && inProgressExportSessionIds.has(currentSessionId)) const isExportActionBusy = isCurrentSessionExporting || isPreparingExportDialog + useEffect(() => { + if (!standaloneSessionWindow) return + if (!normalizedInitialSessionId) return + if (!isConnected || isConnecting) return + if (currentSessionId === normalizedInitialSessionId) return + selectSessionById(normalizedInitialSessionId) + }, [ + standaloneSessionWindow, + normalizedInitialSessionId, + isConnected, + isConnecting, + currentSessionId, + selectSessionById + ]) + // 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称 useEffect(() => { if (!currentSessionId) return @@ -3264,7 +3295,7 @@ function ChatPage(_props: ChatPageProps) { } return ( -
+
{/* 自定义删除确认对话框 */} {deleteConfirm.show && (
@@ -3336,6 +3367,7 @@ function ChatPage(_props: ChatPageProps) {
)} {/* 左侧会话列表 */} + {!standaloneSessionWindow && (
+ )} {/* 拖动调节条 */} -
+ {!standaloneSessionWindow &&
} {/* 右侧消息区域 */}
@@ -4052,7 +4085,8 @@ function ChatPage(_props: ChatPageProps) { ) : (
-

选择一个会话开始查看聊天记录

+

{standaloneSessionWindow ? '会话加载中或暂无会话记录' : '选择一个会话开始查看聊天记录'}

+ {standaloneSessionWindow && connectionError &&

{connectionError}

}
)}
diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 2cd4d2f..799220a 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1115,8 +1115,8 @@ } .table-wrap { - --contacts-message-col-width: 420px; - --contacts-action-col-width: 172px; + --contacts-message-col-width: 120px; + --contacts-action-col-width: 280px; overflow: hidden; border: 1px solid var(--border-color); border-radius: 10px; @@ -1257,7 +1257,7 @@ .contacts-list-header-count { width: var(--contacts-message-col-width); - text-align: right; + text-align: center; flex-shrink: 0; white-space: nowrap; overflow: hidden; @@ -1382,17 +1382,16 @@ min-width: var(--contacts-message-col-width); display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; flex-shrink: 0; - text-align: right; + text-align: center; } .row-message-stats { width: 100%; display: flex; - justify-content: flex-end; - align-items: baseline; - gap: 8px; + justify-content: center; + align-items: center; white-space: nowrap; } @@ -1567,7 +1566,7 @@ } } -.row-action-cell { + .row-action-cell { display: flex; flex-direction: column; align-items: flex-end; @@ -1581,6 +1580,33 @@ gap: 6px; } + .row-open-chat-btn { + border: 1px solid color-mix(in srgb, var(--primary) 38%, var(--border-color)); + border-radius: 8px; + padding: 7px 10px; + background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); + color: var(--primary); + font-size: 12px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + white-space: nowrap; + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--primary) 18%, var(--bg-secondary)); + border-color: color-mix(in srgb, var(--primary) 55%, var(--border-color)); + } + + &:disabled { + opacity: 0.65; + cursor: not-allowed; + color: var(--text-tertiary); + border-color: var(--border-color); + background: var(--bg-secondary); + } + } + .row-detail-btn { border: 1px solid var(--border-color); border-radius: 8px; @@ -2351,8 +2377,8 @@ @media (max-width: 720px) { .table-wrap { - --contacts-message-col-width: 280px; - --contacts-action-col-width: 148px; + --contacts-message-col-width: 104px; + --contacts-action-col-width: 236px; } .table-wrap .contacts-list-header { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index f84805c..3a2c69a 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -4047,7 +4047,7 @@ function ExportPage() { <>
联系人(头像/名称/微信号) - 总消息 + 总消息数 操作
@@ -4091,16 +4091,25 @@ function ExportPage() {
- - 总消息 - - {messageCountLabel} - - + + {messageCountLabel} +
+