M2c:前端镜树页(路径阅读流/候答切换/采用归真/局部树图/SSE 芽体)与灵胚页(创建/导入卡/照心试演/启镜),契约错误码段接入前端

This commit is contained in:
chenxin committed 2026-08-21 23:54:17 +08:00
1 parent 30447fc40f
commit 2a640a901b
21 files changed
+2126 -4

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { EmbryoManager } from "@/components/embryos/EmbryoManager";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { EmbryoDraftView } from "@/types/mirror-tree";
export const dynamic = "force-dynamic";
export default async function EmbryosPage() {
let embryos: EmbryoDraftView[] = [];
let error: string | null = null;
try {
embryos = await serverApi.get<EmbryoDraftView[]>("/embryo-drafts");
} catch (e) {
error = describeError(e);
}
return (
<PageContainer
title="灵胚"
description="启镜前的可审阅草稿:原创、导入或照心共塑;试演不入正式经历。"
>
{error ? (
<ErrorState message={error} />
) : (
<EmbryoManager initialEmbryos={embryos} />
)}
</PageContainer>
);
}
@@ -0,0 +1,62 @@
import { PageContainer } from "@/components/layout/PageContainer";
import { ErrorState } from "@/components/shared/StateViews";
import { TreeWorkspace } from "@/components/trees/TreeWorkspace";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type {
GraphResponse,
MirrorTreeView,
PathResponse,
} from "@/types/mirror-tree";
export const dynamic = "force-dynamic";
export default async function TreeDetailPage({
params,
}: {
params: Promise<{ treeId: string }>;
}) {
const { treeId } = await params;
let tree: MirrorTreeView | null = null;
let error: string | null = null;
try {
tree = await serverApi.get<MirrorTreeView>(`/trees/${treeId}`);
} catch (e) {
error = describeError(e);
}
if (error || !tree) {
return (
<PageContainer title="镜树">
<ErrorState message={error ?? "树不存在。"} />
</PageContainer>
);
}
const [pathData, graphData] = await Promise.all([
serverApi.get<PathResponse>(`/trees/${treeId}/path`).catch(() => null),
serverApi
.get<GraphResponse>(`/trees/${treeId}/local-graph`, { depth: 4 })
.catch(() => null),
]);
if (!pathData || !graphData) {
return (
<PageContainer title="镜树">
<ErrorState message="树数据读取失败,请刷新重试。" />
</PageContainer>
);
}
return (
<PageContainer
title={tree.title}
description="当前路径阅读 · 采用/归真分离 · 局部树图定位。"
>
<TreeWorkspace
tree={tree}
initialPath={pathData}
initialGraph={graphData}
/>
</PageContainer>
);
}
+68
View File
@@ -0,0 +1,68 @@
import Link from "next/link";
import { PageContainer } from "@/components/layout/PageContainer";
import { ErrorState } from "@/components/shared/StateViews";
import { serverApi } from "@/lib/api/server-client";
import { describeError } from "@/lib/utils/error";
import type { Paginated, Character } from "@/types/api";
import type { MirrorTreeView } from "@/types/mirror-tree";
import { TreeCreateForm } from "@/components/trees/TreeCreateForm";
export const dynamic = "force-dynamic";
export default async function TreesPage() {
let trees: MirrorTreeView[] = [];
let characterId: string | undefined;
let error: string | null = null;
try {
const active = await serverApi
.get<Character>("/characters/active")
.catch(() => null);
characterId = active?.id;
// 树列表为非分页数组端点;兼容分页包装
const data = await serverApi.get<
MirrorTreeView[] | Paginated<MirrorTreeView>
>("/trees");
trees = Array.isArray(data) ? data : data.items;
} catch (e) {
error = describeError(e);
}
return (
<PageContainer title="镜树" description="围绕主题自然生长的树状对话。">
{error ? (
<ErrorState message={error} />
) : (
<div className="space-y-6">
{characterId && <TreeCreateForm characterId={characterId} />}
{trees.length === 0 ? (
<p className="rounded-xl border border-dashed border-rule p-6 text-center text-sm text-muted">
还没有镜树。创建一棵,从第一个问题开始。
</p>
) : (
<ul className="grid gap-3 sm:grid-cols-2">
{trees.map((tree) => (
<li key={tree.id}>
<Link
href={`/trees/${tree.id}`}
className="block rounded-xl border border-rule bg-surface p-4 transition-colors hover:border-patina"
>
<h2 className="font-medium">{tree.title}</h2>
<p className="mt-1 text-xs text-muted">
{tree.transplanted_from?.source_tree_id
? "移栽自另一棵树 · "
: ""}
创建于 {new Date(tree.created_at).toLocaleString("zh-CN")}
</p>
</Link>
</li>
))}
</ul>
)}
</div>
)}
</PageContainer>
);
}