feat(chart): hide qizheng from the chart page; backend endpoint kept (T1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5dc977ef2c
commit
af27709de4
@@ -4959,7 +4959,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.chart-page-basics,
|
||||
.chart-page-dasha,
|
||||
.chart-page-western,
|
||||
.chart-page-qizheng,
|
||||
.chart-page-western-side {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -5350,32 +5349,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.chart-page-western-table tbody tr[tabindex] { cursor: pointer; }
|
||||
.chart-page-western-table tbody tr[tabindex]:hover { background: var(--color-canvas-muted); }
|
||||
.chart-page-western-table tr.is-selected { background: var(--color-selected); }
|
||||
.chart-page-qizheng-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-2);
|
||||
margin: var(--space-4) 0;
|
||||
}
|
||||
.chart-page-qizheng-cell {
|
||||
min-height: 88px;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.chart-page-qizheng-cell strong { display: block; font-size: var(--type-title-sm); font-weight: 500; }
|
||||
.chart-page-qizheng-cell span,
|
||||
.chart-page-qizheng-cell small,
|
||||
.chart-page-qizheng-cell p { display: block; margin: var(--space-1) 0 0; color: var(--color-ink-secondary); }
|
||||
.chart-page-qizheng-cell small { font-size: var(--type-caption); }
|
||||
.chart-page-qizheng-hole { min-height: 0; }
|
||||
.chart-page-qizheng-rules {
|
||||
margin: var(--space-3) 0;
|
||||
padding-left: 1.2em;
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.chart-page-unavailable > p { margin: 0 0 var(--space-3); font-size: var(--type-body-md); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
|
||||
import { ChartPageSkeleton } from "./chart-page-skeleton";
|
||||
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";
|
||||
|
||||
@@ -70,7 +69,6 @@ export function ChartPageView({
|
||||
setTab(id);
|
||||
if (id === "dasha") onNeedLayer?.("chara");
|
||||
if (id === "western") onNeedLayer?.("western");
|
||||
if (id === "qizheng") onNeedLayer?.("qizheng");
|
||||
}
|
||||
|
||||
function selectVarga(id: string) {
|
||||
@@ -136,15 +134,6 @@ export function ChartPageView({
|
||||
<ChartWesternTab view={view} />
|
||||
)
|
||||
) : null}
|
||||
{tab === "qizheng" ? (
|
||||
pendingLayers?.has("qizheng") ? (
|
||||
<WaitingNote copy={CHART_VIEW_COPY.waitingLayer} />
|
||||
) : layerFailures?.has("qizheng") ? (
|
||||
<WaitingNote copy={chartLayerFailureCopy(layerFailures.get("qizheng")!)} />
|
||||
) : (
|
||||
<ChartQizhengTab view={view} />
|
||||
)
|
||||
) : null}
|
||||
</>}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { COORDINATE_BOUNDARY, QIZHENG_RULES } from "@/lib/chart-view-labels";
|
||||
import { QizhengPalaceGrid } from "./qizheng-palace-grid";
|
||||
|
||||
function Rules() {
|
||||
return (
|
||||
<ul className="chart-page-qizheng-rules">
|
||||
{QIZHENG_RULES.map((rule) => <li key={rule}>{rule}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartQizhengTab({ view }: { view: ChartViewOk }) {
|
||||
const qizheng = view.qizheng;
|
||||
if (qizheng.status === "unavailable") {
|
||||
return (
|
||||
<div className="chart-page-unavailable">
|
||||
<p className="chart-page-boundary">{COORDINATE_BOUNDARY.qizheng}</p>
|
||||
<p>{qizheng.copy}</p>
|
||||
<Rules />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="chart-page-qizheng">
|
||||
<p className="chart-page-boundary">{qizheng.boundary}</p>
|
||||
<p className="chart-page-note">计都派别:{qizheng.ketuMode} · 坐标系 {qizheng.coordinateSystem}</p>
|
||||
<Rules />
|
||||
<QizhengPalaceGrid qizheng={qizheng} />
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>十一曜宿度</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">曜</th>
|
||||
<th scope="col">宿</th>
|
||||
<th scope="col">宿度</th>
|
||||
<th scope="col">宫</th>
|
||||
<th scope="col">庙旺</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{qizheng.bodies.map((body) => (
|
||||
<tr key={body.name}>
|
||||
<th scope="row">{body.name}</th>
|
||||
<td>{body.mansion}</td>
|
||||
<td>{body.mansionDegree.toFixed(2)}</td>
|
||||
<td>{body.palace}</td>
|
||||
<td>{body.dignityClosed ? body.dignity : `${body.dignity}(未闭合)`}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { ChartViewQizhengOk } from "@/lib/chart-view-contract";
|
||||
import { QIZHENG_BRANCH_GRID } from "@/lib/chart-view-labels";
|
||||
|
||||
export function QizhengPalaceGrid({ qizheng }: { qizheng: ChartViewQizhengOk }) {
|
||||
const byBranch = new Map(qizheng.palaces.map((palace) => [palace.branch, palace]));
|
||||
return (
|
||||
<div className="chart-page-qizheng-grid" role="img" aria-label="七政四余地支十二宫">
|
||||
{QIZHENG_BRANCH_GRID.flatMap((row, rowIndex) => row.map((branch, columnIndex) => {
|
||||
if (!branch) {
|
||||
return <div key={`empty-${rowIndex}-${columnIndex}`} className="chart-page-qizheng-hole" />;
|
||||
}
|
||||
const palace = byBranch.get(branch);
|
||||
return (
|
||||
<article key={branch} className="chart-page-qizheng-cell">
|
||||
<strong>{branch}</strong>
|
||||
<span>{palace?.lifePalace ?? "—"}</span>
|
||||
<small>{palace?.mansions.length ? palace.mansions.join(" ") : "所辖宿未返回"}</small>
|
||||
<p>{palace?.occupants.length ? palace.occupants.join(" ") : "空"}</p>
|
||||
</article>
|
||||
);
|
||||
}))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,6 @@ function mergeLayer(current: ChartViewOk, incoming: ChartViewOk, layer: ChartVie
|
||||
...(layer === "varga" ? { vedic: incoming.vedic } : {}),
|
||||
...(layer === "chara" ? { dasha: incoming.dasha } : {}),
|
||||
...(layer === "western" ? { western: incoming.western } : {}),
|
||||
...(layer === "qizheng" ? { qizheng: incoming.qizheng } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -145,27 +145,6 @@ export const chartViewWesternOkSchema = z.object({
|
||||
})),
|
||||
});
|
||||
|
||||
export const chartViewQizhengOkSchema = z.object({
|
||||
status: z.literal("ok"),
|
||||
coordinateSystem: z.string().min(1),
|
||||
ketuMode: z.string().min(1),
|
||||
boundary: z.string().min(1),
|
||||
palaces: z.array(z.object({
|
||||
branch: z.string().min(1),
|
||||
lifePalace: z.string().min(1),
|
||||
mansions: z.array(z.string()),
|
||||
occupants: z.array(z.string()),
|
||||
})),
|
||||
bodies: z.array(z.object({
|
||||
name: z.string().min(1),
|
||||
mansion: z.string().min(1),
|
||||
mansionDegree: z.number(),
|
||||
palace: z.string().min(1),
|
||||
dignity: z.string().min(1),
|
||||
dignityClosed: z.boolean(),
|
||||
})),
|
||||
});
|
||||
|
||||
export const chartViewProfileSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
date: isoDate,
|
||||
@@ -200,7 +179,6 @@ export const chartViewOkSchema = z.object({
|
||||
varga: chartViewFailureStatusSchema.optional(),
|
||||
chara: chartViewFailureStatusSchema.optional(),
|
||||
western: chartViewFailureStatusSchema.optional(),
|
||||
qizheng: chartViewFailureStatusSchema.optional(),
|
||||
}).optional(),
|
||||
status: z.literal("ok"),
|
||||
billed: z.literal(false),
|
||||
@@ -221,7 +199,6 @@ export const chartViewOkSchema = z.object({
|
||||
dualTrackNote: z.string().min(1),
|
||||
}),
|
||||
western: z.union([chartViewWesternOkSchema, chartViewUnavailableLayerSchema]),
|
||||
qizheng: z.union([chartViewQizhengOkSchema, chartViewUnavailableLayerSchema]),
|
||||
accountId: z.string().min(1).optional(),
|
||||
profileFingerprint: z.string().min(1).optional(),
|
||||
subjectId: z.string().min(1).optional(),
|
||||
@@ -248,7 +225,6 @@ export type ChartViewMessage = z.infer<typeof chartViewMessageSchema>;
|
||||
export type ChartViewResponse = z.infer<typeof chartViewResponseSchema>;
|
||||
export type ChartViewUnavailableLayer = z.infer<typeof chartViewUnavailableLayerSchema>;
|
||||
export type ChartViewWesternOk = z.infer<typeof chartViewWesternOkSchema>;
|
||||
export type ChartViewQizhengOk = z.infer<typeof chartViewQizhengOkSchema>;
|
||||
export type ChartViewVarga = z.infer<typeof chartViewVargaSchema>;
|
||||
export type ChartViewPlanetRow = z.infer<typeof chartViewPlanetRowSchema>;
|
||||
export type NorthIndianChart = z.infer<typeof northIndianChartSchema>;
|
||||
@@ -259,7 +235,6 @@ export const CHART_VIEW_TABS = [
|
||||
{ id: "basics", label: "基础信息" },
|
||||
{ id: "dasha", label: "大运" },
|
||||
{ id: "western", label: "西洋盘" },
|
||||
{ id: "qizheng", label: "七政四余" },
|
||||
] as const;
|
||||
|
||||
export type ChartViewTabId = (typeof CHART_VIEW_TABS)[number]["id"];
|
||||
|
||||
@@ -2,7 +2,7 @@ export const CHART_VIEW_ENGINE_TIMEOUT_MS = 10_000;
|
||||
export const CHART_VIEW_ENGINE_CACHE_TTL_MS = 5 * 60 * 1_000;
|
||||
const ENGINE_CACHE_MAX_ENTRIES = 200;
|
||||
|
||||
export const CHART_VIEW_LAYERS = ["varga", "chara", "western", "qizheng"] as const;
|
||||
export const CHART_VIEW_LAYERS = ["varga", "chara", "western"] as const;
|
||||
export type ChartViewLayer = (typeof CHART_VIEW_LAYERS)[number];
|
||||
|
||||
export type EngineCallResult =
|
||||
|
||||
@@ -121,17 +121,9 @@ export const MODE_ZH: Record<string, string> = {
|
||||
mutable: "变动",
|
||||
};
|
||||
|
||||
export const QIZHENG_BRANCH_GRID: ReadonlyArray<readonly (string | null)[]> = [
|
||||
["巳", "午", "未", "申"],
|
||||
["辰", null, null, "酉"],
|
||||
["卯", null, null, "戌"],
|
||||
["寅", "丑", "子", "亥"],
|
||||
];
|
||||
|
||||
export const COORDINATE_BOUNDARY = {
|
||||
vedic: "这是恒星黄道,岁差用你资料里选的那一套。它和西洋回归黄道、七政宿度不是同一套坐标,不能换算,也不能叠成双重印证。",
|
||||
western: "这是回归黄道,宫位用 Placidus。它和印度盘的恒星黄经、七政宿度不是同一套,不能换算,也不能叠成双重印证。",
|
||||
qizheng: "宿度从角宿初度起算,不是恒星黄经,也不是回归黄道。三套不能换算,也不能叠成双重印证。",
|
||||
vedic: "这是恒星黄道,岁差用你资料里选的那一套。它和西洋回归黄道不是同一套坐标,不能换算,也不能叠成双重印证。",
|
||||
western: "这是回归黄道,宫位用 Placidus。它和印度盘的恒星黄经不是同一套,不能换算,也不能叠成双重印证。",
|
||||
} as const;
|
||||
|
||||
export const DASHA_DUAL_TRACK_NOTE = "只有两条同时指向同一段时间才算证据,单轨命中要降一级来看。";
|
||||
@@ -146,14 +138,6 @@ export const CHARA_ZERO_YEAR_NOTE = "本段时长为 0,不展开";
|
||||
|
||||
export const WESTERN_UNAVAILABLE_COPY = "西洋盘这一栏要等回归黄道端点上线。现在还没有,先空着——不会拿印度盘的度数改写成回归黄道。";
|
||||
|
||||
export const QIZHENG_UNAVAILABLE_COPY = "七政四余这一栏要等宿度端点上线。现在还没有,先空着。";
|
||||
|
||||
export const QIZHENG_RULES = [
|
||||
"宿度从角宿初度起算,不是恒星黄经。",
|
||||
"计都派别(是否取罗睺对点)会改整张盘,必须写明,不能静默默认。",
|
||||
"庙旺表还没闭合,这一列只能当占位,不能拿来下判断。",
|
||||
] as const;
|
||||
|
||||
export const CHART_VIEW_CLIENT_TIMEOUT_MS = 25_000;
|
||||
|
||||
export const CHART_VIEW_COPY = {
|
||||
|
||||
@@ -157,7 +157,7 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
if (result.status !== "ok") layerFailures[layer] = engineFailureReason(result.status);
|
||||
return enginePayload(result);
|
||||
};
|
||||
const [varga, chara, western, qizheng] = await Promise.all([
|
||||
const [varga, chara, western] = await Promise.all([
|
||||
layers.has("varga")
|
||||
? callEngine(input.postEngine, "/api/varga_full", {
|
||||
...followUp,
|
||||
@@ -174,9 +174,6 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
layers.has("western")
|
||||
? callEngine(input.postEngine, "/api/western", payload).then(layerPayload("western"))
|
||||
: Promise.resolve(null),
|
||||
layers.has("qizheng")
|
||||
? callEngine(input.postEngine, "/api/qizheng", payload).then(layerPayload("qizheng"))
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -185,7 +182,6 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
varga,
|
||||
chara,
|
||||
western,
|
||||
qizheng,
|
||||
profile,
|
||||
asOf: input.asOf,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
COORDINATE_BOUNDARY,
|
||||
DASHA_DUAL_TRACK_NOTE,
|
||||
PLANET_ORDER,
|
||||
QIZHENG_UNAVAILABLE_COPY,
|
||||
SIGN_ORDER,
|
||||
VARGA_CHIP_ORDER,
|
||||
VIMSHOTTARI_ORDER,
|
||||
@@ -26,7 +25,6 @@ import { traditionalSymbol } from "./chart-view-symbols.ts";
|
||||
import {
|
||||
chartViewOkSchema,
|
||||
type ChartViewOk,
|
||||
type ChartViewQizhengOk,
|
||||
type ChartViewUnavailableLayer,
|
||||
type ChartViewVarga,
|
||||
type ChartViewWesternOk,
|
||||
@@ -51,7 +49,6 @@ export type ChartViewEngineBundle = {
|
||||
varga: Record<string, unknown> | null;
|
||||
chara: Record<string, unknown> | null;
|
||||
western: Record<string, unknown> | null;
|
||||
qizheng: Record<string, unknown> | null;
|
||||
profile: ChartViewProfileInput;
|
||||
asOf: string;
|
||||
};
|
||||
@@ -467,10 +464,6 @@ function unavailableWestern(reason: string): ChartViewUnavailableLayer {
|
||||
return { status: "unavailable", reason, copy: WESTERN_UNAVAILABLE_COPY };
|
||||
}
|
||||
|
||||
function unavailableQizheng(reason: string): ChartViewUnavailableLayer {
|
||||
return { status: "unavailable", reason, copy: QIZHENG_UNAVAILABLE_COPY };
|
||||
}
|
||||
|
||||
function mapWestern(raw: Record<string, unknown> | null, ayanamsaDisplay: string): ChartViewWesternOk | ChartViewUnavailableLayer {
|
||||
if (!raw) return unavailableWestern("endpoint_missing");
|
||||
const natal = record(raw.natal) ?? raw;
|
||||
@@ -559,96 +552,8 @@ function palaceBranch(row: Record<string, unknown>): string | null {
|
||||
return text(row.branch) ?? text(row.zhi) ?? text(row.earthly_branch) ?? text(row.name);
|
||||
}
|
||||
|
||||
function mapQizheng(raw: Record<string, unknown> | null): ChartViewQizhengOk | ChartViewUnavailableLayer {
|
||||
if (!raw) return unavailableQizheng("endpoint_missing");
|
||||
const palacesRaw = raw.palaces;
|
||||
const bodiesRaw = raw.bodies ?? raw.planets;
|
||||
const palaces: ChartViewQizhengOk["palaces"] = [];
|
||||
if (Array.isArray(palacesRaw)) {
|
||||
for (const entry of palacesRaw) {
|
||||
const row = record(entry);
|
||||
if (!row) continue;
|
||||
const branch = palaceBranch(row);
|
||||
if (!branch) continue;
|
||||
palaces.push({
|
||||
branch,
|
||||
lifePalace: text(row.life_palace) ?? text(row.lifePalace) ?? text(row.human_palace) ?? "—",
|
||||
mansions: Array.isArray(row.mansions)
|
||||
? row.mansions.filter((item): item is string => typeof item === "string")
|
||||
: text(row.mansions)
|
||||
? [String(row.mansions)]
|
||||
: (text(row.xiu) ? [String(row.xiu)] : []),
|
||||
occupants: Array.isArray(row.occupants)
|
||||
? row.occupants.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const asRecord = record(palacesRaw);
|
||||
if (asRecord) {
|
||||
for (const [branch, value] of Object.entries(asRecord)) {
|
||||
const row = record(value) ?? {};
|
||||
palaces.push({
|
||||
branch,
|
||||
lifePalace: text(row.life_palace) ?? text(row.lifePalace) ?? "—",
|
||||
mansions: Array.isArray(row.mansions)
|
||||
? row.mansions.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
occupants: Array.isArray(row.occupants)
|
||||
? row.occupants.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const bodies: ChartViewQizhengOk["bodies"] = [];
|
||||
if (Array.isArray(bodiesRaw)) {
|
||||
for (const entry of bodiesRaw) {
|
||||
const row = record(entry);
|
||||
if (!row) continue;
|
||||
const name = text(row.name) ?? text(row.body) ?? text(row.planet);
|
||||
if (!name) continue;
|
||||
bodies.push({
|
||||
name,
|
||||
mansion: text(row.mansion) ?? text(row.xiu) ?? "—",
|
||||
mansionDegree: finite(row.mansion_degree) ?? finite(row.siderealLon) ?? finite(row.degree) ?? 0,
|
||||
palace: text(row.palace) ?? text(row.branch) ?? "—",
|
||||
dignity: text(row.dignity) ?? text(row.status) ?? "未闭合",
|
||||
dignityClosed: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const asRecord = record(bodiesRaw);
|
||||
if (asRecord) {
|
||||
for (const [name, value] of Object.entries(asRecord)) {
|
||||
const row = record(value) ?? {};
|
||||
bodies.push({
|
||||
name,
|
||||
mansion: text(row.mansion) ?? text(row.xiu) ?? "—",
|
||||
mansionDegree: finite(row.mansion_degree) ?? finite(row.siderealLon) ?? finite(row.degree) ?? 0,
|
||||
palace: text(row.palace) ?? text(row.branch) ?? "—",
|
||||
dignity: text(row.dignity) ?? "未闭合",
|
||||
dignityClosed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (palaces.length === 0 || bodies.length === 0) return unavailableQizheng("payload_incomplete");
|
||||
return {
|
||||
status: "ok",
|
||||
coordinateSystem: text(raw.coordinate_system) ?? "qizheng_mansion_degrees_from_jiao",
|
||||
ketuMode: text(record(raw.calculation)?.engine_ketu_mode)
|
||||
?? text(record(raw.chart)?.ketu_mode)
|
||||
?? text(raw.ketuMode)
|
||||
?? "unspecified",
|
||||
boundary: COORDINATE_BOUNDARY.qizheng,
|
||||
palaces,
|
||||
bodies,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildChartView(bundle: ChartViewEngineBundle): ChartViewOk {
|
||||
const { chart, varga, chara, western, qizheng, profile, asOf } = bundle;
|
||||
const { chart, varga, chara, western, profile, asOf } = bundle;
|
||||
const planets = asPlanetMap(chart.planets);
|
||||
const moon = planets.Moon ?? {};
|
||||
const ascendant = record(chart.ascendant);
|
||||
@@ -701,7 +606,6 @@ export function buildChartView(bundle: ChartViewEngineBundle): ChartViewOk {
|
||||
dualTrackNote: DASHA_DUAL_TRACK_NOTE,
|
||||
},
|
||||
western: mapWestern(western, ayanamsaDisplay),
|
||||
qizheng: mapQizheng(qizheng),
|
||||
};
|
||||
return chartViewOkSchema.parse(view);
|
||||
}
|
||||
|
||||
@@ -214,7 +214,6 @@ async function loadChartSnapshot(started: number, signal: AbortSignal): Promise<
|
||||
...(prior.vedic.vargas.length > 1 ? { vedic: prior.vedic } : {}),
|
||||
dasha: { ...snapshot.view.dasha, ...(prior.dasha.chara.periods.length ? { chara: prior.dasha.chara } : {}) },
|
||||
...(prior.western.status === "ok" ? { western: prior.western } : {}),
|
||||
...(prior.qizheng.status === "ok" ? { qizheng: prior.qizheng } : {}),
|
||||
} };
|
||||
}
|
||||
return rememberChartSnapshot(snapshot);
|
||||
|
||||
@@ -81,7 +81,8 @@ for (const cached of [false, true]) test(`chart 401 ${cached ? "cache" : "respon
|
||||
assert.equal(state().view?.status, "unauthenticated"); assert.deepEqual(assigned, ["/login"]);
|
||||
}));
|
||||
|
||||
for (const layer of ["varga", "chara", "western", "qizheng"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => {
|
||||
// 原值: 含 "qizheng";新值: 去掉;原因: 七政懒加载层已删(TASK-chart-types D1)
|
||||
for (const layer of ["varga", "chara", "western"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => {
|
||||
const initial = await packet();
|
||||
const failed = await packet(layer, { status: "busy", path: "/api/synthetic", elapsedMs: 1, httpStatus: 429 });
|
||||
globalThis.fetch = async (url) => response(String(url).includes("layers=") ? failed : initial);
|
||||
|
||||
@@ -33,7 +33,6 @@ const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.
|
||||
const sidebar = readFileSync(new URL("../src/components/app-sidebar.tsx", import.meta.url), "utf8");
|
||||
const vedicTab = readFileSync(new URL("../src/components/chart-page/chart-vedic-tab.tsx", import.meta.url), "utf8");
|
||||
const westernTab = readFileSync(new URL("../src/components/chart-page/chart-western-tab.tsx", import.meta.url), "utf8");
|
||||
const qizhengTab = readFileSync(new URL("../src/components/chart-page/chart-qizheng-tab.tsx", import.meta.url), "utf8");
|
||||
|
||||
/** Every selector that has at least one rule, comments stripped first. */
|
||||
function declaredSelectors(source: string): Set<string> {
|
||||
@@ -86,7 +85,7 @@ function withSidebarProvider(element: React.ReactElement) {
|
||||
return renderToStaticMarkup(React.createElement(SidebarProvider, null, element));
|
||||
}
|
||||
|
||||
test("the five tabs render and can each be selected", async () => {
|
||||
test("the four tabs render and can each be selected", async () => {
|
||||
const view = await okView();
|
||||
for (const tab of CHART_VIEW_TABS) {
|
||||
const markup = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: tab.id }));
|
||||
@@ -99,7 +98,10 @@ test("the five tabs render and can each be selected", async () => {
|
||||
assert.match(markup, />基础信息</);
|
||||
assert.match(markup, />大运</);
|
||||
assert.match(markup, />西洋盘</);
|
||||
assert.match(markup, />七政四余</);
|
||||
// 原值: 必须含「七政四余」Tab
|
||||
// 新值: 不得含「七政四余」
|
||||
// 原因: 产品 2026-09-28 决定七政四余先不在星盘页展示(TASK-chart-types D1),后端保留
|
||||
assert.doesNotMatch(markup, /七政四余/);
|
||||
assert.doesNotMatch(markup, /正在加载|骨架|spinner/i);
|
||||
if (tab.id === "vedic") assert.match(markup, /D1/);
|
||||
if (tab.id === "basics") {
|
||||
@@ -112,23 +114,15 @@ test("the five tabs render and can each be selected", async () => {
|
||||
assert.match(markup, /只有两条同时指向同一段时间才算证据/);
|
||||
}
|
||||
if (tab.id === "western") assert.match(markup, /西洋盘这一栏要等回归黄道端点上线/);
|
||||
if (tab.id === "qizheng") {
|
||||
assert.match(markup, /七政四余这一栏要等宿度端点上线/);
|
||||
assert.match(markup, /角宿/);
|
||||
assert.match(markup, /计都派别/);
|
||||
assert.match(markup, /庙旺/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("unavailable western and qizheng tabs stay explanatory", async () => {
|
||||
// 原值: 西洋与七政两栏各断言一次;新值: 只断言西洋;原因: 七政 Tab 已删(TASK-chart-types D1)
|
||||
test("unavailable western tab stays explanatory", async () => {
|
||||
const view = await okView();
|
||||
const western = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: "western" }));
|
||||
const qizheng = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: "qizheng" }));
|
||||
assert.doesNotMatch(western, /报错|出错了|500/);
|
||||
assert.doesNotMatch(qizheng, /报错|出错了|500/);
|
||||
assert.match(western, /不能换算/);
|
||||
assert.match(qizheng, /不能换算/);
|
||||
});
|
||||
|
||||
test("a tropical packet draws the western wheel instead of the unavailable copy", async () => {
|
||||
@@ -232,7 +226,8 @@ test("the planet table scrolls inside its own box instead of widening the page",
|
||||
assert.match(cssDeclarations(".chart-page-planet-table-wrap", globalStyles), /min-width:\s*0/);
|
||||
assert.match(cssDeclarations(".chart-page-planet-table", globalStyles), /min-width:\s*\d/);
|
||||
// Every planet table is wrapped, not just the one on the 星盘 tab.
|
||||
for (const source of [vedicTab, westernTab, qizhengTab]) {
|
||||
// 原值: [vedicTab, westernTab, qizhengTab];新值: 去掉 qizhengTab;原因: 文件已删(TASK-chart-types D1)
|
||||
for (const source of [vedicTab, westernTab]) {
|
||||
const tables = [...source.matchAll(/<table className="chart-page-planet-table">/g)].length;
|
||||
const wraps = [...source.matchAll(/<div className="chart-page-planet-table-wrap">/g)].length;
|
||||
assert.equal(wraps, tables);
|
||||
@@ -264,7 +259,9 @@ test("the chart page shell is visible before the natal chart arrives", () => {
|
||||
// 原因:2026-09-24 D1 仅授权星盘盘位骨架(BUG-1016)。
|
||||
assert.match(markup, /data-skeleton="true"/);
|
||||
assert.match(markup, /aria-busy="true"/);
|
||||
assert.equal((markup.match(/disabled=""/g) ?? []).length, 5);
|
||||
// 原值: 5 个禁用 Tab;新值: 4 个(CHART_VIEW_TABS.length);原因: 七政 Tab 已删(TASK-chart-types D1)
|
||||
assert.equal((markup.match(/disabled=""/g) ?? []).length, CHART_VIEW_TABS.length);
|
||||
assert.equal(CHART_VIEW_TABS.length, 4);
|
||||
assert.doesNotMatch(markup, /正在加载|InlineSpinner|spinner|打开即有/i);
|
||||
assert.doesNotMatch(markup, /chart-page-eyebrow/);
|
||||
});
|
||||
@@ -332,18 +329,20 @@ test("varga pending shows the empty shared chart and failure replaces it", async
|
||||
assert.match(render(true, "engine_busy"), /data-skeleton="true"/);
|
||||
});
|
||||
|
||||
test("western qizheng and chara failures are terminal copy rather than waiting", async () => {
|
||||
// 原值: western / qizheng / chara 三栏;新值: 去掉 qizheng;原因: 七政 Tab 已删(TASK-chart-types D1)
|
||||
test("western and chara failures are terminal copy rather than waiting", async () => {
|
||||
const view = await okView();
|
||||
for (const [tab, layer] of [["western", "western"], ["qizheng", "qizheng"], ["dasha", "chara"]] as const) {
|
||||
for (const [tab, layer] of [["western", "western"], ["dasha", "chara"]] as const) {
|
||||
const markup = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: tab, layerFailures: new Map([[layer, "engine_busy" as const]]) }));
|
||||
assert.ok(markup.includes(CHART_VIEW_COPY.busy)); assert.doesNotMatch(markup, /data-skeleton|这一栏还没拿到/);
|
||||
if (tab === "dasha") assert.match(markup, /Vimshottari/);
|
||||
}
|
||||
});
|
||||
|
||||
test("qizheng and chara pending remain static and only western has a wheel skeleton", async () => {
|
||||
// 原值: qizheng 与 chara 两栏;新值: 只剩 chara;原因: 七政 Tab 已删(TASK-chart-types D1)
|
||||
test("chara pending remains static and only western has a wheel skeleton", async () => {
|
||||
const view = await okView();
|
||||
for (const [tab, layer] of [["qizheng", "qizheng"], ["dasha", "chara"]] as const) {
|
||||
for (const [tab, layer] of [["dasha", "chara"]] as const) {
|
||||
const markup = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: tab, pendingLayers: new Set([layer]) }));
|
||||
assert.match(markup, /这一栏还没拿到/); assert.doesNotMatch(markup, /data-skeleton|InlineSpinner/);
|
||||
}
|
||||
|
||||
@@ -121,5 +121,6 @@ test("engine cache keys include ayanamsa and node mode and expire", () => {
|
||||
|
||||
test("layer query parsing keeps only known layers in stable order", () => {
|
||||
assert.deepEqual(parseChartViewLayers(null), []);
|
||||
assert.deepEqual(parseChartViewLayers("western,varga,nope,qizheng"), ["varga", "western", "qizheng"]);
|
||||
// 原值: "qizheng" 被保留为已知层;新值: 与 "nope" 一样被丢弃;原因: 七政层已删(TASK-chart-types D1)
|
||||
assert.deepEqual(parseChartViewLayers("western,varga,nope,qizheng"), ["varga", "western"]);
|
||||
});
|
||||
|
||||
@@ -65,7 +65,6 @@ function engine(overrides: Record<string, EngineCallResult | Record<string, unkn
|
||||
if (path === "/api/varga_full") return asResult(path, golden.varga_full);
|
||||
if (path === "/api/dasha/chara") return asResult(path, golden.chara);
|
||||
if (path === "/api/western") return asResult(path, null);
|
||||
if (path === "/api/qizheng") return asResult(path, null);
|
||||
return asResult(path, null);
|
||||
};
|
||||
return { calls, bodies, postEngine };
|
||||
@@ -162,11 +161,12 @@ test("opening the natal chart does not call follow-up engine paths", async () =>
|
||||
assert.equal(body.billed, false);
|
||||
assert.ok(body.vedic.vargas.some((item) => item.id === "D1"));
|
||||
assert.equal(body.western.status, "unavailable");
|
||||
assert.equal(body.qizheng.status, "unavailable");
|
||||
// 原值: body.qizheng.status === "unavailable";新值: 响应不再带 qizheng;原因: TASK-chart-types D1
|
||||
assert.equal("qizheng" in body, false);
|
||||
assert.deepEqual(calls, ["/api/chart"]);
|
||||
});
|
||||
|
||||
test("western and qizheng stay unavailable without failing the Indian tabs", async () => {
|
||||
test("western stays unavailable without failing the Indian tabs", async () => {
|
||||
const { calls, postEngine } = engine();
|
||||
const result = await assembleChartView({
|
||||
userId: "user-1",
|
||||
@@ -183,18 +183,18 @@ test("western and qizheng stay unavailable without failing the Indian tabs", asy
|
||||
assert.equal(body.dasha.vimshottari.method, "Vimshottari");
|
||||
assert.equal(body.dasha.chara.title, "Chara Dasha(kn_rao 变体)");
|
||||
assert.equal(body.western.status, "unavailable");
|
||||
assert.equal(body.qizheng.status, "unavailable");
|
||||
assert.ok(body.vedic.vargas.some((item) => item.id === "D9"));
|
||||
// 原值: 无 layers 时也会打 /api/western 与 /api/qizheng
|
||||
// 新值: 只有请求对应 layer 才打
|
||||
// 原因: BUG-717 开页不得占满重计算配额;西洋/七政改按需
|
||||
assert.equal(calls.includes("/api/western"), true);
|
||||
assert.equal(calls.includes("/api/qizheng"), true);
|
||||
// 原值: 全部 layer 时会打 /api/qizheng;新值: 任何情况都不打;原因: 七政层已删(TASK-chart-types D1)
|
||||
assert.equal(calls.includes("/api/qizheng"), false);
|
||||
assert.doesNotMatch(calls.join(" "), /billing|mastra|consult/);
|
||||
});
|
||||
|
||||
test("a real western engine packet lights the tropical tab without mixing sidereal longitudes", async () => {
|
||||
const { postEngine } = engine({ "/api/western": golden.western, "/api/qizheng": null });
|
||||
const { postEngine } = engine({ "/api/western": golden.western });
|
||||
const result = await assembleChartView({
|
||||
userId: "user-1",
|
||||
profile,
|
||||
@@ -209,30 +209,10 @@ test("a real western engine packet lights the tropical tab without mixing sidere
|
||||
assert.equal(body.western.houses.length, 12);
|
||||
assert.match(body.western.boundary, /回归黄道/);
|
||||
assert.notEqual(body.western.ascendantLongitude, body.vedic.planets[0]?.degreeInSign);
|
||||
assert.equal(body.qizheng.status, "unavailable");
|
||||
});
|
||||
|
||||
test("qizheng ketuMode follows the engine field, not a request echo", async () => {
|
||||
const qizheng = {
|
||||
coordinate_system: "qizheng_mansion_degrees_from_jiao",
|
||||
calculation: { ketu_mode: "descending-node", engine_ketu_mode: "apogee" },
|
||||
palaces: [{ name: "午", branch: "午", lifePalace: "命宫", mansions: ["星"], occupants: [] }],
|
||||
bodies: { sun: { mansion: "奎", siderealLon: 177.94, palace: "亥宮", dignity: "陷" } },
|
||||
};
|
||||
const { postEngine } = engine({ "/api/qizheng": qizheng });
|
||||
const result = await assembleChartView({
|
||||
userId: "user-1",
|
||||
profile,
|
||||
postEngine,
|
||||
asOf: "2026-09-15",
|
||||
layers: ["qizheng"],
|
||||
});
|
||||
const body = chartViewOkSchema.parse(result.body);
|
||||
assert.equal(body.qizheng.status, "ok");
|
||||
if (body.qizheng.status !== "ok") throw new Error("expected qizheng ok");
|
||||
assert.equal(body.qizheng.ketuMode, "apogee");
|
||||
});
|
||||
|
||||
// 原值: 断言七政 ketuMode 取引擎字段;新值: 整条删除;原因: 七政层已从星盘页删除(TASK-chart-types D1),
|
||||
// ketu_mode 的引擎侧合同仍由 tests/test_qizheng_api_productization.py 守住
|
||||
test("busy vs other natal engine failures split copy and each leave a log line", async () => {
|
||||
const stubs: Array<{ status: EngineCallResult["status"]; httpStatus?: number; errorName?: string }> = [
|
||||
{ status: "busy", httpStatus: 429 },
|
||||
@@ -298,7 +278,9 @@ test("the golden chart-view envelope stays inside the page contract", () => {
|
||||
});
|
||||
assert.match(COORDINATE_BOUNDARY.vedic, /不能换算/);
|
||||
assert.match(COORDINATE_BOUNDARY.western, /不能换算/);
|
||||
assert.match(COORDINATE_BOUNDARY.qizheng, /角宿/);
|
||||
// 原值: COORDINATE_BOUNDARY.qizheng 含「角宿」;新值: 该条已删,印度/西洋两句不再提七政;原因: TASK-chart-types D1
|
||||
assert.equal("qizheng" in COORDINATE_BOUNDARY, false);
|
||||
assert.doesNotMatch(COORDINATE_BOUNDARY.vedic + COORDINATE_BOUNDARY.western, /七政/);
|
||||
});
|
||||
|
||||
test("page.tsx does not host the chart page", () => {
|
||||
|
||||
@@ -44,7 +44,6 @@ function viewFrom(chara: Record<string, unknown>): ChartViewOk {
|
||||
varga: null,
|
||||
chara,
|
||||
western: null,
|
||||
qizheng: null,
|
||||
profile,
|
||||
asOf: "2026-09-25",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user