mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-16 13:19:17 +02:00
Add files via upload
This commit is contained in:
@@ -374,6 +374,7 @@ func setupRoutes(
|
||||
protected.GET("/groups/:id/conversations", groupHandler.GetGroupConversations)
|
||||
protected.POST("/groups/conversations", groupHandler.AddConversationToGroup)
|
||||
protected.DELETE("/groups/:id/conversations/:conversationId", groupHandler.RemoveConversationFromGroup)
|
||||
protected.PUT("/groups/:id/conversations/:conversationId/pinned", groupHandler.UpdateConversationPinnedInGroup)
|
||||
|
||||
// 监控
|
||||
protected.GET("/monitor", monitorHandler.Monitor)
|
||||
|
||||
@@ -242,6 +242,11 @@ func (db *DB) initTables() error {
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if err := db.migrateConversationGroupMappingsTable(); err != nil {
|
||||
db.logger.Warn("迁移conversation_group_mappings表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createIndexes); err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
@@ -334,6 +339,30 @@ func (db *DB) migrateConversationGroupsTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateConversationGroupMappingsTable 迁移conversation_group_mappings表,添加新字段
|
||||
func (db *DB) migrateConversationGroupMappingsTable() error {
|
||||
// 检查pinned字段是否存在
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('conversation_group_mappings') WHERE name='pinned'").Scan(&count)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试添加字段
|
||||
if _, addErr := db.Exec("ALTER TABLE conversation_group_mappings ADD COLUMN pinned INTEGER DEFAULT 0"); addErr != nil {
|
||||
// 如果字段已存在,忽略错误
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if count == 0 {
|
||||
// 字段不存在,添加它
|
||||
if _, err := db.Exec("ALTER TABLE conversation_group_mappings ADD COLUMN pinned INTEGER DEFAULT 0"); err != nil {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKnowledgeDB 创建知识库数据库连接(只包含知识库相关的表)
|
||||
func NewKnowledgeDB(dbPath string, logger *zap.Logger) (*DB, error) {
|
||||
sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=1")
|
||||
|
||||
@@ -222,11 +222,11 @@ func (db *DB) RemoveConversationFromGroup(conversationID, groupID string) error
|
||||
// GetConversationsByGroup 获取分组中的所有对话
|
||||
func (db *DB) GetConversationsByGroup(groupID string) ([]*Conversation, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at
|
||||
`SELECT 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 = ?
|
||||
ORDER BY c.updated_at DESC`,
|
||||
ORDER BY COALESCE(cgm.pinned, 0) DESC, c.updated_at DESC`,
|
||||
groupID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -239,8 +239,9 @@ func (db *DB) GetConversationsByGroup(groupID string) ([]*Conversation, error) {
|
||||
var conv Conversation
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
var groupPinned int
|
||||
|
||||
if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt); err != nil {
|
||||
if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &groupPinned); err != nil {
|
||||
return nil, fmt.Errorf("扫描对话失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -318,3 +319,19 @@ func (db *DB) UpdateGroupPinned(id string, pinned bool) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroup 更新对话在分组中的置顶状态
|
||||
func (db *DB) UpdateConversationPinnedInGroup(conversationID, groupID string, pinned bool) error {
|
||||
pinnedValue := 0
|
||||
if pinned {
|
||||
pinnedValue = 1
|
||||
}
|
||||
_, err := db.Exec(
|
||||
"UPDATE conversation_group_mappings SET pinned = ? WHERE conversation_id = ? AND group_id = ?",
|
||||
pinnedValue, conversationID, groupID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新分组对话置顶状态失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
|
||||
@@ -175,6 +176,16 @@ func (h *GroupHandler) RemoveConversationFromGroup(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "移除成功"})
|
||||
}
|
||||
|
||||
// GroupConversation 分组对话响应结构
|
||||
type GroupConversation struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Pinned bool `json:"pinned"`
|
||||
GroupPinned bool `json:"groupPinned"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// GetGroupConversations 获取分组中的所有对话
|
||||
func (h *GroupHandler) GetGroupConversations(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
@@ -186,7 +197,31 @@ func (h *GroupHandler) GetGroupConversations(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, conversations)
|
||||
// 获取每个对话在分组中的置顶状态
|
||||
groupConvs := make([]GroupConversation, 0, len(conversations))
|
||||
for _, conv := range conversations {
|
||||
// 查询分组内置顶状态
|
||||
var groupPinned int
|
||||
err := h.db.QueryRow(
|
||||
"SELECT COALESCE(pinned, 0) FROM conversation_group_mappings WHERE conversation_id = ? AND group_id = ?",
|
||||
conv.ID, groupID,
|
||||
).Scan(&groupPinned)
|
||||
if err != nil {
|
||||
h.logger.Warn("查询分组内置顶状态失败", zap.String("conversationId", conv.ID), zap.Error(err))
|
||||
groupPinned = 0
|
||||
}
|
||||
|
||||
groupConvs = append(groupConvs, GroupConversation{
|
||||
ID: conv.ID,
|
||||
Title: conv.Title,
|
||||
Pinned: conv.Pinned,
|
||||
GroupPinned: groupPinned != 0,
|
||||
CreatedAt: conv.CreatedAt,
|
||||
UpdatedAt: conv.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groupConvs)
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedRequest 更新对话置顶状态请求
|
||||
@@ -236,3 +271,28 @@ func (h *GroupHandler) UpdateGroupPinned(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroupRequest 更新分组对话置顶状态请求
|
||||
type UpdateConversationPinnedInGroupRequest struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroup 更新对话在分组中的置顶状态
|
||||
func (h *GroupHandler) UpdateConversationPinnedInGroup(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
conversationID := c.Param("conversationId")
|
||||
|
||||
var req UpdateConversationPinnedInGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateConversationPinnedInGroup(conversationID, groupID, req.Pinned); err != nil {
|
||||
h.logger.Error("更新分组对话置顶状态失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
+89
-31
@@ -5130,42 +5130,45 @@ header {
|
||||
}
|
||||
|
||||
.conversation-item-menu {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.conversation-item:hover .conversation-item-menu {
|
||||
.conversation-item:hover .conversation-item-menu,
|
||||
.group-conversation-item:hover .conversation-item-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.conversation-item-menu:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 分组详情页面 */
|
||||
.group-detail-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--bg-primary);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.group-detail-header {
|
||||
@@ -5244,55 +5247,92 @@ header {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.group-conversations-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.group-conversation-item {
|
||||
padding: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.group-conversation-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.group-conversation-item.active {
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.group-conversation-item:hover .conversation-item-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.group-conversation-content-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.group-conversation-item .conversation-item-menu {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group-conversation-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.group-conversation-time {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.group-conversation-content {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-primary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 批量管理模态框 */
|
||||
@@ -5559,7 +5599,18 @@ header {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
max-width: 240px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.context-submenu[style*="right: 100%"] {
|
||||
left: auto;
|
||||
right: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.context-submenu-item {
|
||||
@@ -5572,6 +5623,13 @@ header {
|
||||
font-size: 0.875rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-submenu-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-submenu-item:hover {
|
||||
|
||||
+503
-48
@@ -1162,13 +1162,24 @@ function copyDetailBlock(elementId, triggerBtn = null) {
|
||||
|
||||
// 开始新对话
|
||||
function startNewConversation() {
|
||||
// 如果当前在分组详情页面,先退出分组详情
|
||||
if (currentGroupId) {
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
if (groupDetailPage) groupDetailPage.style.display = 'none';
|
||||
if (chatContainer) chatContainer.style.display = 'flex';
|
||||
currentGroupId = null;
|
||||
// 刷新对话列表
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
|
||||
currentConversationId = null;
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
|
||||
addAttackChainButton(null);
|
||||
updateActiveConversation();
|
||||
// 刷新对话列表,确保显示最新的历史对话
|
||||
loadConversations();
|
||||
loadConversationsWithGroups();
|
||||
// 清除防抖定时器,防止恢复草稿时触发保存
|
||||
if (draftSaveTimer) {
|
||||
clearTimeout(draftSaveTimer);
|
||||
@@ -1428,6 +1439,32 @@ async function loadConversation(conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前在分组详情页面,切换到对话界面
|
||||
// 退出分组详情模式,显示所有最近对话,提供更好的用户体验
|
||||
if (currentGroupId) {
|
||||
const sidebar = document.querySelector('.conversation-sidebar');
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
|
||||
// 确保侧边栏始终可见
|
||||
if (sidebar) sidebar.style.display = 'flex';
|
||||
// 隐藏分组详情页,显示对话界面
|
||||
if (groupDetailPage) groupDetailPage.style.display = 'none';
|
||||
if (chatContainer) chatContainer.style.display = 'flex';
|
||||
|
||||
// 退出分组详情模式,这样最近对话列表会显示所有对话
|
||||
// 用户可以在侧边栏看到所有对话,方便切换
|
||||
const previousGroupId = currentGroupId;
|
||||
currentGroupId = null;
|
||||
|
||||
// 刷新最近对话列表,显示所有对话(包括分组中的)
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
|
||||
// 无论是否在分组详情页面,都刷新分组列表,确保高亮状态正确
|
||||
// 这样可以清除之前分组的高亮状态,确保UI状态一致
|
||||
await loadGroups();
|
||||
|
||||
// 更新当前对话ID
|
||||
currentConversationId = conversationId;
|
||||
updateActiveConversation();
|
||||
@@ -3869,11 +3906,56 @@ function createConversationListItemWithMenu(conversation, isPinned) {
|
||||
}
|
||||
|
||||
// 显示对话上下文菜单
|
||||
function showConversationContextMenu(event) {
|
||||
async function showConversationContextMenu(event) {
|
||||
const menu = document.getElementById('conversation-context-menu');
|
||||
if (!menu) return;
|
||||
|
||||
// 先显示菜单以获取尺寸
|
||||
const convId = contextMenuConversationId;
|
||||
// 先获取对话的置顶状态并更新菜单文本(在显示菜单之前)
|
||||
if (convId) {
|
||||
try {
|
||||
let isPinned = false;
|
||||
if (currentGroupId) {
|
||||
// 如果在分组详情页面,获取分组内置顶状态
|
||||
const response = await apiFetch(`/api/groups/${currentGroupId}/conversations`);
|
||||
if (response.ok) {
|
||||
const groupConvs = await response.json();
|
||||
const conv = groupConvs.find(c => c.id === convId);
|
||||
if (conv) {
|
||||
isPinned = conv.groupPinned || false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不在分组详情页面,获取全局置顶状态
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
if (response.ok) {
|
||||
const conv = await response.json();
|
||||
isPinned = conv.pinned || false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新菜单文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此对话';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取对话置顶状态失败:', error);
|
||||
// 如果获取失败,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此对话';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有对话ID,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此对话';
|
||||
}
|
||||
}
|
||||
|
||||
// 在状态获取完成后再显示菜单
|
||||
menu.style.display = 'block';
|
||||
menu.style.visibility = 'visible';
|
||||
menu.style.opacity = '1';
|
||||
@@ -3886,17 +3968,26 @@ function showConversationContextMenu(event) {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// 获取子菜单的宽度(如果存在)
|
||||
const submenu = document.getElementById('move-to-group-submenu');
|
||||
const submenuWidth = submenu ? 180 : 0; // 子菜单宽度 + 间距
|
||||
|
||||
let left = event.clientX;
|
||||
let top = event.clientY;
|
||||
|
||||
// 如果菜单会超出右边界,调整到左侧
|
||||
if (left + menuRect.width > viewportWidth) {
|
||||
// 考虑子菜单的宽度
|
||||
if (left + menuRect.width + submenuWidth > viewportWidth) {
|
||||
left = event.clientX - menuRect.width;
|
||||
// 如果调整后仍然超出,则放在按钮左侧
|
||||
if (left < 0) {
|
||||
left = Math.max(8, event.clientX - menuRect.width - submenuWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果菜单会超出下边界,调整到上方
|
||||
if (top + menuRect.height > viewportHeight) {
|
||||
top = event.clientY - menuRect.height;
|
||||
top = Math.max(8, event.clientY - menuRect.height);
|
||||
}
|
||||
|
||||
// 确保不超出左边界
|
||||
@@ -3911,6 +4002,19 @@ function showConversationContextMenu(event) {
|
||||
|
||||
menu.style.left = left + 'px';
|
||||
menu.style.top = top + 'px';
|
||||
|
||||
// 如果菜单在右侧,子菜单应该在左侧显示
|
||||
if (submenu && left < event.clientX) {
|
||||
submenu.style.left = 'auto';
|
||||
submenu.style.right = '100%';
|
||||
submenu.style.marginLeft = '0';
|
||||
submenu.style.marginRight = '4px';
|
||||
} else if (submenu) {
|
||||
submenu.style.left = '100%';
|
||||
submenu.style.right = 'auto';
|
||||
submenu.style.marginLeft = '4px';
|
||||
submenu.style.marginRight = '0';
|
||||
}
|
||||
|
||||
// 点击外部关闭菜单
|
||||
const closeMenu = (e) => {
|
||||
@@ -3925,13 +4029,44 @@ function showConversationContextMenu(event) {
|
||||
}
|
||||
|
||||
// 显示分组上下文菜单
|
||||
function showGroupContextMenu(event, groupId) {
|
||||
async function showGroupContextMenu(event, groupId) {
|
||||
const menu = document.getElementById('group-context-menu');
|
||||
if (!menu) return;
|
||||
|
||||
contextMenuGroupId = groupId;
|
||||
|
||||
// 先显示菜单以获取尺寸
|
||||
// 先获取分组的置顶状态并更新菜单文本(在显示菜单之前)
|
||||
try {
|
||||
// 先从缓存中查找
|
||||
let group = groupsCache.find(g => g.id === groupId);
|
||||
let isPinned = false;
|
||||
|
||||
if (group) {
|
||||
isPinned = group.pinned || false;
|
||||
} else {
|
||||
// 如果缓存中没有,从API获取
|
||||
const response = await apiFetch(`/api/groups/${groupId}`);
|
||||
if (response.ok) {
|
||||
group = await response.json();
|
||||
isPinned = group.pinned || false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新菜单文本
|
||||
const pinMenuText = document.getElementById('pin-group-menu-text');
|
||||
if (pinMenuText) {
|
||||
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此分组';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分组置顶状态失败:', error);
|
||||
// 如果获取失败,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-group-menu-text');
|
||||
if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此分组';
|
||||
}
|
||||
}
|
||||
|
||||
// 在状态获取完成后再显示菜单
|
||||
menu.style.display = 'block';
|
||||
menu.style.visibility = 'visible';
|
||||
menu.style.opacity = '1';
|
||||
@@ -4041,21 +4176,45 @@ async function pinConversation() {
|
||||
if (!convId) return;
|
||||
|
||||
try {
|
||||
// 获取当前对话的置顶状态
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
const conv = await response.json();
|
||||
const newPinned = !conv.pinned;
|
||||
// 如果当前在分组详情页面,使用分组内置顶
|
||||
if (currentGroupId) {
|
||||
// 获取当前对话在分组中的置顶状态
|
||||
const response = await apiFetch(`/api/groups/${currentGroupId}/conversations`);
|
||||
const groupConvs = await response.json();
|
||||
const conv = groupConvs.find(c => c.id === convId);
|
||||
|
||||
// 如果找不到对话,说明可能有问题,使用默认值
|
||||
const currentPinned = conv && conv.groupPinned !== undefined ? conv.groupPinned : false;
|
||||
const newPinned = !currentPinned;
|
||||
|
||||
// 更新置顶状态
|
||||
await apiFetch(`/api/conversations/${convId}/pinned`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ pinned: newPinned }),
|
||||
});
|
||||
// 更新分组内置顶状态
|
||||
await apiFetch(`/api/groups/${currentGroupId}/conversations/${convId}/pinned`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ pinned: newPinned }),
|
||||
});
|
||||
|
||||
loadConversationsWithGroups();
|
||||
// 重新加载分组对话
|
||||
loadGroupConversations(currentGroupId);
|
||||
} else {
|
||||
// 不在分组详情页面,使用全局置顶
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
const conv = await response.json();
|
||||
const newPinned = !conv.pinned;
|
||||
|
||||
// 更新全局置顶状态
|
||||
await apiFetch(`/api/conversations/${convId}/pinned`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ pinned: newPinned }),
|
||||
});
|
||||
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('置顶对话失败:', error);
|
||||
alert('置顶失败: ' + (error.message || '未知错误'));
|
||||
@@ -4071,34 +4230,143 @@ async function showMoveToGroupSubmenu() {
|
||||
|
||||
submenu.innerHTML = '';
|
||||
|
||||
// 确保分组列表已加载
|
||||
if (groupsCache.length === 0) {
|
||||
await loadGroups();
|
||||
// 确保分组列表已加载 - 强制重新加载以确保数据是最新的
|
||||
try {
|
||||
// 如果缓存为空,强制加载
|
||||
if (!Array.isArray(groupsCache) || groupsCache.length === 0) {
|
||||
await loadGroups();
|
||||
} else {
|
||||
// 即使缓存不为空,也尝试刷新一次,确保数据是最新的
|
||||
// 但使用静默方式,不显示错误
|
||||
try {
|
||||
const response = await apiFetch('/api/groups');
|
||||
if (response.ok) {
|
||||
const freshGroups = await response.json();
|
||||
if (Array.isArray(freshGroups)) {
|
||||
groupsCache = freshGroups;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// 如果刷新失败,使用缓存的数据
|
||||
console.warn('刷新分组列表失败,使用缓存数据:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次验证缓存
|
||||
if (!Array.isArray(groupsCache)) {
|
||||
console.warn('groupsCache 不是有效数组,重置为空数组');
|
||||
groupsCache = [];
|
||||
// 如果仍然无效,尝试重新加载
|
||||
if (groupsCache.length === 0) {
|
||||
await loadGroups();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分组列表失败:', error);
|
||||
// 即使加载失败,也继续显示菜单,使用现有缓存
|
||||
}
|
||||
|
||||
// 如果有分组,显示所有分组
|
||||
// 如果当前在分组详情页面,显示"移出本组"选项
|
||||
if (currentGroupId && contextMenuConversationId) {
|
||||
// 检查对话是否在当前分组中
|
||||
const convInGroup = conversationGroupMappingCache[contextMenuConversationId] === currentGroupId;
|
||||
if (convInGroup) {
|
||||
const removeItem = document.createElement('div');
|
||||
removeItem.className = 'context-submenu-item';
|
||||
removeItem.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 12l6 6M15 12l-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>移出本组</span>
|
||||
`;
|
||||
removeItem.onclick = () => {
|
||||
removeConversationFromGroup(contextMenuConversationId, currentGroupId);
|
||||
};
|
||||
submenu.appendChild(removeItem);
|
||||
|
||||
// 添加分隔线
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'context-menu-divider';
|
||||
submenu.appendChild(divider);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 groupsCache 是否为有效数组
|
||||
if (!Array.isArray(groupsCache)) {
|
||||
console.warn('groupsCache 不是有效数组,重置为空数组');
|
||||
groupsCache = [];
|
||||
}
|
||||
|
||||
// 如果有分组,显示所有分组(排除当前分组)
|
||||
if (groupsCache.length > 0) {
|
||||
groupsCache.forEach(group => {
|
||||
// 验证分组对象是否有效
|
||||
if (!group || !group.id || !group.name) {
|
||||
console.warn('无效的分组对象:', group);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前在分组详情页面,不显示当前分组
|
||||
if (currentGroupId && group.id === currentGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'context-submenu-item';
|
||||
item.textContent = group.name;
|
||||
item.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>${group.name}</span>
|
||||
`;
|
||||
item.onclick = () => {
|
||||
moveConversationToGroup(contextMenuConversationId, group.id);
|
||||
};
|
||||
submenu.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
// 如果仍然没有分组,记录日志以便调试
|
||||
console.warn('showMoveToGroupSubmenu: groupsCache 为空,无法显示分组列表');
|
||||
}
|
||||
|
||||
// 始终显示"创建分组"选项
|
||||
const addItem = document.createElement('div');
|
||||
addItem.className = 'context-submenu-item add-group-item';
|
||||
addItem.textContent = '+ 创建分组';
|
||||
addItem.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>+ 新增分组</span>
|
||||
`;
|
||||
addItem.onclick = () => {
|
||||
showCreateGroupModal(true);
|
||||
};
|
||||
submenu.appendChild(addItem);
|
||||
|
||||
submenu.style.display = 'block';
|
||||
|
||||
// 计算子菜单位置,防止溢出
|
||||
setTimeout(() => {
|
||||
const submenuRect = submenu.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// 如果子菜单超出右边界,调整到左侧
|
||||
if (submenuRect.right > viewportWidth) {
|
||||
submenu.style.left = 'auto';
|
||||
submenu.style.right = '100%';
|
||||
submenu.style.marginLeft = '0';
|
||||
submenu.style.marginRight = '4px';
|
||||
}
|
||||
|
||||
// 如果子菜单超出下边界,调整位置
|
||||
if (submenuRect.bottom > viewportHeight) {
|
||||
const overflow = submenuRect.bottom - viewportHeight;
|
||||
const currentTop = parseInt(submenu.style.top) || 0;
|
||||
submenu.style.top = (currentTop - overflow - 8) + 'px';
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 移动对话到分组
|
||||
@@ -4116,8 +4384,18 @@ async function moveConversationToGroup(convId, groupId) {
|
||||
});
|
||||
|
||||
// 更新缓存
|
||||
const oldGroupId = conversationGroupMappingCache[convId];
|
||||
conversationGroupMappingCache[convId] = groupId;
|
||||
loadConversationsWithGroups();
|
||||
|
||||
// 如果当前在分组详情页面,重新加载分组对话
|
||||
if (currentGroupId) {
|
||||
// 如果从当前分组移出,或者移动到当前分组,都需要重新加载
|
||||
if (currentGroupId === oldGroupId || currentGroupId === groupId) {
|
||||
loadGroupConversations(currentGroupId);
|
||||
}
|
||||
} else {
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('移动对话到分组失败:', error);
|
||||
alert('移动失败: ' + (error.message || '未知错误'));
|
||||
@@ -4126,6 +4404,38 @@ async function moveConversationToGroup(convId, groupId) {
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
// 从分组中移除对话
|
||||
async function removeConversationFromGroup(convId, groupId) {
|
||||
try {
|
||||
await apiFetch(`/api/groups/${groupId}/conversations/${convId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
// 更新缓存 - 立即删除,确保后续加载时能正确识别
|
||||
delete conversationGroupMappingCache[convId];
|
||||
|
||||
// 如果当前在分组详情页面,重新加载分组对话
|
||||
if (currentGroupId === groupId) {
|
||||
await loadGroupConversations(groupId);
|
||||
}
|
||||
|
||||
// 重新加载分组映射,确保缓存是最新的
|
||||
await loadConversationGroupMapping();
|
||||
|
||||
// 刷新最近对话列表,让移出的对话立即显示
|
||||
// 使用临时变量保存 currentGroupId,然后临时设置为 null,确保显示所有不在分组的对话
|
||||
const savedGroupId = currentGroupId;
|
||||
currentGroupId = null;
|
||||
await loadConversationsWithGroups();
|
||||
currentGroupId = savedGroupId;
|
||||
} catch (error) {
|
||||
console.error('从分组中移除对话失败:', error);
|
||||
alert('移除失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
// 加载对话分组映射
|
||||
async function loadConversationGroupMapping() {
|
||||
try {
|
||||
@@ -4389,6 +4699,11 @@ async function createGroup(event) {
|
||||
}
|
||||
|
||||
const newGroup = await response.json();
|
||||
|
||||
// 检查"移动到分组"子菜单是否打开
|
||||
const submenu = document.getElementById('move-to-group-submenu');
|
||||
const isSubmenuOpen = submenu && submenu.style.display !== 'none';
|
||||
|
||||
await loadGroups();
|
||||
|
||||
const modal = document.getElementById('create-group-modal');
|
||||
@@ -4399,6 +4714,11 @@ async function createGroup(event) {
|
||||
if (shouldMove && contextMenuConversationId) {
|
||||
moveConversationToGroup(contextMenuConversationId, newGroup.id);
|
||||
}
|
||||
|
||||
// 如果子菜单是打开的,刷新它,让新创建的分组立即显示
|
||||
if (isSubmenuOpen) {
|
||||
await showMoveToGroupSubmenu();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建分组失败:', error);
|
||||
alert('创建失败: ' + (error.message || '未知错误'));
|
||||
@@ -4418,15 +4738,22 @@ async function enterGroupDetail(groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏侧边栏,显示分组详情页
|
||||
// 显示分组详情页,隐藏对话界面,但保持侧边栏可见
|
||||
const sidebar = document.querySelector('.conversation-sidebar');
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
const titleEl = document.getElementById('group-detail-title');
|
||||
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
// 保持侧边栏可见
|
||||
if (sidebar) sidebar.style.display = 'flex';
|
||||
// 隐藏对话界面,显示分组详情页
|
||||
if (chatContainer) chatContainer.style.display = 'none';
|
||||
if (groupDetailPage) groupDetailPage.style.display = 'flex';
|
||||
if (titleEl) titleEl.textContent = group.name;
|
||||
|
||||
// 刷新分组列表,确保当前分组高亮显示
|
||||
await loadGroups();
|
||||
|
||||
loadGroupConversations(groupId);
|
||||
} catch (error) {
|
||||
console.error('加载分组失败:', error);
|
||||
@@ -4439,9 +4766,13 @@ function exitGroupDetail() {
|
||||
currentGroupId = null;
|
||||
const sidebar = document.querySelector('.conversation-sidebar');
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
|
||||
// 保持侧边栏可见
|
||||
if (sidebar) sidebar.style.display = 'flex';
|
||||
// 隐藏分组详情页,显示对话界面
|
||||
if (groupDetailPage) groupDetailPage.style.display = 'none';
|
||||
if (chatContainer) chatContainer.style.display = 'flex';
|
||||
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
@@ -4449,15 +4780,67 @@ function exitGroupDetail() {
|
||||
// 加载分组中的对话
|
||||
async function loadGroupConversations(groupId) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/groups/${groupId}/conversations`);
|
||||
const groupConvs = await response.json();
|
||||
|
||||
if (!groupId) {
|
||||
console.error('loadGroupConversations: groupId is null or undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保分组映射已加载
|
||||
if (Object.keys(conversationGroupMappingCache).length === 0) {
|
||||
await loadConversationGroupMapping();
|
||||
}
|
||||
|
||||
// 先清空列表,避免显示旧数据
|
||||
const list = document.getElementById('group-conversations-list');
|
||||
if (!list) return;
|
||||
if (!list) {
|
||||
console.error('group-conversations-list element not found');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
|
||||
|
||||
// 确保使用正确的 groupId
|
||||
const url = `/api/groups/${groupId}/conversations`;
|
||||
const response = await apiFetch(url);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load conversations for group ${groupId}:`, response.statusText);
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载失败,请重试</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let groupConvs = await response.json();
|
||||
|
||||
// 处理 null 或 undefined 的情况,将其视为空数组
|
||||
if (!groupConvs) {
|
||||
groupConvs = [];
|
||||
}
|
||||
|
||||
// 验证返回的数据类型
|
||||
if (!Array.isArray(groupConvs)) {
|
||||
console.error(`Invalid response for group ${groupId}:`, groupConvs);
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">数据格式错误</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新分组映射缓存(只更新当前分组的对话)
|
||||
// 先清理该分组之前的映射(如果有对话被移出)
|
||||
Object.keys(conversationGroupMappingCache).forEach(convId => {
|
||||
if (conversationGroupMappingCache[convId] === groupId) {
|
||||
// 如果这个对话不在新的列表中,说明已被移出
|
||||
if (!groupConvs.find(c => c.id === convId)) {
|
||||
delete conversationGroupMappingCache[convId];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新当前分组的对话映射
|
||||
groupConvs.forEach(conv => {
|
||||
conversationGroupMappingCache[conv.id] = groupId;
|
||||
});
|
||||
|
||||
// 再次清空列表(清除"加载中"提示)
|
||||
list.innerHTML = '';
|
||||
|
||||
if (!Array.isArray(groupConvs) || groupConvs.length === 0) {
|
||||
if (groupConvs.length === 0) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
|
||||
return;
|
||||
}
|
||||
@@ -4465,19 +4848,55 @@ async function loadGroupConversations(groupId) {
|
||||
// 加载每个对话的详细信息以获取消息
|
||||
for (const conv of groupConvs) {
|
||||
try {
|
||||
// 验证对话ID存在
|
||||
if (!conv.id) {
|
||||
console.warn('Conversation missing id:', conv);
|
||||
continue;
|
||||
}
|
||||
|
||||
const convResponse = await apiFetch(`/api/conversations/${conv.id}`);
|
||||
if (!convResponse.ok) {
|
||||
console.error(`Failed to load conversation ${conv.id}:`, convResponse.statusText);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullConv = await convResponse.json();
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'group-conversation-item';
|
||||
item.onclick = () => {
|
||||
exitGroupDetail();
|
||||
loadConversation(conv.id);
|
||||
};
|
||||
item.dataset.conversationId = conv.id;
|
||||
// 只有在分组详情页面且对话ID匹配时才显示active状态
|
||||
// 如果不在分组详情页面,不应该显示active状态
|
||||
if (currentGroupId && conv.id === currentConversationId) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
|
||||
// 创建内容包装器
|
||||
const contentWrapper = document.createElement('div');
|
||||
contentWrapper.className = 'group-conversation-content-wrapper';
|
||||
|
||||
const titleWrapper = document.createElement('div');
|
||||
titleWrapper.style.display = 'flex';
|
||||
titleWrapper.style.alignItems = 'center';
|
||||
titleWrapper.style.gap = '4px';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'group-conversation-title';
|
||||
title.textContent = fullConv.title || conv.title || '未命名对话';
|
||||
titleWrapper.appendChild(title);
|
||||
|
||||
// 如果对话在分组中置顶,显示置顶图标
|
||||
if (conv.groupPinned) {
|
||||
const pinIcon = document.createElement('span');
|
||||
pinIcon.className = 'conversation-item-pinned';
|
||||
pinIcon.innerHTML = '📌';
|
||||
pinIcon.title = '在分组中已置顶';
|
||||
titleWrapper.appendChild(pinIcon);
|
||||
}
|
||||
|
||||
contentWrapper.appendChild(titleWrapper);
|
||||
|
||||
const timeWrapper = document.createElement('div');
|
||||
timeWrapper.className = 'group-conversation-time';
|
||||
@@ -4490,8 +4909,7 @@ async function loadGroupConversations(groupId) {
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
item.appendChild(title);
|
||||
item.appendChild(timeWrapper);
|
||||
contentWrapper.appendChild(timeWrapper);
|
||||
|
||||
// 如果有第一条消息,显示内容预览
|
||||
if (fullConv.messages && fullConv.messages.length > 0) {
|
||||
@@ -4504,10 +4922,32 @@ async function loadGroupConversations(groupId) {
|
||||
preview += '...';
|
||||
}
|
||||
content.textContent = preview;
|
||||
item.appendChild(content);
|
||||
contentWrapper.appendChild(content);
|
||||
}
|
||||
}
|
||||
|
||||
item.appendChild(contentWrapper);
|
||||
|
||||
// 添加三个点菜单按钮
|
||||
const menuBtn = document.createElement('button');
|
||||
menuBtn.className = 'conversation-item-menu';
|
||||
menuBtn.innerHTML = '⋯';
|
||||
menuBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
contextMenuConversationId = conv.id;
|
||||
showConversationContextMenu(e);
|
||||
};
|
||||
item.appendChild(menuBtn);
|
||||
|
||||
item.onclick = () => {
|
||||
// 切换到对话界面,但保持分组详情状态
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
if (groupDetailPage) groupDetailPage.style.display = 'none';
|
||||
if (chatContainer) chatContainer.style.display = 'flex';
|
||||
loadConversation(conv.id);
|
||||
};
|
||||
|
||||
list.appendChild(item);
|
||||
} catch (err) {
|
||||
console.error(`加载对话 ${conv.id} 失败:`, err);
|
||||
@@ -4593,8 +5033,16 @@ async function deleteGroup() {
|
||||
}
|
||||
});
|
||||
|
||||
exitGroupDetail();
|
||||
loadGroups();
|
||||
// 如果"移动到分组"子菜单是打开的,刷新它
|
||||
const submenu = document.getElementById('move-to-group-submenu');
|
||||
if (submenu && submenu.style.display !== 'none') {
|
||||
// 子菜单是打开的,重新加载分组列表并刷新子菜单
|
||||
await loadGroups();
|
||||
await showMoveToGroupSubmenu();
|
||||
} else {
|
||||
exitGroupDetail();
|
||||
loadGroups();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
alert('删除失败: ' + (error.message || '未知错误'));
|
||||
@@ -4726,12 +5174,19 @@ async function deleteGroupFromContext() {
|
||||
}
|
||||
});
|
||||
|
||||
// 如果当前在分组详情页,退出详情页
|
||||
if (currentGroupId === groupId) {
|
||||
exitGroupDetail();
|
||||
// 如果"移动到分组"子菜单是打开的,刷新它
|
||||
const submenu = document.getElementById('move-to-group-submenu');
|
||||
if (submenu && submenu.style.display !== 'none') {
|
||||
// 子菜单是打开的,重新加载分组列表并刷新子菜单
|
||||
await loadGroups();
|
||||
await showMoveToGroupSubmenu();
|
||||
} else {
|
||||
// 如果当前在分组详情页,退出详情页
|
||||
if (currentGroupId === groupId) {
|
||||
exitGroupDetail();
|
||||
}
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
loadGroups();
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
alert('删除失败: ' + (error.message || '未知错误'));
|
||||
|
||||
@@ -897,7 +897,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17v5M5 17h14l-1-7H6l-1 7zM9 10V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>置顶此对话</span>
|
||||
<span id="pin-conversation-menu-text">置顶此对话</span>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="showBatchManageModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -942,7 +942,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17v5M5 17h14l-1-7H6l-1 7zM9 10V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>置顶此分组</span>
|
||||
<span id="pin-group-menu-text">置顶此分组</span>
|
||||
</div>
|
||||
<div class="context-menu-item context-menu-item-danger" onclick="deleteGroupFromContext()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
Reference in New Issue
Block a user