import React, { useState, useMemo, useRef } from 'react'; import './TreeTable.css'; /** * Kole UI TreeTable(React) * 对齐 组件7.txt TreeTable 规范:层级缩进16px/级、展开/折叠箭头、父级汇总(叶子合计)、选择父级联动子级 * columns: [{ key, title, align? }] * data: 树形行 [{ key, children, ...字段 }] * summaryColumn: 指定数值列,父级与表尾显示叶子求和 */ function hasChildren(n) { return !!(n.children && n.children.length); } function leafSum(node, key) { if (!hasChildren(node)) return Number(node[key]) || 0; return node.children.reduce((s, c) => s + leafSum(c, key), 0); } function leafKeys(data, rowKey) { const out = []; const walk = (nodes) => nodes.forEach(n => { if (hasChildren(n)) walk(n.children); else out.push(n[rowKey]); }); walk(data); return out; } function descendantKeys(node, rowKey) { let o = []; (node.children || []).forEach(c => { o.push(c[rowKey]); o = o.concat(descendantKeys(c, rowKey)); }); return o; } function allChildrenSelected(node, set, rowKey) { if (!hasChildren(node)) return set.has(node[rowKey]); return node.children.every(c => allChildrenSelected(c, set, rowKey)); } export default function TreeTable({ columns = [], data = [], rowKey = 'key', selectable = false, selectedKeys = [], defaultExpandedKeys = [], summaryColumn = '', summary = { label: '合计' }, onSelectionChange }) { const [expanded, setExpanded] = useState(() => new Set(defaultExpandedKeys)); const [selected, setSelected] = useState(() => new Set(selectedKeys)); const visibleList = useMemo(() => { const out = []; const walk = (nodes, depth) => nodes.forEach(n => { out.push({ node: n, depth }); if (hasChildren(n) && expanded.has(n[rowKey])) walk(n.children, depth + 1); }); walk(data, 0); return out; }, [data, expanded, rowKey]); const totalOf = (key) => data.reduce((s, n) => s + leafSum(n, key), 0); const allSelected = data.length > 0 && leafKeys(data, rowKey).every(k => selected.has(k)); const leaves = leafKeys(data, rowKey); const nSel = leaves.filter(k => selected.has(k)).length; const someSelected = nSel > 0 && nSel < leaves.length; const toggleExpand = (key) => setExpanded(prev => { const n = new Set(prev); n.has(key) ? n.delete(key) : n.add(key); return n; }); const toggleCheck = (node) => { const key = node[rowKey]; const willCheck = !selected.has(key); const keys = [key, ...descendantKeys(node, rowKey)]; const next = new Set(selected); keys.forEach(k => willCheck ? next.add(k) : next.delete(k)); const propagate = (nodes) => nodes.forEach(n => { if (hasChildren(n)) { allChildrenSelected(n, next, rowKey) ? next.add(n[rowKey]) : next.delete(n[rowKey]); propagate(n.children); } }); propagate(data); setSelected(next); onSelectionChange && onSelectionChange([...next]); }; const toggleAll = () => { const next = allSelected ? [] : leafKeys(data, rowKey); setSelected(new Set(next)); onSelectionChange && onSelectionChange(next); }; const onRowClick = (node) => { setSelected(new Set([node[rowKey]])); onSelectionChange && onSelectionChange([node[rowKey]]); }; const cellValue = (node, key) => (hasChildren(node) && key === summaryColumn ? leafSum(node, key) : node[key]); return (
{selectable && } {columns.map(col => ( ))} {visibleList.map(({ node, depth }) => ( onRowClick(node)}> {selectable && ( )} {columns.map(col => ( ))} ))} {summaryColumn && ( {selectable && {columns.slice(1).map(col => ( ))} )}
el && (el.indeterminate = someSelected)} onChange={toggleAll} /> {col.title}
e.stopPropagation()} onChange={() => toggleCheck(node)} /> {col.key === columns[0].key ? (
{hasChildren(node) ? ( { e.stopPropagation(); toggleExpand(node[rowKey]); }}>▸ ) : } {node[col.key]} {hasChildren(node) && summaryColumn && (汇总)}
) : cellValue(node, col.key)}
} {summary.label || '合计'} {col.key === summaryColumn ? totalOf(col.key) : ''}
); } /* 用法示例: import TreeTable from './TreeTable'; const columns = [ { key: 'name', title: '部门/人员' }, { key: 'count', title: '人数', align: 'right' }, { key: 'budget', title: '预算(万)', align: 'right' } ]; const data = [{ key: 'tech', name: '技术中心', children: [{ key: 'fe', name: '前端组', count: 12, budget: 80 }] }]; */