Files
OJ2/apps/web/src/oj/ai/components/AttemptsChart.vue
yuetsh 8a88fe8d99
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(智能分析): 解题表格改服务端分页,/ai/detail 不再下发整份 solved
原来 /ai/detail 把区间内做出来的每道题连排名带等级整份下发,一个活跃学生一年几百道,
一次请求就是几百 KB,而表格一屏只看得到二十行。

拆成两支:
- GET /ai/detail 只留聚合(solvedCount、difficulty、tags、attempts、activity、errors…)
- GET /ai/solved?offset&limit 按首次通过时间升序分页给逐题明细

排名只跟当前这批题有关,所以分页那支只算一页的 problemIds,不必把整年算一遍。抽出
firstAcQuery / buildSolved / listSolved 三个函数,detail 和分页两边共用。

前端相应改动:
- 难度分布改读后端的 difficulty 聚合(本来就在下发,之前是从逐题列表里现数的)
- 几次做对改读新的 attempts 数组(每道题的尝试次数);tooltip 里的题名去掉了 ——
  明细是分页拿的,不该为了一个 tooltip 把全量拉回来
- Overview 用 solvedCount
- SolvedTable 的代码提交那张走 remote 分页,流程图那张仍是本地分页(一个 OJ 的
  流程图题就那么几道,全量下发没问题);换时间范围回到第一页
- 表格原来是 max-height 1500 的滚动区,现在由分页兜住,滚动条和分页器不再并存

/ai/analysis 的 prompt 仍然带逐题明细(少了它模型只剩聚合数字),但顺手卡了 200 条 ——
以前是整份 solved 无上限塞进去,题做得多的学生一次调用能顶好几倍 token。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZuPwqDmLEiK9zgQ9z9sVn
2026-09-03 03:18:18 -06:00

98 lines
2.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<n-card title="几次做对" size="small" v-if="show">
<template #header-extra>
<n-text depth="3" style="font-size: 12px">
通过前提交了几次看有没有在死磕
</n-text>
</template>
<div class="chart">
<Bar :key="chartKey" :data="data" :options="options" />
</div>
</n-card>
</template>
<script setup lang="ts">
import type { ChartOptions } from "chart.js"
import { Bar } from "vue-chartjs"
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Tooltip,
} from "chart.js"
import { useAIStore } from "oj/store/ai"
import { useChartTheme } from "shared/composables/chartTheme"
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip)
const aiStore = useAIStore()
const { chartKey } = useChartTheme()
// detailsData.attempts 是后端算的「每道题到首次通过为止的提交次数」,分档放在前端,
// 想调档位不用改契约。逐题的题名不在这里 —— 明细是分页拿的,别为了 tooltip 把全量拉回来
const BUCKETS = [
{ label: "一次过", color: "#18A058", min: 1, max: 1 },
{ label: "2-3 次", color: "#2080F0", min: 2, max: 3 },
{ label: "4-6 次", color: "#F0A020", min: 4, max: 6 },
{ label: "7 次以上", color: "#D03050", min: 7, max: Infinity },
]
const buckets = computed(() =>
BUCKETS.map((bucket) => ({
...bucket,
count: aiStore.detailsData.attempts.filter(
(value) => value >= bucket.min && value <= bucket.max,
).length,
})),
)
const show = computed(() => aiStore.detailsData.attempts.length > 0)
const data = computed(() => ({
labels: buckets.value.map((bucket) => bucket.label),
datasets: [
{
label: "题目数量",
data: buckets.value.map((bucket) => bucket.count),
backgroundColor: buckets.value.map((bucket) => bucket.color),
borderColor: buckets.value.map((bucket) => bucket.color),
borderWidth: 1,
borderRadius: 4,
maxBarThickness: 64,
},
],
}))
const options = computed<ChartOptions<"bar">>(() => ({
responsive: true,
maintainAspectRatio: false,
scales: {
x: { grid: { display: false } },
y: {
beginAtZero: true,
ticks: { stepSize: 1, precision: 0 },
title: { display: true, text: "题目数量" },
},
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => {
const value = Number(ctx.parsed.y)
const total = aiStore.detailsData.attempts.length
const percent = total ? (value / total) * 100 : 0
return `${value} 道题(占 ${percent.toFixed(0)}%`
},
},
},
},
}))
</script>
<style scoped>
.chart {
height: 300px;
width: 100%;
}
</style>