Переглянути джерело

feat: 添加网络图标搜索

liaojiaxing 7 місяців тому
батько
коміт
64f8719aa5

+ 1 - 1
apps/designer/.umirc.ts

@@ -31,5 +31,5 @@ export default defineConfig({
     { path: "/", component: "home" },
     { path: "/flow", component: "flow" },
   ],
-  npmClient: 'pnpm',
+  npmClient: 'pnpm'
 });

+ 57 - 0
apps/designer/src/components/ImageNode.tsx

@@ -0,0 +1,57 @@
+import { register } from "@antv/x6-react-shape";
+import { Node } from "@antv/x6";
+import { ports, defaultData } from "./data";
+import { useSizeHook, useShapeProps } from "@/hooks";
+const component = ({ node }: { node: Node }) => {
+  const { fill, stroke, opacity } = node.getData();
+  const { size, ref } = useSizeHook();
+  const {
+    fillContent,
+    defsContent,
+    strokeWidth,
+  } = useShapeProps(fill, size, stroke);
+
+  return (
+    <>
+      <div
+        className="relative text-0 w-full h-full"
+        ref={ref}
+        style={{ opacity: opacity / 100 }}
+      >
+        <svg
+          className="w-full h-full"
+          viewBox={`0 0 ${size?.width} ${size?.height}`}
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <defs>{defsContent}</defs>
+          <rect
+            x={strokeWidth}
+            y={strokeWidth}
+            width={size?.width - 4}
+            height={size?.height - 4}
+            fill={fillContent}
+          />
+        </svg>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "custom-react-image",
+  width: 100,
+  height: 100,
+  effect: ["data"],
+  component: component,
+});
+
+const imageNode = {
+    shape: "custom-react-image",
+    data: {
+      label: "",
+      ...defaultData,
+    },
+    ports,
+  };
+
+export default imageNode;

+ 1 - 0
apps/designer/src/components/data.ts

@@ -114,6 +114,7 @@ export const defaultData = {
     gradientType: "linear",
     gradientValue: 0,
     objectFit: ImageFillType.Fill,
+    imageUrl: "",
   },
   stroke: {
     type: "solid",

+ 25 - 12
apps/designer/src/hooks/useShapeProps.tsx

@@ -37,18 +37,19 @@ export function useShapeProps(
   } = fill;
   const { width, height } = size;
   const id = uniqueId("fill_");
-  const fillContent =
-    fillType === "color"
-      ? color1
-      : `url(#${id})`;
+  const fillContent = fillType === "color" ? color1 : `url(#${id})`;
+  const objectFitMap = {
+    [ImageFillType.Fill]: "xMidYMid slice", // 填充
+    [ImageFillType.Auto]: "", // 自动
+    [ImageFillType.Stretch]: "", // 拉伸
+    [ImageFillType.Original]: "xMidYMid meet", // 原始
+    [ImageFillType.Tiled]: "", // 平铺
+  };
 
   const defsContent = (
     <>
       {fillType === "color" && gradientType === "linear" && (
-        <linearGradient
-          id={id}
-          gradientTransform={`rotate(${gradientValue})`}
-        >
+        <linearGradient id={id} gradientTransform={`rotate(${gradientValue})`}>
           <stop offset="0%" stopColor={color1} />
           <stop offset="100%" stopColor={color2} />
         </linearGradient>
@@ -67,20 +68,32 @@ export function useShapeProps(
         </radialGradient>
       )}
       {fillType === "image" && (
-        <pattern id={id} patternUnits="userSpaceOnUse" width={width} height={height}>
-          <image href={imageUrl} x="0" y="0" width={width} height={height}></image>
+        <pattern
+          id={id}
+          patternUnits="userSpaceOnUse"
+          width={width}
+          height={height}
+        >
+          <image
+            xlinkHref={imageUrl}
+            x="0"
+            y="0"
+            width="100%"
+            height="100%"
+            preserveAspectRatio={objectFitMap[objectFit]}
+          ></image>
         </pattern>
       )}
     </>
   );
 
-  const strokeDasharray  = LineType[stroke.type];
+  const strokeDasharray = LineType[stroke.type];
 
   return {
     strokeColor: stroke.color,
     strokeWidth: stroke.width,
     fillContent,
     defsContent,
-    strokeDasharray
+    strokeDasharray,
   };
 }

+ 2 - 2
apps/designer/src/models/appModel.ts

@@ -32,7 +32,7 @@ export default function appModel() {
     author: "",
   });
   // 隐藏/显示右侧面板
-  const [showRightPanel, setShowRightPanel] = useState(false);
+  const [showRightPanel, setShowRightPanel] = useState(true);
   // 格式刷启用
   const [enableFormatBrush, setEnableFormatBrush] = useState(false);
   // 格式刷样式
