feat: CodeMirror6代码编辑器替代textarea,暗色主题+语法高亮
This commit is contained in:
@@ -13,7 +13,12 @@
|
|||||||
"vue-router": "^4.2.0",
|
"vue-router": "^4.2.0",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
"element-plus": "^2.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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@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="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">
|
<div style="font-size:13px;font-weight:500;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">
|
||||||
<span>Groovy 脚本</span>
|
<span>Groovy 脚本</span>
|
||||||
<el-button size="small" type="primary" @click="saveScript" :loading="saving" v-if="selectedConfigId">保存</el-button>
|
<el-button size="small" type="primary" @click="saveScript" :loading="saving" v-if="selectedConfigId">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="scriptContent" type="textarea" :rows="1" style="flex:1;font-family:monospace;font-size:11px;line-height:1.4"
|
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
|
||||||
placeholder="// 先选配置版本,再编辑脚本 // 内置变量: content, url, jsoup, json, utils" />
|
<CodeEditor v-model="scriptContent" />
|
||||||
|
</div>
|
||||||
<div style="font-size:10px;color:#909399;margin-top:2px">
|
<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>
|
内置: <code>content</code> <code>url</code> <code>jsoup</code> <code>json</code> <code>utils</code>
|
||||||
· 返回 Map{pageName,extracted} 或 List
|
· 返回 Map{pageName,extracted} 或 List
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
|
import CodeEditor from '../components/CodeEditor.vue'
|
||||||
import { Loading } from '@element-plus/icons-vue'
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
Reference in New Issue
Block a user