index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. import React, { useEffect, useMemo, useRef, useState } from "react";
  2. import styles from "./index.less";
  3. import {
  4. Button,
  5. Divider,
  6. Dropdown,
  7. Flex,
  8. InputNumber,
  9. Select,
  10. Tooltip,
  11. } from "antd";
  12. import {
  13. AlignCenterOutlined,
  14. BoldOutlined,
  15. CaretDownOutlined,
  16. ColumnHeightOutlined,
  17. ItalicOutlined,
  18. SwapOutlined,
  19. UnderlineOutlined,
  20. } from "@ant-design/icons";
  21. import { fontFamilyOptions, alignOptionData, textAlignList } from "../../data";
  22. import { useModel } from "umi";
  23. import {
  24. alignCell,
  25. matchSize,
  26. setCellZIndex,
  27. handleSetEdgeStyle,
  28. } from "@/utils";
  29. import CustomColorPicker from "@/components/CustomColorPicker";
  30. import { ConnectorType } from "@/enum";
  31. import { set, cloneDeep } from "lodash-es";
  32. import FindReplaceModal from "@/components/FindReplaceModal";
  33. import { useFindReplace } from "@/hooks/useFindReplace";
  34. export default function ToolBar() {
  35. const {
  36. showRightPanel,
  37. toggleRightPanel,
  38. toggleFormatBrush,
  39. enableFormatBrush,
  40. pageState,
  41. } = useModel("appModel");
  42. const { canRedo, canUndo, onRedo, onUndo, selectedCell, graph } =
  43. useModel("graphModel");
  44. const [formModel, setFormModel] = useState<any>({
  45. fontFamily: "微软雅黑",
  46. fontSize: 14,
  47. bold: false,
  48. italic: false,
  49. underline: false,
  50. color: "#ffffff",
  51. lineHeight: 1.25,
  52. fillColor: "#ffffff",
  53. strokeColor: "#ffffff",
  54. strokeWidth: 1.5,
  55. strokeDasharray: "solid",
  56. startArrow: "",
  57. endArrow: "",
  58. connectorType: ConnectorType.Normal,
  59. });
  60. const {
  61. handleFind,
  62. handleReplace,
  63. handleReplaceAll,
  64. handleClose,
  65. handleFindNext,
  66. handleFindPrev,
  67. findCount,
  68. currentIndex,
  69. setInstance
  70. } = useFindReplace(graph);
  71. useEffect(() => {
  72. graph && setInstance(graph);
  73. }, [graph])
  74. const findModalRef = useRef<any>();
  75. const hasNode = useMemo(() => {
  76. return selectedCell?.find((cell) => cell.isNode());
  77. }, [selectedCell]);
  78. const hasEdge = useMemo(() => {
  79. return selectedCell?.find((cell) => cell.isEdge());
  80. }, [selectedCell]);
  81. const countInfo = useMemo(() => {
  82. let nodeCount = 0;
  83. selectedCell?.forEach((cell) => {
  84. if (cell.isNode()) nodeCount++;
  85. });
  86. // 多个节点
  87. return {
  88. isMulti: nodeCount > 1,
  89. nodeCount,
  90. };
  91. }, [selectedCell]);
  92. useEffect(() => {
  93. const firstNode = selectedCell?.find((item) => item.isNode());
  94. const firstEdge = selectedCell?.find((item) => item.isEdge());
  95. const nodeData = firstNode?.getData();
  96. const edgeData = firstEdge?.getData();
  97. const model = {
  98. fontFamily: nodeData?.text?.fontFamily || "微软雅黑",
  99. fontSize: nodeData?.text?.fontSize || 14,
  100. bold: nodeData?.text?.bold || false,
  101. italic: nodeData?.text?.italic || false,
  102. underline: nodeData?.text?.textDecoration === "underline",
  103. color: nodeData?.text?.color || "#ffffff",
  104. lineHeight: nodeData?.text?.lineHeight || 1.25,
  105. fillColor: nodeData?.fill?.color1 || "#ffffff",
  106. strokeColor:
  107. nodeData?.stroke?.color || edgeData?.stroke?.color || "#ffffff",
  108. strokeWidth: nodeData?.stroke?.width || edgeData?.stroke?.color || 1.5,
  109. strokeDasharray:
  110. nodeData?.stroke?.type || nodeData?.stroke?.type || "solid",
  111. startArrow: edgeData?.startArrow || "",
  112. endArrow: edgeData?.endArrow || "",
  113. connectorType: edgeData?.connectorType ?? ConnectorType.Normal,
  114. };
  115. setFormModel(model);
  116. }, [selectedCell]);
  117. // 执行修改动作
  118. const handleChange = (
  119. target: "node" | "edge" | "all",
  120. key: string,
  121. path: string,
  122. value: any
  123. ) => {
  124. setFormModel((state: any) => {
  125. const formData = {
  126. ...state,
  127. [key]: value,
  128. };
  129. selectedCell?.forEach((cell) => {
  130. const data = cloneDeep(cell.getData());
  131. set(data, path, value);
  132. if ((cell.isNode() && target === "node") || target === "all") {
  133. cell.setData(data);
  134. }
  135. if ((cell.isEdge() && target === "edge") || target === "all") {
  136. cell.setData(data);
  137. handleSetEdgeStyle(cell, formData.connectorType, pageState.jumpover);
  138. }
  139. });
  140. return formData;
  141. });
  142. };
  143. // 移动层级
  144. const handleSetIndex = (type: "top" | "bottom" | "up" | "down") => {
  145. setCellZIndex(type, selectedCell);
  146. };
  147. return (
  148. <div className={styles.toolBar}>
  149. <div className="flex justify-between items-center">
  150. <div className="h-40px flex items-center">
  151. <Tooltip placement="bottom" title="撤销">
  152. <Button
  153. type="text"
  154. icon={<i className="iconfont icon-undo"></i>}
  155. disabled={!canUndo}
  156. onClick={onUndo}
  157. />
  158. </Tooltip>
  159. <Tooltip placement="bottom" title="恢复">
  160. <Button
  161. type="text"
  162. icon={<i className="iconfont icon-redo"></i>}
  163. disabled={!canRedo}
  164. onClick={onRedo}
  165. />
  166. </Tooltip>
  167. <Tooltip
  168. placement="bottom"
  169. title={`格式刷${enableFormatBrush ? "生效中按ESC取消" : "(Ctrl+Shift+C)"}`}
  170. >
  171. <Button
  172. type="text"
  173. icon={<i className="iconfont icon-geshishua"></i>}
  174. disabled={!selectedCell?.length}
  175. className={
  176. enableFormatBrush && selectedCell?.length ? "active" : ""
  177. }
  178. onClick={() => graph && toggleFormatBrush(graph)}
  179. />
  180. </Tooltip>
  181. {/* <Tooltip placement="bottom" title="美化">
  182. <Button
  183. type="text"
  184. icon={<i className="iconfont icon-mofabang"></i>}
  185. />
  186. </Tooltip> */}
  187. <Divider type="vertical" />
  188. <Tooltip placement="bottom" title="字体">
  189. <Select
  190. className="w-100px"
  191. popupMatchSelectWidth={200}
  192. variant="borderless"
  193. suffixIcon={<CaretDownOutlined />}
  194. disabled={!selectedCell?.length}
  195. options={fontFamilyOptions}
  196. labelRender={(item) => item.value}
  197. value={formModel.fontFamily}
  198. onChange={(value) =>
  199. handleChange("all", "fontFamily", "text.fontFamily", value)
  200. }
  201. />
  202. </Tooltip>
  203. <Tooltip placement="bottom" title="字体大小">
  204. <InputNumber
  205. className="w-70px"
  206. variant="filled"
  207. min={12}
  208. max={30}
  209. disabled={!selectedCell.length}
  210. formatter={(value) => `${value}px`}
  211. value={formModel.fontSize}
  212. onChange={(value) =>
  213. handleChange("all", "fontSize", "text.fontSize", value)
  214. }
  215. />
  216. </Tooltip>
  217. <Tooltip placement="bottom" title="字体加粗">
  218. <Button
  219. type="text"
  220. icon={<BoldOutlined />}
  221. disabled={!selectedCell?.length}
  222. className={formModel.bold ? "active" : ""}
  223. onClick={() =>
  224. handleChange("all", "bold", "text.bold", !formModel.bold)
  225. }
  226. />
  227. </Tooltip>
  228. <Tooltip placement="bottom" title="字体倾斜">
  229. <Button
  230. type="text"
  231. icon={<ItalicOutlined />}
  232. disabled={!selectedCell?.length}
  233. className={formModel.italic ? "active" : ""}
  234. onClick={() =>
  235. handleChange("all", "italic", "text.italic", !formModel.italic)
  236. }
  237. />
  238. </Tooltip>
  239. <Tooltip placement="bottom" title="下划线">
  240. <Button
  241. type="text"
  242. icon={<UnderlineOutlined />}
  243. disabled={!selectedCell?.length}
  244. className={formModel.underline ? "active" : ""}
  245. onClick={() =>
  246. handleChange(
  247. "all",
  248. "underline",
  249. "text.textDecoration",
  250. !formModel.underline ? "underline" : "none"
  251. )
  252. }
  253. />
  254. </Tooltip>
  255. <CustomColorPicker
  256. color={formModel.color}
  257. onChange={(color) =>
  258. handleChange("all", "color", "text.color", color)
  259. }
  260. >
  261. <Tooltip placement="bottom" title="字体颜色">
  262. <Button
  263. type="text"
  264. icon={
  265. <div className="flex flex-col">
  266. <span className="iconfont icon-A text-13px"></span>
  267. <span
  268. className="w-16px h-2px border-1px border-#ff0"
  269. style={{ background: formModel.color }}
  270. ></span>
  271. </div>
  272. }
  273. disabled={!selectedCell?.length}
  274. />
  275. </Tooltip>
  276. </CustomColorPicker>
  277. <Tooltip placement="bottom" title="文本行高">
  278. <Select
  279. className="flex-1"
  280. variant="borderless"
  281. suffixIcon={<CaretDownOutlined className="text-12px" />}
  282. labelRender={() => <ColumnHeightOutlined />}
  283. popupMatchSelectWidth={100}
  284. options={[
  285. { label: "1.0", value: 1 },
  286. { label: "1.25", value: 1.25 },
  287. { label: "1.5", value: 1.5 },
  288. { label: "2.0", value: 2 },
  289. { label: "2.5", value: 2.5 },
  290. { label: "3.0", value: 3 },
  291. ]}
  292. disabled={!selectedCell?.length}
  293. value={formModel.lineHeight}
  294. onChange={(value) =>
  295. handleChange("all", "lineHeight", "text.lineHeight", value)
  296. }
  297. />
  298. </Tooltip>
  299. <Dropdown
  300. menu={{
  301. items: textAlignList.map((item) => {
  302. return {
  303. key: item.id,
  304. label: (
  305. <div className="w-120px">
  306. {item.icon}
  307. <span className="ml-8px">{item.name}</span>
  308. </div>
  309. ),
  310. onClick: () =>
  311. handleChange(
  312. "all",
  313. item.target,
  314. `text.${item.target}`,
  315. item.id
  316. ),
  317. };
  318. }),
  319. }}
  320. >
  321. <Tooltip placement="bottom" title="文本对齐">
  322. <Button type="text" className="w-50px" disabled={!hasNode}>
  323. <AlignCenterOutlined />
  324. <CaretDownOutlined className="text-12px" />
  325. </Button>
  326. </Tooltip>
  327. </Dropdown>
  328. <Divider type="vertical" />
  329. <CustomColorPicker
  330. color={formModel.fillColor}
  331. onChange={(color) =>
  332. handleChange("node", "fillColor", "fill.color1", color)
  333. }
  334. >
  335. <Tooltip placement="bottom" title="颜色填充">
  336. <Button
  337. type="text"
  338. disabled={!selectedCell?.length}
  339. icon={
  340. <div className="flex flex-col">
  341. <span className="iconfont icon-paint-bucket text-13px"></span>
  342. <span
  343. className="w-16px h-2px border-1px border-#ff0"
  344. style={{ background: formModel.fillColor }}
  345. ></span>
  346. </div>
  347. }
  348. />
  349. </Tooltip>
  350. </CustomColorPicker>
  351. <CustomColorPicker
  352. color={formModel.strokeColor}
  353. onChange={(color) =>
  354. handleChange("node", "strokeColor", "stroke.color", color)
  355. }
  356. >
  357. <Tooltip placement="bottom" title="连线颜色">
  358. <Button
  359. type="text"
  360. disabled={!selectedCell?.length}
  361. icon={
  362. <div className="flex flex-col">
  363. <span className="iconfont icon-bi text-13px"></span>
  364. <span
  365. className="w-16px h-2px border-1px border-#ff0"
  366. style={{ background: formModel.strokeColor }}
  367. ></span>
  368. </div>
  369. }
  370. />
  371. </Tooltip>
  372. </CustomColorPicker>
  373. <Tooltip placement="bottom" title="连线宽度">
  374. <Select
  375. className="flex-1"
  376. variant="borderless"
  377. suffixIcon={<CaretDownOutlined className="text-12px" />}
  378. labelRender={() => <i className="iconfont icon-xiankuan"></i>}
  379. popupMatchSelectWidth={100}
  380. options={[
  381. { label: "0px", value: 0 },
  382. { label: "0.5px", value: 0.5 },
  383. { label: "1px", value: 1 },
  384. { label: "1.5", value: 1.5 },
  385. { label: "2px", value: 2 },
  386. { label: "3px", value: 3 },
  387. { label: "4px", value: 4 },
  388. { label: "5px", value: 5 },
  389. { label: "6px", value: 6 },
  390. { label: "8px", value: 7 },
  391. { label: "10px", value: 10 },
  392. ]}
  393. disabled={!selectedCell?.length}
  394. value={formModel.strokeWidth}
  395. onChange={(value) =>
  396. handleChange("all", "strokeWidth", "stroke.width", value)
  397. }
  398. />
  399. </Tooltip>
  400. <Tooltip placement="bottom" title="线段样式">
  401. <Select
  402. className="flex-1"
  403. variant="borderless"
  404. suffixIcon={<CaretDownOutlined className="text-12px" />}
  405. labelRender={() => (
  406. <i className="iconfont icon-jixianyangshi-line"></i>
  407. )}
  408. popupMatchSelectWidth={100}
  409. options={[
  410. {
  411. label: (
  412. <img
  413. className="h-30px block"
  414. src={require("@/assets/image/line-solid.png")}
  415. />
  416. ),
  417. value: "solid",
  418. },
  419. {
  420. label: (
  421. <img
  422. className="h-30px block"
  423. src={require("@/assets/image/line-dashed.png")}
  424. />
  425. ),
  426. value: "dashed",
  427. },
  428. {
  429. label: (
  430. <img
  431. className="h-30px block"
  432. src={require("@/assets/image/line-dotted.png")}
  433. />
  434. ),
  435. value: "dotted",
  436. },
  437. {
  438. label: (
  439. <img
  440. className="h-30px block"
  441. src={require("@/assets/image/line-dashdot.png")}
  442. />
  443. ),
  444. value: "dashdot",
  445. },
  446. ]}
  447. disabled={!selectedCell?.length}
  448. value={formModel.strokeDasharray}
  449. onChange={(value) =>
  450. handleChange("all", "strokeDasharray", "stroke.type", value)
  451. }
  452. />
  453. </Tooltip>
  454. <Tooltip placement="bottom" title="连线类型">
  455. <Select
  456. className="flex-1"
  457. variant="borderless"
  458. suffixIcon={<CaretDownOutlined className="text-12px" />}
  459. labelRender={() => <i className="iconfont icon-lianxian"></i>}
  460. popupMatchSelectWidth={100}
  461. options={[
  462. {
  463. label: (
  464. <i className="iconfont icon-a-icon16lianxianleixinghuizhilianxian" />
  465. ),
  466. value: ConnectorType.Rounded,
  467. },
  468. {
  469. label: (
  470. <i className="iconfont icon-a-icon16lianxianleixingbeisaierquxian" />
  471. ),
  472. value: ConnectorType.Smooth,
  473. },
  474. {
  475. label: (
  476. <i className="iconfont icon-a-icon16lianxianleixinghuizhizhixian" />
  477. ),
  478. value: ConnectorType.Normal,
  479. },
  480. ]}
  481. disabled={!hasEdge}
  482. value={formModel.connectorType}
  483. onChange={(value) =>
  484. handleChange("edge", "connectorType", "connectorType", value)
  485. }
  486. />
  487. </Tooltip>
  488. <Divider type="vertical" />
  489. <Dropdown
  490. menu={{
  491. items: [
  492. {
  493. key: "top",
  494. onClick: () => handleSetIndex("top"),
  495. label: (
  496. <Flex className="w-180px" justify="space-between">
  497. <span>
  498. <i className="iconfont icon-zhiding1 mr-8px" />
  499. 置于顶层
  500. </span>
  501. <span className="text-12px color-#a6b9cd">Ctrl+]</span>
  502. </Flex>
  503. ),
  504. },
  505. {
  506. key: "bottom",
  507. onClick: () => handleSetIndex("bottom"),
  508. label: (
  509. <Flex className="w-180px" justify="space-between">
  510. <span>
  511. <i className="iconfont icon-zhidi1 mr-8px" />
  512. 置于底层
  513. </span>
  514. <span className="text-12px color-#a6b9cd">Ctrl+[</span>
  515. </Flex>
  516. ),
  517. },
  518. {
  519. key: "up",
  520. onClick: () => handleSetIndex("up"),
  521. label: (
  522. <Flex className="w-180px" justify="space-between">
  523. <span>
  524. <i className="iconfont icon-shangyiyiceng1 mr-8px" />
  525. 上移一层
  526. </span>
  527. <span className="text-12px color-#a6b9cd">
  528. Ctrl+Shift+]
  529. </span>
  530. </Flex>
  531. ),
  532. },
  533. {
  534. key: "dowm",
  535. onClick: () => handleSetIndex("down"),
  536. label: (
  537. <Flex className="w-180px" justify="space-between">
  538. <span>
  539. <i className="iconfont icon-xiayiyiceng1 mr-8px" />
  540. 下移一层
  541. </span>
  542. <span className="text-12px color-#a6b9cd">
  543. Ctrl+Shift+[
  544. </span>
  545. </Flex>
  546. ),
  547. },
  548. ],
  549. }}
  550. >
  551. <Tooltip placement="bottom" title="图层排列">
  552. <Button
  553. type="text"
  554. className="w-50px"
  555. disabled={!selectedCell?.length}
  556. >
  557. <i className="iconfont icon-zhiding1"></i>
  558. <CaretDownOutlined className="text-12px" />
  559. </Button>
  560. </Tooltip>
  561. </Dropdown>
  562. <Dropdown
  563. menu={{
  564. items: alignOptionData.map((item) => {
  565. return {
  566. key: item.id,
  567. disabled:
  568. ["v", "h"].includes(item.id) && countInfo.nodeCount < 3,
  569. label: (
  570. <Flex justify="space-between" className="w-180px">
  571. <div>
  572. <i className={"mr-8px iconfont " + item.icon} />
  573. <span>{item.name}</span>
  574. </div>
  575. <span className="text-12px color-#a6b9cd">
  576. {item.fastKey}
  577. </span>
  578. </Flex>
  579. ),
  580. onClick: () =>
  581. selectedCell && alignCell(item.id, selectedCell),
  582. };
  583. }),
  584. }}
  585. >
  586. <Tooltip placement="bottom" title="分布对齐">
  587. <Button
  588. type="text"
  589. className="w-50px"
  590. disabled={!countInfo.isMulti}
  591. >
  592. <i className="iconfont icon-zuoduiqi2" />
  593. <CaretDownOutlined className="text-12px" />
  594. </Button>
  595. </Tooltip>
  596. </Dropdown>
  597. <Dropdown
  598. menu={{
  599. items: [
  600. {
  601. key: "width",
  602. label: (
  603. <div>
  604. <i className="iconfont icon-gaodu mr-8px" />
  605. 宽度匹配
  606. </div>
  607. ),
  608. onClick: () =>
  609. selectedCell && matchSize("width", selectedCell),
  610. },
  611. {
  612. key: "height",
  613. label: (
  614. <div>
  615. <i className="iconfont icon-gaodu mr-8px" />
  616. 高度匹配
  617. </div>
  618. ),
  619. onClick: () =>
  620. selectedCell && matchSize("height", selectedCell),
  621. },
  622. {
  623. key: "auto",
  624. label: (
  625. <div>
  626. <i className="iconfont icon-shiyingkuangao mr-8px" />
  627. 宽高匹配
  628. </div>
  629. ),
  630. onClick: () =>
  631. selectedCell && matchSize("auto", selectedCell),
  632. },
  633. ],
  634. }}
  635. >
  636. <Tooltip placement="bottom" title="宽高匹配">
  637. <Button
  638. type="text"
  639. className="w-50px"
  640. disabled={!countInfo.isMulti}
  641. >
  642. <i className="iconfont icon-shiyingkuangao"></i>
  643. <CaretDownOutlined className="text-12px" />
  644. </Button>
  645. </Tooltip>
  646. </Dropdown>
  647. {/* <Dropdown menu={{ items: [] }}>
  648. <Button type="text" className="w-50px">
  649. <span>更多</span>
  650. <CaretDownOutlined className="text-12px" />
  651. </Button>
  652. </Dropdown> */}
  653. </div>
  654. <FindReplaceModal
  655. ref={findModalRef}
  656. current={currentIndex}
  657. count={findCount}
  658. onClose={handleClose}
  659. onFind={handleFind}
  660. onFindNext={handleFindNext}
  661. onFindPrev={handleFindPrev}
  662. onReplace={handleReplace}
  663. onReplaceAll={handleReplaceAll}
  664. />
  665. <div>
  666. <Tooltip placement="bottom" title="替换">
  667. <Button
  668. type="text"
  669. icon={<i className="iconfont icon-chaxun" />}
  670. className="m-r-16px"
  671. onClick={() => {
  672. findModalRef.current?.open();
  673. }}
  674. />
  675. </Tooltip>
  676. <Tooltip placement="bottom" title="样式">
  677. <Button
  678. type="text"
  679. icon={<SwapOutlined />}
  680. className={showRightPanel ? "active" : ""}
  681. onClick={() => toggleRightPanel()}
  682. />
  683. </Tooltip>
  684. </div>
  685. </div>
  686. </div>
  687. );
  688. }