roymondchen 1b66ab1b88 refactor(editor): 抽取 serializeConfig 工具统一序列化配置
将分散在 CodeLink、CodeEditor 及 playground 中重复的 serialize-javascript
序列化逻辑收敛为 @editor/utils/editor 的 serializeConfig 并对外导出复用。
2026-06-02 16:34:23 +08:00

415 lines
11 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 class="magic-code-editor">
<Teleport to="body" :disabled="!fullScreen">
<div :class="{ 'magic-code-editor-wrapper': true, 'full-screen': fullScreen }" :style="{ height: computeHeight }">
<TMagicButton
v-if="!disabledFullScreen"
class="magic-code-editor-full-screen-icon"
circle
size="small"
@click="fullScreenHandler"
><MIcon :icon="FullScreen"></MIcon
></TMagicButton>
<div ref="codeEditor" class="magic-code-editor-content"></div>
</div>
</Teleport>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import { FullScreen } from '@element-plus/icons-vue';
import { throttle } from 'lodash-es';
import type * as Monaco from 'monaco-editor';
import { TMagicButton } from '@tmagic/design';
import MIcon from '@editor/components/Icon.vue';
import { getEditorConfig } from '@editor/utils/config';
import { serializeConfig } from '@editor/utils/editor';
import loadMonaco from '@editor/utils/monaco-editor';
defineOptions({
name: 'MEditorCodeEditor',
});
const props = withDefaults(
defineProps<{
initValues?: any;
modifiedValues?: any;
type?: 'diff';
language?: string;
options?: Monaco.editor.IStandaloneEditorConstructionOptions;
height?: string;
autoSave?: boolean;
parse?: boolean;
disabledFullScreen?: boolean;
autosize?: {
minRows?: number;
maxRows?: number;
};
editorCustomType?: string;
}>(),
{
initValues: '',
autoSave: true,
language: 'javascript',
options: () => ({
tabSize: 2,
}),
parse: false,
disabledFullScreen: false,
},
);
const emit = defineEmits(['initd', 'save']);
const autoHeight = ref<string>('');
let cachedExtraHeight: number | null = null;
const computeHeight = computed(() => {
if (fullScreen.value) {
return '100%';
}
if (props.height) {
return props.height;
}
if (props.autosize) {
return autoHeight.value;
}
return '100%';
});
const calculateExtraHeight = (): number => {
let extraHeight = 10; // 默认值
if (vsEditor && codeEditorEl.value) {
try {
// 获取编辑器容器的总高度和内容区域高度
const editorElement = codeEditorEl.value.querySelector('.monaco-editor');
const scrollableElement = codeEditorEl.value.querySelector('.monaco-scrollable-element');
if (editorElement && scrollableElement) {
const editorRect = editorElement.getBoundingClientRect();
const scrollableRect = scrollableElement.getBoundingClientRect();
// 计算编辑器的边框、内边距等额外高度
extraHeight = Math.max(editorRect.height - scrollableRect.height, 0);
// 如果无法获取到有效的差值,使用编辑器配置中的相关选项
if (extraHeight === 0 && monaco) {
const editorOptions = vsEditor.getOptions();
const scrollBeyondLastLine = editorOptions.get(monaco.editor.EditorOption.scrollBeyondLastLine);
const padding = editorOptions.get(monaco.editor.EditorOption.padding);
const lineHeight = editorOptions.get(monaco.editor.EditorOption.lineHeight) || 20;
extraHeight = (scrollBeyondLastLine ? lineHeight : 0) + (padding?.top || 0) + (padding?.bottom || 0) + 10; // 基础边距
}
}
} catch (error) {
// 如果获取失败,保持默认值
console.warn('Failed to calculate editor extra height:', error);
}
}
return extraHeight;
};
const setAutoHeight = (v = '') => {
let lines = Math.max(v.split('\n').length, props.autosize?.minRows || 1);
if (v) {
if (props.autosize?.maxRows) {
lines = Math.min(lines, props.autosize.maxRows);
}
}
// 获取编辑器实际行高,如果编辑器还未初始化则使用默认值
let lineHeight = 20;
if (vsEditor && monaco) {
const editorOptions = vsEditor.getOptions();
lineHeight = editorOptions.get(monaco.editor.EditorOption.lineHeight) || 20;
}
// 获取缓存的额外高度,如果没有缓存则计算并缓存
if (cachedExtraHeight === null) {
cachedExtraHeight = calculateExtraHeight();
}
const newHeight = `${lines * lineHeight + cachedExtraHeight}px`;
// 只有当高度真正改变时才更新
if (autoHeight.value !== newHeight) {
autoHeight.value = newHeight;
// 高度变化后需要重新布局编辑器
nextTick(() => {
vsEditor?.layout();
// 确保内容在可视区域内,滚动到顶部
if (vsEditor) {
vsEditor.setScrollTop(0);
vsEditor.revealLine(1);
}
});
}
};
const toString = (v: string | any, language: string): string => {
let value: string;
if (typeof v !== 'string') {
if (language === 'json') {
value = JSON.stringify(v, null, 2);
} else {
value = serializeConfig(v);
}
} else {
value = v;
}
if (language === 'javascript' && value.startsWith('{') && value.endsWith('}')) {
value = `(${value})`;
}
return value;
};
const parseCode = (v: string | any, language: string): any => {
if (typeof v !== 'string') {
return v;
}
if (language === 'json') {
return JSON.parse(v);
}
return getEditorConfig('parseDSL')(v);
};
let monaco: typeof import('monaco-editor') | null = null;
let vsEditor: Monaco.editor.IStandaloneCodeEditor | null = null;
let vsDiffEditor: Monaco.editor.IStandaloneDiffEditor | null = null;
const values = ref('');
const loading = ref(false);
const codeEditorEl = useTemplateRef<HTMLDivElement>('codeEditor');
const resizeObserver = new globalThis.ResizeObserver(
throttle((): void => {
vsEditor?.layout();
vsDiffEditor?.layout();
}, 300),
);
const setEditorValue = (v: string | any, m: string | any) => {
values.value = toString(v, props.language.toLocaleLowerCase());
setAutoHeight(values.value);
if (!monaco) return;
if (props.type === 'diff') {
const originalModel = monaco.editor.createModel(values.value, 'text/javascript');
const modifiedModel = monaco.editor.createModel(toString(m, props.language), 'text/javascript');
// 保存视图状态(光标、选区、滚动、折叠等)
const viewState = vsDiffEditor?.saveViewState();
const result = vsDiffEditor?.setModel({
original: originalModel,
modified: modifiedModel,
});
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0这里也放到 nextTick 中
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
if (viewState) {
nextTick(() => {
vsDiffEditor?.restoreViewState(viewState);
vsDiffEditor?.focus();
});
}
return result;
}
// 保存视图状态(光标、选区、滚动、折叠等)
const viewState = vsEditor?.saveViewState();
const result = vsEditor?.setValue(values.value);
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0这里也放到 nextTick 中
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
if (viewState) {
nextTick(() => {
vsEditor?.restoreViewState(viewState);
vsEditor?.focus();
});
}
return result;
};
const getEditorValue = () =>
(props.type === 'diff' ? vsDiffEditor?.getModifiedEditor().getValue() : vsEditor?.getValue()) || '';
const handleKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === 83 && (navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
e.stopPropagation();
const newValue = getEditorValue();
values.value = newValue;
emit('save', props.parse ? parseCode(newValue, props.language) : newValue);
}
};
const init = async () => {
if (!codeEditorEl.value) return;
if (codeEditorEl.value.clientHeight === 0) {
await nextTick();
}
// 重置缓存的额外高度,因为编辑器重新初始化
cachedExtraHeight = null;
monaco = await loadMonaco();
const options = {
value: values.value,
language: props.language,
theme: 'vs-dark',
editorCustomType: props.editorCustomType,
...props.options,
};
if (props.type === 'diff') {
vsDiffEditor = await getEditorConfig('customCreateMonacoDiffEditor')(monaco!, codeEditorEl.value, options);
// 监听diff编辑器内容变化
vsDiffEditor.getModifiedEditor().onDidChangeModelContent(() => {
// 如果使用 autosize内容变化时重新计算高度
if (props.autosize) {
setAutoHeight(getEditorValue());
}
});
} else {
vsEditor = await getEditorConfig('customCreateMonacoEditor')(monaco!, codeEditorEl.value, options);
// 监听编辑器内容变化
vsEditor.onDidChangeModelContent(() => {
// 如果使用 autosize内容变化时重新计算高度
if (props.autosize) {
setAutoHeight(getEditorValue());
}
});
}
setEditorValue(props.initValues, props.modifiedValues);
emit('initd', vsEditor);
codeEditorEl.value.addEventListener('keydown', handleKeyDown);
if (props.type !== 'diff' && props.autoSave) {
vsEditor?.onDidBlurEditorWidget(() => {
const newValue = getEditorValue();
if (values.value !== newValue) {
values.value = newValue;
emit('save', props.parse ? parseCode(newValue, props.language) : newValue);
}
});
}
resizeObserver.observe(codeEditorEl.value);
};
watch(
() => props.initValues,
(v, preV) => {
if (v !== preV) {
setEditorValue(props.initValues, props.modifiedValues);
}
},
{
deep: true,
immediate: true,
},
);
// diff 模式下,对比的"当前值"modifiedValues也可能在外部变化例如 lastValues 不变、当前 model 变了),
// 此时同样需要重新设置编辑器值,否则右侧编辑器内容会停留在初始化时的快照。
watch(
() => props.modifiedValues,
(v, preV) => {
if (props.type !== 'diff') return;
if (v !== preV) {
setEditorValue(props.initValues, props.modifiedValues);
}
},
{
deep: true,
},
);
watch(
() => props.options,
(v) => {
vsEditor?.updateOptions(v);
vsDiffEditor?.updateOptions(v);
},
{
deep: true,
},
);
onMounted(async () => {
loading.value = true;
await init();
loading.value = false;
});
onBeforeUnmount(() => {
resizeObserver.disconnect();
vsEditor?.dispose();
vsDiffEditor?.dispose();
vsEditor = null;
vsDiffEditor = null;
monaco = null;
// 重置缓存
cachedExtraHeight = null;
});
onUnmounted(() => {
codeEditorEl.value?.removeEventListener('keydown', handleKeyDown);
});
const fullScreen = ref(false);
const fullScreenHandler = () => {
fullScreen.value = !fullScreen.value;
setTimeout(() => {
vsEditor?.focus();
vsEditor?.layout();
vsDiffEditor?.focus();
vsDiffEditor?.layout();
});
};
defineExpose({
values,
getEditor() {
return vsEditor || vsDiffEditor;
},
getVsEditor() {
return vsEditor;
},
getVsDiffEditor() {
return vsDiffEditor;
},
setEditorValue,
getEditorValue,
focus() {
vsEditor?.focus();
vsDiffEditor?.focus();
},
});
</script>