Files
labelmain-demo/components/tree-select/VirtualizedTree.tsx
2026-05-11 18:28:26 +08:00

143 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// VirtualizedTree.tsx
import {
RenderTreeNodePayload,
TreeNodeData,
UseTreeReturnType,
} from "@mantine/core"
import React, { CSSProperties, useMemo } from "react"
import { List } from "react-window"
import "./mantine-scrollbar.css"
export interface VirtualizedTreeProps {
/** Mantine tree controller (from useTree()) — optional, if not provided this component won't control state */
tree: UseTreeReturnType
/** Mantine tree data */
data: TreeNodeData[]
/** renderNode keeps same signature as Mantine: receives RenderTreeNodePayload */
renderNode: (payload: RenderTreeNodePayload) => React.ReactNode
/** pixels to indent per level (matches Mantine's levelOffset) */
levelOffset?: number
/** react-window container height */
height?: string | number
/** react-window item (row) height */
itemSize?: number
/** overscan rows */
overscanCount?: number
}
/** 小工具:把 TreeNodeData 扁平化为 [node, level] 列表,依据 tree.expandedState */
function flattenByExpanded(
nodes: TreeNodeData[],
expandedState: Record<string, boolean>,
level = 0
): { node: TreeNodeData; level: number }[] {
const res: { node: TreeNodeData; level: number }[] = []
for (const node of nodes) {
res.push({ node, level })
const value = node.value
const isExpanded = !!expandedState?.[String(value)]
if (isExpanded && node.children && node.children.length > 0) {
res.push(...flattenByExpanded(node.children, expandedState, level + 1))
}
}
return res
}
/**
* VirtualizedTree - Mantine v8 compatible virtualized Tree
* - Accepts tree (useTree() result) and data: TreeNodeData[]
* - Passes RenderTreeNodePayload to renderNode exactly like Mantine
* - Uses react-window FixedSizeList for virtualization
*/
export function VirtualizedTree({
tree,
data,
renderNode,
// levelOffset = 24,
height = 400,
itemSize = 36,
overscanCount = 5,
}: VirtualizedTreeProps) {
// 扁平化数据:根据 tree.expandedState 决定哪些子节点可见
const flattened = useMemo(
() =>
flattenByExpanded(data, (tree && (tree as any).expandedState) || {}, 0),
[data, tree]
)
// react-window 的行渲染
/** 单行渲染组件 */
const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
const { node, level } = flattened[index]
// 构造 Mantine 的 RenderTreeNodePayload
const nodeValue = String(node.value)
const hasChildren = Array.isArray(node.children) && node.children.length > 0
const expanded = !!(tree && (tree as any).expandedState?.[nodeValue])
const selected = !!(
tree && (tree as any).selectedState?.includes?.(nodeValue)
)
const hovered = tree ? (tree as any).hoveredNode === nodeValue : false
// elementProps: 与 Mantine Tree 内部的 props shape 对齐,外部的 renderNode 会 spread 这个对象
const elementProps = {
className: "",
style: {
display: "flex",
alignItems: "center",
width: "100%",
} as React.CSSProperties,
onClick: (e: React.MouseEvent) => {
// 保持与 Mantine Tree 一致的基础行为:选中、展开切换由 tree 控制
// 点击行为在业务端通常由 renderNode 决定(例如中间区域用于 toggleExpanded
// 这里我们调用 tree.toggleExpanded 以保证基础交互一致(不会阻止外部覆盖)
if (tree && typeof tree.toggleExpanded === "function") {
tree.toggleExpanded(nodeValue)
}
false && console.log(e)
},
"data-selected": selected as boolean | undefined,
"data-value": nodeValue,
"data-hovered": hovered as boolean | undefined,
}
const payload: RenderTreeNodePayload = {
level,
expanded,
hasChildren,
selected,
node,
tree,
elementProps,
}
// paddingLeft 模拟 Mantine 的 levelOffset 行为renderNode 可使用 elementProps.style 覆盖)
const paddedStyle: React.CSSProperties = {
...style,
boxSizing: "border-box",
overflow: "hidden",
}
// 将 style 作为容器传入Mantine 的 renderNode 也会接受 elementProps.style
return <div style={paddedStyle}>{renderNode(payload)}</div>
}
return (
<List
rowCount={flattened.length}
style={{ height, width: "100%" }}
className="custom-scrollbar"
rowHeight={itemSize}
rowComponent={({ index, style }) => <Row index={index} style={style} />}
rowProps={{}}
overscanCount={overscanCount}></List>
)
}