Match-id-d9d1111f6e1761d1de56351a998c5c7fff5cb483

This commit is contained in:
* 2022-04-07 11:51:43 +08:00 committed by *
parent f5600dd58c
commit d490707e78
4 changed files with 142 additions and 54 deletions

View File

@ -0,0 +1,11 @@
.container {
position: relative;
overflow-y: auto;
height: 100%;
width: 100%;
}
.item {
position: absolute;
width: 100%;
}

View File

@ -0,0 +1,78 @@
import { useState, useRef, useEffect } from 'horizon';
import styles from './VList.less';
interface IProps<T extends { id: string }> {
data: T[],
width: number, // 暂时未用到,当需要支持横向滚动时使用
height: number, // VList 的高度
children: any, // horizon 组件,组件类型是 T
itemHeight: number,
scrollIndex?: number,
onRendered:(renderInfo: renderInfoType) => void;
filter?(data: T): boolean, // false 表示该行不显示
}
const defaultRenderInfo = {
visibleItems: ([] as string[])
};
export type renderInfoType = typeof defaultRenderInfo;
export function VList<T extends { id: string }>(props: IProps<T>) {
const {
data,
height,
children,
itemHeight,
scrollIndex = 0,
filter,
onRendered,
} = props;
const [scrollTop, setScrollTop] = useState(scrollIndex * itemHeight);
const renderInfo = useRef({visibleItems: []});
useEffect(() => {
onRendered(renderInfo.current);
});
const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop;
setScrollTop(scrollTop);
};
const showList: any[] = [];
let totalHeight = 0;
// 顶部冗余
const startShowTopValue = Math.max(scrollTop - itemHeight * 4, 0);
// 底部冗余
const showNum = Math.floor(height / itemHeight) + 4;
// 如果最后一个显示不全,不统计在显示 ids 内
const maxTop = scrollTop + height - itemHeight;
// 清空记录的上次渲染的数据
renderInfo.current.visibleItems.length = 0;
data.forEach((item, i) => {
if (filter && !filter(item)) {
return;
}
if (totalHeight >= startShowTopValue && showList.length <= showNum) {
showList.push(
<div
key={String(item.id)}
className={styles.item}
style={{ transform: `translateY(${totalHeight}px)` }} >
{children(i, item)}
</div>
);
if (totalHeight >= scrollTop && totalHeight < maxTop) {
renderInfo.current.visibleItems.push(item);
}
}
totalHeight += itemHeight;
});
return (
<div className={styles.container} onScroll={handleScroll}>
{showList}
<div style={{ marginTop: totalHeight }} />
</div>
);
}

View File

