From c3a1d95a92ae51102f1b215f5dd13ca9e186e9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:32:14 +0800 Subject: [PATCH] Add files via upload --- internal/database/group.go | 74 +++++++++++++++++++++- internal/handler/group.go | 12 +++- web/static/css/style.css | 57 +++++++++++++++++ web/static/js/chat.js | 126 ++++++++++++++++++++++++++++++++++--- web/templates/index.html | 13 +++- 5 files changed, 270 insertions(+), 12 deletions(-) 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 = '