Remove workflow code editor dependency
This commit is contained in:
@@ -1,406 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import type {
|
||||
CodeEditorEmits,
|
||||
CodeEditorExpose,
|
||||
CodeEditorProps,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from '@codemirror/commands';
|
||||
import {
|
||||
bracketMatching,
|
||||
defaultHighlightStyle,
|
||||
foldGutter,
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
placeholder as placeholderExt,
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
|
||||
import { getLanguageExtension } from './languages';
|
||||
|
||||
const props = withDefaults(defineProps<CodeEditorProps>(), {
|
||||
modelValue: '',
|
||||
language: 'javascript',
|
||||
theme: 'auto',
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
height: 'auto',
|
||||
minHeight: '100px',
|
||||
tabSize: 2,
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
bracketMatching: true,
|
||||
autocompletion: true,
|
||||
indentGuide: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<CodeEditorEmits>();
|
||||
|
||||
const editorRef = ref<HTMLDivElement>();
|
||||
const view = shallowRef<EditorView | null>(null);
|
||||
|
||||
// 主题跟随系统
|
||||
const { isDark } = usePreferences();
|
||||
|
||||
// Compartments 用于动态更新配置
|
||||
const languageCompartment = new Compartment();
|
||||
const themeCompartment = new Compartment();
|
||||
const readonlyCompartment = new Compartment();
|
||||
const lineNumbersCompartment = new Compartment();
|
||||
const lineWrappingCompartment = new Compartment();
|
||||
const foldGutterCompartment = new Compartment();
|
||||
const highlightActiveLineCompartment = new Compartment();
|
||||
const bracketMatchingCompartment = new Compartment();
|
||||
const autocompletionCompartment = new Compartment();
|
||||
const placeholderCompartment = new Compartment();
|
||||
|
||||
// 计算当前主题
|
||||
const currentTheme = computed(() => {
|
||||
if (props.theme === 'auto') {
|
||||
return isDark.value ? 'dark' : 'light';
|
||||
}
|
||||
return props.theme;
|
||||
});
|
||||
|
||||
// 计算样式
|
||||
const editorStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
|
||||
if (props.height !== 'auto') {
|
||||
style.height =
|
||||
typeof props.height === 'number' ? `${props.height}px` : props.height;
|
||||
}
|
||||
|
||||
if (props.minHeight) {
|
||||
style.minHeight =
|
||||
typeof props.minHeight === 'number'
|
||||
? `${props.minHeight}px`
|
||||
: props.minHeight;
|
||||
}
|
||||
|
||||
if (props.maxHeight) {
|
||||
style.maxHeight =
|
||||
typeof props.maxHeight === 'number'
|
||||
? `${props.maxHeight}px`
|
||||
: props.maxHeight;
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
// 获取主题扩展
|
||||
function getThemeExtension(): Extension {
|
||||
return currentTheme.value === 'dark' ? oneDark : [];
|
||||
}
|
||||
|
||||
// 创建基础扩展
|
||||
function createBaseExtensions(): Extension[] {
|
||||
return [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
crosshairCursor(),
|
||||
rectangularSelection(),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
||||
EditorState.tabSize.of(props.tabSize),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const value = update.state.doc.toString();
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
}
|
||||
if (update.focusChanged) {
|
||||
if (update.view.hasFocus) {
|
||||
emit('focus');
|
||||
} else {
|
||||
emit('blur');
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// 初始化编辑器
|
||||
async function initEditor() {
|
||||
if (!editorRef.value) return;
|
||||
|
||||
const languageExt = await getLanguageExtension(props.language);
|
||||
|
||||
const extensions: Extension[] = [
|
||||
...createBaseExtensions(),
|
||||
languageCompartment.of(languageExt || []),
|
||||
themeCompartment.of(getThemeExtension()),
|
||||
readonlyCompartment.of(
|
||||
EditorState.readOnly.of(props.readonly || props.disabled),
|
||||
),
|
||||
lineNumbersCompartment.of(
|
||||
props.lineNumbers ? [lineNumbers(), highlightActiveLineGutter()] : [],
|
||||
),
|
||||
lineWrappingCompartment.of(
|
||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||
),
|
||||
foldGutterCompartment.of(props.foldGutter ? foldGutter() : []),
|
||||
highlightActiveLineCompartment.of(
|
||||
props.highlightActiveLine ? highlightActiveLine() : [],
|
||||
),
|
||||
bracketMatchingCompartment.of(
|
||||
props.bracketMatching ? bracketMatching() : [],
|
||||
),
|
||||
autocompletionCompartment.of(props.autocompletion ? autocompletion() : []),
|
||||
placeholderCompartment.of(
|
||||
props.placeholder ? placeholderExt(props.placeholder) : [],
|
||||
),
|
||||
];
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
});
|
||||
|
||||
view.value = new EditorView({
|
||||
state,
|
||||
parent: editorRef.value,
|
||||
});
|
||||
|
||||
emit('ready', view.value);
|
||||
}
|
||||
|
||||
// 更新编辑器内容
|
||||
function updateContent(value: string) {
|
||||
if (!view.value) return;
|
||||
const currentValue = view.value.state.doc.toString();
|
||||
if (currentValue !== value) {
|
||||
view.value.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
updateContent(value);
|
||||
},
|
||||
);
|
||||
|
||||
// 监听语言变化
|
||||
watch(
|
||||
() => props.language,
|
||||
async (language) => {
|
||||
if (!view.value) return;
|
||||
const languageExt = await getLanguageExtension(language);
|
||||
view.value.dispatch({
|
||||
effects: languageCompartment.reconfigure(languageExt || []),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 监听主题变化
|
||||
watch(currentTheme, () => {
|
||||
if (!view.value) return;
|
||||
view.value.dispatch({
|
||||
effects: themeCompartment.reconfigure(getThemeExtension()),
|
||||
});
|
||||
});
|
||||
|
||||
// 监听只读状态变化
|
||||
watch(
|
||||
() => [props.readonly, props.disabled],
|
||||
([readonly, disabled]) => {
|
||||
if (!view.value) return;
|
||||
view.value.dispatch({
|
||||
effects: readonlyCompartment.reconfigure(
|
||||
EditorState.readOnly.of(Boolean(readonly || disabled)),
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 监听行号显示变化
|
||||
watch(
|
||||
() => props.lineNumbers,
|
||||
(show) => {
|
||||
if (!view.value) return;
|
||||
view.value.dispatch({
|
||||
effects: lineNumbersCompartment.reconfigure(
|
||||
show ? [lineNumbers(), highlightActiveLineGutter()] : [],
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 监听自动换行变化
|
||||
watch(
|
||||
() => props.lineWrapping,
|
||||
(wrap) => {
|
||||
if (!view.value) return;
|
||||
view.value.dispatch({
|
||||
effects: lineWrappingCompartment.reconfigure(
|
||||
wrap ? EditorView.lineWrapping : [],
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 监听占位符变化
|
||||
watch(
|
||||
() => props.placeholder,
|
||||
(text) => {
|
||||
if (!view.value) return;
|
||||
view.value.dispatch({
|
||||
effects: placeholderCompartment.reconfigure(
|
||||
text ? placeholderExt(text) : [],
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 暴露方法
|
||||
const expose: CodeEditorExpose = {
|
||||
getView: () => view.value,
|
||||
focus: () => view.value?.focus(),
|
||||
getValue: () => view.value?.state.doc.toString() || '',
|
||||
setValue: (value: string) => updateContent(value),
|
||||
format: () => {
|
||||
if (!view.value || props.language !== 'json') return;
|
||||
try {
|
||||
const content = view.value.state.doc.toString();
|
||||
const formatted = JSON.stringify(
|
||||
JSON.parse(content),
|
||||
null,
|
||||
props.tabSize,
|
||||
);
|
||||
updateContent(formatted);
|
||||
} catch {
|
||||
// JSON 解析失败,忽略
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
defineExpose(expose);
|
||||
|
||||
onMounted(() => {
|
||||
initEditor();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
view.value?.destroy();
|
||||
view.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="code-editor"
|
||||
:class="{
|
||||
'code-editor--disabled': disabled,
|
||||
'code-editor--readonly': readonly,
|
||||
'code-editor--dark': currentTheme === 'dark',
|
||||
}"
|
||||
:style="editorStyle"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.code-editor {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-editor) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-content) {
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-scroller) {
|
||||
font-family: 'Fira Code', Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-focused) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-editor:focus-within {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.code-editor--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.code-editor--disabled :deep(.cm-editor) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-gutters) {
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.code-editor--dark :deep(.cm-gutters) {
|
||||
background-color: var(--el-fill-color-darker);
|
||||
border-right-color: var(--el-border-color-darker);
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-activeLineGutter) {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.code-editor :deep(.cm-placeholder) {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as CodeEditor } from './code-editor.vue';
|
||||
export { supportedLanguages } from './languages';
|
||||
export * from './types';
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import type { CodeLanguage } from './types';
|
||||
|
||||
// 语言扩展懒加载映射
|
||||
const languageLoaders: Record<CodeLanguage, () => Promise<Extension>> = {
|
||||
python: async () => {
|
||||
const { python } = await import('@codemirror/lang-python');
|
||||
return python();
|
||||
},
|
||||
sql: async () => {
|
||||
const { sql } = await import('@codemirror/lang-sql');
|
||||
return sql();
|
||||
},
|
||||
json: async () => {
|
||||
const { json } = await import('@codemirror/lang-json');
|
||||
return json();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言扩展
|
||||
* @param language 语言类型
|
||||
* @returns 语言扩展
|
||||
*/
|
||||
export async function getLanguageExtension(
|
||||
language: CodeLanguage | string,
|
||||
): Promise<Extension | null> {
|
||||
const loader = languageLoaders[language as CodeLanguage];
|
||||
if (loader) {
|
||||
return await loader();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的语言列表
|
||||
*/
|
||||
export const supportedLanguages: CodeLanguage[] = [
|
||||
'python',
|
||||
'sql',
|
||||
'json',
|
||||
];
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
|
||||
export type CodeLanguage =
|
||||
| 'json'
|
||||
| 'python'
|
||||
| 'sql';
|
||||
|
||||
export interface CodeEditorProps {
|
||||
/** 代码内容 */
|
||||
modelValue?: string;
|
||||
/** 语言类型 */
|
||||
language?: CodeLanguage | string;
|
||||
/** 主题: light/dark/auto(跟随系统) */
|
||||
theme?: 'auto' | 'dark' | 'light';
|
||||
/** 是否只读 */
|
||||
readonly?: boolean;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 高度 */
|
||||
height?: number | string;
|
||||
/** 最小高度 */
|
||||
minHeight?: number | string;
|
||||
/** 最大高度 */
|
||||
maxHeight?: number | string;
|
||||
/** 占位符 */
|
||||
placeholder?: string;
|
||||
/** Tab 大小 */
|
||||
tabSize?: number;
|
||||
/** 是否显示行号 */
|
||||
lineNumbers?: boolean;
|
||||
/** 是否自动换行 */
|
||||
lineWrapping?: boolean;
|
||||
/** 是否显示代码折叠 */
|
||||
foldGutter?: boolean;
|
||||
/** 是否高亮当前行 */
|
||||
highlightActiveLine?: boolean;
|
||||
/** 是否显示括号匹配 */
|
||||
bracketMatching?: boolean;
|
||||
/** 是否启用自动补全 */
|
||||
autocompletion?: boolean;
|
||||
/** 是否显示缩进指南 */
|
||||
indentGuide?: boolean;
|
||||
}
|
||||
|
||||
export interface CodeEditorEmits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
(e: 'change', value: string): void;
|
||||
(e: 'focus'): void;
|
||||
(e: 'blur'): void;
|
||||
(e: 'ready', view: EditorView): void;
|
||||
}
|
||||
|
||||
export interface CodeEditorExpose {
|
||||
/** 获取 EditorView 实例 */
|
||||
getView: () => EditorView | null;
|
||||
/** 聚焦编辑器 */
|
||||
focus: () => void;
|
||||
/** 获取代码内容 */
|
||||
getValue: () => string;
|
||||
/** 设置代码内容 */
|
||||
setValue: (value: string) => void;
|
||||
/** 格式化代码(仅支持 JSON) */
|
||||
format: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user