| ... | ... | @@ -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"> | 
... | ... |  |