Some checks failed
Deploy / deploy (push) Has been cancelled
原来 /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
98 lines
2.7 KiB
Vue
98 lines
2.7 KiB
Vue
<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>
|