mirror of
				https://github.com/whyour/qinglong.git
				synced 2025-10-31 08:56:06 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			303 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import intl from 'react-intl-universal';
 | |
| import { useState, useEffect, useCallback, Key, useRef } from 'react';
 | |
| import {
 | |
|   TreeSelect,
 | |
|   Tree,
 | |
|   Input,
 | |
|   Empty,
 | |
|   Button,
 | |
|   message,
 | |
|   Modal,
 | |
|   Tooltip,
 | |
|   Typography,
 | |
| } from 'antd';
 | |
| import config from '@/utils/config';
 | |
| import { PageContainer } from '@ant-design/pro-layout';
 | |
| import Editor from '@monaco-editor/react';
 | |
| import { request } from '@/utils/http';
 | |
| import styles from './index.module.less';
 | |
| import CodeMirror from '@uiw/react-codemirror';
 | |
| import SplitPane from 'react-split-pane';
 | |
| import { useOutletContext } from '@umijs/max';
 | |
| import { SharedContext } from '@/layouts';
 | |
| import { DeleteOutlined } from '@ant-design/icons';
 | |
| import { depthFirstSearch } from '@/utils';
 | |
| import debounce from 'lodash/debounce';
 | |
| import uniq from 'lodash/uniq';
 | |
| import useFilterTreeData from '@/hooks/useFilterTreeData';
 | |
| 
 | |
| const { Text } = Typography;
 | |
| 
 | |
