fix(web): paint the chart page shell before engine calls
Independent Staging Quality Gate / validate (push) Successful in 27m22s
Independent Staging Quality Gate / publish (push) Canceled after 47s

BUG-715/716/717: typed engine failures with logs and split copy, natal-only open, on-demand tabs, 10s timeout, in-memory cache, no speed-promise eyebrow.
This commit is contained in:
jesse-ux
2026-09-15 22:51:15 +08:00
parent 8caa2a2b4c
commit 5a8328e591
22 changed files with 1027 additions and 133 deletions
@@ -1,4 +1,5 @@
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { CHART_VIEW_COPY } from "@/lib/chart-view-labels";
function Track({
title,
@@ -41,12 +42,24 @@ function Track({
);
}
export function ChartDashaTab({ view }: { view: ChartViewOk }) {
export function ChartDashaTab({
view,
charaPending = false,
}: {
view: ChartViewOk;
charaPending?: boolean;
}) {
return (
<div className="chart-page-dasha">
<div className="chart-page-dasha-columns">
<Track {...view.dasha.vimshottari} />
<Track {...view.dasha.chara} />
{charaPending ? (
<section className="chart-page-dasha-track">
<p className="chart-page-note">{CHART_VIEW_COPY.waitingLayer}</p>
</section>
) : (
<Track {...view.dasha.chara} />
)}
</div>
<p className="chart-page-boundary">{view.dasha.dualTrackNote}</p>
</div>
@@ -4,28 +4,60 @@ import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { useState } from "react";
import { useChartPage } from "@/hooks/use-chart-page";
import {
CHART_VIEW_TABS,
type ChartViewResponse,
type ChartViewTabId,
} from "@/lib/chart-view-contract";
import type { ChartViewLayer } from "@/lib/chart-view-engine";
import { CHART_VIEW_COPY } from "@/lib/chart-view-labels";
import { ChartBasicsTab } from "./chart-basics-tab";
import { ChartDashaTab } from "./chart-dasha-tab";
import { ChartQizhengTab } from "./chart-qizheng-tab";
import { ChartVedicTab } from "./chart-vedic-tab";
import { ChartWesternTab } from "./chart-western-tab";
export function ChartPageRoute() {
const { view, pendingLayers, requestLayer } = useChartPage();
return (
<ChartPageView
view={view}
pendingLayers={pendingLayers}
onNeedLayer={requestLayer}
/>
);
}
function WaitingNote({ copy }: { copy: string }) {
return <p className="chart-page-note">{copy}</p>;
}
export function ChartPageView({
view,
pendingLayers,
onNeedLayer,
initialTab = "vedic",
}: {
view: ChartViewResponse;
view: ChartViewResponse | null;
pendingLayers?: ReadonlySet<ChartViewLayer>;
onNeedLayer?: (layer: ChartViewLayer) => void;
initialTab?: ChartViewTabId;
}) {
const [tab, setTab] = useState<ChartViewTabId>(initialTab);
const [vargaId, setVargaId] = useState(
view.status === "ok" ? view.vedic.vargas[0]?.id ?? "D1" : "D1",
);
const [vargaId, setVargaId] = useState("D1");
function selectTab(id: ChartViewTabId) {
setTab(id);
if (id === "dasha") onNeedLayer?.("chara");
if (id === "western") onNeedLayer?.("western");
if (id === "qizheng") onNeedLayer?.("qizheng");
}
function selectVarga(id: string) {
setVargaId(id);
if (id !== "D1") onNeedLayer?.("varga");
}
return (
<main className="chart-page-shell">
@@ -36,16 +68,20 @@ export function ChartPageView({
</Link>
</header>
{view.status !== "ok" ? (
{view == null ? (
<section className="chart-page-hero">
<h1>星盘</h1>
<p>{CHART_VIEW_COPY.waitingChart}</p>
</section>
) : view.status !== "ok" ? (
<section className="chart-page-hero">
<p className="chart-page-eyebrow">直接计算 · 打开即有 · 不消耗点数</p>
<h1>星盘</h1>
<p>{view.message}</p>
</section>
) : (
<>
<section className="chart-page-hero">
<p className="chart-page-eyebrow">直接计算 · 打开即有 · 不消耗点数</p>
<p className="chart-page-eyebrow">{CHART_VIEW_COPY.eyebrow}</p>
<h1>星盘</h1>
<p className="chart-page-birthline">
{view.profile.name} · {view.profile.date} {view.profile.time} · {view.profile.placeLabel}
@@ -59,7 +95,7 @@ export function ChartPageView({
role="tab"
aria-selected={tab === item.id}
className={tab === item.id ? "chart-page-tab is-current" : "chart-page-tab"}
onClick={() => setTab(item.id)}
onClick={() => selectTab(item.id)}
>
{item.label}
</button>
@@ -69,14 +105,28 @@ export function ChartPageView({
{tab === "vedic" ? (
<ChartVedicTab
view={view}
varga={view.vedic.vargas.find((item) => item.id === vargaId) ?? view.vedic.vargas[0]!}
onSelectVarga={setVargaId}
vargaId={vargaId}
onSelectVarga={selectVarga}
/>
) : null}
{tab === "basics" ? <ChartBasicsTab view={view} /> : null}
{tab === "dasha" ? <ChartDashaTab view={view} /> : null}
{tab === "western" ? <ChartWesternTab view={view} /> : null}
{tab === "qizheng" ? <ChartQizhengTab view={view} /> : null}
{tab === "dasha" ? (
<ChartDashaTab view={view} charaPending={pendingLayers?.has("chara") ?? false} />
) : null}
{tab === "western" ? (
pendingLayers?.has("western") ? (
<WaitingNote copy={CHART_VIEW_COPY.waitingLayer} />
) : (
<ChartWesternTab view={view} />
)
) : null}
{tab === "qizheng" ? (
pendingLayers?.has("qizheng") ? (
<WaitingNote copy={CHART_VIEW_COPY.waitingLayer} />
) : (
<ChartQizhengTab view={view} />
)
) : null}
</section>
</>
)}
@@ -1,76 +1,89 @@
"use client";
import { VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
import type { ChartViewOk, ChartViewVarga } from "@/lib/chart-view-contract";
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "@/lib/chart-view-labels";
import { formatDegree } from "./chart-format";
import { ChartParamLines } from "./chart-param-lines";
export function ChartVedicTab({
view,
varga,
vargaId,
onSelectVarga,
}: {
view: ChartViewOk;
varga: ChartViewVarga;
vargaId: string;
onSelectVarga: (id: string) => void;
}) {
const selected = view.vedic.vargas.find((item) => item.id === vargaId);
const natal = view.vedic.vargas.find((item) => item.id === "D1") ?? view.vedic.vargas[0];
const varga = selected ?? (vargaId === "D1" ? natal : undefined);
const waiting = vargaId !== "D1" && !selected;
return (
<div className="chart-page-vedic">
<div className="chart-page-varga-chips" role="tablist" aria-label="分盘">
{view.vedic.vargas.map((item) => (
<button
key={item.id}
type="button"
role="tab"
aria-selected={item.id === varga.id}
className={item.id === varga.id ? "chart-page-chip is-current" : "chart-page-chip"}
onClick={() => onSelectVarga(item.id)}
>
{item.id}
</button>
))}
{VARGA_CHIP_ORDER.map((division) => {
const id = `D${division}`;
return (
<button
key={id}
type="button"
role="tab"
aria-selected={id === vargaId}
className={id === vargaId ? "chart-page-chip is-current" : "chart-page-chip"}
onClick={() => onSelectVarga(id)}
>
{id}
</button>
);
})}
</div>
<div className="chart-page-vedic-layout">
<div className="chart-page-vedic-stage">
<VedicChartSvg chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`} />
<div className="chart-page-center-card">
<div className="chart-page-center-compact">
<ChartParamLines view={view} compact />
</div>
<div className="chart-page-center-extra">
<ChartParamLines view={view} />
{waiting || !varga ? (
<p className="chart-page-note">{CHART_VIEW_COPY.waitingVarga}</p>
) : (
<div className="chart-page-vedic-layout">
<div className="chart-page-vedic-stage">
<VedicChartSvg chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`} />
<div className="chart-page-center-card">
<div className="chart-page-center-compact">
<ChartParamLines view={view} compact />
</div>
<div className="chart-page-center-extra">
<ChartParamLines view={view} />
</div>
</div>
</div>
</div>
<div className="chart-page-params-below">
<ChartParamLines view={view} />
<p className="chart-page-boundary">{view.vedic.boundary}</p>
</div>
<div className="chart-page-planet-table-wrap">
<table className="chart-page-planet-table">
<caption>{varga.id} · {varga.meaning}</caption>
<thead>
<tr>
<th scope="col">星体</th>
<th scope="col">星座与度数</th>
<th scope="col">宫位</th>
<th scope="col">状态</th>
</tr>
</thead>
<tbody>
{view.vedic.planets.map((planet) => (
<tr key={planet.name}>
<th scope="row">{planet.label}</th>
<td>{planet.signLabel} {formatDegree(planet.degreeInSign)}</td>
<td>{planet.house}</td>
<td>{planet.status}</td>
<div className="chart-page-params-below">
<ChartParamLines view={view} />
<p className="chart-page-boundary">{view.vedic.boundary}</p>
</div>
<div className="chart-page-planet-table-wrap">
<table className="chart-page-planet-table">
<caption>{varga.id} · {varga.meaning}</caption>
<thead>
<tr>
<th scope="col">星体</th>
<th scope="col">星座与度数</th>
<th scope="col">宫位</th>
<th scope="col">状态</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{view.vedic.planets.map((planet) => (
<tr key={planet.name}>
<th scope="row">{planet.label}</th>
<td>{planet.signLabel} {formatDegree(planet.degreeInSign)}</td>
<td>{planet.house}</td>
<td>{planet.status}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<p className="chart-page-boundary chart-page-boundary-desktop">{view.vedic.boundary}</p>
)}
{varga ? <p className="chart-page-boundary chart-page-boundary-desktop">{view.vedic.boundary}</p> : null}
</div>
);
}