forked from zhangyonghao/minimap
first commit
This commit is contained in:
220
frontend/app/mindmap/[id]/page.tsx
Normal file
220
frontend/app/mindmap/[id]/page.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
} from "react";
|
||||
import MindmapCanvas from "@/components/MindmapCanvas";
|
||||
import ChatPanel from "@/components/ChatPanel";
|
||||
import { getMindmap } from "@/lib/api";
|
||||
import type { Mindmap } from "@/types/mindmap";
|
||||
|
||||
type TriggerRequest = {
|
||||
id: number;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const MIN_CHAT_PANEL_WIDTH = 320;
|
||||
const DEFAULT_CHAT_PANEL_WIDTH = 420;
|
||||
const MAX_CHAT_PANEL_WIDTH_RATIO = 0.55;
|
||||
|
||||
export default function MindmapDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const uniqueId = params?.id ?? "";
|
||||
const containerRef = useRef<HTMLElement | null>(null);
|
||||
const [mindmap, setMindmap] = useState<Mindmap | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [isResizingChatPanel, setIsResizingChatPanel] = useState(false);
|
||||
const [chatPanelWidth, setChatPanelWidth] = useState(DEFAULT_CHAT_PANEL_WIDTH);
|
||||
const [triggerRequest, setTriggerRequest] = useState<TriggerRequest | undefined>();
|
||||
|
||||
const clampChatPanelWidth = useCallback((nextWidth: number) => {
|
||||
const containerWidth =
|
||||
containerRef.current?.clientWidth ??
|
||||
(typeof window === "undefined" ? DEFAULT_CHAT_PANEL_WIDTH : window.innerWidth);
|
||||
const maxWidth = Math.max(
|
||||
MIN_CHAT_PANEL_WIDTH,
|
||||
Math.floor(containerWidth * MAX_CHAT_PANEL_WIDTH_RATIO),
|
||||
);
|
||||
|
||||
return Math.min(Math.max(nextWidth, MIN_CHAT_PANEL_WIDTH), maxWidth);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadPageData() {
|
||||
if (!uniqueId) {
|
||||
setLoading(false);
|
||||
setError("缺少脑图 ID");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const mindmapData = await getMindmap(uniqueId);
|
||||
setMindmap(mindmapData);
|
||||
} catch (pageError) {
|
||||
const message =
|
||||
pageError instanceof Error ? pageError.message : "加载脑图失败";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void loadPageData();
|
||||
}, [uniqueId]);
|
||||
|
||||
const handleNodeChat = useCallback(
|
||||
(nodeLabel: string) => {
|
||||
setShowChat(true);
|
||||
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
||||
setTriggerRequest({
|
||||
id: Date.now() + Math.floor(Math.random() * 1000),
|
||||
content: `帮我解释一下 ${nodeLabel}`,
|
||||
});
|
||||
},
|
||||
[clampChatPanelWidth],
|
||||
);
|
||||
|
||||
const handleCloseChat = useCallback(() => {
|
||||
setIsResizingChatPanel(false);
|
||||
setShowChat(false);
|
||||
setTriggerRequest(undefined);
|
||||
}, []);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
||||
setIsResizingChatPanel(true);
|
||||
},
|
||||
[clampChatPanelWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showChat) {
|
||||
setIsResizingChatPanel(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleWindowResize = () => {
|
||||
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
||||
};
|
||||
|
||||
handleWindowResize();
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => window.removeEventListener("resize", handleWindowResize);
|
||||
}, [clampChatPanelWidth, showChat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showChat || !isResizingChatPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = containerRef.current.getBoundingClientRect();
|
||||
const nextWidth = bounds.right - event.clientX;
|
||||
setChatPanelWidth(clampChatPanelWidth(nextWidth));
|
||||
};
|
||||
|
||||
const stopResizing = () => {
|
||||
setIsResizingChatPanel(false);
|
||||
};
|
||||
|
||||
const { style } = document.body;
|
||||
const previousCursor = style.cursor;
|
||||
const previousUserSelect = style.userSelect;
|
||||
|
||||
style.cursor = "col-resize";
|
||||
style.userSelect = "none";
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
window.addEventListener("blur", stopResizing);
|
||||
|
||||
return () => {
|
||||
style.cursor = previousCursor;
|
||||
style.userSelect = previousUserSelect;
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
window.removeEventListener("blur", stopResizing);
|
||||
};
|
||||
}, [clampChatPanelWidth, isResizingChatPanel, showChat]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="flex h-screen w-screen items-center justify-center bg-white">
|
||||
<div className="rounded-3xl bg-white px-8 py-6 shadow-panel">
|
||||
正在加载思维导图...
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !mindmap) {
|
||||
return (
|
||||
<main className="flex h-screen w-screen flex-col items-center justify-center bg-white px-6 text-center">
|
||||
<div className="rounded-3xl bg-white px-8 py-8 shadow-panel">
|
||||
<p className="text-lg font-semibold text-rose-600">
|
||||
{error || "未找到对应脑图"}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main ref={containerRef} className="flex h-screen w-screen overflow-hidden bg-white">
|
||||
<section className="min-w-0 flex-1">
|
||||
<MindmapCanvas tree={mindmap.tree} onNodeChat={handleNodeChat} />
|
||||
</section>
|
||||
{showChat && (
|
||||
<>
|
||||
<div
|
||||
role="separator"
|
||||
aria-label="调整聊天区域宽度"
|
||||
aria-orientation="vertical"
|
||||
onMouseDown={handleResizeStart}
|
||||
className="group relative w-3 shrink-0 cursor-col-resize bg-transparent"
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-y-0 left-1/2 w-px -translate-x-1/2 transition-colors ${
|
||||
isResizingChatPanel
|
||||
? "bg-blue-500"
|
||||
: "bg-slate-200 group-hover:bg-blue-300"
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute left-1/2 top-1/2 h-16 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full transition-colors ${
|
||||
isResizingChatPanel
|
||||
? "bg-blue-500"
|
||||
: "bg-slate-300 group-hover:bg-blue-400"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<aside
|
||||
style={{ width: `${chatPanelWidth}px` }}
|
||||
className="min-w-0 shrink-0 overflow-hidden bg-slate-50 shadow-[-18px_0_36px_rgba(15,23,42,0.06)]"
|
||||
>
|
||||
<ChatPanel
|
||||
sessionId={mindmap.session_id}
|
||||
triggerRequest={triggerRequest}
|
||||
onClose={handleCloseChat}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user