Topic.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import { register } from "@antv/x6-react-shape";
  2. import { EventArgs, Graph, Node } from "@antv/x6";
  3. import { topicData } from "@/config/data";
  4. import { useSizeHook, useShapeProps } from "@/hooks";
  5. import { useEffect, useMemo, useRef, useState } from "react";
  6. import { PlusOutlined } from "@ant-design/icons";
  7. import { TopicType } from "@/enum";
  8. import { addTopic } from "@/pages/mindmap/mindMap";
  9. import Link from "./Link";
  10. import ExtraModule from "./ExtraModule";
  11. import CustomTag from "@/components/mindMap/CustomTag";
  12. import { TopicItem } from "@/types";
  13. import { selectTopic } from "@/utils/mindmapHander";
  14. import { Tooltip, Popover } from "antd";
  15. import LinkForm from "./LinkForm";
  16. import Text from "./Text";
  17. import FlowExtra from "../FlowExtra";
  18. const component = ({ node, graph }: { node: Node; graph: Graph }) => {
  19. const {
  20. fill,
  21. stroke,
  22. opacity,
  23. label,
  24. text,
  25. borderSize,
  26. setMindProjectInfo,
  27. icons,
  28. tags,
  29. extraModules,
  30. remark,
  31. href,
  32. children,
  33. type,
  34. collapsed,
  35. fixedWidth,
  36. linkTopicId,
  37. } = node.getData();
  38. const { size, ref } = useSizeHook();
  39. const { fillContent, strokeColor, strokeWidth } = useShapeProps(
  40. fill,
  41. size,
  42. stroke
  43. );
  44. const [selected, setSelected] = useState(false);
  45. const [showPopover, setShowPopover] = useState(false);
  46. const [popoverContent, setPopoverContent] = useState<React.ReactNode>();
  47. const handleSelect = (_args: EventArgs["node:selected"]) => {
  48. const cells = graph.getSelectedCells();
  49. setSelected(!!cells.find((item) => item.id === node.id));
  50. };
  51. const [showCollapsePoint, setShowCollapsePoint] = useState(collapsed);
  52. const extraModuleRef = useRef<HTMLDivElement>(null);
  53. const titleRef = useRef<HTMLDivElement>(null);
  54. const tagRef = useRef<HTMLDivElement>(null);
  55. const remarkRef = useRef<HTMLDivElement>(null);
  56. const padding = useMemo(() => {
  57. switch (type) {
  58. case TopicType.main:
  59. return {
  60. y: 14,
  61. x: 28,
  62. };
  63. case TopicType.branch:
  64. return {
  65. y: 8,
  66. x: 16,
  67. };
  68. default:
  69. return {
  70. y: 4,
  71. x: 6,
  72. };
  73. }
  74. }, [type]);
  75. const showHrefConfig = () => {
  76. setShowPopover(true);
  77. setPopoverContent(
  78. <LinkForm
  79. title={href?.title}
  80. value={href?.value}
  81. onCancel={() => setShowPopover(false)}
  82. onConfirm={(data) => {
  83. setShowPopover(false);
  84. node.setData({ href: data });
  85. }}
  86. />
  87. );
  88. };
  89. // @ts-ignore 绑定一个外部调用方法
  90. node.extendAttr = {
  91. showHrefConfig,
  92. };
  93. useEffect(() => {
  94. // graph.createTransformWidget(node);
  95. // graph.select(node);
  96. graph.on("node:selected", handleSelect);
  97. graph.on("node:unselected", handleSelect);
  98. return () => {
  99. graph.off("node:selected", handleSelect);
  100. graph.off("node:unselected", handleSelect);
  101. };
  102. }, []);
  103. const changeSize = () => {
  104. const { clientHeight = 0, clientWidth = 0 } = ref.current || {};
  105. if (
  106. clientHeight &&
  107. (size.width !== clientWidth || size.height !== clientHeight)
  108. ) {
  109. node.setData({
  110. width: clientWidth,
  111. height: clientHeight,
  112. });
  113. }
  114. };
  115. useEffect(() => {
  116. changeSize();
  117. }, [node.data]);
  118. const childrenCount = useMemo(() => {
  119. let count = 0;
  120. const traverse = (topics: TopicItem[]) => {
  121. topics.forEach((item) => {
  122. count++;
  123. if (item.children) {
  124. traverse(item.children);
  125. }
  126. });
  127. };
  128. traverse(children);
  129. return count;
  130. }, [children]);
  131. const handleShowRemark = () => {
  132. selectTopic(graph, node.data);
  133. // @ts-ignore
  134. graph.extendAttr.setRightToolbarActive("remark");
  135. };
  136. const handleAddBranch = () => {
  137. const data = node.getData();
  138. let topic;
  139. if (data.type === TopicType.main) {
  140. topic = addTopic(TopicType.branch, setMindProjectInfo, node);
  141. } else {
  142. topic = addTopic(TopicType.sub, setMindProjectInfo, node);
  143. }
  144. selectTopic(graph, topic);
  145. };
  146. const handleToggleCollapse = () => {
  147. node.setData({
  148. collapsed: !collapsed,
  149. });
  150. };
  151. const handleDeleteHeft = () => {
  152. node.setData(
  153. {
  154. href: undefined,
  155. },
  156. {
  157. deep: false,
  158. }
  159. );
  160. };
  161. const handleChangeTag = (tagInfo: { color?: string; title?: string }, index: number) => {
  162. node.setData({
  163. tags: tags.map((item: { color?: string; title?: string }, i: number) => {
  164. if (index === i) {
  165. return {
  166. ...item,
  167. ...tagInfo
  168. }
  169. }
  170. return item;
  171. }, {
  172. deep: false
  173. })
  174. })
  175. }
  176. const handleDeleteTag = (index: number) => {
  177. tags.splice(index, 1);
  178. node.setData({
  179. tags
  180. }, {
  181. deep: false
  182. });
  183. }
  184. return (
  185. <div>
  186. {selected && (
  187. <div
  188. style={{
  189. width: `calc(100% + 4px)`,
  190. height: `calc(100% + 4px)`,
  191. border: "1.5px solid #239edd",
  192. position: "absolute",
  193. top: -2,
  194. left: -2,
  195. }}
  196. />
  197. )}
  198. <Popover open={showPopover} content={popoverContent}>
  199. <div
  200. className="content relative text-0"
  201. ref={ref}
  202. style={{
  203. width: fixedWidth ? "100%" : "fit-content",
  204. opacity: opacity / 100,
  205. border: `${stroke.type} ${strokeWidth}px ${strokeColor}`,
  206. background: fillContent,
  207. borderRadius: borderSize,
  208. padding: `${padding.y}px ${padding.x}px`,
  209. }}
  210. onMouseOver={() => !collapsed && setShowCollapsePoint(true)}
  211. onMouseLeave={() => !collapsed && setShowCollapsePoint(false)}
  212. >
  213. <FlowExtra node={node}/>
  214. {/* 扩展模块 */}
  215. {extraModules && (
  216. <div className="extra" ref={extraModuleRef}>
  217. <ExtraModule node={node} extraModules={extraModules} />
  218. </div>
  219. )}
  220. {/* 图标、标题、链接等 */}
  221. <div className="tit flex items-center justify-center" ref={titleRef}>
  222. <div className="flex items-center text-20px">
  223. {icons?.map((icon: string) => {
  224. return (
  225. <svg key={icon} className="icon mr-6px" aria-hidden="true">
  226. <use xlinkHref={`#${icon}`}></use>
  227. </svg>
  228. );
  229. })}
  230. </div>
  231. <Text
  232. value={label}
  233. node={node}
  234. fixedWidth={fixedWidth}
  235. styles={{
  236. ...text,
  237. }}
  238. txtStyle={{
  239. position: "relative",
  240. ...(fixedWidth
  241. ? { maxWidth: `calc(100% - ${2 * padding.x}px)` }
  242. : { width: "max-content" }),
  243. }}
  244. />
  245. <div
  246. className="flex items-center color-#fff m-l-8px"
  247. ref={remarkRef}
  248. >
  249. {href && (
  250. <Link
  251. link={href}
  252. onEdit={showHrefConfig}
  253. onDelete={handleDeleteHeft}
  254. />
  255. )}
  256. {remark && (
  257. <Tooltip color="yellow" title={remark}>
  258. <i
  259. className="iconfont icon-pinglun1 cursor-pointer ml-4px"
  260. onClick={handleShowRemark}
  261. />
  262. </Tooltip>
  263. )}
  264. {linkTopicId && (
  265. <Tooltip color="yellow" title={remark}>
  266. <i
  267. className="iconfont icon-liangdianlianjie-01 cursor-pointer ml-4px"
  268. onClick={handleShowRemark}
  269. />
  270. </Tooltip>
  271. )}
  272. </div>
  273. </div>
  274. {/* 标签 */}
  275. <div className="" ref={tagRef}>
  276. {tags?.map((item: { name: string; color: string }, index: number) => {
  277. return (
  278. <CustomTag
  279. className="text-14px inline-block mr-8px"
  280. key={item.name}
  281. title={item.name}
  282. color={item.color}
  283. onChangeTag={(tag) => handleChangeTag(tag, index)}
  284. onDelete={() => handleDeleteTag(index)}
  285. hideEditBtn
  286. >
  287. {item.name}
  288. </CustomTag>
  289. );
  290. })}
  291. </div>
  292. {/* 添加主题按钮 */}
  293. {selected && !children?.length && (
  294. <div
  295. className={`
  296. absolute
  297. w-20px
  298. h-20px
  299. rounded-full
  300. right--25px
  301. top-50%
  302. translate-y-[-50%]
  303. flex
  304. justify-center
  305. items-center
  306. text-12px
  307. cursor-pointer
  308. bg-#eef0f3
  309. color-#9aa5b8
  310. hover:bg-#067bef
  311. hover:color-white`}
  312. onClick={handleAddBranch}
  313. >
  314. <PlusOutlined />
  315. </div>
  316. )}
  317. {type !== TopicType.main && children.length && (
  318. <div
  319. className="absolute right--30px top-0 w-30px h-full"
  320. onMouseOver={() => !collapsed && setShowCollapsePoint(true)}
  321. onMouseOut={() => !collapsed && setShowCollapsePoint(false)}
  322. />
  323. )}
  324. {/* 折叠按钮 */}
  325. {type !== TopicType.main && children?.length && showCollapsePoint && (
  326. <div
  327. className={`
  328. absolute
  329. rounded-full
  330. bg-white
  331. top-50%
  332. translate-y-[-50%]
  333. cursor-pointer
  334. hover:bg-#e6e6e6
  335. text-12px
  336. flex
  337. items-center
  338. justify-center
  339. ${collapsed ? "w-16px h-16px right--20px" : "w-10px h-10px right--15px"}
  340. `}
  341. onClick={handleToggleCollapse}
  342. style={{
  343. border: `1px solid ${fill.color1}`,
  344. color: fill.color1,
  345. }}
  346. >
  347. {collapsed && childrenCount}
  348. </div>
  349. )}
  350. </div>
  351. </Popover>
  352. </div>
  353. );
  354. };
  355. // 主题节点
  356. register({
  357. shape: "mind-map-topic",
  358. width: 206,
  359. height: 70,
  360. effect: ["data"],
  361. component: component,
  362. });
  363. const baseNode = {
  364. shape: "mind-map-topic",
  365. data: {
  366. label: "",
  367. ...topicData,
  368. },
  369. };
  370. export default baseNode;