| const Log = () => {
 | |
|   const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
 | |
|   const [value, setValue] = useState(intl.get('请选择日志文件'));
 | |
|   const [select, setSelect] = useState<string>(intl.get('请选择日志文件'));
 | |
|   const [data, setData] = useState<any[]>([]);
 | |
|   const [loading, setLoading] = useState(false);
 | |
|   const [height, setHeight] = useState<number>();
 | |
|   const treeDom = useRef<any>();
 | |
|   const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
 | |
|   const [currentNode, setCurrentNode] = useState<any>();
 | |
|   const [searchValue, setSearchValue] = useState('');
 | |
| 
 | |
|   const getLogs = () => {
 | |
|     setLoading(true);
 | |
|     request
 | |
|       .get(`${config.apiPrefix}logs`)
 | |
|       .then(({ code, data }) => {
 | |
|         if (code === 200) {
 | |
|           setData(data);
 | |
|         }
 | |
|       })
 | |
|       .finally(() => setLoading(false));
 | |
|   };
 | |
| 
 | |
|   const getLog = (node: any) => {
 | |
|     request
 | |
|       .get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
 | |
|       .then(({ code, data }) => {
 | |
|         if (code === 200) {
 | |
|           setValue(data);
 | |
|         }
 | |
|       });
 | |
|   };
 | |
| 
 | |
|   const onSelect = (value: any, node: any) => {
 | |
|     setCurrentNode(node);
 | |
|     setSelect(value);
 | |
| 
 | |
|     if (node.key === select || !value) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (node.type === 'directory') {
 | |
|       setValue(intl.get('请选择日志文件'));
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     setValue(intl.get('加载中...'));
 | |
|     getLog(node);
 | |
|   };
 | |
| 
 | |
|   const onTreeSelect = useCallback((keys: Key[], e: any) => {
 | |
|     onSelect(keys[0], e.node);
 | |
|   }, []);
 | |
| 
 | |
|   const onSearch = useCallback(
 | |
|     (e) => {
 | |
|       const keyword = e.target.value;
 | |
|       debounceSearch(keyword);
 | |
|     },
 | |
|     [data],
 | |
|   );
 | |
| 
 | |
|   const debounceSearch = useCallback(
 | |
|     debounce((keyword) => {
 | |
|       setSearchValue(keyword);
 | |
|     }, 300),
 | |
|     [data],
 | |
|   );
 | |
| 
 | |
|   const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
 | |
|     data,
 | |
|     searchValue,
 | |
|     { treeNodeFilterProp: 'title' },
 | |
|   );
 | |
| 
 | |
|   useEffect(() => {
 | |
|     setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
 | |
|   }, [searchExpandedKeys]);
 | |
| 
 | |
|   const deleteFile = () => {
 | |
|     Modal.confirm({
 | |
|       title: `确认删除`,
 | |
|       content: (
 | |
|         <>
 | |
|           {intl.get('确认删除')}
 | |
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | |
|             {select}
 | |
|           </Text>
 | |
|           {intl.get('文件')}
 | |
|           {currentNode.type === 'directory' ? intl.get('夹下所以日志') : ''}
 | |
|           {intl.get(',删除后不可恢复')}
 | |
|         </>
 | |
|       ),
 | |
|       onOk() {
 | |
|         request
 | |
|           .delete(`${config.apiPrefix}logs`, {
 | |
|             data: {
 | |
|               filename: currentNode.title,
 | |
|               path: currentNode.parent || '',
 | |
|               type: currentNode.type,
 | |
|             },
 | |
|           })
 | |
|           .then(({ code }) => {
 | |
|             if (code === 200) {
 | |
|               message.success(`删除成功`);
 | |
|               let newData = [...data];
 | |
|               if (currentNode.parent) {
 | |
|                 newData = depthFirstSearch(
 | |
|                   newData,
 | |
|                   (c) => c.key === currentNode.key,
 | |
|                 );
 | |
|               } else {
 | |
|                 const index = newData.findIndex(
 | |
|                   (x) => x.key === currentNode.key,
 | |
|                 );
 | |
|                 if (index !== -1) {
 | |
|                   newData.splice(index, 1);
 | |
|                 }
 | |
|               }
 | |
|               setData(newData);
 | |
|               initState();
 | |
|             }
 | |
|           });
 | |
|       },
 | |
|       onCancel() {
 | |
|         console.log('Cancel');
 | |
|       },
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   const initState = () => {
 | |
|     setSelect('');
 | |
|     setCurrentNode(null);
 | |
|     setValue(intl.get('请选择脚本文件'));
 | |
|   };
 | |
| 
 | |
|   const onExpand = (expKeys: any) => {
 | |
|     setExpandedKeys(expKeys);
 | |
|   };
 | |
| 
 | |
|   useEffect(() => {
 | |
|     getLogs();
 | |
|   }, []);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     if (treeDom.current) {
 | |
|       setHeight(treeDom.current.clientHeight);
 | |
|     }
 | |
|   }, [treeDom.current, data]);
 | |
| 
 | |
|   return (
 | |
|     <PageContainer
 | |
|       className="ql-container-wrapper log-wrapper"
 | |
|       title={
 | |
|         <>
 | |
|           {select}
 | |
|           {currentNode?.type === 'file' && (
 | |
|             <span style={{ marginLeft: 6, fontSize: 12, color: '#999' }}>
 | |
|               {(currentNode.size / 1024).toFixed(3)}KB
 | |
|             </span>
 | |
|           )}
 | |
|         </>
 | |
|       }
 | |
|       loading={loading}
 | |
|       extra={
 | |
|         isPhone
 | |
|           ? [
 | |
|               <TreeSelect
 | |
|                 treeExpandAction="click"
 | |
|                 className="log-select"
 | |
|                 value={select}
 | |
|                 dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
 | |
|                 treeData={data}
 | |
|                 placeholder={intl.get('请选择日志')}
 | |
|                 fieldNames={{ value: 'key' }}
 | |
|                 treeNodeFilterProp="title"
 | |
|                 showSearch
 | |
|                 allowClear
 | |
|                 onSelect={onSelect}
 | |
|               />,
 | |
|             ]
 | |
|           : [
 | |
|               <Tooltip title={intl.get('删除')}>
 | |
|                 <Button
 | |
|                   type="primary"
 | |
|                   disabled={!select}
 | |
|                   onClick={deleteFile}
 | |
|                   icon={<DeleteOutlined />}
 | |
|                 />
 | |
|               </Tooltip>,
 | |
|             ]
 | |
|       }
 | |
|       header={{
 | |
|         style: headerStyle,
 | |
|       }}
 | |
|     >
 | |
|       <div className={`${styles['log-container']} log-container`}>
 | |
|         {!isPhone && (
 | |
|           /*// @ts-ignore*/
 | |
|           <SplitPane split="vertical" size={200} maxSize={-100}>
 | |
|             <div className={styles['left-tree-container']}>
 | |
|               {data.length > 0 ? (
 | |
|                 <>
 | |
|                   <Input.Search
 | |
|                     className={styles['left-tree-search']}
 | |
|                     onChange={onSearch}
 | |
|                     placeholder={intl.get('请输入日志名')}
 | |
|                     allowClear
 | |
|                   ></Input.Search>
 | |
|                   <div className={styles['left-tree-scroller']} ref={treeDom}>
 | |
|                     <Tree
 | |
|                       expandAction="click"
 | |
|                       className={styles['left-tree']}
 | |
|                       treeData={filterData}
 | |
|                       showIcon={true}
 | |
|                       height={height}
 | |
|                       selectedKeys={[select]}
 | |
|                       showLine={{ showLeafIcon: true }}
 | |
|                       onSelect={onTreeSelect}
 | |
|                       expandedKeys={expandedKeys}
 | |
|                       onExpand={onExpand}
 | |
|                     ></Tree>
 | |
|                   </div>
 | |
|                 </>
 | |
|               ) : (
 | |
|                 <div
 | |
|                   style={{
 | |
|                     display: 'flex',
 | |
|                     justifyContent: 'center',
 | |
|                     alignItems: 'center',
 | |
|                     height: '100%',
 | |
|                   }}
 | |
|                 >
 | |
|                   <Empty
 | |
|                     description={intl.get('暂无日志')}
 | |
|                     image={Empty.PRESENTED_IMAGE_SIMPLE}
 | |
|                   />
 | |
|                 </div>
 | |
|               )}
 | |
|             </div>
 | |
|             <Editor
 | |
|               language="shell"
 | |
|               theme={theme}
 | |
|               value={value}
 | |
|               options={{
 | |
|                 readOnly: true,
 | |
|                 fontSize: 12,
 | |
|                 minimap: { enabled: false },
 | |
|                 lineNumbersMinChars: 3,
 | |
|                 folding: false,
 | |
|                 glyphMargin: false,
 | |
|               }}
 | |
|             />
 | |
|           </SplitPane>
 | |
|         )}
 | |
|         {isPhone && (
 | |
|           <CodeMirror
 | |
|             value={value}
 | |
|             readOnly={true}
 | |
|             theme={theme.includes('dark') ? 'dark' : 'light'}
 | |
|             onChange={(value, viewUpdate) => {
 | |
|               setValue(value);
 | |
|             }}
 | |
|           />
 | |
|         )}
 | |
|       </div>
 | |
|     </PageContainer>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default Log;
 | 
