From 8c1b04376953b3a1154691653b241be87da0eda9 Mon Sep 17 00:00:00 2001
From: cc <98377878+hicccc77@users.noreply.github.com>
Date: Sun, 15 Mar 2026 23:19:45 +0800
Subject: [PATCH] =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E7=A8=B3=E5=AE=9A=E7=9A=84?=
=?UTF-8?q?=E7=89=88=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/ChatPage.scss | 307 ++++++++++++++++-------
src/pages/ChatPage.tsx | 526 ++++++++++++++++++++++++++++++++--------
src/types/models.ts | 5 +
3 files changed, 642 insertions(+), 196 deletions(-)
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
)}
+ {/* 全局消息搜索结果 */}
+ {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
}
// 聊天记录项