index.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import React, {
  2. useEffect,
  3. useMemo,
  4. useRef,
  5. useState,
  6. } from "react";
  7. import { Collapse, Input, Space, Spin, Tooltip } from "antd";
  8. import { useModel } from "umi";
  9. import { Dnd } from "@antv/x6-plugin-dnd";
  10. import insertCss from "insert-css";
  11. import { SearchOutlined } from "@ant-design/icons";
  12. import { CompoundedComponent } from "@/types";
  13. import { useRequest } from "ahooks";
  14. import ImageNode from "@/components/ImageNode";
  15. import { basic } from "@/components/basic";
  16. import { flowchart } from "@/components/flowchart";
  17. import { er } from "@/components/er";
  18. import { lane } from "@/components/lane";
  19. export default function Libary() {
  20. const { graph, initDnd, startDrag } = useModel("graphModel");
  21. const containerRef = useRef<HTMLDivElement>(null);
  22. const [search, setSearch] = useState("");
  23. const [showSearch, setShowSearch] = useState(false);
  24. const [systemSearchResult, setSystemSearchResult] = useState<
  25. CompoundedComponent[]
  26. >([]);
  27. const [activeKeys, setActiveKeys] = useState(["1", "2", "3", "4"]);
  28. useEffect(() => {
  29. if (!containerRef.current || !graph) return;
  30. const dnd = new Dnd({
  31. target: graph,
  32. scaled: false,
  33. dndContainer: containerRef.current,
  34. });
  35. initDnd(dnd);
  36. }, [graph, containerRef.current]);
  37. insertCss(`
  38. .shalu-collapse-item {
  39. border-bottom: 1px solid #dfe2e5 !important;
  40. border-radius: 0 !important;
  41. }
  42. .shalu-tabs-content {
  43. height: 100%;
  44. overflow: auto;
  45. }
  46. `);
  47. const paramRef = useRef<Record<string, any>>({
  48. appkey: "66dbfc87e4b0eb0606e13055",
  49. page: 1,
  50. size: 25,
  51. query: "",
  52. });
  53. const [iconList, setIconList] = useState<CompoundedComponent[]>([]);
  54. const searchServer = async (): Promise<any> => {
  55. const params = new URLSearchParams(paramRef.current).toString();
  56. const res = await fetch(`https://iconsapi.com/api/search?${params}`).then(
  57. (res) => res.json()
  58. );
  59. const list: CompoundedComponent[] = (res?.pages?.elements || []).map(
  60. (item: { iconName: string; url: string }) => {
  61. return {
  62. name: item.iconName,
  63. icon: item.url,
  64. node: {
  65. ...ImageNode,
  66. data: {
  67. ...ImageNode.data,
  68. fill: {
  69. ...ImageNode.data.fill,
  70. fillType: "image",
  71. imageUrl: item.url,
  72. },
  73. },
  74. },
  75. };
  76. }
  77. );
  78. if(paramRef.current.page === 1) {
  79. setIconList(list);
  80. } else {
  81. setIconList([...iconList, ...list]);
  82. }
  83. return res?.pages || {};
  84. };
  85. const { data, loading, run } = useRequest(searchServer, {
  86. manual: true,
  87. cacheKey: "iconsapi",
  88. cacheTime: 1000 * 60 * 60 * 24,
  89. });
  90. const renderItem = (data: any, key: number) => {
  91. return (
  92. <Tooltip title={data.name} key={key}>
  93. <img
  94. className="w-32px cursor-move"
  95. src={data.icon}
  96. onMouseDown={(e) => startDrag(e, data.node)}
  97. />
  98. </Tooltip>
  99. );
  100. };
  101. const items = useMemo(() => {
  102. const list = [
  103. {
  104. key: "1",
  105. label: "基础图形",
  106. children: (
  107. <Space wrap size={6}>
  108. {basic.map((item, index) => renderItem(item, index))}
  109. </Space>
  110. ),
  111. },
  112. {
  113. key: "2",
  114. label: "Flowchart流程图",
  115. children: (
  116. <Space wrap size={6}>
  117. {flowchart.map((item, index) => renderItem(item, index))}
  118. </Space>
  119. ),
  120. },
  121. {
  122. key: "3",
  123. label: "实体关系图(E-R图)",
  124. children: (
  125. <Space wrap size={6}>
  126. {er.map((item, index) => renderItem(item, index))}
  127. </Space>
  128. ),
  129. },
  130. {
  131. key: "4",
  132. label: "泳池/泳道",
  133. children: (
  134. <Space wrap size={6}>
  135. {lane.map((item, index) => renderItem(item, index))}
  136. </Space>
  137. ),
  138. },
  139. ];
  140. if (showSearch) {
  141. list.unshift({
  142. key: "search-icon",
  143. label: "网络图形",
  144. children: (
  145. <Spin spinning={loading}>
  146. <Space wrap size={6}>
  147. {iconList.map((item, index) => renderItem(item, index))}
  148. </Space>
  149. <div
  150. className="text-12px text-center color-#9aa5b8 w-full h-20px leading-20px cursor-pointer hover:bg-#f2f2f2 hover:color-#212930"
  151. onClick={() => {
  152. if(!(data && data?.pageCount === data?.curPage)) {
  153. handleNextPage(data.curPage + 1);
  154. }
  155. }}
  156. >
  157. {
  158. loading ? '加载中...'
  159. : data && data?.pageCount === data?.curPage ? '暂无更多' : '加载更多'
  160. }
  161. </div>
  162. </Spin>
  163. ),
  164. });
  165. if (!activeKeys.includes("search-icon")) {
  166. setActiveKeys((state) => [...state, "search-icon"]);
  167. }
  168. }
  169. if (showSearch && systemSearchResult.length) {
  170. list.unshift({
  171. key: "search-system",
  172. label: "系统图形",
  173. children: (
  174. <Space wrap size={6}>
  175. {systemSearchResult.map((item, index) => renderItem(item, index))}
  176. </Space>
  177. ),
  178. });
  179. if (!activeKeys.includes("search-system")) {
  180. setActiveKeys((state) => [...state, "search-system"]);
  181. }
  182. }
  183. return list;
  184. }, [systemSearchResult, showSearch, data, iconList, loading]);
  185. const handleChange = (keys: string | string[]) => {
  186. setActiveKeys(Array.isArray(keys) ? keys : [keys]);
  187. };
  188. const handleSearch = () => {
  189. if (!search) return;
  190. const allData = [...basic, ...flowchart, ...er, ...lane];
  191. setShowSearch(true);
  192. setSystemSearchResult(allData.filter((item) => item.name.includes(search)));
  193. paramRef.current = {
  194. ...paramRef.current,
  195. page: 1,
  196. query: search,
  197. };
  198. run();
  199. };
  200. const handleNextPage = (page: number) => {
  201. paramRef.current = {
  202. ...paramRef.current,
  203. page,
  204. };
  205. run();
  206. }
  207. useEffect(() => {
  208. if (!search) {
  209. setActiveKeys(["1", "2", "3", "4"]);
  210. setShowSearch(false);
  211. setSystemSearchResult([]);
  212. paramRef.current = {
  213. ...paramRef.current,
  214. page: 1,
  215. query: "",
  216. };
  217. setActiveKeys((state) =>
  218. state.filter(
  219. (item) => !(item === "search-system" || item === "search-icon")
  220. )
  221. );
  222. }
  223. }, [search]);
  224. return (
  225. <div ref={containerRef} className="h-full overflow-auto">
  226. <div className="px-4">
  227. <Input
  228. prefix={<SearchOutlined />}
  229. size="small"
  230. allowClear
  231. placeholder="请输入搜索内容"
  232. value={search}
  233. onChange={(e) => setSearch(e.target.value)}
  234. onPressEnter={handleSearch}
  235. />
  236. </div>
  237. <Collapse
  238. ghost
  239. items={items}
  240. activeKey={activeKeys}
  241. onChange={handleChange}
  242. />
  243. </div>
  244. );
  245. }