From e0138496bec74cb666280df582d3e0acb62c760a Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Fri, 17 Jul 2026 16:08:49 +0800 Subject: [PATCH] Add local chart library management --- frontend/src/app/globals.css | 9 +++ frontend/src/app/page.tsx | 152 ++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1f59aa1e..d3d1bcf5 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -383,6 +383,15 @@ button:disabled { cursor: default; opacity: .45; } .default-chart-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-top: var(--space-5); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface-subtle); } .default-chart-card span, .default-chart-card small { display: block; color: var(--color-ink-secondary); font-size: var(--type-caption); } .default-chart-card strong { display: block; margin: 4px 0; color: var(--color-ink); font-size: var(--type-body); font-weight: 500; } +.chart-library-panel { display: grid; gap: var(--space-5); margin-top: var(--space-5); } +.chart-library-group { display: grid; gap: var(--space-3); } +.chart-library-group > b { color: var(--color-ink); font-size: var(--type-caption); font-weight: 600; } +.chart-library-item { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); } +.chart-library-item strong, .chart-library-item small { display: block; } +.chart-library-item strong { color: var(--color-ink); font-size: var(--type-body); font-weight: 500; } +.chart-library-item small, .chart-library-item > span, .empty-library-copy { color: var(--color-ink-secondary); font-size: var(--type-caption); } +.chart-library-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); } +.chart-library-form { padding-top: var(--space-4); border-top: 1px solid var(--color-border); } input, select { width: 100%; min-height: 44px; padding: 0 12px; border: 1px solid var(--color-border-strong); color: var(--color-ink); border-color: var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); font-size: 14px; } input:disabled, select:disabled { color: var(--color-ink-tertiary); background: var(--color-canvas-muted); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ce01f57f..7f72d2a5 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -33,6 +33,12 @@ type Profile = { cityCode: string; districtCode: string; }; +type ChartLibraryRecord = { + id: string; + role: "self" | "other"; + profile: Profile; + updatedAt: number; +}; type ChatSession = { id: string; title: string; theme: Theme; modelId: string; messages: Message[]; updatedAt: number }; type RequestError = { sessionId: string; message: string }; type StreamingReply = { sessionId: string; text: string }; @@ -166,6 +172,37 @@ function selectedBirthPlace(profile: Profile): BirthPlace | null { return { label, lat: location.center[1], lon: location.center[0], tz: china.timezone }; } +function chartLibraryStorageKey(accountId: string) { + return `jyotisha_chart_library:${accountId}`; +} + +function profileReadyForLibrary(profile: Profile) { + return !missingProfileStep(profile); +} + +function buildSelfChartRecord(profile: Profile): ChartLibraryRecord { + return { id: "self", role: "self", profile, updatedAt: timestamp() }; +} + +function upsertSelfChart(library: ChartLibraryRecord[], profile: Profile) { + if (!profileReadyForLibrary(profile)) return library.filter((record) => record.role !== "self"); + const others = library.filter((record) => record.role !== "self"); + return [buildSelfChartRecord(profile), ...others]; +} + +function readChartLibrary(accountId: string): ChartLibraryRecord[] { + try { + const parsed = JSON.parse(localStorage.getItem(chartLibraryStorageKey(accountId)) || "[]") as ChartLibraryRecord[]; + return Array.isArray(parsed) ? parsed.filter((record) => record?.id && record?.profile) : []; + } catch { + return []; + } +} + +function profilePlaceLabel(profile: Profile) { + return selectedBirthPlace(profile)?.label || "地点未完整"; +} + function missingProfileStep(profile: Profile): OnboardingStep | null { if (!profile.name.trim()) return "name"; if (!profile.date || !profile.time) return "birth"; @@ -330,12 +367,12 @@ function BirthLocationFields({ value, onChange }: { value: Profile; onChange: (p ); } -function ProfileFields({ value, onChange }: { value: Profile; onChange: (profile: Profile) => void }) { +function ProfileFields({ value, onChange, nameInputId }: { value: Profile; onChange: (profile: Profile) => void; nameInputId?: string }) { return ( <> @@ -428,6 +465,9 @@ async function fetchModelCatalog(signal?: AbortSignal) { export default function Home() { const [profile, setProfile] = useState(emptyProfile); const [profileDraft, setProfileDraft] = useState(emptyProfile); + const [chartLibrary, setChartLibrary] = useState([]); + const [chartLibraryOpen, setChartLibraryOpen] = useState(false); + const [otherProfileDraft, setOtherProfileDraft] = useState(emptyProfile); const [profileOpen, setProfileOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [profileNotice, setProfileNotice] = useState(""); @@ -491,6 +531,24 @@ export default function Home() { }, [activeSessionId]); const activeSuggestions = activeSession?.messages.reduce((latest, message) => message.role === "assistant" && message.suggestions?.length ? message.suggestions : latest, [] as string[]) ?? []; const accountId = account?.user.id; + + useEffect(() => { + if (!accountId) { + setChartLibrary([]); + return; + } + setChartLibrary(upsertSelfChart(readChartLibrary(accountId), profile)); + }, [accountId, profile]); + + useEffect(() => { + if (!accountId) return; + setChartLibrary((current) => { + const next = upsertSelfChart(current, profile); + localStorage.setItem(chartLibraryStorageKey(accountId), JSON.stringify(next)); + return next; + }); + }, [accountId, profile]); + const profileComplete = isProfileComplete(profile); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted @@ -932,6 +990,55 @@ export default function Home() { if (!data) throw new Error("账户档案不存在,请重新登录后再试。"); } + function saveOtherChart(event: FormEvent) { + event.preventDefault(); + const nextProfile = { ...otherProfileDraft, name: otherProfileDraft.name.trim() }; + if (missingProfileStep(nextProfile)) { + setAccountError("请补全其他星盘的称呼、出生时间和出生地点。"); + return; + } + if (!accountId) return; + const record: ChartLibraryRecord = { + id: globalThis.crypto.randomUUID(), + role: "other", + profile: nextProfile, + updatedAt: timestamp(), + }; + setChartLibrary((current) => { + const next = [...upsertSelfChart(current, profile), record]; + localStorage.setItem(chartLibraryStorageKey(accountId), JSON.stringify(next)); + return next; + }); + setOtherProfileDraft(emptyProfile); + setAccountError(""); + setProfileNotice("已添加到星盘库。"); + } + + function deleteOtherChart(recordId: string) { + if (!accountId) return; + setChartLibrary((current) => { + const next = current.filter((record) => record.id !== recordId || record.role === "self"); + localStorage.setItem(chartLibraryStorageKey(accountId), JSON.stringify(next)); + return next; + }); + } + + async function makeDefaultChart(record: ChartLibraryRecord) { + if (record.role !== "other" || profileSaving) return; + setProfileSaving(true); + setAccountError(""); + try { + await persistProfile(record.profile); + setProfile(record.profile); + setProfileDraft(record.profile); + setProfileNotice("已设为当前默认星盘。"); + } catch (caught) { + setAccountError(friendlyError(caught instanceof Error ? caught.message : "默认星盘保存失败")); + } finally { + setProfileSaving(false); + } + } + async function saveProfile(event: FormEvent) { event.preventDefault(); if (!isProfileComplete(profileDraft) || !account || profileSaving) return; @@ -1732,11 +1839,48 @@ export default function Home() { {profileDraft.name.trim() || "未命名"} 角色:本人 - + + {chartLibraryOpen && ( +
+
+ 本人 + {chartLibrary.filter((record) => record.role === "self").map((record) => ( +
+
+ {record.profile.name || "未命名"} + {record.profile.date} {record.profile.time} · {profilePlaceLabel(record.profile)} +
+ 当前默认 +
+ ))} +
+
+ 其他 + {chartLibrary.filter((record) => record.role === "other").length === 0 &&

还没有其他星盘。

} + {chartLibrary.filter((record) => record.role === "other").map((record) => ( +
+
+ {record.profile.name || "未命名"} + {record.profile.date} {record.profile.time} · {profilePlaceLabel(record.profile)} +
+
+ + +
+
+ ))} +
+
+
添加其他星盘用于合盘、亲友盘或客户盘。
+ + + +
+ )} {profileNotice &&

{profileNotice}

}
- +