Commit 2a3f55e6 by zhaochengxiang

调整资产浏览关系图

parent 8fda5845
......@@ -16,30 +16,26 @@ const secondaryHighlightColors = ['#b36cd1', '#48b5a0', '#4a6acc', '#356eff', '#
class Relation extends React.Component {
// componentDidMount() {
// const { data, onClick } = this.props;
// if (data) {
// this.graph = init(this)(this.elem, data, onClick);
// }
// }
componentDidUpdate(prevProps, prevState) {
const { data, expandTree, onCenterClick, onExpandClick } = this.props;
const { data, expandTree, expandId, onCenterClick, onExpandClick } = this.props;
if (data) {
if (data !== prevProps.data) {
const newData = JSON.parse(JSON.stringify(data));
this.graph?.destroy();
this.graph = init(this)(this.elem, newData, onCenterClick, onExpandClick);
if ((expandId||'') === '') {
this.graph?.destroy();
this.graph = init(this)(this.elem, data, onCenterClick, onExpandClick);
} else {
if (data && (data.children||[]).length > 0) {
this.graph?.updateChild(data, 'root');
}
}
}
}
if (expandTree !== prevProps.expandTree && prevProps.expandTree!==null) {
if (!this.elem || !this.elem.scrollWidth || !this.elem.scrollHeight) return;
this.graph?.changeSize(this.elem.scrollWidth, this.elem.scrollHeight);
if (!this.elem || !this.elem.clientWidth || !this.elem.clientHeight) return;
this.graph?.changeSize(this.elem.clientWidth, this.elem.clientHeight);
}
}
......@@ -379,6 +375,7 @@ const init = (ctx) => function (container, data, onCenterClick, onExpandClick) {
height,
linkCenter: true,
plugins: [tooltip],
animate: false,
modes: {
default: [
'drag-canvas',
......@@ -406,6 +403,9 @@ const init = (ctx) => function (container, data, onCenterClick, onExpandClick) {
rankSep: 150,
radial: true,
},
//fitView不能放在render后面, 否则调用updateChild会刷新界面
fitView: (data && (data?.children||[]).length > 0) ? true : false,
});
const fittingString = (str, maxWidth, fontSize) => {
......@@ -441,9 +441,7 @@ const init = (ctx) => function (container, data, onCenterClick, onExpandClick) {
graph.data(data);
graph.render();
if (data && (data?.children||[]).length>0) {
graph.fitView();
} else {
if (data && (data?.children||[]).length === 0) {
//只有一个节点的时候 居中显示
graph.fitCenter();
}
......
......@@ -184,7 +184,7 @@ const RelationContainer = (props) => {
return (
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
<Relation data={relationData} expandTree={expandTree} onCenterClick={onCenterClick} onExpandClick={onExpandClick} />
<Relation data={relationData} expandId={nodeParams?.expandId} expandTree={expandTree} onCenterClick={onCenterClick} onExpandClick={onExpandClick} />
<div className='title-text' style={{ position: 'absolute', left: 15, top: 24, fontWeight: 'bold' }} >关系图</div>
</div>
);
......
......@@ -62,7 +62,7 @@ const AssetBrowse = (props) => {
<Separate height={15} />
<div className='flex' style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
<div style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
<RelationContainer reference={reference} nodeParams={nodeParams} onChange={onRelationChange} expandTree={expandTree} />
<RelationContainer reference={reference} nodeParams={nodeParams} onChange={onRelationChange} expandTree={expandTree} />
</div>
<Separate width={15} />
<div style={{ flex: 1, overflow: 'hidden' }}>
......
/*
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
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