在线代码编辑器
- 文章说明
- 前台核心代码
- 后台核心代码
- 效果展示
- 源码下载
文章说明
采用Java结合vue3设计实现的在线代码编辑功能,支持在线编辑代码、运行代码,同时支持导入文件,支持图片识别,支持复制代码,可将代码导出为图片
暂时还未添加图片代码识别导入的功能,然后目前Linux版本下安装C#的编译器我还没找到好的方案,这个C#代码运行在Linux环境下暂时有点问题
目前对于代码安全方面还未添加校验,只作为简单的学习使用,存在着不小的风险,后续会考虑新开坑,书写更完善的在线编辑器
目前的这个结果展示的编码方面还有一些问题,主要是控制台输入、命令行以及文件本身的编码会有不一致的情况,有些难整,后续考虑采用新方案给这个问题解决一下
前台核心代码
前台主要采用 md-editor-v3 编辑器作为代码编辑区域,虽然没有提示功能,但是展示效果上会好一些,然后运行的时候调用后台接口,采用Java调用cmd命令行来实现代码的运行
其中代码下载为图片采用的是对dom对象进行下载,采用html2canvas来实现,会有一些小bug,不过效果感觉还可以
前台核心代码
<script setup>
import {MdEditor, MdPreview} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import 'md-editor-v3/lib/preview.css';
import {computed, onBeforeMount, reactive, ref, watch} from "vue";
import html2canvas from "html2canvas";
import {
confirm,
downloadFileToLocalByUrl,
exportCommon,
getRequest,
loading,
message,
postRequest,
prompt
} from "@/util";
const data = reactive({
language: "",
codeText: "",
inputText: "",
displayText: "",
exportDialogVisible: false,
exportImgUrl: "",
uploadExist: true,
});
const languageList = [
"Java",
"Python",
"Javascript",
"C",
"C++",
"C#",
];
const codeText = computed(() => {
return "```" + data.language + "\n" + data.codeText + "\n```"
});
function exportCodeToImg() {
const displayContent = document.getElementsByClassName("display-content")[0];
data.exportDialogVisible = true;
html2canvas(displayContent, {}).then((canvas) => {
data.exportImgUrl = canvas.toDataURL('image/png');
});
}
function exportCode() {
prompt("导出代码文件", "导出代码文件名称", data.language + "_" + new Date().toLocaleString() + ".txt", (value) => {
exportCommon(data.codeText, value);
});
}
function exportCodeToImgConfirm() {
prompt("导出代码图片", "导出代码图片名称", data.language + "_" + new Date().toLocaleString() + ".png", (value) => {
downloadFileToLocalByUrl(data.exportImgUrl, value);
data.exportDialogVisible = false;
});
}
function reset() {
confirm("确认清空吗,包括代码区域、控制台文件输入以及结果展示区域?", () => {
data.codeText = "";
data.inputText = "";
data.displayText = "";
});
}
function run() {
if (!data.language) {
message("请先选择编程语言", "warning");
return;
}
const loadingInstance = loading("运行中...");
postRequest("/codeRunner/run", null, {
language: data.language,
codeText: data.codeText,
inputText: data.inputText,
}).then((res) => {
loadingInstance.close();
if (res.data.code === 200) {
data.displayText = res.data.data;
message("代码运行成功", "success");
} else if (res.data.code === 500) {
message("代码运行失败", "error");
}
})
}
const languageText = {};
onBeforeMount(async () => {
for (let i = 0; i < languageList.length; i++) {
languageText[languageList[i]] = await getLanguageDefaultText(languageList[i]);
}
});
async function getLanguageDefaultText(language) {
const res = await getRequest("/codeRunner/getDefaultText", {
language
});
if (res.data.code === 200) {
return res.data.data;
} else if (res.data.code === 500) {
message(language + "语言默认代码获取失败", "error");
}
}
watch(() => data.language, (newValue, oldValue) => {
if (data.codeText) {
languageText[oldValue] = data.codeText;
}
data.codeText = languageText[data.language];
});
const uploadFileRef = ref();
function uploadFile(event) {
const loadingInstance = loading("导入中...");
const file = event.target.files[0];
const reader = new FileReader();
reader.readAsText(file, 'utf8');
data.uploadExist = false;
reader.onload = () => {
loadingInstance.close();
const codeText = reader.result.toString();
data.uploadExist = true;
if (!codeText) {
message("导出文件内容为空", "warning");
return;
}
data.codeText = codeText;
}
}
function singleImport() {
if (!data.language) {
message("请先选择编程语言", "warning");
return;
}
if (!data.uploadExist) {
return;
}
uploadFileRef.value.click();
}
</script>
<template>
<div class="container">
<div class="tool-bar">
<div class="row">
<el-select v-model="data.language" placeholder="请选择编程语言" style="width: 80%">
<template v-for="item in languageList" :key="item">
<el-option :label="item" :value="item"/>
</template>
</el-select>
</div>
<div class="row" style="margin-top: -0.5rem">
<el-button type="primary" @click="run">运行</el-button>
<el-button type="danger" @click="reset">清空</el-button>
<el-button type="danger" @click="singleImport">导入代码</el-button>
<input v-if="data.uploadExist" ref="uploadFileRef" accept=".txt,.java,.py,.js,.c,.cpp,.cs" style="display: none"
type="file" @change="uploadFile($event)">
</div>
<div class="row" style="margin-top: -0.5rem">
<el-button type="primary" @click="exportCode">导出代码</el-button>
<el-button type="danger" @click="exportCodeToImg">下载为图片</el-button>
</div>
</div>
<div class="content-container">
<div class="editor-content">
<MdEditor v-model="data.codeText" :preview="false" :toolbars="[]" placeholder="代码输入区域"
style="height: 100%; width: 100%" theme="dark"/>
</div>
<div class="display-content">
<MdPreview v-model="codeText" :show-code-row-number="false" style="height: 100%; width: 100%"/>
</div>
<div class="input-content">
<MdEditor v-model="data.inputText" :preview="false" :toolbars="[]" placeholder="控制台文件输入区域"
style="height: 100%; width: 100%" theme="dark"/>
</div>
<div class="result-content">
<MdEditor v-model="data.displayText" :preview="false" :toolbars="[]" placeholder="结果展示区域" read-only
style="height: 100%; width: 100%" theme="dark"/>
</div>
</div>
</div>
<el-dialog v-model="data.exportDialogVisible" title="代码下载为图片" width="90%">
<img :src="data.exportImgUrl" alt="" style="width: 100%; height: auto"/>
<template #footer>
<el-button type="primary" @click="data.exportDialogVisible = false">取消</el-button>
<el-button type="danger" @click="exportCodeToImgConfirm">确认</el-button>
</template>
</el-dialog>
</template>
<style lang="scss">
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
.tool-bar {
height: 9rem;
background-color: #282c34;
border-bottom: 0.1rem solid gray;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
.row {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.content-container {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.md-editor-footer {
display: none;
}
.cm-scroller {
background-color: #282c34;
}
.editor-content {
height: fit-content;
border-bottom: 0.1rem solid gray;
}
.display-content {
height: fit-content;
border-bottom: 0.1rem solid gray;
.md-editor-preview-wrapper {
padding: 0;
}
.md-editor-code {
margin: 0;
}
.md-editor-code-head {
border-radius: 0 !important;
}
code {
border-radius: 0 !important;
white-space: pre-wrap;
}
}
.input-content {
flex: 1;
background-color: #282c34;
border-bottom: 0.1rem solid gray;
}
.result-content {
flex: 1;
background-color: #282c34;
}
}
}
</style>
后台核心代码
后台采用命令行运行代码,在Java、C、C++、C#,代码运行时会需要多步骤进行,目前采用的都是将代码写入到指定文件,然后采用指定命令运行绝对路径的文件,限制较大,后续考虑运行项目的形式,可以更加方便的解决文件编码问题以及代码运行问题
Java调用cmd运行代码工具类
package com.boot.util;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import static com.boot.util.DefaultConfig.*;
import static com.boot.util.LanguageType.*;
/**
* @author bbyh
* @date 2023/2/27 0027 15:10
*/
@Slf4j
public class ExecUtil {
public static String exec(String language, String codeText, String inputText) throws Exception {
File codeTextInputFile = new File(LANGUAGE_INPUT_FILE_MAP.get(language));
if (codeTextInputFile.exists()) {
try (FileOutputStream outputStream = new FileOutputStream(codeTextInputFile)) {
outputStream.write(codeText.getBytes(StandardCharsets.UTF_8));
}
}
File inputTextInputFile = new File(INPUT_TEXT_PUT_FILE);
if (inputTextInputFile.exists()) {
try (FileOutputStream outputStream = new FileOutputStream(inputTextInputFile)) {
outputStream.write(inputText.getBytes(StandardCharsets.UTF_8));
}
}
if (judgeLinux()) {
int exeCode = execLinux(language);
logCodeTextRunLog(language, codeText, inputText, exeCode);
} else if (judgeWindows()) {
int exeCode = execWindows(language);
logCodeTextRunLog(language, codeText, inputText, exeCode);
}
String[] outputFileNames = {ERROR_OUTPUT_FILE, OUTPUT_FILE};
for (String outputFileName : outputFileNames) {
File outputFile = new File(outputFileName);
if (outputFile.exists()) {
try (FileInputStream inputStream = new FileInputStream(outputFile)) {
byte[] buf = new byte[1024 * 1024];
int read = inputStream.read(buf);
if (read >= 0) {
String result = new String(buf, 0, read);
String encoding = getEncoding(result);
if (!"".equals(encoding)) {
return new String(buf, 0, read, encoding);
}
}
}
}
}
return "";
}
private static void logCodeTextRunLog(String language, String codeText, String inputText, int exeCode) {
log.info("运行时间:{}", new Date());
log.info("编程语言:{}", language);
log.info("运行代码:{}", codeText);
log.info("运行输入:{}", inputText);
log.info("运行结果:{}", exeCode);
}
private static int execLinux(String language) throws Exception {
switch (language) {
case JAVA:
if (execLinuxCommand("javac " + JAVA_INPUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
int start = JAVA_INPUT_FILE.lastIndexOf("/") + 1;
int end = JAVA_INPUT_FILE.lastIndexOf(".");
String javaClassFileName = JAVA_INPUT_FILE.substring(start, end);
return execLinuxCommand("java -classpath " + LINUX_PREFIX + " " + javaClassFileName + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
case PYTHON:
return execLinuxCommand("python " + PYTHON_INPUT_FILE + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
case JAVASCRIPT:
return execLinuxCommand("node " + JAVASCRIPT_INPUT_FILE + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
case C:
if (execLinuxCommand("gcc " + C_INPUT_FILE + " -o " + LINUX_PREFIX + "a.exe > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execLinuxCommand(LINUX_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
case C_PLUS_PLUS:
if (execLinuxCommand("g++ " + C_PLUS_PLUS_INPUT_FILE + " -o " + LINUX_PREFIX + "a.exe > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execLinuxCommand(LINUX_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + "> " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
case C_SHARP:
if (execLinuxCommand("csc /out:" + LINUX_PREFIX.replaceAll("/", "\\\\") + "a.exe "
+ C_SHARP_INPUT_FILE.replaceAll("/", "\\\\") + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execLinuxCommand(LINUX_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
}
return 0;
}
private static int execLinuxCommand(String command) throws Exception {
ArrayList<String> commandList = new ArrayList<>();
commandList.add("/bin/sh");
commandList.add("-c");
commandList.add(command);
Process exec = Runtime.getRuntime().exec(commandList.toArray(new String[0]));
return exec.waitFor();
}
private static int execWindows(String language) throws Exception {
switch (language) {
case JAVA:
if (execWindowsCommand("javac " + JAVA_INPUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
int start = JAVA_INPUT_FILE.lastIndexOf("/") + 1;
int end = JAVA_INPUT_FILE.lastIndexOf(".");
String javaClassFileName = JAVA_INPUT_FILE.substring(start, end);
return execWindowsCommand("java -classpath " + WINDOWS_PREFIX + " " + javaClassFileName + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
break;
case PYTHON:
return execWindowsCommand("python " + PYTHON_INPUT_FILE + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
case JAVASCRIPT:
return execWindowsCommand("node " + JAVASCRIPT_INPUT_FILE + " < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
case C:
if (execWindowsCommand("gcc " + C_INPUT_FILE + " -o " + WINDOWS_PREFIX + "a.exe > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execWindowsCommand(WINDOWS_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
case C_PLUS_PLUS:
if (execWindowsCommand("g++ " + C_PLUS_PLUS_INPUT_FILE + " -o " + WINDOWS_PREFIX + "a.exe > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execWindowsCommand(WINDOWS_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
case C_SHARP:
if (execWindowsCommand("csc /out:" + WINDOWS_PREFIX.replaceAll("/", "\\\\") + "a.exe "
+ C_SHARP_INPUT_FILE.replaceAll("/", "\\\\") + " > " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE) == 0) {
return execWindowsCommand(WINDOWS_PREFIX + "a.exe < " + INPUT_TEXT_PUT_FILE + "> " + OUTPUT_FILE + " 2> " + ERROR_OUTPUT_FILE);
}
return 0;
}
return 0;
}
private static int execWindowsCommand(String command) throws Exception {
Process exec = Runtime.getRuntime().exec("cmd /c " + command);
return exec.waitFor();
}
public static Boolean judgeWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
public static Boolean judgeLinux() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
public static String getEncoding(String str) throws Exception {
String[] encodes = {"UTF-8", "GBK"};
for (String encode : encodes) {
if (str.equals(new String(str.getBytes(encode), encode))) {
return encode;
}
}
return "";
}
}
效果展示
运行Java代码
运行Python代码
运行JavaScript代码
运行C语言代码
运行C++代码
运行C#代码
将代码下载为图片
源码下载
在线代码编辑器