diff --git a/electron/preload.ts b/electron/preload.ts index 2dcc561..8a0f823 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -218,6 +218,8 @@ contextBridge.exposeInMainWorld('electronAPI', { getContacts: () => ipcRenderer.invoke('chat:getContacts'), getMessage: (sessionId: string, localId: number) => ipcRenderer.invoke('chat:getMessage', sessionId, localId), + searchMessages: (keyword: string, sessionId?: string, limit?: number, offset?: number, beginTimestamp?: number, endTimestamp?: number) => + ipcRenderer.invoke('chat:searchMessages', keyword, sessionId, limit, offset, beginTimestamp, endTimestamp), onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => { ipcRenderer.on('wcdb-change', callback) return () => ipcRenderer.removeListener('wcdb-change', callback) diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index e00feb5..1025d7c 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -4630,3 +4630,126 @@ } } } + +// 会话内搜索栏 +.in-session-search-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; + + .in-session-search-icon { + color: var(--text-tertiary); + flex-shrink: 0; + } + + .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 { + font-size: 12px; + color: var(--text-tertiary); + flex-shrink: 0; + } +} + +.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; + } + } +} + +// 全局消息搜索结果面板 +.global-msg-search-results { + max-height: 320px; + overflow-y: auto; + background: var(--bg-primary); + border-top: 1px solid var(--border-color); + + .global-msg-searching, + .global-msg-empty { + display: flex; + align-items: center; + gap: 6px; + padding: 12px; + font-size: 13px; + color: var(--text-tertiary); + } + + .global-msg-result-item { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--border-color); + + &:hover { background: var(--bg-secondary); } + + .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; + } + } +} + +.msg-search-toggle-btn.active { + color: var(--accent-color, #07c160); +} +.in-session-search-btn.active { + color: var(--accent-color, #07c160); +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index f0f4a7c..0b31d39 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -564,6 +564,17 @@ function ChatPage(props: ChatPageProps) { const [isDeleting, setIsDeleting] = useState(false) const [deleteProgress, setDeleteProgress] = useState({ current: 0, total: 0 }) const [cancelDeleteRequested, setCancelDeleteRequested] = useState(false) + // 会话内搜索 + const [showInSessionSearch, setShowInSessionSearch] = useState(false) + const [inSessionQuery, setInSessionQuery] = useState('') + const [inSessionResults, setInSessionResults] = useState([]) + const [inSessionSearching, setInSessionSearching] = useState(false) + const inSessionSearchRef = useRef(null) + // 全局消息搜索 + const [showGlobalMsgSearch, setShowGlobalMsgSearch] = useState(false) + const [globalMsgQuery, setGlobalMsgQuery] = useState('') + const [globalMsgResults, setGlobalMsgResults] = useState([]) + const [globalMsgSearching, setGlobalMsgSearching] = useState(false) // 自定义删除确认对话框 const [deleteConfirm, setDeleteConfirm] = useState<{ @@ -2608,6 +2619,56 @@ function ChatPage(props: ChatPageProps) { setSearchKeyword('') } + // 会话内搜索 + const handleInSessionSearch = useCallback(async (keyword: string) => { + setInSessionQuery(keyword) + if (!keyword.trim() || !currentSessionId) { + setInSessionResults([]) + return + } + setInSessionSearching(true) + try { + const res = await window.electronAPI.chat.searchMessages(keyword.trim(), currentSessionId, 50, 0) + setInSessionResults(res?.messages || []) + } catch { + setInSessionResults([]) + } finally { + setInSessionSearching(false) + } + }, [currentSessionId]) + + const handleToggleInSessionSearch = useCallback(() => { + setShowInSessionSearch(v => { + if (v) { setInSessionQuery(''); setInSessionResults([]) } + else setTimeout(() => inSessionSearchRef.current?.focus(), 50) + return !v + }) + }, []) + + // 全局消息搜索 + const handleGlobalMsgSearch = useCallback(async (keyword: string) => { + setGlobalMsgQuery(keyword) + if (!keyword.trim()) { + setGlobalMsgResults([]) + return + } + setGlobalMsgSearching(true) + try { + const res = await window.electronAPI.chat.searchMessages(keyword.trim(), undefined, 50, 0) + setGlobalMsgResults(res?.messages || []) + } catch { + setGlobalMsgResults([]) + } finally { + setGlobalMsgSearching(false) + } + }, []) + + const handleCloseGlobalMsgSearch = useCallback(() => { + setShowGlobalMsgSearch(false) + setGlobalMsgQuery('') + setGlobalMsgResults([]) + }, []) + // 滚动加载更多 + 显示/隐藏回到底部按钮(优化:节流,避免频繁执行) const scrollTimeoutRef = useRef(null) const handleScroll = useCallback(() => { @@ -3895,20 +3956,57 @@ function ChatPage(props: ChatPageProps) { handleSearch(e.target.value)} + placeholder={showGlobalMsgSearch ? '搜索消息内容...' : '搜索'} + value={showGlobalMsgSearch ? globalMsgQuery : searchKeyword} + onChange={(e) => showGlobalMsgSearch ? handleGlobalMsgSearch(e.target.value) : handleSearch(e.target.value)} /> - {searchKeyword && ( - )} + + {/* 全局消息搜索结果面板 */} + {showGlobalMsgSearch && ( +
+ {globalMsgSearching && ( +
搜索中...
+ )} + {!globalMsgSearching && globalMsgQuery && globalMsgResults.length === 0 && ( +
没有找到相关消息
+ )} + {globalMsgResults.map((msg, i) => ( +
{ + const sid = msg._session_id || msg.username + if (sid) { + const target = sessions.find(s => s.username === sid) + if (target) { + handleSelectSession(target) + handleCloseGlobalMsgSearch() + } + } + }}> +
{msg._session_id || msg.username || '未知会话'}
+
{(msg.content || msg.strContent || msg.message_content || '').slice(0, 60)}
+
{(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' }) : ''}
+
+ ))} +
+ )} {/* 折叠群 header */}
@@ -4150,6 +4248,14 @@ function ChatPage(props: ChatPageProps) {
, document.body )} + + + )} + {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' }) : ''} +
+ ))} +
+ )}
{standaloneSessionWindow && standaloneLoadStage !== 'ready' && (