├── preview.png ├── public ├── logo.jpg ├── favicon.ico ├── robots.txt ├── manifest.json └── index.html ├── src ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── logo.svg ├── App.css └── App.js ├── README.md ├── .gitignore └── package.json /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxue/noxue-code-ui/HEAD/preview.png -------------------------------------------------------------------------------- /public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxue/noxue-code-ui/HEAD/public/logo.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxue/noxue-code-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 说明 3 | 4 | * 这是在线编译器的前台界面,后端接口项目地址: 5 | 6 | 7 | ## 部署 8 | 9 | yarn build 10 | 11 | * 配置 nginx 伪静态规则,用于请求到后端接口,我接口部署在同服务器的8585端口,规则如下: 12 | 13 | ```nginx 14 | location ^~ /api{ 15 | proxy_pass http://127.0.0.1:8585; 16 | } 17 | ``` 18 | 19 | ## 效果图 20 | 21 | ![](./preview.png) 22 | 23 | 24 | ## 联系方式 25 | 26 | * 有bug请联系我,微信qq同号:173126019 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "在线编译器", 3 | "name": "代码在线编译器 - 不学网", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.jpg", 12 | "type": "image/png" 13 | } 14 | ], 15 | "start_url": ".", 16 | "display": "standalone", 17 | "theme_color": "#000000", 18 | "background_color": "#ffffff" 19 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 代码在线编译器 - 不学网 18 | 19 | 20 | 21 |
22 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noxue-code-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@codemirror/lang-cpp": "^0.19.1", 7 | "@codemirror/lang-java": "^0.19.1", 8 | "@codemirror/lang-javascript": "^0.19.7", 9 | "@codemirror/lang-php": "^0.19.1", 10 | "@codemirror/lang-python": "^0.19.4", 11 | "@codemirror/lang-rust": "^0.19.1", 12 | "@codemirror/legacy-modes": "^0.19.1", 13 | "@codemirror/stream-parser": "^0.19.7", 14 | "@testing-library/jest-dom": "^5.14.1", 15 | "@testing-library/react": "^12.0.0", 16 | "@testing-library/user-event": "^13.2.1", 17 | "@uiw/react-codemirror": "^4.5.1", 18 | "axios": "^0.26.1", 19 | "codemirror": "^5.65.2", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "react-loading": "^2.0.3", 23 | "react-scripts": "5.0.0", 24 | "reset-css": "^5.0.1", 25 | "web-vitals": "^2.1.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body,html{ 2 | overflow: hidden; 3 | } 4 | 5 | .app{ 6 | display: flex; 7 | flex-direction: column; 8 | overflow: hidden; 9 | } 10 | 11 | 12 | .tool-bar{ 13 | display: flex; 14 | flex-direction: row-reverse; 15 | height:40px; 16 | line-height: 40px; 17 | background-color: #333; 18 | color:#f7f7f7; 19 | font-size: large; 20 | } 21 | 22 | .tool-bar>div{ 23 | margin:auto 10px; 24 | } 25 | 26 | 27 | .tool-bar>div>label{ 28 | margin-right: 5px; 29 | } 30 | 31 | .result{ 32 | width:100%; 33 | height:calc(100vh - 41px); 34 | border-top:1px solid #eee; 35 | } 36 | 37 | .stdin,.stdout{ 38 | width:100vw; 39 | height: calc(100% - 40px); 40 | background:#fff; 41 | } 42 | 43 | 44 | .stdin>textarea, .stdout>textarea{ 45 | width:calc(100vw - 12px); 46 | height: calc(100% - 10px); 47 | min-height: 100px; 48 | margin:0 0px; 49 | padding:0 5px; 50 | padding-top:10px; 51 | border:0; 52 | color:#333; 53 | background:#f7f7f7; 54 | } 55 | 56 | 57 | *::-webkit-scrollbar{ 58 | /*滚动条整体样式*/ 59 | width : 8px; /*高宽分别对应横竖滚动条的尺寸*/ 60 | height: 1px; 61 | } 62 | 63 | ::-webkit-scrollbar-thumb { 64 | /*滚动条里面小方块*/ 65 | border-radius: 2px; 66 | box-shadow : inset 0 0 5px rgba(223, 222, 222, 0.2); 67 | background : #d8d8d8; 68 | } 69 | 70 | *::-webkit-scrollbar-track { 71 | /*滚动条里面轨道*/ 72 | box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); 73 | /* border-radius: 2px; */ 74 | background : #3d3d3d; 75 | } 76 | 77 | .stdin,.stdin>textarea{ 78 | background-color: #fff; 79 | color:#000; 80 | } 81 | 82 | .stdout,.stdout>textarea{ 83 | background-color: #000; 84 | color:#fff; 85 | } 86 | 87 | /* 解决手机端 文字颜色问题 */ 88 | .stdout>textarea:disabled{ 89 | opacity: 1; 90 | -webkit-text-fill-color: #fff; 91 | } 92 | 93 | 94 | .stdin>textarea:focus{ 95 | outline: none !important; 96 | } 97 | 98 | .tabs{ 99 | position: relative; 100 | display: flex; 101 | box-shadow:0px 5px 5px 0px #00000033; 102 | z-index: 1; 103 | 104 | /* 电脑端禁止选中文本 */ 105 | -moz-user-select:none; /*火狐*/ 106 | -webkit-user-select:none; /*webkit浏览器*/ 107 | -ms-user-select:none; /*IE10*/ 108 | -khtml-user-select:none; /*早期浏览器*/ 109 | user-select:none; 110 | 111 | /* 移动端禁止选中文本 */ 112 | -webkit-touch-callout: none; 113 | -webkit-user-select: none; 114 | -khtml-user-select: none; 115 | -moz-user-select: none; 116 | -ms-user-select: none; 117 | user-select: none; 118 | } 119 | 120 | .tabs>div{ 121 | height:30px; 122 | line-height: 30px; 123 | cursor: pointer; 124 | padding:5px 20px; 125 | } 126 | 127 | .tabs>div.run-code{ 128 | margin:auto 10px; 129 | margin-left:50px; 130 | padding-left:20px; 131 | padding-right: 20px; 132 | background: rgb(13, 174, 248); 133 | color:#fff; 134 | height:20px; 135 | line-height: 20px; 136 | border-radius: 10px; 137 | } 138 | 139 | .run-code:hover{ 140 | background: rgb(110, 202, 245); 141 | } 142 | 143 | .loading{ 144 | background:#00000099; 145 | position:absolute; 146 | width:100vw; 147 | height:100vh; 148 | z-index: 100; 149 | } 150 | 151 | .loading>.ani{ 152 | position: relative; 153 | top:calc(50vh - 32px); 154 | left:calc(50vw - 32px); 155 | } 156 | 157 | 158 | .links>a{ 159 | color:#fff; 160 | text-decoration: none; 161 | margin:auto 10px; 162 | } 163 | 164 | @media screen and (max-width: 600px) { 165 | .links>a{ 166 | display: none; 167 | } 168 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | 3 | import React from 'react' 4 | import axios from 'axios' 5 | import 'reset-css'; 6 | import CodeMirror from '@uiw/react-codemirror'; 7 | import { StreamLanguage } from '@codemirror/stream-parser'; 8 | import { go } from '@codemirror/legacy-modes/mode/go'; 9 | import { shell } from '@codemirror/legacy-modes/mode/shell'; 10 | import { perl } from '@codemirror/legacy-modes/mode/perl'; 11 | import { ruby } from '@codemirror/legacy-modes/mode/ruby'; 12 | import { lua } from '@codemirror/legacy-modes/mode/lua'; 13 | import { swift } from '@codemirror/legacy-modes/mode/swift'; 14 | import { textile } from '@codemirror/legacy-modes/mode/textile'; 15 | import { cpp } from "@codemirror/lang-cpp"; 16 | import { python } from "@codemirror/lang-python"; 17 | import { php } from "@codemirror/lang-php"; 18 | import { rust } from '@codemirror/lang-rust'; 19 | import { java } from '@codemirror/lang-java'; 20 | import { javascript } from '@codemirror/lang-javascript'; 21 | 22 | 23 | 24 | 25 | 26 | import ReactLoading from "react-loading"; 27 | 28 | class App extends React.Component { 29 | 30 | state = { 31 | editorHeight: 500, // 编辑器高度,用于分隔条自动拖动 32 | isDrag: false, // 分隔栏,是否正在拖动 33 | prevPos: 0, // 记录上一个拖动事件的y坐标 34 | theme: 'dark', // 编辑器主题 35 | code: '', // 初始代码 36 | lang: 'c', // 默认语言 37 | stdin: '', // 标准输入内容 38 | stdout: '', // 标准输出 39 | stderr: '', // 标准错误输出 40 | tabIndex: 0, // 指定显示的选项卡 0:显示输出,1显示输入 41 | running: false, // 记录是否在运行代码 42 | } 43 | 44 | // 主题列表 45 | themes = ["dark", "light"] 46 | 47 | // 语言列表 48 | langs = [ 49 | "rust", 50 | "c", "c89", "c99", "c11", "c17", 51 | "cpp", "cpp98", "cpp11", "cpp14", "cpp17", "cpp20", "cpp23", 52 | "java", "nasm", "golang", 53 | "python2", "python3", 54 | "php5", "php7", "php8", 55 | "nodejs", "shell", "swift", "ruby", "perl", 56 | ] 57 | 58 | langModes = { 59 | "c": cpp(), 60 | "c89": cpp(), 61 | "c99": cpp(), 62 | "c11": cpp(), 63 | "c17": cpp(), 64 | "cpp": cpp(), 65 | "cpp98": cpp(), 66 | "cpp11": cpp(), 67 | "cpp14": cpp(), 68 | "cpp17": cpp(), 69 | "cpp20": cpp(), 70 | "cpp23": cpp(), 71 | "rust": rust(), 72 | "java": java(), 73 | "nasm": StreamLanguage.define(textile), 74 | "golang": StreamLanguage.define(go), 75 | "php5": php(), 76 | "php7": php(), 77 | "php8": php(), 78 | "python2": python(), 79 | "python3": python(), 80 | "ruby": StreamLanguage.define(ruby), 81 | "shell": StreamLanguage.define(shell), 82 | "nodejs": javascript(), 83 | "perl": StreamLanguage.define(perl), 84 | "swift": StreamLanguage.define(swift) 85 | } 86 | 87 | constructor(props) { 88 | super(); 89 | this.editerRef = React.createRef(); 90 | this.resultRef = React.createRef(); 91 | } 92 | 93 | componentDidMount() { 94 | const that = this 95 | // 禁止ctrl+s 96 | window.addEventListener('keydown', function (e) { 97 | if (e.keyCode === 83 && (navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)) { 98 | e.preventDefault(); 99 | if (that.state.running) return; 100 | that.runCode(1) 101 | } 102 | }) 103 | 104 | 105 | // 移动端textarea无法滚动问题 106 | window.addEventListener('touchmove', function (e) { 107 | let target = e.target 108 | if (target && target.tagName === 'TEXTAREA') { 109 | e.stopPropagation(); 110 | } 111 | }, true) 112 | 113 | 114 | // 禁止手机浏览器下拉 115 | document.addEventListener('touchmove', function (ev) { 116 | ev.preventDefault(); 117 | }, { passive: false }); 118 | 119 | 120 | 121 | let defaultCode = `#include 122 | 123 | // 欢迎使用不学网(noxue.com)提供的在线编译器 124 | // 快捷键 Ctrl+S 自动编译 125 | // 代码会自动保存,打开浏览器依然存在 126 | // 发现bug或者漏洞,欢迎在github提交给我,也可以微信告知我,感激不尽。 127 | // 微信号:noxuecom 128 | int main(){ 129 | printf("做人如果没有梦想,那和咸鱼有什么分别!"); 130 | return 0; 131 | } 132 | ` 133 | 134 | let code = localStorage.getItem("noxue-code"); 135 | this.setState({ 136 | code: code ? code : defaultCode 137 | }) 138 | 139 | let lang = localStorage.getItem("noxue-lang"); 140 | let theme = localStorage.getItem("noxue-theme"); 141 | 142 | if (lang) { 143 | this.setState({ lang }) 144 | } 145 | if (theme) { 146 | this.setState({ theme }) 147 | } 148 | } 149 | 150 | languageChange = (e) => { 151 | console.log(e.target.value) 152 | // 切换语言,清空代码和输入输出 153 | this.setState({ lang: e.target.value, stdin: "", stdout: "" }); 154 | window.localStorage.setItem("noxue-lang", e.target.value); 155 | } 156 | themeChange = (e) => { 157 | console.log(e.target.value) 158 | this.setState({ theme: e.target.value }); 159 | window.localStorage.setItem("noxue-theme", e.target.value); 160 | } 161 | 162 | codeChange = (value) => { 163 | console.log(value) 164 | this.setState({ code: value }) 165 | window.localStorage.setItem("noxue-code", value); 166 | } 167 | 168 | stdinChange = (e) => { 169 | this.setState({ stdin: e.target.value }) 170 | } 171 | 172 | runCode = (e) => { 173 | const that = this 174 | 175 | // 防止重复提交 176 | if (this.state.running) return; 177 | 178 | this.setState({ running: true }); 179 | 180 | axios({ 181 | method: 'post', 182 | url: process.env.NODE_ENV === "development" ? 'https://code.noxue.com/api/run' : '/api/run', 183 | 184 | data: { 185 | lang: this.state.lang, 186 | code: this.state.code, 187 | input: this.state.stdin, 188 | } 189 | }).then(function (response) { 190 | 191 | // 请求结束后,切换到输出界面,并清空输入内容 192 | that.setState({ running: false, tabIndex: 0, stdin: "" }); 193 | 194 | const res = response.data; 195 | 196 | if (res.code !== 0) { 197 | that.setState({ stdout: res.msg, stderr: '' }); 198 | return; 199 | } 200 | 201 | that.setState({ 202 | stdout: res.data.stdout, 203 | stderr: res.data.stderr, 204 | }) 205 | 206 | }).catch((e) => { 207 | console.log("请求出错:", e) 208 | that.setState({ running: false, stdout: "请求出错:" + e }); 209 | }); 210 | } 211 | 212 | // 鼠标按下时,设置为开始拖动 213 | tabsMouseDown = (e) => { 214 | console.log(this.editerRef.current.view.dom.clientHeight) 215 | console.log(this.resultRef.current.clientHeight) 216 | this.setState({ isDrag: true, prevPos: 0 }) 217 | } 218 | 219 | // 鼠标移动时,如果已经开始拖动,那就根据鼠标上下的移动距离修改编辑器高度 220 | tabsMouseMove = (e) => { 221 | console.log("move:", e) 222 | if (this.state.isDrag) { 223 | 224 | // 为了兼容移动端和电脑端 225 | const screenY = e.screenY || e.touches[0].screenY; 226 | 227 | // 相同值不处理,因为没移动 228 | if (this.state.prevPos === screenY) return; 229 | 230 | // 如果初始值为0,需要先记录第一个值 231 | if (this.state.prevPos === 0) { 232 | this.setState({ 233 | prevPos: screenY 234 | }) 235 | return; 236 | } 237 | 238 | 239 | console.log(this.state.editorHeight, this.state.prevPos, screenY); 240 | 241 | // 处理后记录当前值,作为下次移动参考,同时改变编辑器大小 242 | this.setState({ 243 | editorHeight: this.state.editorHeight + (screenY - this.state.prevPos), 244 | prevPos: screenY 245 | }) 246 | } 247 | } 248 | 249 | // 鼠标松开时,停止拖动 250 | tabsMouseUp = (e) => { 251 | this.setState({ isDrag: false, prevPos: 0 }) 252 | } 253 | 254 | 255 | render() { 256 | 257 | return ( 258 |
264 |
265 | 266 |
267 |
268 |
269 | 不学网 270 | 本站源码 271 |
272 |
273 | 274 | 280 |
281 |
282 | 283 | 290 |
291 |
292 | 293 | 302 | 303 | {/* 高度根据编辑器高度自动计算,这样只要修改编辑器高度就可以实现分割条的自由拖动 */} 304 |
305 | 306 |
307 |
{ 308 | this.setState({ tabIndex: 0 }) 309 | }}>标准输出
310 |
{ 311 | this.setState({ tabIndex: 1 }) 312 | }}>输入
313 |
运行
314 |
315 | { 316 | this.state.tabIndex === 1 ? ( 317 |
318 |