手机端使用codemirror

This commit is contained in:
hanhh
2021-07-31 23:09:53 +08:00
parent 38609feee9
commit 9728119101
20 changed files with 735 additions and 120 deletions
+68 -14
View File
@@ -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;
}
}
+4 -1
View File
@@ -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 (
<ProLayout
selectedKeys={[props.location.pathname]}
@@ -76,8 +78,9 @@ export default function (props: any) {
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 5,
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
{version}
+36 -16
View File
@@ -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<any[]>([]);
const [theme, setTheme] = useState<string>('');
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 = () => {
},
}}
>
<Editor
defaultLanguage="shell"
value={value}
theme={theme}
options={{
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onChange={(val) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
{isPhone ? (
<CodeMirror
value={value}
options={{
lineNumbers: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
) : (
<Editor
defaultLanguage="shell"
value={value}
theme={theme}
options={{
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onChange={(val) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
)}
</PageContainer>
);
};
+1
View File
@@ -96,6 +96,7 @@ const CronLogModal = ({
title={titleElement()}
visible={visible}
centered
className="log-modal"
bodyStyle={{
overflowY: 'auto',
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
+60 -28
View File
@@ -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<string>('');
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 (
<PageContainer
@@ -80,22 +84,50 @@ const Crontab = () => {
},
}}
>
<DiffEditor
language={"shell"}
original={sample}
modified={value}
options={{
readOnly: true,
fontSize: 12,
minimap: {enabled: width==='100%'},
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
renderSideBySide: width==='100%',
wordWrap: 'on'
}}
theme={theme}
/>
{isPhone ? (
<ReactDiffViewer
styles={{
diffContainer: {
overflowX: 'auto',
minWidth: 768,
},
diffRemoved: {
overflowX: 'auto',
maxWidth: 300,
},
diffAdded: {
overflowX: 'auto',
maxWidth: 300,
},
line: {
wordBreak: 'break-word',
},
}}
oldValue={value}
newValue={sample}
splitView={true}
leftTitle="config.sh"
rightTitle="config.sample.sh"
disableWordDiff={true}
/>
) : (
<DiffEditor
language={'shell'}
original={sample}
modified={value}
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
renderSideBySide: width === '100%',
wordWrap: 'on',
}}
theme={theme}
/>
)}
</PageContainer>
);
};
+4
View File
@@ -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);
}
}
}
+36 -18
View File
@@ -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 = () => {
</div>
</div>
)}
<Editor
language="shell"
theme={theme}
value={value}
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
onChange={(val, ev) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
{isPhone ? (
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
) : (
<Editor
language="shell"
theme={theme}
value={value}
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
onChange={(val, ev) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
)}
</div>
</PageContainer>
);
+2 -2
View File
@@ -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) {
+209
View File
@@ -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<string>('');
const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false);
const [isNewFile, setIsNewFile] = useState<boolean>(false);
const [log, setLog] = useState<string>('');
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 (
<Drawer
className="edit-modal"
title={
<>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect
style={{ marginRight: 8, width: 120 }}
value={currentFile}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData}
placeholder="请选择脚本文件"
showSearch
key="value"
onSelect={onSelect}
/>
<Select
value={language}
style={{ width: 120, marginRight: 8 }}
onChange={(e) => {
setLanguage(e);
}}
>
<Option value="javascript">javascript</Option>
<Option value="typescript">typescript</Option>
<Option value="shell">shell</Option>
<Option value="python">python</Option>
</Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setLog('');
}}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setSettingModalVisible(true);
}}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={createFile}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setSaveModalVisible(true);
}}
>
</Button>
</>
}
width={'100%'}
headerStyle={{ padding: '11px 24px' }}
onClose={cancel}
visible={visible}
>
<SplitPane split="vertical" minSize={200} defaultSize="50%">
<Editor
language={language}
value={value}
theme={theme}
options={{
fontSize: 12,
minimap: { enabled: false },
lineNumbersMinChars: 3,
glyphMargin: false,
}}
onChange={(val) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
<div>
<pre>{log}</pre>
</div>
</SplitPane>
<SaveModal
visible={saveModalVisible}
handleCancel={() => {
setSaveModalVisible(false);
}}
isNewFile={isNewFile}
file={{ content: value, filename: fileName }}
/>
<SettingModal
visible={settingModalVisible}
handleCancel={() => {
setSettingModalVisible(false);
}}
/>
</Drawer>
);
};
export default EditModal;
+4
View File
@@ -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);
}
}
}
+68 -27
View File
@@ -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<number>();
const treeDom = useRef<any>();
const [theme, setTheme] = useState<string>('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const getScripts = () => {
setLoading(true);
@@ -119,18 +122,29 @@ const Script = () => {
title={title}
loading={loading}
extra={
isPhone && [
<TreeSelect
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择脚本文件"
showSearch
key="value"
onSelect={onSelect}
/>,
]
isPhone
? [
<TreeSelect
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择脚本文件"
showSearch
key="value"
onSelect={onSelect}
/>,
]
: [
<Button
type="primary"
onClick={() => {
setIsLogModalVisible(true);
}}
>
</Button>,
]
}
header={{
style: {
@@ -164,20 +178,47 @@ const Script = () => {
</div>
</div>
)}
<Editor
language={mode}
value={value}
theme={theme}
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onChange={(val) => {
setValue((val as string).replace(/\r\n/g, '\n'));
{isPhone ? (
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
) : (
<Editor
language={mode}
value={value}
theme={theme}
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: width === '100%' },
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onChange={(val) => {
setValue((val as string).replace(/\r\n/g, '\n'));
}}
/>
)}
<EditModal
visible={isLogModalVisible}
treeData={data}
currentFile={select}
content={value}
handleCancel={() => {
setIsLogModalVisible(false);
}}
/>
</div>
+87
View File
@@ -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 (
<Modal
title="保存文件"
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="script_modal"
initialValues={file}
>
<Form.Item
name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item
name="path"
label="保存目录"
rules={[{ required: true, message: '请输入保存目录' }]}
>
<Input placeholder="请输入保存目录" />
</Form.Item>
</Form>
</Modal>
);
};
export default SaveModal;
+67
View File
@@ -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 (
<Modal
title="运行设置"
visible={visible}
forceRender
onCancel={() => handleCancel()}
>
<Form
form={form}
layout="vertical"
name="setting_modal"
initialValues={file}
>
<Form.Item
name="filename"
label="待开发"
rules={[{ required: true, message: '待开发' }]}
>
<Input placeholder="待开发" />
</Form.Item>
</Form>
</Modal>
);
};
export default SettingModal;