update
This commit is contained in:
@@ -6,6 +6,10 @@ import { useConfigStore } from "shared/store/config"
|
|||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { Bar, Radar } from "vue-chartjs"
|
import { Bar, Radar } from "vue-chartjs"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
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 {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -72,6 +76,11 @@ const duration = ref<string>("")
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const hasTimeRange = 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 保持一致)
|
// 时间段选项(与 rank/list.vue 保持一致)
|
||||||
const timeRangeOptions: SelectOption[] = [
|
const timeRangeOptions: SelectOption[] = [
|
||||||
{ label: "全部时间", value: "" },
|
{ 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) {
|
function getRankColor(index: number) {
|
||||||
if (index === 0) return { type: "success" as const, text: "1" }
|
if (index === 0) return { type: "success" as const, text: "1" }
|
||||||
@@ -566,8 +637,41 @@ const radarChartOptions = {
|
|||||||
>
|
>
|
||||||
开始PK
|
开始PK
|
||||||
</n-button>
|
</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-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-grid v-if="comparisons.length > 0" :cols="2" :x-gap="16" :y-gap="16">
|
||||||
<n-gi
|
<n-gi
|
||||||
@@ -802,16 +906,31 @@ const radarChartOptions = {
|
|||||||
|
|
||||||
<!-- 可视化图表 - 专注于对比 -->
|
<!-- 可视化图表 - 专注于对比 -->
|
||||||
<template v-if="comparisons.length > 0">
|
<template v-if="comparisons.length > 0">
|
||||||
<!-- 综合分对比 - 一眼看出胜负 -->
|
<!-- 综合分对比 + 多维度雷达图 同行 -->
|
||||||
<n-card title="综合分对比(满分100)" style="margin-top: 20px">
|
<n-grid style="margin-top: 20px" :cols="2" :x-gap="16">
|
||||||
<div style="height: 300px">
|
<n-gi>
|
||||||
<Bar
|
<n-card title="综合分对比(满分100)" style="height: 100%">
|
||||||
v-if="compositeScoreChartData"
|
<div style="height: 380px">
|
||||||
:data="compositeScoreChartData"
|
<Bar
|
||||||
:options="compositeScoreChartOptions"
|
v-if="compositeScoreChartData"
|
||||||
/>
|
:data="compositeScoreChartData"
|
||||||
</div>
|
:options="compositeScoreChartOptions"
|
||||||
</n-card>
|
/>
|
||||||
|
</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核心指标对比 - 三个独立图表并排显示 -->
|
<!-- AC核心指标对比 - 三个独立图表并排显示 -->
|
||||||
<n-card title="AC核心指标对比" style="margin-top: 20px">
|
<n-card title="AC核心指标对比" style="margin-top: 20px">
|
||||||
@@ -912,16 +1031,6 @@ const radarChartOptions = {
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
</n-card>
|
</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>
|
</template>
|
||||||
|
|
||||||
<!-- 对比表格 -->
|
<!-- 对比表格 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user