update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-06-02 10:48:21 -06:00
parent 0460a2f7a0
commit 8444d6e21a

View File

@@ -6,6 +6,10 @@ import { useConfigStore } from "shared/store/config"
import { Icon } from "@iconify/vue"
import { Bar, Radar } from "vue-chartjs"
import { useBreakpoints } from "shared/composables/breakpoints"
import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css"
import { consumeJSONEventStream } from "utils/stream"
import { getCSRFToken } from "utils/functions"
import {
Chart as ChartJS,
CategoryScale,
@@ -72,6 +76,11 @@ const duration = ref<string>("")
const loading = ref(false)
const hasTimeRange = ref(false)
const aiLoading = ref(false)
const aiContent = ref("")
const showAIModal = ref(false)
let aiController: AbortController | null = null
// 时间段选项(与 rank/list.vue 保持一致)
const timeRangeOptions: SelectOption[] = [
{ label: "全部时间", value: "" },
@@ -145,6 +154,68 @@ async function compare() {
}
}
async function analyzeWithAI() {
if (aiController) {
aiController.abort()
}
const controller = new AbortController()
aiController = controller
const timeRangeLabel =
timeRangeOptions.find((o) => o.value === duration.value)?.label ?? "全部时间"
showAIModal.value = true
aiContent.value = ""
aiLoading.value = true
const headers: Record<string, string> = { "Content-Type": "application/json" }
const csrfToken = getCSRFToken()
if (csrfToken) headers["X-CSRFToken"] = csrfToken
try {
const response = await fetch("/api/ai/class_pk", {
method: "POST",
headers,
body: JSON.stringify({
comparisons: comparisons.value,
time_range_label: timeRangeLabel,
}),
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) aiLoading.value = false
},
onMessage(payload) {
const parsed = payload as { type?: string; content?: string; message?: string }
if (parsed.type === "delta" && parsed.content) {
if (!hasStarted) {
hasStarted = true
aiLoading.value = false
}
aiContent.value += parsed.content
} else if (parsed.type === "error") {
throw new Error(parsed.message || "AI 服务异常")
} else if (parsed.type === "done" && !hasStarted) {
aiLoading.value = false
}
},
})
} catch (error: any) {
if (controller.signal.aborted) return
message.error(error?.message || "AI 分析失败,请稍后再试")
aiLoading.value = false
} finally {
if (aiController === controller) aiController = null
}
}
// 计算排名颜色
function getRankColor(index: number) {
if (index === 0) return { type: "success" as const, text: "1" }
@@ -566,8 +637,41 @@ const radarChartOptions = {
>
开始PK
</n-button>
<n-button
type="info"
@click="analyzeWithAI"
:loading="aiLoading"
:disabled="comparisons.length === 0"
style="margin-top: 26px"
>
<template #icon>
<Icon icon="mingcute:ai-line" />
</template>
AI分析
</n-button>
</n-flex>
<n-modal
v-model:show="showAIModal"
preset="card"
title="AI 分析报告"
:style="{ width: '800px', maxWidth: '95vw' }"
>
<n-spin :show="aiLoading" :delay="50">
<div style="min-height: 200px">
<MdPreview v-if="aiContent" :model-value="aiContent" />
<n-flex
v-else-if="!aiLoading"
align="center"
justify="center"
style="min-height: 200px"
>
<n-empty description="暂无分析内容" />
</n-flex>
</div>
</n-spin>
</n-modal>
<!-- 班级对比卡片 -->
<n-grid v-if="comparisons.length > 0" :cols="2" :x-gap="16" :y-gap="16">
<n-gi
@@ -802,16 +906,31 @@ const radarChartOptions = {
<!-- 可视化图表 - 专注于对比 -->
<template v-if="comparisons.length > 0">
<!-- 综合分对比 - 一眼看出胜负 -->
<n-card title="综合分对比满分100" style="margin-top: 20px">
<div style="height: 300px">
<Bar
v-if="compositeScoreChartData"
:data="compositeScoreChartData"
:options="compositeScoreChartOptions"
/>
</div>
</n-card>
<!-- 综合分对比 + 多维度雷达图 同行 -->
<n-grid style="margin-top: 20px" :cols="2" :x-gap="16">
<n-gi>
<n-card title="综合分对比满分100" style="height: 100%">
<div style="height: 380px">
<Bar
v-if="compositeScoreChartData"
:data="compositeScoreChartData"
:options="compositeScoreChartOptions"
/>
</div>
</n-card>
</n-gi>
<n-gi>
<n-card title="多维度综合对比" style="height: 100%">
<div style="height: 380px">
<Radar
v-if="radarChartData"
:data="radarChartData"
:options="radarChartOptions"
/>
</div>
</n-card>
</n-gi>
</n-grid>
<!-- AC核心指标对比 - 三个独立图表并排显示 -->
<n-card title="AC核心指标对比" style="margin-top: 20px">
@@ -912,16 +1031,6 @@ const radarChartOptions = {
</n-grid>
</n-card>
<!-- 多维度雷达图 - 综合对比 -->
<n-card title="多维度综合对比" style="margin-top: 20px">
<div style="height: 500px">
<Radar
v-if="radarChartData"
:data="radarChartData"
:options="radarChartOptions"
/>
</div>
</n-card>
</template>
<!-- 对比表格 -->