diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 767528b4..d523a550 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index af88166b..d021400a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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; const accountDialogClasses = { profile: "profile-modal", "chart-library": "chart-library-modal", + general: "general-modal", logout: "logout-modal", } as const satisfies Record; @@ -1223,6 +1226,7 @@ export default function Home() { const [otherProfileDraft, setOtherProfileDraft] = useState(emptyProfile); const [otherChartRelationship, setOtherChartRelationship] = useState>("other"); const [editingChartId, setEditingChartId] = useState(null); + const [editingSelfChart, setEditingSelfChart] = useState(false); const [synastryReportCard, setSynastryReportCard] = useState(null); const [synastryHistory, setSynastryHistory] = useState([]); const [dailyStarlanguage, setDailyStarlanguage] = useState({ 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 &&

{avatarNotice}

} )} -
-
出生资料加密传输并保存到云端,用于此账号的所有对话
-
-
- 当前默认星盘 - {profileDraft.name.trim() || "未命名"} - 角色:本人 -
- -
- +
+
账户信息登录与账户识别信息
+
+
昵称
{profile.name.trim() || "尚未设置"}与“我的星盘”名称同步
+
登录邮箱
{account.user.email || "尚未读取邮箱"}
+
- {profileNotice &&

{profileNotice}

} -
- - - +
+ +
); }, @@ -4056,8 +4059,22 @@ export default function Home() { {record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)} {profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)} +
+ +
))} + {editingSelfChart && ( +
+
编辑本人星盘这份资料会用于默认解盘与新对话。
+ + {profileNotice &&

{profileNotice}

} +
+ + +
+ + )} {chartLibrary.filter((record) => record.role === "self").length === 0 &&

请先在个人资料中补全你的出生资料。

}
@@ -4139,6 +4156,9 @@ export default function Home() { ); }, + renderGeneral() { + return ; + }, 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")} /> diff --git a/frontend/src/components/account-dialog-overlay.tsx b/frontend/src/components/account-dialog-overlay.tsx index 3c360fd4..31758af9 100644 --- a/frontend/src/components/account-dialog-overlay.tsx +++ b/frontend/src/components/account-dialog-overlay.tsx @@ -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; closeButtonRef: MutableRefObject; 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; }>) { @@ -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 (
); diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 119a2b67..104e7c06 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -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({ + + - diff --git a/frontend/src/components/theme-preference-menu.tsx b/frontend/src/components/theme-preference-menu.tsx index 4880d4f1..c0095ae1 100644 --- a/frontend/src/components/theme-preference-menu.tsx +++ b/frontend/src/components/theme-preference-menu.tsx @@ -23,15 +23,44 @@ function OptionIcon({ value }: { readonly value: ThemePreference }) { return