diff --git a/src/oj/rank/list.vue b/src/oj/rank/list.vue index 57da47e..0438ac0 100644 --- a/src/oj/rank/list.vue +++ b/src/oj/rank/list.vue @@ -9,7 +9,7 @@ import { getClassPK, } from "oj/api" import { useBreakpoints } from "shared/composables/breakpoints" -import { getACRate } from "utils/functions" +import { getACRate, getCSRFToken } from "utils/functions" import type { Rank } from "utils/types" import Pagination from "shared/components/Pagination.vue" import { ChartType } from "utils/constants" @@ -18,6 +18,9 @@ import Chart from "./components/Chart.vue" import Index from "./components/Index.vue" import { useUserStore } from "shared/store/user" import { Icon } from "@iconify/vue" +import { MdPreview } from "md-editor-v3" +import "md-editor-v3/lib/preview.css" +import { consumeJSONEventStream } from "utils/stream" const gradeOptions = [ { label: "24年级", value: 24 }, @@ -57,6 +60,11 @@ const showClassDetailModal = ref(false) const classDetailData = ref(null) const classDetailLoading = ref(false) +const classDetailAiLoading = ref(false) +const classDetailAiContent = ref("") +const showClassDetailAiModal = ref(false) +let classDetailAiController: AbortController | null = null + async function loadClassDetail(className: string) { showClassDetailModal.value = true classDetailLoading.value = true @@ -71,6 +79,60 @@ async function loadClassDetail(className: string) { } } +async function analyzeSingleClassWithAI() { + if (!classDetailData.value) return + if (classDetailAiController) classDetailAiController.abort() + const controller = new AbortController() + classDetailAiController = controller + + showClassDetailModal.value = false + showClassDetailAiModal.value = true + classDetailAiContent.value = "" + classDetailAiLoading.value = true + + const headers: Record = { "Content-Type": "application/json" } + const csrfToken = getCSRFToken() + if (csrfToken) headers["X-CSRFToken"] = csrfToken + + try { + const response = await fetch("/api/ai/class_single", { + method: "POST", + headers, + body: JSON.stringify({ comparison: classDetailData.value }), + signal: controller.signal, + }) + if (!response.ok) throw new Error("AI 分析生成失败") + + let hasStarted = false + await consumeJSONEventStream(response, { + signal: controller.signal, + onEvent(event) { + if (event === "end" && !hasStarted) classDetailAiLoading.value = false + }, + onMessage(payload) { + const parsed = payload as { type?: string; content?: string; message?: string } + if (parsed.type === "delta" && parsed.content) { + if (!hasStarted) { + hasStarted = true + classDetailAiLoading.value = false + } + classDetailAiContent.value += parsed.content + } else if (parsed.type === "error") { + throw new Error(parsed.message || "AI 服务异常") + } else if (parsed.type === "done" && !hasStarted) { + classDetailAiLoading.value = false + } + }, + }) + } catch (error: any) { + if (controller.signal.aborted) return + message.error(error?.message || "AI 分析失败,请稍后再试") + classDetailAiLoading.value = false + } finally { + if (classDetailAiController === controller) classDetailAiController = null + } +} + interface ClassRank { rank: number class_name: string @@ -674,10 +736,21 @@ watch( - + 综合分: {{ classDetailData.composite_score.toFixed(1) }} + + + AI分析 + + + + +
+ + + + +
+
+