feat(智能分析): 解题表格改服务端分页,/ai/detail 不再下发整份 solved
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
This commit is contained in:
2026-09-03 03:18:18 -06:00
parent 4d7be969d9
commit 8a88fe8d99
9 changed files with 225 additions and 89 deletions

View File

@@ -28,8 +28,8 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip)
const aiStore = useAIStore()
const { chartKey } = useChartTheme()
// solved[].attempts 是后端算的「到首次通过为止的提交次数」,分档放在前端,
// 想调档位不用改契约
// detailsData.attempts 是后端算的「每道题到首次通过为止的提交次数」,分档放在前端,
// 想调档位不用改契约。逐题的题名不在这里 —— 明细是分页拿的,别为了 tooltip 把全量拉回来
const BUCKETS = [
{ label: "一次过", color: "#18A058", min: 1, max: 1 },
{ label: "2-3 次", color: "#2080F0", min: 2, max: 3 },
@@ -40,20 +40,20 @@ const BUCKETS = [
const buckets = computed(() =>
BUCKETS.map((bucket) => ({
...bucket,
problems: aiStore.detailsData.solved.filter(
(item) => item.attempts >= bucket.min && item.attempts <= bucket.max,
),
count: aiStore.detailsData.attempts.filter(
(value) => value >= bucket.min && value <= bucket.max,
).length,
})),
)
const show = computed(() => aiStore.detailsData.solved.length > 0)
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.problems.length),
data: buckets.value.map((bucket) => bucket.count),
backgroundColor: buckets.value.map((bucket) => bucket.color),
borderColor: buckets.value.map((bucket) => bucket.color),
borderWidth: 1,
@@ -78,14 +78,11 @@ const options = computed<ChartOptions<"bar">>(() => ({
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.parsed.y} 道题`,
afterLabel: (ctx) => {
const titles = buckets.value[ctx.dataIndex]!.problems.map(
(item) => `${item.problem.displayId} ${item.problem.title}`,
)
if (!titles.length) return ""
if (titles.length <= 5) return titles
return [...titles.slice(0, 4), `… 还有 ${titles.length - 4}`]
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)}%`
},
},
},