The four account panes now share a fixed frame, chart profiles open as list then detail, and membership lives in the homepage dialog instead of a separate page. Co-authored-by: Cursor <cursoragent@cursor.com>
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronRight, Settings, UserRound, Users, WalletCards, X } from "lucide-react";
|
|
import { memo, type MutableRefObject, type ReactNode } from "react";
|
|
|
|
export type AccountSettingsDialog = "profile" | "chart-library" | "billing" | "general";
|
|
|
|
export type AccountOverlayModel = Readonly<{
|
|
title: string;
|
|
dialogClass: string;
|
|
signingOut: boolean;
|
|
close: () => void;
|
|
navigate: (dialog: AccountSettingsDialog) => void;
|
|
overlayRef: MutableRefObject<HTMLElement | null>;
|
|
closeButtonRef: MutableRefObject<HTMLButtonElement | null>;
|
|
renderProfile: () => ReactNode;
|
|
renderChartLibrary: () => ReactNode;
|
|
renderBilling: () => ReactNode;
|
|
renderGeneral: () => ReactNode;
|
|
renderLogout: () => ReactNode;
|
|
}>;
|
|
|
|
const settingsItems: ReadonlyArray<{
|
|
dialog: AccountSettingsDialog;
|
|
label: string;
|
|
icon: typeof UserRound;
|
|
}> = [
|
|
{ dialog: "profile", label: "个人资料", icon: UserRound },
|
|
{ dialog: "chart-library", label: "星盘资料", icon: Users },
|
|
{ dialog: "billing", label: "账户与点数", icon: WalletCards },
|
|
{ dialog: "general", label: "通用设置", icon: Settings },
|
|
];
|
|
|
|
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
|
open,
|
|
dialog,
|
|
model,
|
|
}: Readonly<{
|
|
open: boolean;
|
|
dialog: "profile" | "chart-library" | "billing" | "general" | "logout" | null;
|
|
model: AccountOverlayModel | null;
|
|
}>) {
|
|
if (!open || dialog === null || model === null) return null;
|
|
|
|
const isSettingsDialog = dialog !== "logout";
|
|
const renderSettingsContent = () => {
|
|
if (dialog === "profile") return model.renderProfile();
|
|
if (dialog === "chart-library") return model.renderChartLibrary();
|
|
if (dialog === "billing") return model.renderBilling();
|
|
return model.renderGeneral();
|
|
};
|
|
|
|
return (
|
|
<div className="account-modal-overlay" onMouseDown={model.close}>
|
|
<section
|
|
className={`account-modal ${model.dialogClass}`}
|
|
ref={model.overlayRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="account-dialog-title"
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|
>
|
|
<header className="account-modal-header">
|
|
<h2 id="account-dialog-title">{model.title}</h2>
|
|
<button
|
|
className="dialog-close"
|
|
ref={model.closeButtonRef}
|
|
aria-label="关闭"
|
|
type="button"
|
|
onClick={model.close}
|
|
disabled={model.signingOut}
|
|
>
|
|
<X aria-hidden="true" />
|
|
</button>
|
|
</header>
|
|
{isSettingsDialog ? (
|
|
<div className="account-settings-shell">
|
|
<nav className="settings-dialog-nav" aria-label="设置分区">
|
|
{settingsItems.map(({ dialog: itemDialog, label, icon: Icon }) => (
|
|
<button
|
|
className="settings-dialog-nav-item"
|
|
key={itemDialog}
|
|
type="button"
|
|
aria-current={dialog === itemDialog ? "page" : undefined}
|
|
onClick={() => model.navigate(itemDialog)}
|
|
>
|
|
<Icon aria-hidden="true" />
|
|
<span>{label}</span>
|
|
<ChevronRight aria-hidden="true" />
|
|
</button>
|
|
))}
|
|
</nav>
|
|
<div className="settings-dialog-content">{renderSettingsContent()}</div>
|
|
</div>
|
|
) : model.renderLogout()}
|
|
</section>
|
|
</div>
|
|
);
|
|
});
|