mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 07:09:12 +08:00
feat(export): add open-chat window from session list
This commit is contained in:
@@ -85,6 +85,7 @@ let agreementWindow: BrowserWindow | null = null
|
||||
let onboardingWindow: BrowserWindow | null = null
|
||||
// Splash 启动窗口
|
||||
let splashWindow: BrowserWindow | null = null
|
||||
const sessionChatWindows = new Map<string, BrowserWindow>()
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
// 数据库路径
|
||||
|
||||
@@ -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 <ChatHistoryPage />
|
||||
}
|
||||
|
||||
// 独立会话聊天窗口(仅显示聊天内容区域)
|
||||
if (isStandaloneChatWindow) {
|
||||
const sessionId = new URLSearchParams(location.search).get('sessionId') || ''
|
||||
return <ChatPage standaloneSessionWindow initialSessionId={sessionId} />
|
||||
}
|
||||
|
||||
// 独立通知窗口
|
||||
if (isNotificationWindow) {
|
||||
return <NotificationWindow />
|
||||
|
||||
@@ -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 (
|
||||
<div className={`chat-page ${isResizing ? 'resizing' : ''}`}>
|
||||
<div className={`chat-page ${isResizing ? 'resizing' : ''} ${standaloneSessionWindow ? 'standalone session-only' : ''}`}>
|
||||
{/* 自定义删除确认对话框 */}
|
||||
{deleteConfirm.show && (
|
||||
<div className="delete-confirm-overlay">
|
||||
@@ -3336,6 +3367,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
</div>
|
||||
)}
|
||||
{/* 左侧会话列表 */}
|
||||
{!standaloneSessionWindow && (
|
||||
<div
|
||||
className="session-sidebar"
|
||||
ref={sidebarRef}
|
||||
@@ -3470,9 +3502,10 @@ function ChatPage(_props: ChatPageProps) {
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 拖动调节条 */}
|
||||
<div className="resize-handle" onMouseDown={handleResizeStart} />
|
||||
{!standaloneSessionWindow && <div className="resize-handle" onMouseDown={handleResizeStart} />}
|
||||
|
||||
{/* 右侧消息区域 */}
|
||||
<div className="message-area">
|
||||
@@ -4052,7 +4085,8 @@ function ChatPage(_props: ChatPageProps) {
|
||||
) : (
|
||||
<div className="empty-chat">
|
||||
<MessageSquare />
|
||||
<p>选择一个会话开始查看聊天记录</p>
|
||||
<p>{standaloneSessionWindow ? '会话加载中或暂无会话记录' : '选择一个会话开始查看聊天记录'}</p>
|
||||
{standaloneSessionWindow && connectionError && <p className="hint">{connectionError}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4047,7 +4047,7 @@ function ExportPage() {
|
||||
<>
|
||||
<div className="contacts-list-header">
|
||||
<span className="contacts-list-header-main">联系人(头像/名称/微信号)</span>
|
||||
<span className="contacts-list-header-count">总消息</span>
|
||||
<span className="contacts-list-header-count">总消息数</span>
|
||||
<span className="contacts-list-header-actions">操作</span>
|
||||
</div>
|
||||
<div className="contacts-list" ref={contactsListRef} onScroll={onContactsListScroll}>
|
||||
@@ -4091,16 +4091,25 @@ function ExportPage() {
|
||||
</div>
|
||||
<div className="row-message-count">
|
||||
<div className="row-message-stats">
|
||||
<span className="row-message-stat total">
|
||||
<span className="label">总消息</span>
|
||||
<strong className={`row-message-count-value ${typeof displayedMessageCount === 'number' ? '' : 'muted'}`}>
|
||||
{messageCountLabel}
|
||||
</strong>
|
||||
</span>
|
||||
<strong className={`row-message-count-value ${typeof displayedMessageCount === 'number' ? '' : 'muted'}`}>
|
||||
{messageCountLabel}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-action-cell">
|
||||
<div className="row-action-main">
|
||||
<button
|
||||
className="row-open-chat-btn"
|
||||
disabled={!canExport}
|
||||
title={canExport ? '在新窗口打开该会话' : '该联系人暂无会话记录'}
|
||||
onClick={() => {
|
||||
if (!canExport) return
|
||||
void window.electronAPI.window.openSessionChatWindow(contact.username)
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
打开对话
|
||||
</button>
|
||||
<button
|
||||
className={`row-detail-btn ${showSessionDetailPanel && sessionDetail?.wxid === contact.username ? 'active' : ''}`}
|
||||
onClick={() => openSessionDetail(contact.username)}
|
||||
|
||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -13,6 +13,7 @@ export interface ElectronAPI {
|
||||
resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise<void>
|
||||
openImageViewerWindow: (imagePath: string, liveVideoPath?: string) => Promise<void>
|
||||
openChatHistoryWindow: (sessionId: string, messageId: number) => Promise<boolean>
|
||||
openSessionChatWindow: (sessionId: string) => Promise<boolean>
|
||||
}
|
||||
config: {
|
||||
get: (key: string) => Promise<unknown>
|
||||
|
||||
Reference in New Issue
Block a user