From 3b9a4f0834a0800fccfec675462e6fd2f83ccd53 Mon Sep 17 00:00:00 2001 From: whyour Date: Thu, 2 Mar 2023 23:16:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=8A=B6=E6=80=81=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/services/cron.ts | 7 +- src/components/vlist.tsx | 404 ++++++++++++++++++++++++++++++ src/hooks/useTableScrollHeight.ts | 23 +- src/layouts/index.less | 23 ++ src/pages/crontab/index.tsx | 36 ++- src/pages/env/index.tsx | 265 +++++++++++--------- src/utils/index.ts | 2 +- 7 files changed, 632 insertions(+), 128 deletions(-) create mode 100644 src/components/vlist.tsx diff --git a/back/services/cron.ts b/back/services/cron.ts index 26d71843..95b00dc6 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -245,8 +245,11 @@ export default class CronService { const filterKeys: any = Object.keys(filterQuery); for (const key of filterKeys) { let q: any = {}; - if (filterKeys[key]) { - q[key] = filterKeys[key]; + if (!filterQuery[key]) continue; + if (key === 'status' && filterQuery[key].includes(2)) { + q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] }; + } else { + q[key] = filterQuery[key]; } query[Op.and].push(q); } diff --git a/src/components/vlist.tsx b/src/components/vlist.tsx new file mode 100644 index 00000000..8d129360 --- /dev/null +++ b/src/components/vlist.tsx @@ -0,0 +1,404 @@ +import React, { + useRef, + useEffect, + useContext, + createContext, + useReducer, + useState, + useMemo, +} from 'react'; +import { throttle, isNumber, debounce } from 'lodash'; + +const initialState = { + // 行高度 + rowHeight: 0, + // 当前的scrollTop + curScrollTop: 0, + // 总行数 + totalLen: 0, +}; + +function reducer(state, action) { + const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action; + + let stateScrollTop = state.curScrollTop; + switch (action.type) { + // 改变trs 即 改变渲染的列表trs + case 'changeTrs': + return { + ...state, + curScrollTop, + }; + // 更改totalLen + case 'changeTotalLen': + if (totalLen === 0) { + stateScrollTop = 0; + } + + return { + ...state, + totalLen, + curScrollTop: stateScrollTop, + }; + + case 'reset': + return { + ...state, + curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop, + }; + default: + throw new Error(); + } +} + +// ==============全局常量 ================== // +const DEFAULT_VID = 'vtable'; +const vidMap = new Map(); +let preData = 0; + +// ===============context ============== // +const ScrollContext = createContext({ + dispatch: undefined, + renderLen: 1, + start: 0, + offsetStart: 0, + // ============= + rowHeight: initialState.rowHeight, + totalLen: 0, + vid: DEFAULT_VID, +}); + +// =============组件 =================== // + +function VCell(props: any): JSX.Element { + const { children, ...restProps } = props; + + return ( + +
{children[1]}
+ + ); +} + +function VRow(props: any, ref: any): JSX.Element { + const { rowHeight } = useContext(ScrollContext); + + const { children, style, ...restProps } = props; + + const trRef = useRef(null); + + return ( + + {children} + + ); +} + +function VWrapper(props: any): JSX.Element { + const { children, ...restProps } = props; + + const { renderLen, start, dispatch, vid, totalLen } = + useContext(ScrollContext); + + const contents = useMemo(() => { + return children[1]; + }, [children]); + + const contentsLen = useMemo(() => { + return contents?.length ?? 0; + }, [contents]); + + useEffect(() => { + if (totalLen !== contentsLen) { + dispatch({ + type: 'changeTotalLen', + totalLen: contentsLen ?? 0, + }); + } + }, [contentsLen, dispatch, vid, totalLen]); + + let tempNode = null; + if (Array.isArray(contents) && contents.length) { + tempNode = [ + children[0], + contents.slice(start, start + (renderLen ?? 1)).map((item) => { + if (Array.isArray(item)) { + // 兼容antd v4.3.5 --- rc-table 7.8.1及以下 + return item[0]; + } + // 处理antd ^v4.4.0 --- rc-table ^7.8.2 + return item; + }), + ]; + } else { + tempNode = children; + } + + return {tempNode}; +} + +function VTable(props: any, otherParams): JSX.Element { + const { style, children, ...rest } = props; + const { width, ...rest_style } = style; + + const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } = + otherParams ?? {}; + + const [state, dispatch] = useReducer(reducer, { + ...initialState, + curScrollTop: scrollTop, + rowHeight, + }); + + const wrap_tableRef = useRef(null); + const tableRef = useRef(null); + + const ifChangeRef = useRef(false); + + // 数据的总条数 + const [totalLen, setTotalLen] = useState( + children[1]?.props?.data?.length ?? 0, + ); + + useEffect(() => { + setTotalLen(state.totalLen); + }, [state.totalLen]); + + // 组件卸载的清除操作 + useEffect(() => { + return () => { + vidMap.delete(vid); + }; + }, [vid]); + + // 数据变更 + useEffect(() => { + ifChangeRef.current = true; + // console.log('数据变更') + if (isNumber(children[1]?.props?.data?.length)) { + dispatch({ + type: 'changeTotalLen', + totalLen: children[1]?.props?.data?.length ?? 0, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [children[1].props.data]); + + // table总高度 + const tableHeight = useMemo(() => { + let temp: string | number = 'auto'; + + if (rowHeight && totalLen) { + temp = rowHeight * totalLen; + } + return temp; + }, [totalLen]); + + // table的scrollY值 + const [tableScrollY, setTableScrollY] = useState(0); + + // tableScrollY 随scrollY / tableHeight 进行变更 + useEffect(() => { + let temp = 0; + + if (typeof scrollY === 'string') { + temp = + (wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0; + } else { + temp = scrollY; + } + + // if (isNumber(tableHeight) && tableHeight < temp) { + // temp = tableHeight; + // } + + // 处理tableScrollY <= 0的情况 + if (temp <= 0) { + temp = 0; + } + + setTableScrollY(temp); + }, [scrollY, tableHeight]); + + // 渲染的条数 + const renderLen = useMemo(() => { + let temp = 1; + if (rowHeight && totalLen && tableScrollY) { + if (tableScrollY <= 0) { + temp = 0; + } else { + const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10; + // console.log('tempRenderLen', tempRenderLen) + // temp = tempRenderLen > totalLen ? totalLen : tempRenderLen; + temp = tempRenderLen; + } + } + + return temp; + }, [totalLen, tableScrollY]); + + // 渲染中的第一条 + let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0; + + start = start < 5 ? 0 : start - 5 + 1; + + // 偏移量 + let offsetStart = state.curScrollTop % (rowHeight * 5); + + if (start > 0) { + offsetStart = offsetStart % rowHeight; + } + // console.log(offsetStart) + // offsetStart= offsetStart%rowHeight + // 用来优化向上滚动出现的空白 + if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) { + // start -= 1 + // if (offsetStart >= rowHeight) { + // offsetStart += + // } else { + offsetStart += rowHeight * 4; + // } + } else { + start = 0; + } + + // console.log(state.curScrollTop, start, offsetStart) + + // 数据变更 操作scrollTop + useEffect(() => { + const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement; + if (ifChangeRef?.current) { + // console.log(scrollNode) + ifChangeRef.current = false; + + if (resetScrollTopWhenDataChange) { + // 重置scrollTop + if (scrollNode) { + scrollNode.scrollTop = 0; + } + + dispatch({ type: 'reset', ifScrollTopClear: true }); + } else { + // console.log(preData) + // scrollNode.scrollTop = preData+53 + // 不重置scrollTop 不清空curScrollTop + dispatch({ type: 'reset', ifScrollTopClear: false }); + } + } + + if (vidMap.has(vid)) { + vidMap.set(vid, { + ...vidMap.get(vid), + scrollNode, + }); + } + }, [totalLen, resetScrollTopWhenDataChange, vid, children]); + + useEffect(() => { + const throttleScroll = throttle((e) => { + const scrollTop: number = e?.target?.scrollTop ?? 0; + // const scrollHeight: number = e?.target?.scrollHeight ?? 0 + // const clientHeight: number = e?.target?.clientHeight ?? 0 + if (scrollTop) { + preData = scrollTop; + } + // 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点. + // 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空. + dispatch({ + type: 'changeTrs', + curScrollTop: renderLen <= totalLen ? scrollTop : 0, + }); + }, 60); + + const ref = wrap_tableRef?.current?.parentNode as HTMLElement; + + if (ref) { + ref.addEventListener('scroll', throttleScroll, { passive: true }); + } + + return () => { + ref.removeEventListener('scroll', throttleScroll); + }; + }, [renderLen, totalLen]); + + return ( +
+ + + {children} +
+
+
+ ); +} + +// ================导出=================== +export function VList(props: { + height: number; + // 唯一标识 + vid?: string; + rowHeight: number | string; + reset?: boolean; + scrollTop?: number | string; +}): any { + const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props; + + const resetScrollTopWhenDataChange = reset ?? true; + + if (!vidMap.has(vid)) { + vidMap.set(vid, { _id: vid }); + } + + return { + table: (p) => + VTable(p, { + vid, + scrollY: height, + rowHeight, + resetScrollTopWhenDataChange, + scrollTop, + }), + body: { + wrapper: VWrapper, + row: VRow, + cell: VCell, + }, + }; +} diff --git a/src/hooks/useTableScrollHeight.ts b/src/hooks/useTableScrollHeight.ts index 9d32412b..187a4eaa 100644 --- a/src/hooks/useTableScrollHeight.ts +++ b/src/hooks/useTableScrollHeight.ts @@ -1,16 +1,19 @@ import { MutableRefObject, useLayoutEffect, useState } from 'react'; -import useResizeObserver from '@react-hook/resize-observer' +import useResizeObserver from '@react-hook/resize-observer'; import { getTableScroll } from '@/utils'; -export default (target: MutableRefObject, extraHeight?: number) => { - const [height, setHeight] = useState() +export default ( + target: MutableRefObject, + extraHeight?: number, +) => { + const [height, setHeight] = useState(0); useResizeObserver(target, (entry) => { - let _targe = entry.target as any - if (!_targe.classList.contains('ant-table-wrapper')) { - _targe = entry.target.querySelector('.ant-table-wrapper') + let _target = entry.target as any; + if (!_target.classList.contains('ant-table-wrapper')) { + _target = entry.target.querySelector('.ant-table-wrapper'); } - setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement })) - }) - return height -} \ No newline at end of file + setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement })); + }); + return height; +}; diff --git a/src/layouts/index.less b/src/layouts/index.less index 8308f115..df628b4a 100644 --- a/src/layouts/index.less +++ b/src/layouts/index.less @@ -354,3 +354,26 @@ pre { white-space: break-spaces !important; padding: 0 !important; } + +.virtuallist { + .ant-table-tbody > tr > td > div { + white-space: unset !important; + } +} + +.virtuallist .ant-table-tbody > tr > td > div { + box-sizing: border-box; + white-space: nowrap; + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +} + +.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div { + overflow: inherit; +} + +.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td { + border-right: 1px solid #f0f0f0; +} diff --git a/src/pages/crontab/index.tsx b/src/pages/crontab/index.tsx index b896db99..af974b2d 100644 --- a/src/pages/crontab/index.tsx +++ b/src/pages/crontab/index.tsx @@ -53,7 +53,7 @@ import { SharedContext } from '@/layouts'; import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import { getCommandScript, parseCrontab } from '@/utils'; import { ColumnProps } from 'antd/lib/table'; -import { VList } from 'virtuallist-antd'; +import { VList } from '../../components/vlist'; const { Text, Paragraph } = Typography; const { Search } = Input; @@ -339,6 +339,7 @@ const Crontab = () => { { + setReset(false); e.stopPropagation(); runCron(record, index); }} @@ -351,6 +352,7 @@ const Crontab = () => { { + setReset(false); e.stopPropagation(); stopCron(record, index); }} @@ -362,6 +364,7 @@ const Crontab = () => { { + setReset(false); e.stopPropagation(); setLogCron({ ...record, timestamp: Date.now() }); }} @@ -405,6 +408,10 @@ const Crontab = () => { const [moreMenuActive, setMoreMenuActive] = useState(false); const tableRef = useRef(); const tableScrollHeight = useTableScrollHeight(tableRef); + const resetRef = useRef(true); + const setReset = (v) => { + resetRef.current = v; + }; const goToScriptManager = (record: any) => { const result = getCommandScript(record.command); @@ -424,9 +431,9 @@ const Crontab = () => { }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( filters, )}`; - if (sorter && sorter.field) { + if (sorter && sorter.column && sorter.order) { url += `&sorter=${JSON.stringify({ - field: sorter.field, + field: sorter.column.key, type: sorter.order === 'ascend' ? 'ASC' : 'DESC', })}`; } @@ -688,6 +695,7 @@ const Crontab = () => { items: getMenuItems(record), onClick: ({ key, domEvent }) => { domEvent.stopPropagation(); + setReset(false); action(key, record, index); }, }} @@ -723,6 +731,7 @@ const Crontab = () => { }; const onSearch = (value: string) => { + setReset(true); setSearchText(value.trim()); }; @@ -750,6 +759,10 @@ const Crontab = () => { setSelectedRowIds(selectedIds); }; + useEffect(() => { + setReset(false); + }, [selectedRowIds]); + const rowSelection = { selectedRowKeys: selectedRowIds, onChange: onSelectChange, @@ -777,6 +790,7 @@ const Crontab = () => { }; const operateCrons = (operationStatus: number) => { + setReset(false); Modal.confirm({ title: `确认${OperationName[operationStatus]}`, content: <>确认{OperationName[operationStatus]}选中的定时任务吗, @@ -803,6 +817,7 @@ const Crontab = () => { sorter: SorterResult | SorterResult[], ) => { const { current, pageSize } = pagination; + setReset(true); setPageConf({ page: current as number, size: pageSize as number, @@ -917,10 +932,22 @@ const Crontab = () => { const tabClick = (key: string) => { const view = enabledCronViews.find((x) => x.id == key); setSelectedRowIds([]); + setReset(true); setPageConf({ ...pageConf, page: 1 }); setViewConf(view ? view : null); }; + const vComponents = useMemo(() => { + return VList({ + height: tableScrollHeight, + reset: resetRef.current, + rowHeight: 69, + scrollTop: resetRef.current + ? 0 + : tableRef.current?.querySelector('.ant-table-body')?.scrollTop, + }); + }, [tableScrollHeight, resetRef.current]); + return ( { enterButton allowClear loading={loading} + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} onSearch={onSearch} />, - - - - - - 已选择 - {selectedRowIds?.length}项 - - - )} - - { - return { - index, - moveRow, - } as any; - }} - /> - +
+ {selectedRowIds.length > 0 && ( +
+ + + + + + + 已选择 + {selectedRowIds?.length}项 + +
+ )} + +
{ + return { + index, + moveRow, + } as any; + }} + /> + +