diff --git a/ai_knowledge_base_v2/apps/backend/app/api/chat.py b/ai_knowledge_base_v2/apps/backend/app/api/chat.py index a5477a8..7a4f718 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/chat.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/chat.py @@ -136,6 +136,16 @@ def mark_help_card_copied( return api_success(help_card_dict(HelpCardService.mark_copied(db, user=current_user, card_id=card_id))) +@router.delete("/help-card/{card_id}") +def delete_help_card( + card_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> dict: + HelpCardService.delete(db, user=current_user, card_id=card_id) + return api_success() + + @router.post("/session/{session_id}/share-draft") def generate_share_draft( session_id: int, @@ -165,6 +175,16 @@ def mark_share_draft_copied( return api_success(share_draft_dict(ShareDraftService.mark_copied(db, user=current_user, draft_id=draft_id))) +@router.delete("/share-draft/{draft_id}") +def delete_share_draft( + draft_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> dict: + ShareDraftService.delete(db, user=current_user, draft_id=draft_id) + return api_success() + + @router.post("/completions") def completions( payload: ChatCompletionRequest, diff --git a/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py b/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py index e380afd..6067365 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py @@ -62,6 +62,14 @@ class HelpCardService: db.refresh(card) return card + @staticmethod + def delete(db: Session, *, user: User, card_id: int) -> None: + card = db.scalar(select(TeacherHelpCard).where(TeacherHelpCard.id == card_id, TeacherHelpCard.user_id == user.id)) + if card is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="求助卡不存在") + db.delete(card) + db.commit() + def help_card_dict(card: TeacherHelpCard) -> dict: return { diff --git a/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py b/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py index 3c481d7..941649e 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py @@ -61,6 +61,14 @@ class ShareDraftService: db.refresh(draft) return draft + @staticmethod + def delete(db: Session, *, user: User, draft_id: int) -> None: + draft = db.scalar(select(ShareDraft).where(ShareDraft.id == draft_id, ShareDraft.user_id == user.id)) + if draft is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分享稿不存在") + db.delete(draft) + db.commit() + def share_draft_dict(draft: ShareDraft) -> dict: return { diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py b/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py index e87c6ae..4535082 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py @@ -2,6 +2,8 @@ from __future__ import annotations from datetime import UTC, datetime +import pytest +from fastapi import HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool @@ -63,3 +65,14 @@ def test_generate_help_card_from_topic_summary_and_mark_copied(): assert copied.copied == 1 assert copied.copied_at is not None + + other_user = User(id=2, phone="13800000002", name="其他学员", daily_chat_limit=100, daily_chat_used=0) + db.add(other_user) + db.commit() + with pytest.raises(HTTPException) as error: + HelpCardService.delete(db, user=other_user, card_id=card.id) + assert error.value.status_code == 404 + assert db.get(TeacherHelpCard, card.id) is not None + + HelpCardService.delete(db, user=user, card_id=card.id) + assert db.get(TeacherHelpCard, card.id) is None diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py b/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py index 740efd3..7c5ca0b 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py @@ -2,6 +2,8 @@ from __future__ import annotations from datetime import UTC, datetime +import pytest +from fastapi import HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool @@ -65,3 +67,14 @@ def test_generate_share_draft_from_topic_summary_and_mark_copied(): assert copied.copied == 1 assert copied.copied_at is not None + + other_user = User(id=2, phone="13800000002", name="其他学员", daily_chat_limit=100, daily_chat_used=0) + db.add(other_user) + db.commit() + with pytest.raises(HTTPException) as error: + ShareDraftService.delete(db, user=other_user, draft_id=draft.id) + assert error.value.status_code == 404 + assert db.get(ShareDraft, draft.id) is not None + + ShareDraftService.delete(db, user=user, draft_id=draft.id) + assert db.get(ShareDraft, draft.id) is None diff --git a/ai_knowledge_base_v2/apps/user-client/src/App.vue b/ai_knowledge_base_v2/apps/user-client/src/App.vue index 18901b3..dd3e0be 100644 --- a/ai_knowledge_base_v2/apps/user-client/src/App.vue +++ b/ai_knowledge_base_v2/apps/user-client/src/App.vue @@ -36,6 +36,7 @@ const helpCardHistory = ref([]); const shareDraftHistory = ref([]); const reportHistory = ref([]); const personalCenterLoading = ref(false); +const cardOperationPending = ref(false); const finishingTopic = ref(false); const generatingHelpCard = ref(false); const generatingShareDraft = ref(false); @@ -322,6 +323,62 @@ async function copyShareDraft() { } } +async function copyHistoryHelpCard(card: TeacherHelpCard) { + try { + await copyText(card.content); + const updated = await api.markHelpCardCopied(card.id); + helpCardHistory.value = helpCardHistory.value.map((item) => item.id === updated.id ? updated : item); + showToast("求助卡已复制,可再次粘贴给老师"); + } catch (error) { + handleError(error, "复制失败,请稍后重试"); + } +} + +async function copyHistoryShareDraft(draft: ShareDraft) { + try { + await copyText(draft.content); + const updated = await api.markShareDraftCopied(draft.id); + shareDraftHistory.value = shareDraftHistory.value.map((item) => item.id === updated.id ? updated : item); + showToast("分享稿已复制,可再次粘贴到班级群"); + } catch (error) { + handleError(error, "复制失败,请稍后重试"); + } +} + +async function deleteHistoryHelpCard(cardId: number, done: (success: boolean) => void) { + if (cardOperationPending.value) return; + cardOperationPending.value = true; + try { + await api.deleteHelpCard(cardId); + helpCardHistory.value = helpCardHistory.value.filter((item) => item.id !== cardId); + if (helpCard.value?.id === cardId) helpCard.value = null; + showToast("求助卡已删除"); + done(true); + } catch (error) { + done(false); + handleError(error, "求助卡删除失败"); + } finally { + cardOperationPending.value = false; + } +} + +async function deleteHistoryShareDraft(draftId: number, done: (success: boolean) => void) { + if (cardOperationPending.value) return; + cardOperationPending.value = true; + try { + await api.deleteShareDraft(draftId); + shareDraftHistory.value = shareDraftHistory.value.filter((item) => item.id !== draftId); + if (shareDraft.value?.id === draftId) shareDraft.value = null; + showToast("分享稿已删除"); + done(true); + } catch (error) { + done(false); + handleError(error, "分享稿删除失败"); + } finally { + cardOperationPending.value = false; + } +} + async function openPersonalCenter(section: "overview" | "review" | "records" = "overview") { personalCenterSection.value = section; personalCenterOpen.value = true; @@ -522,8 +579,13 @@ async function copyText(text: string) { :reports="reportHistory" :help-cards="helpCardHistory" :share-drafts="shareDraftHistory" + :operation-pending="cardOperationPending" @close="personalCenterOpen = false" @logout="personalCenterOpen = false; logoutDialogOpen = true" + @copy-help-card="copyHistoryHelpCard" + @copy-share-draft="copyHistoryShareDraft" + @delete-help-card="deleteHistoryHelpCard" + @delete-share-draft="deleteHistoryShareDraft" /> diff --git a/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue b/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue index b882c39..ed448b1 100644 --- a/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue +++ b/ai_knowledge_base_v2/apps/user-client/src/components/PersonalCenterDialog.vue @@ -4,11 +4,13 @@ import { CheckCircle2, ChevronDown, CircleMinus, + Copy, LifeBuoy, LogOut, PackageCheck, Share2, Sparkles, + Trash2, } from "@lucide/vue"; import { computed, ref } from "vue"; @@ -26,15 +28,21 @@ const props = defineProps<{ reports: PeriodicReport[]; helpCards: TeacherHelpCard[]; shareDrafts: ShareDraft[]; + operationPending: boolean; }>(); -defineEmits<{ +const emit = defineEmits<{ close: []; logout: []; + copyHelpCard: [card: TeacherHelpCard]; + copyShareDraft: [draft: ShareDraft]; + deleteHelpCard: [cardId: number, done: (success: boolean) => void]; + deleteShareDraft: [draftId: number, done: (success: boolean) => void]; }>(); const activeSection = ref(props.initialSection); const activeCardSection = ref("help"); +const deletingCard = ref<{ type: CardSection; id: number } | null>(null); const entitlement = computed(() => props.user.entitlement); const displayName = computed(() => props.user.nickname?.trim() || props.user.name); const nameInitial = computed(() => displayName.value.slice(0, 1)); @@ -75,6 +83,19 @@ function settlementStatusLabel(status: string) { failed: "整理失败", }[status] || status; } + +function submitDeleteCard() { + if (!deletingCard.value || props.operationPending) return; + const target = deletingCard.value; + const done = (success: boolean) => { + if (success) deletingCard.value = null; + }; + if (target.type === "help") { + emit("deleteHelpCard", target.id, done); + return; + } + emit("deleteShareDraft", target.id, done); +} + + +

+ 确定删除这张{{ deletingCard.type === "help" ? "老师求助卡" : "班级分享稿" }}吗?删除后无法恢复。 +

+ +