import IconFont from '@/components/iconfont'; import useFilterTreeData from '@/hooks/useFilterTreeData'; import { SharedContext } from '@/layouts'; import { depthFirstSearch, findNode, getEditorMode } from '@/utils'; import config from '@/utils/config'; import { request } from '@/utils/http'; import { canPreviewInMonaco } from '@/utils/monaco'; import { CloudDownloadOutlined, DeleteOutlined, EditOutlined, EllipsisOutlined, PlusOutlined, } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import Editor from '@monaco-editor/react'; import { langs } from '@uiw/codemirror-extensions-langs'; import CodeMirror from '@uiw/react-codemirror'; import { history, useOutletContext } from '@umijs/max'; import { Button, Dropdown, Empty, Input, MenuProps, message, Modal, Tooltip, Tree, TreeSelect, Typography, } from 'antd'; import { saveAs } from 'file-saver'; import debounce from 'lodash/debounce'; import uniq from 'lodash/uniq'; import prettyBytes from 'pretty-bytes'; import { parse } from 'query-string'; import { Key, useCallback, useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import intl from 'react-intl-universal'; import SplitPane from 'react-split-pane'; import EditModal from './editModal'; import EditScriptNameModal from './editNameModal'; import styles from './index.module.less'; import RenameModal from './renameModal'; import UnsupportedFilePreview from './components/UnsupportedFilePreview'; const { Text } = Typography; const Script = () => { const { headerStyle, isPhone, theme } = useOutletContext(); const [value, setValue] = useState(intl.get('请选择脚本文件')); const [select, setSelect] = useState(intl.get('请选择脚本文件')); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [mode, setMode] = useState(''); const [height, setHeight] = useState(); const treeDom = useRef(); const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [searchValue, setSearchValue] = useState(''); const [isEditing, setIsEditing] = useState(false); const editorRef = useRef(null); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isRenameFileModalVisible, setIsRenameFileModalVisible] = useState(false); const [currentNode, setCurrentNode] = useState(); const [expandedKeys, setExpandedKeys] = useState([]); const [showMonaco, setShowMonaco] = useState(true); const handleIsEditing = (filename: string, value: boolean) => { setIsEditing(value && canPreviewInMonaco(filename)); }; const getScripts = (needLoading: boolean = true) => { needLoading && setLoading(true); request .get(`${config.apiPrefix}scripts`) .then(({ code, data }) => { if (code === 200) { setData(data); initState(); initGetScript(data); } }) .finally(() => needLoading && setLoading(false)); }; const getDetail = (node: any, options: any = {}) => { request .get( `${config.apiPrefix}scripts/detail?file=${encodeURIComponent( node.title, )}&path=${node.parent || ''}`, ) .then(({ code, data }) => { if (code === 200) { setValue(data); if (options.callback) { options.callback(); } } }); }; const downloadScript = () => { request .post( `${config.apiPrefix}scripts/download`, { filename: currentNode.title, path: currentNode.parent || '', }, { responseType: 'blob' }, ) .then((res) => { saveAs(res, currentNode.title); }); }; const initGetScript = (_data: any) => { const { p, s } = parse(history.location.search); if (s) { const vkey = `${p}/${s}`; const obj = { node: { title: s, key: p ? vkey : s, parent: p, }, }; const item = findNode(_data, (c) => c.key === obj.node.key); if (item) { obj.node = item; setExpandedKeys([p as string]); onTreeSelect([vkey], obj); } } }; const onSelect = (value: any, node: any) => { if (node.key === select || !value) { return; } setSelect(node.key); setCurrentNode(node); if (node.type === 'directory') { setValue(intl.get('请选择脚本文件')); setShowMonaco(true); return; } if (!canPreviewInMonaco(node.title)) { setShowMonaco(false); return; } setShowMonaco(true); const newMode = getEditorMode(value); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setValue(intl.get('加载中...')); getDetail(node, { callback: () => { if (isEditing) { setIsEditing(true); } }, }); }; const onTreeSelect = useCallback( (keys: Key[], e: any) => { const node = e.node; if (node.key === select && isEditing) { return; } const currentContent = editorRef.current ? editorRef.current.getValue().replace(/\r\n/g, '\n') : value; const originalContent = value.replace(/\r\n/g, '\n'); if (currentContent !== originalContent && isEditing) { Modal.confirm({ title: intl.get('确认离开'), content: <>{intl.get('当前文件未保存,确认离开吗')}, onOk() { onSelect(keys[0], e.node); handleIsEditing(e.node.title, false); }, }); } else { handleIsEditing(e.node.title, false); onSelect(keys[0], e.node); } }, [value, select, isEditing], ); 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 onExpand = (expKeys: any) => { setExpandedKeys(expKeys); }; const onDoubleClick = (e: any, node: any) => { if (node.type === 'file') { setSelect(node.key); setCurrentNode(node); handleIsEditing(node.title, true); } }; const editFile = () => { setTimeout(() => { handleIsEditing(currentNode.title, true); }, 300); }; const cancelEdit = () => { handleIsEditing(currentNode.title, false); setValue(intl.get('加载中...')); getDetail(currentNode); }; const saveFile = () => { Modal.confirm({ title: `确认保存`, content: ( <> {intl.get('确认保存文件')} {' '} {currentNode.title} {intl.get(',保存后不可恢复')} ), onOk() { const content = editorRef.current ? editorRef.current.getValue().replace(/\r\n/g, '\n') : value; return new Promise((resolve, reject) => { request .put(`${config.apiPrefix}scripts`, { filename: currentNode.title, path: currentNode.parent || '', content, }) .then(({ code, data }) => { if (code === 200) { message.success(`保存成功`); setValue(content); handleIsEditing(currentNode.title, false); } resolve(null); }) .catch((e) => reject(e)); }); }, }); }; const deleteFile = () => { Modal.confirm({ title: `确认删除`, content: ( <> {intl.get('确认删除')} {' '} {select}{' '} {intl.get('文件')} {currentNode.type === 'directory' ? intl.get('夹及其子文件') : ''} {intl.get(',删除后不可恢复')} ), onOk() { request .delete(`${config.apiPrefix}scripts`, { 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(); } }); }, }); }; const renameFile = () => { setIsRenameFileModalVisible(true); }; const handleRenameFileCancel = () => { setIsRenameFileModalVisible(false); getScripts(false); }; const addFile = () => { setIsAddFileModalVisible(true); }; const addFileModalClose = async ( { filename, path, key, type, }: { filename: string; path: string; key: string; type?: string } = { filename: '', path: '', key: '', }, ) => { if (filename) { const res = await request.get(`${config.apiPrefix}scripts`); let newData = res.data; if (type === 'directory' && filename.includes('/')) { const parts = filename.split('/'); parts.pop(); const parentPath = parts.join('/'); path = path ? `${path}/${parentPath}` : parentPath; } const item = findNode(newData, (c) => c.key === key); if (path) { const keys = path.split('/'); const sKeys: string[] = []; keys.reduce((p, c) => { sKeys.push(p); return `${p}/${c}`; }); setExpandedKeys([...expandedKeys, ...sKeys, path]); } setData(newData); onSelect(item.title, item); handleIsEditing(item.title, true); } setIsAddFileModalVisible(false); }; const initState = () => { setSelect(intl.get('请选择脚本文件')); setCurrentNode(null); setValue(intl.get('请选择脚本文件')); }; useEffect(() => { getScripts(); }, []); useEffect(() => { if (treeDom.current) { setHeight(treeDom.current.clientHeight - 6); } }, [treeDom.current, data]); useHotkeys( 'mod+s', (e) => { if (isEditing) { saveFile(); } }, { enableOnFormTags: ['textarea'], preventDefault: true }, ); useHotkeys( 'mod+d', (e) => { if (currentNode.title) { deleteFile(); } }, { preventDefault: true }, ); useHotkeys( 'mod+o', (e) => { if (!isEditing) { addFile(); } }, { preventDefault: true }, ); useHotkeys( 'mod+e', (e) => { if (currentNode.title) { cancelEdit(); } }, { preventDefault: true }, ); const action = (key: string | number) => { switch (key) { case 'save': saveFile(); break; case 'exit': cancelEdit(); break; default: break; } }; const menuAction = (key: string | number) => { switch (key) { case 'add': addFile(); break; case 'edit': editFile(); break; case 'delete': deleteFile(); break; case 'rename': renameFile(); break; default: break; } }; const menu: MenuProps = isEditing ? { items: [ { label: intl.get('保存'), key: 'save', icon: }, { label: intl.get('退出编辑'), key: 'exit', icon: }, ], onClick: ({ key, domEvent }) => { domEvent.stopPropagation(); action(key); }, } : { items: [ { label: intl.get('创建'), key: 'add', icon: }, { label: intl.get('编辑'), key: 'edit', icon: , disabled: !currentNode, }, { label: intl.get('重命名'), key: 'rename', icon: , disabled: !currentNode, }, { label: intl.get('删除'), key: 'delete', icon: , disabled: !currentNode, }, ], onClick: ({ key, domEvent }) => { domEvent.stopPropagation(); menuAction(key); }, }; const handleForceOpen = () => { if (!currentNode) return; setMode('plaintext'); setValue(intl.get('加载中...')); setShowMonaco(true); getDetail(currentNode, { callback: () => { setIsEditing(true); }, }); }; return ( {select} {currentNode?.type === 'file' && ( {prettyBytes(currentNode.size)} )} } loading={loading} extra={ isPhone ? [ , , , ] : [ , ] } header={{ style: headerStyle, }} >
{!isPhone && ( /*// @ts-ignore*/
{data.length > 0 ? ( <>
) : (
)}
{showMonaco ? ( { editorRef.current = editor; }} /> ) : ( )}
)} {isPhone && ( { setValue(value); }} /> )} {isLogModalVisible && ( { setIsLogModalVisible(false); }} /> )}
); }; export default Script;