第一次提交

This commit is contained in:
rjb
2026-01-19 00:09:36 +08:00
parent de4b5059e9
commit 6674060f2f
191 changed files with 40940 additions and 0 deletions

View File

@@ -0,0 +1,537 @@
<template>
<div class="agent-chat-preview">
<div class="chat-header">
<div class="agent-info">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<span class="agent-name">{{ agentName || 'Agent' }}</span>
</div>
<div class="header-actions">
<el-button text size="small" @click="handleClearChat">
<el-icon><Delete /></el-icon>
清空对话
</el-button>
</div>
</div>
<div class="chat-messages" ref="messagesContainer">
<!-- 开场白 -->
<div v-if="openingMessage" class="message agent-message">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<div class="message-content">
<div class="message-bubble" v-html="formatMessage(openingMessage)"></div>
</div>
</div>
<!-- 预设问题 -->
<div v-if="presetQuestions.length > 0 && messages.length === 0" class="preset-questions">
<div
v-for="(question, index) in presetQuestions"
:key="index"
class="preset-question"
@click="handlePresetQuestion(question)"
>
{{ question }}
</div>
</div>
<!-- 对话消息 -->
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.role === 'user' ? 'user-message' : 'agent-message']"
>
<el-avatar
v-if="message.role === 'agent'"
:size="32"
:src="agentAvatar"
:icon="UserFilled"
/>
<div class="message-content">
<div class="message-bubble" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<el-avatar
v-if="message.role === 'user'"
:size="32"
:icon="UserFilled"
style="background-color: #409eff;"
/>
</div>
<!-- 加载中 -->
<div v-if="loading" class="message agent-message">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<div class="message-content">
<div class="message-bubble loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在思考...</span>
</div>
</div>
</div>
<!-- 节点测试结果 -->
<div v-if="props.nodeTestResult" class="node-test-result">
<el-alert
:type="props.nodeTestResult.result.status === 'success' ? 'success' : 'error'"
:closable="true"
show-icon
@close="handleCloseNodeTest"
>
<template #title>
<div class="node-test-header">
<strong>节点测试: {{ props.nodeTestResult.node.data?.label || props.nodeTestResult.node.type }}</strong>
<span class="node-test-time">{{ props.nodeTestResult.result.execution_time }}ms</span>
</div>
</template>
<div class="node-test-content">
<div v-if="props.nodeTestResult.result.status === 'success'">
<div class="test-section">
<strong>输入:</strong>
<pre>{{ JSON.stringify(props.nodeTestResult.input, null, 2) }}</pre>
</div>
<div class="test-section">
<strong>输出:</strong>
<pre>{{ JSON.stringify(props.nodeTestResult.result.output, null, 2) }}</pre>
</div>
</div>
<div v-else>
<div class="test-section">
<strong>错误:</strong>
<pre>{{ props.nodeTestResult.result.error_message || '未知错误' }}</pre>
</div>
</div>
</div>
</el-alert>
</div>
</div>
<div class="chat-input-area">
<div class="input-toolbar">
<el-button text size="small" @click="handleAttachFile">
<el-icon><Paperclip /></el-icon>
</el-button>
</div>
<el-input
v-model="inputMessage"
type="textarea"
:rows="2"
placeholder="发送消息..."
@keydown.enter.exact.prevent="handleSendMessage"
@keydown.enter.shift.exact="handleNewLine"
:disabled="loading || !agentId"
/>
<div class="input-footer">
<div class="disclaimer">
内容由AI生成无法确保真实准确仅供参考
</div>
<div class="input-actions">
<el-button text size="small" @click="handleVoiceInput">
<el-icon><Microphone /></el-icon>
</el-button>
<el-button
type="primary"
@click="handleSendMessage"
:disabled="!inputMessage.trim() || loading || !agentId"
:loading="loading"
>
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import {
UserFilled,
Delete,
Loading,
Paperclip,
Microphone,
Promotion
} from '@element-plus/icons-vue'
import api from '@/api'
interface Message {
role: 'user' | 'agent'
content: string
timestamp: number
}
const props = defineProps<{
agentId?: string
agentName?: string
agentAvatar?: string
openingMessage?: string
presetQuestions?: string[]
nodeTestResult?: any
}>()
const messages = ref<Message[]>([])
const inputMessage = ref('')
const loading = ref(false)
const messagesContainer = ref<HTMLElement>()
// 发送消息
const handleSendMessage = async () => {
if (!inputMessage.value.trim() || loading.value || !props.agentId) return
const userMessage = inputMessage.value.trim()
inputMessage.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userMessage,
timestamp: Date.now()
})
// 滚动到底部
scrollToBottom()
// 发送到Agent
loading.value = true
try {
const response = await api.post('/api/v1/executions', {
agent_id: props.agentId,
input_data: {
USER_INPUT: userMessage,
query: userMessage
}
})
const execution = response.data
// 轮询执行状态
const checkStatus = async () => {
try {
const statusResponse = await api.get(`/api/v1/executions/${execution.id}`)
const exec = statusResponse.data
if (exec.status === 'completed') {
// 提取Agent回复
let agentReply = ''
if (exec.output_data) {
// 优先从 result 字段获取(工作流执行结果)
if (exec.output_data.result) {
// result 字段应该是纯文本字符串End节点的输出
if (typeof exec.output_data.result === 'string') {
agentReply = exec.output_data.result
} else {
agentReply = String(exec.output_data.result)
}
} else if (typeof exec.output_data === 'string') {
agentReply = exec.output_data
} else if (exec.output_data.output) {
agentReply = exec.output_data.output
} else if (exec.output_data.response) {
agentReply = exec.output_data.response
} else if (exec.output_data.text) {
agentReply = exec.output_data.text
} else {
agentReply = JSON.stringify(exec.output_data, null, 2)
}
}
messages.value.push({
role: 'agent',
content: agentReply || '执行完成',
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
} else if (exec.status === 'failed') {
messages.value.push({
role: 'agent',
content: `执行失败: ${exec.error_message || '未知错误'}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
} else {
// 继续轮询
setTimeout(checkStatus, 1000)
}
} catch (error: any) {
messages.value.push({
role: 'agent',
content: `获取执行结果失败: ${error.response?.data?.detail || error.message}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
}
}
// 开始轮询
setTimeout(checkStatus, 1000)
} catch (error: any) {
console.error('发送消息失败:', error)
messages.value.push({
role: 'agent',
content: `发送失败: ${error.response?.data?.detail || error.message}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
ElMessage.error(error.response?.data?.detail || '发送失败')
}
}
// 预设问题点击
const handlePresetQuestion = (question: string) => {
inputMessage.value = question
handleSendMessage()
}
// 清空对话
const handleClearChat = () => {
messages.value = []
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 格式化消息支持简单的Markdown
const formatMessage = (content: string) => {
if (!content) return ''
// 简单的换行处理
return content.replace(/\n/g, '<br>')
}
// 格式化时间
const formatTime = (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// 附件
const handleAttachFile = () => {
ElMessage.info('文件上传功能开发中')
}
// 语音输入
const handleVoiceInput = () => {
ElMessage.info('语音输入功能开发中')
}
// 换行
const handleNewLine = () => {
// Shift+Enter 换行,不需要特殊处理
}
// 关闭节点测试结果
const handleCloseNodeTest = () => {
// 通过事件通知父组件清除测试结果
// 这里暂时不做处理,由父组件自动清除
}
// 监听消息变化,自动滚动
watch(messages, () => {
scrollToBottom()
}, { deep: true })
</script>
<style scoped>
.agent-chat-preview {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f5f5;
border-left: 1px solid #e4e7ed;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.agent-info {
display: flex;
align-items: center;
gap: 8px;
}
.agent-name {
font-weight: 500;
font-size: 14px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 12px;
align-items: flex-start;
}
.user-message {
flex-direction: row-reverse;
}
.message-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.user-message .message-content {
align-items: flex-end;
}
.message-bubble {
padding: 10px 14px;
border-radius: 12px;
max-width: 70%;
word-wrap: break-word;
line-height: 1.5;
}
.agent-message .message-bubble {
background: white;
color: #303133;
border: 1px solid #e4e7ed;
}
.user-message .message-bubble {
background: #409eff;
color: white;
}
.message-bubble.loading {
display: flex;
align-items: center;
gap: 8px;
color: #909399;
}
.message-time {
font-size: 12px;
color: #909399;
padding: 0 4px;
}
.preset-questions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.preset-question {
padding: 10px 14px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: #409eff;
}
.preset-question:hover {
background: #ecf5ff;
border-color: #409eff;
}
.node-test-result {
margin-top: 16px;
margin-bottom: 16px;
}
.node-test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.node-test-time {
font-size: 12px;
color: #909399;
font-weight: normal;
}
.node-test-content {
margin-top: 8px;
}
.test-section {
margin-bottom: 12px;
}
.test-section strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
color: #606266;
}
.test-section pre {
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow: auto;
margin: 0;
border: 1px solid #e4e7ed;
}
.chat-input-area {
background: white;
border-top: 1px solid #e4e7ed;
padding: 12px;
}
.input-toolbar {
margin-bottom: 8px;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.disclaimer {
font-size: 12px;
color: #909399;
}
.input-actions {
display: flex;
gap: 8px;
align-items: center;
}
:deep(.el-textarea__inner) {
border: 1px solid #dcdfe6;
border-radius: 8px;
resize: none;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="main-layout">
<el-container>
<el-header>
<div class="header-content">
<h1>低代码智能体平台</h1>
<div class="header-actions">
<span v-if="userStore.user">欢迎{{ userStore.user.username }}</span>
<el-button @click="handleLogout">退出</el-button>
</div>
</div>
</el-header>
<el-main>
<!-- 导航菜单 -->
<el-menu
:default-active="activeMenu"
class="nav-menu"
mode="horizontal"
:ellipsis="false"
@select="handleMenuSelect"
>
<el-menu-item index="workflows">
<el-icon><Document /></el-icon>
<span>工作流管理</span>
</el-menu-item>
<el-menu-item index="agents">
<el-icon><User /></el-icon>
<span>Agent管理</span>
</el-menu-item>
<el-menu-item index="executions">
<el-icon><List /></el-icon>
<span>执行历史</span>
</el-menu-item>
<el-menu-item index="data-sources" @click="router.push('/data-sources')">
<el-icon><Connection /></el-icon>
<span>数据源管理</span>
</el-menu-item>
<el-menu-item index="model-configs" @click="router.push('/model-configs')">
<el-icon><Setting /></el-icon>
<span>模型配置管理</span>
</el-menu-item>
<el-menu-item index="node-templates" @click="router.push('/node-templates')">
<el-icon><Document /></el-icon>
<span>节点模板</span>
</el-menu-item>
<el-menu-item index="template-market" @click="router.push('/template-market')">
<el-icon><Star /></el-icon>
<span>模板市场</span>
</el-menu-item>
<el-menu-item
v-if="userStore.user?.role === 'admin'"
index="permissions"
@click="router.push('/permissions')"
>
<el-icon><Lock /></el-icon>
<span>权限管理</span>
</el-menu-item>
<el-menu-item index="monitoring" @click="router.push('/monitoring')">
<el-icon><Monitor /></el-icon>
<span>系统监控</span>
</el-menu-item>
<el-menu-item index="alert-rules" @click="router.push('/alert-rules')">
<el-icon><Bell /></el-icon>
<span>告警规则</span>
</el-menu-item>
</el-menu>
<!-- 页面内容 -->
<slot />
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 当前激活的菜单
const activeMenu = computed(() => {
if (route.path === '/' || route.path === '/workflow' || route.path.startsWith('/workflow/')) return 'workflows'
if (route.path === '/agents' || route.path.startsWith('/agents/')) return 'agents'
if (route.path === '/executions' || route.path.startsWith('/executions/')) return 'executions'
if (route.path === '/data-sources') return 'data-sources'
if (route.path === '/model-configs') return 'model-configs'
if (route.path === '/node-templates') return 'node-templates'
if (route.path === '/permissions') return 'permissions'
if (route.path === '/template-market') return 'template-market'
if (route.path === '/monitoring') return 'monitoring'
if (route.path === '/alert-rules') return 'alert-rules'
return 'workflows'
})
// 菜单选择
const handleMenuSelect = (key: string) => {
if (key === 'workflows') {
router.push('/')
} else if (key === 'agents') {
router.push('/agents')
} else if (key === 'executions') {
router.push('/executions')
} else if (key === 'data-sources') {
router.push('/data-sources')
} else if (key === 'model-configs') {
router.push('/model-configs')
} else if (key === 'node-templates') {
router.push('/node-templates')
} else if (key === 'template-market') {
router.push('/template-market')
} else if (key === 'permissions') {
router.push('/permissions')
} else if (key === 'monitoring') {
router.push('/monitoring')
} else if (key === 'alert-rules') {
router.push('/alert-rules')
}
}
// 退出登录
const handleLogout = () => {
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.main-layout {
width: 100%;
height: 100vh;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
padding: 0 20px;
background: #409eff;
color: white;
}
.header-content h1 {
margin: 0;
font-size: 20px;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.nav-menu {
margin-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
:deep(.el-menu--horizontal) {
border-bottom: 1px solid #e4e7ed;
}
:deep(.el-menu-item) {
height: 50px;
line-height: 50px;
}
</style>

View File

@@ -0,0 +1,493 @@
/**
* 自定义节点类型定义
*/
import { defineComponent, h } from 'vue'
import { Handle, Position } from '@vue-flow/core'
// 开始节点(只有输出)
export const StartNode = defineComponent({
name: 'StartNode',
props: {
data: {
type: Object,
default: () => ({})
}
},
setup(props, { attrs }) {
const nodeClass = (attrs.class as string) || ''
const executionClass = props.data?.executionClass || ''
const allClasses = ['custom-node', 'start-node', nodeClass, executionClass].filter(Boolean).join(' ')
return () => {
// 根据执行状态动态设置样式
const isExecuting = allClasses.includes('executing')
const isExecuted = allClasses.includes('executed')
const isFailed = allClasses.includes('failed')
const baseStyle: any = {
padding: '8px 16px',
borderRadius: '6px',
background: '#67c23a',
color: 'white',
textAlign: 'center',
minWidth: '100px',
fontSize: '13px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
position: 'relative',
border: '2px solid transparent',
transition: 'all 0.3s ease-in-out'
}
// 执行状态样式(覆盖基础样式)
if (isExecuting) {
baseStyle.border = '3px solid #409eff'
baseStyle.boxShadow = '0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8)'
baseStyle.transform = 'scale(1.05)'
} else if (isExecuted) {
baseStyle.border = '3px solid #67c23a'
baseStyle.boxShadow = '0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3)'
} else if (isFailed) {
baseStyle.border = '3px solid #f56c6c'
baseStyle.boxShadow = '0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3)'
}
const errorMessage = props.data?.errorMessage
return h('div', {
class: allClasses,
style: baseStyle,
title: isFailed && errorMessage ? errorMessage : undefined
}, [
h(Handle, {
type: 'source',
position: Position.Bottom,
id: 'bottom',
style: {
background: '#67c23a',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'source',
position: Position.Right,
id: 'right',
style: {
background: '#67c23a',
width: '8px',
height: '8px'
}
}),
h('div', {
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '4px'
}
}, [
props.data.label || '开始',
isFailed ? h('span', {
style: {
fontSize: '12px',
marginLeft: '4px'
}
}, '❌') : null
])
])
}
}
})
// LLM节点有输入和输出
export const LLMNode = defineComponent({
name: 'LLMNode',
props: {
data: {
type: Object,
default: () => ({})
}
},
setup(props, { attrs }) {
const nodeClass = (attrs.class as string) || ''
const executionClass = props.data?.executionClass || ''
const allClasses = ['custom-node', 'llm-node', nodeClass, executionClass].filter(Boolean).join(' ')
return () => {
// 根据执行状态动态设置样式
const isExecuting = allClasses.includes('executing')
const isExecuted = allClasses.includes('executed')
const isFailed = allClasses.includes('failed')
const baseStyle: any = {
padding: '8px 16px',
borderRadius: '6px',
background: '#409eff',
color: 'white',
textAlign: 'center',
minWidth: '100px',
fontSize: '13px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
position: 'relative',
border: '2px solid transparent',
transition: 'all 0.3s ease-in-out'
}
// 执行状态样式(覆盖基础样式)
if (isExecuting) {
baseStyle.border = '3px solid #409eff'
baseStyle.boxShadow = '0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8)'
baseStyle.transform = 'scale(1.05)'
baseStyle.animation = 'pulse-blue 1.5s infinite'
} else if (isExecuted) {
baseStyle.border = '3px solid #67c23a'
baseStyle.boxShadow = '0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3)'
} else if (isFailed) {
baseStyle.border = '3px solid #f56c6c'
baseStyle.boxShadow = '0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3)'
}
return h('div', {
class: allClasses,
style: baseStyle
}, [
h(Handle, {
type: 'target',
position: Position.Top,
id: 'top',
style: {
background: '#409eff',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'target',
position: Position.Left,
id: 'left',
style: {
background: '#409eff',
width: '8px',
height: '8px'
}
}),
h('div', {
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '4px'
}
}, [
props.data.label || 'LLM',
isFailed ? h('span', {
style: {
fontSize: '12px',
marginLeft: '4px'
}
}, '❌') : null
]),
h(Handle, {
type: 'source',
position: Position.Bottom,
id: 'bottom',
style: {
background: '#409eff',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'source',
position: Position.Right,
id: 'right',
style: {
background: '#409eff',
width: '8px',
height: '8px'
}
})
])
}
}
})
// 条件节点有输入和两个输出true/false
export const ConditionNode = defineComponent({
name: 'ConditionNode',
props: {
data: {
type: Object,
default: () => ({})
}
},
setup(props, { attrs }) {
const nodeClass = (attrs.class as string) || ''
const executionClass = props.data?.executionClass || ''
const allClasses = ['custom-node', 'condition-node', nodeClass, executionClass].filter(Boolean).join(' ')
return () => {
const isExecuting = allClasses.includes('executing')
const isExecuted = allClasses.includes('executed')
const isFailed = allClasses.includes('failed')
const baseStyle: any = {
padding: '8px 16px',
borderRadius: '6px',
background: '#e6a23c',
color: 'white',
textAlign: 'center',
minWidth: '100px',
fontSize: '13px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
position: 'relative',
border: '2px solid transparent',
transition: 'all 0.3s ease-in-out'
}
if (isExecuting) {
baseStyle.border = '3px solid #409eff'
baseStyle.boxShadow = '0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8)'
baseStyle.transform = 'scale(1.05)'
baseStyle.animation = 'pulse-blue 1.5s infinite'
} else if (isExecuted) {
baseStyle.border = '3px solid #67c23a'
baseStyle.boxShadow = '0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3)'
} else if (isFailed) {
baseStyle.border = '3px solid #f56c6c'
baseStyle.boxShadow = '0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3)'
}
return h('div', {
class: allClasses,
style: baseStyle
}, [
h(Handle, {
type: 'target',
position: Position.Top,
id: 'top',
style: {
background: '#e6a23c',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'target',
position: Position.Left,
id: 'left',
style: {
background: '#e6a23c',
width: '8px',
height: '8px'
}
}),
props.data.label || '条件',
h(Handle, {
type: 'source',
position: Position.Bottom,
id: 'true',
style: {
background: '#67c23a',
width: '8px',
height: '8px',
left: '30%'
}
}),
h(Handle, {
type: 'source',
position: Position.Bottom,
id: 'false',
style: {
background: '#f56c6c',
width: '8px',
height: '8px',
right: '30%'
}
}),
h(Handle, {
type: 'source',
position: Position.Right,
id: 'right',
style: {
background: '#e6a23c',
width: '8px',
height: '8px'
}
})
])
}
}
})
// 结束节点(只有输入)
export const EndNode = defineComponent({
name: 'EndNode',
props: {
data: {
type: Object,
default: () => ({})
}
},
setup(props, { attrs }) {
const nodeClass = (attrs.class as string) || ''
const executionClass = props.data?.executionClass || ''
const allClasses = ['custom-node', 'end-node', nodeClass, executionClass].filter(Boolean).join(' ')
return () => {
const isExecuting = allClasses.includes('executing')
const isExecuted = allClasses.includes('executed')
const isFailed = allClasses.includes('failed')
const baseStyle: any = {
padding: '8px 16px',
borderRadius: '6px',
background: '#f56c6c',
color: 'white',
textAlign: 'center',
minWidth: '100px',
fontSize: '13px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
position: 'relative',
border: '2px solid transparent',
transition: 'all 0.3s ease-in-out'
}
if (isExecuting) {
baseStyle.border = '3px solid #409eff'
baseStyle.boxShadow = '0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8)'
baseStyle.transform = 'scale(1.05)'
baseStyle.animation = 'pulse-blue 1.5s infinite'
} else if (isExecuted) {
baseStyle.border = '3px solid #67c23a'
baseStyle.boxShadow = '0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3)'
} else if (isFailed) {
baseStyle.border = '3px solid #f56c6c'
baseStyle.boxShadow = '0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3)'
}
return h('div', {
class: allClasses,
style: baseStyle
}, [
h(Handle, {
type: 'target',
position: Position.Top,
id: 'top',
style: {
background: '#f56c6c',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'target',
position: Position.Left,
id: 'left',
style: {
background: '#f56c6c',
width: '8px',
height: '8px'
}
}),
props.data.label || '结束'
])
}
}
})
// 默认节点(有输入和输出)
export const DefaultNode = defineComponent({
name: 'DefaultNode',
props: {
data: {
type: Object,
default: () => ({})
}
},
setup(props, { attrs }) {
const nodeClass = (attrs.class as string) || ''
const executionClass = props.data?.executionClass || ''
const allClasses = ['custom-node', 'default-node', nodeClass, executionClass].filter(Boolean).join(' ')
return () => {
const isExecuting = allClasses.includes('executing')
const isExecuted = allClasses.includes('executed')
const isFailed = allClasses.includes('failed')
const baseStyle: any = {
padding: '8px 16px',
borderRadius: '6px',
background: '#909399',
color: 'white',
textAlign: 'center',
minWidth: '100px',
fontSize: '13px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
position: 'relative',
border: '2px solid transparent',
transition: 'all 0.3s ease-in-out'
}
if (isExecuting) {
baseStyle.border = '3px solid #409eff'
baseStyle.boxShadow = '0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8)'
baseStyle.transform = 'scale(1.05)'
baseStyle.animation = 'pulse-blue 1.5s infinite'
} else if (isExecuted) {
baseStyle.border = '3px solid #67c23a'
baseStyle.boxShadow = '0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3)'
} else if (isFailed) {
baseStyle.border = '3px solid #f56c6c'
baseStyle.boxShadow = '0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3)'
}
return h('div', {
class: allClasses,
style: baseStyle
}, [
h(Handle, {
type: 'target',
position: Position.Top,
id: 'top',
style: {
background: '#909399',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'target',
position: Position.Left,
id: 'left',
style: {
background: '#909399',
width: '8px',
height: '8px'
}
}),
props.data.label || '节点',
h(Handle, {
type: 'source',
position: Position.Bottom,
id: 'bottom',
style: {
background: '#909399',
width: '8px',
height: '8px'
}
}),
h(Handle, {
type: 'source',
position: Position.Right,
id: 'right',
style: {
background: '#909399',
width: '8px',
height: '8px'
}
})
])
}
}
})

File diff suppressed because it is too large Load Diff