@@ -123,6 +123,7 @@ export default function appModel() {
       graphRef.current?.off("blank:click", handleClick);
     } else {
       if(args.cell.data?.lock) return;
+      // 应用格式刷
       const data = args.cell.data;
       args.cell.setData({
         text: formatBrushStyle.current?.text || data?.text,
@@ -130,7 +131,6 @@ export default function appModel() {
         stroke: formatBrushStyle.current?.stroke || data?.stroke,
         opacity: formatBrushStyle.current?.opacity || data?.opacity
       })
-      console.log(args.cell.data, formatBrushStyle.current)
     }
   };
 

+ 215 - 37
apps/designer/src/pages/flow/components/Libary/index.tsx

@@ -1,21 +1,37 @@
-import React, { useCallback, useEffect, useRef, useState } from "react";
-import { Collapse, Input, Space, Tooltip } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { Collapse, Input, Space, Spin, Tooltip } from "antd";
 import { useModel } from "umi";
-import { Dnd } from '@antv/x6-plugin-dnd';
-import insertCss from 'insert-css';
+import { Dnd } from "@antv/x6-plugin-dnd";
+import insertCss from "insert-css";
+import { SearchOutlined } from "@ant-design/icons";
+import { CompoundedComponent } from "@/types";
+import { useRequest } from "ahooks";
+import ImageNode from "@/components/ImageNode";
 
 import { basic } from "@/components/basic";
 import { flowchart } from "@/components/flowchart";
 import { er } from "@/components/er";
 import { lane } from "@/components/lane";
 export default function Libary() {
-  const { graph, initDnd, startDrag } = useModel('graphModel');
-  
+  const { graph, initDnd, startDrag } = useModel("graphModel");
+
   const containerRef = useRef<HTMLDivElement>(null);
+  const [search, setSearch] = useState("");
+  const [showSearch, setShowSearch] = useState(false);
+  const [systemSearchResult, setSystemSearchResult] = useState<
+    CompoundedComponent[]
+  >([]);
+  const [activeKeys, setActiveKeys] = useState(["1", "2", "3", "4"]);
 
   useEffect(() => {
-    if(!containerRef.current || !graph) return;
-    
+    if (!containerRef.current || !graph) return;
+
     const dnd = new Dnd({
       target: graph,
       scaled: false,
@@ -34,45 +50,207 @@ export default function Libary() {
       overflow: auto;
     }
   `);
-  
-  const renderItem = (data: any, key: number) => {
-      return <Tooltip title={data.name} key={key}>
-          <img className="w-32px cursor-move" src={data.icon} onMouseDown={(e) => startDrag(e, data.node)}/>
-        </Tooltip>
+
+  const paramRef = useRef<Record<string, any>>({
+    appkey: "66dbfc87e4b0eb0606e13055",
+    page: 1,
+    size: 25,
+    query: "",
+  });
+
+  const [iconList, setIconList] = useState<CompoundedComponent[]>([]);
+
+  const searchServer = async (): Promise<any> => {
+    const params = new URLSearchParams(paramRef.current).toString();
+    const res = await fetch(`https://iconsapi.com/api/search?${params}`).then(
+      (res) => res.json()
+    );
+    const list: CompoundedComponent[] = (res?.pages?.elements || []).map(
+      (item: { iconName: string; url: string }) => {
+        return {
+          name: item.iconName,
+          icon: item.url,
+          node: {
+            ...ImageNode,
+            data: {
+              ...ImageNode.data,
+              fill: {
+                ...ImageNode.data.fill,
+                fillType: "image",
+                imageUrl: item.url,
+              },
+            },
+          },
+        };
+      }
+    );
+    if(paramRef.current.page === 1) {
+      setIconList(list);
+    } else {
+      setIconList([...iconList, ...list]);
+    }
+    return res?.pages || {};
   };
 
-  const [items, setItems] = useState([
-    {
-      key: "1",
-      label: "基础图形",
-      children: <Space wrap size={6}>{basic.map((item, index) => renderItem(item, index))}</Space>,
-    },
-    {
-      key: "2",
-      label: "Flowchart流程图",
-      children: <Space wrap size={6}>{flowchart.map((item, index) => renderItem(item, index))}</Space>,
-    },
-    {
-      key: "3",
-      label: "实体关系图(E-R图)",
-      children: <Space wrap size={6}>{er.map((item, index) => renderItem(item, index))}</Space>,
-    },
-    {
-      key: "4",
-      label: "泳池/泳道",
-      children: <Space wrap size={6}>{lane.map((item, index) => renderItem(item, index))}</Space>,
-    },
-  ]);
+  const { data, loading, run } = useRequest(searchServer, {
+    manual: true,
+    cacheKey: "iconsapi",
+    cacheTime: 1000 * 60 * 60 * 24,
+  });
 
-  const [activeKeys, setActiveKeys] = useState(["1", "2", "3", "4"]);
+  const renderItem = (data: any, key: number) => {
+    return (
+      <Tooltip title={data.name} key={key}>
+        <img
+          className="w-32px cursor-move"
+          src={data.icon}
+          onMouseDown={(e) => startDrag(e, data.node)}
+        />
+      </Tooltip>
+    );
+  };
+
+  const items = useMemo(() => {
+    const list = [
+      {
+        key: "1",
+        label: "基础图形",
+        children: (
+          <Space wrap size={6}>
+            {basic.map((item, index) => renderItem(item, index))}
+          </Space>
+        ),
+      },
+      {
+        key: "2",
+        label: "Flowchart流程图",
+        children: (
+          <Space wrap size={6}>
+            {flowchart.map((item, index) => renderItem(item, index))}
+          </Space>
+        ),
+      },
+      {
+        key: "3",
+        label: "实体关系图(E-R图)",
+        children: (
+          <Space wrap size={6}>
+            {er.map((item, index) => renderItem(item, index))}
+          </Space>
+        ),
+      },
+      {
+        key: "4",
+        label: "泳池/泳道",
+        children: (
+          <Space wrap size={6}>
+            {lane.map((item, index) => renderItem(item, index))}
+          </Space>
+        ),
+      },
+    ];
+    if (showSearch) {
+      list.unshift({
+        key: "search-icon",
+        label: "网络图形",
+        children: (
+          <Spin spinning={loading}>
+            <Space wrap size={6}>
+              {iconList.map((item, index) => renderItem(item, index))}
+            </Space>
+            <div 
+              className="text-12px text-center color-#9aa5b8 w-full h-20px leading-20px cursor-pointer hover:bg-#f2f2f2 hover:color-#212930"
+              onClick={() => {
+                if(!(data && data?.pageCount === data?.curPage)) {
+                  handleNextPage(data.curPage + 1);
+                }
+              }}
+              >
+                {
+                  loading ? '加载中...'
+                  : data && data?.pageCount === data?.curPage ? '暂无更多' : '加载更多'
+                }
+            </div>
+          </Spin>
+        ),
+      });
+      if (!activeKeys.includes("search-icon")) {
+        setActiveKeys((state) => [...state, "search-icon"]);
+      }
+    }
+    if (showSearch && systemSearchResult.length) {
+      list.unshift({
+        key: "search-system",
+        label: "系统图形",
+        children: (
+          <Space wrap size={6}>
+            {systemSearchResult.map((item, index) => renderItem(item, index))}
+          </Space>
+        ),
+      });
+      if (!activeKeys.includes("search-system")) {
+        setActiveKeys((state) => [...state, "search-system"]);
+      }
+    }
+    return list;
+  }, [systemSearchResult, showSearch, data, iconList, loading]);
 
   const handleChange = (keys: string | string[]) => {
     setActiveKeys(Array.isArray(keys) ? keys : [keys]);
   };
+
+  const handleSearch = () => {
+    if (!search) return;
+
+    const allData = [...basic, ...flowchart, ...er, ...lane];
+    setShowSearch(true);
+    setSystemSearchResult(allData.filter((item) => item.name.includes(search)));
+    paramRef.current = {
+      ...paramRef.current,
+      page: 1,
+      query: search,
+    };
+    run();
+  };
+
+  const handleNextPage = (page: number) => {
+    paramRef.current = {
+      ...paramRef.current,
+      page,
+    };
+    run();
+  }
+
+  useEffect(() => {
+    if (!search) {
+      setActiveKeys(["1", "2", "3", "4"]);
+      setShowSearch(false);
+      setSystemSearchResult([]);
+      paramRef.current = {
+        ...paramRef.current,
+        page: 1,
+        query: "",
+      };
+      setActiveKeys((state) =>
+        state.filter(
+          (item) => !(item === "search-system" || item === "search-icon")
+        )
+      );
+    }
+  }, [search]);
+
   return (
     <div ref={containerRef} className="h-full overflow-auto">
       <div className="px-4">
-        <Input size="small" allowClear placeholder="请输入搜索内容" />
+        <Input
+          prefix={<SearchOutlined />}
+          size="small"
+          allowClear
+          placeholder="请输入搜索内容"
+          value={search}
+          onChange={(e) => setSearch(e.target.value)}
+          onPressEnter={handleSearch}
+        />
       </div>
       <Collapse
         ghost