...
|
...
|
@@ -2,7 +2,11 @@ import chatStyles from "@/app/components/chat.module.scss"; |
|
|
import homeStyles from "@/app/components/home.module.scss";
|
|
|
|
|
|
import { MindSiderBar } from "./mind-siderBar";
|
|
|
import MindElixir, { type Options, type MindElixirData } from "mind-elixir";
|
|
|
import MindElixir, {
|
|
|
type Options,
|
|
|
type MindElixirData,
|
|
|
NodeObj,
|
|
|
} from "mind-elixir";
|
|
|
import { WindowContent } from "@/app/components/home";
|
|
|
import { useMobileScreen } from "@/app/utils";
|
|
|
import { IconButton } from "../button";
|
...
|
...
|
@@ -18,22 +22,57 @@ import ReturnIcon from "@/app/icons/return.svg"; |
|
|
import MinIcon from "@/app/icons/min.svg";
|
|
|
import MaxIcon from "@/app/icons/max.svg";
|
|
|
import SDIcon from "@/app/icons/sd.svg";
|
|
|
import BoldIcon from "@/app/icons/bold.svg";
|
|
|
import FontColorIcon from "@/app/icons/fontColor.svg";
|
|
|
import FontBgIcon from "@/app/icons/fontBg.svg";
|
|
|
import CenterIcon from "@/app/icons/center.svg";
|
|
|
import MindIcon from "@/app/icons/mind.svg";
|
|
|
import LeftIcon from "@/app/icons/leftMind.svg";
|
|
|
import RightIcon from "@/app/icons/rightMind.svg";
|
|
|
import LineIcon from "@/app/icons/line.svg";
|
|
|
import InitSideIcon from "@/app/icons/initSide.svg";
|
|
|
|
|
|
import { useChatStore, useMindMapStore } from "@/app/store";
|
|
|
import { message } from "antd";
|
|
|
|
|
|
// 常量配置抽取
|
|
|
const INITIAL_DATA: MindElixirData = {
|
|
|
nodeData: {
|
|
|
id: "root",
|
|
|
topic: "中心主题",
|
|
|
},
|
|
|
};
|
|
|
const LOADING_DATA: MindElixirData = {
|
|
|
nodeData: {
|
|
|
id: "root",
|
|
|
topic: "生成中....",
|
|
|
},
|
|
|
};
|
|
|
import {
|
|
|
message,
|
|
|
Select,
|
|
|
ColorPicker,
|
|
|
Button,
|
|
|
Space,
|
|
|
Slider,
|
|
|
Popover,
|
|
|
} from "antd";
|
|
|
import type { ColorPickerProps, GetProp } from "antd";
|
|
|
|
|
|
import {
|
|
|
INITIAL_DATA,
|
|
|
LOADING_DATA,
|
|
|
FONT_SIZE_OPTIONS,
|
|
|
EXPORT_OPTIONS,
|
|
|
useColor,
|
|
|
} from "./mind-utils";
|
|
|
|
|
|
type Color = Extract<
|
|
|
GetProp<ColorPickerProps, "value">,
|
|
|
string | { cleared: any }
|
|
|
>;
|
|
|
const ScaleControl = ({
|
|
|
value,
|
|
|
onChange,
|
|
|
}: {
|
|
|
value: number;
|
|
|
onChange: (v: number) => void;
|
|
|
}) => (
|
|
|
<Slider
|
|
|
vertical
|
|
|
value={value}
|
|
|
onChange={onChange}
|
|
|
style={{ height: "100px" }}
|
|
|
min={35}
|
|
|
max={200}
|
|
|
tooltip={{ formatter: (v) => `${v}%` }}
|
|
|
/>
|
|
|
);
|
|
|
|
|
|
export function MindPage() {
|
|
|
const isMobileScreen = useMobileScreen();
|
...
|
...
|
@@ -43,15 +82,21 @@ export function MindPage() { |
|
|
const config = useAppConfig();
|
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
|
const isMind = location.pathname === Path.Mind;
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
const mindInstance = useRef<InstanceType<typeof MindElixir> | null>(null);
|
|
|
const [isLoading, setIsLoading] = useState(false); //加载 辅助
|
|
|
const containerRef = useRef<HTMLDivElement>(null); //思维导图容器
|
|
|
const mindInstance = useRef<InstanceType<typeof MindElixir> | null>(null); //思维导图实例
|
|
|
const chatStore = useChatStore();
|
|
|
const { newMessages, content } = useMindMapStore.getState();
|
|
|
const { newMessages, content } = useMindMapStore.getState(); //聊天页面跳转时要生成的信息
|
|
|
const query = useLocation();
|
|
|
let { msg } = query.state || {};
|
|
|
const [data, setData] = useState<MindElixirData>(INITIAL_DATA);
|
|
|
const [data, setData] = useState<MindElixirData>(INITIAL_DATA); //思维导图数据
|
|
|
|
|
|
const [fontColor, setFontColor] = useColor("#1677ff"); //字体颜色
|
|
|
const [nodeBgColor, setNodeBgColor] = useColor("#1677ff"); //节点背景颜色
|
|
|
const [lineColor, setLinrColor] = useColor("#1677ff"); //分支颜色
|
|
|
const [selectedNode, setSelectedNode] = useState<NodeObj | null>(null);
|
|
|
const [scaleValue, setScaleValue] = useState(100);
|
|
|
//如果是在聊天页面跳转过来时需要发起请求
|
|
|
const fetchData = async () => {
|
|
|
if (!msg) return;
|
|
|
if (!content) return;
|
...
|
...
|
@@ -84,27 +129,73 @@ export function MindPage() { |
|
|
const options: Options = {
|
|
|
el: containerRef.current,
|
|
|
locale: "zh_CN",
|
|
|
draggable: false,
|
|
|
draggable: true,
|
|
|
contextMenu: true,
|
|
|
toolBar: true,
|
|
|
nodeMenu: true,
|
|
|
toolBar: false,
|
|
|
nodeMenu: false,
|
|
|
};
|
|
|
// 创建实例
|
|
|
mindInstance.current = new MindElixir(options);
|
|
|
mindInstance.current.init(data);
|
|
|
const el = mindInstance.current?.container.querySelector("me-root");
|
|
|
//添加鼠标移入中心主题时鼠标变手的样式
|
|
|
const style = document.createElement("style");
|
|
|
// 添加 CSS 样式
|
|
|
style.textContent = `
|
|
|
me-root:hover {
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
`;
|
|
|
// 将样式插入到文档中
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
const handleContainerClick = (e: MouseEvent) => {
|
|
|
const target = e.target as HTMLElement;
|
|
|
// 检查点击的目标是否是某个特定的元素(比如 me-root)
|
|
|
if (target.closest("me-root")) {
|
|
|
console.log("Clicked me-root element!", target);
|
|
|
// 获取 me-nodes 父元素
|
|
|
const parentElement = target.closest("me-nodes");
|
|
|
// 确保父元素存在并且是 HTMLElement 类型
|
|
|
if (parentElement && parentElement instanceof HTMLElement) {
|
|
|
// 鼠标按下时开始跟随
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
|
// 记录鼠标按下的初始位置和父元素的初始位置
|
|
|
const initialX = e.clientX;
|
|
|
const initialY = e.clientY;
|
|
|
const initialElementX = parentElement.offsetLeft;
|
|
|
const initialElementY = parentElement.offsetTop;
|
|
|
// 鼠标移动时更新父元素位置
|
|
|
const onMouseMove = (moveEvent: MouseEvent) => {
|
|
|
const deltaX = moveEvent.clientX - initialX;
|
|
|
const deltaY = moveEvent.clientY - initialY;
|
|
|
parentElement.style.position = "absolute";
|
|
|
parentElement.style.left = `${initialElementX + deltaX}px`;
|
|
|
parentElement.style.top = `${initialElementY + deltaY}px`;
|
|
|
};
|
|
|
// 鼠标松开时停止跟随
|
|
|
const onMouseUp = () => {
|
|
|
window.removeEventListener("mousemove", onMouseMove);
|
|
|
window.removeEventListener("mouseup", onMouseUp);
|
|
|
};
|
|
|
// 添加事件监听器
|
|
|
window.addEventListener("mousemove", onMouseMove);
|
|
|
window.addEventListener("mouseup", onMouseUp);
|
|
|
};
|
|
|
// 添加鼠标按下事件监听器
|
|
|
target.addEventListener("mousedown", onMouseDown);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
//注册点击事件
|
|
|
mindInstance.current.container.addEventListener(
|
|
|
"click",
|
|
|
handleContainerClick,
|
|
|
);
|
|
|
|
|
|
fetchData();
|
|
|
//设置选择的节点用于调节样式
|
|
|
mindInstance.current?.bus.addListener("selectNode", (node) => {
|
|
|
setSelectedNode(node);
|
|
|
});
|
|
|
|
|
|
return () => {
|
|
|
mindInstance.current?.container.removeEventListener(
|
...
|
...
|
@@ -117,6 +208,7 @@ export function MindPage() { |
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
//data更新思维导图更新数据
|
|
|
useEffect(() => {
|
|
|
mindInstance.current?.refresh(data);
|
|
|
}, [data]);
|
...
|
...
|
@@ -127,6 +219,168 @@ export function MindPage() { |
|
|
}
|
|
|
}, [isLoading]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (selectedNode) {
|
|
|
// 更新字体颜色
|
|
|
updateNodeStyle({ color: fontColor });
|
|
|
}
|
|
|
}, [fontColor]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (selectedNode) {
|
|
|
// 更新节点背景颜色
|
|
|
updateNodeStyle({ background: nodeBgColor });
|
|
|
}
|
|
|
}, [nodeBgColor]);
|
|
|
// 导出函数
|
|
|
const handleDownload = async (type: "png" | "svg") => {
|
|
|
try {
|
|
|
const blob =
|
|
|
type === "png"
|
|
|
? await mindInstance.current?.exportPng(false)
|
|
|
: await mindInstance.current?.exportSvg(false);
|
|
|
|
|
|
if (!blob) return;
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement("a");
|
|
|
a.href = url;
|
|
|
a.download = `${data.nodeData.topic}.${type}`;
|
|
|
a.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
} catch (error) {
|
|
|
console.error("导出失败:", error);
|
|
|
message.error("导出失败,请重试");
|
|
|
}
|
|
|
};
|
|
|
//思维导图缩放函数
|
|
|
const handleScaleChange = (value: number) => {
|
|
|
setScaleValue(value);
|
|
|
mindInstance.current?.scale(value / 100);
|
|
|
};
|
|
|
//更新样式
|
|
|
const updateNodeStyle = (style: Partial<NodeObj["style"]>) => {
|
|
|
if (selectedNode) {
|
|
|
mindInstance.current?.reshapeNode(MindElixir.E(selectedNode.id), {
|
|
|
style: { ...selectedNode.style, ...style },
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
//更新分支颜色
|
|
|
const updateBranchColor = (value: string) => {
|
|
|
if (selectedNode) {
|
|
|
mindInstance.current?.reshapeNode(MindElixir.E(selectedNode.id), {
|
|
|
branchColor: value,
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const mindType = (
|
|
|
<div>
|
|
|
<Button
|
|
|
icon={<LeftIcon />}
|
|
|
onClick={() => {
|
|
|
mindInstance.current?.initLeft();
|
|
|
}}
|
|
|
/>
|
|
|
<Button
|
|
|
icon={<RightIcon />}
|
|
|
onClick={() => {
|
|
|
mindInstance.current?.initRight();
|
|
|
}}
|
|
|
/>
|
|
|
<Button
|
|
|
icon={
|
|
|
<InitSideIcon
|
|
|
onClick={() => {
|
|
|
mindInstance.current?.initSide();
|
|
|
}}
|
|
|
/>
|
|
|
}
|
|
|
/>
|
|
|
</div>
|
|
|
);
|
|
|
//工具栏
|
|
|
const renderToolbar = () => (
|
|
|
<Space>
|
|
|
<Popover
|
|
|
content={
|
|
|
<ScaleControl value={scaleValue} onChange={handleScaleChange} />
|
|
|
}
|
|
|
>
|
|
|
<Button>{scaleValue}%</Button>
|
|
|
</Popover>
|
|
|
<Popover content={mindType}>
|
|
|
<Button icon={<MindIcon />}></Button>
|
|
|
</Popover>
|
|
|
<Select
|
|
|
options={EXPORT_OPTIONS}
|
|
|
onSelect={handleDownload}
|
|
|
placeholder="导出"
|
|
|
style={{ width: 120 }}
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
options={FONT_SIZE_OPTIONS}
|
|
|
onSelect={(v) => updateNodeStyle({ fontSize: v })}
|
|
|
placeholder="字号"
|
|
|
style={{ width: 100 }}
|
|
|
disabled={!selectedNode}
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
icon={<CenterIcon />}
|
|
|
onClick={() => mindInstance.current?.toCenter()}
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
icon={<BoldIcon />}
|
|
|
onClick={() =>
|
|
|
updateNodeStyle({
|
|
|
fontWeight:
|
|
|
selectedNode?.style?.fontWeight === "bold" ? "normal" : "bold",
|
|
|
})
|
|
|
}
|
|
|
disabled={!selectedNode}
|
|
|
/>
|
|
|
<ColorPicker
|
|
|
value={fontColor}
|
|
|
onChange={(value) => {
|
|
|
setFontColor(value || "#000000"); // 处理可能的空值
|
|
|
updateNodeStyle({ color: value?.toHexString() || "#000000" });
|
|
|
}}
|
|
|
disabled={!selectedNode}
|
|
|
showText
|
|
|
>
|
|
|
<Button icon={<FontColorIcon />} />
|
|
|
</ColorPicker>
|
|
|
|
|
|
<ColorPicker
|
|
|
value={nodeBgColor}
|
|
|
onChange={(value) => {
|
|
|
setNodeBgColor(value || "#ffffff");
|
|
|
updateNodeStyle({ background: value?.toHexString() || "#ffffff" });
|
|
|
}}
|
|
|
disabled={!selectedNode}
|
|
|
showText
|
|
|
>
|
|
|
<Button icon={<FontBgIcon />} />
|
|
|
</ColorPicker>
|
|
|
|
|
|
<ColorPicker
|
|
|
value={lineColor}
|
|
|
onChange={(value) => {
|
|
|
setLinrColor(value || "#ffffff");
|
|
|
updateBranchColor(value?.toHexString() || "#ffffff");
|
|
|
}}
|
|
|
disabled={!selectedNode}
|
|
|
showText
|
|
|
>
|
|
|
<Button icon={<LineIcon />} />
|
|
|
</ColorPicker>
|
|
|
</Space>
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
<MindSiderBar
|
...
|
...
|
@@ -160,12 +414,7 @@ export function MindPage() { |
|
|
</div>
|
|
|
<div className={chatStyles["chat-message-actions"]}>
|
|
|
<div className={chatStyles["chat-input-actions"]}>
|
|
|
{/* <ChatAction
|
|
|
text={Locale.Chat.Actions.ReWrite}
|
|
|
icon={<ReloadIcon />}
|
|
|
onClick={() => { }
|
|
|
}
|
|
|
/> */}
|
|
|
{renderToolbar()}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="window-actions">
|
...
|
...
|
|