279 lines
8.7 KiB
Vue
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>
|