所有分类
  • 所有分类
  • Html5资源
  • React资源
  • Vue资源
  • Php资源
  • ‌小程序资源
  • Python资源

daisyUI 扩展之 tree 组件开发

daisyUI 是一个基于 Tailwind CSS 的组件库,它为你提供了大量预设的、美观且高度可定制的 UI 组件,例如按钮、卡片、表单、导航栏等等。无需编写复杂的 CSS,也无需记忆成堆的 Tailwind 工具类,即可轻松构建现代化界面。

为什么选择 daisyUI?

  • 极速开发 :通过预设组件,大幅减少编写 HTML 和 CSS 的时间,让你专注于业务逻辑。
  • 纯净 HTML :告别冗长的 class 列表!daisyUI 的组件类名简洁明了,让你的 HTML 结构更清晰、更易维护。
  • 高度可定制 :轻松通过 Tailwind CSS 的 tailwind.config.js 文件定制主题颜色、组件样式,完美契合你的品牌风格。
  • 丰富主题 :内置多种精美主题,一键切换,省时省力。
  • 轻量高效 :daisyUI 只会生成你实际用到的 CSS,保持最终产物的小巧。
  • 语义化类名 :例如 .btn-primary ,直观易懂。
  • 免费开源 :完全免费,并在 GitHub 上开源,拥有活跃的社区支持。

整体来说不管是响应式还是对 Tailwind CSS 的支持都非常完美,定制主题颜色、组件样式非常方便。也内置了多种精美主题。但是相对于antd、element-ui组件库,唯一不足之处,组件不是很丰富。部分组件需要自已扩展。

今天笔者分享的是 tree 通用组件开发,费话不多说,直接上源码。

index.tsx

// index.tsx
import React, { useState, useEffect, useCallback } from 'react';
import TreeNode from './TreeNode';
import { TreeProps, TreeNodeType } from './types';

