feat(report): replace the reader edition with the full-data report
Generate the full-data edition (pl9_personal_long_report.v4) and remove the reader edition. English still contains Han, so the API withholds it (BUG-1272, investigating). This commit is not pushed to staging.
This commit is contained in:
+2
-1
@@ -634,7 +634,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
|
||||
### Personal report centre
|
||||
|
||||
- **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title.
|
||||
- **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title.
|
||||
It used to be a standalone full-screen route with a `report-center-shell` root, a `report-center-topbar` holding one 「返回对话」 link, and a `report-center-hero` with a page-sized h1 — the sidebar vanished the moment you opened it, and the only way back was that link.
|
||||
- **Row list, not a card grid:** `.report-center-list` is one column of `.report-center-row`, hairline-separated by a 1px grid gap over a `--color-border` ground so each boundary is a single rule rather than two touching borders. A card grid costs one scan per card; past five or six reports the reader is looking for state, and a single column puts every status chip on the same x. D12.
|
||||
- **Paging (2026-09-30, BUG-1124):** the list shows the newest 10; under it an outline 「加载更多」 button (centred, at least 160px; full width below 767px) brings the next 10. While that page loads the button reads 「加载中」 and is disabled — no spinner. It disappears when nothing older exists. A failed page is a toast 「更早的报告没读出来,再试一次。」, the list stays. The 3s poll while a report is generating and the 刷新 button re-read the first page only and merge it over the pages already opened, so a refresh never collapses the list back to 10. Entering 我的报告 shows the cached first page at once and revalidates behind it (BUG-1123); a cold entry keeps the one shared waiting state.
|
||||
@@ -664,6 +664,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
- **Switching editions keeps your place (BUG-1108):** the chapter at the top of the reader is remembered; the new edition renders every chapter up to it at once and scrolls back to it.
|
||||
- **判断不了的分盘提示 (2026-10-01, BUG-1139):** for a segment-v1 adoption whose card left D9/D10 undetermined, the longform body carries one plain paragraph directly under each such chart heading and its 数值位置表 heading — no new component, no blockquote, no colour. Under a chart heading it becomes the chart card's caption (`report-chart-grid-rehype.ts::parseCard` accepts exactly one paragraph between heading and fence), so the chart grid never splits. Added before the snapshot hash; legacy reports are untouched.
|
||||
- **到底部 / 回到顶部:** one floating pill (`.report-edge-jump`, fixed bottom-right, same look as the chat's 跳到最新). It reads 「到底部」 until the reader is within 480px of the end, then 「回到顶部」 — never both. Sections below the fold mount lazily, so the click first mounts every section synchronously (`flushSync`) and then scrolls to `scrollHeight`, landing on the real end in one click. It is not a scroll follower and owns no anchor; it is hidden on paper.
|
||||
- **Long full-data reports (2026-10-08):** when the Markdown is longer than 200,000 characters and has more than one chapter, the reader shows one chapter at a time. The TOC and 「上一章 / 下一章」 (44px, screen only) change the chapter; the article carries `data-chapter-index` so a 中文 / English switch returns to the same chapter. 「到底部」 opens the last chapter. Shorter reports keep the lazy-section reader. There is no full-text search. Print still mounts every chapter. Markdown download stays on the existing 「导出」 sheet.
|
||||
- **TOC rail:** a persistent right-hand column built from the outline's own `##`/`###` ids — no second slugger, and nothing that touches the chart-grid rehype pass. It is sticky under the report's own action bar, highlights the section in view via `IntersectionObserver`, and marks it with `aria-current="location"` plus a 2px `--color-action` bar. Placement is explicit (`grid-column: 2`) rather than DOM-ordered, so the narrow-screen drawer can stay first in the source. Below 860px the rail is gone and what remains is the collapsed 「目录」 drawer.
|
||||
- **Paper stays paper:** the rail reads in the app palette because it is chrome. `--report-paper` / `--report-rule` / `--report-accent` are the document's own, deliberately out of step with the app accent, and nothing in this surface puts them on chrome or takes chrome colour onto the page. D11, continuing D3.
|
||||
- **Surface:** page floor `--color-canvas-soft`; the Markdown article is a `--color-canvas` sheet with a hairline and `--radius-lg`. Print flattens the sheet, hides chrome and the TOC, and pins the light palette.
|
||||
|
||||
@@ -184,7 +184,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
- 盘面不显示引擎品牌,也不替换成「自研引擎」「本站引擎」。岁差、交点等计算口径与准确性边界照旧。
|
||||
- 图标说明只写名称和已有事实:上升、行星、逆行、星座名称/编号、宫位、ASC/MC 等。没有可靠度数就不写度数,不加吉凶、性格或个人运势解释。
|
||||
- 鼠标悬停、键盘聚焦、手机轻点展示同一份说明,以标记旁的小气泡出现,不遮住被说明的标记。关闭说明不改变西洋相位选择。2026-09-29 晚起不再有默认提示句(原「停在或轻点盘内标记可查看说明。」已删,BUG-1101)。
|
||||
- 「我的报告」生成卡片:标题写得到什么「完整本命报告」,说明一句「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」,按钮写动作「生成报告」。标题与按钮不说同一件事(BUG-1103)。
|
||||
- 「我的报告」生成卡片:标题写得到什么「完整本命报告」,说明一句「星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。」,按钮写动作「生成报告」。标题与按钮不说同一件事(BUG-1103)。完整数据版超过约 20 万字符时,阅读页一次只画一章,目录和「上一章 / 下一章」换章;不另做全文搜索。导出仍是原来的「导出」按钮,下载 Markdown。
|
||||
- 分盘庙旺仅指入旺、自宫、落陷三档;未命中写「—」,上升写「不适用」。同位仅 D9 展示,其他分盘不留空列。缺能力声明不装作已支持。
|
||||
|
||||
## 星盘空状态、0 年大运、星历切日期
|
||||
|
||||
@@ -4298,6 +4298,32 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
@media (max-width: 767px) {
|
||||
.report-edge-jump { right: var(--space-4); bottom: calc(var(--space-4) + env(safe-area-inset-bottom)); }
|
||||
}
|
||||
.personal-report-chapter-pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
.personal-report-chapter-pager button {
|
||||
min-height: 44px;
|
||||
padding: 0 var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
font: inherit;
|
||||
font-size: var(--type-body-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.personal-report-chapter-pager button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.personal-report-chapter-pager span {
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.personal-report-toc-drawer { display: none; }
|
||||
.personal-report-toc-desktop p {
|
||||
margin: 0 0 var(--space-2);
|
||||
|
||||
@@ -214,7 +214,7 @@ export function PersonalReportCenter() {
|
||||
{/* The title says what you get, the button says what it does (2026-09-29):
|
||||
「生成个人报告」 over a 「生成报告」 button said the same thing twice. */}
|
||||
<h2 id="report-center-create-title">完整本命报告</h2>
|
||||
<p>星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。</p>
|
||||
<p>星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。</p>
|
||||
<GeneratePersonalReportButton onCreated={() => void load()} />
|
||||
</section>
|
||||
<section className="report-center-section">
|
||||
|
||||
@@ -107,9 +107,9 @@ function renderMarkdown(markdown: string, headings: readonly LongformHeading[],
|
||||
}
|
||||
|
||||
/**
|
||||
* 「到底部」 has to mount every lazy section before it scrolls. The flag lives
|
||||
* outside React state so the article view itself stays state-free (its tree is
|
||||
* memoized and must not re-render as a whole); only the sections subscribe.
|
||||
* 「到底部」 has to mount every lazy section before it scrolls. Scroll position
|
||||
* stays outside this view. The only state here is the chapter index and the
|
||||
* print flag; markdown trees stay in useMemo.
|
||||
*/
|
||||
type RevealSignal = { revealed: boolean; listeners: Set<() => void> };
|
||||
|
||||
@@ -163,9 +163,23 @@ const LazyMarkdownSection = memo(function LazyMarkdownSection({
|
||||
);
|
||||
});
|
||||
|
||||
function ReportToc({ headings, language }: { headings: readonly LongformHeading[]; language: ReportLanguage }) {
|
||||
/** Above this size the reader shows one chapter at a time. Shorter reports keep lazy sections. */
|
||||
export const FULL_DATA_CHAPTER_PAGE_CHARS = 200_000;
|
||||
|
||||
function ReportToc({
|
||||
headings,
|
||||
language,
|
||||
onSelect,
|
||||
activeHeadingId,
|
||||
}: {
|
||||
headings: readonly LongformHeading[];
|
||||
language: ReportLanguage;
|
||||
onSelect?: (id: string) => void;
|
||||
activeHeadingId?: string;
|
||||
}) {
|
||||
const label = language === "en" ? "Contents" : "目录";
|
||||
const [activeId, setActiveId] = useState(headings[0]?.id ?? "");
|
||||
const [observedId, setObservedId] = useState(headings[0]?.id ?? "");
|
||||
const activeId = activeHeadingId || observedId;
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0 || typeof IntersectionObserver === "undefined") return;
|
||||
@@ -174,7 +188,7 @@ function ReportToc({ headings, language }: { headings: readonly LongformHeading[
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((left, right) => left.boundingClientRect.top - right.boundingClientRect.top);
|
||||
const id = visible[0]?.target.id;
|
||||
if (id) setActiveId(id);
|
||||
if (id) setObservedId(id);
|
||||
}, { rootMargin: "-20% 0px -70% 0px", threshold: [0, 1] });
|
||||
for (const item of headings) {
|
||||
const node = document.getElementById(item.id);
|
||||
@@ -187,11 +201,11 @@ function ReportToc({ headings, language }: { headings: readonly LongformHeading[
|
||||
<nav aria-label={language === "en" ? "Report contents" : "报告目录"} className="personal-report-toc">
|
||||
<details className="personal-report-toc-drawer">
|
||||
<summary>{label}</summary>
|
||||
<TocList activeId={activeId} items={headings} />
|
||||
<TocList activeId={activeId} items={headings} onSelect={onSelect} />
|
||||
</details>
|
||||
<div className="personal-report-toc-desktop">
|
||||
<p>{label}</p>
|
||||
<TocList activeId={activeId} items={headings} />
|
||||
<TocList activeId={activeId} items={headings} onSelect={onSelect} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
@@ -204,17 +218,92 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
drawnThrough?: number | null;
|
||||
}) {
|
||||
const outline = useMemo(() => buildLongformOutline(markdown), [markdown]);
|
||||
const chapters = useMemo(() => outline.sections.filter((section) => !section.eager), [outline]);
|
||||
const paged = markdown.length > FULL_DATA_CHAPTER_PAGE_CHARS && chapters.length > 1;
|
||||
const [chapter, setChapter] = useState(() => {
|
||||
if (drawnThrough === null) return 0;
|
||||
return Math.min(Math.max(drawnThrough, 0), Math.max(chapters.length - 1, 0));
|
||||
});
|
||||
const [printing, setPrinting] = useState(false);
|
||||
const reveal = useMemo<RevealSignal>(() => ({ revealed: false, listeners: new Set() }), []);
|
||||
useEffect(() => {
|
||||
if (!paged) return;
|
||||
const before = () => setPrinting(true);
|
||||
const after = () => setPrinting(false);
|
||||
window.addEventListener("beforeprint", before);
|
||||
window.addEventListener("afterprint", after);
|
||||
return () => {
|
||||
window.removeEventListener("beforeprint", before);
|
||||
window.removeEventListener("afterprint", after);
|
||||
};
|
||||
}, [paged]);
|
||||
// Synchronous on purpose: the caller scrolls to scrollHeight right after, and
|
||||
// that height is only final once every lazy section has committed.
|
||||
const revealAll = useCallback(() => flushSync(() => {
|
||||
if (paged) {
|
||||
setChapter(Math.max(chapters.length - 1, 0));
|
||||
return;
|
||||
}
|
||||
reveal.revealed = true;
|
||||
for (const listener of reveal.listeners) listener();
|
||||
}), [reveal]);
|
||||
}), [chapters.length, paged, reveal]);
|
||||
const lead = useMemo(
|
||||
() => (outline.leadMarkdown ? renderMarkdown(outline.leadMarkdown, outline.headings, language) : null),
|
||||
[outline, language],
|
||||
);
|
||||
const safeChapter = Math.min(chapter, Math.max(chapters.length - 1, 0));
|
||||
const currentChapter = chapters[safeChapter];
|
||||
const pagedChapter = useMemo(() => (
|
||||
paged && currentChapter
|
||||
? renderMarkdown(currentChapter.markdown, currentChapter.headings, language)
|
||||
: null
|
||||
), [paged, currentChapter, language]);
|
||||
const printedChapters = useMemo(() => (
|
||||
paged && printing
|
||||
? chapters.map((section) => renderMarkdown(section.markdown, section.headings, language))
|
||||
: null
|
||||
), [paged, printing, chapters, language]);
|
||||
const openChapter = useCallback((id: string) => {
|
||||
const index = chapters.findIndex((section) => section.id === id || section.headings.some((heading) => heading.id === id));
|
||||
if (index >= 0) setChapter(index);
|
||||
}, [chapters]);
|
||||
|
||||
if (paged) {
|
||||
return (
|
||||
<div className="personal-report-md-layout">
|
||||
<ReportToc
|
||||
activeHeadingId={currentChapter?.id}
|
||||
headings={outline.headings}
|
||||
language={language}
|
||||
onSelect={openChapter}
|
||||
/>
|
||||
<article className="personal-report-document personal-report-md-article" data-chapter-index={safeChapter}>
|
||||
{safeChapter === 0 && lead ? (
|
||||
<section className="personal-report-md-lead">
|
||||
{lead}
|
||||
</section>
|
||||
) : null}
|
||||
{printing
|
||||
? chapters.map((section, index) => (
|
||||
<section className="personal-report-md-section" id={section.id} key={section.id}>
|
||||
{printedChapters?.[index]}
|
||||
</section>
|
||||
))
|
||||
: currentChapter ? (
|
||||
<section className="personal-report-md-section" id={currentChapter.id}>
|
||||
{pagedChapter}
|
||||
</section>
|
||||
) : null}
|
||||
<nav aria-label="章节" className="personal-report-chapter-pager personal-report-screen-only">
|
||||
<button type="button" disabled={safeChapter <= 0} onClick={() => setChapter(safeChapter - 1)}>上一章</button>
|
||||
<span>{safeChapter + 1} / {chapters.length}</span>
|
||||
<button type="button" disabled={safeChapter >= chapters.length - 1} onClick={() => setChapter(safeChapter + 1)}>下一章</button>
|
||||
</nav>
|
||||
</article>
|
||||
<ReportEdgeJump revealAll={revealAll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="personal-report-md-layout">
|
||||
@@ -226,10 +315,10 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
</section>
|
||||
) : null}
|
||||
{/* Each section is one chapter heading, in the same order as the article's h2s. */}
|
||||
{outline.sections.map((section, chapter) => (section.eager ? null : (
|
||||
{outline.sections.map((section, index) => (section.eager ? null : (
|
||||
<LazyMarkdownSection
|
||||
key={section.id}
|
||||
drawn={drawnThrough !== null && chapter <= drawnThrough}
|
||||
drawn={drawnThrough !== null && index <= drawnThrough}
|
||||
language={language}
|
||||
reveal={reveal}
|
||||
section={section}
|
||||
@@ -244,9 +333,11 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
function TocList({
|
||||
items,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: {
|
||||
items: readonly LongformHeading[];
|
||||
activeId: string;
|
||||
onSelect?: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<ol>
|
||||
@@ -256,6 +347,10 @@ function TocList({
|
||||
aria-current={item.id === activeId ? "location" : undefined}
|
||||
className={item.id === activeId ? "is-current" : undefined}
|
||||
href={`#${item.id}`}
|
||||
onClick={onSelect ? (event) => {
|
||||
event.preventDefault();
|
||||
onSelect(item.id);
|
||||
} : undefined}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
|
||||
@@ -218,6 +218,11 @@ function readEnglishEdition(json: Record<string, unknown>): Pick<
|
||||
|
||||
/** Index of the chapter heading at or above the top of the reader, or null at the very top. */
|
||||
function currentChapterIndex(): number | null {
|
||||
const paged = document.querySelector<HTMLElement>(".personal-report-md-article[data-chapter-index]");
|
||||
if (paged) {
|
||||
const index = Number(paged.dataset.chapterIndex);
|
||||
return Number.isFinite(index) ? index : null;
|
||||
}
|
||||
const reader = document.querySelector<HTMLElement>(".personal-report-reader");
|
||||
if (!reader || reader.scrollTop < 40) return null;
|
||||
const top = reader.getBoundingClientRect().top + 80;
|
||||
@@ -282,6 +287,11 @@ export function PersonalReportPage({ reportId, initialLanguage = "zh" }: { repor
|
||||
setDrawnThrough(chapter);
|
||||
if (chapter !== null) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
const article = document.querySelector<HTMLElement>(".personal-report-md-article[data-chapter-index]");
|
||||
if (article) {
|
||||
article.scrollIntoView({ block: "start" });
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll<HTMLElement>(".personal-report-md-article h2")[chapter]?.scrollIntoView({ block: "start" });
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ async function fetchLongformMarkdown(input: Readonly<{
|
||||
const upstream = await input.fetchImpl(`${input.apiBase.replace(/\/$/, "")}/api/professional_report_reference`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ ...input.payload, include_fact_tables: true, edition: "reader_main", languages: ["zh", "en"] }),
|
||||
body: JSON.stringify({ ...input.payload, include_fact_tables: true, edition: "full_data", languages: ["zh", "en"] }),
|
||||
cache: "no-store",
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -77,6 +77,29 @@ test("markdown view skips HTML, blocks remote resources, wraps tables, and lazy-
|
||||
assert.match(markup, /aria-label="报告目录"/);
|
||||
});
|
||||
|
||||
test("long markdown pages one chapter and keeps the first paint small", () => {
|
||||
const chapters = Array.from({ length: 80 }, (_, index) => {
|
||||
const body = index === 0 ? `第一章正文${"字".repeat(80)}` : `秘密正文-${index}`;
|
||||
return `## 第${index}章\n\n${body}\n`;
|
||||
});
|
||||
for (const size of [1_000_000, 3_500_000]) {
|
||||
const markdown = `# 标题\n\n${chapters.join("\n")}${"补".repeat(size)}`;
|
||||
const before = process.memoryUsage().heapUsed;
|
||||
const started = Date.now();
|
||||
const markup = renderToStaticMarkup(React.createElement(PersonalReportMarkdownView, { markdown }));
|
||||
const elapsed = Date.now() - started;
|
||||
const delta = process.memoryUsage().heapUsed - before;
|
||||
assert.ok(elapsed < 1500, `size ${size} first paint ${elapsed}ms heap ${delta}`);
|
||||
assert.match(markup, /第一章正文/);
|
||||
assert.match(markup, /上一章/);
|
||||
assert.match(markup, /下一章/);
|
||||
assert.match(markup, /1 \/ 80/);
|
||||
assert.doesNotMatch(markup, /秘密正文-79/);
|
||||
assert.doesNotMatch(markup, /type="search"|placeholder="搜索"/);
|
||||
assert.ok(delta < 120 * 1024 * 1024, `size ${size} heap delta ${delta}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("export filename includes the report date", () => {
|
||||
assert.equal(personalReportMarkdownFilename("2026-09-06T12:00:00.000Z"), "个人报告-2026-09-06");
|
||||
});
|
||||
|
||||
@@ -175,7 +175,11 @@ test("article markdown trees are memoized and hold no scroll state", () => {
|
||||
const viewEnd = src.indexOf("\nfunction TocList", viewStart);
|
||||
assert.ok(viewStart >= 0 && viewEnd > viewStart);
|
||||
const view = src.slice(viewStart, viewEnd);
|
||||
assert.doesNotMatch(view, /\buseState\b/);
|
||||
// 原值: 这个函数里不得出现 useState。新值: 只允许章节序号和打印标记两处。原因: 超过 20 万字要按章翻页,滚动位置仍不放在这里。
|
||||
assert.deepEqual(view.match(/\buseState\b/g), ["useState", "useState"]);
|
||||
assert.match(view, /const \[chapter, setChapter\] = useState/);
|
||||
assert.match(view, /const \[printing, setPrinting\] = useState\(false\)/);
|
||||
assert.doesNotMatch(view, /useState\([\s\S]{0,80}scroll/i);
|
||||
const lines = src.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (!line.includes("renderMarkdown(")) return;
|
||||
|
||||
@@ -101,12 +101,13 @@ function executeReadyReportExport(cached: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
test("new longform generation requests the reader_main edition", () => {
|
||||
test("new longform generation requests the full_data edition", () => {
|
||||
const generateSource = readFileSync(
|
||||
new URL("../src/lib/personal-report-longform-generate.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(generateSource, /edition:\s*"reader_main"/);
|
||||
assert.match(generateSource, /edition:\s*"full_data"/);
|
||||
assert.doesNotMatch(generateSource, /edition:\s*"reader_main"/);
|
||||
assert.match(generateSource, /include_fact_tables:\s*true/);
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ test("switching editions keeps the reader on the same chapter (BUG-1108)", () =>
|
||||
assert.match(page, /setDrawnThrough\(chapter\);/);
|
||||
assert.match(page, /drawnThrough=\{drawnThrough\}/);
|
||||
const view = read("../src/components/personal-report/personal-report-markdown-view.tsx");
|
||||
assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/);
|
||||
// 原值: drawn={drawnThrough !== null && chapter <= drawnThrough}。新值: 循环变量改为 index,超长报告的初始章取 drawnThrough。原因: 懒加载路径改名;按章翻页仍从切换前的章号打开。
|
||||
assert.match(view, /drawn=\{drawnThrough !== null && index <= drawnThrough\}/);
|
||||
assert.match(view, /Math\.min\(Math\.max\(drawnThrough, 0\), Math\.max\(chapters\.length - 1, 0\)\)/);
|
||||
assert.match(view, /useState\(section\.eager \|\| drawn\)/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user