feat: Monaco editor for StringEditor

- Install monaco-editor + @guolao/vue-monaco-editor
- main.ts: loader.config + MonacoEnvironment worker setup
- StringEditor.vue: replace textarea with Monaco editor
- JSON syntax highlighting, folding, word wrap
- Read-only mode when not editing, auto-detect JSON
- Theme follows app theme (vs-dark/vs)
- Font size follows app settings
This commit is contained in:
2026-07-07 20:37:04 +08:00
parent 6a1f94cd48
commit 05a6f6aa17
5 changed files with 156 additions and 23 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
- [x] **自动更新** — electron-updater 集成,启动检查,下载进度,发行说明
- [x] **多标签页** — 单连接支持同时打开 Status/Keys/CLI/SlowLog/内存分析/批量删除 多个标签
- [x] **多连接并行** — 同时打开多个连接,各自独立标签页
- [ ] **Monaco 编辑器** — 替换 StringEditor 的 textarea,支持 JSON 折叠/展开/语法高亮
- [x] **Monaco 编辑器** — 替换 StringEditor 的 textarea,支持 JSON 折叠/展开/语法高亮
- [ ] **Redis 命令自动补全** — CLI 输入时智能提示(基于 commands.js 命令数据库)
- [ ] **CLI SUBSCRIBE/MONITOR** — 实时消息流 + MONITOR 命令流 + 停止按钮
- [ ] **CLI MULTI/EXEC** — 事务模式支持
+101
View File
@@ -9,9 +9,11 @@
"version": "2.0.0",
"hasInstallScript": true,
"dependencies": {
"@guolao/vue-monaco-editor": "^1.6.0",
"electron-store": "^11.0.2",
"electron-updater": "^6.8.9",
"ioredis": "^5.7.0",
"monaco-editor": "^0.55.1",
"pinia": "^3.0.4",
"ssh2": "^1.17.0"
},
@@ -1131,6 +1133,52 @@
"dev": true,
"license": "MIT"
},
"node_modules/@guolao/vue-monaco-editor": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.6.0.tgz",
"integrity": "sha512-w2IiJ6eJGGeuIgCK6EKZOAfhHTTUB5aZwslzwGbZ5e89Hb4avx6++GkLTW8p84Sng/arFMjLPPxSBI56cFudyQ==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.6.1",
"vue-demi": "latest"
},
"peerDependencies": {
"@vue/composition-api": "^1.7.2",
"monaco-editor": ">=0.43.0",
"vue": "^2.6.14 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@guolao/vue-monaco-editor/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@ioredis/commands": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
@@ -1344,6 +1392,15 @@
"node": ">=10"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@npmcli/fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
@@ -1974,6 +2031,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/verror": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
@@ -3752,6 +3816,15 @@
"license": "MIT",
"optional": true
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dot-prop": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz",
@@ -5568,6 +5641,18 @@
"node": ">=12"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -5823,6 +5908,16 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6969,6 +7064,12 @@
"node": ">= 6"
}
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+2
View File
@@ -64,9 +64,11 @@
}
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.6.0",
"electron-store": "^11.0.2",
"electron-updater": "^6.8.9",
"ioredis": "^5.7.0",
"monaco-editor": "^0.55.1",
"pinia": "^3.0.4",
"ssh2": "^1.17.0"
},
+37 -22
View File
@@ -1,17 +1,26 @@
<script setup lang="ts">
// src/renderer/src/components/StringEditor.vue
import { ref, watch } from 'vue'
// src/components/StringEditor.vue
import { ref, watch, computed } from 'vue'
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { useAppStore } from '@renderer/stores/app'
import { ElMessage } from 'element-plus'
const keyStore = useKeyStore()
const connStore = useConnectionStore()
const appStore = useAppStore()
const editedValue = ref('')
const isJson = ref(false)
const isEditing = ref(false)
const monacoTheme = computed(() => {
if (appStore.theme === 'light') return 'vs'
if (appStore.theme === 'dark') return 'vs-dark'
return 'vs-dark' // system default (will be refined)
})
function formatValue(val: string | null) {
if (val === null) return ''
try {
@@ -35,6 +44,25 @@ watch(
{ immediate: true }
)
const editorOptions = computed(() => ({
readOnly: !isEditing.value,
minimap: { enabled: false },
fontSize: appStore.fontSize,
wordWrap: 'on' as const,
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
lineNumbers: 'on' as const,
folding: true,
renderWhitespace: 'none',
scrollbar: {
vertical: 'auto' as const,
horizontal: 'auto' as const,
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
},
}))
async function save() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
@@ -59,14 +87,12 @@ async function save() {
</template>
</div>
<div class="editor-content">
<el-input
v-model="editedValue"
type="textarea"
:rows="12"
:disabled="!isEditing"
:class="{ json: isJson }"
spellcheck="false"
class="editor-textarea"
<vue-monaco-editor
v-model:value="editedValue"
:theme="monacoTheme"
:language="isJson ? 'json' : 'plaintext'"
:options="editorOptions"
height="100%"
/>
</div>
</div>
@@ -100,17 +126,6 @@ async function save() {
.editor-content {
flex: 1;
padding: 12px;
overflow: auto;
}
.editor-textarea :deep(.el-textarea__inner) {
background: var(--bg-primary);
border-color: var(--border-color);
color: var(--text-primary);
font-family: 'Cascadia Code', 'Fira Code', monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
overflow: hidden;
}
</style>
+15
View File
@@ -4,9 +4,24 @@ import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { loader } from '@guolao/vue-monaco-editor'
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import App from './App.vue'
import './styles/global.css'
// Monaco worker setup for Vite
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') return new jsonWorker()
return new editorWorker()
},
}
// Use local monaco-editor
loader.config({ monaco })
const app = createApp(App)
app.use(createPinia())
app.use(ElementPlus, { size: 'small' })