fix(frontend): complete settings MVP structure
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user