feat(export): add open-chat window from session list

This commit is contained in:
tisonhuang
2026-03-04 18:29:41 +08:00
parent cf7190aaec
commit 926ca72331
7 changed files with 205 additions and 38 deletions

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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)}

View File

@@ -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>