feat: CodeMirror6代码编辑器替代textarea,暗色主题+语法高亮
This commit is contained in:
@@ -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",
|
||||
|
||||
67
web/src/components/CodeEditor.vue
Normal file
67
web/src/components/CodeEditor.vue
Normal 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>
|
||||
@@ -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="// 先选配置版本,再编辑脚本 // 内置变量: 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()
|
||||
|
||||
Reference in New Issue
Block a user