frontend-oj/src/components/CodeEditor/CodeEditor.vue

193 lines
5.1 KiB
Vue

<template>
<div class="editor-container">
<div ref="editor" class="code-editor"></div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from "vue";
import { EditorView, keymap, gutter, GutterMarker, lineNumbers } from "@codemirror/view";
import { EditorState, StateField, StateEffect } from "@codemirror/state";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { RangeSet } from "@codemirror/rangeset";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";
import { indentUnit } from "@codemirror/language";
class BreakpointMarker extends GutterMarker {
toDOM() {
const marker = document.createElement("div");
marker.className = "breakpoint-marker";
marker.textContent = "●";
return marker;
}
}
export default {
name: "CodeEditor",
props: {
modelValue: String, // 双向绑定代码内容
language: String, // 代码语言,默认 JS
height: {
type: String,
default: '600px'
}
},
emits: ["update:modelValue"], // 监听代码变更
setup(props, { emit }) {
const editor = ref(null);
let editorView = null;
const setBreakpoint = StateEffect.define();
const breakpointsField = StateField.define({
create: () => new Set(),
update(breakpoints, tr) {
const newBreakpoints = new Set(breakpoints);
for (let effect of tr.effects) {
if (effect.is(setBreakpoint)) {
const line = effect.value;
if (newBreakpoints.has(line)) {
newBreakpoints.delete(line);
} else {
newBreakpoints.add(line);
}
}
}
return newBreakpoints;
},
});
const breakpointGutter = gutter({
class: "cm-breakpoint-gutter",
markers: (view) => {
const breakpoints = view.state.field(breakpointsField);
const markers = [];
for (const line of breakpoints) {
try {
const lineInfo = view.state.doc.line(line);
markers.push(new BreakpointMarker().range(lineInfo.from));
} catch (e) {
console.warn("Invalid line number skipped:", line);
}
}
return RangeSet.of(markers, true);
},
domEventHandlers: {
click: (view, block) => {
const lineNumber = view.state.doc.lineAt(block.from).number;
view.dispatch({
effects: [setBreakpoint.of(lineNumber)],
});
return true;
},
},
});
onMounted(() => {
if (!editor.value) return;
editorView = new EditorView({
state: EditorState.create({
doc: props.modelValue || "",
extensions: [
lineNumbers(),
breakpointGutter,
breakpointsField,
javascript(),
oneDark,
indentUnit.of(" "), // 使用4个空格作为缩进
keymap.of([
...defaultKeymap,
indentWithTab
]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
emit("update:modelValue", update.state.doc.toString());
}
}),
EditorView.theme({
"&": {
fontSize: "14px",
height: "100%"
},
".cm-content": {
fontFamily: "'Fira Code', monospace",
padding: "12px 0",
},
".cm-line": {
padding: "0 12px",
lineHeight: "1.6"
},
".cm-gutters": {
backgroundColor: "#1e1e1e",
color: "#666",
border: "none"
},
".cm-activeLineGutter": {
backgroundColor: "#333"
},
".cm-activeLine": {
backgroundColor: "rgba(255,255,255,0.03)"
}
}),
EditorView.baseTheme({
".cm-breakpoint-gutter": {
width: "30px",
backgroundColor: "#1e1e1e",
borderRight: "1px solid #333"
},
".breakpoint-marker": {
color: "#ff5555",
fontSize: "14px",
cursor: "pointer",
textAlign: "center"
},
}),
],
}),
parent: editor.value,
});
});
onBeforeUnmount(() => {
editorView?.destroy();
});
return { editor };
},
};
</script>
<style>
.editor-container {
width: 100%;
height: 100%;
background: #282c34;
}
.code-editor {
height: 100%;
overflow: auto;
}
/* 自定义滚动条样式 */
.code-editor ::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.code-editor ::-webkit-scrollbar-track {
background: #21252b;
}
.code-editor ::-webkit-scrollbar-thumb {
background: #454c59;
border-radius: 6px;
border: 3px solid #21252b;
}
.code-editor ::-webkit-scrollbar-thumb:hover {
background: #535b6a;
}
</style>