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