diff --git a/back/config/util.ts b/back/config/util.ts index b4ade67b..222322ef 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -312,6 +312,7 @@ export function readDirs( return { title: file, type: 'file', + isLeaf: true, key, parent: relativePath, }; @@ -346,7 +347,7 @@ export function readDir( export function emptyDir(path: string) { const files = fs.readdirSync(path); - files.forEach(file => { + files.forEach((file) => { const filePath = `${path}/${file}`; const stats = fs.statSync(filePath); if (stats.isDirectory()) { @@ -358,7 +359,6 @@ export function emptyDir(path: string) { fs.rmdirSync(path); } - export function promiseExec(command: string): Promise { return new Promise((resolve, reject) => { exec( @@ -379,22 +379,29 @@ export function parseHeaders(headers: string) { let val; let i; - headers && headers.split('\n').forEach(function parser(line) { - i = line.indexOf(':'); - key = line.substring(0, i).trim().toLowerCase(); - val = line.substring(i + 1).trim(); + headers && + headers.split('\n').forEach(function parser(line) { + i = line.indexOf(':'); + key = line.substring(0, i).trim().toLowerCase(); + val = line.substring(i + 1).trim(); - if (!key) { - return; - } + if (!key) { + return; + } - parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; - }); + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + }); return parsed; -}; +} -export function parseBody(body: string, contentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded') { +export function parseBody( + body: string, + contentType: + | 'application/json' + | 'multipart/form-data' + | 'application/x-www-form-urlencoded', +) { if (!body) return ''; const parsed: any = {}; @@ -402,22 +409,23 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi let val; let i; - body && body.split('\n').forEach(function parser(line) { - i = line.indexOf(':'); - key = line.substring(0, i).trim().toLowerCase(); - val = line.substring(i + 1).trim(); + body && + body.split('\n').forEach(function parser(line) { + i = line.indexOf(':'); + key = line.substring(0, i).trim().toLowerCase(); + val = line.substring(i + 1).trim(); - if (!key || parsed[key]) { - return; - } + if (!key || parsed[key]) { + return; + } - parsed[key] = val; - }); + parsed[key] = val; + }); switch (contentType) { case 'multipart/form-data': return Object.keys(parsed).reduce((p, c) => { - p.append(c, parsed[c]) + p.append(c, parsed[c]); return p; }, new FormData()); case 'application/x-www-form-urlencoded': @@ -427,4 +435,4 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi } return parsed; -}; \ No newline at end of file +} diff --git a/package.json b/package.json index d6680cd4..13d8083c 100644 --- a/package.json +++ b/package.json @@ -125,12 +125,12 @@ "qrcode.react": "^1.0.1", "query-string": "^7.1.1", "rc-tween-one": "^3.0.6", - "react": "18.x", + "react": "18.2.0", "react-codemirror2": "^7.2.1", "react-diff-viewer": "^3.1.1", "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", - "react-dom": "18.x", + "react-dom": "18.2.0", "react-split-pane": "^0.1.92", "sockjs-client": "^1.6.0", "ts-node": "^10.6.0", diff --git a/src/hooks/useFilterTreeData.ts b/src/hooks/useFilterTreeData.ts new file mode 100644 index 00000000..5fa39290 --- /dev/null +++ b/src/hooks/useFilterTreeData.ts @@ -0,0 +1,48 @@ +import { useMemo, useState } from 'react'; + +export default ( + treeData: any[], + searchValue: string, + { + treeNodeFilterProp, + }: { + treeNodeFilterProp: string; + }, +) => { + return useMemo(() => { + const keys: string[] = []; + + if (!searchValue) { + return { treeData, keys }; + } + + const upperStr = searchValue.toUpperCase(); + function filterOptionFunc(_: string, dataNode: any[]) { + const value = dataNode[treeNodeFilterProp as any]; + + return String(value).toUpperCase().includes(upperStr); + } + + function dig(list: any[], keepAll: boolean = false): any[] { + return list + .map((dataNode) => { + const children = dataNode.children; + + const match = keepAll || filterOptionFunc!(searchValue, dataNode); + const childList = dig(children || [], match); + + if (match || childList.length) { + childList.length && keys.push(dataNode.key); + return { + ...dataNode, + children: childList, + }; + } + return null; + }) + .filter((node) => node); + } + + return { treeData: dig(treeData), keys }; + }, [treeData, searchValue, treeNodeFilterProp]); +}; diff --git a/src/pages/log/index.tsx b/src/pages/log/index.tsx index ef408974..719a68c8 100644 --- a/src/pages/log/index.tsx +++ b/src/pages/log/index.tsx @@ -21,44 +21,16 @@ import { useOutletContext } from '@umijs/max'; import { SharedContext } from '@/layouts'; import { DeleteOutlined } from '@ant-design/icons'; import { depthFirstSearch } from '@/utils'; -import { debounce } from 'lodash'; +import { debounce, uniq } from 'lodash'; +import useFilterTreeData from '@/hooks/useFilterTreeData'; const { Text } = Typography; -function getFilterData(keyword: string, data: any) { - const expandedKeys: string[] = []; - if (keyword) { - const tree: any = []; - data.forEach((item: any) => { - if (item.title.toLocaleLowerCase().includes(keyword)) { - tree.push(item); - } else { - const children: any[] = []; - (item.children || []).forEach((subItem: any) => { - if (subItem.title.toLocaleLowerCase().includes(keyword)) { - children.push(subItem); - } - }); - if (children.length > 0) { - tree.push({ - ...item, - children, - }); - expandedKeys.push(item.key); - } - } - }); - return { tree, expandedKeys }; - } - return { tree: data, expandedKeys }; -} - const Log = () => { const { headerStyle, isPhone, theme } = useOutletContext(); const [value, setValue] = useState('请选择日志文件'); const [select, setSelect] = useState(''); const [data, setData] = useState([]); - const [filterData, setFilterData] = useState([]); const [loading, setLoading] = useState(false); const [height, setHeight] = useState(); const treeDom = useRef(); @@ -73,7 +45,6 @@ const Log = () => { .then(({ code, data }) => { if (code === 200) { setData(data); - setFilterData(data); } }) .finally(() => setLoading(false)); @@ -115,22 +86,26 @@ const Log = () => { const keyword = e.target.value; debounceSearch(keyword); }, - [data, setFilterData], + [data], ); const debounceSearch = useCallback( debounce((keyword) => { setSearchValue(keyword); - const { tree, expandedKeys } = getFilterData( - keyword.toLocaleLowerCase(), - data, - ); - setFilterData(tree); - setExpandedKeys(expandedKeys); }, 300), - [data, setFilterData], + [data], ); + const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData( + data, + searchValue, + { treeNodeFilterProp: 'title' }, + ); + + useEffect(() => { + setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys])); + }, [searchExpandedKeys]); + const deleteFile = () => { Modal.confirm({ title: `确认删除`, @@ -187,11 +162,9 @@ const Log = () => { setValue('请选择脚本文件'); }; - useEffect(() => { - const word = searchValue || ''; - const { tree } = getFilterData(word.toLocaleLowerCase(), data); - setFilterData(tree); - }, [data]); + const onExpand = (expKeys: any) => { + setExpandedKeys(expKeys); + }; useEffect(() => { getLogs(); @@ -217,8 +190,10 @@ const Log = () => { dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} treeData={data} placeholder="请选择日志" - fieldNames={{ value: 'key', label: 'title' }} + fieldNames={{ value: 'key' }} + treeNodeFilterProp="title" showSearch + allowClear onSelect={onSelect} />, ] @@ -260,6 +235,8 @@ const Log = () => { selectedKeys={[select]} showLine={{ showLeafIcon: true }} onSelect={onTreeSelect} + expandedKeys={expandedKeys} + onExpand={onExpand} > diff --git a/src/pages/script/index.tsx b/src/pages/script/index.tsx index ddd2073c..cd1abdb3 100644 --- a/src/pages/script/index.tsx +++ b/src/pages/script/index.tsx @@ -37,37 +37,11 @@ import { history, useOutletContext, useLocation } from '@umijs/max'; import { parse } from 'query-string'; import { depthFirstSearch } from '@/utils'; import { SharedContext } from '@/layouts'; +import useFilterTreeData from '@/hooks/useFilterTreeData'; +import { uniq } from 'lodash'; const { Text } = Typography; -function getFilterData(keyword: string, data: any) { - const expandedKeys: string[] = []; - if (keyword) { - const tree: any = []; - data.forEach((item: any) => { - if (item.title.toLocaleLowerCase().includes(keyword)) { - tree.push(item); - } else { - const children: any[] = []; - (item.children || []).forEach((subItem: any) => { - if (subItem.title.toLocaleLowerCase().includes(keyword)) { - children.push(subItem); - } - }); - if (children.length > 0) { - tree.push({ - ...item, - children, - }); - expandedKeys.push(item.key); - } - } - }); - return { tree, expandedKeys }; - } - return { tree: data, expandedKeys }; -} - const LangMap: any = { '.py': 'python', '.js': 'javascript', @@ -81,7 +55,6 @@ const Script = () => { const [value, setValue] = useState('请选择脚本文件'); const [select, setSelect] = useState(''); const [data, setData] = useState([]); - const [filterData, setFilterData] = useState([]); const [loading, setLoading] = useState(false); const [mode, setMode] = useState(''); const [height, setHeight] = useState(); @@ -101,7 +74,6 @@ const Script = () => { .then(({ code, data }) => { if (code === 200) { setData(data); - setFilterData(data); initGetScript(); } }) @@ -153,10 +125,6 @@ const Script = () => { getDetail(node); }; - const onExpand = (expKeys: any) => { - setExpandedKeys(expKeys); - }; - const onTreeSelect = useCallback( (keys: Key[], e: any) => { const content = editorRef.current @@ -187,22 +155,30 @@ const Script = () => { const keyword = e.target.value; debounceSearch(keyword); }, - [data, setFilterData], + [data], ); const debounceSearch = useCallback( debounce((keyword) => { setSearchValue(keyword); - const { tree, expandedKeys } = getFilterData( - keyword.toLocaleLowerCase(), - data, - ); - setExpandedKeys(expandedKeys); - setFilterData(tree); }, 300), - [data, setFilterData], + [data], ); + const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData( + data, + searchValue, + { treeNodeFilterProp: 'title' }, + ); + + useEffect(() => { + setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys])); + }, [searchExpandedKeys]); + + const onExpand = (expKeys: any) => { + setExpandedKeys(expKeys); + }; + const editFile = () => { setTimeout(() => { setIsEditing(true); @@ -367,12 +343,6 @@ const Script = () => { setValue('请选择脚本文件'); }; - useEffect(() => { - const word = searchValue || ''; - const { tree } = getFilterData(word.toLocaleLowerCase(), data); - setFilterData(tree); - }, [data]); - useEffect(() => { getScripts(); }, []); @@ -462,8 +432,10 @@ const Script = () => { dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} treeData={data} placeholder="请选择脚本" - fieldNames={{ value: 'key', label: 'title' }} + fieldNames={{ value: 'key' }} + treeNodeFilterProp="title" showSearch + allowClear onSelect={onSelect} />,