feat: CodeMirror6代码编辑器替代textarea,暗色主题+语法高亮

This commit is contained in:
mediabot-pt
2026-07-01 13:38:56 +08:00
parent 621e436eb0
commit e05d337567
3 changed files with 78 additions and 4 deletions

View File

@@ -13,7 +13,12 @@
"vue-router": "^4.2.0",
"axios": "^1.7.0",
"element-plus": "^2.7.0",
"@element-plus/icons-vue": "^2.3.0"
"@element-plus/icons-vue": "^2.3.0",
"codemirror": "^6.0.1",
"@codemirror/view": "^6.35.0",
"@codemirror/state": "^6.5.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/theme-one-dark": "^6.1.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",

View File

@@ -0,0 +1,67 @@
<template>
<div ref="editorEl" style="height:100%;overflow:hidden"></div>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue'
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor, highlightActiveLine } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language'
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from '@codemirror/autocomplete'
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'
import { lintKeymap } from '@codemirror/lint'
const props = defineProps({ modelValue: { type: String, default: '' } })
const emit = defineEmits(['update:modelValue'])
const editorEl = ref(null)
let view = null
onMounted(() => {
view = new EditorView({
doc: props.modelValue,
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap,
...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap
]),
javascript(), // 用 JS 语法高亮近似 Groovy
oneDark,
EditorView.updateListener.of(update => {
if (update.docChanged) {
emit('update:modelValue', update.state.doc.toString())
}
}),
EditorView.theme({ '&': { height: '100%' }, '.cm-scroller': { overflow: 'auto' } })
],
parent: editorEl.value
})
})
watch(() => props.modelValue, val => {
if (view && val !== view.state.doc.toString()) {
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: val } })
}
})
onUnmounted(() => { view?.destroy() })
</script>

View File

@@ -34,13 +34,14 @@
<!-- 主体:脚本 + 结果 -->
<div style="display:flex;gap:12px;flex:1;min-height:0">
<!-- 左侧:脚本编辑器 -->
<div style="flex:0 0 420px;display:flex;flex-direction:column;min-height:0">
<div style="flex:0 0 440px;display:flex;flex-direction:column;min-height:0">
<div style="font-size:13px;font-weight:500;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">
<span>Groovy 脚本</span>
<el-button size="small" type="primary" @click="saveScript" :loading="saving" v-if="selectedConfigId">保存</el-button>
</div>
<el-input v-model="scriptContent" type="textarea" :rows="1" style="flex:1;font-family:monospace;font-size:11px;line-height:1.4"
placeholder="// 先选配置版本,再编辑脚本&#10;// 内置变量: content, url, jsoup, json, utils" />
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
<CodeEditor v-model="scriptContent" />
</div>
<div style="font-size:10px;color:#909399;margin-top:2px">
内置: <code>content</code> <code>url</code> <code>jsoup</code> <code>json</code> <code>utils</code>
· 返回 Map{pageName,extracted} 或 List
@@ -111,6 +112,7 @@
<script setup>
import { useRoute } from 'vue-router'
import api from '../api'
import CodeEditor from '../components/CodeEditor.vue'
import { Loading } from '@element-plus/icons-vue'
const route = useRoute()