Remove workflow code editor dependency

This commit is contained in:
2026-06-10 17:16:22 +08:00
parent 8862f8ec99
commit f272d75315
12 changed files with 103 additions and 781 deletions
@@ -130,11 +130,6 @@ const ZqIconPicker = defineAsyncComponent(() =>
(res) => res.default,
),
);
const CodeEditor = defineAsyncComponent(() =>
import('#/components/zq-form/code-editor/code-editor.vue').then(
(res) => res.default,
),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
@@ -275,7 +270,10 @@ function initComponentAdapter() {
ImageSelector: withDefaultPlaceholder(ImageSelector, 'select'),
Input: withDefaultPlaceholder(ElInput, 'input'),
Textarea: withDefaultPlaceholder(ElInput, 'input', { type: 'textarea' }),
CodeEditor,
CodeEditor: withDefaultPlaceholder(ElInput, 'input', {
autosize: { minRows: 8 },
type: 'textarea',
}),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: (props, { attrs, slots }) => {
let defaultSlot;
@@ -355,7 +353,7 @@ function initComponentAdapter() {
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
copyPreferencesSuccess: (title: string, content: string) => {
void import('element-plus/es/components/notification/index').then(
({ ElNotification }) => {
ElNotification({
@@ -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;
}
@@ -17,7 +17,6 @@ import {
} from '@vben/icons';
import { $t } from '@vben/locales';
import { CodeEditor } from '#/components/zq-form/code-editor';
import { ZqDialog } from '#/components/zq-dialog';
export interface IterationResult {
@@ -254,15 +253,8 @@ const formattedInputs = computed(() => {
<div class="text-foreground mb-2 text-sm font-medium">
{{ $t('ai-platform.workflow.editor.nodeResultCard.input') }}
</div>
<div class="min-h-0 flex-1 overflow-hidden rounded border">
<CodeEditor
:model-value="formattedInputs"
language="json"
height="100%"
:readonly="true"
:line-numbers="true"
:fold-gutter="true"
/>
<div class="result-json-view min-h-0 flex-1 overflow-auto rounded border">
<pre>{{ formattedInputs }}</pre>
</div>
</div>
@@ -270,15 +262,8 @@ const formattedInputs = computed(() => {
<div class="text-foreground mb-2 text-sm font-medium">
{{ $t('ai-platform.workflow.editor.nodeResultCard.output') }}
</div>
<div class="min-h-0 flex-1 overflow-hidden rounded border">
<CodeEditor
:model-value="formattedOutput"
language="json"
height="100%"
:readonly="true"
:line-numbers="true"
:fold-gutter="true"
/>
<div class="result-json-view min-h-0 flex-1 overflow-auto rounded border">
<pre>{{ formattedOutput }}</pre>
</div>
</div>
</div>
@@ -477,3 +462,23 @@ const formattedInputs = computed(() => {
</div>
</div>
</template>
<style scoped>
.result-json-view {
background: hsl(var(--muted) / 35%);
padding: 12px;
}
.result-json-view pre {
min-height: 100%;
margin: 0;
color: hsl(var(--muted-foreground));
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
</style>
@@ -13,7 +13,6 @@ import {
ElSwitch,
} from 'element-plus';
import { CodeEditor } from '#/components/zq-form/code-editor';
import { ZqDialog } from '#/components/zq-dialog';
interface SchemaField {
@@ -464,11 +463,11 @@ const previewSchema = computed(() => {
<!-- JSON 编辑模式 -->
<div v-if="editMode === 'json'" class="flex h-full flex-col">
<CodeEditor
<ElInput
v-model="jsonContent"
language="json"
height="100%"
:line-wrapping="true"
type="textarea"
resize="none"
class="schema-json-textarea"
/>
<div v-if="jsonError" class="mt-2 text-xs text-red-500">
{{ jsonError }}
@@ -1136,4 +1135,17 @@ const previewSchema = computed(() => {
border-radius: 4px;
overflow: hidden;
}
:deep(.schema-json-textarea),
:deep(.schema-json-textarea textarea) {
height: 100%;
}
:deep(.schema-json-textarea textarea) {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
</style>
@@ -13,7 +13,6 @@ import {
ElTooltip,
} from 'element-plus';
import { CodeEditor } from '#/components/zq-form/code-editor';
import { ZqDialog } from '#/components/zq-dialog';
import SmartInput from '../components/SmartInput.vue';
@@ -186,15 +185,13 @@ const codeEditorVisible = ref(false);
</ElButton>
</ElTooltip>
</div>
<CodeEditor
<ElInput
v-model="form.code"
language="python"
height="280px"
type="textarea"
:rows="14"
resize="vertical"
class="workflow-code-textarea"
:placeholder="$t('ai-platform.workflow.panels.code.codePlaceholder')"
:fold-gutter="true"
:bracket-matching="true"
:autocompletion="true"
:line-numbers="false"
/>
</div>
</ElForm>
@@ -207,15 +204,12 @@ const codeEditorVisible = ref(false);
:show-footer="false"
>
<div class="code-editor-dialog-content">
<CodeEditor
<ElInput
v-model="form.code"
language="python"
height="100%"
type="textarea"
resize="none"
class="workflow-code-textarea workflow-code-textarea--dialog"
:placeholder="$t('ai-platform.workflow.panels.code.codePlaceholder')"
:fold-gutter="true"
:bracket-matching="true"
:autocompletion="true"
:line-numbers="true"
/>
</div>
</ZqDialog>
@@ -245,12 +239,6 @@ const codeEditorVisible = ref(false);
min-height: 0;
}
.code-editor-dialog .code-editor-dialog-content .code-editor {
flex: 1;
min-height: 0;
height: 100% !important;
}
.code-editor-dialog.is-fullscreen .el-dialog__body {
display: flex;
flex: 1;
@@ -266,4 +254,17 @@ const codeEditorVisible = ref(false);
.code-editor-dialog.is-fullscreen .zq-dialog-body .el-scrollbar {
height: 100% !important;
}
.workflow-code-textarea textarea {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
.workflow-code-textarea--dialog,
.workflow-code-textarea--dialog textarea {
height: 100%;
}
</style>
@@ -17,7 +17,6 @@ import {
ElTooltip,
} from 'element-plus';
import { CodeEditor } from '#/components/zq-form/code-editor';
import { ZqDialog } from '#/components/zq-dialog';
import { getParamTypeOptions } from '#/views/_core/data-source/data';
@@ -171,16 +170,13 @@ function handleConnectionChange(payload: { dbName: string; dbType: string }) {
</ElButton>
</ElTooltip>
</div>
<CodeEditor
<ElInput
v-model="form.sql"
language="sql"
height="280px"
type="textarea"
:rows="12"
resize="vertical"
:placeholder="$t('ai-platform.workflow.panels.dbSql.sqlPlaceholder')"
:fold-gutter="true"
:bracket-matching="true"
:autocompletion="true"
:line-numbers="false"
class="font-mono"
class="workflow-code-textarea"
/>
<div class="mt-2 flex flex-wrap items-center gap-1">
<span class="text-muted-foreground mr-2 text-xs">
@@ -290,16 +286,12 @@ function handleConnectionChange(payload: { dbName: string; dbType: string }) {
:show-footer="false"
>
<div class="code-editor-dialog-content">
<CodeEditor
<ElInput
v-model="form.sql"
language="sql"
height="100%"
type="textarea"
resize="none"
:placeholder="$t('ai-platform.workflow.panels.dbSql.sqlPlaceholder')"
:fold-gutter="true"
:bracket-matching="true"
:autocompletion="true"
:line-numbers="true"
class="font-mono"
class="workflow-code-textarea workflow-code-textarea--dialog"
/>
</div>
</ZqDialog>
@@ -314,6 +306,14 @@ function handleConnectionChange(payload: { dbName: string; dbType: string }) {
width: 100%;
min-width: 0;
}
:deep(.workflow-code-textarea textarea) {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
</style>
<style>
@@ -340,12 +340,6 @@ function handleConnectionChange(payload: { dbName: string; dbType: string }) {
min-height: 0;
}
.code-editor-dialog .code-editor-dialog-content .code-editor {
flex: 1;
min-height: 0;
height: 100% !important;
}
.code-editor-dialog.is-fullscreen .el-dialog__body {
display: flex;
flex: 1;
@@ -361,4 +355,9 @@ function handleConnectionChange(payload: { dbName: string; dbType: string }) {
.code-editor-dialog.is-fullscreen .zq-dialog-body .el-scrollbar {
height: 100% !important;
}
.workflow-code-textarea--dialog,
.workflow-code-textarea--dialog textarea {
height: 100%;
}
</style>
@@ -16,8 +16,6 @@ import {
ElTabs,
} from 'element-plus';
import { CodeEditor } from '#/components/zq-form/code-editor';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
@@ -175,13 +173,13 @@ const removeParam = (index: number) => {
</ElTabPane>
<ElTabPane label="Body">
<CodeEditor
<ElInput
v-model="form.body"
language="json"
height="200px"
type="textarea"
:rows="10"
resize="vertical"
class="workflow-code-textarea"
placeholder='{"key": "value"}'
:fold-gutter="true"
:bracket-matching="true"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.http.bodyHint') }}
@@ -206,4 +204,12 @@ const removeParam = (index: number) => {
:deep(.http-tabs .el-tabs__content) {
padding: 12px;
}
:deep(.workflow-code-textarea textarea) {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
</style>