Browse Source

Merge branch 'main' of https://git.shalu.com/Shalu/shalu-agent-workflow

jiaxing.liao 18 hours ago
parent
commit
c177cfd40a

+ 1 - 0
apps/web/package.json

@@ -28,6 +28,7 @@
     "@repo/ui": "workspace:*",
     "@repo/workflow": "workspace:*",
     "@repo/api-service": "workspace:*",
+    "@types/lodash-es": "^4.17.12",
     "@vitejs/plugin-vue": "^6.0.1",
     "@vue/tsconfig": "^0.8.1",
     "esbuild": "^0.27.2",

+ 382 - 104
apps/web/src/components/SetterCommon/Code/CodeEditor.vue

@@ -1,60 +1,228 @@
 <script setup lang="ts">
+import { nextTick, onMounted, reactive, ref, watch, onBeforeUnmount, computed } from 'vue'
+import { useLocalEditorTheme } from "@/utils/useLocalEditorTheme"
 import type { editor } from 'monaco-editor'
-
-import { nextTick, onMounted, ref, watch } from 'vue'
+import { ElMessage } from "element-plus"
 import * as monaco from 'monaco-editor'
 import { IconButton } from '@repo/ui'
+import { debounce } from "lodash-es"
+
+interface CodeEditorType {
+    // 是否展示工具栏
+    tools?: boolean
+    // 是否展示全屏工具
+    allowFullscreen?: boolean
+    // 是否展示复制工具
+    copyCode?: boolean
+    // 是否自动切换主题
+    autoToggleTheme?: boolean
+    // 挂载DOM
+    appendTo?: string | HTMLElement
+    // 内容
+    modelValue?: any
+    // 语法  
+    language?: string
+    // 主题
+    theme?: 'vs-light' | 'vs-dark'
+    // 是否只读
+    readOnly?: boolean
+    // 是否显示行号
+    lineNumbers?: 'on' | 'off'
+    // 设置代码模块高度,总高度多40px
+    height?: number
+    // 返回结果为string,也可转化为json
+    formatValue?: 'string' | 'json'
+    // 插件内部配置
+    config?: editor.IStandaloneEditorConstructionOptions
 
-const props = withDefaults(
-    defineProps<{
-        allowFullscreen?: boolean
-        autoToggleTheme?: boolean
-        bordered?: boolean
-        config?: editor.IStandaloneEditorConstructionOptions
-        language?: string
-        lineNumbers?: 'off' | 'on'
-        modelValue?: any
-        readOnly?: boolean
-        theme?: 'hc-black' | 'vs-dark' | 'vs-light'
-        valueFormat?: string
-        height?: string
-        appendTo?: string | HTMLElement
-    }>(),
+}
+
+const props = withDefaults(defineProps<CodeEditorType>(),
     {
+        //默认配置
+        tools: true,
+        copyCode: true,
+        readOnly: false,
         allowFullscreen: true,
-        config: () => ({
-            minimap: {
-                enabled: false
-            },
-            selectOnLineNumbers: true
-        }),
         autoToggleTheme: true,
-        language: 'json',
+        language: 'javascript',
         lineNumbers: 'on',
-        readOnly: false,
         theme: 'vs-light',
-        valueFormat: 'string',
-        height: '120px'
+        formatValue: 'string',
+        height: 150,
+        config: () => ({
+            minimap: { enabled: false },
+            selectOnLineNumbers: true
+        }),
     }
 )
 
-// 动态切换主题
-const theme = ref('dark')
 
 const emit = defineEmits(['update:modelValue', 'update:language'])
 
+let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
+let model: editor.ITextModel | null = null
+const editContainer = ref<HTMLElement>()
+
 const isFullScreen = ref(false)
+const {
+    monacoTheme,
+    themeClass,
+    toggleTheme
+} = useLocalEditorTheme({
+    defaultTheme: props.theme,
+    autoToggleTheme: props.autoToggleTheme
+})
 
-const fullScreenStyle = `position: fixed;
-  top: 0;
-  left: 0;      
-  right: 0;
-  bottom: 0;
-  z-index: 2999;`
+let componentConfig = reactive({
+    language: props.language
+})
 
-const editContainer = ref<HTMLElement | null>(null)
+const languageSource = [
+    { id: 'javascript', name: 'javascript' },
+    { id: 'python', name: 'python' },
+    { id: 'json', name: 'json' }
+]
+
+/**
+ * @description: formatValue为json 格式,需要转换处理
+ * @return 
+ */
+const formatValue = (value: any) => {
+    if (props.formatValue === 'json') {
+        return JSON.stringify(value ?? {}, null, 2)
+    }
+    return value ?? ''
+}
+
+onMounted(() => {
+    // 处理代码转换
+    model = monaco.editor.createModel(
+        formatValue(props.modelValue),
+        componentConfig.language
+    )
+
+    // 接入配置
+    monacoEditor = monaco.editor.create(editContainer.value!, {
+        model,
+        wordWrap: 'on',
+        automaticLayout: true,
+        theme: monacoTheme.value,
+        readOnly: props.readOnly,
+        lineNumbers: props.lineNumbers,
+        ...props.config,
+    })
+
+    //  添加空格
+    monacoEditor.onKeyDown((e) => {
+        if (e.keyCode === monaco.KeyCode.Space) {
+            e.preventDefault();
+            monacoEditor?.trigger('keyboard', 'type', { text: ' ' });
+        }
+    });
+
+    // 监听代码输入
+    monacoEditor.onDidChangeModelContent(updateModelValue)
+
+})
+
+
+// 读取传值数据
+watch(() => props.modelValue, (value) => {
+    nextTick(() => {
+        if (!model) return
+        const next = formatValue(value)
+        if (model.getValue() !== next) {
+            model.setValue(next)
+        }
+    })
+}, { immediate: true })
+
+
+// 切换语法,触发回调
+watch(() => componentConfig.language, (lang) => {
+    nextTick(() => {
+        if (!model) return
+        monaco.editor.setModelLanguage(model, lang)
+        emit('update:language', lang)
+    })
+}, { immediate: true })
+
+// 监听全屏, 重新计算layout
+watch(isFullScreen, () => {
+    nextTick(() => {
+        monacoEditor?.layout()
+    })
+})
+
+
+
+
+/**
+ * @description: 发送回调(formatValue:json,需要转换处理)
+ * @return {*}
+ */
+const updateModelValue = debounce(() => {
+    if (!model) return
+    const value = model.getValue()
+
+    if (props.formatValue === 'json') {
+        try {
+            emit('update:modelValue', JSON.parse(value))
+        } catch {
+            return ElMessage.warning('JSON 语法错误')
+        }
+    } else {
+        emit('update:modelValue', value)
+    }
+}, 1000)
+
+
+/**
+ * @description: 复制代码到粘贴板
+ * @return {*}
+ */
+const copyCode = () => {
+    const text = model?.getValue()
+    if (!text) return
+    try {
+        navigator.clipboard.writeText(text).then(() => {
+            ElMessage.success('复制成功!')
+        })
+    } catch {
+        const textarea = document.createElement('textarea')
+        textarea.value = text
+        document.body.appendChild(textarea)
+        textarea.select()
+        document.execCommand('copy')
+        textarea.remove()
+        ElMessage.success('复制成功!')
+    }
+}
+
+
+/**
+ * @description: 设置全屏样式
+ * @return {*}
+ */
+const fullScreenStyle = computed(() => {
+    if (isFullScreen.value) return {}
+
+    return {
+        height: `${props.height + 40}px`
+    }
+})
+/**
+ * @description: 切换主题, 并设置对应样式
+ * @return {*}
+ */
+const toolTipClass = computed(() => {
+    return `editor-tooltip editor-tooltip--${themeClass.value}`
+})
+const onToggleTheme = () => {
+    toggleTheme()
+}
 
-let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
 
 /**
  * 设置文本
@@ -64,6 +232,7 @@ function setValue(text: string) {
     monacoEditor?.setValue(text || '')
 }
 
+
 /**
  * 光标处插入文本
  * @param text
@@ -96,60 +265,10 @@ function insertText(text: string) {
     monacoEditor?.focus()
 }
 
-onMounted(() => {
-    monacoEditor = monaco.editor.create(editContainer.value as HTMLElement, {
-        value: getValue(),
-        ...props.config,
-        automaticLayout: true,
-        language: props.language,
-        lineNumbers: props.lineNumbers,
-        readOnly: props.readOnly,
-        scrollBeyondLastLine: false,
-        theme: props.theme
-    })
-
-    function handleToggleTheme() {
-        if (theme.value === 'dark') {
-            monaco.editor.setTheme('vs-dark')
-        } else {
-            monaco.editor.setTheme('vs-light')
-        }
-    }
-
-    // 自动切换主题
-    if (props.autoToggleTheme) {
-        watch(
-            () => theme,
-            () => {
-                nextTick(() => handleToggleTheme())
-            },
-            {
-                immediate: true
-            }
-        )
-    }
-
-    // 获取值
-    function getValue() {
-        // valueFormat 为json 格式,需要转换处理
-        if (props.valueFormat === 'json' && props.modelValue) {
-            return JSON.stringify(props.modelValue, null, 2)
-        }
-        return props.modelValue ?? ''
-    }
-
-    // 监听值变化
-    monacoEditor.onDidChangeModelContent(() => {
-        const currenValue = monacoEditor?.getValue()
-
-        // valueFormat 为json 格式,需要转换处理
-        if (props.valueFormat === 'json' && currenValue) {
-            emit('update:modelValue', JSON.parse(currenValue))
-            return
-        }
-
-        emit('update:modelValue', currenValue ?? '')
-    })
+// 销毁model
+onBeforeUnmount(() => {
+    monacoEditor?.dispose()
+    model?.dispose()
 })
 
 defineExpose({
@@ -159,28 +278,187 @@ defineExpose({
 </script>
 <template>
     <Teleport :disabled="!appendTo" :to="appendTo">
-        <div ref="editContainer" :class="{ bordered: props.bordered }"
-            :style="isFullScreen ? fullScreenStyle : { height: props.height }"
-            class="code-editor w-full h-full relative">
-            <div class="z-999 text-$epic-text-helper absolute right-4 top-2 cursor-pointer text-xl"
-                @click="isFullScreen = !isFullScreen" v-if="props.allowFullscreen">
-                <IconButton v-if="!isFullScreen" icon="lucide:fullscreen" link />
-                <IconButton v-else icon="material-symbols-light:fullscreen-exit-rounded" link />
+        <div class="monacoEditor !m-0" :class="[themeClass, { 'is-fullscreen': isFullScreen }]"
+            :style="fullScreenStyle">
+
+            <div class="tools  h-[33px] flex items-center justify-between gap-2" v-if="props.tools">
+                <!-- 语法切换 -->
+                <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="切换语法">
+                    <div class="w-1/3">
+
+                        <ElSelect v-model="componentConfig.language">
+                            <ElOption v-for="value in languageSource" :label="value.name" :value="value.id" />
+                        </ElSelect>
+
+                    </div>
+                </ElTooltip>
+                <div class="flex-1 flex items-center justify-end gap-1">
+                    <!-- 放大/缩小 -->
+                    <ElTooltip :effect="themeClass" placement="top" :popper-class="toolTipClass"
+                        :content="!isFullScreen ? '放大' : '恢复正常'">
+                        <div class="cursor-pointer text-xl text-center px-2" @click="isFullScreen = !isFullScreen"
+                            v-if="props.allowFullscreen">
+                            <IconButton :icon="isFullScreen ? 'lucide:minimize' : 'lucide:fullscreen'" link
+                                class="fullscreen" />
+                        </div>
+                    </ElTooltip>
+
+                    <!-- copy -->
+                    <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="复制">
+                        <div class="copy text-center px-2" @click="copyCode" v-if="props.copyCode">
+                            <IconButton icon="lucide:copy" link class="copyIcon"></IconButton>
+                        </div>
+                    </ElTooltip>
+
+                    <!-- 主题 -->
+                    <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="主题">
+                        <div class="copy text-center px-2" @click="onToggleTheme">
+                            <IconButton :icon="themeClass === 'dark' ? 'lucide:moon' : 'lucide:sun'" link
+                                class="themeIcon">
+                            </IconButton>
+                        </div>
+                    </ElTooltip>
+                </div>
             </div>
+            <!-- 编辑器 -->
+            <div class="editor-wrapper">
+                <div ref="editContainer" class="code-editor"></div>
+            </div>
+
         </div>
     </Teleport>
 </template>
+<style lang="less">
+.editor-tooltip {
+    border-radius: 6px;
+    font-size: 12px;
+    padding: 6px 10px;
+}
+
+// light
+.editor-tooltip--light {
+    background-color: #ffffff !important;
+    color: #1e1e1e !important;
+    border: 1px solid #e5e7eb;
+
+    .el-popper__arrow::before {
+        background: #ffffff !important;
+        border: 1px solid #e5e7eb;
+    }
+}
+
+// dark
+.editor-tooltip--dark {
+    background-color: #1e1e1e !important;
+    color: #ffffff !important;
+
+    .el-popper__arrow::before {
+        background: #1e1e1e !important;
+    }
+}
+</style>
 <style lang="less" scoped>
-.code-editor {
-    width: 100%;
-    min-height: 150px;
+.monacoEditor {
+    border: 1px solid #eee;
+    display: flex;
+    flex-direction: column;
+    transition: height 0.25s ease, inset 0.25s ease, background-color 0.2s;
 
-    :deep(.monaco-editor) {
+    &.is-fullscreen {
+        position: absolute;
+        inset: 0;
+        z-index: 1000;
+    }
+
+    .tools {
+        border-bottom: 1px solid #eeeeee83;
+        padding-bottom: 6px;
+
+        ::v-deep(.el-select .el-select__wrapper) {
+            border: none;
+            box-shadow: none;
+            background-color: #ffff;
+        }
+
+        ::v-deep(.el-select .el-select__placeholder) {
+            color: #1e1e1e;
+        }
+    }
+
+    .editor-wrapper {
+        flex: 1;
+        overflow: hidden;
+    }
+
+    .code-editor {
+        width: 100%;
         height: 100%;
+
+        ::v-deep(.monaco-editor) {
+            height: 100%;
+            background-color: #ffffff;
+        }
+
+        &.bordered {
+            border: 1px solid var(--epic-border-color);
+        }
     }
 
-    &.bordered {
-        border: 1px solid var(--epic-border-color);
+}
+
+.light {
+    background-color: #ffffff;
+    color: #1e1e1e;
+
+    &:hover {
+        color: #1e1e1e;
+    }
+
+    .tools {
+        ::v-deep(.el-select .el-select__wrapper) {
+            border: none;
+            box-shadow: none;
+            background-color: #ffffff;
+        }
+
+        ::v-deep(.el-select .el-select__placeholder) {
+            color: #1e1e1e;
+        }
+
+        .fullscreen,
+        .fullscreen-exit,
+        .themeIcon,
+        .copyIcon {
+            color: #1e1e1e;
+        }
+    }
+}
+
+.dark {
+    background-color: #1e1e1e;
+    color: #ffffff;
+
+    &:hover {
+        color: #ffffff;
+    }
+
+    .tools {
+        ::v-deep(.el-select .el-select__wrapper) {
+            border: none;
+            box-shadow: none;
+            background-color: #1e1e1e;
+        }
+
+        ::v-deep(.el-select .el-select__placeholder) {
+            color: #ffffff;
+        }
+
+        .fullscreen,
+        .fullscreen-exit,
+        .themeIcon,
+        .copyIcon {
+            color: #ffffff;
+        }
     }
 }
 </style>

+ 4 - 2
apps/web/src/components/SetterCommon/Code/ErrorHandling.vue

@@ -14,9 +14,10 @@
     </div>
     <div class="!mt-0">
         <div v-if="config.errorHandling === 'default'">
-            <p class="text-sm text-gray-600 m-0 mb-1">当发生异常时,指定默认输出内容。</p>
+            <p class="text-sm text-gray-600 m-0 mb-1">当发生异常时,指定默认输出内容。( result Object )</p>
             <!-- 代码编辑器 -->
-            <CodeEditor v-model="config.errorCodeReturn" v-model:language="config.language" value-format="javascript" />
+            <CodeEditor v-model="config.errorCodeReturn" v-model:language="config.language" :tools="false"
+                theme="vs-light" />
         </div>
         <div v-else-if="config.errorHandling === 'errBranch'">
             <p class="text-sm m-0 mb-1 text-gray-600">在画布自定义失败分支逻辑。</p>
@@ -51,6 +52,7 @@ watch(
             errorCodeReturn: '',
             language: 'javascript',
         }
+        console.log(config.value.errorCodeReturn)
     }
 )
 const config = ref<ErrorHandlerConfig>(props.modelValue || {

+ 5 - 9
apps/web/src/components/setter/CodeSetter.vue

@@ -8,8 +8,9 @@
                     <InputVariables v-model="formData.inputVariables" />
 
                     <!-- 代码编辑器 -->
-                    <CodeEditor v-model="formData.code" v-model:language="formData.language"
-                        value-format="javascript" />
+                    <div class="mt-4">
+                        <CodeEditor v-model="formData.code" v-model:language="formData.language" theme="vs-light" />
+                    </div>
 
                     <!-- 输出变量 -->
                     <OutputVariables v-model="formData.outputVariables" />
@@ -55,15 +56,10 @@ interface ErrorHandler {
     language: string
 }
 
-// const props = withDefaults(defineProps<{
-// }>(), {
-// })
-//
-// const emit = defineEmits<{
-// }>()
+
 
 const formData = reactive({
-    // 输入变量, 变量默认值
+    // 输入变量
     inputVariables: [
         // { id: 'var_1', name: 'arg1' },
     ] as Variable[],

+ 79 - 0
apps/web/src/utils/useLocalEditorTheme.ts

@@ -0,0 +1,79 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-01-30 19:47:19
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-01-30 20:20:59
+ * @Describe: 单独抽离换肤
+ */
+import { ref, onMounted, onBeforeUnmount } from 'vue'
+import * as monaco from 'monaco-editor'
+
+type EditorTheme = 'vs-light' | 'vs-dark'
+type ThemeClass = 'light' | 'dark'
+
+interface UseLocalEditorThemeOptions {
+    defaultTheme?: EditorTheme
+    autoToggleTheme?: boolean
+}
+
+export function useLocalEditorTheme(
+    options: UseLocalEditorThemeOptions = {}
+) {
+    const {
+        defaultTheme = 'vs-light',
+        autoToggleTheme = false
+    } = options
+
+    /** 当前 Monaco 主题(语法) */
+    const monacoTheme = ref<EditorTheme>(defaultTheme)
+
+    /** 当前 UI 主题(样式) */
+    const themeClass = ref<ThemeClass>(
+        defaultTheme === 'vs-dark' ? 'dark' : 'light'
+    )
+
+    /** 系统暗黑监听 */
+    let mediaQuery: MediaQueryList | null = null
+
+    const applyTheme = (theme: EditorTheme) => {
+        monacoTheme.value = theme
+        themeClass.value = theme === 'vs-dark' ? 'dark' : 'light'
+
+        monaco.editor.setTheme(theme)
+    }
+
+    /** 手动切换 */
+    const toggleTheme = () => {
+        applyTheme(
+            monacoTheme.value === 'vs-light' ? 'vs-dark' : 'vs-light'
+        )
+    }
+
+    /** 系统主题变化 */
+    const handleSystemThemeChange = (e: MediaQueryListEvent) => {
+        applyTheme(e.matches ? 'vs-dark' : 'vs-light')
+    }
+
+    onMounted(() => {
+        applyTheme(defaultTheme)
+
+        if (autoToggleTheme) {
+            mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+            applyTheme(mediaQuery.matches ? 'vs-dark' : 'vs-light')
+            mediaQuery.addEventListener('change', handleSystemThemeChange)
+        }
+    })
+
+    onBeforeUnmount(() => {
+        if (mediaQuery) {
+            mediaQuery.removeEventListener('change', handleSystemThemeChange)
+        }
+    })
+
+    return {
+        monacoTheme,
+        themeClass,
+        toggleTheme,
+        setTheme: applyTheme
+    }
+}

+ 3 - 0
pnpm-lock.yaml

@@ -185,6 +185,9 @@ importers:
       '@repo/workflow':
         specifier: workspace:*
         version: link:../../packages/workflow
+      '@types/lodash-es':
+        specifier: ^4.17.12
+        version: 4.17.12
       '@vitejs/plugin-vue':
         specifier: ^6.0.1
         version: 6.0.3(rolldown-vite@7.2.5(@types/node@25.1.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(yaml@1.10.2))(vue@3.5.27(typescript@5.9.3))