Store calculation history on server with bazi input and chart snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-13 09:57:16 +08:00
parent fcf071cfaa
commit 123a5cce6d
17 changed files with 419 additions and 54 deletions
+72 -11
View File
@@ -6,6 +6,7 @@ import { Download, Trash2 } from "lucide-react";
import PageShell from "@/components/page-shell";
import { Button } from "@/components/ui/button";
import { ZenCard } from "@/components/ui/zen-card";
import BaziChartDisplay from "@/components/modes/bazi-chart";
import Markdown from "react-markdown";
import {
buildHistoryMarkdown,
@@ -18,21 +19,46 @@ import { MODE_LABELS, type CalcHistoryEntry } from "@/lib/history/types";
export default function HistoryPageClient() {
const [items, setItems] = useState<CalcHistoryEntry[]>([]);
const [activeId, setActiveId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
const list = loadHistory();
setItems(list);
setActiveId(list[0]?.id ?? null);
let cancelled = false;
(async () => {
try {
const list = await loadHistory();
if (cancelled) {
return;
}
setItems(list);
setActiveId(list[0]?.id ?? null);
} catch (e) {
if (!cancelled) {
setError(e instanceof Error ? e.message : String(e));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, []);
const active = items.find((e) => e.id === activeId) ?? null;
function handleDelete(id: string) {
deleteHistoryEntry(id);
const next = loadHistory();
setItems(next);
if (activeId === id) {
setActiveId(next[0]?.id ?? null);
async function handleDelete(id: string) {
try {
await deleteHistoryEntry(id);
const next = items.filter((item) => item.id !== id);
setItems(next);
if (activeId === id) {
setActiveId(next[0]?.id ?? null);
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}
@@ -41,11 +67,17 @@ export default function HistoryPageClient() {
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold tracking-wide"></h1>
<p className="mt-2 text-sm text-muted-foreground">
</p>
</div>
{items.length === 0 ? (
{loading ? (
<ZenCard className="text-center text-sm text-muted-foreground">
</ZenCard>
) : error ? (
<ZenCard className="text-center text-sm text-destructive">{error}</ZenCard>
) : items.length === 0 ? (
<ZenCard className="text-center text-sm text-muted-foreground">
<p></p>
<Link href="/liuyao" className="mt-3 inline-block text-primary underline">
@@ -70,6 +102,14 @@ export default function HistoryPageClient() {
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{item.question}
</p>
{item.baziChart && (
<p className="mt-1 font-mono text-xs text-muted-foreground">
{item.baziChart.pillars.year.ganZhi}{" "}
{item.baziChart.pillars.month.ganZhi}{" "}
{item.baziChart.pillars.day.ganZhi}{" "}
{item.baziChart.pillars.time.ganZhi}
</p>
)}
<p className="mt-1 text-xs text-muted-foreground">
{MODE_LABELS[item.mode]} ·{" "}
{new Date(item.createdAt).toLocaleString("zh-CN")}
@@ -87,6 +127,27 @@ export default function HistoryPageClient() {
{active.summary && (
<p className="text-sm text-muted-foreground">{active.summary}</p>
)}
{active.baziInput && (
<div className="rounded-md border bg-background/50 p-3 text-sm text-muted-foreground">
<p>
{active.baziInput.birthPlaceName} ·{" "}
{active.baziInput.date}{" "}
{active.baziInput.unknownHour
? "时辰不详"
: active.baziInput.time}{" "}
· {active.baziInput.gender === "male" ? "男" : "女"}
</p>
</div>
)}
{active.baziChart && <BaziChartDisplay chart={active.baziChart} />}
{active.hexagram && (
<div className="rounded-md border bg-background/50 p-3 text-sm">
<p className="font-medium">{active.hexagram.guaTitle}</p>
<p className="mt-1 text-muted-foreground">
{active.hexagram.guaResult}
</p>
</div>
)}
<div className="flex flex-wrap gap-2">
<Button
size="sm"
+13 -2
View File
@@ -92,16 +92,27 @@ export default function BaziForm() {
},
setCompletion,
);
saveHistoryEntry({
await saveHistoryEntry({
mode: "bazi",
title: "生辰八字解读",
question,
summary: activeChart.lunarDate,
completion: text,
baziInput: {
date: input.date,
time: input.time,
gender: input.gender,
longitude: input.longitude,
unknownHour: !!input.unknownHour,
birthPlaceName: location!.name,
},
baziChart: activeChart,
meta: {
出生地域: location!.name,
: `${input.date} ${input.time}`,
: `${input.date} ${unknownHour ? "时辰不详" : input.time}`,
农历: activeChart.lunarDate,
性别: gender === "male" ? "男" : "女",
: `${activeChart.pillars.year.ganZhi} ${activeChart.pillars.month.ganZhi} ${activeChart.pillars.day.ganZhi} ${activeChart.pillars.time.ganZhi}`,
},
});
} catch (err) {
+22 -1
View File
@@ -155,16 +155,37 @@ export default function CombinedForm() {
},
setCompletion,
);
saveHistoryEntry({
await saveHistoryEntry({
mode: "combined",
title: "综合测算解读",
question,
summary: activeChart.lunarDate,
completion: text,
baziInput: {
date: birthDate,
time: unknownHour ? "12:00" : birthTime,
gender,
longitude: birthLocation!.longitude,
unknownHour,
birthPlaceName: birthLocation!.name,
},
baziChart: activeChart,
hexagram:
withHexagram && guaData
? {
guaMark: guaData.result.guaMark,
guaTitle: guaData.result.guaTitle,
guaResult: guaData.result.guaResult,
guaChange: guaData.result.guaChange,
}
: undefined,
meta: {
出生地域: birthLocation!.name,
当前地域: currentLocation!.name,
: `${calcDate} ${calcTime}`,
农历: activeChart.lunarDate,
性别: gender === "male" ? "男" : "女",
: `${activeChart.pillars.year.ganZhi} ${activeChart.pillars.month.ganZhi} ${activeChart.pillars.day.ganZhi} ${activeChart.pillars.time.ganZhi}`,
六爻: withHexagram && guaData ? guaData.result.guaTitle : "无",
},
});
+7 -1
View File
@@ -86,12 +86,18 @@ export default function LiuyaoForm() {
},
setCompletion,
);
saveHistoryEntry({
await saveHistoryEntry({
mode: "liuyao",
title: guaData!.result.guaTitle,
question,
summary: guaData!.result.guaResult,
completion: text,
hexagram: {
guaMark: guaData!.result.guaMark,
guaTitle: guaData!.result.guaTitle,
guaResult: guaData!.result.guaResult,
guaChange: guaData!.result.guaChange,
},
meta: {
地域: location!.name,
: `${calcDate} ${calcTime}`,