Files
Jyotisha/frontend/src/components/chart-library-panel.tsx
T
Jesse_ChenandCursor 54269fcfcc
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now
live in their own modules so later splits can land without editing the 4k-line
Home. Source-lock tests follow the moved tokens; the orphan user-data contract
is aligned and added to the quick gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:58:02 +08:00

270 lines
15 KiB
TypeScript

"use client";
import type { Dispatch, FormEvent, SetStateAction } from "react";
import { ProfileFields } from "@/components/profile-fields";
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";
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 editingSelfChart: boolean;
readonly setEditingSelfChart: Dispatch<SetStateAction<boolean>>;
readonly otherProfileDraft: Profile;
readonly setOtherProfileDraft: Dispatch<SetStateAction<Profile>>;
readonly otherChartRelationship: Exclude<ChartRelationship, "self">;
readonly setOtherChartRelationship: Dispatch<SetStateAction<Exclude<ChartRelationship, "self">>>;
readonly editingChartId: string | null;
readonly setEditingChartId: Dispatch<SetStateAction<string | null>>;
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,
editingSelfChart,
setEditingSelfChart,
otherProfileDraft,
setOtherProfileDraft,
otherChartRelationship,
setOtherChartRelationship,
editingChartId,
setEditingChartId,
synastryRelationshipType,
setSynastryRelationshipType,
synastryPendingId,
synastryReportCard,
setSynastryReportCard,
synastryHistory,
activeChartId,
setActiveChartId,
saveProfile,
draftSynastryQuestionFromChart,
}: ChartLibraryPanelProps) {
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 ? "已更新其他人的星盘资料。" : "已保存到云端星盘库。请选择关系类型后点击“用于合盘”。");
} 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("");
}
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("已从云端星盘库删除。");
}
function makeDefaultChart(record: ChartLibraryRecord) {
if (record.role !== "other" || profileSaving || !accountId) return;
setActiveChartId(record.id);
localStorage.setItem(activeChartStorageKey(accountId), record.id);
setProfile(record.profile);
setProfileNotice("已设为当前使用资料,账户本人的出生资料未被覆盖。");
}
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">{chartLibrary.filter((record) => record.role === "self").length}</span>
</div>
{chartLibrary.filter((record) => record.role === "self").map((record) => (
<article className="chart-library-item" key={record.id}>
<div className="chart-library-item-main">
<div className="chart-library-item-title"><strong>{record.profile.name || "未命名"}</strong><span className="chart-role-badge is-self">本人</span><span className="chart-default-badge">当前默认</span></div>
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
</div>
<div className="chart-library-actions">
<button className="button-secondary" type="button" onClick={() => { setProfileDraft(record.profile); setEditingSelfChart(true); }} disabled={profileSaving || editingChartId !== null}>编辑本人资料</button>
</div>
</article>
))}
{editingSelfChart && (
<form className="profile-form chart-library-form" onSubmit={saveProfile}>
<div className="section-heading"><b>编辑本人星盘</b><small>这份资料会用于默认解盘与新对话。</small></div>
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="self-profile-name" />
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
<div className="dialog-actions">
<button className="button-secondary" type="button" onClick={() => { setEditingSelfChart(false); setProfileDraft(profile); }}>取消编辑</button>
<button className="button-primary save-profile" type="submit" disabled={!account || profileSaving}>{profileSaving ? "保存中" : "保存本人资料"}</button>
</div>
</form>
)}
{chartLibrary.filter((record) => record.role === "self").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">{chartLibrary.filter((record) => record.role === "other").length}</span>
</div>
{chartLibrary.filter((record) => record.role === "other").length === 0 && <p className="empty-library-copy">还没有其他星盘,先添加一份资料。</p>}
{chartLibrary.filter((record) => record.role === "other").map((record) => (
<article className="chart-library-item" key={record.id}>
<div className="chart-library-item-main">
<div className="chart-library-item-title"><strong>{record.profile.name || "未命名"}</strong><span className="chart-role-badge">{chartRelationshipLabel(record.relationship)}</span></div>
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
</div>
<div className="chart-library-actions">
<select aria-label={`${record.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(record, synastryRelationshipType)} disabled={synastryPendingId !== null}>{synastryPendingId === record.id ? "正在计算合盘..." : "用于合盘"}</button>
<button className="button-secondary" type="button" onClick={() => editOtherChart(record)} disabled={profileSaving || editingChartId !== null}>编辑</button>
<button className="button-secondary" type="button" onClick={() => makeDefaultChart(record)} disabled={profileSaving}>设为默认</button>
<button className="button-secondary" type="button" onClick={() => void deleteOtherChart(record.id)}>删除</button>
</div>
</article>
))}
</div>
{synastryReportCard && (
<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>
)}
{synastryHistory.length > 0 && (
<div className="synastry-history-list" aria-label="合盘历史">
<b>合盘历史</b>
{synastryHistory.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>
)}
<form className="profile-form chart-library-form" onSubmit={saveOtherChart}>
<div className="section-heading"><b>{editingChartId ? "编辑其他人的星盘" : "添加其他人的星盘"}</b><small>用于合盘、亲友盘或客户盘。</small></div>
<label>
<span>关系</span>
<select value={otherChartRelationship} onChange={(event) => setOtherChartRelationship(event.target.value as Exclude<ChartRelationship, "self">)}>
<option value="partner">伴侣</option>
<option value="family">家人</option>
<option value="friend">朋友</option>
<option value="client">客户</option>
<option value="other">其他</option>
</select>
</label>
<ProfileFields value={otherProfileDraft} onChange={setOtherProfileDraft} nameInputId="other-profile-name" />
<div className="dialog-actions">
{editingChartId && <button className="button-secondary" type="button" onClick={() => { setEditingChartId(null); setOtherProfileDraft(emptyProfile); setOtherChartRelationship("other"); }}>取消编辑</button>}
<button className="button-primary save-profile" type="submit" disabled={!account || profileSaving}>{profileSaving ? "保存中" : editingChartId ? "保存修改" : "添加到星盘库"}</button>
</div>
</form>
</div>
);
}