修复定时任务状态筛选

This commit is contained in:
whyour 2023-03-02 23:16:18 +08:00
parent 67bc305950
commit 3b9a4f0834
7 changed files with 632 additions and 128 deletions

View File

@ -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);
}

404
src/components/vlist.tsx Normal file
View File

@ -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 (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
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 <tbody {...restProps}>{tempNode}</tbody>;
}
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<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
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<string | number>(() => {
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<number>(() => {
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 (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
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,
},
};
}

View File

@ -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 <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
const [height, setHeight] = useState<number>()
export default <T extends HTMLElement>(
target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(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
}
setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
});
return height;
};

View File

@ -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;
}

View File

@ -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 = () => {
<Tooltip title={isPc ? '运行' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
runCron(record, index);
}}
@ -351,6 +352,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '停止' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
stopCron(record, index);
}}
@ -362,6 +364,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() });
}}
@ -405,6 +408,10 @@ const Crontab = () => {
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const resetRef = useRef<boolean>(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<any> | SorterResult<any>[],
) => {
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 (
<PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@ -932,6 +959,8 @@ const Crontab = () => {
enterButton
allowClear
loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
@ -1053,6 +1082,7 @@ const Crontab = () => {
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={vComponents}
/>
</div>
<CronLogModal

View File

@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState, useEffect } from 'react';
import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import {
Button,
message,
@ -33,6 +39,8 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from '../../components/vlist';
const { Text } = Typography;
const { Search } = Input;
@ -58,50 +66,6 @@ enum OperationPath {
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
@ -259,6 +223,10 @@ const Env = () => {
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const getEnvs = () => {
setLoading(true);
@ -273,6 +241,7 @@ const Env = () => {
};
const enabledOrDisabledEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: (
@ -318,16 +287,19 @@ const Env = () => {
};
const addEnv = () => {
setReset(false);
setEditedEnv(null as any);
setIsModalVisible(true);
};
const editEnv = (record: any, index: number) => {
setReset(false);
setEditedEnv(record);
setIsModalVisible(true);
};
const deleteEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: (
@ -367,12 +339,73 @@ const Env = () => {
getEnvs();
};
const components = {
body: {
row: DragableBodyRow,
},
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
reset: resetRef.current,
rowHeight: 48,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight, resetRef.current]);
const DragableBodyRow = (props: any) => {
const { index, moveRow, className, style, ...restProps } = props;
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [drag, drop]);
const components = useMemo(() => vComponents.body.row, []);
const tempProps = useMemo(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
};
const components = useMemo(() => {
return {
...vComponents,
body: {
...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) {
@ -399,12 +432,17 @@ const Env = () => {
setSelectedRowIds(selectedIds);
};
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
const delEnvs = () => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: <></>,
@ -426,6 +464,7 @@ const Env = () => {
};
const operateEnvs = (operationStatus: number) => {
setReset(false);
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
@ -458,6 +497,7 @@ const Env = () => {
};
const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim());
};
@ -521,69 +561,70 @@ const Env = () => {
style: headerStyle,
}}
>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={modifyName}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delEnvs}
>
</Button>
<Button
type="primary"
onClick={() => exportEnvs()}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
loading={loading}
onRow={(record: any, index: number | undefined) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={modifyName}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delEnvs}
>
</Button>
<Button
type="primary"
onClick={() => exportEnvs()}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateEnvs(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
loading={loading}
onRow={(record: any, index: number | undefined) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
</div>
<EnvModal
visible={isModalVisible}
handleCancel={handleCancel}

View File

@ -183,7 +183,7 @@ export function getTableScroll({
extraHeight,
target,
}: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') {
if (typeof extraHeight === 'undefined') {
// 47 + 40 + 12
extraHeight = 99;
}