const Tree: React.FC<TreeProps> = ({
  data = [],
  defaultExpandAll = false,
  defaultCheckedKeys = [],
  checkable = true,
  onCheck,
  onSelect,
  fieldNames = { title: 'title', key: 'key', children: 'children' },
  className = '',
}) => {
  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  const [checkedKeys, setCheckedKeys] = useState<string[]>(defaultCheckedKeys || []);
  const [indeterminateKeys, setIndeterminateKeys] = useState<string[]>([]); // 添加半选状态
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);

  // 初始化展开的节点
  useEffect(() => {
    if (defaultExpandAll) {
      const keys: string[] = [];
      const getKeys = (nodes: TreeNodeType[]) => {
        nodes.forEach(node => {
          const key = node[fieldNames.key] as string;
          keys.push(key);
          if (node[fieldNames.children] && (node[fieldNames.children] as TreeNodeType[]).length > 0) {
            getKeys(node[fieldNames.children] as TreeNodeType[]);
          }
        });
      };
      getKeys(data);
      setExpandedKeys(keys);
    }
  }, [data, defaultExpandAll, fieldNames.key, fieldNames.children]);

  // 获取所有子节点的键
  const getChildKeys = useCallback((node: TreeNodeType): string[] => {
    const childKeys: string[] = [];
    const traverse = (n: TreeNodeType) => {
      const nodeKey = n[fieldNames.key] as string;
      childKeys.push(nodeKey);
      
      const children = n[fieldNames.children] as TreeNodeType[] | undefined;
      if (children && children.length > 0) {
        children.forEach(child => traverse(child));
      }
    };
    
    traverse(node);
    return childKeys;
  }, [fieldNames.key, fieldNames.children]);

  // 获取节点的所有父节点键
  const getParentKey = useCallback((key: string, nodes: TreeNodeType[]): string | null => {
    let parentKey: string | null = null;
    
    const traverse = (list: TreeNodeType[], parent: string | null = null) => {
      for (const node of list) {
        const nodeKey = node[fieldNames.key] as string;
        if (nodeKey === key) {
          parentKey = parent;
          return;
        }
        
        const children = node[fieldNames.children] as TreeNodeType[] | undefined;
        if (children && children.length > 0) {
          traverse(children, nodeKey);
        }
      }
    };
    
    traverse(nodes);
    return parentKey;
  }, [fieldNames.key, fieldNames.children]);

  // 获取所有父节点键
  const getAllParentKeys = useCallback((key: string, nodes: TreeNodeType[]): string[] => {
    const parents: string[] = [];
    let parent = getParentKey(key, nodes);
    
    while (parent) {
      parents.push(parent);
      parent = getParentKey(parent, nodes);
    }
    
    return parents;
  }, [getParentKey]);

  // 处理展开/折叠
  const handleExpand = useCallback((key: string, expanded: boolean) => {
    setExpandedKeys(prev => {
      if (expanded) {
        return [...prev, key];
      } else {
        return prev.filter(k => k !== key);
      }
    });
  }, []);

  // 计算半选状态
  const calculateIndeterminate = useCallback((checkedKeysValue: string[]) => {
    const indeterminate: string[] = [];
    
    // 获取所有节点的父子关系
    const nodeRelations: Record<string, { parent: string | null; children: string[] }> = {};
    
    const buildRelations = (nodes: TreeNodeType[], parent: string | null = null) => {
      nodes.forEach(node => {
        const nodeKey = node[fieldNames.key] as string;
        const children = node[fieldNames.children] as TreeNodeType[] | undefined;
        
        if (!nodeRelations[nodeKey]) {
          nodeRelations[nodeKey] = { parent, children: [] };
        } else {
          nodeRelations[nodeKey].parent = parent;
        }
        
        if (children && children.length > 0) {
          const childKeys = children.map(child => child[fieldNames.key] as string);
          nodeRelations[nodeKey].children = childKeys;
          
          // 为父节点添加子节点
          childKeys.forEach(childKey => {
            if (!nodeRelations[childKey]) {
              nodeRelations[childKey] = { parent: nodeKey, children: [] };
            } else {
              nodeRelations[childKey].parent = nodeKey;
            }
          });
          
          buildRelations(children, nodeKey);
        }
      });
    };
    
    buildRelations(data);
    
    // 计算每个父节点的半选状态
    Object.keys(nodeRelations).forEach(key => {
      const { children, parent } = nodeRelations[key];
      
      if (children.length > 0) {
        // 如果有子节点
        const allChecked = children.every(child => checkedKeysValue.includes(child));
        const someChecked = children.some(child => 
          checkedKeysValue.includes(child) || indeterminate.includes(child)
        );
        
        if (!allChecked && someChecked) {
          // 如果部分子节点被选中,则父节点为半选状态
          indeterminate.push(key);
        }
      }
    });
    
    return indeterminate;
  }, [data, fieldNames.key, fieldNames.children]);

  // 更新选中状态时同时计算半选状态
  useEffect(() => {
    const newIndeterminateKeys = calculateIndeterminate(checkedKeys);
    setIndeterminateKeys(newIndeterminateKeys);
  }, [checkedKeys, calculateIndeterminate]);

  // 处理选中/取消选中
  const handleCheck = useCallback((key: string, checked: boolean) => {
    setCheckedKeys(prev => {
      let newCheckedKeys = [...prev];
      
      // 查找当前节点
      const findNode = (nodes: TreeNodeType[]): TreeNodeType | null => {
        for (const node of nodes) {
          const nodeKey = node[fieldNames.key] as string;
          if (nodeKey === key) {
            return node;
          }
          
          const children = node[fieldNames.children] as TreeNodeType[] | undefined;
          if (children && children.length > 0) {
            const found = findNode(children);
            if (found) return found;
          }
        }
        return null;
      };
      
      const currentNode = findNode(data);
      if (!currentNode) return newCheckedKeys;
      
      if (checked) {
        // 选中当前节点及其所有子节点
        const childKeys = getChildKeys(currentNode).filter(k => k !== key);
        newCheckedKeys = [...new Set([...newCheckedKeys, key, ...childKeys])];
        
        // 检查是否需要选中父节点
        const parentKeys = getAllParentKeys(key, data);
        parentKeys.forEach(parentKey => {
          // 获取父节点的所有直接子节点
          const parentNode = findNode(data);
          if (!parentNode) return;
          
          const siblings = (parentNode[fieldNames.children] as TreeNodeType[] || [])
            .map(node => node[fieldNames.key] as string);
          
          // 如果所有兄弟节点都被选中,则选中父节点
          const allSiblingsChecked = siblings.every(siblingKey => 
            newCheckedKeys.includes(siblingKey)
          );
          
          if (allSiblingsChecked && !newCheckedKeys.includes(parentKey)) {
            newCheckedKeys.push(parentKey);
          }
        });
      } else {
        // 取消选中当前节点及其所有子节点
        const childKeys = getChildKeys(currentNode);
        newCheckedKeys = newCheckedKeys.filter(k => !childKeys.includes(k));
        
        // 取消选中所有父节点
        const parentKeys = getAllParentKeys(key, data);
        newCheckedKeys = newCheckedKeys.filter(k => !parentKeys.includes(k));
      }
      
      // 使用异步回调避免在渲染周期内触发更新
      if (onCheck) {
        setTimeout(() => {
          onCheck(newCheckedKeys);
        }, 0);
      }
      
      return newCheckedKeys;
    });
  }, [data, fieldNames.key, fieldNames.children, getChildKeys, getAllParentKeys, onCheck]);

  const handleSelect = useCallback((key: string) => {
    setSelectedKeys([key]);
    
    // 使用异步回调避免在渲染周期内触发更新
    if (onSelect) {
      setTimeout(() => {
        onSelect(key);
      }, 0);
    }
  }, [onSelect]);

  return (
    <div className={`tree-component ${className}`}>
      <ul className="menu bg-base-100 w-full">
        {data.map(node => (
          <TreeNode
            key={node[fieldNames.key] as string}
            node={node}
            fieldNames={fieldNames}
            expandedKeys={expandedKeys}
            checkedKeys={checkedKeys}
            indeterminateKeys={indeterminateKeys} // 传递半选状态
            selectedKeys={selectedKeys}
            onExpand={handleExpand}
            onCheck={handleCheck}
            onSelect={handleSelect}
            checkable={checkable}
          />
        ))}
      </ul>
    </div>
  );
};

