Files
toolbox/resources/js/components/env/CodeEditor.vue
2025-12-02 10:16:32 +08:00

290 lines
6.5 KiB
Vue

<template>
<div class="codemirror-editor">
<div ref="editorContainer" class="editor-container"></div>
</div>
</template>
<script>
import { EditorView, keymap, highlightActiveLine, highlightActiveLineGutter, lineNumbers, scrollPastEnd } from '@codemirror/view'
import { EditorState, Compartment } from '@codemirror/state'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'
import { foldGutter, indentOnInput, indentUnit, bracketMatching } from '@codemirror/language'
import { highlightSelectionMatches as highlightSelection } from '@codemirror/search'
import { javascript } from '@codemirror/lang-javascript'
import { php } from '@codemirror/lang-php'
import { oneDark } from '@codemirror/theme-one-dark'
import { env } from '../../lang-env.js'
export default {
name: 'CodeEditor',
props: {
modelValue: {
type: String,
default: ''
},
language: {
type: String,
default: 'env' // 'env', 'javascript', 'php'
},
theme: {
type: String,
default: 'light' // 'light', 'dark'
},
readonly: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'change'],
data() {
return {
editor: null,
languageCompartment: new Compartment(),
themeCompartment: new Compartment()
}
},
mounted() {
this.initEditor();
},
watch: {
modelValue(newValue) {
if (this.editor && newValue !== this.editor.state.doc.toString()) {
this.editor.dispatch({
changes: {
from: 0,
to: this.editor.state.doc.length,
insert: newValue || ''
}
});
}
},
language() {
this.updateLanguage();
},
theme() {
this.updateTheme();
}
},
beforeUnmount() {
if (this.editor) {
this.editor.destroy();
}
},
methods: {
getLanguageExtension() {
switch (this.language) {
case 'javascript':
return javascript();
case 'php':
return php();
case 'env':
return env();
default:
return [];
}
},
getThemeExtension() {
return this.theme === 'dark' ? oneDark : [];
},
initEditor() {
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
highlightActiveLine(),
foldGutter(),
indentOnInput(),
indentUnit.of(' '),
bracketMatching(),
closeBrackets(),
autocompletion(),
highlightSelectionMatches(),
history(),
scrollPastEnd(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
]),
this.languageCompartment.of(this.getLanguageExtension()),
this.themeCompartment.of(this.getThemeExtension()),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const content = update.state.doc.toString();
this.$emit('update:modelValue', content);
this.$emit('change', content);
}
}),
// 确保编辑器可以滚动
EditorView.theme({
'&': {
height: '100%'
},
'.cm-scroller': {
fontFamily: 'inherit',
overflow: 'auto'
},
'.cm-content': {
padding: '12px',
minHeight: '100%'
},
'.cm-editor': {
height: '100%'
},
'.cm-focused': {
outline: 'none'
}
})
];
// 添加只读模式
if (this.readonly) {
extensions.push(EditorView.editable.of(false));
}
const state = EditorState.create({
doc: this.modelValue || '',
extensions
});
this.editor = new EditorView({
state,
parent: this.$refs.editorContainer
});
},
updateLanguage() {
if (!this.editor) return;
this.editor.dispatch({
effects: this.languageCompartment.reconfigure(this.getLanguageExtension())
});
},
updateTheme() {
if (!this.editor) return;
this.editor.dispatch({
effects: this.themeCompartment.reconfigure(this.getThemeExtension())
});
}
}
}
</script>
<style scoped>
.codemirror-editor {
width: 100%;
height: 100%;
border-radius: 8px;
border: 1px solid #e5e7eb;
background: #ffffff;
display: flex;
flex-direction: column;
}
.editor-container {
width: 100%;
height: 100%;
flex: 1;
min-height: 0;
}
/* CodeMirror 自定义样式 */
.codemirror-editor :deep(.cm-editor) {
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Monaco', 'Inconsolata', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
height: 100%;
}
.codemirror-editor :deep(.cm-scroller) {
overflow: auto !important;
max-height: 100%;
}
/* 基本语法高亮样式 */
.codemirror-editor :deep(.cm-variableName) {
color: #0969da;
font-weight: 600;
}
.codemirror-editor :deep(.cm-string) {
color: #0a3069;
}
.codemirror-editor :deep(.cm-number) {
color: #0550ae;
}
.codemirror-editor :deep(.cm-keyword) {
color: #8250df;
font-weight: 500;
}
.codemirror-editor :deep(.cm-operator) {
color: #cf222e;
}
.codemirror-editor :deep(.cm-comment) {
color: #6a737d;
font-style: italic;
}
.codemirror-editor :deep(.cm-content) {
padding: 12px;
min-height: 100%;
}
.codemirror-editor :deep(.cm-focused) {
outline: none;
}
/* 自定义滚动条样式 */
.codemirror-editor :deep(.cm-scroller)::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.codemirror-editor :deep(.cm-scroller)::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.codemirror-editor :deep(.cm-scroller)::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.codemirror-editor :deep(.cm-scroller)::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 深色主题样式 */
.codemirror-editor.dark :deep(.cm-editor) {
background: #1e1e1e;
color: #d4d4d4;
}
.codemirror-editor.dark :deep(.cm-scroller)::-webkit-scrollbar-track {
background: #2d2d2d;
}
.codemirror-editor.dark :deep(.cm-scroller)::-webkit-scrollbar-thumb {
background: #555;
}
.codemirror-editor.dark :deep(.cm-scroller)::-webkit-scrollbar-thumb:hover {
background: #777;
}
</style>