diff --git a/.umirc.ts b/.umirc.ts index 03d7b666..9e43bb64 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -29,10 +29,15 @@ export default defineConfig({ react: 'window.React', 'react-dom': 'window.ReactDOM', darkreader: 'window.DarkReader', + codemirror: 'window.CodeMirror', }, scripts: [ 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 'https://cdn.jsdelivr.net/npm/darkreader@4.9.34/darkreader.min.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.js', ], }); diff --git a/back/api/auth.ts b/back/api/auth.ts index 329d4340..79e09572 100644 --- a/back/api/auth.ts +++ b/back/api/auth.ts @@ -35,7 +35,7 @@ export default (app: Router) => { ); return res.send({ code: 100, - msg: '已初始化密码,请前往auth.json查看并重新登录', + message: '已初始化密码,请前往auth.json查看并重新登录', }); } if ( @@ -57,10 +57,10 @@ export default (app: Router) => { ); res.send({ code: 200, token }); } else { - res.send({ code: 400, msg: config.authError }); + res.send({ code: 400, message: config.authError }); } } else { - res.send({ err: 400, msg: '请输入用户名密码!' }); + res.send({ err: 400, message: '请输入用户名密码!' }); } }); } catch (e) { @@ -101,7 +101,7 @@ export default (app: Router) => { try { fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => { if (err) console.log(err); - res.send({ code: 200, msg: '更新成功' }); + res.send({ code: 200, message: '更新成功' }); }); } catch (e) { logger.error('🔥 error: %o', e); diff --git a/back/api/config.ts b/back/api/config.ts index 175478a2..1c4afb2a 100644 --- a/back/api/config.ts +++ b/back/api/config.ts @@ -68,7 +68,7 @@ export default (app: Router) => { const { name, content } = req.body; const path = `${config.configPath}${name}`; fs.writeFileSync(path, content); - res.send({ code: 200, msg: '保存成功' }); + res.send({ code: 200, message: '保存成功' }); } catch (e) { logger.error('🔥 error: %o', e); return next(e); diff --git a/back/api/script.ts b/back/api/script.ts index 87480375..1b0a09c4 100644 --- a/back/api/script.ts +++ b/back/api/script.ts @@ -48,4 +48,44 @@ export default (app: Router) => { } }, ); + + route.post( + '/scripts', + celebrate({ + body: Joi.object({ + filename: Joi.string().required(), + path: Joi.string().required(), + content: Joi.string().required(), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + let { filename, path, content } = req.body as { + filename: string; + path: string; + content: string; + }; + if (!path.endsWith('/')) { + path += '/'; + } + if (config.writePathList.every((x) => !path.startsWith(x))) { + return res.send({ code: 400, data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak' }); + } + const filePath = `${path}${filename.replace(/\//g, '')}`; + const bakPath = '/ql/bak'; + if (fs.existsSync(filePath)) { + if (!fs.existsSync(bakPath)) { + fs.mkdirSync(bakPath); + } + fs.copyFileSync(filePath, bakPath); + } + fs.writeFileSync(filePath, content); + return res.send({ code: 200 }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); }; diff --git a/back/config/index.ts b/back/config/index.ts index fa127dc8..4d37381a 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -67,4 +67,10 @@ export default { 'crontab.list', 'env.sh', ], + writePathList: [ + '/ql/scripts/', + '/ql/config/', + '/ql/jbot/', + '/ql/bak/', + ], }; diff --git a/package.json b/package.json index 597c28da..e3fb27fc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ ] }, "dependencies": { - "@monaco-editor/react": "4.1.3", "body-parser": "^1.19.0", "celebrate": "^13.0.3", "cors": "^2.8.5", @@ -45,6 +44,7 @@ "devDependencies": { "@ant-design/icons": "^4.6.2", "@ant-design/pro-layout": "^6.5.0", + "@monaco-editor/react": "^4.2.1", "@types/cors": "^2.8.10", "@types/express": "^4.17.8", "@types/express-jwt": "^6.0.1", @@ -58,6 +58,7 @@ "@types/react-dom": "^17.0.0", "@umijs/plugin-antd": "^0.9.1", "@umijs/test": "^3.3.9", + "codemirror": "^5.62.2", "compression-webpack-plugin": "6.1.1", "darkreader": "^4.9.27", "lint-staged": "^10.0.7", @@ -65,6 +66,7 @@ "prettier": "^2.2.0", "qrcode.react": "^1.0.1", "react": "17.x", + "react-codemirror2": "^7.2.1", "react-diff-viewer": "^3.1.1", "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494ea49c..0ad29434 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: 5.3 specifiers: '@ant-design/icons': ^4.6.2 '@ant-design/pro-layout': ^6.5.0 - '@monaco-editor/react': 4.1.3 + '@monaco-editor/react': ^4.2.1 '@types/cors': ^2.8.10 '@types/express': ^4.17.8 '@types/express-jwt': ^6.0.1 @@ -19,6 +19,7 @@ specifiers: '@umijs/test': ^3.3.9 body-parser: ^1.19.0 celebrate: ^13.0.3 + codemirror: ^5.62.2 compression-webpack-plugin: 6.1.1 cors: ^2.8.5 cron-parser: ^3.5.0 @@ -37,6 +38,7 @@ specifiers: prettier: ^2.2.0 qrcode.react: ^1.0.1 react: 17.x + react-codemirror2: ^7.2.1 react-diff-viewer: ^3.1.1 react-dnd: ^14.0.2 react-dnd-html5-backend: ^14.0.0 @@ -54,7 +56,6 @@ specifiers: yorkie: ^2.0.0 dependencies: - '@monaco-editor/react': 4.1.3_react-dom@17.0.2+react@17.0.2 body-parser: 1.19.0 celebrate: 13.0.4 cors: 2.8.5 @@ -75,6 +76,7 @@ dependencies: devDependencies: '@ant-design/icons': 4.6.2_react-dom@17.0.2+react@17.0.2 '@ant-design/pro-layout': 6.18.0_react-dom@17.0.2+react@17.0.2 + '@monaco-editor/react': 4.2.1_react-dom@17.0.2+react@17.0.2 '@types/cors': 2.8.10 '@types/express': 4.17.11 '@types/express-jwt': 6.0.1 @@ -88,6 +90,7 @@ devDependencies: '@types/react-dom': 17.0.5 '@umijs/plugin-antd': 0.9.1_5ccfec03b6e15849b3687a64fe975f75 '@umijs/test': 3.4.20_ts-node@9.1.1 + codemirror: 5.62.2 compression-webpack-plugin: 6.1.1_webpack@5.37.0 darkreader: 4.9.32 lint-staged: 10.5.4 @@ -95,6 +98,7 @@ devDependencies: prettier: 2.3.0 qrcode.react: 1.0.1_react@17.0.2 react: 17.0.2 + react-codemirror2: 7.2.1_codemirror@5.62.2+react@17.0.2 react-diff-viewer: 3.1.1_react-dom@17.0.2+react@17.0.2 react-dnd: 14.0.2_695545ed68ea337339babea285839fc0 react-dnd-html5-backend: 14.0.0 @@ -872,12 +876,12 @@ packages: monaco-editor: '>= 0.21.0 < 1' dependencies: state-local: 1.0.7 - dev: false + dev: true - /@monaco-editor/react/4.1.3_react-dom@17.0.2+react@17.0.2: - resolution: {integrity: sha512-kqcjVuoy6btcgALAk4RV/SlasveM+WTw5lzzlyq5FhKXjF8wu5tSe/2oCQ1uhLpcdtxcHfx3L0HrcAPWnejFnQ==} + /@monaco-editor/react/4.2.1_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-yN8qVY0PyFIbqPjfrZ5TbR/wrcfeiwoys8+0QkmyfiOzG74vXxSBOPIUxk7Ly+qCj7qWHPq1uDJskzFGaIqaPA==} peerDependencies: - monaco-editor: ^0.23.0 + monaco-editor: '>= 0.25.0 < 1' react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 dependencies: @@ -886,7 +890,7 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 state-local: 1.0.7 - dev: false + dev: true /@npmcli/move-file/1.1.2: resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} @@ -2587,6 +2591,10 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true + /codemirror/5.62.2: + resolution: {integrity: sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw==} + dev: true + /collect-v8-coverage/1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} dev: true @@ -5303,6 +5311,7 @@ packages: /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true /js-yaml/3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -5715,6 +5724,7 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 + dev: true /lowercase-keys/1.0.1: resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} @@ -6954,6 +6964,7 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + dev: true /proxy-addr/2.0.6: resolution: {integrity: sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==} @@ -7719,6 +7730,16 @@ packages: strip-json-comments: 2.0.1 dev: true + /react-codemirror2/7.2.1_codemirror@5.62.2+react@17.0.2: + resolution: {integrity: sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw==} + peerDependencies: + codemirror: 5.x + react: '>=15.5 <=16.x' + dependencies: + codemirror: 5.62.2 + react: 17.0.2 + dev: true + /react-diff-viewer/3.1.1_react-dom@17.0.2+react@17.0.2: resolution: {integrity: sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==} engines: {node: '>= 8'} @@ -7798,6 +7819,7 @@ packages: /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: true /react-is/17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -8732,7 +8754,7 @@ packages: /state-local/1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - dev: false + dev: true /static-extend/0.1.2: resolution: {integrity: sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=} diff --git a/src/layouts/index.less b/src/layouts/index.less index 470182cb..91650eda 100644 --- a/src/layouts/index.less +++ b/src/layouts/index.less @@ -1,4 +1,5 @@ @import '~@/styles/variable.less'; +@import '~codemirror/lib/codemirror.css'; @font-face { font-family: 'Source Code Pro'; @@ -13,7 +14,7 @@ body { background-color: rgb(248, 248, 248); } -.ant-modal { +.log-modal .ant-modal { padding-bottom: 0 !important; width: 580px !important; } @@ -21,22 +22,14 @@ body { .monaco-editor:not(.rename-box) { height: calc(100vh - 128px) !important; height: calc(100vh - var(--vh-offset, 0px) - 128px) !important; - .view-overlays .current-line{ + .view-overlays .current-line { border-width: 0; } } -.log-modal { - .monaco-editor:not(.rename-box) { - height: calc(100vh - 176px) !important; - height: calc(100vh - var(--vh-offset, 0px) - 176px) !important; - background-color: transparent !important; - } -} - -.rename-box { +.rename-box { height: 0; - .rename-input{ + .rename-input { height: 0; padding: 0 !important; } @@ -144,12 +137,17 @@ input:-webkit-autofill:active { height: calc(100vh - 184px); height: calc(100vh - var(--vh-offset, 0px) - 184px); } - .monaco-editor:not(.rename-box) { + .monaco-editor:not(.rename-box), + .CodeMirror { height: calc(100vh - 216px) !important; height: calc(100vh - var(--vh-offset, 0px) - 216px) !important; } + .CodeMirror { + width: calc(100vw - 80px); + } } - .monaco-editor:not(.rename-box) { + .monaco-editor:not(.rename-box), + .CodeMirror { height: calc(100vh - 176px) !important; height: calc(100vh - var(--vh-offset, 0px) - 176px) !important; } @@ -166,3 +164,59 @@ input:-webkit-autofill:active { min-height: calc(100vh - var(--vh-offset, 0px) - 72px); } } + +.Resizer { + background: #000; + opacity: 0.2; + z-index: 1; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -moz-background-clip: padding; + -webkit-background-clip: padding; + background-clip: padding-box; +} + +.Resizer:hover { + -webkit-transition: all 2s ease; + transition: all 2s ease; +} + +.Resizer.horizontal { + height: 11px; + margin: -5px 0; + border-top: 5px solid rgba(255, 255, 255, 0); + border-bottom: 5px solid rgba(255, 255, 255, 0); + cursor: row-resize; + width: 100%; +} + +.Resizer.horizontal:hover { + border-top: 5px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid rgba(0, 0, 0, 0.5); +} + +.Resizer.vertical { + width: 11px; + margin: 0 -5px; + border-left: 5px solid rgba(255, 255, 255, 0); + border-right: 5px solid rgba(255, 255, 255, 0); + cursor: col-resize; +} + +.Resizer.vertical:hover { + border-left: 5px solid rgba(0, 0, 0, 0.5); + border-right: 5px solid rgba(0, 0, 0, 0.5); +} +.Resizer.disabled { + cursor: not-allowed; +} +.Resizer.disabled:hover { + border-color: transparent; +} + +.edit-modal { + .ant-drawer-body { + padding: 0; + } +} diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 1717dbd2..61890750 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -65,6 +65,8 @@ export default function (props: any) { const isSafari = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome'); + const isQQBrowser = navigator.userAgent.includes('QQBrowser'); + return ( {version} diff --git a/src/pages/config/index.tsx b/src/pages/config/index.tsx index 031cce9e..59a56d16 100644 --- a/src/pages/config/index.tsx +++ b/src/pages/config/index.tsx @@ -4,6 +4,7 @@ import config from '@/utils/config'; import { PageContainer } from '@ant-design/pro-layout'; import { request } from '@/utils/http'; import Editor from '@monaco-editor/react'; +import { Controlled as CodeMirror } from 'react-codemirror2'; const Config = () => { const [width, setWidth] = useState('100%'); @@ -15,6 +16,7 @@ const Config = () => { const [select, setSelect] = useState('config.sh'); const [data, setData] = useState([]); const [theme, setTheme] = useState(''); + const [isPhone, setIsPhone] = useState(false); const getConfig = (name: string) => { request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => { @@ -38,7 +40,7 @@ const Config = () => { data: { content: value, name: select }, }) .then((data: any) => { - message.success(data.msg); + message.success(data.message); }); }; @@ -53,10 +55,12 @@ const Config = () => { setWidth('auto'); setMarginLeft(0); setMarginTop(0); + setIsPhone(true); } else { setWidth('100%'); setMarginLeft(0); setMarginTop(-72); + setIsPhone(false); } getFiles(); getConfig('config.sh'); @@ -111,21 +115,37 @@ const Config = () => { }, }} > - { - setValue((val as string).replace(/\r\n/g, '\n')); - }} - /> + {isPhone ? ( + { + setValue(value); + }} + onChange={(editor, data, value) => {}} + /> + ) : ( + { + setValue((val as string).replace(/\r\n/g, '\n')); + }} + /> + )} ); }; diff --git a/src/pages/crontab/logModal.tsx b/src/pages/crontab/logModal.tsx index dcbbd964..488f03b9 100644 --- a/src/pages/crontab/logModal.tsx +++ b/src/pages/crontab/logModal.tsx @@ -96,6 +96,7 @@ const CronLogModal = ({ title={titleElement()} visible={visible} centered + className="log-modal" bodyStyle={{ overflowY: 'auto', maxHeight: 'calc(80vh - var(--vh-offset, 0px))', diff --git a/src/pages/diff/index.tsx b/src/pages/diff/index.tsx index e2f11d8b..34eff25a 100644 --- a/src/pages/diff/index.tsx +++ b/src/pages/diff/index.tsx @@ -3,9 +3,9 @@ import { Button, message, Modal } from 'antd'; import config from '@/utils/config'; import { PageContainer } from '@ant-design/pro-layout'; import { request } from '@/utils/http'; -import ReactDiffViewer from 'react-diff-viewer'; import './index.less'; -import { DiffEditor } from "@monaco-editor/react"; +import { DiffEditor } from '@monaco-editor/react'; +import ReactDiffViewer from 'react-diff-viewer'; const Crontab = () => { const [width, setWidth] = useState('100%'); @@ -15,6 +15,7 @@ const Crontab = () => { const [sample, setSample] = useState(''); const [loading, setLoading] = useState(true); const [theme, setTheme] = useState(''); + const [isPhone, setIsPhone] = useState(false); const getConfig = () => { request.get(`${config.apiPrefix}configs/config.sh`).then((data) => { @@ -37,30 +38,33 @@ const Crontab = () => { setWidth('auto'); setMarginLeft(0); setMarginTop(0); + setIsPhone(true); } else { setWidth('100%'); setMarginLeft(0); setMarginTop(-72); + setIsPhone(false); } getConfig(); getSample(); }, []); - useEffect(()=>{ + useEffect(() => { const media = window.matchMedia('(prefers-color-scheme: dark)'); const storageTheme = localStorage.getItem('qinglong_dark_theme'); - const isDark = (media.matches && storageTheme !== 'light') || storageTheme === 'dark'; - setTheme(isDark?'vs-dark':'vs'); - media.addEventListener('change',(e)=>{ - if(storageTheme === 'auto' || !storageTheme){ - if(e.matches){ - setTheme('vs-dark') - }else{ + const isDark = + (media.matches && storageTheme !== 'light') || storageTheme === 'dark'; + setTheme(isDark ? 'vs-dark' : 'vs'); + media.addEventListener('change', (e) => { + if (storageTheme === 'auto' || !storageTheme) { + if (e.matches) { + setTheme('vs-dark'); + } else { setTheme('vs'); } } - }) - },[]) + }); + }, []); return ( { }, }} > - + {isPhone ? ( + + ) : ( + + )} ); }; diff --git a/src/pages/log/index.module.less b/src/pages/log/index.module.less index 73ee7c51..9e7812a9 100644 --- a/src/pages/log/index.module.less +++ b/src/pages/log/index.module.less @@ -27,5 +27,9 @@ .ant-pro-grid-content.wide .ant-pro-page-container-children-content { background-color: #f8f8f8; } + + .CodeMirror { + width: calc(100% - 32px - @tree-width); + } } } diff --git a/src/pages/log/index.tsx b/src/pages/log/index.tsx index d91f044d..9e353352 100644 --- a/src/pages/log/index.tsx +++ b/src/pages/log/index.tsx @@ -5,6 +5,7 @@ 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 { Controlled as CodeMirror } from 'react-codemirror2'; function getFilterData(keyword: string, data: any) { const expandedKeys: string[] = []; @@ -188,24 +189,41 @@ const Log = () => { )} - { - setValue((val as string).replace(/\r\n/g, '\n')); - }} - /> + {isPhone ? ( + { + setValue(value); + }} + onChange={(editor, data, value) => {}} + /> + ) : ( + { + setValue((val as string).replace(/\r\n/g, '\n')); + }} + /> + )} ); diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index ca67de21..a517b50c 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -21,9 +21,9 @@ const Login = () => { localStorage.setItem(config.authKey, data.token); history.push('/crontab'); } else if (data.code === 100) { - message.warn(data.msg); + message.warn(data.message); } else { - message.error(data.msg); + message.error(data.message); } }) .catch(function (error) { diff --git a/src/pages/script/editModal.tsx b/src/pages/script/editModal.tsx new file mode 100644 index 00000000..0fb937c1 --- /dev/null +++ b/src/pages/script/editModal.tsx @@ -0,0 +1,209 @@ +import React, { useEffect, useState } from 'react'; +import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; +import SplitPane from 'react-split-pane'; +import Editor from '@monaco-editor/react'; +import SaveModal from './saveModal'; +import SettingModal from './setting'; + +const { Option } = Select; +const LangMap: any = { + '.py': 'python', + '.js': 'javascript', + '.sh': 'shell', + '.ts': 'typescript', +}; +const prefixMap: any = { + python: '.py', + javascript: '.js', + shell: '.sh', + typescript: '.ts', +}; + +const EditModal = ({ + treeData, + currentFile, + content, + handleCancel, + visible, +}: { + treeData?: any; + currentFile?: string; + content?: string; + visible: boolean; + handleCancel: () => void; +}) => { + const [value, setValue] = useState(''); + const [theme, setTheme] = useState(''); + const [language, setLanguage] = useState('javascript'); + const [fileName, setFileName] = useState(''); + const [saveModalVisible, setSaveModalVisible] = useState(false); + const [settingModalVisible, setSettingModalVisible] = + useState(false); + const [isNewFile, setIsNewFile] = useState(false); + const [log, setLog] = useState(''); + + const cancel = () => { + handleCancel(); + }; + + const onSelect = (value: any, node: any) => { + const newMode = LangMap[value.slice(-3)] || ''; + setFileName(value); + setLanguage(newMode); + setIsNewFile(false); + getDetail(node); + }; + + const getDetail = (node: any) => { + request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { + setValue(data.data); + }); + }; + + const createFile = () => { + setFileName(`未命名${prefixMap[language]}`); + setIsNewFile(true); + setValue(''); + }; + + const run = () => {}; + + useEffect(() => { + if (!currentFile) { + createFile(); + } else { + setFileName(currentFile); + setValue(content as string); + } + setIsNewFile(!currentFile); + }, []); + + useEffect(() => { + const media = window.matchMedia('(prefers-color-scheme: dark)'); + const storageTheme = localStorage.getItem('qinglong_dark_theme'); + const isDark = + (media.matches && storageTheme !== 'light') || storageTheme === 'dark'; + setTheme(isDark ? 'vs-dark' : 'vs'); + media.addEventListener('change', (e) => { + if (storageTheme === 'auto' || !storageTheme) { + if (e.matches) { + setTheme('vs-dark'); + } else { + setTheme('vs'); + } + } + }); + }, []); + + return ( + + {fileName} + + + + + + + + + } + width={'100%'} + headerStyle={{ padding: '11px 24px' }} + onClose={cancel} + visible={visible} + > + + { + setValue((val as string).replace(/\r\n/g, '\n')); + }} + /> +
+
{log}
+
+
+ { + setSaveModalVisible(false); + }} + isNewFile={isNewFile} + file={{ content: value, filename: fileName }} + /> + { + setSettingModalVisible(false); + }} + /> +
+ ); +}; + +export default EditModal; diff --git a/src/pages/script/index.module.less b/src/pages/script/index.module.less index 73ee7c51..9e7812a9 100644 --- a/src/pages/script/index.module.less +++ b/src/pages/script/index.module.less @@ -27,5 +27,9 @@ .ant-pro-grid-content.wide .ant-pro-page-container-children-content { background-color: #f8f8f8; } + + .CodeMirror { + width: calc(100% - 32px - @tree-width); + } } } diff --git a/src/pages/script/index.tsx b/src/pages/script/index.tsx index e47e64d5..60c1fdbc 100644 --- a/src/pages/script/index.tsx +++ b/src/pages/script/index.tsx @@ -1,10 +1,12 @@ import { useState, useEffect, useCallback, Key, useRef } from 'react'; -import { TreeSelect, Tree, Input } from 'antd'; +import { TreeSelect, Tree, Input, Button } 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 EditModal from './editModal'; +import { Controlled as CodeMirror } from 'react-codemirror2'; function getFilterData(keyword: string, data: any) { if (keyword) { @@ -41,6 +43,7 @@ const Script = () => { const [height, setHeight] = useState(); const treeDom = useRef(); const [theme, setTheme] = useState(''); + const [isLogModalVisible, setIsLogModalVisible] = useState(false); const getScripts = () => { setLoading(true); @@ -119,18 +122,29 @@ const Script = () => { title={title} loading={loading} extra={ - isPhone && [ - , - ] + isPhone + ? [ + , + ] + : [ + , + ] } header={{ style: { @@ -164,20 +178,47 @@ const Script = () => { )} - { - setValue((val as string).replace(/\r\n/g, '\n')); + {isPhone ? ( + { + setValue(value); + }} + onChange={(editor, data, value) => {}} + /> + ) : ( + { + setValue((val as string).replace(/\r\n/g, '\n')); + }} + /> + )} + { + setIsLogModalVisible(false); }} /> diff --git a/src/pages/script/saveModal.tsx b/src/pages/script/saveModal.tsx new file mode 100644 index 00000000..55780c18 --- /dev/null +++ b/src/pages/script/saveModal.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, message, Input, Form } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; + +const SaveModal = ({ + file, + handleCancel, + visible, + isNewFile, +}: { + file?: any; + visible: boolean; + handleCancel: (cks?: any[]) => void; + isNewFile: boolean; +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleOk = async (values: any) => { + console.log(file.filename); + setLoading(true); + const payload = { ...file, ...values }; + request + .post(`${config.apiPrefix}scripts`, { + data: payload, + }) + .then(({ code, data }) => { + if (code === 200) { + message.success('保存文件成功'); + handleCancel(data); + } else { + message.error(data); + } + setLoading(false); + }); + }; + + useEffect(() => { + form.resetFields(); + setLoading(false); + }, [file, visible]); + + return ( + { + form + .validateFields() + .then((values) => { + handleOk(values); + }) + .catch((info) => { + console.log('Validate Failed:', info); + }); + }} + onCancel={() => handleCancel()} + confirmLoading={loading} + > +
+ + + + + + +
+
+ ); +}; + +export default SaveModal; diff --git a/src/pages/script/setting.tsx b/src/pages/script/setting.tsx new file mode 100644 index 00000000..c51e7498 --- /dev/null +++ b/src/pages/script/setting.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, message, Input, Form } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; + +const SettingModal = ({ + file, + handleCancel, + visible, +}: { + file?: any; + visible: boolean; + handleCancel: (cks?: any[]) => void; +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleOk = async (values: any) => { + console.log(file.filename); + setLoading(true); + const payload = { ...file, ...values }; + request + .post(`${config.apiPrefix}scripts`, { + data: payload, + }) + .then(({ code, data }) => { + if (code === 200) { + message.success('保存文件成功'); + handleCancel(data); + } else { + message.error(data); + } + setLoading(false); + }); + }; + + useEffect(() => { + form.resetFields(); + setLoading(false); + }, [file, visible]); + + return ( + handleCancel()} + > +
+ + + +
+
+ ); +}; + +export default SettingModal;