Add files via upload

This commit is contained in:
公明
2026-01-09 19:32:14 +08:00
committed by GitHub
parent 60e3795322
commit c3a1d95a92
5 changed files with 270 additions and 12 deletions
+73 -1
View File
@@ -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
+11 -1
View File
@@ -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()})
+57
View File
@@ -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;
+117 -9
View File
@@ -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 = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
// 显示加载状态
if (searchQuery) {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">搜索中...</div>';
} else {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
}
// 确保使用正确的 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 = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
if (searchQuery && searchQuery.trim()) {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">未找到匹配的对话</div>';
} else {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
}
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, '');
}
}
// 初始化时加载分组
+12 -1
View File
@@ -218,7 +218,7 @@
</button>
<h2 id="group-detail-title" class="group-detail-title"></h2>
<div class="group-detail-actions">
<button class="group-action-btn" onclick="searchInGroup()" title="搜索">
<button class="group-action-btn" onclick="toggleGroupSearch()" title="搜索" id="group-search-toggle-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -237,6 +237,17 @@
</button>
</div>
</div>
<div id="group-search-container" class="group-search-container" style="display: none;">
<div class="group-search-input-wrapper">
<input type="text" id="group-search-input" class="group-search-input" placeholder="搜索分组中的对话..." onkeyup="handleGroupSearchInput(event)" oninput="handleGroupSearchInput(event)">
<button class="group-search-clear-btn" onclick="clearGroupSearch()" title="清除搜索" id="group-search-clear-btn" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="m8 8 8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div class="group-detail-content">
<div id="group-conversations-list" class="group-conversations-list"></div>
</div>