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>
348 lines
14 KiB
TypeScript
348 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronRight } from "lucide-react";
|
|
import { useState, type Dispatch, FormEvent, SetStateAction } from "react";
|
|
import { ChartProfileForm } from "@/components/chart-profile-form";
|
|
import { activeChartStorageKey, deleteCloudChartProfile, saveCloudChartProfile, updateCloudChartProfile } from "@/lib/home-cloud-sync";
|
|
import {
|
|
chartRelationshipLabel,
|
|
formatChartUpdatedAt,
|
|
missingOtherProfileStep,
|
|
profileBirthTimeLabel,
|
|
profileBirthTimeStatusLabel,
|
|
profilePlaceLabel,
|
|
readProfile,
|
|
upsertSelfChart,
|
|
} from "@/lib/home-profile";
|
|
import {
|
|
emptyProfile,
|
|
timestamp,
|
|
type Account,
|
|
type ChartLibraryRecord,
|
|
type ChartRelationship,
|
|
type Profile,
|
|
type SynastryRelationshipType,
|
|
type SynastryReportCard,
|
|
} from "@/lib/home-types";
|
|
import {
|
|
CHART_LIBRARY_LIST_VIEW,
|
|
synastryHistoryForPartner,
|
|
type ChartLibraryView,
|
|
} from "@/lib/chart-library-view";
|
|
|
|
export type ChartLibraryPanelProps = {
|
|
readonly account: Account | null;
|
|
readonly accountId: string | undefined;
|
|
readonly chartLibrary: ChartLibraryRecord[];
|
|
readonly setChartLibrary: Dispatch<SetStateAction<ChartLibraryRecord[]>>;
|
|
readonly profile: Profile;
|
|
readonly profileDraft: Profile;
|
|
readonly setProfileDraft: Dispatch<SetStateAction<Profile>>;
|
|
readonly setProfile: Dispatch<SetStateAction<Profile>>;
|
|
readonly profileSaving: boolean;
|
|
readonly profileNotice: string;
|
|
readonly setProfileNotice: Dispatch<SetStateAction<string>>;
|
|
readonly setAccountError: Dispatch<SetStateAction<string>>;
|
|
readonly synastryRelationshipType: SynastryRelationshipType;
|
|
readonly setSynastryRelationshipType: Dispatch<SetStateAction<SynastryRelationshipType>>;
|
|
readonly synastryPendingId: string | null;
|
|
readonly synastryReportCard: SynastryReportCard | null;
|
|
readonly setSynastryReportCard: Dispatch<SetStateAction<SynastryReportCard | null>>;
|
|
readonly synastryHistory: SynastryReportCard[];
|
|
readonly activeChartId: string;
|
|
readonly setActiveChartId: Dispatch<SetStateAction<string>>;
|
|
readonly saveProfile: (event: FormEvent<HTMLFormElement>) => void;
|
|
readonly draftSynastryQuestionFromChart: (record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) => void;
|
|
};
|
|
|
|
export function ChartLibraryPanel({
|
|
account,
|
|
accountId,
|
|
chartLibrary,
|
|
setChartLibrary,
|
|
profile,
|
|
profileDraft,
|
|
setProfileDraft,
|
|
setProfile,
|
|
profileSaving,
|
|
profileNotice,
|
|
setProfileNotice,
|
|
setAccountError,
|
|
synastryRelationshipType,
|
|
setSynastryRelationshipType,
|
|
synastryPendingId,
|
|
synastryReportCard,
|
|
setSynastryReportCard,
|
|
synastryHistory,
|
|
activeChartId,
|
|
setActiveChartId,
|
|
saveProfile,
|
|
draftSynastryQuestionFromChart,
|
|
}: ChartLibraryPanelProps) {
|
|
const [view, setView] = useState<ChartLibraryView>(CHART_LIBRARY_LIST_VIEW);
|
|
const [otherProfileDraft, setOtherProfileDraft] = useState<Profile>(emptyProfile);
|
|
const [otherChartRelationship, setOtherChartRelationship] = useState<Exclude<ChartRelationship, "self">>("other");
|
|
const [editingChartId, setEditingChartId] = useState<string | null>(null);
|
|
|
|
const selfCharts = chartLibrary.filter((record) => record.role === "self");
|
|
const otherCharts = chartLibrary.filter((record) => record.role === "other");
|
|
const selectedOther = view.kind === "other"
|
|
? otherCharts.find((record) => record.id === view.id) ?? null
|
|
: null;
|
|
const partnerHistory = selectedOther
|
|
? synastryHistoryForPartner(synastryHistory, {
|
|
id: selectedOther.id,
|
|
name: selectedOther.profile.name || "对方",
|
|
})
|
|
: [];
|
|
|
|
function goList() {
|
|
setView(CHART_LIBRARY_LIST_VIEW);
|
|
setEditingChartId(null);
|
|
setOtherProfileDraft(emptyProfile);
|
|
setOtherChartRelationship("other");
|
|
setProfileDraft(profile);
|
|
}
|
|
|
|
async function saveOtherChart(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
const nextProfile = { ...otherProfileDraft, name: otherProfileDraft.name.trim(), chartRelationship: otherChartRelationship };
|
|
|
|
if (missingOtherProfileStep(nextProfile)) {
|
|
setAccountError("请补全其他星盘的称呼、出生时间和出生地点。");
|
|
return;
|
|
}
|
|
if (!accountId) return;
|
|
const record: ChartLibraryRecord = {
|
|
id: editingChartId || globalThis.crypto.randomUUID(),
|
|
role: "other",
|
|
profile: nextProfile,
|
|
relationship: otherChartRelationship,
|
|
updatedAt: timestamp(),
|
|
};
|
|
try {
|
|
const saved = editingChartId
|
|
? await updateCloudChartProfile(record)
|
|
: await saveCloudChartProfile(record);
|
|
const selfProfile = account ? readProfile(account.profile) : profile;
|
|
setChartLibrary((current) => {
|
|
const others = editingChartId
|
|
? current.map((item) => item.id === saved.id ? saved : item)
|
|
: [...current, saved];
|
|
return upsertSelfChart(others, selfProfile);
|
|
});
|
|
setOtherProfileDraft(emptyProfile);
|
|
setOtherChartRelationship("other");
|
|
setEditingChartId(null);
|
|
setAccountError("");
|
|
setProfileNotice(editingChartId ? "已更新其他人的星盘资料。" : "已保存到云端星盘库。请选择关系类型后点击“用于合盘”。");
|
|
setView(CHART_LIBRARY_LIST_VIEW);
|
|
} catch {
|
|
setProfileNotice("保存失败,请重试");
|
|
setAccountError("");
|
|
}
|
|
}
|
|
|
|
function editOtherChart(record: ChartLibraryRecord) {
|
|
if (record.role !== "other") return;
|
|
setOtherProfileDraft(record.profile);
|
|
setOtherChartRelationship(record.relationship === "self" ? "other" : record.relationship);
|
|
setEditingChartId(record.id);
|
|
setAccountError("");
|
|
setProfileNotice("");
|
|
setView({ kind: "other", id: record.id });
|
|
}
|
|
|
|
async function deleteOtherChart(recordId: string) {
|
|
if (!accountId || !window.confirm("确定删除这份其他人的星盘资料吗?删除后无法恢复。")) return;
|
|
try {
|
|
await deleteCloudChartProfile(recordId);
|
|
} catch {
|
|
setProfileNotice("删除失败,请重试");
|
|
setAccountError("");
|
|
return;
|
|
}
|
|
setChartLibrary((current) => {
|
|
const next = current.filter((record) => record.id !== recordId || record.role === "self");
|
|
if (activeChartId === recordId) {
|
|
setActiveChartId("self");
|
|
localStorage.setItem(activeChartStorageKey(accountId), "self");
|
|
}
|
|
return next;
|
|
});
|
|
setAccountError("");
|
|
setProfileNotice("已从云端星盘库删除。");
|
|
goList();
|
|
}
|
|
|
|
function makeDefaultChart(record: ChartLibraryRecord) {
|
|
if (record.role !== "other" || profileSaving || !accountId) return;
|
|
setActiveChartId(record.id);
|
|
localStorage.setItem(activeChartStorageKey(accountId), record.id);
|
|
setProfile(record.profile);
|
|
setProfileNotice("已设为当前使用资料,账户本人的出生资料未被覆盖。");
|
|
}
|
|
|
|
function listRow(record: ChartLibraryRecord) {
|
|
const isSelf = record.role === "self";
|
|
return (
|
|
<button
|
|
className="chart-library-item"
|
|
key={record.id}
|
|
type="button"
|
|
onClick={() => {
|
|
if (isSelf) {
|
|
setProfileDraft(record.profile);
|
|
setView({ kind: "self" });
|
|
return;
|
|
}
|
|
editOtherChart(record);
|
|
}}
|
|
>
|
|
<div className="chart-library-item-main">
|
|
<div className="chart-library-item-title">
|
|
<strong>{record.profile.name || "未命名"}</strong>
|
|
<span className={`chart-role-badge${isSelf ? " is-self" : ""}`}>{isSelf ? "本人" : chartRelationshipLabel(record.relationship)}</span>
|
|
{isSelf ? <span className="chart-default-badge">当前默认</span> : null}
|
|
</div>
|
|
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
|
|
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
|
|
</div>
|
|
<ChevronRight className="chart-library-item-chevron" aria-hidden="true" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
if (view.kind === "self") {
|
|
return (
|
|
<div className="chart-library-panel" aria-label="星盘资料管理">
|
|
<button className="chart-library-back" type="button" onClick={goList}>← 星盘资料</button>
|
|
<ChartProfileForm
|
|
title="编辑本人星盘"
|
|
description="这份资料会用于默认解盘与新对话。"
|
|
value={profileDraft}
|
|
onChange={setProfileDraft}
|
|
nameInputId="self-profile-name"
|
|
showAyanamsa
|
|
onSubmit={saveProfile}
|
|
onCancel={goList}
|
|
cancelLabel="返回列表"
|
|
submitLabel={profileSaving ? "保存中" : "保存本人资料"}
|
|
submitDisabled={!account || profileSaving}
|
|
notice={profileNotice}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (view.kind === "add" || (view.kind === "other" && selectedOther)) {
|
|
const isAdd = view.kind === "add";
|
|
return (
|
|
<div className="chart-library-panel" aria-label="星盘资料管理">
|
|
<button className="chart-library-back" type="button" onClick={goList}>← 星盘资料</button>
|
|
<ChartProfileForm
|
|
title={isAdd ? "添加其他人的星盘" : "编辑其他人的星盘"}
|
|
description="用于合盘、亲友盘或客户盘。"
|
|
value={otherProfileDraft}
|
|
onChange={setOtherProfileDraft}
|
|
nameInputId="other-profile-name"
|
|
relationship={otherChartRelationship}
|
|
onRelationshipChange={setOtherChartRelationship}
|
|
onSubmit={saveOtherChart}
|
|
onCancel={goList}
|
|
cancelLabel="返回列表"
|
|
submitLabel={profileSaving ? "保存中" : editingChartId ? "保存修改" : "添加到星盘库"}
|
|
submitDisabled={!account || profileSaving}
|
|
notice={profileNotice}
|
|
/>
|
|
{!isAdd && selectedOther ? (
|
|
<>
|
|
<div className="chart-library-actions">
|
|
<button className="button-secondary" type="button" onClick={() => makeDefaultChart(selectedOther)} disabled={profileSaving}>设为默认</button>
|
|
<button className="button-secondary" type="button" onClick={() => void deleteOtherChart(selectedOther.id)}>删除</button>
|
|
</div>
|
|
<section className="chart-library-group" aria-label="合盘">
|
|
<div className="chart-library-group-heading">
|
|
<div><b>合盘</b><small>把这份资料用于合盘分析</small></div>
|
|
</div>
|
|
<div className="chart-library-actions">
|
|
<select aria-label={`${selectedOther.profile.name || "其他人"}的关系类型`} value={synastryRelationshipType} onChange={(event) => setSynastryRelationshipType(event.target.value as SynastryRelationshipType)} disabled={synastryPendingId !== null}>
|
|
<option value="romance">婚恋</option>
|
|
<option value="business">商业合作</option>
|
|
<option value="family">亲友/家庭</option>
|
|
<option value="general">其他关系</option>
|
|
</select>
|
|
<button className="button-secondary" type="button" onClick={() => void draftSynastryQuestionFromChart(selectedOther, synastryRelationshipType)} disabled={synastryPendingId !== null}>{synastryPendingId === selectedOther.id ? "正在计算合盘..." : "用于合盘"}</button>
|
|
</div>
|
|
{synastryReportCard && synastryHistoryForPartner([synastryReportCard], { id: selectedOther.id, name: selectedOther.profile.name || "对方" }).length > 0 && (
|
|
<article className="synastry-report-card" aria-label="合盘结果摘要">
|
|
<div>
|
|
<span>合盘结果摘要</span>
|
|
<strong>{synastryReportCard.partnerName}</strong>
|
|
<small>Ashtakoot {synastryReportCard.score ?? "?"}/{synastryReportCard.maxScore ?? "?"} · {synastryReportCard.assessment || synastryReportCard.scoreBand || "待解释"}</small>
|
|
</div>
|
|
{synastryReportCard.headline && <p>{synastryReportCard.headline}</p>}
|
|
<details>
|
|
<summary>查看证据</summary>
|
|
<ul>
|
|
{(synastryReportCard.strengths || []).map((item) => <li key={item}>{item}</li>)}
|
|
{(synastryReportCard.risks || []).map((item) => <li key={item}>{item}</li>)}
|
|
</ul>
|
|
<small>下一步证据:{(synastryReportCard.nextEvidence || []).join(" / ") || "双方 Dasha / UL-DK / D9 7宫"}</small>
|
|
</details>
|
|
</article>
|
|
)}
|
|
{partnerHistory.length > 0 && (
|
|
<div className="synastry-history-list" aria-label="合盘历史">
|
|
<b>合盘历史</b>
|
|
{partnerHistory.slice(0, 5).map((item) => (
|
|
<button key={item.id} type="button" className="synastry-history-item" onClick={() => setSynastryReportCard(item)}>
|
|
<span>{item.partnerName}</span>
|
|
<small>Ashtakoot {item.score ?? "?"}/{item.maxScore ?? "?"} · {item.assessment || item.scoreBand || "待解释"}</small>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="chart-library-panel" aria-label="星盘资料管理">
|
|
<div className="chart-library-group">
|
|
<div className="chart-library-group-heading">
|
|
<div><b>我的星盘</b><small>当前账号的默认资料</small></div>
|
|
<span className="chart-library-count">{selfCharts.length}</span>
|
|
</div>
|
|
{selfCharts.map(listRow)}
|
|
{selfCharts.length === 0 && <p className="empty-library-copy">请先在个人资料中补全你的出生资料。</p>}
|
|
</div>
|
|
<div className="chart-library-group">
|
|
<div className="chart-library-group-heading">
|
|
<div><b>其他人</b><small>亲友、伴侣或客户资料</small></div>
|
|
<span className="chart-library-count">{otherCharts.length}</span>
|
|
</div>
|
|
{otherCharts.length === 0 && <p className="empty-library-copy">还没有其他星盘,先添加一份资料。</p>}
|
|
{otherCharts.map(listRow)}
|
|
<button
|
|
className="button-secondary chart-library-add"
|
|
type="button"
|
|
onClick={() => {
|
|
setOtherProfileDraft(emptyProfile);
|
|
setOtherChartRelationship("other");
|
|
setEditingChartId(null);
|
|
setAccountError("");
|
|
setProfileNotice("");
|
|
setView({ kind: "add" });
|
|
}}
|
|
>
|
|
添加其他人
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|