diff --git a/backend/routes/conversations.py b/backend/routes/conversations.py index f387e65..b5f41c4 100644 --- a/backend/routes/conversations.py +++ b/backend/routes/conversations.py @@ -115,3 +115,16 @@ async def get_conversation( for message in messages ], ) + + +@router.delete("/{conversation_id}") +async def delete_conversation( + conversation_id: str, + session: AsyncSession = Depends(get_session), +): + conversation = await session.get(ConversationSession, conversation_id) + if not conversation: + raise HTTPException(404, "对话记录不存在") + await session.delete(conversation) + await session.commit() + return {"ok": True} diff --git a/frontend/src/components/pages/HistoryPage.tsx b/frontend/src/components/pages/HistoryPage.tsx index 54497e2..85ddf34 100644 --- a/frontend/src/components/pages/HistoryPage.tsx +++ b/frontend/src/components/pages/HistoryPage.tsx @@ -6,12 +6,20 @@ import { Eye, Loader2, MessageSquareText, + MoreHorizontal, + Trash2, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DataList, type DataListColumn } from "@/components/ui/data-list"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Dialog, DialogClose, @@ -32,7 +40,7 @@ import { } from "@/lib/api"; import { cn } from "@/lib/utils"; -const PAGE_SIZE = 20; +const PAGE_SIZE = 5; const channelFilters = ["全部", "WebRTC", "WebSocket"] as const; type ChannelFilter = (typeof channelFilters)[number]; type SortOrder = "newest" | "oldest"; @@ -96,6 +104,14 @@ export function HistoryPage() { const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailError, setDetailError] = useState(""); + const [deletingId, setDeletingId] = useState(null); + const skipRowClickRef = useRef(false); + + const clearSkipRowClick = useCallback(() => { + window.setTimeout(() => { + skipRowClickRef.current = false; + }, 200); + }, []); const load = useCallback(async () => { setLoading(true); @@ -139,6 +155,45 @@ export function HistoryPage() { } }, []); + const remove = useCallback( + async (conversation: Conversation) => { + const label = conversation.assistantName || "调试会话"; + skipRowClickRef.current = true; + const confirmed = window.confirm(`确认删除对话“${label}”?`); + if (!confirmed) { + clearSkipRowClick(); + return; + } + setDeletingId(conversation.id); + try { + await conversationsApi.remove(conversation.id); + if (detail?.id === conversation.id) { + setDialogOpen(false); + setDetail(null); + } + if (rows.length === 1 && currentPage > 1) { + setCurrentPage(currentPage - 1); + } else { + await load(); + } + } catch (error) { + setLoadError(error instanceof Error ? error.message : "删除失败"); + } finally { + setDeletingId(null); + clearSkipRowClick(); + } + }, + [clearSkipRowClick, currentPage, detail?.id, load, rows.length], + ); + + const handleRowClick = useCallback( + (conversation: Conversation) => { + if (skipRowClickRef.current) return; + void openDetail(conversation); + }, + [openDetail], + ); + function changeSearch(value: string) { setSearch(value); setCurrentPage(1); @@ -244,30 +299,69 @@ export function HistoryPage() { key: "actions", header: "操作", align: "right", - cellClassName: "flex justify-end", cell: (row) => ( - + + + + + + + { + event.preventDefault(); + window.setTimeout(() => void remove(row), 0); + }} + > + {deletingId === row.id ? ( + + ) : ( + + )} + 删除 + + + + ), }, ], - [openDetail, sortOrder], + [deletingId, openDetail, remove, sortOrder], ); const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const safeCurrentPage = Math.min(currentPage, totalPages); const pageStart = (safeCurrentPage - 1) * PAGE_SIZE; - const pageEnd = Math.min(pageStart + rows.length, total); + const pageEnd = pageStart + rows.length; return (
@@ -302,7 +396,7 @@ export function HistoryPage() { loading={loading} error={loadError || null} onRetry={() => void load()} - onRowClick={(row) => void openDetail(row)} + onRowClick={handleRowClick} empty={{ title: total === 0 && !search && filter === "全部" @@ -320,7 +414,7 @@ export function HistoryPage() { summary: total === 0 ? "没有数据" - : `显示 ${pageStart + 1}-${pageEnd} / 共 ${total} 次会话`, + : `显示 ${pageStart + 1}-${Math.min(pageEnd, total)} / 共 ${total} 次会话`, }} /> diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0c5d6e4..0170f9b 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -249,6 +249,8 @@ export const conversationsApi = { }, get: (id: string) => request(`/api/conversations/${id}`), + remove: (id: string) => + request<{ ok: boolean }>(`/api/conversations/${id}`, { method: "DELETE" }), }; // ---------- 工具 ----------