diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index e810bd5..be4d15f 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -1129,8 +1129,12 @@ color: var(--text-secondary); white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; flex: 1; + + .highlight { + color: var(--primary); + font-weight: 500; + } } .unread-badge { @@ -2761,7 +2765,7 @@ display: flex; align-items: center; gap: 6px; - font-size: 12px; + font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; @@ -4540,7 +4544,7 @@ display: flex; align-items: center; gap: 6px; - font-size: 12px; + font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; @@ -4634,116 +4638,239 @@ } // 会话内搜索栏 -.in-session-search-bar { +// 会话内搜索浮窗 +.in-session-search-popup { + position: absolute; + top: 60px; + right: 16px; + width: 360px; + max-height: 500px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + z-index: 1000; display: flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - flex-shrink: 0; + flex-direction: column; + overflow: hidden; - .in-session-search-icon { - color: var(--text-tertiary); + .in-session-search-header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); flex-shrink: 0; + + .search-icon { + color: var(--text-secondary); + flex-shrink: 0; + } + + .search-input { + flex: 1; + border: none; + background: transparent; + outline: none; + font-size: 14px; + color: var(--text-primary); + min-width: 0; + &::placeholder { color: var(--text-tertiary); } + } + + .spin { + animation: spin 1s linear infinite; + color: var(--primary); + flex-shrink: 0; + } + + .close-btn { + padding: 4px; + border-radius: 4px; + background: transparent; + border: none; + color: var(--text-tertiary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + flex-shrink: 0; + + &:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + } + } } - .in-session-search-input { - flex: 1; - border: none; - background: transparent; - outline: none; - font-size: 13px; - color: var(--text-primary); - min-width: 0; - &::placeholder { color: var(--text-tertiary); } - } - - .in-session-result-count { + .search-result-header { + padding: 6px 16px; font-size: 12px; - color: var(--text-tertiary); + color: var(--text-secondary); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); flex-shrink: 0; } + + .in-session-results { + flex: 1; + overflow-y: auto; + min-height: 0; + + .result-item { + display: flex; + align-items: flex-start; + padding: 12px 16px; + cursor: pointer; + gap: 10px; + border-bottom: 1px solid var(--border-color); + transition: background 0.15s; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: var(--bg-secondary); + } + + .result-header { + flex-shrink: 0; + + .result-info { + display: none; + } + } + + .result-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + + .result-sender { + font-size: 13px; + color: var(--text-primary); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .result-text { + font-size: 13px; + color: var(--text-secondary); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 1.4; + } + } + + .result-time { + font-size: 11px; + color: var(--text-tertiary); + flex-shrink: 0; + } + } + } + + .no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + color: var(--text-tertiary); + gap: 12px; + + p { + margin: 0; + font-size: 14px; + } + } } -.in-session-results { - max-height: 220px; - overflow-y: auto; - border-bottom: 1px solid var(--border-color); - background: var(--bg-primary); - flex-shrink: 0; - - .in-session-result-item { - display: flex; - flex-direction: column; - padding: 7px 12px; - cursor: pointer; - gap: 2px; - border-bottom: 1px solid var(--border-color); - - &:hover { background: var(--bg-secondary); } - - .result-sender { - font-size: 11px; - color: var(--text-tertiary); - } - .result-content { - font-size: 13px; - color: var(--text-primary); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .result-time { - font-size: 11px; - color: var(--text-tertiary); - align-self: flex-end; - } - } +// 搜索分类标题 +.search-section-header { + padding: 8px 16px; + font-size: 12px; + color: var(--text-tertiary); + background: var(--bg-secondary); + font-weight: 500; } // 全局消息搜索结果面板 .global-msg-search-results { - flex: 1; + max-height: 300px; overflow-y: auto; - background: var(--bg-primary); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); - .global-msg-searching, - .global-msg-empty { + .search-loading, + .no-results { display: flex; + flex-direction: column; align-items: center; - gap: 6px; - padding: 12px; - font-size: 13px; + justify-content: center; + gap: 8px; + padding: 24px; color: var(--text-tertiary); + font-size: 13px; } - .global-msg-result-item { - padding: 8px 12px; - cursor: pointer; - border-bottom: 1px solid var(--border-color); + .search-results-list { + .session-item { + display: flex; + padding: 12px 16px; + cursor: pointer; + border-bottom: 1px solid var(--border-color); + gap: 12px; + background: var(--bg-secondary); - &:hover { background: var(--bg-secondary); } + &:hover { + background: var(--bg-hover); + } - .global-msg-result-session { - font-size: 11px; - color: var(--text-accent, #07c160); - margin-bottom: 2px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .global-msg-result-content { - font-size: 13px; - color: var(--text-primary); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .global-msg-result-time { - font-size: 11px; - color: var(--text-tertiary); - margin-top: 2px; + .session-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; + + .session-top { + display: flex; + justify-content: space-between; + align-items: center; + + .session-name { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + } + } + + .session-preview { + font-size: 13px; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .highlight { + color: var(--primary); + font-weight: 500; + } + } + + .search-count { + font-size: 12px; + color: var(--primary); + } + } } } } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 6b8697f..12215e8 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -327,23 +327,99 @@ interface LoadMessagesOptions { switchRequestSeq?: number } -// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts import { avatarLoadQueue } from '../utils/AvatarLoadQueue' import { Avatar } from '../components/Avatar' // 头像组件 - 支持骨架屏加载和懒加载(优化:限制并发,使用 memo 避免不必要的重渲染) +// 高亮搜索关键词组件 +const HighlightText = React.memo(({ text, keyword }: { text: string; keyword: string }) => { + if (!keyword) return <>{text} + + const lowerText = text.toLowerCase() + const lowerKeyword = keyword.toLowerCase() + const matchIndex = lowerText.indexOf(lowerKeyword) + + if (matchIndex === -1) return <>{text} + + // 如果匹配位置在后面且文本过长,截断前面部分 + const maxLength = 50 + let displayText = text + + if (text.length > maxLength && matchIndex > 20) { + const start = Math.max(0, matchIndex - 15) + displayText = '...' + text.slice(start) + } + + const parts = displayText.split(new RegExp(`(${keyword})`, 'gi')) + return ( + <> + {parts.map((part, i) => + part.toLowerCase() === lowerKeyword ? + {part} : part + )} + + ) +}) + +const HighlightTextNoTruncate = React.memo(({ text, keyword }: { text: string; keyword: string }) => { + if (!keyword) return <>{text} + + const lowerText = text.toLowerCase() + const lowerKeyword = keyword.toLowerCase() + const matchIndex = lowerText.indexOf(lowerKeyword) + + if (matchIndex === -1) return <>{text} + + const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const matchEnd = matchIndex + keyword.length + const maxDisplayLength = 25 + + // 如果匹配位置不在开头,或文本过长,则居中显示 + if (matchIndex > 5 || text.length > maxDisplayLength) { + const start = Math.max(0, matchIndex - 8) + const end = Math.min(text.length, matchEnd + 15) + const prefix = start > 0 ? '...' : '' + const suffix = end < text.length ? '...' : '' + const middleText = text.slice(start, end) + + const parts = middleText.split(new RegExp(`(${escapedKeyword})`, 'gi')) + return ( + <> + {prefix} + {parts.map((part, i) => + part.toLowerCase() === lowerKeyword ? + {part} : part + )} + {suffix} + + ) + } + + const parts = text.split(new RegExp(`(${escapedKeyword})`, 'gi')) + return ( + <> + {parts.map((part, i) => + part.toLowerCase() === lowerKeyword ? + {part} : part + )} + + ) +}) + // 会话项组件(使用 memo 优化,避免不必要的重渲染) const SessionItem = React.memo(function SessionItem({ session, isActive, onSelect, - formatTime + formatTime, + searchKeyword }: { session: ChatSession isActive: boolean onSelect: (session: ChatSession) => void formatTime: (timestamp: number) => string + searchKeyword?: string }) { const timeText = useMemo(() => formatTime(session.lastTimestamp || session.sortTimestamp), @@ -375,6 +451,16 @@ const SessionItem = React.memo(function SessionItem({ ) } + // 根据匹配字段显示不同的 summary + const summaryContent = useMemo(() => { + if (session.matchedField === 'wxid') { + return wxid: + } else if (session.matchedField === 'alias' && session.alias) { + return 微信号: + } + return {session.summary || '暂无消息'} + }, [session.matchedField, session.username, session.alias, session.summary, searchKeyword]) + return (
- {session.displayName || session.username} + + {(() => { + const shouldHighlight = (session.matchedField as any) === 'name' && searchKeyword + if (shouldHighlight) { + console.log('高亮名字:', session.displayName, 'keyword:', searchKeyword) + } + return shouldHighlight ? ( + + ) : ( + session.displayName || session.username + ) + })()} + {timeText}
- {session.summary || '暂无消息'} + {summaryContent}
{session.isMuted && } {session.unreadCount > 0 && ( @@ -411,11 +509,14 @@ const SessionItem = React.memo(function SessionItem({ prevProps.session.displayName === nextProps.session.displayName && prevProps.session.avatarUrl === nextProps.session.avatarUrl && prevProps.session.summary === nextProps.session.summary && + prevProps.session.matchedField === nextProps.session.matchedField && + prevProps.session.alias === nextProps.session.alias && 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 + prevProps.isActive === nextProps.isActive && + prevProps.searchKeyword === nextProps.searchKeyword ) }) @@ -573,8 +674,9 @@ function ChatPage(props: ChatPageProps) { // 全局消息搜索 const [showGlobalMsgSearch, setShowGlobalMsgSearch] = useState(false) const [globalMsgQuery, setGlobalMsgQuery] = useState('') - const [globalMsgResults, setGlobalMsgResults] = useState([]) + const [globalMsgResults, setGlobalMsgResults] = useState([]) const [globalMsgSearching, setGlobalMsgSearching] = useState(false) + const pendingInSessionSearchRef = useRef<{ keyword: string; firstMsgTime: number; results: any[] } | null>(null) // 自定义删除确认对话框 const [deleteConfirm, setDeleteConfirm] = useState<{ @@ -2074,7 +2176,7 @@ function ChatPage(props: ChatPageProps) { } // 联系人信息更新队列(防抖批量更新,避免频繁重渲染) - const contactUpdateQueueRef = useRef>(new Map()) + const contactUpdateQueueRef = useRef>(new Map()) const contactUpdateTimerRef = useRef(null) const lastUpdateTimeRef = useRef(0) @@ -2108,12 +2210,14 @@ function ChatPage(props: ChatPageProps) { if (update) { const newDisplayName = update.displayName || session.displayName || session.username const newAvatarUrl = update.avatarUrl || session.avatarUrl - if (newDisplayName !== session.displayName || newAvatarUrl !== session.avatarUrl) { + const newAlias = update.alias || session.alias + if (newDisplayName !== session.displayName || newAvatarUrl !== session.avatarUrl || newAlias !== session.alias) { hasChanges = true return { ...session, displayName: newDisplayName, - avatarUrl: newAvatarUrl + avatarUrl: newAvatarUrl, + alias: newAlias } } } @@ -2145,7 +2249,7 @@ function ChatPage(props: ChatPageProps) { const dllStart = performance.now() const result = await window.electronAPI.chat.enrichSessionsContactInfo(usernames) as { success: boolean - contacts?: Record + contacts?: Record error?: string } const dllTime = performance.now() - dllStart @@ -2468,6 +2572,46 @@ function ChatPage(props: ChatPageProps) { pendingSessionLoadRef.current = null initialLoadRequestedSessionRef.current = null setIsSessionSwitching(false) + + // 处理从全局搜索跳转过来的情况 + if (pendingInSessionSearchRef.current) { + const { keyword, firstMsgTime, results } = pendingInSessionSearchRef.current + pendingInSessionSearchRef.current = null + + setShowInSessionSearch(true) + setInSessionQuery(keyword) + + if (firstMsgTime > 0) { + handleJumpDateSelect(new Date(firstMsgTime * 1000)) + } + + // 先获取完整消息,再补充发送者信息 + const sid = currentSessionId + if (sid) { + Promise.all( + results.map(async (msg: any) => { + try { + const full = await window.electronAPI.chat.getMessages(sid, 0, 3, msg.createTime, msg.createTime, false) + const found = full?.messages?.find((m: any) => m.localId === msg.localId) || msg + + if (found.senderUsername) { + const contact = await window.electronAPI.chat.getContact(found.senderUsername) + if (contact) { + found.senderDisplayName = contact.remark || contact.nickName || found.senderUsername + } + const avatarData = await window.electronAPI.chat.getContactAvatar(found.senderUsername) + if (avatarData?.avatarUrl) { + found.senderAvatarUrl = avatarData.avatarUrl + } + } + return found + } catch { + return msg + } + }) + ).then(enriched => setInSessionResults(enriched)) + } + } } } } @@ -2553,6 +2697,13 @@ function ChatPage(props: ChatPageProps) { const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1 sessionSwitchRequestSeqRef.current = switchRequestSeq + // 清空会话内搜索状态(除非是从全局搜索跳转过来) + if (!pendingInSessionSearchRef.current) { + setShowInSessionSearch(false) + setInSessionQuery('') + setInSessionResults([]) + } + setCurrentSession(normalizedSessionId, { preserveMessages: false }) setNoMessageTable(false) @@ -2561,6 +2712,45 @@ function ChatPage(props: ChatPageProps) { pendingSessionLoadRef.current = null initialLoadRequestedSessionRef.current = null setIsSessionSwitching(false) + + // 处理从全局搜索跳转过来的情况 + if (pendingInSessionSearchRef.current) { + const { keyword, firstMsgTime, results } = pendingInSessionSearchRef.current + pendingInSessionSearchRef.current = null + + setShowInSessionSearch(true) + setInSessionQuery(keyword) + + if (firstMsgTime > 0) { + handleJumpDateSelect(new Date(firstMsgTime * 1000)) + } + + // 先获取完整消息,再补充发送者信息 + const sid = normalizedSessionId + Promise.all( + results.map(async (msg: any) => { + try { + const full = await window.electronAPI.chat.getMessages(sid, 0, 3, msg.createTime, msg.createTime, false) + const found = full?.messages?.find((m: any) => m.localId === msg.localId) || msg + + if (found.senderUsername) { + const contact = await window.electronAPI.chat.getContact(found.senderUsername) + if (contact) { + found.senderDisplayName = contact.remark || contact.nickName || found.senderUsername + } + const avatarData = await window.electronAPI.chat.getContactAvatar(found.senderUsername) + if (avatarData?.avatarUrl) { + found.senderAvatarUrl = avatarData.avatarUrl + } + } + return found + } catch { + return msg + } + }) + ).then(enriched => setInSessionResults(enriched)) + } + void refreshSessionIncrementally(normalizedSessionId, switchRequestSeq) } else { pendingSessionLoadRef.current = normalizedSessionId @@ -2639,7 +2829,36 @@ function ChatPage(props: ChatPageProps) { try { const res = await window.electronAPI.chat.searchMessages(keyword.trim(), sid, 50, 0) if (gen !== inSessionSearchGenRef.current) return - setInSessionResults(res?.messages || []) + const messages = res?.messages || [] + + // 查询完整消息信息 + const enriched = await Promise.all( + messages.map(async (msg: any) => { + try { + const full = await window.electronAPI.chat.getMessages(sid, 0, 3, msg.createTime, msg.createTime, false) + const found: any = full?.messages?.find((m: any) => m.localId === msg.localId) + if (found && found.senderUsername) { + try { + const contact = await window.electronAPI.chat.getContact(found.senderUsername) + if (contact) { + found.senderDisplayName = contact.remark || contact.nickName || found.senderUsername + } + const avatarData = await window.electronAPI.chat.getContactAvatar(found.senderUsername) + if (avatarData?.avatarUrl) { + found.senderAvatarUrl = avatarData.avatarUrl + } + } catch {} + } + return found || msg + } catch { + return msg + } + }) + ) + + if (gen !== inSessionSearchGenRef.current) return + console.log('补充后:', enriched[0]) + setInSessionResults(enriched) } catch { if (gen !== inSessionSearchGenRef.current) return setInSessionResults([]) @@ -2683,9 +2902,15 @@ function ChatPage(props: ChatPageProps) { if (gen !== globalMsgSearchGenRef.current) return setGlobalMsgSearching(true) try { - const res = await window.electronAPI.chat.searchMessages(keyword.trim(), undefined, 50, 0) + const results: Array = [] + for (const session of sessions) { + const res = await window.electronAPI.chat.searchMessages(keyword.trim(), session.username, 10, 0) + if (res?.messages) { + results.push(...res.messages.map(msg => ({ ...msg, sessionId: session.username }))) + } + } if (gen !== globalMsgSearchGenRef.current) return - setGlobalMsgResults(res?.messages || []) + setGlobalMsgResults(results as any) } catch { if (gen !== globalMsgSearchGenRef.current) return setGlobalMsgResults([]) @@ -3119,11 +3344,24 @@ function ChatPage(props: ChatPageProps) { return } const lower = searchKeyword.toLowerCase() - setFilteredSessions(visible.filter(s => - s.displayName?.toLowerCase().includes(lower) || - s.username.toLowerCase().includes(lower) || - s.summary.toLowerCase().includes(lower) - )) + setFilteredSessions(visible.filter(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) + + if (matchedByUsername && !matchedByName && !matchedByAlias) { + s.matchedField = 'wxid' + } else if (matchedByAlias && !matchedByName && !matchedByUsername) { + s.matchedField = 'alias' + } else if (matchedByName && !matchedByUsername && !matchedByAlias) { + (s as any).matchedField = 'name' + console.log('设置 matchedField=name:', s.displayName) + } else { + s.matchedField = undefined + } + + return matchedByName || matchedByUsername || matchedByAlias + })) }, [sessions, searchKeyword, setFilteredSessions]) // 折叠群列表(独立计算,供折叠 panel 使用) @@ -3132,11 +3370,22 @@ function ChatPage(props: ChatPageProps) { const folded = sessions.filter(s => s.isFolded) if (!searchKeyword.trim() || !foldedView) return folded const lower = searchKeyword.toLowerCase() - return folded.filter(s => - s.displayName?.toLowerCase().includes(lower) || - s.username.toLowerCase().includes(lower) || - s.summary.toLowerCase().includes(lower) - ) + return folded.filter(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) + const matchedBySummary = s.summary.toLowerCase().includes(lower) + + if (matchedByUsername && !matchedByName && !matchedBySummary && !matchedByAlias) { + s.matchedField = 'wxid' + } else if (matchedByAlias && !matchedByName && !matchedBySummary && !matchedByUsername) { + s.matchedField = 'alias' + } else { + s.matchedField = undefined + } + + return matchedByName || matchedByUsername || matchedByAlias || matchedBySummary + }) }, [sessions, searchKeyword, foldedView]) const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0 @@ -4033,6 +4282,74 @@ function ChatPage(props: ChatPageProps) {
)} + {/* 全局消息搜索结果 */} + {globalMsgQuery && ( +
+ {globalMsgSearching ? ( +
+ + 搜索中... +
+ ) : globalMsgResults.length > 0 ? ( + <> +
聊天记录:
+
+ {Object.entries( + globalMsgResults.reduce((acc, msg) => { + const sessionId = (msg as any).sessionId || '未知'; + if (!acc[sessionId]) acc[sessionId] = []; + acc[sessionId].push(msg); + return acc; + }, {} as Record) + ).map(([sessionId, messages]) => { + const session = sessions.find(s => s.username === sessionId); + const firstMsg = messages[0]; + const count = messages.length; + return ( +
{ + if (session) { + pendingInSessionSearchRef.current = { + keyword: globalMsgQuery, + firstMsgTime: firstMsg.createTime || 0, + results: messages + }; + handleSelectSession(session); + } + }} + > + +
+
+ {session?.displayName || sessionId} +
+
+ +
+ {count > 1 && ( +
共 {count} 条相关聊天记录
+ )} +
+
+ ); + })} +
+ + ) : ( +
+ +

未找到相关消息

+
+ )} +
+ )} + {/* ... (previous content) ... */} {shouldShowSessionsSkeleton ? (
@@ -4048,66 +4365,39 @@ function ChatPage(props: ChatPageProps) {
) : (
- {/* 全局消息搜索结果 */} - {showGlobalMsgSearch ? ( -
- {globalMsgSearching && ( -
搜索中...
- )} - {!globalMsgSearching && globalMsgQuery && globalMsgResults.length === 0 && ( -
没有找到相关消息
- )} - {!globalMsgSearching && globalMsgResults.map((msg, i) => { - const sid = msg._session_id || msg.username || '' - const sessionObj = sessions.find(s => s.username === sid) - const sessionName = sessionObj?.displayName || sid || '未知会话' - const content = (msg.content || msg.strContent || msg.message_content || '').slice(0, 60) - const ts = msg.createTime || msg.create_time - const timeStr = ts ? new Date(ts * 1000).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '' - return ( -
{ - if (sessionObj) { - handleSelectSession(sessionObj) - handleCloseGlobalMsgSearch() - setSearchKeyword('') - } - }}> -
{sessionName}
-
{content}
-
{timeStr}
-
- ) - })} -
- ) : ( - <> {/* 普通会话列表 */}
{Array.isArray(filteredSessions) && filteredSessions.length > 0 ? ( -
{ - isScrollingRef.current = true - if (sessionScrollTimeoutRef.current) { - clearTimeout(sessionScrollTimeoutRef.current) - } - sessionScrollTimeoutRef.current = window.setTimeout(() => { - isScrollingRef.current = false - sessionScrollTimeoutRef.current = null - }, 200) - }} - > - {filteredSessions.map(session => ( + <> + {searchKeyword && ( +
联系人:
+ )} +
{ + isScrollingRef.current = true + if (sessionScrollTimeoutRef.current) { + clearTimeout(sessionScrollTimeoutRef.current) + } + sessionScrollTimeoutRef.current = window.setTimeout(() => { + isScrollingRef.current = false + sessionScrollTimeoutRef.current = null + }, 200) + }} + > + {filteredSessions.map(session => ( ))}
+ ) : (
@@ -4128,6 +4418,7 @@ function ChatPage(props: ChatPageProps) { isActive={currentSessionId === session.username} onSelect={handleSelectSession} formatTime={formatSessionTime} + searchKeyword={searchKeyword} /> ))}
@@ -4138,16 +4429,11 @@ function ChatPage(props: ChatPageProps) {
)}
- - )}
)} - -
)} - {/* 拖动调节条 */} {!standaloneSessionWindow &&
} {/* 右侧消息区域 */} @@ -4326,40 +4612,68 @@ function ChatPage(props: ChatPageProps) { onClose={() => setChatSnsTimelineTarget(null)} /> - {/* 会话内搜索栏 */} + {/* 会话内搜索浮窗 */} {showInSessionSearch && ( -
- - handleInSessionSearch(e.target.value)} - className="in-session-search-input" - /> - {inSessionSearching && } - {inSessionQuery && !inSessionSearching && ( - {inSessionResults.length} 条结果 - )} - -
- )} - {showInSessionSearch && inSessionResults.length > 0 && ( -
- {inSessionResults.map((msg, i) => ( -
{ - // 跳转到消息时间 - const ts = msg.createTime || msg.create_time - if (ts) handleJumpDateSelect(new Date(ts * 1000)) - }}> - {msg.displayName || msg.talker || msg.username || ''} - {(msg.content || msg.strContent || msg.message_content || '').slice(0, 80)} - {msg.createTime || msg.create_time ? new Date((msg.createTime || msg.create_time) * 1000).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''} +
+
+ + handleInSessionSearch(e.target.value)} + className="search-input" + /> + {inSessionSearching && } + +
+ {inSessionQuery && ( +
+ {inSessionSearching ? '搜索中...' : `找到 ${inSessionResults.length} 条结果`}
- ))} + )} + {inSessionResults.length > 0 && ( +
+ {inSessionResults.map((msg, i) => { + const msgData = msg as any; + const senderName = msgData.senderDisplayName || msgData.senderUsername || '未知'; + const senderAvatar = msgData.senderAvatarUrl; + + return ( +
{ + const ts = msg.createTime || msgData.create_time; + if (ts && currentSessionId) { + setCurrentOffset(0); + setJumpStartTime(0); + setJumpEndTime(0); + void loadMessages(currentSessionId, 0, ts - 1, ts + 1, false); + } + }}> +
+ +
+
+ {senderName} + {(msg.content || msgData.strContent || msgData.message_content || msgData.parsedContent || '').slice(0, 80)} +
+ {msg.createTime || msgData.create_time ? new Date((msg.createTime || msgData.create_time) * 1000).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''} +
+ ); + })} +
+ )} + {inSessionQuery && !inSessionSearching && inSessionResults.length === 0 && ( +
+ +

未找到相关消息

+
+ )}
)} +
{standaloneSessionWindow && standaloneLoadStage !== 'ready' && (
diff --git a/src/types/models.ts b/src/types/models.ts index 0af87b1..92d6506 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -15,6 +15,8 @@ export interface ChatSession { selfWxid?: string // Helper field to avoid extra API calls isFolded?: boolean // 是否已折叠进"折叠的群聊" isMuted?: boolean // 是否开启免打扰 + alias?: string // 微信号 + matchedField?: 'wxid' | 'alias' | 'name' // 搜索匹配的字段 } // 联系人 @@ -107,6 +109,9 @@ export interface Message { chatRecordTitle?: string // 聊天记录标题 chatRecordList?: ChatRecordItem[] // 聊天记录列表 _db_path?: string + // 运行时补充的发送者信息 + senderDisplayName?: string + senderAvatarUrl?: string } // 聊天记录项