Commit 14a68e30 by zhaochengxiang

模型增加虚拟滚动

parent e4fb0d89
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
"local-storage": "^2.0.0", "local-storage": "^2.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-contexify": "^5.0.0", "react-contexify": "^5.0.0",
"react-contextmenu": "^2.14.0",
"react-data-grid": "^7.0.0-beta.12",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
......
import CaretDownOutlined from '@ant-design/icons/CaretDownOutlined';
import CaretUpOutlined from '@ant-design/icons/CaretUpOutlined';
import classNames from 'classnames'
const prefixCls = 'yy'
export const upNode = (active:boolean) => (
<CaretUpOutlined
className={classNames(`${prefixCls}-table-column-sorter-up anticon-caret-up anticon`, {
active,
})}
/>
);
export const downNode = (active:boolean) => (
<CaretDownOutlined
className={classNames(`${prefixCls}-table-column-sorter-down anticon-caret-down anticon`, {
active,
})}
/>
);
.virtual-table {
.react-contextmenu-wrapper {
display: contents;
}
.contextMenu {
border: 1px solid black;
.react-contextmenu {
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
color: #373a3c;
font-size: 16px;
margin-block-start: 2px;
margin-block-end: 0;
margin-inline-start: 0;
margin-inline-end: 0;
min-inline-size: 160px;
outline: none;
opacity: 0;
padding-block: 5px;
padding-inline: 0;
pointer-events: none;
text-align: start;
transition: opacity 250ms ease !important;
}
.react-contextmenu.react-contextmenu--visible {
opacity: 1;
pointer-events: auto;
}
.react-contextmenu-item {
background: 0 0;
border: 0;
color: #373a3c;
cursor: pointer;
font-weight: 400;
line-height: 1.5;
padding-block: 3px;
padding-inline: 20px;
text-align: inherit;
white-space: nowrap;
}
.react-contextmenu-item.react-contextmenu-item--active,
.react-contextmenu-item.react-contextmenu-item--selected {
color: #fff;
background-color: #20a0ff;
border-color: #20a0ff;
text-decoration: none;
}
.react-contextmenu-item.react-contextmenu-item--disabled,
.react-contextmenu-item.react-contextmenu-item--disabled:hover {
background-color: transparent;
border-color: rgba(0, 0, 0, 0.15);
color: #878a8c;
}
.react-contextmenu-item--divider {
border-block-end: 1px solid rgba(0, 0, 0, 0.15);
cursor: inherit;
margin-block-end: 3px;
padding-block: 2px;
padding-inline: 0;
}
.react-contextmenu-item--divider:hover {
background-color: transparent;
border-color: rgba(0, 0, 0, 0.15);
}
.react-contextmenu-item.react-contextmenu-submenu {
padding: 0;
}
.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item::after {
content: '▶';
display: inline-block;
position: absolute;
inset-inline-end: 7px;
}
}
.loadMoreRowsClassname {
inline-size: 180px;
padding-block: 8px;
padding-inline: 16px;
position: absolute;
inset-block-end: 8px;
inset-inline-end: 8px;
color: white;
line-height: 35px;
background: rgb(0 0 0 / 0.6);
}
}
\ No newline at end of file
import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ContextMenu, ContextMenuTrigger } from 'react-contextmenu';
import DataGrid, { Column, DataGridProps, Row as GridRow, RowsChangeData, SelectCellFormatter, SortColumn, SortIconProps } from 'react-data-grid';
import type { RowRendererProps, DataGridHandle } from 'react-data-grid';
import classNames from 'classnames';
import { nanoid } from 'nanoid';
import { downNode, upNode } from './test-table-helper';
import './test-table.less';
export enum RowAction {
None, Expand, Select
}
export enum RowType {
None, Detail
}
export interface RowData {
id: string,
__pid__?: string
__row__?: any
__type__?: RowType
__expanded__?: boolean
__selected__?: boolean
__action__?: RowAction
}
interface Props<Row> {
contextMenu?: {
id: string
menu: ReactElement
}
gridRef?: React.RefObject<DataGridHandle>
ctxRef?: React.RefObject<{
getSelected?: () => string[]
}>
// setRows?: (rows: any) => void
expandRow?: (pid: string, row: Row) => React.ReactElement
getComparator?: (sortColumn: string) => (a: Row, b: Row) => number
loadMoreRows?: (length: number) => Promise<Row[]> | undefined
}
function RowRenderer<Row, SR>(id: string) {
return (props: RowRendererProps<Row, SR>) => (
<ContextMenuTrigger id={id} collect={() => ({ rowIdx: props.rowIdx })}>
<GridRow {...props} />
</ContextMenuTrigger>
);
}
export function getExpandingCol<Row extends RowData, SR>({ colSpan, expand }: {
colSpan: () => number
expand?: (pid: string, row: Row) => React.ReactElement
}) {
const col: Column<Row, SR> = {
key: 'expanded',
name: '',
minWidth: 30,
width: 30,
colSpan(args) {
return args.type === 'ROW' && args.row.__type__ === RowType.Detail ? colSpan() : undefined;
},
cellClass(row) {
return row.__type__ === RowType.Detail ? 'detail' : undefined;
},
formatter({ row, isCellSelected, onRowChange }) {
if (row.__type__ === RowType.Detail) {
if (expand) {
return expand(row.__pid__!, row.__row__)
}
return (
<div>{row.__pid__}</div>
);
}
// 展开收起
return (
<div style={{ cursor: 'pointer', color: isCellSelected ? '#777' : '#ccc' }}
onClick={() => {
onRowChange({
...row,
__expanded__: !row.__expanded__,
__action__: RowAction.Expand
});
}}
>
{row.__expanded__ ? '\u25BC' : '\u25B6'}
</div>
);
}
}
return col
}
export function getSelectCol<Row extends RowData, SR>(selectAll: () => void, checkAll: boolean) {
const col: Column<Row, SR> = {
key: 'select',
name: '',
headerRenderer({ isCellSelected }) {
return (
<SelectCellFormatter
value={checkAll}
onChange={selectAll}
isCellSelected={isCellSelected}
/>
)
},
width: 80,
formatter({ row, onRowChange, isCellSelected }) {
// debug
// return <b>{JSON.stringify({ isCellSelected, selected: !!row.__selected__ })}</b>
// 选中
return (
<SelectCellFormatter
value={!!row.__selected__}
onChange={() => {
onRowChange({
...row,
__selected__: !row.__selected__,
__action__: RowAction.Select
});
}}
isCellSelected={isCellSelected}
/>
);
},
}
return col
}
function FC<Row extends RowData, SR, K extends React.Key = React.Key>(props: DataGridProps<Row, SR, K> & Props<Row>) {
const {
gridRef, ctxRef,
expandRow,
getComparator,
loadMoreRows,
contextMenu, columns, rows, ...rest } = props
const rowKeyGetter = (row: Row): K => {
return row.id as K;
}
const [checkAll, setCheckAll] = useState(false)
// 初始化onRowsChange
const onRowsChange = useCallback((rows: RowData[], { indexes }: RowsChangeData<Row, SR>) => {
const row = rows[indexes[0]];
if (row.__action__ === RowAction.Expand) {
if (!row.__expanded__) {
rows.splice(indexes[0] + 1, 1); // 删除下一行
} else {
rows.splice(indexes[0] + 1, 0, { // 插入下一行
id: nanoid(), __type__: RowType.Detail, __pid__: row.id, __row__: row,
});
}
} else if (row.__action__ === RowAction.Select) {
// nothing to do, just update
}
_setRows(rows as Row[]);
}, [])
// 已排序行
const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>([]); // 排序列图标状态
const [_rows, _setRows] = useState<readonly Row[]>([])
useEffect(() => {
if (sortColumns.length === 0) {
_setRows(rows);
} else {
_setRows([...rows]
// .filter(item => item.__type__ !== RowType.Detail)
.sort((a, b) => {
for (const sort of sortColumns) {
const comparator = getComparator ? getComparator(sort.columnKey) : GetComparator(sort.columnKey);
const compResult = comparator(a, b);
if (compResult !== 0) {
return sort.direction === 'ASC' ? compResult : -compResult;
}
}
return 0;
}));
setCheckAll(false)
}
}, [rows, sortColumns, getComparator]);
// 组装功能s列
const cols = useMemo(() => {
if (expandRow) {
const cols: readonly Column<Row, SR>[] = [
getExpandingCol<Row, SR>({
colSpan: () => cols.length, expand: expandRow
}),
getSelectCol(() => { // 全选
const rows = []
for (const row of _rows) {
const _row = { ...row, __selected__: !checkAll }
rows.push(_row)
}
_setRows(rows)
setCheckAll(!checkAll)
}, checkAll),
...columns,
]
return cols
}
return columns
}, [columns, expandRow, _rows, checkAll, setCheckAll])
// 取得选中
const getSelected = useCallback(() => {
const selected = _rows.filter((item) => item.__selected__ === true).map(item => item.id)
console.debug('selected', selected, selected.length)
return selected
}, [_rows])
if (ctxRef?.current) {
ctxRef.current.getSelected = getSelected
}
// 处理滚动
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
if (!isAtBottom(event)) return;
loadMoreRows?.(rows.length)
}, [loadMoreRows, rows])
const contextMenuId = contextMenu?.id ?? nanoid()
return (
<>
<DataGrid
className='virtual-table'
ref={gridRef}
{...rest}
columns={cols}
rows={_rows}
rowKeyGetter={rowKeyGetter}
components={{
sortIcon: SortIcon,
rowRenderer: contextMenu ? RowRenderer<Row, SR>(contextMenuId) : undefined
}}
onRowsChange={onRowsChange}
// headerRowHeight={45}
rowHeight={(args) => (args.type === 'ROW' && args.row.__type__ === RowType.Detail ? 300 : 45)}
// defaultColumnOptions={{
// sortable: true,
// resizable: true
// }}
// 排序
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
// 滚动
onScroll={loadMoreRows ? handleScroll : undefined}
/>
{contextMenu && createPortal(
<div className='contextMenu'>
<ContextMenu id={contextMenuId} rtl={false}>
{contextMenu.menu}
</ContextMenu>
</div>,
document.body
)}
</>
);
}
export default FC
function GetComparator(sortColumn: string): (a: any, b: any) => number {
return (a, b) => {
const aVal = a[sortColumn], bVal = b[sortColumn]
if (aVal !== undefined && bVal !== undefined)
return aVal.localeCompare(bVal);
return 0
};
}
function isAtBottom({ currentTarget }: React.UIEvent<HTMLDivElement>): boolean {
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight;
}
function SortIcon({ sortDirection }: SortIconProps) {
const asc = sortDirection === 'ASC'
const desc = sortDirection === 'DESC'
return <div className="yy-table-column-sorter yy-table-column-sorter-full">
<span className="yy-table-column-sorter-inner">
{upNode(asc)}
{downNode(desc)}
</span>
</div>
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment