Commit 532f07d4 by zhaochengxiang

支持t sts

parent 634f1ed4
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.12",
"ahooks": "^3.1.7", "ahooks": "^3.1.7",
"antd": "^4.18.2", "antd": "^4.18.2",
"axios": "^0.19.0", "axios": "^0.19.0",
...@@ -35,8 +39,10 @@ ...@@ -35,8 +39,10 @@
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.3",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-saga": "^1.0.5", "redux-saga": "^1.0.5",
"rxjs": "^7.5.4",
"showdown": "^1.9.1", "showdown": "^1.9.1",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"typescript": "^4.6.2",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
......
declare const rctable: any;
declare module 'rc-table' {
export default rctable;
}
declare const nprogress: any;
declare module 'nprogress' {
export default nprogress;
}
declare module 'react-resizable' {
export const Resizable:any
}
declare module 'd3v3'
declare module '*' // for import jsx
\ No newline at end of file
/// <reference types="react-scripts" />
/*
https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript
*/
export function getScrollbarWidth() {
// Creating invisible container
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll'; // forcing scrollbar to appear
(outer.style as any).msOverflowStyle = 'scrollbar'; // needed for WinJS apps
document.body.appendChild(outer);
// Creating inner element and placing it in the container
const inner = document.createElement('div');
outer.appendChild(inner);
// Calculating difference between container's full width and the child width
const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);
// Removing temporary elements from the DOM
outer.parentNode?.removeChild(outer);
return scrollbarWidth;
}
\ No newline at end of file
.virtual-table .ant-table-container:before, .virtual-table .ant-table-container:after {
display: none;
}
.virtual-table-tbody>div.virtual-table-row>div {
display: inline-block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
/* padding: 16px; */
border-bottom: 1px solid #e8e8e8;
background: #FFF;
}
[data-theme="dark"] .virtual-table-cell {
box-sizing: border-box;
/* padding: 16px; */
border-bottom: 1px solid #303030;
background: #141414;
}
.ant-table-thead > tr > th.w0::before {
width: 0 !important;
}
.virtual-table-tbody>div.virtual-table-row:hover>div {
background-color: #fafafa;
}
/*
https://github.com/ant-design/ant-design/blob/master/components/table/demo/resizable-column.md
*/
.virtual-table .react-resizable {
position: relative;
background-clip: padding-box;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.virtual-table .react-resizable-handle {
position: absolute;
right: -5px;
bottom: 0;
z-index: 1;
width: 10px;
height: 100%;
cursor: col-resize;
}
.virtual-table .ant-table-header {
background-color: rgba(0, 0, 0, 0.06);
}
.virtual-table .virtual-table-tbody>div.virtual-table-row>div.virtual-table-cell {
padding: 10px;
}
.virtual-table .virtual-table-tbody>div.virtual-table-row:hover>div.virtual-table-cell {
background: #fafafa;
}
.virtual-table .footer {
text-align: center;
}
.virtual-table .footer {
text-align: center;
margin-top: 10px;
}
.virtual-table .footer > span {
background-color: #fafafa;
padding: 10px;
border-radius: 8px;
}
\ No newline at end of file
import { useState, useRef, useEffect, Key, useMemo } from 'react';
import { useVirtualList } from 'ahooks';
import { Resizable } from 'react-resizable';
import ResizeObserver, { SizeInfo } from 'rc-resize-observer';
import classNames from 'classnames';
import { Button, Checkbox, Spin, Table, Typography } from 'antd';
import { getScrollbarWidth } from './helper';
import './table.css'
import { Subject } from 'rxjs';
import { throttle, debounceTime } from 'rxjs/operators';
export type FetchMoreFunc = (ds?: any) => Promise<void> // 返回空, 不需要在promise返回数据
export interface Ctx {
scrollTo?: (index: number) => void
}
const itemHeight = 45
const minWidth = 50
const scrollbarSize = getScrollbarWidth()
const headerHeight = 55
const defaultWidth = 150
const ColTypes = { Select: 1, Padding: 2, Scrollbar: 3 }
const PaddingCol = {
type: ColTypes.Padding,
width: 0
}
const ScrollbarCol = {
type: ColTypes.Scrollbar,
width: scrollbarSize
}
function GetRowWidth(columns: any[]) {
const rowWidth = columns.reduce((preVal, col) => {
if (col.type === ColTypes.Scrollbar) {
return preVal
}
return (col._width as number) + preVal
}, 0)
return rowWidth
}
function SetColWidth(tableWidth: number, columns: any[]) {
let totalWidth = 0, noneWidth = 0
for (const col of columns) {
if (typeof col.width === 'number') {
totalWidth += col.width
} else {
noneWidth++
}
}
let width = 0
if (noneWidth > 0) {
if (tableWidth > totalWidth) {
width = Math.floor((tableWidth - totalWidth) / noneWidth)
width = width > defaultWidth ? width : defaultWidth
}
console.debug(tableWidth, width)
for (const col of columns) {
col._width = col.width ?? width
}
}
}
// 可变列宽
const ResizableTitle = (props: any) => {
const { onResize, width, type, ...restProps } = props;
if (!width) {
return <th {...restProps} />;
}
if (ColTypes.Padding === type) {
return <th className="w0" />;
}
if (ColTypes.Scrollbar === type) {
return <th className="" />;
}
if (ColTypes.Select === type) {
return <th {...restProps} className="ant-table-cell ant-table-selection-column" />;
}
return (
<Resizable
width={width}
height={0}
handle={
<span
className="react-resizable-handle"
onClick={e => {
e.stopPropagation();
}}
/>
}
onResize={onResize}
draggableOpts={{ enableUserSelectHack: false }}
>
<th {...restProps} />
</Resizable>
);
};
export default function VirtualTable(props: Parameters<typeof Table>[0] & { height?: any, fetchMore?: FetchMoreFunc, hasMore: boolean, ctx: React.MutableRefObject<Ctx> }) {
const ref = useRef(null)
const curIndex = useRef(-1)
const { columns, height, dataSource: ds, fetchMore, hasMore, rowSelection, ctx, ...rest } = props;
const [dataSource, setDataSource] = useState<any>();
const [tableSize, setTableSize] = useState<SizeInfo>();
const [scrollbarWidth, setScrollbarWidth] = useState(0)
const [cols, setCols] = useState<any[]>()
const [loading, setLoading] = useState(false)
const [selectedRows, setSelectedRows] = useState<[string, any][]>()
const initCols = useRef<Subject<() => void>>()
const _initCols = useRef(true) // 初始列
const _scrollbarWidth = useRef(0) // 产生滚动条
const scrollToBottom = useRef<Subject<any>>()
useEffect(() => {
initCols.current = new Subject()
const subscription = initCols.current.pipe(debounceTime(300)).subscribe((cb)=>cb())
return () => {
subscription.unsubscribe()
}
}, [])
// 判断是否有滚动条 console.debug(tableSize.height,data.length * itemHeight)
useEffect(() => {
if (!!tableSize && !!dataSource) {
setScrollbarWidth((tableSize.height - headerHeight) < dataSource.length * itemHeight ? scrollbarSize : 0)
}
}, [dataSource, tableSize])
// 内部数据,刷新table
useEffect(() => {
setDataSource(ds)
}, [ds])
// 选中数量
const selected = useMemo(() => {
if (selectedRows) {
const keys: Key[] = [], rows: any[] = []
selectedRows?.forEach(([k, v]) => {
keys.push(k)
rows.push(v)
})
rowSelection?.onChange?.(keys, rows)
return keys.length
}
return 0
}, [selectedRows])
// column 列宽改变事件
const handleResize = (index: number) => (e: any, { size }: any) => {
// 最小宽度
if (size.width < minWidth) {
return
}
curIndex.current = index
setCols((pre: any) => {
const nextColumns = [...pre];
nextColumns[index] = {
...nextColumns[index],
_width: size.width,
};
// const div = ref.current! as HTMLDivElement
// SetTableWidth(div, nextColumns)
return nextColumns;
});
};
// 是否全选
const checkedMap = useRef<{ [key: string]: any }>({})
const Checked = ({ id, data, checked: ck }: { id: string, data: any, checked: boolean }) => {
useEffect(() => {
// console.debug(iid, checkedMap.current[iid], ck)
setChecked(ck)
}, [ck])
const [checked, setChecked] = useState(false)
return <Checkbox
checked={checked}
onChange={(e) => {
setChecked(e.target.checked)
if (e.target.checked) {
checkedMap.current[id] = data
} else {
delete checkedMap.current[id]
}
setSelectedRows(Object.entries(checkedMap.current))
}}></Checkbox>
}
const selRowCol = {
title: <Checkbox
onChange={(e) => {
if (e.target.checked) {
setDataSource((pre: any) => {
pre?.map((item: any) => {
checkedMap.current[item.id] = item
})
setSelectedRows(Object.entries(checkedMap.current))
return [...pre]
})
} else {
checkedMap.current = {}
setSelectedRows(Object.entries(checkedMap.current))
setDataSource((pre: any) => ([...pre]))
}
}}></Checkbox>,
dataIndex: 'id',
width: 36,
type: ColTypes.Select,
render: (id: any, data: any) => {
const checked = checkedMap.current[id]
return <Checked id={id} data={data} checked={!!checked} />
}
}
useEffect(() => {
const scrollbarWidthChanged = (_scrollbarWidth.current !== scrollbarWidth)
const __initCols = ()=>{
setCols((__cols) => {
if (!!columns) {
let _cols = [...columns, PaddingCol]
if (!!rowSelection) { //是否全选
_cols.unshift(selRowCol)
}
if (scrollbarWidth > 0) {
_cols.push(ScrollbarCol)
}
if (!!dataSource && !!tableSize) {
_initCols.current = false
SetColWidth(tableSize.width, _cols)
return _cols
}
}
return __cols
})
}
if (_initCols.current) {
initCols.current?.next(__initCols) // 避免多次初始化
} else if (scrollbarWidthChanged) {
__initCols()
}
_scrollbarWidth.current = scrollbarWidth
}, [columns, dataSource, rowSelection, tableSize, scrollbarWidth])
// 合并渲染列
let _padding: any = undefined // padding column
const mergedColumns: any[] = (cols ?? []).map((column, index) => {
let _width = column._width
const _column = {
...column, width: _width,
onHeaderCell: (column: any) => ({
width: column.width,
type: column.type,
onResize: handleResize(index),
}),
};
if (column.type === ColTypes.Padding) {
_padding = _column
}
return _column
});
let rowWidth = !!ref.current ? GetRowWidth(mergedColumns) : 0;
if (!!_padding && !!tableSize) {
let _tableWidth = tableSize.width - scrollbarWidth
const width = Math.floor(_tableWidth - rowWidth)
if (width >= 0) {
_padding.width = width// === 0 ? `${width}px` : width
rowWidth = _tableWidth
}
}
// 虚拟滚动
const RenderVirtualList = (rawData: object[], { /* scrollbarSize, */ ref, onScroll }: any) => {
const containerRef: any = useRef();
const wrapperRef: any = useRef();
const [list, scrollTo] = useVirtualList(rawData, {
containerTarget: containerRef,
wrapperTarget: wrapperRef,
itemHeight,
overscan: 5,
});
ctx.current.scrollTo = scrollTo
return (
<div
ref={containerRef}
style={{ height, overflow: 'auto', width: tableSize?.width }}
onScroll={(e: any) => {
const scrollLeft = e.target.scrollLeft
onScroll({ scrollLeft });
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
if (scrollTop + clientHeight === scrollHeight) {
// console.log("reached bottom",scrollTop, scrollHeight, clientHeight );
hasMore && scrollToBottom.current?.next(ds)
}
}}
>
<div
ref={wrapperRef}
style={{ width: rowWidth, }}
className="virtual-table-tbody"
>
{list?.map((item: any) => (
<div
key={item.index}
style={{ height: itemHeight }}
className="virtual-table-row"
>
{mergedColumns?./* filter(col => col?.type !== ColTypes.Padding). */map((col: any, colIndex: number) => {
const { width } = mergedColumns[colIndex];
let cell = item.data[col.dataIndex]
if (col?.type === ColTypes.Scrollbar) {
return (
<div
key={colIndex}></div>
)
}
else if (col?.type === ColTypes.Padding) {
return (
<div
key={colIndex}
style={{ width, height: itemHeight }}></div>
)
}
else if (col.render) {
cell = col.render(cell, item.data)
}
return (
<div
key={colIndex}
style={{ width, height: itemHeight }}
className={classNames('virtual-table-cell', {
'virtual-table-cell-last': colIndex === mergedColumns.length - 1,
})}
>
{/* {item.index}-{colIndex} */} {cell}
</div>
)
})}
</div>
))}
</div>
</div>
)
}
// 监测滚动
useEffect(() => {
scrollToBottom.current = new Subject()
const getPromise = (ds: any) => {
setLoading(true)
const p = fetchMore ? fetchMore(ds) : Promise.resolve()
return p
.catch(console.error)
.finally(() => setLoading(false))
}
const subscription = scrollToBottom.current.pipe(throttle(getPromise)).subscribe()
return () => {
console.debug('_scrollToBottom unsubscribe')
subscription.unsubscribe()
}
}, [fetchMore])
// 加载初始数据
useEffect(() => {
scrollToBottom.current?.next(undefined)
}, [])
return (
<div className="virtual-table">
<ResizeObserver
onResize={(size) => {
setTableSize(size);
}}
>
<Table
ref={ref}
{...rest}
columns={mergedColumns as any}
pagination={false}
dataSource={dataSource}
scroll={{ y: 0 }}
components={{
header: {
cell: ResizableTitle,
},
body: RenderVirtualList as any,
}}
// onChange={(_1, _2, sorter: any) => {
// // console.debug(sorter)
// }}
>
</Table>
</ResizeObserver>
<div className="footer">
<span >
{hasMore ? (
loading ? (
<Button type="link" disabled>
<Spin size="small" />
</Button>) : (
<Button type="link"
onClick={() => {
scrollToBottom.current?.next(ds)
}}>加载更多</Button>
)
) : (
<Button type="link" disabled>
加载完毕!
</Button>
)}
<Typography.Text>{dataSource?.length}条数据</Typography.Text>
{selected > 0 && <Typography.Text>,已选中{selected}条数据</Typography.Text>}
</span>
</div>
{/* {JSON.stringify(checked)} */}
</div>
);
}
\ No newline at end of file
{
"compilerOptions": {
"target": "es2016",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
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