feat(frontend): separate profile and chart settings
This commit is contained in:
@@ -1596,6 +1596,9 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.account-modal-overlay { position: fixed; z-index: 40; inset: 0; display: grid; place-items: center; padding: var(--space-4); background: var(--color-scrim); animation: account-overlay-enter 180ms ease-out both; }
|
||||
.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); }
|
||||
.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); }
|
||||
.profile-modal .birth-section { padding-top: var(--space-5); }
|
||||
.avatar-section { padding-top: 0; }
|
||||
@@ -1766,6 +1769,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.select-list { max-height: min(288px, 48dvh) !important; }
|
||||
.account-modal { max-height: calc(100dvh - var(--space-8)); padding: var(--space-6); }
|
||||
.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); }
|
||||
.avatar-editor > .user-avatar { width: 72px !important; height: 72px !important; }
|
||||
.avatar-palette-list { grid-template-columns: repeat(2, minmax(64px, 1fr)); }
|
||||
|
||||
+22
-25
@@ -272,7 +272,7 @@ type Account = {
|
||||
profile: unknown;
|
||||
};
|
||||
type OnboardingStep = "name" | "birth" | "place" | "rectification";
|
||||
type AccountDialog = "profile" | "logout";
|
||||
type AccountDialog = "profile" | "chart-library" | "logout";
|
||||
type DailyStarlanguageCard = { trend: string; action: string; caution: string };
|
||||
type DailyStarlanguageApiResponse = {
|
||||
status?: "ok" | "unavailable" | "unauthenticated";
|
||||
@@ -356,11 +356,13 @@ const themes = defaultGuidedJyotishTopics;
|
||||
|
||||
const accountDialogTitles = {
|
||||
profile: "个人资料",
|
||||
"chart-library": "星盘资料",
|
||||
logout: "退出登录?",
|
||||
} as const satisfies Record<AccountDialog, string>;
|
||||
|
||||
const accountDialogClasses = {
|
||||
profile: "profile-modal",
|
||||
"chart-library": "chart-library-modal",
|
||||
logout: "logout-modal",
|
||||
} as const satisfies Record<AccountDialog, string>;
|
||||
|
||||
@@ -1197,7 +1199,6 @@ export default function Home() {
|
||||
const modelSelectionVersions = useRef(new Map<string, number>());
|
||||
const activeSessionIdRef = useRef("");
|
||||
const chartLibraryLoadedAccount = useRef("");
|
||||
const openChartLibraryOnDialog = useRef(false);
|
||||
const accountOverlayRef = useRef<AccountOverlayModel | null>(null);
|
||||
const accountOverlayEpochRef = useRef(0);
|
||||
const chatActionsRef = useRef<ChatTranscriptActions>({
|
||||
@@ -3797,14 +3798,9 @@ export default function Home() {
|
||||
dialogClass: activeAccountDialog ? accountDialogClasses[activeAccountDialog] : "",
|
||||
signingOut,
|
||||
close: closeAccountDialog,
|
||||
takeOpenLibraryRequest() {
|
||||
if (!openChartLibraryOnDialog.current) return false;
|
||||
openChartLibraryOnDialog.current = false;
|
||||
return true;
|
||||
},
|
||||
overlayRef: accountDialog,
|
||||
closeButtonRef: closeButton,
|
||||
renderProfile(library) {
|
||||
renderProfile() {
|
||||
return (
|
||||
<>
|
||||
{accountError && <p className="form-error" role="alert">{accountError}</p>}
|
||||
@@ -3855,9 +3851,22 @@ export default function Home() {
|
||||
<strong>{profileDraft.name.trim() || "未命名"}</strong>
|
||||
<small>角色:本人</small>
|
||||
</div>
|
||||
<button className="button-secondary" type="button" onClick={library.toggle}>管理星盘库</button>
|
||||
<button className="button-secondary" type="button" onClick={() => openAccountDialog("chart-library")}>管理星盘库</button>
|
||||
</div>
|
||||
{library.open && (
|
||||
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
renderChartLibrary() {
|
||||
return (
|
||||
<>
|
||||
<p className="settings-dialog-copy">管理本人和其他人的出生资料,之后可直接用于切换默认星盘或发起合盘。</p>
|
||||
<div className="chart-library-panel" aria-label="星盘库">
|
||||
<div className="chart-library-group">
|
||||
<b>本人</b>
|
||||
@@ -3929,13 +3938,6 @@ export default function Home() {
|
||||
<button className="button-primary save-profile" type="submit" disabled={!account}>添加到星盘库</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -3995,15 +3997,10 @@ export default function Home() {
|
||||
onNewChat={() => void startNewChat()}
|
||||
onOpenReports={() => router.push("/reports")}
|
||||
onSelectSession={selectSession}
|
||||
onSelectChart={() => {
|
||||
openChartLibraryOnDialog.current = true;
|
||||
openAccountDialog("profile");
|
||||
}}
|
||||
onAddChart={() => {
|
||||
openChartLibraryOnDialog.current = true;
|
||||
openAccountDialog("profile");
|
||||
}}
|
||||
onSelectChart={() => openAccountDialog("chart-library")}
|
||||
onAddChart={() => openAccountDialog("chart-library")}
|
||||
onOpenProfile={() => openAccountDialog("profile")}
|
||||
onOpenChartLibrary={() => openAccountDialog("chart-library")}
|
||||
onOpenRedeem={() => router.push(membershipHref("account-menu"))}
|
||||
onOpenLogout={() => openAccountDialog("logout")}
|
||||
/>
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { memo, useEffect, useState, type MutableRefObject, type ReactNode } from "react";
|
||||
|
||||
export type AccountOverlayLibraryControls = Readonly<{
|
||||
open: boolean;
|
||||
toggle: () => void;
|
||||
}>;
|
||||
import { memo, type MutableRefObject, type ReactNode } from "react";
|
||||
|
||||
export type AccountOverlayModel = Readonly<{
|
||||
title: string;
|
||||
dialogClass: string;
|
||||
signingOut: boolean;
|
||||
close: () => void;
|
||||
takeOpenLibraryRequest: () => boolean;
|
||||
overlayRef: MutableRefObject<HTMLElement | null>;
|
||||
closeButtonRef: MutableRefObject<HTMLButtonElement | null>;
|
||||
renderProfile: (library: AccountOverlayLibraryControls) => ReactNode;
|
||||
renderProfile: () => ReactNode;
|
||||
renderChartLibrary: () => ReactNode;
|
||||
renderLogout: () => ReactNode;
|
||||
}>;
|
||||
|
||||
@@ -27,17 +22,10 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
||||
modelRef,
|
||||
}: Readonly<{
|
||||
open: boolean;
|
||||
dialog: "profile" | "logout" | null;
|
||||
dialog: "profile" | "chart-library" | "logout" | null;
|
||||
openEpoch: number;
|
||||
modelRef: MutableRefObject<AccountOverlayModel | null>;
|
||||
}>) {
|
||||
const [chartLibraryOpen, setChartLibraryOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (modelRef.current?.takeOpenLibraryRequest()) setChartLibraryOpen(true);
|
||||
}, [dialog, modelRef, open]);
|
||||
|
||||
if (!open || dialog === null) return null;
|
||||
const model = modelRef.current;
|
||||
if (!model) return null;
|
||||
@@ -66,10 +54,8 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
||||
<X aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
{dialog === "profile" && model.renderProfile({
|
||||
open: chartLibraryOpen,
|
||||
toggle: () => setChartLibraryOpen((current) => !current),
|
||||
})}
|
||||
{dialog === "profile" && model.renderProfile()}
|
||||
{dialog === "chart-library" && model.renderChartLibrary()}
|
||||
{dialog === "logout" && model.renderLogout()}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -71,6 +71,7 @@ export type AppSidebarProps = {
|
||||
onSelectChart: (chartId: string) => void;
|
||||
onAddChart: () => void;
|
||||
onOpenProfile: () => void;
|
||||
onOpenChartLibrary: () => void;
|
||||
onOpenRedeem: () => void;
|
||||
onOpenLogout: () => void;
|
||||
};
|
||||
@@ -92,6 +93,7 @@ export function AppSidebar({
|
||||
onSelectChart,
|
||||
onAddChart,
|
||||
onOpenProfile,
|
||||
onOpenChartLibrary,
|
||||
onOpenRedeem,
|
||||
onOpenLogout,
|
||||
}: AppSidebarProps) {
|
||||
@@ -303,6 +305,9 @@ export function AppSidebar({
|
||||
<Menu.Item className="account-menu-item" onClick={onOpenProfile}>
|
||||
<UserRound aria-hidden="true" /><span>个人资料</span><ChevronRight aria-hidden="true" />
|
||||
</Menu.Item>
|
||||
<Menu.Item className="account-menu-item" onClick={onOpenChartLibrary}>
|
||||
<Users aria-hidden="true" /><span>星盘资料</span><ChevronRight aria-hidden="true" />
|
||||
</Menu.Item>
|
||||
<Menu.Item className="account-menu-item" onClick={onOpenRedeem}>
|
||||
<Gift aria-hidden="true" /><span>兑换点数</span><small>{account.credits} 点</small>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -19,13 +19,15 @@ test("a closed account overlay does not render profile or logout content", () =>
|
||||
dialogClass: "profile-modal",
|
||||
signingOut: false,
|
||||
close() {},
|
||||
takeOpenLibraryRequest() { return false; },
|
||||
overlayRef,
|
||||
closeButtonRef,
|
||||
renderProfile() {
|
||||
profileRenders += 1;
|
||||
return null;
|
||||
},
|
||||
renderChartLibrary() {
|
||||
return null;
|
||||
},
|
||||
renderLogout() {
|
||||
logoutRenders += 1;
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user