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>
270 lines
15 KiB
TypeScript
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>
|
|
|
|
);
|
|
}
|