feat(reports): 我的报告 enters on a static shell and joins the prefetch; 过往的报告 pages 10 at a time with 加载更多 (BUG-1123, BUG-1124)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-30 13:45:44 +08:00
co-authored by Claude Opus 5.5
parent 90b1b6fba9
commit 9a616107c2
14 changed files with 470 additions and 33 deletions
+22 -7
View File
@@ -44,6 +44,7 @@ import {
subjectIdFromQuery,
} from "@/lib/subject-birth";
import { consultationSubjectFailureResponse } from "@/lib/consultation-subject-resolver";
import { clampReportLimit, nextReportCursor, parseReportCursor, reportCursorFilter } from "@/lib/report-cursor";
export const runtime = "nodejs";
export const maxDuration = 120;
@@ -107,7 +108,16 @@ export async function GET(request: Request) {
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const subjectId = subjectIdFromQuery(new URL(request.url).searchParams.get("subject"));
const url = new URL(request.url);
// Keyset pages of 10 (BUG-1124). No `limit` is the first page, so an old
// client that never sends one gets the newest 10 (was a fixed 20).
const limit = clampReportLimit(url.searchParams.get("limit"));
const beforeRaw = url.searchParams.get("before");
const cursor = parseReportCursor(beforeRaw);
if (beforeRaw && !cursor) {
return NextResponse.json({ error: "报告列表请求无效" }, { status: 400 });
}
const subjectId = subjectIdFromQuery(url.searchParams.get("subject"));
if (subjectId !== "self" && !isChartSubjectId(subjectId)) {
return NextResponse.json({ error: "星盘不存在", code: "subject_not_found" }, { status: 404 });
}
@@ -118,11 +128,16 @@ export async function GET(request: Request) {
listQuery = subjectId === "self"
? listQuery.is("chart_profile_id", null)
: listQuery.eq("chart_profile_id", subjectId);
const { data, error } = await listQuery
listQuery = listQuery
.order("created_at", { ascending: false })
.limit(20);
.order("id", { ascending: false })
.limit(limit + 1);
if (cursor) listQuery = listQuery.or(reportCursorFilter(cursor));
const { data: pageRows, error } = await listQuery;
if (error) throw error;
const reports = Array.isArray(data) ? data.map((row) => listReportView(row)).filter((report) => report.id) : [];
const nextCursor = nextReportCursor(Array.isArray(pageRows) ? pageRows : [], limit);
const data = Array.isArray(pageRows) ? pageRows.slice(0, limit) : [];
const reports = data.map((row) => listReportView(row)).filter((report) => report.id);
const failed = reports.filter((report) => report.status === "failed" && report.requestId);
if (failed.length > 0) {
const { data: sections, error: sectionError } = await supabase
@@ -144,9 +159,9 @@ export async function GET(request: Request) {
}
for (const [index, report] of reports.entries()) {
if (report.status !== "failed") continue;
const source = Array.isArray(data) ? data.find((row) => (
const source = data.find((row) => (
row && typeof row === "object" && (row as { id?: unknown }).id === report.id
)) : null;
)) ?? null;
reports[index] = listReportView(source, summarizePersonalReportFailure({
themeCount: report.themes.length,
sections: grouped.get(report.requestId) ?? [],
@@ -154,7 +169,7 @@ export async function GET(request: Request) {
}));
}
}
return NextResponse.json({ reports });
return NextResponse.json({ reports, nextCursor });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json(