Chart and people pages no longer load the city table up front (issue #120, BUG-1469)
The birth editor on /chart now loads when the pen row is clicked (warmed on hover/focus), the people page's add/edit form is fetched after mount, and the people city column reads the on-demand table. /chart first load -116 KB gzip, /people -111 KB; home +0.19%, still static. A static import graph test keeps the table out of both pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoz1Qk3y9buDv1mkmTu
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a31b6dda39
commit
142595f810
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 星盘页、星盘档案页打开更快
|
||||
|
||||
- 星盘页第一次打开少下载约 116 KB:「改出生资料」弹窗等到点了那一行才加载(鼠标移上去就开始预取)。
|
||||
- 星盘档案页第一次打开少下载约 111 KB:「添加 / 编辑人物」的表单在页面出来后再在后台取,城市一列改读按需加载的城市表。
|
||||
- 页面内容和操作都不变。Skill 版本不变(BUG-1469,issue #120)。
|
||||
|
||||
## 2026-10-28 — 每个页面的侧栏会话都能「…」和拖动
|
||||
|
||||
- 在星盘、星历、我的报告、星盘档案页,侧栏的会话也有「…」菜单(收藏、重命名、转发、删除),也能拖动排序,跟首页一样。
|
||||
|
||||
@@ -20033,3 +20033,19 @@
|
||||
- 相关记录:BUG-1042~1047(该测试来源)、BUG-1467。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/session-shell-116-20261027`。
|
||||
|
||||
## BUG-1469 | 星盘页、星盘档案页首次加载静态带着全国城市表(BUG-1130 在次级页复发)
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署,真机待产品)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检 → #120)
|
||||
- 影响面:`/chart`、`/people` 首次加载;`frontend/src/components/chart-page/chart-page-view.tsx`、`frontend/src/components/people/people-page.tsx`、`frontend/src/lib/people-archive-view.ts`。
|
||||
- 现象:星盘页首次加载约 704 KB(gzip,本地构建),其中约 108 KB 是只有点「改出生资料」才用得到的弹窗、出生地选择器、全国城市表和日历;档案页约 663 KB,同样带着城市表。
|
||||
- 触发条件:冷开 `/chart` 或 `/people`。
|
||||
- 根因:星盘页静态引入 `ChartBirthEditor`(→ `chart-profile-form` → `profile-fields` → `birth-location-fields` → `birth-place-picker` → `china-birth-place` → `data/china-locations`);档案页静态引入 `ChartProfileForm`,城市一列的 `peopleCityLabel` 又直接用 `china-birth-place` 查表。BUG-1130 只把首页首屏里的城市表挪走,并有意让档案页继续静态带表;防复发只覆盖首页首屏,所以次级页的静态路径没人拦。
|
||||
- 修复:星盘页的编辑弹窗改为点击时加载(`next/dynamic`,无占位),鼠标移上或聚焦那一行时预取;档案页的添加 / 编辑表单改为页面挂载后在后台取;`peopleCityLabel` 改读按需加载的表(`home-profile` 的 `findProvince` / `findCity`):只有编码没有已存地点的老资料,冷启动在人物列表落定前已把表载入(`current-subject.ts` `loadPlacesFor`),其余资料用已存地点名里的城市段,名字相同。
|
||||
- 验证:本地 `next build`(gzip-9,含 noModule 补丁,前后同口径):`/chart` 703,510 → 587,183 B(−116 KB),`/people` 662,898 → 551,646 B(−111 KB),首页 702,985 → 704,289 B(+0.19%,仍 `○ Static`)。新测试 `frontend/tests/secondary-first-load-20261028.test.ts`:从两个页面沿静态 import 图走,走不到城市表(在改动前的代码上失败并打印完整引用链);城市列在表未载入时用已存地点名、载入后与查表结果一致。
|
||||
- 防复发:上面的静态 import 图测试覆盖两个次级页;只在少数操作里用到的大模块(编辑弹窗、地点选择、城市表)不进任何页面的首次加载,首页之外的页面同样适用。
|
||||
- 相关记录:BUG-1130(首页首屏版本)、BUG-1127~1129。
|
||||
- 复发自:BUG-1130(防复发只覆盖首页首屏)
|
||||
- 修复版本:分支 `codex/chart-lazy-editor-20261028`。
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# 代码体检小单真机清单(issue #118,#120~#139)
|
||||
|
||||
每张小单部署到 staging 后在这里加几条。需要 staging 测试账号。
|
||||
|
||||
## #120 星盘页、档案页打开更快(BUG-1469)
|
||||
|
||||
1. 电脑打开星盘页:盘、表、右栏照常出来。点盘上方那行「日期 时间 · 地点」:弹出编辑框,能改能存,存完盘按新资料重排。
|
||||
2. 手机打开星盘页,点那一行:同样能弹出编辑框(可能比以前慢一点点,但不出现转圈)。
|
||||
3. 打开星盘档案页:每个人那一行的「日期 · 时间 · 城市」和以前一样;老资料(只选过省市、很早建的)城市也照常显示。
|
||||
4. 档案页点「添加一个人」、点某人的「编辑」:表单照常弹出,出生地能选、能存。
|
||||
+1
-1
@@ -412,7 +412,7 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
- Before the first message the list selects a person and updates that empty session's binding. Self is the visible default. After the first message the current person stays selected; another person offers only `新建会话并使用此人物` and does not rebind the old session. That action waits until the current request ends.
|
||||
- Loading shows the small orbit (`OrbitWaiting`, 2026-09-30), with `人物列表还在读` for screen readers only. Failure reads `人物列表暂时读不出来` with `重试`. When the people API itself fails, the popover reads `暂时读不到其他人` and lists only the signed-in person; history stays on that person and the page does not drop into the recoverable error screen. An incomplete row reads `这份资料还不完整` and cannot be chosen. A deleted person reads `资料已删除 · {名字}`; the old session stays readable and sending is blocked. None of these states use a spinner.
|
||||
- Rectification and daily horoscope do not get this control. Their quiet note, when present, remains the static `.chat-header-chart` span and still drops out below 768px. The person button stays on narrow screens and ellipsizes so the header remains one line.
|
||||
- The chart page birth line is a button (`.chart-birth-line`) after the person: `日期 时间 · 地点` and a pen. The name is not repeated. The whole row opens an editor on this page. Self can change every field. Someone else's already-saved birth time stays locked. The sentence `填报的出生时间保存后不能再改` appears once, beside that field, and is not repeated in the form description. Changing the date, time, or place shows `改了出生时间、日期或地点,之前生时校正采用的时间会作废,星盘改按你新填的时间排。` Saving uses the shared other-person write (self still goes through the account). The editor is a component-library dialog (2026-10-25, K3). A click inside the form, including its padding, does not close it or drop unsaved edits. Escape and a click on the backdrop do close it. The fields stay the shared profile form used by the people page, so this page does not grow a second set of fields. Below 768px that row wraps onto a second line and the chart header grows to fit it.
|
||||
- The chart page birth line is a button (`.chart-birth-line`) after the person: `日期 时间 · 地点` and a pen. The name is not repeated. The whole row opens an editor on this page. Self can change every field. Someone else's already-saved birth time stays locked. The sentence `填报的出生时间保存后不能再改` appears once, beside that field, and is not repeated in the form description. Changing the date, time, or place shows `改了出生时间、日期或地点,之前生时校正采用的时间会作废,星盘改按你新填的时间排。` Saving uses the shared other-person write (self still goes through the account). The editor is a component-library dialog (2026-10-25, K3). A click inside the form, including its padding, does not close it or drop unsaved edits. Escape and a click on the backdrop do close it. The fields stay the shared profile form used by the people page, so this page does not grow a second set of fields. Below 768px that row wraps onto a second line and the chart header grows to fit it. The editor's code is not in `/chart`'s first load: it loads on the click and is warmed when the pen row is hovered or focused, and nothing is drawn while it arrives (no placeholder; BUG-1469). The people page's add / edit form likewise stays out of `/people`'s first load and is fetched right after the page mounts.
|
||||
- With the composer strip gone as well, the two pieces of permanent chrome went from 68 + 148 to 46 + 124 px.
|
||||
- On viewports ≤767px the header row is 52px plus the safe-area inset. The credit control (`.credit-balance`) is a normal outline button, 44px, with a coins icon and the number; 点 stays in the accessible name. The rectification 「当前盘面」 chip stays 40px. The shared `::before` hit area stays inset -2px. The onboarding checklist trigger stays 32px.
|
||||
- Window-level scroll is reset by `ViewportScrollLock` after the on-screen keyboard closes, so the header cannot be left above the screen.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Pencil } from "lucide-react";
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
ChartTags,
|
||||
} from "@/components/chart-board/chart-side-cards";
|
||||
import { ChartTypeTable } from "@/components/chart-board/chart-type-table";
|
||||
import { ChartBirthEditor } from "@/components/chart-page/chart-birth-editor";
|
||||
import { SecondaryPageShell } from "@/components/secondary-page-shell";
|
||||
import { useBirthSkyCover } from "@/hooks/use-birth-sky-cover";
|
||||
import { useChartBoard } from "@/hooks/use-chart-board";
|
||||
@@ -47,6 +47,14 @@ import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
|
||||
import { ChartPageSkeleton } from "./chart-page-skeleton";
|
||||
import { ChartWesternTab } from "./chart-western-tab";
|
||||
|
||||
// The pencil is the only way into the birth editor, so its code (profile form,
|
||||
// place picker with the China city table, calendar) loads on that click and is
|
||||
// warmed when the pencil is hovered or focused. It used to ride in /chart's
|
||||
// first load (BUG-1469, the secondary-page return of BUG-1130).
|
||||
const loadChartBirthEditor = () => import("@/components/chart-page/chart-birth-editor")
|
||||
.then((module) => module.ChartBirthEditor);
|
||||
const ChartBirthEditor = dynamic(loadChartBirthEditor, { ssr: false, loading: () => null });
|
||||
|
||||
export function ChartPageRoute() {
|
||||
const { view, pendingLayers, layerFailures, requestLayer, reload } = useChartPage();
|
||||
const ok = view?.status === "ok" ? view : null;
|
||||
@@ -277,6 +285,8 @@ export function ChartPageView({
|
||||
variant="ghost"
|
||||
className="chart-birth-line h-auto rounded-none border-0 bg-transparent px-0 font-normal shadow-none"
|
||||
aria-label={`编辑出生资料,${view.profile.date} ${view.profile.time},${view.profile.placeLabel}`}
|
||||
onPointerEnter={() => { void loadChartBirthEditor(); }}
|
||||
onFocus={() => { void loadChartBirthEditor(); }}
|
||||
onClick={() => setEditingBirth(true)}
|
||||
>
|
||||
<span>{`${view.profile.date} ${view.profile.time} · ${view.profile.placeLabel}`}</span>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useEffect, useRef, useState, type Dispatch, type FormEvent, type ReactNode, type SetStateAction } from "react";
|
||||
import { ChevronLeft, ChevronRight, FileText, MessageSquareText, MoreHorizontal, Orbit, Plus } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { newChatHref } from "@/lib/chat-session-url";
|
||||
import { effectiveBirthDate } from "@/lib/effective-birth-date";
|
||||
import { ChartProfileForm } from "@/components/chart-profile-form";
|
||||
import { personRelationLabel } from "@/lib/person-relation";
|
||||
import { SecondaryPageShell } from "@/components/secondary-page-shell";
|
||||
import { bindCurrentSubjectAccount, invalidateSubjectCatalog, setCurrentSubject, useCurrentSubject } from "@/lib/current-subject";
|
||||
@@ -60,6 +60,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
// The add / edit form carries the place picker and with it the China city
|
||||
// table. It is not needed to read the archive, so it stays out of /people's
|
||||
// first load and is fetched right after the page mounts (BUG-1469).
|
||||
const loadChartProfileForm = () => import("@/components/chart-profile-form").then((module) => module.ChartProfileForm);
|
||||
const ChartProfileForm = dynamic(loadChartProfileForm, { ssr: false, loading: () => null });
|
||||
|
||||
export type PersonRow = {
|
||||
readonly id: string;
|
||||
readonly role: "self" | "other";
|
||||
@@ -119,6 +125,8 @@ export function PeoplePage() {
|
||||
const accountIdRef = useRef("");
|
||||
const { setAccount } = useSessionList();
|
||||
|
||||
useEffect(() => { void loadChartProfileForm(); }, []);
|
||||
|
||||
async function load() {
|
||||
setStatus("loading");
|
||||
try {
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
import { AYANAMSA_OPTIONS, resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { effectiveBirthDate } from "@/lib/effective-birth-date";
|
||||
import { findCityNode, findProvinceNode } from "@/lib/china-birth-place";
|
||||
import { profileBirthTimeLabel } from "@/lib/home-profile";
|
||||
import { findCity, findProvince, profileBirthTimeLabel } from "@/lib/home-profile";
|
||||
import type { Profile } from "@/lib/home-types";
|
||||
|
||||
/** D4: four existing element tokens; the person's id picks one, stably. */
|
||||
@@ -40,8 +39,11 @@ function trimCitySuffix(name: string): string {
|
||||
* of a 「中国 · 省 · 市 · 区」 label. Empty when the place is not complete.
|
||||
*/
|
||||
export function peopleCityLabel(profile: Profile): string {
|
||||
// The people page carries the table itself (china-birth-place imports it statically).
|
||||
const city = findCityNode(findProvinceNode(profile.provinceCode), profile.cityCode);
|
||||
// Reads the on-demand table (BUG-1469). A profile that has codes but no saved
|
||||
// label + coordinates has the table loaded before the people list settles
|
||||
// (current-subject.ts loadPlacesFor); every other profile carries its label,
|
||||
// whose city segment is the same name.
|
||||
const city = findCity(findProvince(profile.provinceCode), profile.cityCode);
|
||||
if (city) return trimCitySuffix(city.name);
|
||||
const label = profile.birthPlaceLabel.trim();
|
||||
if (!label) return "";
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { emptyProfile } from "../src/lib/home-types.ts";
|
||||
import { ensureChinaLocations, resetChinaLocationsForTests } from "../src/lib/china-locations-client.ts";
|
||||
import { peopleCityLabel } from "../src/lib/people-archive-view.ts";
|
||||
|
||||
// BUG-1469 (issue #120): BUG-1130 took the China city table out of the home
|
||||
// first screen, but /chart still loaded it statically through the birth
|
||||
// editor and /people through the add / edit form and the city column.
|
||||
// These tests walk the static import graph, so a new static path back to the
|
||||
// table fails here, whichever file it goes through.
|
||||
|
||||
const src = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../src");
|
||||
const extensions = [".ts", ".tsx", ".json", "/index.ts", "/index.tsx"];
|
||||
|
||||
function resolveImport(from: string, specifier: string): string | null {
|
||||
let base: string;
|
||||
if (specifier.startsWith("@/")) base = path.join(src, specifier.slice(2));
|
||||
else if (specifier.startsWith(".")) base = path.resolve(path.dirname(from), specifier);
|
||||
else return null;
|
||||
if (existsSync(base) && statSync(base).isFile()) return base;
|
||||
for (const extension of extensions) {
|
||||
if (existsSync(base + extension)) return base + extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Value imports and re-exports only: `import type`, `export type` and `import()` do not load code. */
|
||||
function staticImports(file: string): string[] {
|
||||
if (file.endsWith(".json")) return [];
|
||||
const text = readFileSync(file, "utf8");
|
||||
const found: string[] = [];
|
||||
const pattern = /^\s*(?:import|export)\s+(?!type\b)(?:[^;'"]*?\sfrom\s+)?["']([^"']+)["']/gm;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const resolved = resolveImport(file, match[1]);
|
||||
if (resolved) found.push(resolved);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function staticallyReaches(entry: string, target: RegExp): string[] | null {
|
||||
const seen = new Map<string, string | null>([[entry, null]]);
|
||||
const queue = [entry];
|
||||
while (queue.length > 0) {
|
||||
const file = queue.shift()!;
|
||||
if (target.test(file)) {
|
||||
const chain: string[] = [];
|
||||
for (let at: string | null = file; at; at = seen.get(at) ?? null) chain.unshift(path.relative(src, at));
|
||||
return chain;
|
||||
}
|
||||
for (const next of staticImports(file)) {
|
||||
if (seen.has(next)) continue;
|
||||
seen.set(next, file);
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const cityTable = /[\\/]data[\\/]china-locations(\.json|\.ts)?$/;
|
||||
|
||||
for (const entry of ["components/chart-page/chart-page-view.tsx", "components/people/people-page.tsx"]) {
|
||||
test(`${entry} does not load the China city table statically (BUG-1469)`, () => {
|
||||
const chain = staticallyReaches(path.join(src, entry), cityTable);
|
||||
assert.equal(chain, null, chain ? `static path: ${chain.join(" → ")}` : "");
|
||||
});
|
||||
}
|
||||
|
||||
test("the walker does see the table where it is still static, so the check above is not vacuous", () => {
|
||||
assert.ok(staticallyReaches(path.join(src, "components/birth-place-picker.tsx"), cityTable));
|
||||
});
|
||||
|
||||
test("the people city column reads the on-demand table and falls back to the saved label (BUG-1469)", async () => {
|
||||
resetChinaLocationsForTests();
|
||||
// Codes plus the label the picker saves: the label's city segment, same name as the table's.
|
||||
const saved = { ...emptyProfile, provinceCode: "130000", cityCode: "130100", birthPlaceLabel: "中国 · 河北省 · 石家庄市 · 长安区" };
|
||||
assert.equal(peopleCityLabel(saved), "石家庄");
|
||||
// Codes only (an old profile): blank until the table is loaded, which the cold boot does before the list settles.
|
||||
const codesOnly = { ...emptyProfile, provinceCode: "130000", cityCode: "130100" };
|
||||
assert.equal(peopleCityLabel(codesOnly), "");
|
||||
await ensureChinaLocations();
|
||||
assert.equal(peopleCityLabel(codesOnly), "石家庄");
|
||||
assert.equal(peopleCityLabel(saved), "石家庄");
|
||||
resetChinaLocationsForTests();
|
||||
});
|
||||
Reference in New Issue
Block a user