fix(report): group chart cards in the existing grid and stop remounting on scroll (BUG-616, BUG-617)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 18:44:14 +08:00
co-authored by Cursor
parent 584a56bd81
commit 87daffe251
10 changed files with 449 additions and 66 deletions
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { memo, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -10,6 +10,7 @@ import {
parseReportChartBlock,
toNorthIndianChart,
} from "@/lib/report-chart-block";
import { reportChartGrid } from "@/lib/report-chart-grid-rehype";
import { NorthIndianChartSvg } from "@/components/personal-report/vedic-chart-svg";
import {
buildLongformOutline,
@@ -90,6 +91,7 @@ function renderMarkdown(markdown: string, headings: readonly LongformHeading[])
<ReactMarkdown
components={markdownComponents(headings)}
disallowedElements={["script", "iframe", "object", "embed", "img"]}
rehypePlugins={[reportChartGrid]}
remarkPlugins={[remarkGfm]}
skipHtml
unwrapDisallowed
@@ -100,7 +102,7 @@ function renderMarkdown(markdown: string, headings: readonly LongformHeading[])
);
}
function LazyMarkdownSection({
const LazyMarkdownSection = memo(function LazyMarkdownSection({
section,
}: {
section: LongformSection;
@@ -120,21 +122,24 @@ function LazyMarkdownSection({
observer.observe(ref.current);
return () => observer.disconnect();
}, [visible]);
const content = useMemo(
() => (visible ? renderMarkdown(section.markdown, section.headings) : (
<h2 id={section.id}>{section.title}</h2>
)),
[visible, section],
);
return (
<section ref={ref} className="personal-report-md-section">
{visible ? renderMarkdown(section.markdown, section.headings) : (
<h2 id={section.id}>{section.title}</h2>
)}
{content}
</section>
);
}
});
export function PersonalReportMarkdownView({ markdown }: { markdown: string }) {
const outline = useMemo(() => buildLongformOutline(markdown), [markdown]);
const [activeId, setActiveId] = useState(outline.headings[0]?.id ?? "");
function ReportToc({ headings }: { headings: readonly LongformHeading[] }) {
const [activeId, setActiveId] = useState(headings[0]?.id ?? "");
useEffect(() => {
if (outline.headings.length === 0 || typeof IntersectionObserver === "undefined") return;
if (headings.length === 0 || typeof IntersectionObserver === "undefined") return;
const observer = new IntersectionObserver((entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
@@ -142,29 +147,41 @@ export function PersonalReportMarkdownView({ markdown }: { markdown: string }) {
const id = visible[0]?.target.id;
if (id) setActiveId(id);
}, { rootMargin: "-20% 0px -70% 0px", threshold: [0, 1] });
for (const item of outline.headings) {
for (const item of headings) {
const node = document.getElementById(item.id);
if (node) observer.observe(node);
}
return () => observer.disconnect();
}, [outline.headings]);
}, [headings]);
return (
<nav aria-label="报告目录" className="personal-report-toc">
<details className="personal-report-toc-drawer">
<summary></summary>
<TocList activeId={activeId} items={headings} />
</details>
<div className="personal-report-toc-desktop">
<p></p>
<TocList activeId={activeId} items={headings} />
</div>
</nav>
);
}
export function PersonalReportMarkdownView({ markdown }: { markdown: string }) {
const outline = useMemo(() => buildLongformOutline(markdown), [markdown]);
const lead = useMemo(
() => (outline.leadMarkdown ? renderMarkdown(outline.leadMarkdown, outline.headings) : null),
[outline],
);
return (
<div className="personal-report-md-layout">
<nav aria-label="报告目录" className="personal-report-toc">
<details className="personal-report-toc-drawer">
<summary></summary>
<TocList activeId={activeId} items={outline.headings} />
</details>
<div className="personal-report-toc-desktop">
<p></p>
<TocList activeId={activeId} items={outline.headings} />
</div>
</nav>
<ReportToc headings={outline.headings} />
<article className="personal-report-document personal-report-md-article">
{outline.leadMarkdown ? (
{lead ? (
<section className="personal-report-md-lead">
{renderMarkdown(outline.leadMarkdown, outline.headings)}
{lead}
</section>
) : null}
{outline.sections.filter((section) => !section.eager).map((section) => (