123a5cce6d
Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
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,
|
||
deleteHistoryEntry,
|
||
downloadMarkdown,
|
||
loadHistory,
|
||
} from "@/lib/history/storage";
|
||
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(() => {
|
||
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;
|
||
|
||
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));
|
||
}
|
||
}
|
||
|
||
return (
|
||
<PageShell className="py-8">
|
||
<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>
|
||
|
||
{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">
|
||
去六爻算卦
|
||
</Link>
|
||
</ZenCard>
|
||
) : (
|
||
<div className="grid gap-6 lg:grid-cols-[minmax(0,280px)_1fr]">
|
||
<div className="space-y-2">
|
||
{items.map((item) => (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
onClick={() => setActiveId(item.id)}
|
||
className={`w-full rounded-xl border px-4 py-3 text-left text-sm transition ${
|
||
activeId === item.id
|
||
? "border-primary/40 bg-primary/5"
|
||
: "border-border/60 bg-card/80 hover:border-primary/20"
|
||
}`}
|
||
>
|
||
<p className="font-medium">{item.title}</p>
|
||
<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")}
|
||
</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{active && (
|
||
<ZenCard
|
||
title={active.title}
|
||
subtitle={`${MODE_LABELS[active.mode]} · ${new Date(active.createdAt).toLocaleString("zh-CN")}`}
|
||
>
|
||
<p className="text-sm text-muted-foreground">问事:{active.question}</p>
|
||
{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"
|
||
variant="outline"
|
||
onClick={() =>
|
||
downloadMarkdown(
|
||
buildHistoryMarkdown(active),
|
||
`${active.title}.md`,
|
||
)
|
||
}
|
||
>
|
||
<Download size={14} className="mr-1" />
|
||
下载 Markdown
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => handleDelete(active.id)}
|
||
>
|
||
<Trash2 size={14} className="mr-1" />
|
||
删除
|
||
</Button>
|
||
</div>
|
||
<div className="max-h-[60vh] overflow-y-auto rounded-md border bg-background/50 p-4">
|
||
<Markdown className="prose max-w-none text-sm dark:prose-invert">
|
||
{active.completion}
|
||
</Markdown>
|
||
</div>
|
||
</ZenCard>
|
||
)}
|
||
</div>
|
||
)}
|
||
</PageShell>
|
||
);
|
||
}
|