Bläddra i källkod

feat[pc-components]: DageTreeActions

chenlei 9 månader sedan
förälder
incheckning
6e551a6b5b

+ 2 - 0
packages/backend-cli/template/.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+@dage:registry=http://192.168.20.245:4873/

+ 7 - 0
packages/backend-cli/template/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @dage/backend-template
 
+## 1.0.13
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.3.2
+
 ## 1.0.12
 
 ### Patch Changes

+ 1 - 1
packages/backend-cli/template/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/backend-template",
-  "version": "1.0.12",
+  "version": "1.0.13",
   "private": true,
   "dependencies": {
     "@ant-design/icons": "^5.1.4",

+ 1 - 1
packages/docs/.umirc.ts

@@ -58,7 +58,7 @@ export default defineConfig({
       },
       {
         title: "布局",
-        children: ["/components/TableActions"],
+        children: ["/components/TableActions", "/components/TreeActions"],
       },
       {
         title: "表单组件",

+ 135 - 0
packages/docs/docs/components/TreeActions/index.md

@@ -0,0 +1,135 @@
+## DageTreeActions 树形控件操作栏
+
+### 基本用法
+
+```tsx
+import React, { useState, Key } from "react";
+import { message } from "antd";
+import { DataNode } from "antd/es/tree";
+import { DageTreeActions } from "@dage/pc-components";
+
+let key = 0;
+
+export default () => {
+  const [data, setData] = useState([
+    {
+      title: "parent 1",
+      key: "0",
+      children: [
+        {
+          title: "leaf 1",
+          key: "0-0",
+        },
+        {
+          title: "leaf 2",
+          key: "0-1",
+          children: [
+            {
+              title: "leaf 2-1",
+              key: "0-1-0",
+            },
+          ],
+        },
+      ],
+    },
+  ]);
+
+  const handleAddNode = async (item: DataNode, path: number[]) => {
+    return new Promise((res) => {
+      setTimeout(() => {
+        const temp = [...data];
+        let _item: DataNode = temp;
+
+        while (path.length) {
+          _item = _item[path.shift()];
+          if (path.length) {
+            _item = _item.children;
+          }
+        }
+
+        if (!_item.children) {
+          _item.children = [
+            {
+              title: "add leaf " + key,
+              key: "add-" + key++,
+            },
+          ];
+        } else {
+          _item.children.push({
+            title: "add leaf " + key,
+            key: "add-" + key++,
+          });
+        }
+
+        setData(temp);
+        res(true);
+      }, 1000);
+    });
+  };
+
+  const handleEditNode = (item: DataNode, path: number[]) => {
+    message.info("编辑节点路径:" + JSON.stringify(path));
+  };
+
+  const handleDeleteNode = (item: DataNode, path: number[]) => {
+    return new Promise((res) => {
+      setTimeout(() => {
+        const temp = [...data];
+        let _item: DataNode = temp;
+
+        while (path.length) {
+          if (path.length === 1) {
+            _item.splice(path[0], 1);
+            break;
+          }
+
+          _item = _item[path.shift()];
+          if (path.length) {
+            _item = _item.children;
+          }
+        }
+
+        setData(temp);
+        res(true);
+      }, 1000);
+    });
+  };
+
+  const handleSelect = (selectedKeys: Key[]) => {
+    message.success("选中节点 key:" + JSON.stringify(selectedKeys));
+  };
+
+  return (
+    <DageTreeActions
+      maxLevel={4}
+      treeData={data}
+      onAdd={handleAddNode}
+      onEdit={handleEditNode}
+      onDelete={handleDeleteNode}
+      onSelect={handleSelect}
+    />
+  );
+};
+```
+
+## API
+
+<API hideTitle src='@dage/pc-components/components/DageTreeActions/index.d.ts'></API>
+
+## Methods
+
+### findNodeLevel 获取节点层级
+
+| Name      | Description | Type         | Default      |
+| --------- | ----------- | ------------ | ------------ |
+| nodes     | --          | `DataNode[]` | `(required)` |
+| targetKey | 目标 key    | `React.Key`  | `(required)` |
+| curLevel  | 当前层级    | `number`     | `--`         |
+
+### findKeyPath 获取节点路径
+
+| Name      | Description | Type         | Default      |
+| --------- | ----------- | ------------ | ------------ |
+| nodes     | --          | `DataNode[]` | `(required)` |
+| targetKey | 目标 key    | `React.Key`  | `(required)` |
+| path      | 路径        | `number[]`   | `[]`         |

+ 6 - 0
packages/docs/docs/log/PC-COMPONENTS_CHANGELOG.md

@@ -1,5 +1,11 @@
 # @dage/pc-components
 
+## 1.3.2
+
+### Patch Changes
+
+- 新增 `DageTreeActions` 组件
+
 ## 1.3.1
 
 ### Patch Changes

+ 6 - 0
packages/pc-components/CHANGELOG.md

@@ -1,5 +1,11 @@
 # @dage/pc-components
 
+## 1.3.2
+
+### Patch Changes
+
+- 新增 `DageTreeActions` 组件
+
 ## 1.3.1
 
 ### Patch Changes

+ 1 - 1
packages/pc-components/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/pc-components",
-  "version": "1.3.1",
+  "version": "1.3.2",
   "description": "PC 端组件库",
   "module": "dist/index.js",
   "main": "dist/index.js",

+ 95 - 0
packages/pc-components/src/components/DageTreeActions/Title.tsx

@@ -0,0 +1,95 @@
+import { FC, useState } from "react";
+import { Button, Modal } from "antd";
+import { DataNode } from "antd/es/tree";
+import { ExclamationCircleFilled } from "@ant-design/icons";
+import { DageTreeTitleProps } from "./types";
+import { TreeTitle } from "./style";
+import { findKeyPath, findNodeLevel } from "./utils";
+
+const { confirm } = Modal;
+
+export const DageTreeTitle: FC<DageTreeTitleProps> = ({
+  data,
+  maxLevel,
+  treeData,
+  onAdd,
+  onEdit,
+  onDelete,
+}) => {
+  const [addLoading, setAddLoading] = useState(false);
+  const [editLoading, setEditLoading] = useState(false);
+
+  const handleAdd = async (data: DataNode) => {
+    try {
+      setAddLoading(true);
+      await onAdd?.(data, findKeyPath(treeData, data.key)!);
+    } finally {
+      setAddLoading(false);
+    }
+  };
+
+  const handleEdit = async (data: DataNode) => {
+    try {
+      setEditLoading(true);
+      await onEdit?.(data, findKeyPath(treeData, data.key)!);
+    } finally {
+      setEditLoading(false);
+    }
+  };
+
+  const handleDelete = (data: DataNode) => {
+    confirm({
+      title: `提示`,
+      icon: <ExclamationCircleFilled />,
+      content: `确认是否删除 ${data.title} 节点`,
+      okText: "确认",
+      cancelText: "取消",
+      onOk() {
+        return onDelete?.(data, findKeyPath(treeData, data.key)!);
+      },
+    });
+  };
+
+  return (
+    <TreeTitle>
+      <p>{data.title as string}</p>
+      <div
+        className="dage-tree-title-toolbar"
+        onClick={(e) => e.stopPropagation()}
+      >
+        {Boolean(onAdd) &&
+          (!maxLevel || findNodeLevel(treeData, data.key) < maxLevel) && (
+            <Button
+              type="link"
+              size="small"
+              loading={addLoading}
+              onClick={handleAdd.bind(undefined, data)}
+            >
+              新增
+            </Button>
+          )}
+        {Boolean(onEdit) && (
+          <Button
+            type="link"
+            size="small"
+            loading={editLoading}
+            color="default"
+            onClick={handleEdit.bind(undefined, data)}
+          >
+            编辑
+          </Button>
+        )}
+        {Boolean(onDelete) && (
+          <Button
+            type="link"
+            danger
+            size="small"
+            onClick={handleDelete.bind(undefined, data)}
+          >
+            删除
+          </Button>
+        )}
+      </div>
+    </TreeTitle>
+  );
+};

+ 34 - 0
packages/pc-components/src/components/DageTreeActions/index.tsx

@@ -0,0 +1,34 @@
+import { FC } from "react";
+import { DageTreeActionsProps } from "./types";
+import { DageTreeTitle } from "./Title";
+import { TreeActions } from "./style";
+
+export const DageTreeActions: FC<DageTreeActionsProps> = ({
+  maxLevel,
+  onAdd,
+  onEdit,
+  onDelete,
+  ...props
+}) => {
+  return (
+    <TreeActions
+      blockNode
+      defaultExpandAll
+      virtual={false}
+      {...props}
+      titleRender={(data) => (
+        <DageTreeTitle
+          data={data}
+          treeData={props.treeData}
+          maxLevel={maxLevel}
+          onAdd={onAdd}
+          onEdit={onEdit}
+          onDelete={onDelete}
+        />
+      )}
+    />
+  );
+};
+
+export * from "./types";
+export * from "./utils";

+ 58 - 0
packages/pc-components/src/components/DageTreeActions/style.ts

@@ -0,0 +1,58 @@
+import { Tree, TreeProps } from "antd";
+import { FC } from "react";
+import styled from "styled-components";
+
+export const TreeActions = styled(Tree).attrs({
+  className: "dage-tree-actions",
+})`
+  --dage-tree-actions-title-height: 30px;
+
+  .ant-tree-treenode {
+    padding: 0;
+
+    &.ant-tree-treenode-selected,
+    &:hover {
+      background: rgba(182, 227, 255, 0.3);
+    }
+    &:hover {
+      .dage-tree-title-toolbar {
+        display: block;
+      }
+    }
+  }
+  .ant-tree-node-content-wrapper {
+    &.ant-tree-node-selected,
+    &:hover {
+      background: none;
+    }
+  }
+  .ant-tree-switcher {
+    line-height: var(--dage-tree-actions-title-height);
+  }
+  .ant-btn[color="default"] {
+    color: inherit;
+  }
+` as FC<TreeProps>;
+
+export const TreeTitle = styled.div.attrs({
+  className: "dage-tree-title",
+})`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  height: var(--dage-tree-actions-title-height);
+  font-size: 12px;
+
+  p {
+    margin: 0;
+    flex: 1;
+    width: 0;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+  .dage-tree-title-toolbar {
+    display: none;
+  }
+`;

+ 21 - 0
packages/pc-components/src/components/DageTreeActions/types.ts

@@ -0,0 +1,21 @@
+import { DataNode, TreeProps } from "antd/es/tree";
+
+export interface DageTreeActionsProps
+  extends Omit<TreeProps, "titleRender" | "treeData"> {
+  /**
+   * 最大层级,从0开始
+   */
+  maxLevel?: number;
+  treeData: DataNode[];
+  onAdd?: (item: DataNode, path: number[]) => void;
+  onEdit?: (item: DataNode, path: number[]) => void;
+  onDelete?: (item: DataNode, path: number[]) => void;
+}
+
+export interface DageTreeTitleProps
+  extends Pick<
+    DageTreeActionsProps,
+    "onAdd" | "onEdit" | "onDelete" | "maxLevel" | "treeData"
+  > {
+  data: DataNode;
+}

+ 45 - 0
packages/pc-components/src/components/DageTreeActions/utils.ts

@@ -0,0 +1,45 @@
+import { Key } from "react";
+import { DataNode } from "antd/es/tree";
+
+export const findNodeLevel = (
+  nodes: DataNode[],
+  targetKey: Key,
+  curLevel = 0
+): number => {
+  for (let node of nodes) {
+    if (node.key === targetKey) {
+      return curLevel;
+    }
+    if (node.children) {
+      const childLevel = findNodeLevel(node.children, targetKey, curLevel + 1);
+      if (childLevel !== -1) {
+        return childLevel;
+      }
+    }
+  }
+  return -1;
+};
+
+export const findKeyPath = (
+  nodes: DataNode[],
+  targetKey: Key,
+  path: number[] = []
+): number[] | null => {
+  for (let i = 0; i < nodes.length; i++) {
+    const node = nodes[i];
+    const currentPath = [...path, i];
+
+    if (node.key === targetKey) {
+      return currentPath;
+    }
+
+    if (node.children) {
+      const childPath = findKeyPath(node.children, targetKey, currentPath);
+      if (childPath) {
+        return childPath;
+      }
+    }
+  }
+
+  return null;
+};

+ 1 - 0
packages/pc-components/src/components/index.ts

@@ -5,3 +5,4 @@ export * from "./DageTableActions";
 export * from "./DageEditor";
 export * from "./DageLoading";
 export * from "./DageCheckboxGroup";
+export * from "./DageTreeActions";