| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- <template>
- <div class="w-full h-full flex flex-col">
- <div
- class="h-60px shrink-0 border-b border-b-solid border-gray-200 flex items-center justify-between px-12px"
- >
- <div class="left flex items-center gap-4">
- <el-breadcrumb separator="/" class="flex items-center">
- <el-breadcrumb-item>{{ t('pages.editor.workspace') }}</el-breadcrumb-item>
- <el-breadcrumb-item>
- <Input ref="inputRef" v-model="workflow.name" variant="borderless" />
- </el-breadcrumb-item>
- </el-breadcrumb>
- <div class="flex gap-2" v-show="!showTagInput" @click="showTagInput = true">
- <el-tag type="info" v-for="tag in workflow.tags" :key="tag" :disable-transitions="false">
- {{ tag }}
- </el-tag>
- </div>
- <!-- <el-input-tag
- v-show="showTagInput"
- v-model="workflow.tags"
- :placeholder="t('pages.editor.tagPlaceholder')"
- :aria-label="t('pages.editor.tagPlaceholder')"
- :max="5"
- @blur="showTagInput = false"
- /> -->
- <!-- <IconButton
- v-if="!workflow.tags?.length && !showTagInput"
- icon="iconoir:plus"
- type="primary"
- link
- @click="showTagInput = true"
- >
- {{ t('pages.editor.tagButton') }}
- </IconButton> -->
- </div>
- <div class="right flex items-center gap-2">
- <PublishBtn :workflow="workflow" @published="handlePublished" />
- <IconButton icon="lucide:history" type="default" link></IconButton>
- <el-dropdown placement="bottom-end" popper-class="w-120px">
- <IconButton icon="fluent-mdl2:more" type="default" link></IconButton>
- <template #dropdown>
- <el-dropdown-item>{{ t('pages.editor.menu.description') }}</el-dropdown-item>
- <el-dropdown-item>{{ t('pages.editor.menu.reuse') }}</el-dropdown-item>
- <el-dropdown-item @click="handleRename">{{
- t('pages.editor.menu.rename')
- }}</el-dropdown-item>
- <el-dropdown-item divided @click="handleDelete">{{
- t('pages.editor.menu.delete')
- }}</el-dropdown-item>
- </template>
- </el-dropdown>
- </div>
- </div>
- <el-splitter layout="vertical" class="flex-1">
- <el-splitter-panel>
- <NodeView :key="workflow.id" :workflow="workflow" :reload-workflow="loadAgentWorkflow" />
- </el-splitter-panel>
- <el-splitter-panel v-model:size.lazy="footerHeight" :min="32">
- <EditorFooter @toggle="handleFooterToggle" />
- </el-splitter-panel>
- </el-splitter>
- </div>
- </template>
- <script setup lang="ts">
- import { inject, nextTick, onBeforeUnmount, ref, type CSSProperties, watch } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { dayjs, ElMessage, ElMessageBox } from 'element-plus'
- import { agent } from '@repo/api-service'
- import EditorFooter from '@/features/editorFooter/index.vue'
- import NodeView from './NodeView.vue'
- import PublishBtn from './PublishBtn.vue'
- import { nodeMap } from '@/nodes'
- import { IconButton, Input } from '@repo/ui'
- import { useI18n } from '@/composables/useI18n'
- import type { IWorkflow } from '@repo/workflow'
- const layout = inject<{ setMainStyle: (style: CSSProperties) => void }>('layout')
- layout?.setMainStyle({
- padding: '0px'
- })
- const route = useRoute()
- const router = useRouter()
- const { t } = useI18n()
- const footerHeight = ref(32)
- const showTagInput = ref(false)
- const inputRef = ref<InstanceType<typeof Input>>()
- const saveAgentTimer = ref<number | undefined>(undefined)
- const saveVarsTimer = ref<number | undefined>(undefined)
- const isHydrating = ref(false)
- const notifyTimestamps = new Map<string, number>()
- const id = route.params?.id as string
- const projectMap = JSON.parse(localStorage.getItem('workflow-map') || '{}') as Record<
- string,
- IWorkflow
- >
- const workflow = ref<IWorkflow>(
- projectMap?.[id]
- ? projectMap[id]
- : {
- id,
- name: 'workflow_1',
- created: dayjs().format('MM 月DD 日'),
- nodes: [],
- edges: []
- }
- )
- // 从后端节点数据中提取当前画布需要的节点类型。
- const normalizeNodeType = (node: any) => {
- const sourceNodeType = node?.nodeType || node?.data?.nodeType || node?.data?.type || node?.type
- return sourceNodeType || 'code'
- }
- // 将原始节点数据转换为画布组件可直接消费的节点结构。
- const toWorkflowNode = (node: any) => {
- const normalizedNodeType = normalizeNodeType(node)
- const schema = nodeMap[normalizedNodeType]?.schema
- const position = node?.position || schema?.position || { x: 20, y: 30 }
- const width = node?.width ?? schema?.width ?? 96
- const height = node?.height ?? schema?.height ?? 96
- return {
- ...schema,
- ...node,
- id: node.id,
- type: 'canvas-node',
- position,
- width,
- height,
- zIndex: node?.zIndex ?? schema?.zIndex ?? 1,
- selected: false,
- data: {
- ...(schema?.data || {}),
- ...(node?.data || {}),
- id: node.id,
- position,
- width,
- height,
- nodeType: normalizedNodeType
- }
- }
- }
- // 将跨循环作用域的边重定向到父节点,确保外层画布能正确渲染。
- const normalizeEdgeEndpoints = (
- edge: { source: string; target: string },
- nodes: Array<{ id: string; parentId?: string }>
- ) => {
- const sourceNode = nodes.find((node) => node.id === edge.source)
- const targetNode = nodes.find((node) => node.id === edge.target)
- const sourceParentId = sourceNode?.parentId || ''
- const targetParentId = targetNode?.parentId || ''
- if (sourceParentId && sourceParentId !== targetParentId) {
- return {
- source: sourceParentId,
- target: edge.target
- }
- }
- if (targetParentId && targetParentId !== sourceParentId) {
- return {
- source: edge.source,
- target: targetParentId
- }
- }
- return edge
- }
- // 为边补齐兜底 id 和 handle,避免画布渲染时缺少必要字段。
- const toWorkflowEdge = (
- edge: any,
- index: number,
- nodes: Array<{ id: string; parentId?: string }> = []
- ) => {
- if (!edge || typeof edge !== 'object' || !edge.source || !edge.target) {
- return null
- }
- const normalizedEdge = normalizeEdgeEndpoints(edge, nodes)
- return {
- ...edge,
- ...normalizedEdge,
- sourceHandle: edge.sourceHandle === 'source' ? `${edge.source}-source` : edge.sourceHandle,
- targetHandle: edge.targetHandle === 'target' ? `${edge.target}-target` : edge.targetHandle,
- id: edge.id || `edge-${normalizedEdge.source}-${normalizedEdge.target}-${index}`,
- type: 'canvas-edge',
- data: edge.data || {}
- }
- }
- // 自动保存频繁触发时,避免重复弹出相同成功提示。
- const notifySuccess = (key: string, message: string, cooldown = 1500) => {
- const now = Date.now()
- const last = notifyTimestamps.get(key) || 0
- if (now - last < cooldown) return
- notifyTimestamps.set(key, now)
- ElMessage.success(message)
- }
- // 统一处理编辑页保存类接口的成功与失败提示。
- const handleApiResult = (response: any, successMessage?: string, errorMessage?: string) => {
- if (response?.isSuccess) {
- if (successMessage) {
- notifySuccess(successMessage, successMessage)
- }
- return true
- }
- if (response?.code === 0 && response?.error) {
- ElMessage.error(response.error)
- return false
- }
- if (errorMessage) {
- ElMessage.error(errorMessage)
- }
- return false
- }
- // 画布依赖归一化后的节点和边结构,因此在页面入口统一完成数据适配。
- const loadAgentWorkflow = async (agentId: string) => {
- if (!agentId) return
- isHydrating.value = true
- try {
- const response = await agent.postAgentGetAgentInfo({ id: agentId })
- const result = response?.result
- if (!response?.isSuccess || !result) {
- throw new Error('获取智能体信息失败')
- }
- const normalizedNodes = (result.nodes || []).map(toWorkflowNode)
- const normalizedEdges = (result.edges || [])
- .map((edge: any, index: number) => toWorkflowEdge(edge, index, normalizedNodes))
- .filter(Boolean)
- workflow.value = {
- ...(result as unknown as IWorkflow),
- nodes: normalizedNodes,
- edges: normalizedEdges as IWorkflow['edges']
- }
- await nextTick()
- } catch (error) {
- console.error('loadAgentWorkflow error', error)
- ElMessage.error(t('pages.editor.messages.loadFailed'))
- } finally {
- isHydrating.value = false
- }
- }
- // 保存页面头部编辑的工作流基础信息。
- const saveAgentMeta = async () => {
- if (!workflow.value?.id) return
- try {
- const response = await agent.postAgentDoEditAgent({
- data: workflow.value
- })
- handleApiResult(
- response,
- t('pages.editor.messages.saved'),
- t('pages.editor.messages.saveFailed')
- )
- } catch (error) {
- console.error('saveAgentMeta error', error)
- ElMessage.error(t('pages.editor.messages.saveFailed'))
- }
- }
- // 保存不在节点画布内编辑的工作流变量。
- const saveAgentVariables = async () => {
- if (!workflow.value?.id) return
- try {
- const response = await agent.postAgentDoSaveAgentVariables({
- appAgentId: workflow.value.id,
- conversation_variables: workflow.value.conversation_variables || [],
- env_variables: (workflow.value.env_variables || []).map((item: any) => ({
- name: item?.name || '',
- value: item?.value ?? '',
- type: item?.type || 'string'
- }))
- })
- handleApiResult(
- response,
- t('pages.editor.messages.varsSaved'),
- t('pages.editor.messages.saveFailed')
- )
- } catch (error) {
- console.error('saveAgentVariables error', error)
- ElMessage.error(t('pages.editor.messages.saveFailed'))
- }
- }
- // 为基础信息保存增加防抖,减少输入过程中的重复请求。
- const scheduleSaveAgentMeta = () => {
- if (saveAgentTimer.value) window.clearTimeout(saveAgentTimer.value)
- if (!workflow.value?.id) return
- saveAgentTimer.value = window.setTimeout(() => {
- saveAgentMeta()
- }, 600)
- }
- // 为变量保存增加防抖,避免短时间内连续提交。
- const scheduleSaveAgentVariables = () => {
- if (saveVarsTimer.value) window.clearTimeout(saveVarsTimer.value)
- if (!workflow.value?.id) return
- saveVarsTimer.value = window.setTimeout(() => {
- saveAgentVariables()
- }, 600)
- }
- watch(
- () => workflow.value,
- (currentWorkflow) => {
- projectMap[currentWorkflow.id] = currentWorkflow
- localStorage.setItem('workflow-map', JSON.stringify(projectMap))
- },
- { deep: true }
- )
- watch(
- () => [workflow.value?.name, workflow.value?.description, workflow.value?.tags],
- () => {
- if (isHydrating.value) return
- scheduleSaveAgentMeta()
- },
- { deep: true }
- )
- watch(
- () => [workflow.value?.conversation_variables, workflow.value?.env_variables],
- () => {
- if (isHydrating.value) return
- scheduleSaveAgentVariables()
- },
- { deep: true }
- )
- watch(
- () => route.params?.id,
- async (nextId) => {
- if (nextId) {
- await loadAgentWorkflow(nextId as string)
- }
- },
- { immediate: true }
- )
- // 根据底部面板开关状态调整面板高度。
- const handleFooterToggle = (open: boolean) => {
- footerHeight.value = open ? 200 : 32
- }
- // 聚焦标题输入框,方便直接重命名当前工作流。
- const handleRename = () => {
- inputRef.value?.focus()
- inputRef.value?.select()
- }
- // 删除本地缓存中的当前工作流并退出编辑页。
- const handleDelete = () => {
- ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
- confirmButtonText: t('common.confirm'),
- cancelButtonText: t('common.cancel'),
- type: 'warning'
- }).then(() => {
- localStorage.removeItem(`project_${id}`)
- router.push('/')
- })
- }
- const handlePublished = async () => {
- if (!workflow.value?.id) return
- await loadAgentWorkflow(workflow.value.id)
- }
- onBeforeUnmount(() => {
- if (saveAgentTimer.value) window.clearTimeout(saveAgentTimer.value)
- if (saveVarsTimer.value) window.clearTimeout(saveVarsTimer.value)
- layout?.setMainStyle({})
- })
- </script>
|