feat(智能分析): 解题表格改服务端分页,/ai/detail 不再下发整份 solved
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:
xuyueandClaude Opus 5 committed 2026-09-03 03:18:18 -06:00
1 parent 4d7be969d9
commit 8a88fe8d99
9 files changed
+225 -89

No files matched your search

+40 -3
View File
@@ -1,10 +1,11 @@
import type { DetailsData, DurationData } from "utils/types"
import type { DetailsData, DurationData, SolvedProblem } from "utils/types"
import { aiStreamError, consumeJSONEventStream } from "utils/stream"
import {
getAIDetailData,
getAIDurationData,
getAIHeatmapData,
getAIPinnedReport,
getAISolved,
} from "../api"
export const useAIStore = defineStore("ai", () => {
@@ -23,7 +24,8 @@ export const useAIStore = defineStore("ai", () => {
tags: {},
difficulty: {},
contestCount: 0,
solved: [],
solvedCount: 0,
attempts: [],
flowcharts: [],
activity: [],
errors: [],
@@ -31,10 +33,17 @@ export const useAIStore = defineStore("ai", () => {
})
const heatmapData = ref<{ timestamp: number; value: number }[]>([])
// 解题明细走服务端分页:一个活跃学生一年几百道题,整份跟着 detail 一起下发没必要
const solvedRows = ref<SolvedProblem[]>([])
const solvedTotal = ref(0)
const solvedPage = ref(1)
const solvedPageSize = ref(20)
const loading = reactive({
fetching: false, // 合并 details 和 duration 的 loading
ai: false,
heatmap: false,
solved: false,
})
const mdContent = ref("")
@@ -48,12 +57,13 @@ export const useAIStore = defineStore("ai", () => {
)
detailsData.start = res.start
detailsData.end = res.end
detailsData.solved = res.solved
detailsData.grade = res.grade
detailsData.className = res.className
detailsData.tags = res.tags
detailsData.difficulty = res.difficulty
detailsData.contestCount = res.contestCount
detailsData.solvedCount = res.solvedCount
detailsData.attempts = res.attempts
detailsData.activity = res.activity
detailsData.errors = res.errors
detailsData.rankScope = res.rankScope
@@ -69,6 +79,25 @@ export const useAIStore = defineStore("ai", () => {
durationData.value = res
}
async function fetchSolved(page = solvedPage.value) {
if (!rangeStart.value || !rangeEnd.value) return
loading.solved = true
try {
const res = await getAISolved(
rangeStart.value,
rangeEnd.value,
(page - 1) * solvedPageSize.value,
solvedPageSize.value,
targetUsername.value || undefined,
)
solvedRows.value = res.results
solvedTotal.value = res.total
solvedPage.value = page
} finally {
loading.solved = false
}
}
async function fetchHeatmapData() {
loading.heatmap = true
const res = await getAIHeatmapData(targetUsername.value || undefined)
@@ -83,11 +112,14 @@ export const useAIStore = defineStore("ai", () => {
) {
rangeStart.value = start
rangeEnd.value = end
// 换时间范围就回到第一页,否则停在第 5 页但新范围只有两页
solvedPage.value = 1
loading.fetching = true
try {
await Promise.all([
fetchDetailsData(start, end),
fetchDurationData(end, duration),
fetchSolved(1),
])
} finally {
loading.fetching = false
@@ -196,6 +228,11 @@ export const useAIStore = defineStore("ai", () => {
return {
fetchAnalysisData,
fetchHeatmapData,
fetchSolved,
solvedRows,
solvedTotal,
solvedPage,
solvedPageSize,
fetchAIAnalysis,
fetchPinnedReport,
simulatePinnedStream,