export default Tree;

TreeNode.tsx

// TreeNode.tsx

import React from 'react';
import { TreeNodeProps, TreeNodeType } from './types';

const TreeNode: React.FC<TreeNodeProps> = ({
  node,
  fieldNames,
  expandedKeys,
  checkedKeys,
  indeterminateKeys,
  selectedKeys,
  onExpand,
  onCheck,
  onSelect,
  checkable,
}) => {
  const { title, key, children } = fieldNames;
  const nodeKey = node[key] as string;
  const nodeTitle = node[title] as string;
  const nodeChildren = node[children] as TreeNodeType[] | undefined;
  const hasChildren = nodeChildren && nodeChildren.length > 0;
  
  const isExpanded = expandedKeys.includes(nodeKey);
  const isChecked = checkedKeys.includes(nodeKey);
  const isIndeterminate = indeterminateKeys.includes(nodeKey);
  const isSelected = selectedKeys.includes(nodeKey);

  // 处理展开/折叠
  const handleToggle = (e: React.MouseEvent) => {
    e.stopPropagation();
    onExpand(nodeKey, !isExpanded);
  };

  // 处理复选框变更
  const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    e.stopPropagation();
    onCheck(nodeKey, e.target.checked);
  };

  // 处理节点点击 - 修改这里,无论是否有复选框都触发选择事件
  const handleClick = (e: React.MouseEvent) => {
    // 移除条件判断,始终触发选择事件
    e.stopPropagation();
    onSelect(nodeKey);
  };

  return (
    <li>
      <div 
        className={`flex gap-0 items-center ${isSelected ? 'bg-base-200' : ''}`}
        onClick={handleClick} // 点击节点触发选择事件
      >
        {hasChildren && (
          <span 
            className="mr-2 cursor-pointer flex"
            onClick={handleToggle}
          >
            <i className={`iconify ${isExpanded ? 'lucide--chevron-down' : 'lucide--chevron-right'} size-4`}></i>
          </span>
        )}
        
        {checkable && (
          <label className="cursor-pointer label" onClick={(e) => e.stopPropagation()}>
            <input
              type="checkbox"
              className={`checkbox checkbox-sm`}
              checked={isChecked}
              onChange={handleCheckboxChange}
              ref={el => {
                if (el) {
                  el.indeterminate = isIndeterminate;
                }
              }}
            />
          </label>
        )}
        
        <span className="ml-2">{nodeTitle}</span>
      </div>
      
      {hasChildren && isExpanded && (
        <ul className="pl-4">
          {nodeChildren!.map(childNode => (
            <TreeNode
              key={childNode[key] as string}
              node={childNode}
              fieldNames={fieldNames}
              expandedKeys={expandedKeys}
              checkedKeys={checkedKeys}
              indeterminateKeys={indeterminateKeys}
              selectedKeys={selectedKeys}
              onExpand={onExpand}
              onCheck={onCheck}
              onSelect={onSelect}
              checkable={checkable}
            />
          ))}
        </ul>
      )}
    </li>
  );
};

export default TreeNode;

type.ts

export interface FieldNames {
  title: string;
  key: string;
  children: string;
}

export interface TreeNodeType {
  [key: string]: any;
}

export interface TreeProps {
  data: TreeNodeType[];
  defaultExpandAll?: boolean;
  defaultCheckedKeys?: string[];
  checkable?: boolean;
  onCheck?: (checkedKeys: string[]) => void;
  onSelect?: (key: string) => void;
  fieldNames?: FieldNames;
  className?: string;
}

export interface TreeNodeProps {
  node: TreeNodeType;
  fieldNames: FieldNames;
  expandedKeys: string[];
  checkedKeys: string[];
  indeterminateKeys: string[]; // 添加半选状态键数组
  selectedKeys: string[];
  onExpand: (key: string, expanded: boolean) => void;
  onCheck: (key: string, checked: boolean) => void;
  onSelect: (key: string) => void;
  checkable: boolean;
}

组件示例

默认树

<Tree 
    data={treeData} 
    onCheck={handleCheck}
    onSelect={handleSelect}
  />

默认展开所有节点

<Tree 
  data={treeData} 
  defaultExpandAll
  onCheck={handleCheck}
  onSelect={handleSelect}
/>

无复选框

<Tree 
  data={treeData} 
  checkable={false}
  onSelect={handleSelect}
/>

自定义字段名

<Tree 
  data={[
    {
      name: '自定义节点1',
      id: 'custom-0',
      items: [
        { name: '自定义子节点1-1', id: 'custom-0-0' }
      ]
    }
  ]} 
  fieldNames={{ title: 'name', key: 'id', children: 'items' }}
  onCheck={handleCheck}
  onSelect={handleSelect}
/>
原文链接:https://code.ifrontend.net/archives/306,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?