Private
Public Access
1
0
Files
u-desk/web/src/components/CodeEditor.vue
绝尘 0229cab550 重构:CodeMirror 架构优化
核心优化:
- 新增统一导出避免多实例问题
- 语言加载器从动态改为静态导入
- 使用 Compartment 实现主题/语言动态切换

依赖清理:
- 移除废弃的 @codemirror/highlight
- 移除不再使用的 @codemirror/legacy-modes

组件优化:
- CodeEditor 添加内容更新防抖
- 改进亮色主题样式
- 移除不必要的编辑器重建逻辑

构建配置:
- 简化 Vite manualChunks 配置
- 优化依赖预加载列表

文档清理:
- 删除过期的代码审查文档
- 更新版本号 0.3.0 → 0.3.2
2026-03-31 11:49:25 +08:00

220 lines
5.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div ref="editorContainer" class="codemirror-editor"></div>
</template>
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, computed, nextTick } from 'vue'
import {
EditorView, lineNumbers, highlightActiveLineGutter, keymap,
EditorState, Compartment,
defaultKeymap, history,
bracketMatching, defaultHighlightStyle, syntaxHighlighting,
oneDark
} from '@/utils/codemirrorExports'
import { useThemeStore } from '@/stores/theme'
import { loadLanguageExtension, getLanguageFromExtension } from '@/utils/codeMirrorLoader'
// ==================== 主题定义 ====================
// 亮色主题的基础样式
const lightTheme = EditorView.theme({
'&': { backgroundColor: '#ffffff' },
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e8e8e8', color: '#333' },
'.cm-line': { caretColor: '#000' },
'.cm-selection': { backgroundColor: '#d9d9d9' },
'.cm-cursor': { borderLeftColor: '#000' }
})
// ==================== Props & Emits ====================
const props = defineProps({
modelValue: { type: String, required: true },
fileExtension: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
// ==================== 状态管理 ====================
const themeStore = useThemeStore()
const editorContainer = ref(null)
let view = null
// 使用 Compartment 实现动态切换,避免重建编辑器
const themeCompartment = new Compartment()
const languageCompartment = new Compartment()
// ==================== 防抖处理 ====================
let emitTimeout = null
const debouncedEmit = (value) => {
if (emitTimeout) {
clearTimeout(emitTimeout)
}
emitTimeout = setTimeout(() => {
emit('update:modelValue', value)
}, 150)
}
// 获取当前主题扩展
const getThemeExtension = () => {
if (themeStore.isDark) {
return [oneDark]
} else {
// 亮色主题:使用默认语法高亮样式
return [
EditorView.theme({
'&': { backgroundColor: '#ffffff' },
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e8e8e8', color: '#333' },
'.cm-line': { caretColor: '#000' },
'.cm-selection': { backgroundColor: '#d9d9d9' },
'.cm-cursor': { borderLeftColor: '#000' }
}),
syntaxHighlighting(defaultHighlightStyle)
]
}
}
// ==================== 扩展配置 ====================
const createExtensions = () => {
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
history(),
keymap.of(defaultKeymap),
bracketMatching(),
// 内容更新监听(带防抖)
EditorView.updateListener.of((update) => {
if (update.docChanged) {
debouncedEmit(update.state.doc.toString())
}
}),
// 基础样式
EditorView.theme({
'&': { height: '100%', fontSize: '13px' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'Consolas, Monaco, Courier New, monospace' },
'.cm-content': { padding: '8px', minHeight: '100%' },
'.cm-line': { padding: '0 0' },
'&.cm-focused': { outline: 'none' }
}),
// 使用 Compartment 支持动态切换主题
themeCompartment.of(getThemeExtension()),
// 使用 Compartment 支持动态切换语言
languageCompartment.of([])
]
return extensions
}
// ==================== 语言管理 ====================
const initLanguage = async () => {
const language = getLanguageFromExtension(props.fileExtension)
if (language === 'text') return
try {
const langExtension = await loadLanguageExtension(language)
if (langExtension && view) {
view.dispatch({
effects: languageCompartment.reconfigure(langExtension)
})
}
} catch (error) {
console.warn(`[CodeEditor] 加载语言包失败: ${language}`, error)
}
}
// ==================== 编辑器创建 ====================
const createEditor = (docContent = '') => {
if (!editorContainer.value) return
const state = EditorState.create({
doc: docContent,
extensions: createExtensions()
})
view = new EditorView({ state, parent: editorContainer.value })
// 初始化语言
initLanguage()
}
// ==================== 生命周期 ====================
onMounted(() => {
createEditor(props.modelValue || '')
// 确保主题正确应用(在下一 tick
nextTick(() => {
if (view) {
view.dispatch({
effects: themeCompartment.reconfigure(getThemeExtension())
})
}
})
})
onBeforeUnmount(() => {
if (emitTimeout) {
clearTimeout(emitTimeout)
}
view?.destroy()
view = null
})
// ==================== 监听器 ====================
// 监听外部内容变化
watch(() => props.modelValue, (newValue) => {
if (view && newValue !== view.state.doc.toString()) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: newValue || '' }
})
}
})
// 监听主题变化(使用 Compartment 重建,不丢失状态)
watch(() => themeStore.isDark, () => {
if (view) {
view.dispatch({
effects: themeCompartment.reconfigure(getThemeExtension())
})
}
})
// 监听文件扩展名变化(重新加载语言)
watch(() => props.fileExtension, () => {
initLanguage()
})
</script>
<style scoped>
.codemirror-editor {
height: 100%;
width: 100%;
overflow: hidden;
}
.codemirror-editor :deep(.cm-editor) {
height: 100%;
width: 100%;
}
.codemirror-editor :deep(.cm-scroller) {
overflow: auto;
height: 100%;
}
.codemirror-editor :deep(.cm-content) {
height: 100%;
}
</style>