@ -1,10 +1,7 @@
@import 'assets.less'; @import 'assets.less';
.treeContainer { .treeContainer {
position: relative;
width: 100%;
height: 100%; height: 100%;
overflow-y: auto;
.treeItem { .treeItem {
width: 100%; width: 100%;

View File

@ -1,7 +1,9 @@
import { useState } from 'horizon'; import { useState, useEffect } from 'horizon';
import styles from './VTree.less'; import styles from './VTree.less';
import Arrow from '../svgs/Arrow'; import Triangle from '../svgs/Triangle';
import { createRegExp } from './../utils'; import { createRegExp } from './../utils';
import { SizeObserver } from './SizeObserver';
import { renderInfoType, VList } from './VList';
export interface IData { export interface IData {
id: string; id: string;
@ -11,7 +13,6 @@ export interface IData {
} }
type IItem = { type IItem = {
style: any,
hasChild: boolean, hasChild: boolean,
onCollapse: (id: string) => void, onCollapse: (id: string) => void,
onClick: (id: string) => void, onClick: (id: string) => void,
@ -20,15 +21,11 @@ type IItem = {
highlightValue: string, highlightValue: string,
} & IData } & IData
// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值
const showNum = 70;
const lineHeight = 18;
const indentationLength = 20; const indentationLength = 20;
function Item(props: IItem) { function Item(props: IItem) {
const { const {
name, name,
style,
userKey, userKey,
hasChild, hasChild,
onCollapse, onCollapse,
@ -37,17 +34,17 @@ function Item(props: IItem) {
indentation, indentation,
onClick, onClick,
isSelect, isSelect,
highlightValue, highlightValue = '',
} = props; } = props;
const isShowKey = userKey !== ''; const isShowKey = userKey !== '';
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : ''; const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
const handleClickCollapse = () => { const handleClickCollapse = () => {
onCollapse(id); onCollapse(id);
}; };
const handleClick = () => { const handleClick = () => {
onClick(id); onClick(id);
}; };
const itemAttr: any = { style, className: styles.treeItem, onClick: handleClick }; const itemAttr: any = { className: styles.treeItem, onClick: handleClick };
if (isSelect) { if (isSelect) {
itemAttr.tabIndex = 0; itemAttr.tabIndex = 0;
itemAttr.className = styles.treeItem + ' ' + styles.select; itemAttr.className = styles.treeItem + ' ' + styles.select;
@ -93,10 +90,17 @@ function Item(props: IItem) {
); );
} }
function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) { function VTree({ data, highlightValue, selectedId, onRendered }: {
const [scrollTop, setScrollTop] = useState(0); data: IData[],
highlightValue: string,
selectedId: number,
onRendered: (renderInfo: renderInfoType) => void
}) {
const [collapseNode, setCollapseNode] = useState(new Set<string>()); const [collapseNode, setCollapseNode] = useState(new Set<string>());
const [selectItem, setSelectItem] = useState(); const [selectItem, setSelectItem] = useState(selectedId);
useEffect(() => {
setSelectItem(selectedId);
}, [selectedId]);
const changeCollapseNode = (id: string) => { const changeCollapseNode = (id: string) => {
const nodes = new Set<string>(); const nodes = new Set<string>();
collapseNode.forEach(value => { collapseNode.forEach(value => {
@ -112,17 +116,14 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string
const handleClickItem = (id: string) => { const handleClickItem = (id: string) => {
setSelectItem(id); setSelectItem(id);
}; };
const showList: any[] = [];
let totalHeight = 0;
let currentCollapseIndentation: null | number = null; let currentCollapseIndentation: null | number = null;
data.forEach((item, index) => { // 过滤掉折叠的 item不展示在 VList 中
// 存在未处理完的收起节点 const filter = (item: IData) => {
if (currentCollapseIndentation !== null) { if (currentCollapseIndentation !== null) {
const indentation = item.indentation;
// 缩进更大,不显示 // 缩进更大,不显示
if (indentation > currentCollapseIndentation) { if (item.indentation > currentCollapseIndentation) {
return; return false;
} else { } else {
// 缩进小,说明完成了该收起节点的子节点处理。 // 缩进小,说明完成了该收起节点的子节点处理。
currentCollapseIndentation = null; currentCollapseIndentation = null;
@ -130,44 +131,45 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string
} }
const id = item.id; const id = item.id;
const isCollapsed = collapseNode.has(id); const isCollapsed = collapseNode.has(id);
if (totalHeight >= scrollTop && showList.length <= showNum) {
const nextItem = data[index + 1];
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const hasChild = nextItem ? nextItem.indentation > item.indentation : false;
showList.push(
<Item
key={id}
hasChild={hasChild}
style={{
transform: `translateY(${totalHeight}px)`,
}}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
isCollapsed={isCollapsed}
isSelect={id === selectItem}
highlightValue={highlightValue}
{...item} />
);
}
totalHeight = totalHeight + lineHeight;
if (isCollapsed) { if (isCollapsed) {
// 该节点需要收起子节点 // 该节点需要收起子节点
currentCollapseIndentation = item.indentation; currentCollapseIndentation = item.indentation;
} }
}); return true;
const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop;
// 顶部留 100px 冗余高度
setScrollTop(Math.max(scrollTop - 100, 0));
}; };
return ( return (
<div className={styles.treeContainer} onScroll={handleScroll}> <SizeObserver className={styles.treeContainer}>
{showList} {(width, height) => {
{/* 确保有足够的高度 */} return (
<div style={{ marginTop: totalHeight }} /> <VList
</div> data={data}
width={width}
height={height}
itemHeight={18}
scrollIndex={data.indexOf(selectItem)}
filter={filter}
onRendered={onRendered}
>
{(index: number, item: IData) => {
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const nextItem = data[index + 1];
const hasChild = nextItem && nextItem.indentation > item.indentation;
return (
<Item
hasChild={hasChild}
isCollapsed={collapseNode.has(item.id)}
isSelect={selectItem === item.id}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
highlightValue={highlightValue}
{...item} />
);
}}
</VList>
);
}}
</SizeObserver>
); );
} }