fix(frontend): complete settings MVP structure
Independent Staging Quality Gate / validate (push) Failing after 10m21s
Independent Staging Quality Gate / publish (push) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-30 16:32:58 +08:00
parent f5e6d51646
commit a671986a1d
7 changed files with 209 additions and 31 deletions
+27 -1
View File
@@ -1587,6 +1587,18 @@ button:disabled { cursor: default; opacity: .45; }
.theme-menu-check { display: inline-grid; margin-left: auto; place-items: center; color: var(--color-action); }
.theme-menu-check svg { width: 15px; height: 15px; }
.account-menu-separator { height: 1px; margin: var(--space-2) var(--space-3); background: var(--color-border); }
.account-info-list { display: grid; gap: var(--space-3); margin: var(--space-5) 0 0; }
.account-info-list > div { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: var(--space-3); padding: var(--space-3) 0; border-top: 1px solid var(--color-border); }
.account-info-list dt { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.account-info-list dd { min-width: 0; margin: 0; color: var(--color-ink); font-size: var(--type-body-sm); }
.account-info-list dd small { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); }
.theme-preference-panel { display: grid; gap: var(--space-4); margin-top: var(--space-2); }
.theme-preference-options { display: grid; gap: var(--space-2); }
.theme-preference-option { min-height: 44px; display: grid; grid-template-columns: 20px minmax(0, 1fr) 18px; align-items: center; gap: var(--space-3); padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
.theme-preference-option:hover, .theme-preference-option[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
.theme-preference-option > svg { width: 18px; height: 18px; color: var(--color-ink-tertiary); }
.theme-preference-option > svg:last-child { color: var(--color-action); }
.account-menu-danger, .account-menu-danger > svg { color: var(--color-danger); }
.account-menu-danger[data-highlighted] { background: var(--color-danger-muted); }
@@ -1598,6 +1610,14 @@ button:disabled { cursor: default; opacity: .45; }
.account-modal { width: min(100%, 560px); max-height: min(84dvh, 760px); overflow-y: auto; padding: var(--space-8); border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-canvas); box-shadow: var(--shadow-elevated); animation: account-dialog-enter 180ms var(--ease-out) both; }
.profile-modal { width: min(100%, 680px); scroll-padding-block-start: calc(var(--space-8) + 72px); }
.chart-library-modal { width: min(100%, 760px); scroll-padding-block-start: calc(var(--space-8) + 72px); }
.account-settings-shell { display: grid; grid-template-columns: 176px minmax(0, 1fr); gap: var(--space-8); align-items: start; }
.settings-dialog-nav { display: grid; gap: var(--space-1); position: sticky; top: 0; }
.settings-dialog-nav-item { min-height: 44px; display: grid; grid-template-columns: 18px minmax(0, 1fr) 16px; align-items: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-secondary); cursor: pointer; text-align: left; }
.settings-dialog-nav-item:hover, .settings-dialog-nav-item[aria-current="page"] { background: var(--color-canvas-muted); color: var(--color-ink); }
.settings-dialog-nav-item[aria-current="page"] { box-shadow: inset 2px 0 0 var(--color-action); }
.settings-dialog-nav-item > svg { width: 18px; height: 18px; color: var(--color-ink-tertiary); }
.settings-dialog-nav-item > svg:last-child { width: 15px; height: 15px; margin-left: auto; }
.settings-dialog-content { min-width: 0; }
.chart-library-modal .account-modal-header { position: sticky; z-index: 1; top: 0; margin: calc(var(--space-8) * -1) calc(var(--space-8) * -1) var(--space-3); padding: var(--space-8) var(--space-8) var(--space-5); border-bottom: 1px solid var(--color-border); background: var(--color-canvas); }
.settings-dialog-copy { margin: 0 0 var(--space-5); color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; }
.profile-modal .account-modal-header { position: sticky; z-index: 1; top: 0; margin: calc(var(--space-8) * -1) calc(var(--space-8) * -1) var(--space-5); padding: var(--space-8) var(--space-8) var(--space-5); border-bottom: 1px solid var(--color-border); background: var(--color-canvas); }
@@ -1777,7 +1797,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
:root { --composer-reserve: 116px; }
.select-content { max-height: min(320px, 56dvh); }
.select-list { max-height: min(288px, 48dvh) !important; }
.account-modal { max-height: calc(100dvh - var(--space-8)); padding: var(--space-6); }
.account-modal-overlay { padding: 0; }
.account-modal { width: 100%; max-height: 100dvh; min-height: 100dvh; border-radius: 0; padding: var(--space-6); }
.account-settings-shell { grid-template-columns: 1fr; gap: var(--space-4); }
.settings-dialog-nav { position: static; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-1); padding-bottom: var(--space-3); border-bottom: 1px solid var(--color-border); }
.settings-dialog-nav-item { min-height: 40px; grid-template-columns: 1fr; justify-items: center; gap: 2px; padding: var(--space-2); text-align: center; font-size: var(--type-caption); }
.settings-dialog-nav-item > svg:last-child { display: none; }
.settings-dialog-nav-item[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--color-action); }
.profile-modal .account-modal-header { margin: calc(var(--space-6) * -1) calc(var(--space-6) * -1) var(--space-4); padding: var(--space-6) var(--space-6) var(--space-4); }
.chart-library-modal .account-modal-header { margin: calc(var(--space-6) * -1) calc(var(--space-6) * -1) var(--space-4); padding: var(--space-6) var(--space-6) var(--space-4); }
.avatar-editor { grid-template-columns: 72px minmax(0, 1fr); gap: var(--space-4); }
+39 -18
View File
@@ -8,6 +8,7 @@ import { ArrowDown, ArrowUpRight, Sparkles } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { AccountDialogOverlay, type AccountOverlayModel } from "@/components/account-dialog-overlay";
import { ThemePreferencePanel } from "@/components/theme-preference-menu";
import { AppSidebar } from "@/components/app-sidebar";
import { UserAvatar } from "@/components/user-avatar";
import {
@@ -283,7 +284,7 @@ type Account = {
profile: unknown;
};
type OnboardingStep = "name" | "birth" | "place" | "rectification";
type AccountDialog = "profile" | "chart-library" | "logout";
type AccountDialog = "profile" | "chart-library" | "general" | "logout";
type DailyStarlanguageCard = { trend: string; action: string; caution: string };
type DailyStarlanguageApiResponse = {
status?: "ok" | "unavailable" | "unauthenticated";
@@ -368,12 +369,14 @@ const themes = defaultGuidedJyotishTopics;
const accountDialogTitles = {
profile: "个人资料",
"chart-library": "星盘资料",
general: "通用设置",
logout: "退出登录?",
} as const satisfies Record<AccountDialog, string>;
const accountDialogClasses = {
profile: "profile-modal",
"chart-library": "chart-library-modal",
general: "general-modal",
logout: "logout-modal",
} as const satisfies Record<AccountDialog, string>;
@@ -1223,6 +1226,7 @@ export default function Home() {
const [otherProfileDraft, setOtherProfileDraft] = useState<Profile>(emptyProfile);
const [otherChartRelationship, setOtherChartRelationship] = useState<Exclude<ChartRelationship, "self">>("other");
const [editingChartId, setEditingChartId] = useState<string | null>(null);
const [editingSelfChart, setEditingSelfChart] = useState(false);
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
const [dailyStarlanguage, setDailyStarlanguage] = useState<DailyStarlanguageState>({ kind: "pending" });
@@ -2413,10 +2417,14 @@ export default function Home() {
setAccountMenuOpen(false);
setAccountError("");
if (dialog === "profile") {
setProfileDraft(profile);
setProfileNotice("");
setAvatarNotice("");
}
if (dialog === "chart-library") {
setProfileDraft(profile);
setProfileNotice("");
setEditingSelfChart(false);
}
setActiveAccountDialog(dialog);
}
@@ -3974,6 +3982,8 @@ export default function Home() {
dialogClass: activeAccountDialog ? accountDialogClasses[activeAccountDialog] : "",
signingOut,
close: closeAccountDialog,
navigate: (dialog) => openAccountDialog(dialog, dialogReturnTarget.current),
openRedeem: () => router.push(membershipHref("account-menu")),
overlayRef: accountDialog,
closeButtonRef: closeButton,
renderProfile() {
@@ -4019,23 +4029,16 @@ export default function Home() {
{avatarNotice && <p className="form-success avatar-notice" role="status">{avatarNotice}</p>}
</section>
)}
<section className="sheet-section birth-section">
<div className="section-heading"><b></b><small></small></div>
<div className="default-chart-card" aria-label="当前默认星盘">
<div>
<span></span>
<strong>{profileDraft.name.trim() || "未命名"}</strong>
<small></small>
</div>
<button className="button-secondary" type="button" onClick={() => openAccountDialog("chart-library")}></button>
</div>
<section className="sheet-section account-info-section" aria-labelledby="account-info-title">
<div className="section-heading"><b id="account-info-title"></b><small></small></div>
<dl className="account-info-list">
<div><dt></dt><dd>{profile.name.trim() || "尚未设置"}<small></small></dd></div>
<div><dt></dt><dd>{account.user.email || "尚未读取邮箱"}</dd></div>
</dl>
</section>
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
<form className="profile-form" onSubmit={saveProfile}>
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="profile-name" />
<button className="button-primary save-profile" type="submit" disabled={profileSaving || !account}>{profileSaving ? "保存中" : "保存出生资料"}</button>
</form>
<div className="dialog-actions">
<button className="button-secondary" type="button" onClick={() => openAccountDialog("chart-library")}></button>
</div>
</>
);
},
@@ -4056,8 +4059,22 @@ export default function Home() {
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
</div>
<div className="chart-library-actions">
<button className="button-secondary" type="button" onClick={() => { setProfileDraft(record.profile); setEditingSelfChart(true); }} disabled={profileSaving || editingChartId !== null}></button>
</div>
</article>
))}
{editingSelfChart && (
<form className="profile-form chart-library-form" onSubmit={saveProfile}>
<div className="section-heading"><b></b><small></small></div>
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="self-profile-name" />
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
<div className="dialog-actions">
<button className="button-secondary" type="button" onClick={() => { setEditingSelfChart(false); setProfileDraft(profile); }}></button>
<button className="button-primary save-profile" type="submit" disabled={!account || profileSaving}>{profileSaving ? "保存中" : "保存本人资料"}</button>
</div>
</form>
)}
{chartLibrary.filter((record) => record.role === "self").length === 0 && <p className="empty-library-copy"></p>}
</div>
<div className="chart-library-group">
@@ -4139,6 +4156,9 @@ export default function Home() {
</>
);
},
renderGeneral() {
return <ThemePreferencePanel />;
},
renderLogout() {
return (
<>
@@ -4199,6 +4219,7 @@ export default function Home() {
onAddChart={() => openAccountDialog("chart-library")}
onOpenProfile={() => openAccountDialog("profile")}
onOpenChartLibrary={() => openAccountDialog("chart-library")}
onOpenGeneral={() => openAccountDialog("general")}
onOpenRedeem={() => router.push(membershipHref("account-menu"))}
onOpenLogout={() => openAccountDialog("logout")}
/>