feat(chart): 大运右栏在 Chara 与 Yogini 之间切换
点开大运只取 Chara。切到 Yogini 才去取那一轨。失败留在右栏,再点一次会再请求。 Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
5ebbd57b04
commit
7b1070f86d
@@ -1,10 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
|
||||
import { OrbitWaiting } from "@/components/orbit-waiting";
|
||||
import type { ChartViewFailureStatus, ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import type { ChartViewLayer } from "@/lib/chart-view-engine";
|
||||
import { CHARA_ZERO_YEAR_LABEL, CHARA_ZERO_YEAR_NOTE, CHART_VIEW_COPY } from "@/lib/chart-view-labels";
|
||||
|
||||
type DashaTrack = ChartViewOk["dasha"]["vimshottari"];
|
||||
type DashaPeriod = DashaTrack["periods"][number];
|
||||
type DashaSystemId = "chara" | "yogini";
|
||||
|
||||
const DASHA_SYSTEMS: ReadonlyArray<{ id: DashaSystemId; label: string }> = [
|
||||
{ id: "chara", label: "Chara" },
|
||||
{ id: "yogini", label: "Yogini" },
|
||||
];
|
||||
|
||||
function formatYearMonth(iso: string): string {
|
||||
return `${iso.slice(0, 4)}.${iso.slice(5, 7)}`;
|
||||
@@ -90,9 +101,11 @@ function Track({
|
||||
antardashaNote,
|
||||
periods,
|
||||
asOf,
|
||||
}: DashaTrack & { asOf: string }) {
|
||||
leading,
|
||||
}: DashaTrack & { asOf: string; leading?: ReactNode }) {
|
||||
return (
|
||||
<section className="chart-page-dasha-track">
|
||||
{leading}
|
||||
<header>
|
||||
<h3>{title}</h3>
|
||||
{method && method !== title ? <p>{method}</p> : null}
|
||||
@@ -141,26 +154,61 @@ function Track({
|
||||
|
||||
export function ChartDashaTab({
|
||||
view,
|
||||
charaPending = false,
|
||||
charaFailure,
|
||||
pendingLayers,
|
||||
layerFailures,
|
||||
system: controlledSystem,
|
||||
initialSystem = "chara",
|
||||
onSelectSystem,
|
||||
}: {
|
||||
view: ChartViewOk;
|
||||
charaPending?: boolean;
|
||||
charaFailure?: ChartViewFailureStatus;
|
||||
pendingLayers?: ReadonlySet<ChartViewLayer>;
|
||||
layerFailures?: ReadonlyMap<ChartViewLayer, ChartViewFailureStatus>;
|
||||
system?: DashaSystemId;
|
||||
initialSystem?: DashaSystemId;
|
||||
onSelectSystem?: (system: DashaSystemId) => void;
|
||||
}) {
|
||||
const [uncontrolledSystem, setUncontrolledSystem] = useState<DashaSystemId>(initialSystem);
|
||||
const system = controlledSystem ?? uncontrolledSystem;
|
||||
const asOf = view.dasha.asOf;
|
||||
const pending = pendingLayers?.has(system) ?? false;
|
||||
const failure = layerFailures?.get(system);
|
||||
const track = view.dasha[system];
|
||||
|
||||
function selectSystem(id: DashaSystemId) {
|
||||
if (controlledSystem === undefined) setUncontrolledSystem(id);
|
||||
onSelectSystem?.(id);
|
||||
}
|
||||
|
||||
const switcher = (
|
||||
<div className="chart-page-dasha-switch" role="tablist" aria-label="大运体系">
|
||||
{DASHA_SYSTEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={system === item.id}
|
||||
className={system === item.id ? "chart-page-tab is-current" : "chart-page-tab"}
|
||||
onClick={() => selectSystem(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chart-page-dasha">
|
||||
<div className="chart-page-dasha-columns">
|
||||
<Track {...view.dasha.vimshottari} asOf={asOf} />
|
||||
{charaPending || charaFailure ? (
|
||||
{pending || failure ? (
|
||||
<section className="chart-page-dasha-track">
|
||||
{charaPending
|
||||
{switcher}
|
||||
{pending
|
||||
? <OrbitWaiting label={CHART_VIEW_COPY.waitingLayer} />
|
||||
: <p className="chart-page-note">{chartLayerFailureCopy(charaFailure!)}</p>}
|
||||
: <p className="chart-page-note">{chartLayerFailureCopy(failure!)}</p>}
|
||||
</section>
|
||||
) : (
|
||||
<Track {...view.dasha.chara} asOf={asOf} />
|
||||
<Track {...track} asOf={asOf} leading={switcher} />
|
||||
)}
|
||||
</div>
|
||||
<p className="chart-page-boundary">{view.dasha.dualTrackNote}</p>
|
||||
|
||||
@@ -95,6 +95,7 @@ export function ChartPageView({
|
||||
const [tab, setTab] = useState<ChartViewTabId>(initialTab);
|
||||
const [vargaId, setVargaId] = useState<ChartTypeId>("D1");
|
||||
const [editingBirth, setEditingBirth] = useState(false);
|
||||
const [dashaSystem, setDashaSystem] = useState<"chara" | "yogini">("chara");
|
||||
|
||||
function selectTab(id: ChartViewTabId) {
|
||||
setTab(id);
|
||||
@@ -102,6 +103,11 @@ export function ChartPageView({
|
||||
if (id === "western") onNeedLayer?.("western");
|
||||
}
|
||||
|
||||
function selectDashaSystem(id: "chara" | "yogini") {
|
||||
setDashaSystem(id);
|
||||
onNeedLayer?.(id);
|
||||
}
|
||||
|
||||
function selectVarga(id: ChartTypeId) {
|
||||
setVargaId(id);
|
||||
const layer = chartTypeLayer(id);
|
||||
@@ -126,7 +132,10 @@ export function ChartPageView({
|
||||
|
||||
async function afterBirthSaved() {
|
||||
await onBirthSaved?.();
|
||||
if (tab === "dasha") onNeedLayer?.("chara");
|
||||
if (tab === "dasha") {
|
||||
onNeedLayer?.("chara");
|
||||
if (dashaSystem === "yogini") onNeedLayer?.("yogini");
|
||||
}
|
||||
if (tab === "western") onNeedLayer?.("western");
|
||||
if (tab === "vedic") {
|
||||
const layer = chartTypeLayer(vargaId);
|
||||
@@ -185,7 +194,13 @@ export function ChartPageView({
|
||||
) : null}
|
||||
{tab === "basics" ? <ChartBasicsTab view={view} /> : null}
|
||||
{tab === "dasha" ? (
|
||||
<ChartDashaTab view={view} charaPending={pendingLayers?.has("chara") ?? false} charaFailure={layerFailures?.get("chara")} />
|
||||
<ChartDashaTab
|
||||
view={view}
|
||||
pendingLayers={pendingLayers}
|
||||
layerFailures={layerFailures}
|
||||
system={dashaSystem}
|
||||
onSelectSystem={selectDashaSystem}
|
||||
/>
|
||||
) : null}
|
||||
{tab === "western" ? (
|
||||
pendingLayers?.has("western") ? (
|
||||
|
||||
@@ -11,14 +11,15 @@ import { EMPTY_NORTH_INDIAN_CHART, VedicChartSvg } from "../src/components/perso
|
||||
import { ChartPageView } from "../src/components/chart-page/chart-page-view.tsx";
|
||||
import { SidebarProvider } from "../src/components/ui/sidebar.tsx";
|
||||
import { assembleChartView } from "../src/lib/chart-view-load.ts";
|
||||
import { CHART_VIEW_TABS, type ChartViewOk } from "../src/lib/chart-view-contract.ts";
|
||||
import { CHART_VIEW_LAYERS } from "../src/lib/chart-view-engine.ts";
|
||||
import { CHART_VIEW_TABS, type ChartViewFailureStatus, type ChartViewOk } from "../src/lib/chart-view-contract.ts";
|
||||
import { CHART_VIEW_LAYERS, type ChartViewLayer } from "../src/lib/chart-view-engine.ts";
|
||||
import { chartViewFailureResponse } from "../src/lib/chart-view-failure.ts";
|
||||
import { CHART_VIEW_COPY } from "../src/lib/chart-view-labels.ts";
|
||||
import { CHART_SETTINGS_HREF } from "../src/lib/settings-url.ts";
|
||||
import { westernLabelBox } from "../src/components/chart-page/western-wheel-layout.ts";
|
||||
import { cssDeclarations } from "./css-contract-test-support.ts";
|
||||
import { goldenSky } from "./birth-sky-test-support.ts";
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
@@ -551,3 +552,70 @@ test("birth-sky prefetch follows the current subject and the dialog is a lazy ch
|
||||
assert.match(cover, /await import\("\.\/render\.ts"\)/);
|
||||
assert.doesNotMatch(chartView, /birth-sky-dialog|lib\/birth-sky\/(draw|render)/);
|
||||
});
|
||||
|
||||
function buttonByLabel(h: ReturnType<typeof createClientLifecycleHarness>, label: string) {
|
||||
const node = h.elements().find((item) => item.tagName === "BUTTON" && item.text === label);
|
||||
assert.ok(node, `missing button ${label}`);
|
||||
return node;
|
||||
}
|
||||
|
||||
test("opening dasha requests only chara, and Yogini is a separate request", async () => {
|
||||
const view = await okView();
|
||||
const calls: string[] = [];
|
||||
const h = createClientLifecycleHarness();
|
||||
try {
|
||||
await h.render(React.createElement(SidebarProvider, null, React.createElement(ChartPageView, {
|
||||
view,
|
||||
onNeedLayer: (layer: string) => { calls.push(layer); },
|
||||
})));
|
||||
await h.event(buttonByLabel(h, "大运"));
|
||||
assert.deepEqual(calls, ["chara"]);
|
||||
assert.match(h.container.text, /Chara Dasha(kn_rao 变体)/);
|
||||
assert.match(h.container.text, /Vimshottari/);
|
||||
await h.event(buttonByLabel(h, "Yogini"));
|
||||
assert.deepEqual(calls, ["chara", "yogini"]);
|
||||
} finally {
|
||||
await h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("a yogini failure stays in the right column, and clicking Yogini again asks again", async () => {
|
||||
const view = await okView();
|
||||
const calls: string[] = [];
|
||||
const h = createClientLifecycleHarness();
|
||||
try {
|
||||
await h.render(React.createElement(SidebarProvider, null, React.createElement(ChartPageView, {
|
||||
view,
|
||||
layerFailures: new Map<ChartViewLayer, ChartViewFailureStatus>([["yogini", "engine_busy"]]),
|
||||
onNeedLayer: (layer: string) => { calls.push(layer); },
|
||||
})));
|
||||
await h.event(buttonByLabel(h, "大运"));
|
||||
await h.event(buttonByLabel(h, "Yogini"));
|
||||
assert.match(h.container.text, /Vimshottari/);
|
||||
assert.match(h.container.text, new RegExp(CHART_VIEW_COPY.busy));
|
||||
assert.doesNotMatch(h.container.text, /小运未经外部软件核对。/);
|
||||
assert.equal(h.elements().some((node) => node.hasAttribute("data-skeleton")), false);
|
||||
await h.event(buttonByLabel(h, "Yogini"));
|
||||
assert.deepEqual(calls, ["chara", "yogini", "yogini"]);
|
||||
} finally {
|
||||
await h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("yogini pending reuses the orbit sentence and does not draw a skeleton", async () => {
|
||||
const view = await okView();
|
||||
const h = createClientLifecycleHarness();
|
||||
try {
|
||||
await h.render(React.createElement(SidebarProvider, null, React.createElement(ChartPageView, {
|
||||
view,
|
||||
pendingLayers: new Set<ChartViewLayer>(["yogini"]),
|
||||
})));
|
||||
await h.event(buttonByLabel(h, "大运"));
|
||||
await h.event(buttonByLabel(h, "Yogini"));
|
||||
assert.match(h.container.text, /这一栏还没拿到/);
|
||||
assert.match(h.container.text, /Vimshottari/);
|
||||
assert.equal(h.elements().some((node) => node.hasAttribute("data-skeleton")), false);
|
||||
} finally {
|
||||
await h.close();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user