fix(ui): 空态居中、大运页去重、星历加日历、顶栏动作按 46px 收

真机走查四条:

1. 空态不居中——问候+输入框+pill 整组贴在屏幕底部。加一个可压缩的
   配重行,让 chat-panel 变成 46px/1fr/auto/1fr,两个 1fr 抵消,
   整组居中;composer 仍是同一个 DOM 位置,不重挂载。

2. 大运页「Vimshottari」出现两遍、「当前 当前 Chara 段…」两个当前。
   两条轨道的 method 与自身 title 同字符串,method 只在不同时才渲染;
   兜底文案自带的「当前」去掉——它同时被参数表当「当前大运」的值用,
   不能把前缀塞进数据。

3. 星历只能一天一天点。日期改成可点的日历弹层,复用出生日期字段
   同一套 Calendar+Popover。新增 parseEphemerisDate/toEphemerisDate,
   都走本地日历字段、不经 UTC,避免跨时区差一天。

4. 「生成完整报告」在 46px 顶栏里是 44px 实心大按钮,像个横幅 CTA。
   顶栏作用域内动作统一 32px + outline,触摸目标靠 ::after 扩热区补。

测试 3372,fail 仍 31 且与基线逐条一致;四个路由标记不变;
CSS gzip 40,080。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-16 08:06:12 +00:00
co-authored by Claude Opus 5
parent d548bd0554
commit 49bc4a8ba1
8 changed files with 109 additions and 7 deletions
@@ -12,8 +12,12 @@ function Track({
<section className="chart-page-dasha-track">
<header>
<h3>{title}</h3>
<p>{method}</p>
<p>当前 {currentLabel}</p>
{/* Both tracks ship a `method` equal to their own title, which rendered
the name twice; only show it when it actually adds something. */}
{method && method !== title ? <p>{method}</p> : null}
{/* No space, and no second 当前: the fallback labels used to start with
it themselves, which read as 「当前 当前 Chara 段…」. */}
<p>当前{currentLabel}</p>
</header>
<ol className="chart-page-dasha-list">
{periods.map((period) => (
@@ -3,7 +3,11 @@
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { CalendarIcon } from "lucide-react";
import { zhCN } from "date-fns/locale";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { SecondaryShell } from "@/components/secondary-shell";
import { setComposerDraft } from "@/lib/composer-draft";
import { parseEphemerisOkResponse, type EphemerisOkResponse } from "@/lib/ephemeris-contract";
@@ -15,6 +19,8 @@ import {
formatHouse,
qualityLabel,
shiftEphemerisDate,
parseEphemerisDate,
toEphemerisDate,
} from "@/lib/ephemeris-view";
type PageState =
@@ -94,6 +100,7 @@ export function EphemerisPage() {
onPrevious={() => activeDate && changeDate(shiftEphemerisDate(activeDate, -1))}
onNext={() => activeDate && changeDate(shiftEphemerisDate(activeDate, 1))}
onToday={() => today && changeDate(today)}
onSelectDate={(iso) => changeDate(iso)}
onAsk={askAboutDate}
/>
);
@@ -106,6 +113,7 @@ export function EphemerisView(props: {
readonly onPrevious: () => void;
readonly onNext: () => void;
readonly onToday: () => void;
readonly onSelectDate: (iso: string) => void;
readonly onAsk: () => void;
}) {
const factors = props.payload?.panchanga.available ? props.payload.panchanga.factors : [];
@@ -114,6 +122,8 @@ export function EphemerisView(props: {
const eventsUnavailable = !props.payload || props.payload.events.status === "unavailable";
const events = props.payload?.events.status === "ok" ? props.payload.events.items : [];
const isToday = Boolean(props.date) && props.date === props.today;
const [pickerOpen, setPickerOpen] = useState(false);
const selectedDay = props.date ? parseEphemerisDate(props.date) : undefined;
return (
<SecondaryShell
@@ -142,7 +152,41 @@ export function EphemerisView(props: {
>
<span aria-hidden="true">‹</span>
</Button>
<p className="ephemeris-date-label">{props.date ? formatEphemerisDate(props.date) : ""}</p>
{/* The date itself opens a month grid. Stepping a day at a time was the
only way to reach another date, so anything more than a few days
away meant repeated clicking. Same Calendar + Popover the birth
date field uses. */}
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
<PopoverTrigger
render={<Button
type="button"
variant="ghost"
className="ephemeris-date-label"
aria-label={`${props.date ? formatEphemerisDate(props.date) : ""},选择日期`}
/>}
>
<CalendarIcon aria-hidden="true" />
<span>{props.date ? formatEphemerisDate(props.date) : ""}</span>
</PopoverTrigger>
<PopoverContent align="center" className="w-auto p-0">
<Calendar
mode="single"
locale={zhCN}
className="[--cell-size:2.5rem]"
selected={selectedDay}
defaultMonth={selectedDay}
captionLayout="dropdown"
navLayout="after"
startMonth={new Date(1900, 0)}
endMonth={new Date(2100, 11)}
onSelect={(next) => {
if (next === undefined) return;
setPickerOpen(false);
props.onSelectDate(toEphemerisDate(next));
}}
/>
</PopoverContent>
</Popover>
<Button
type="button"
variant="outline"
@@ -192,15 +192,20 @@ export function GeneratePersonalReportButton({
}
return (
/* Sized for the 46px page header it now lives in. It used to be the
default filled Button at full height, which read as a banner CTA
floating in the top-right corner rather than the header's own action. */
<Button
type="button"
variant="outline"
size="sm"
className="shrink-0 whitespace-nowrap"
onClick={() => void handleGenerate()}
disabled={submitting}
title="根据已保存的具体出生分钟生成完整报告;未校正会标明方向性参考"
>
<FileText aria-hidden="true" />
{submitting ? "正在创建…" : "生成完整报告"}
{submitting ? "正在创建…" : "生成报告"}
</Button>
);
}