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
@@ -1,20 +1,35 @@
"use client";
import { X } from "lucide-react";
import { ChevronRight, Settings, UserRound, Users, WalletCards, X } from "lucide-react";
import { memo, type MutableRefObject, type ReactNode } from "react";
export type AccountSettingsDialog = "profile" | "chart-library" | "general";
export type AccountOverlayModel = Readonly<{
title: string;
dialogClass: string;
signingOut: boolean;
close: () => void;
navigate: (dialog: AccountSettingsDialog) => void;
openRedeem: () => void;
overlayRef: MutableRefObject<HTMLElement | null>;
closeButtonRef: MutableRefObject<HTMLButtonElement | null>;
renderProfile: () => ReactNode;
renderChartLibrary: () => 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: "general", label: "通用设置", icon: Settings },
];
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
open,
dialog,
@@ -22,7 +37,7 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
modelRef,
}: Readonly<{
open: boolean;
dialog: "profile" | "chart-library" | "logout" | null;
dialog: "profile" | "chart-library" | "general" | "logout" | null;
openEpoch: number;
modelRef: MutableRefObject<AccountOverlayModel | null>;
}>) {
@@ -31,6 +46,13 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
if (!model) return null;
void openEpoch;
const isSettingsDialog = dialog !== "logout";
const renderSettingsContent = () => {
if (dialog === "profile") return model.renderProfile();
if (dialog === "chart-library") return model.renderChartLibrary();
return model.renderGeneral();
};
return (
<div className="account-modal-overlay" onMouseDown={model.close}>
<section
@@ -54,9 +76,31 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
<X aria-hidden="true" />
</button>
</header>
{dialog === "profile" && model.renderProfile()}
{dialog === "chart-library" && model.renderChartLibrary()}
{dialog === "logout" && model.renderLogout()}
{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>
))}
<button className="settings-dialog-nav-item" type="button" onClick={model.openRedeem}>
<WalletCards aria-hidden="true" />
<span></span>
<ChevronRight aria-hidden="true" />
</button>
</nav>
<div className="settings-dialog-content">{renderSettingsContent()}</div>
</div>
) : model.renderLogout()}
</section>
</div>
);
+8 -2
View File
@@ -6,8 +6,9 @@ import {
ChevronRight,
Clock3,
FileText,
Gift,
LogOut,
Settings,
WalletCards,
MessageSquareText,
SquarePen,
Star,
@@ -72,6 +73,7 @@ export type AppSidebarProps = {
onAddChart: () => void;
onOpenProfile: () => void;
onOpenChartLibrary: () => void;
onOpenGeneral: () => void;
onOpenRedeem: () => void;
onOpenLogout: () => void;
};
@@ -94,6 +96,7 @@ export function AppSidebar({
onAddChart,
onOpenProfile,
onOpenChartLibrary,
onOpenGeneral,
onOpenRedeem,
onOpenLogout,
}: AppSidebarProps) {
@@ -308,8 +311,11 @@ export function AppSidebar({
<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={onOpenGeneral}>
<Settings 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>
<WalletCards aria-hidden="true" /><span></span><small>{account.credits} </small>
</Menu.Item>
<Menu.Separator className="account-menu-separator" />
<ThemePreferenceMenu />
@@ -23,15 +23,44 @@ function OptionIcon({ value }: { readonly value: ThemePreference }) {
return <Monitor aria-hidden="true" />;
}
export function ThemePreferenceMenu() {
// The stored choice is browser state, not React state: read it through the
// external store so the server snapshot stays "system" (no hydration
// mismatch) and a change in another tab lands here too.
const preference = useSyncExternalStore(
function useThemePreference() {
return useSyncExternalStore(
subscribeThemePreference,
readThemePreference,
() => "system" as ThemePreference,
);
}
export function ThemePreferencePanel() {
const preference = useThemePreference();
return (
<section className="theme-preference-panel" aria-labelledby="theme-preference-title">
<div className="section-heading">
<b id="theme-preference-title"></b>
<small> Jyotisha </small>
</div>
<div className="theme-preference-options" role="group" aria-label="外观主题">
{options.map((option) => (
<button
className="theme-preference-option"
key={option.value}
type="button"
aria-pressed={preference === option.value}
onClick={() => applyThemePreference(option.value)}
>
<OptionIcon value={option.value} />
<span>{option.label}</span>
{preference === option.value ? <Check aria-hidden="true" /> : null}
</button>
))}
</div>
</section>
);
}
export function ThemePreferenceMenu() {
const preference = useThemePreference();
return (
<Menu.Group className="theme-menu">