diff --git a/frontend/src/views/knowledge/components/DocumentListView.vue b/frontend/src/views/knowledge/components/DocumentListView.vue index 80f890991..fef9fa619 100644 --- a/frontend/src/views/knowledge/components/DocumentListView.vue +++ b/frontend/src/views/knowledge/components/DocumentListView.vue @@ -265,8 +265,8 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know flex-direction: column; width: 100%; background: var(--td-bg-color-container, #fff); - border: 1px solid var(--td-component-border, #e7e7e7); - border-radius: 10px; + border: 1px solid var(--td-component-stroke, #f0f0f0); + border-radius: 8px; overflow: hidden; } @@ -291,12 +291,13 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know position: sticky; top: 0; z-index: 2; - height: 40px; + height: 36px; font-size: 12px; font-weight: 500; - color: var(--td-text-color-secondary, #888); - background: var(--td-bg-color-secondarycontainer, #fafafa); - border-bottom: 1px solid var(--td-component-border, #e7e7e7); + letter-spacing: 0.02em; + color: var(--td-text-color-placeholder, #a6a6a6); + background: var(--td-bg-color-page, #fafbfc); + border-bottom: 1px solid var(--td-component-stroke, #f0f0f0); } .doc-list-body { @@ -305,25 +306,31 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know } .doc-list-row { + position: relative; height: 48px; font-size: 13px; color: var(--td-text-color-primary, #232323); - border-bottom: 1px solid var(--td-component-stroke, #f0f0f0); + border-bottom: 1px solid var(--td-component-stroke, #f3f3f3); cursor: pointer; - transition: background-color 0.12s ease; + transition: background-color 0.12s ease, box-shadow 0.12s ease; &:last-child { border-bottom: 0; } - &:hover, - &.menu-open { - background: var(--td-bg-color-container-hover, #f6f8fa); - .row-more-btn { opacity: 1; } + &:hover:not(.selected), + &.menu-open:not(.selected) { + background: var(--td-bg-color-page, #f7f8fa); } &.selected { - background: var(--td-brand-color-1, #f0f6ff); - &:hover { background: var(--td-brand-color-2, #e6f0ff); } + background: var(--td-brand-color-1, #f2f5fc); + box-shadow: inset 3px 0 0 var(--td-brand-color, #0052d9); + + &:hover { background: var(--td-brand-color-light, #e8eefc); } } + + &:hover .row-more-btn, + &.menu-open .row-more-btn, + &.selected .row-more-btn { opacity: 1; } } .cell { diff --git a/internal/handler/knowledge.go b/internal/handler/knowledge.go index 77822c56a..116cd959b 100644 --- a/internal/handler/knowledge.go +++ b/internal/handler/knowledge.go @@ -187,6 +187,29 @@ func (h *KnowledgeHandler) handleDuplicateKnowledgeError(c *gin.Context, return false } +// enqueueKnowledgeListDelete enqueues an async batch-delete task for the +// given knowledge IDs and returns the asynq task ID. +func (h *KnowledgeHandler) enqueueKnowledgeListDelete( + ctx context.Context, tenantID uint64, ids []string, +) (string, error) { + payload := types.KnowledgeListDeletePayload{ + TenantID: tenantID, + KnowledgeIDs: ids, + } + langfuse.InjectTracing(ctx, &payload) + payloadBytes, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("marshal payload: %w", err) + } + task := asynq.NewTask(types.TypeKnowledgeListDelete, payloadBytes, + asynq.Queue("low"), asynq.MaxRetry(3)) + info, err := h.asynqClient.Enqueue(task) + if err != nil { + return "", fmt.Errorf("enqueue task: %w", err) + } + return info.ID, nil +} + // CreateKnowledgeFromFile godoc // @Summary 从文件创建知识 // @Description 上传文件并创建知识条目 @@ -715,16 +738,23 @@ func (h *KnowledgeHandler) BatchDeleteKnowledge(c *gin.Context) { } } - logger.Infof(ctx, "Batch deleting %d knowledge entries from KB %s", len(ids), secutils.SanitizeForLog(kbID)) - if err := h.kgService.DeleteKnowledgeList(ctx, ids); err != nil { - logger.ErrorWithFields(ctx, err, nil) - c.Error(errors.NewInternalServerError(err.Error())) + taskID, err := h.enqueueKnowledgeListDelete(ctx, effectiveTenantID, ids) + if err != nil { + logger.Errorf(ctx, "Failed to enqueue batch knowledge delete task: %v", err) + c.Error(errors.NewInternalServerError("Failed to enqueue batch delete task")) return } + logger.Infof(ctx, "Batch knowledge delete task enqueued: %s, kb_id: %s, count: %d", + taskID, secutils.SanitizeForLog(kbID), len(ids)) + c.JSON(http.StatusOK, gin.H{ "success": true, - "deleted": len(ids), + "message": "Batch delete task submitted", + "data": gin.H{ + "task_id": taskID, + "deleted_count": len(ids), + }, }) } @@ -782,21 +812,7 @@ func (h *KnowledgeHandler) ClearKnowledgeBaseContents(c *gin.Context) { knowledgeIDs = append(knowledgeIDs, knowledge.ID) } - payload := types.KnowledgeListDeletePayload{ - TenantID: effectiveTenantID, - KnowledgeIDs: knowledgeIDs, - } - langfuse.InjectTracing(ctx, &payload) - payloadBytes, err := json.Marshal(payload) - if err != nil { - logger.Errorf(ctx, "Failed to marshal knowledge list delete payload: %v", err) - c.Error(errors.NewInternalServerError("Failed to create cleanup task")) - return - } - - task := asynq.NewTask(types.TypeKnowledgeListDelete, payloadBytes, - asynq.Queue("low"), asynq.MaxRetry(3)) - info, err := h.asynqClient.Enqueue(task) + taskID, err := h.enqueueKnowledgeListDelete(ctx, effectiveTenantID, knowledgeIDs) if err != nil { logger.Errorf(ctx, "Failed to enqueue knowledge list delete task: %v", err) c.Error(errors.NewInternalServerError("Failed to enqueue cleanup task")) @@ -804,7 +820,7 @@ func (h *KnowledgeHandler) ClearKnowledgeBaseContents(c *gin.Context) { } logger.Infof(ctx, "Knowledge base contents clear task enqueued: %s, kb_id: %s, count: %d", - info.ID, secutils.SanitizeForLog(kbID), len(knowledgeIDs)) + taskID, secutils.SanitizeForLog(kbID), len(knowledgeIDs)) c.JSON(http.StatusOK, gin.H{ "success": true,