Files
nexus-terminal/packages/frontend/src/components/MonacoEditor.vue
T
2025-05-24 00:19:50 +08:00

279 lines
8.7 KiB
Vue

<template>
<div ref="editorContainer" class="monaco-editor-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, defineExpose, defineProps, defineEmits } from 'vue';
import * as monaco from 'monaco-editor';
const FONT_SIZE_STORAGE_KEY = 'monacoEditorFontSize'; // localStorage key
const props = defineProps({
modelValue: {
type: String,
default: '',
},
language: {
type: String,
default: 'plaintext',
},
theme: {
type: String,
default: 'vs-dark',
},
readOnly: {
type: Boolean,
default: false,
},
fontFamily: {
type: String,
default: 'Consolas, "Courier New", monospace',
},
fontSize: { // 新增 prop
type: Number,
default: 14, // 默认字体大小
},
initialScrollTop: {
type: Number,
default: 0,
},
initialScrollLeft: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['update:modelValue', 'request-save', 'update:scrollPosition', 'update:fontSize']); // 添加 'update:fontSize'
const editorContainer = ref<HTMLElement | null>(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
// 用于驱动编辑器实例的 ref,并与 localStorage 和 props.fontSize 同步
const internalEditorFontSize = ref(props.fontSize);
onMounted(() => {
// 优先使用 props.fontSize 初始化 internalEditorFontSize
internalEditorFontSize.value = props.fontSize;
// 检查 localStorage 中是否有用户通过滚轮操作留下的偏好
const storedUserPreference = localStorage.getItem(FONT_SIZE_STORAGE_KEY);
if (storedUserPreference) {
const parsedSize = parseInt(storedUserPreference, 10);
if (!isNaN(parsedSize) && parsedSize >= 8 && parsedSize <= 40) {
// 如果 localStorage 的值与当前的 internalEditorFontSize (来自 prop) 不同,
// 表明用户可能通过滚轮调整过,此时以 localStorage 为准,并通知更新全局状态
if (parsedSize !== internalEditorFontSize.value) {
emit('update:fontSize', parsedSize);
}
internalEditorFontSize.value = parsedSize;
}
}
if (editorContainer.value) {
editorInstance = monaco.editor.create(editorContainer.value, {
value: props.modelValue,
language: props.language,
theme: props.theme,
fontSize: internalEditorFontSize.value, // 使用 internalEditorFontSize
fontFamily: props.fontFamily, // 使用 prop 的字体家族
automaticLayout: true,
readOnly: props.readOnly,
minimap: { enabled: true },
lineNumbers: 'on',
scrollBeyondLastLine: false,
});
editorInstance.onDidChangeModelContent(() => {
if (editorInstance) {
const currentValue = editorInstance.getValue();
if (currentValue !== props.modelValue) {
emit('update:modelValue', currentValue);
}
}
});
// Ctrl+S / Cmd+S
editorInstance.addAction({
id: 'save-file',
label: 'Save File',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
],
precondition: undefined,
keybindingContext: undefined,
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
run: () => {
console.log('[MonacoEditor] Save action triggered (Ctrl+S / Cmd+S)');
emit('request-save');
},
});
// 应用初始滚动位置
if (props.initialScrollTop > 0 || props.initialScrollLeft > 0) {
editorInstance.setScrollPosition({
scrollTop: props.initialScrollTop,
scrollLeft: props.initialScrollLeft,
});
}
// 监听滚动事件
editorInstance.onDidScrollChange((e) => {
if (editorInstance) {
// 只有当滚动是由用户操作或实际视口变化引起时才发出
// setScrollPosition 也会触发此事件,需要避免循环
// 一个简单的检查是,如果事件中的滚动值与 props 中的初始值不同,则认为是有效滚动
// 但更好的方式是父组件在设置初始值后才开始监听此事件,或此组件内部处理
// 为简单起见,我们直接 emit
emit('update:scrollPosition', {
scrollTop: editorInstance.getScrollTop(),
scrollLeft: editorInstance.getScrollLeft(),
});
}
});
editorInstance.onDidChangeModelContent(() => {
if (editorInstance) {
const currentValue = editorInstance.getValue();
if (currentValue !== props.modelValue) {
emit('update:modelValue', currentValue);
}
}
});
//Ctrl+S / Cmd+S
editorInstance.addAction({
id: 'save-file',
label: 'Save File',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
],
precondition: undefined,
keybindingContext: undefined,
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
run: () => {
console.log('[MonacoEditor] Save action triggered (Ctrl+S / Cmd+S)');
emit('request-save');
},
});
// --- 添加带防抖的鼠标滚轮缩放功能 ---
const editorDomNode = editorInstance?.getDomNode();
if (editorDomNode && editorInstance) {
// console.log('[MonacoEditor] Adding wheel event listener.');
editorDomNode.addEventListener('wheel', (event: WheelEvent) => {
if (event.ctrlKey && editorInstance) {
event.preventDefault();
const currentSizeOpt = editorInstance.getOption(monaco.editor.EditorOption.fontSize);
const currentSize = typeof currentSizeOpt === 'number' ? currentSizeOpt : internalEditorFontSize.value;
let newSize: number;
if (event.deltaY < 0) {
newSize = Math.min(currentSize + 1, 40); // 字体上限 40
} else {
newSize = Math.max(currentSize - 1, 8); // 字体下限 8
}
if (newSize !== currentSize) {
// console.log(`[MonacoEditor] Updating font size to: ${newSize}`);
editorInstance.updateOptions({ fontSize: newSize });
localStorage.setItem(FONT_SIZE_STORAGE_KEY, newSize.toString());
internalEditorFontSize.value = newSize; // 更新 internal ref
emit('update:fontSize', newSize); // 发出事件以更新 store
}
}
}, { passive: false });
} else {
// console.error('[MonacoEditor] editorDomNode or editorInstance is null, cannot add wheel listener.');
}
// --- 移除鼠标滚轮缩放功能 ---
// const editorDomNode = editorInstance?.getDomNode();
// if (editorDomNode) {
// editorDomNode.addEventListener('wheel', (event: WheelEvent) => {
// if (event.ctrlKey) {
// event.preventDefault();
// // ... (移除字体大小调整逻辑) ...
// // if (editorInstance) {
// // editorInstance.updateOptions({ fontSize: fontSize.value }); // 使用本地 fontSize
// // }
// }
// }, { passive: false });
// }
}
});
watch(() => props.modelValue, (newValue) => {
if (editorInstance && editorInstance.getValue() !== newValue) {
editorInstance.setValue(newValue);
}
});
watch(() => props.language, (newLanguage) => {
if (editorInstance && editorInstance.getModel()) {
monaco.editor.setModelLanguage(editorInstance.getModel()!, newLanguage);
}
});
watch(() => props.theme, (newTheme) => {
if (editorInstance) {
monaco.editor.setTheme(newTheme);
}
});
watch(() => props.readOnly, (newReadOnly) => {
if (editorInstance) {
editorInstance.updateOptions({ readOnly: newReadOnly });
}
});
watch(() => props.fontFamily, (newFontFamily) => {
if (editorInstance) {
editorInstance.updateOptions({ fontFamily: newFontFamily });
};
});
// 监听来自父组件 (全局设置) 的 fontSize 变化
watch(() => props.fontSize, (newGlobalSize) => {
// 只有当全局设置的 fontSize (通过 prop) 改变时,并且与 internalEditorFontSize (编辑器当前实际或本地调整后) 不同时才更新
if (editorInstance && newGlobalSize !== internalEditorFontSize.value) {
// console.log(`[MonacoEditor] Global font size changed to: ${newGlobalSize}, updating editor.`);
editorInstance.updateOptions({ fontSize: newGlobalSize });
localStorage.setItem(FONT_SIZE_STORAGE_KEY, newGlobalSize.toString()); // 保持 localStorage 同步
internalEditorFontSize.value = newGlobalSize;
}
});
onBeforeUnmount(() => {
if (editorInstance) {
editorInstance.dispose();
editorInstance = null;
}
});
defineExpose({
focus: () => editorInstance?.focus()
});
</script>
<style scoped>
.monaco-editor-container {
width: 100%;
height: 100%;
min-height: 300px;
text-align: left;
}
</style>