diff --git a/internal/database/group.go b/internal/database/group.go index 5e61a501..35e249f6 100644 --- a/internal/database/group.go +++ b/internal/database/group.go @@ -205,7 +205,7 @@ func (db *DB) AddConversationToGroup(conversationID, groupID string) error { if err != nil { return fmt.Errorf("删除对话旧分组关联失败: %w", err) } - + // 然后插入新的分组关联 id := uuid.New().String() _, err = db.Exec( @@ -282,6 +282,78 @@ func (db *DB) GetConversationsByGroup(groupID string) ([]*Conversation, error) { return conversations, nil } +// SearchConversationsByGroup 搜索分组中的对话(按标题和消息内容模糊匹配) +func (db *DB) SearchConversationsByGroup(groupID string, searchQuery string) ([]*Conversation, error) { + // 构建SQL查询,支持按标题和消息内容搜索 + // 使用 DISTINCT 避免因为一个对话有多条匹配消息而重复 + query := `SELECT DISTINCT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, COALESCE(cgm.pinned, 0) as group_pinned + FROM conversations c + INNER JOIN conversation_group_mappings cgm ON c.id = cgm.conversation_id + WHERE cgm.group_id = ?` + + args := []interface{}{groupID} + + // 如果有搜索关键词,添加标题和消息内容搜索条件 + if searchQuery != "" { + searchPattern := "%" + searchQuery + "%" + // 搜索标题或消息内容 + // 使用 LEFT JOIN 连接消息表,这样即使没有消息的对话也能被搜索到(通过标题) + query += ` AND ( + LOWER(c.title) LIKE LOWER(?) + OR EXISTS ( + SELECT 1 FROM messages m + WHERE m.conversation_id = c.id + AND LOWER(m.content) LIKE LOWER(?) + ) + )` + args = append(args, searchPattern, searchPattern) + } + + query += " ORDER BY COALESCE(cgm.pinned, 0) DESC, c.updated_at DESC" + + rows, err := db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("搜索分组对话失败: %w", err) + } + defer rows.Close() + + var conversations []*Conversation + for rows.Next() { + var conv Conversation + var createdAt, updatedAt string + var pinned int + var groupPinned int + + if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &groupPinned); err != nil { + return nil, fmt.Errorf("扫描对话失败: %w", err) + } + + // 尝试多种时间格式解析 + var err1, err2 error + conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err1 != nil { + conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err1 != nil { + conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + } + + conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt) + if err2 != nil { + conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt) + } + if err2 != nil { + conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + } + + conv.Pinned = pinned != 0 + + conversations = append(conversations, &conv) + } + + return conversations, nil +} + // GetGroupByConversation 获取对话所属的分组 func (db *DB) GetGroupByConversation(conversationID string) (string, error) { var groupID string diff --git a/internal/handler/group.go b/internal/handler/group.go index 9ecf5a95..d3bfc9a8 100644 --- a/internal/handler/group.go +++ b/internal/handler/group.go @@ -189,8 +189,18 @@ type GroupConversation struct { // GetGroupConversations 获取分组中的所有对话 func (h *GroupHandler) GetGroupConversations(c *gin.Context) { groupID := c.Param("id") + searchQuery := c.Query("search") // 获取搜索参数 + + var conversations []*database.Conversation + var err error + + // 如果有搜索关键词,使用搜索方法;否则使用普通方法 + if searchQuery != "" { + conversations, err = h.db.SearchConversationsByGroup(groupID, searchQuery) + } else { + conversations, err = h.db.GetConversationsByGroup(groupID) + } - conversations, err := h.db.GetConversationsByGroup(groupID) if err != nil { h.logger.Error("获取分组对话失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/web/static/css/style.css b/web/static/css/style.css index 5ef913c2..8c94d8e2 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5780,6 +5780,63 @@ header { border-color: var(--error-color); } +.group-search-container { + padding: 12px 24px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); + flex-shrink: 0; +} + +.group-search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.group-search-input { + width: 100%; + padding: 8px 36px 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.875rem; + color: var(--text-primary); + background: var(--bg-secondary); + transition: all 0.2s ease; +} + +.group-search-input:focus { + outline: none; + border-color: var(--accent-color); + background: var(--bg-primary); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.group-search-input::placeholder { + color: var(--text-muted); +} + +.group-search-clear-btn { + position: absolute; + right: 8px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; + padding: 0; +} + +.group-search-clear-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + .group-detail-content { flex: 1; overflow-y: auto; diff --git a/web/static/js/chat.js b/web/static/js/chat.js index dace4d7c..8c8d193c 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -5148,7 +5148,8 @@ async function enterGroupDetail(groupId) { // 刷新分组列表,确保当前分组高亮显示 await loadGroups(); - loadGroupConversations(groupId); + // 加载分组对话(如果有搜索查询则使用搜索查询) + loadGroupConversations(groupId, currentGroupSearchQuery); } catch (error) { console.error('加载分组失败:', error); currentGroupId = null; @@ -5158,6 +5159,14 @@ async function enterGroupDetail(groupId) { // 退出分组详情 function exitGroupDetail() { currentGroupId = null; + currentGroupSearchQuery = ''; // 清除搜索状态 + + // 隐藏搜索框并清除搜索内容 + const searchContainer = document.getElementById('group-search-container'); + const searchInput = document.getElementById('group-search-input'); + if (searchContainer) searchContainer.style.display = 'none'; + if (searchInput) searchInput.value = ''; + const sidebar = document.querySelector('.conversation-sidebar'); const groupDetailPage = document.getElementById('group-detail-page'); const chatContainer = document.querySelector('.chat-container'); @@ -5172,7 +5181,7 @@ function exitGroupDetail() { } // 加载分组中的对话 -async function loadGroupConversations(groupId) { +async function loadGroupConversations(groupId, searchQuery = '') { try { if (!groupId) { console.error('loadGroupConversations: groupId is null or undefined'); @@ -5190,10 +5199,20 @@ async function loadGroupConversations(groupId) { console.error('group-conversations-list element not found'); return; } - list.innerHTML = '
加载中...
'; + + // 显示加载状态 + if (searchQuery) { + list.innerHTML = '
搜索中...
'; + } else { + list.innerHTML = '
加载中...
'; + } - // 确保使用正确的 groupId - const url = `/api/groups/${groupId}/conversations`; + // 构建URL,如果有搜索关键词则添加search参数 + let url = `/api/groups/${groupId}/conversations`; + if (searchQuery && searchQuery.trim()) { + url += '?search=' + encodeURIComponent(searchQuery.trim()); + } + const response = await apiFetch(url); if (!response.ok) { console.error(`Failed to load conversations for group ${groupId}:`, response.statusText); @@ -5235,7 +5254,11 @@ async function loadGroupConversations(groupId) { list.innerHTML = ''; if (groupConvs.length === 0) { - list.innerHTML = '
该分组暂无对话
'; + if (searchQuery && searchQuery.trim()) { + list.innerHTML = '
未找到匹配的对话
'; + } else { + list.innerHTML = '
该分组暂无对话
'; + } return; } @@ -5631,9 +5654,94 @@ function closeGroupContextMenu() { } -// 在分组中搜索(占位函数) -function searchInGroup() { - alert('搜索功能待实现'); +// 分组搜索相关变量 +let groupSearchTimer = null; +let currentGroupSearchQuery = ''; + +// 切换分组搜索框显示/隐藏 +function toggleGroupSearch() { + const searchContainer = document.getElementById('group-search-container'); + const searchInput = document.getElementById('group-search-input'); + + if (!searchContainer || !searchInput) return; + + if (searchContainer.style.display === 'none') { + searchContainer.style.display = 'block'; + searchInput.focus(); + } else { + searchContainer.style.display = 'none'; + clearGroupSearch(); + } +} + +// 处理分组搜索输入 +function handleGroupSearchInput(event) { + // 支持回车键搜索 + if (event.key === 'Enter') { + event.preventDefault(); + performGroupSearch(); + return; + } + + // 支持ESC键关闭搜索 + if (event.key === 'Escape') { + clearGroupSearch(); + toggleGroupSearch(); + return; + } + + const searchInput = document.getElementById('group-search-input'); + const clearBtn = document.getElementById('group-search-clear-btn'); + + if (!searchInput) return; + + const query = searchInput.value.trim(); + + // 显示/隐藏清除按钮 + if (clearBtn) { + clearBtn.style.display = query ? 'block' : 'none'; + } + + // 防抖搜索 + if (groupSearchTimer) { + clearTimeout(groupSearchTimer); + } + + groupSearchTimer = setTimeout(() => { + performGroupSearch(); + }, 300); // 300ms 防抖 +} + +// 执行分组搜索 +async function performGroupSearch() { + const searchInput = document.getElementById('group-search-input'); + if (!searchInput || !currentGroupId) return; + + const query = searchInput.value.trim(); + currentGroupSearchQuery = query; + + // 加载搜索结果 + await loadGroupConversations(currentGroupId, query); +} + +// 清除分组搜索 +function clearGroupSearch() { + const searchInput = document.getElementById('group-search-input'); + const clearBtn = document.getElementById('group-search-clear-btn'); + + if (searchInput) { + searchInput.value = ''; + } + if (clearBtn) { + clearBtn.style.display = 'none'; + } + + currentGroupSearchQuery = ''; + + // 重新加载分组对话(不搜索) + if (currentGroupId) { + loadGroupConversations(currentGroupId, ''); + } } // 初始化时加载分组 diff --git a/web/templates/index.html b/web/templates/index.html index 965990e8..4b9b8723 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -218,7 +218,7 @@

-
+