Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,926 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
Agent,
|
||||
AgentChatEvent,
|
||||
AgentMessage,
|
||||
ReasoningStep,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import type { ChatMessage } from '#/components/ChatBox/index';
|
||||
|
||||
/**
|
||||
* Agent 对话面板组件
|
||||
* 可复用于 Agent 编辑页面和 Agent 对话页面
|
||||
*/
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
feedbackAgentMessageApi,
|
||||
getAgentMessagesApi,
|
||||
sendAgentMessageStream,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ChatBox } from '#/components/ChatBox/index';
|
||||
import { AppDesignPanel, DesignEditorPanel } from '#/components/form-editor';
|
||||
|
||||
import AiWorkingAnimation from '../../../../components/ai-loading/AiWorkingAnimation.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
/** Agent 详情(可选,用于显示名称和欢迎配置) */
|
||||
agent?: Agent | null;
|
||||
/** Agent ID */
|
||||
agentId: string;
|
||||
/** 当前对话 ID(用于加载历史消息) */
|
||||
currentConversationId?: null | string;
|
||||
/** 是否禁用输入 */
|
||||
disabled?: boolean;
|
||||
/** 是否启用附件 */
|
||||
enableAttachment?: boolean;
|
||||
/** 是否启用图片 */
|
||||
enableImage?: boolean;
|
||||
/** 是否启用语音 */
|
||||
enableVoice?: boolean;
|
||||
/** 占位符文本 */
|
||||
placeholder?: string;
|
||||
/** 是否显示清空按钮 */
|
||||
showClearButton?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'conversation-created', conversationId: string): void;
|
||||
}>();
|
||||
|
||||
// 设计面板显示状态
|
||||
const showDesignPanel = ref(false);
|
||||
const showAppDesignPanel = ref(false);
|
||||
|
||||
// 对话状态
|
||||
const chatMessages = ref<AgentMessage[]>([]);
|
||||
const sending = ref(false);
|
||||
const streamingContent = ref('');
|
||||
const streamingSteps = ref<ReasoningStep[]>([]);
|
||||
const currentStep = ref('');
|
||||
const conversationId = ref<null | string>(null);
|
||||
const chatBoxRef = ref<InstanceType<typeof ChatBox> | null>(null);
|
||||
|
||||
// 对话流交互状态
|
||||
const waitingForInput = ref(false);
|
||||
const waitingConfig = ref<any>(null);
|
||||
|
||||
// 设计面板状态
|
||||
const currentDesign = ref<null | {
|
||||
data: any;
|
||||
form_fields?: any[];
|
||||
nodeId: string;
|
||||
table_configs?: any[];
|
||||
title: string;
|
||||
type: string;
|
||||
}>(null);
|
||||
|
||||
// 应用设计面板状态
|
||||
const currentAppDesign = ref<null | {
|
||||
data: any;
|
||||
nodeId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}>(null);
|
||||
|
||||
// 取消流式请求
|
||||
let cancelStream: (() => void) | null = null;
|
||||
|
||||
// 生成消息 ID
|
||||
const generateId = () =>
|
||||
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// 转换消息格式为 ChatMessage
|
||||
const formattedMessages = computed<ChatMessage[]>(() => {
|
||||
return chatMessages.value.map((msg) => ({
|
||||
id: msg.id,
|
||||
role: msg.role as 'assistant' | 'user',
|
||||
content: msg.content,
|
||||
status: msg.status as any,
|
||||
timestamp: msg.created_at,
|
||||
elapsed_time: msg.elapsed_time,
|
||||
tokens_used: msg.total_tokens,
|
||||
error_message: msg.error_message,
|
||||
feedback: (msg.feedback as any) || null,
|
||||
reasoning_steps: msg.reasoning_steps?.map((step) => ({
|
||||
type: step.type as any,
|
||||
content: step.content || '',
|
||||
tool: step.tool,
|
||||
params: step.params,
|
||||
})),
|
||||
interaction: msg.interaction,
|
||||
voice: msg.voice as any,
|
||||
}));
|
||||
});
|
||||
|
||||
// 欢迎配置
|
||||
const welcomeConfig = computed(() => {
|
||||
if (!props.agent) return undefined;
|
||||
return {
|
||||
title: props.agent.name,
|
||||
description:
|
||||
props.agent.status === 'published'
|
||||
? props.agent.welcome_message ||
|
||||
$t('ai-platform.agent.chatPanel.sendToStart')
|
||||
: $t('ai-platform.agent.chatPanel.publishFirst'),
|
||||
suggestions:
|
||||
props.agent.status === 'published' ? props.agent.suggested_questions : [],
|
||||
};
|
||||
});
|
||||
|
||||
// 监听 agentId 变化,清空对话
|
||||
watch(
|
||||
() => props.agentId,
|
||||
() => {
|
||||
clearChat();
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 currentConversationId 变化,加载历史消息
|
||||
watch(
|
||||
() => props.currentConversationId,
|
||||
async (newId) => {
|
||||
if (newId) {
|
||||
await loadMessages(newId);
|
||||
} else {
|
||||
clearChat();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 加载历史消息
|
||||
async function loadMessages(convId: string) {
|
||||
// 如果正在发送消息或等待用户输入,跳过加载以避免覆盖当前状态
|
||||
if (sending.value || waitingForInput.value) {
|
||||
conversationId.value = convId;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getAgentMessagesApi(convId, { pageSize: 100 });
|
||||
chatMessages.value = res.items;
|
||||
conversationId.value = convId;
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息 (由 ChatBox 触发)
|
||||
async function handleSendMessage(message: string) {
|
||||
if (!message.trim() || sending.value) return;
|
||||
|
||||
// 如果正在等待用户输入(对话流模式),处理恢复逻辑
|
||||
if (waitingForInput.value) {
|
||||
// 移除最后一条助手消息的交互配置
|
||||
const lastAssistantMsg = chatMessages.value.findLast(
|
||||
(m) => m.role === 'assistant',
|
||||
);
|
||||
if (lastAssistantMsg && lastAssistantMsg.interaction) {
|
||||
const msgIndex = chatMessages.value.findIndex(
|
||||
(m) => m.id === lastAssistantMsg.id,
|
||||
);
|
||||
if (msgIndex !== -1) {
|
||||
chatMessages.value[msgIndex] = {
|
||||
...lastAssistantMsg,
|
||||
interaction: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
const userMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: message,
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMsg);
|
||||
|
||||
// 重置等待状态并继续工作流
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
doSendMessage(message, false);
|
||||
return;
|
||||
}
|
||||
|
||||
doSendMessage(message, true);
|
||||
}
|
||||
|
||||
// 实际发送消息的函数
|
||||
async function doSendMessage(message: string, addUserMessage: boolean = true) {
|
||||
sending.value = true;
|
||||
streamingContent.value = '';
|
||||
streamingSteps.value = [];
|
||||
currentStep.value = '';
|
||||
|
||||
// 如果工作流类型是 "application",立即显示右侧面板(加载状态)
|
||||
if (props.agent?.workflow_type === 'application') {
|
||||
showDesignPanel.value = true;
|
||||
currentDesign.value = {
|
||||
type: 'loading',
|
||||
title: $t('ai-platform.agent.chatPanel.aiDeveloping'),
|
||||
data: null,
|
||||
nodeId: '',
|
||||
};
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
if (addUserMessage) {
|
||||
const userMessage: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: message,
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMessage);
|
||||
}
|
||||
|
||||
// 添加助手消息占位
|
||||
const assistantMessage: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
status: 'pending',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(assistantMessage);
|
||||
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
|
||||
// 发送流式请求
|
||||
// 系统变量(application_id 等)由 API 层自动注入
|
||||
cancelStream = sendAgentMessageStream(
|
||||
props.agentId,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId.value || undefined,
|
||||
},
|
||||
(event: AgentChatEvent) => handleStreamEvent(event, assistantMessage),
|
||||
(error) => {
|
||||
console.error('Stream error:', error);
|
||||
assistantMessage.status = 'failed';
|
||||
assistantMessage.error_message = error.message;
|
||||
sending.value = false;
|
||||
},
|
||||
() => {
|
||||
sending.value = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 处理流式事件
|
||||
function handleStreamEvent(
|
||||
event: AgentChatEvent,
|
||||
assistantMessage: AgentMessage,
|
||||
) {
|
||||
const msgIndex = chatMessages.value.findIndex(
|
||||
(m) => m.id === assistantMessage.id,
|
||||
);
|
||||
|
||||
switch (event.type) {
|
||||
case 'action': {
|
||||
currentStep.value = 'action';
|
||||
streamingSteps.value.push({
|
||||
type: 'action',
|
||||
content: '',
|
||||
tool: event.tool,
|
||||
params: event.params,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'annotation_reply':
|
||||
|
||||
case 'knowledge_retrieval': {
|
||||
currentStep.value = event.type;
|
||||
streamingSteps.value.push({
|
||||
type: event.type as 'annotation_reply' | 'knowledge_retrieval',
|
||||
content: event.content || '',
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'answer': {
|
||||
currentStep.value = 'answering';
|
||||
streamingContent.value = event.content || '';
|
||||
assistantMessage.content = streamingContent.value;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'complete': {
|
||||
assistantMessage.status = 'completed';
|
||||
assistantMessage.total_tokens = event.tokens_used || 0;
|
||||
assistantMessage.elapsed_time = event.elapsed_time || 0;
|
||||
currentStep.value = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
assistantMessage.status = 'failed';
|
||||
assistantMessage.error_message =
|
||||
event.content || $t('ai-platform.agent.chatPanel.executionFailed');
|
||||
currentStep.value = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_complete': {
|
||||
// 查找已存在的 node_start 步骤并更新为 node_complete
|
||||
const existingIndex = streamingSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id && s.type === 'node_start',
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
// 如果没有找到对应的 start,添加新的 complete 步骤
|
||||
streamingSteps.value.push({
|
||||
type: 'node_complete',
|
||||
content: event.node_label || event.node_type || '',
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
const existingStep = streamingSteps.value[existingIndex]!;
|
||||
// 更新已存在的步骤
|
||||
streamingSteps.value[existingIndex] = {
|
||||
...existingStep,
|
||||
type: 'node_complete',
|
||||
content: event.node_label || event.node_type || '',
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
timestamp: existingStep.timestamp || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
case 'node_event': {
|
||||
// 节点事件(如消息节点发送消息)
|
||||
const nodeEvent = event.event;
|
||||
if (nodeEvent?.type === 'message') {
|
||||
// 从消息列表中查找初始消息,检查其内容是否为空
|
||||
const initialMsg = chatMessages.value.find(
|
||||
(m) => m.id === assistantMessage.id,
|
||||
);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
// 更新初始消息(typing indicator -> 实际内容)
|
||||
initialMsg.content = nodeEvent.content || '';
|
||||
initialMsg.status = 'completed';
|
||||
} else {
|
||||
// 已有内容,创建新消息
|
||||
const newMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: nodeEvent.content || '',
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(newMsg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_start': {
|
||||
// 检查是否已存在相同 node_id 的步骤(避免重复)
|
||||
const existingIndex = streamingSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
// 添加新的执行步骤
|
||||
streamingSteps.value.push({
|
||||
type: 'node_start',
|
||||
content: `${event.node_label || event.node_type}`,
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'observation': {
|
||||
currentStep.value = 'observation';
|
||||
streamingSteps.value.push({
|
||||
type: 'observation',
|
||||
content: event.content || '',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start': {
|
||||
if (event.conversation_id && !conversationId.value) {
|
||||
conversationId.value = event.conversation_id;
|
||||
emit('conversation-created', event.conversation_id);
|
||||
}
|
||||
if (event.message_id) {
|
||||
assistantMessage.id = event.message_id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'thought': {
|
||||
currentStep.value = 'thinking';
|
||||
streamingSteps.value.push({
|
||||
type: 'thought',
|
||||
content: event.content || '',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
assistantMessage.reasoning_steps = [...streamingSteps.value];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'waiting_input': {
|
||||
assistantMessage.status = 'completed';
|
||||
currentStep.value = '';
|
||||
waitingForInput.value = true;
|
||||
waitingConfig.value = event.config;
|
||||
|
||||
const configData = event.config as any;
|
||||
|
||||
// 检查是否是设计预览类型
|
||||
if (configData?.type === 'design_preview') {
|
||||
const previewType = configData.preview_type;
|
||||
|
||||
// 应用设计方案使用 AppDesignPanel(富文本编辑器)
|
||||
if (previewType === 'app_design') {
|
||||
currentAppDesign.value = {
|
||||
type: previewType,
|
||||
title:
|
||||
configData.title ||
|
||||
$t('ai-platform.agent.chatPanel.appDesignTitle'),
|
||||
data: configData.data || {},
|
||||
nodeId: event.node_id || '',
|
||||
};
|
||||
showAppDesignPanel.value = true;
|
||||
} else {
|
||||
// 其他设计类型使用 DesignEditorPanel(表单编辑器)
|
||||
currentDesign.value = {
|
||||
type: previewType,
|
||||
title:
|
||||
configData.title ||
|
||||
$t('ai-platform.agent.chatPanel.designPreview'),
|
||||
data: configData.data || {},
|
||||
nodeId: event.node_id || '',
|
||||
form_fields: configData.form_fields || [],
|
||||
table_configs: configData.table_configs || [],
|
||||
};
|
||||
showDesignPanel.value = true;
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
const waitingContent = `**${configData.title}**\n\n${configData.message || $t('ai-platform.agent.chatPanel.designPanelHint')}`;
|
||||
const newMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: waitingContent,
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(newMsg);
|
||||
} else {
|
||||
// 普通对话流交互
|
||||
const newMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: getWaitingPrompt(event.config),
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
interaction: event.config,
|
||||
};
|
||||
chatMessages.value.push(newMsg);
|
||||
}
|
||||
sending.value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgIndex !== -1) {
|
||||
chatMessages.value[msgIndex] = { ...assistantMessage };
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 获取等待提示文本
|
||||
function getWaitingPrompt(config: any) {
|
||||
if (!config) return $t('ai-platform.agent.chatPanel.waitingInput');
|
||||
switch (config.type) {
|
||||
case 'choice': {
|
||||
return config.question || $t('ai-platform.agent.chatPanel.selectPrompt');
|
||||
}
|
||||
case 'confirm': {
|
||||
return `**${config.title || $t('ai-platform.agent.chatPanel.confirm')}**\n\n${config.content || $t('ai-platform.agent.chatPanel.confirmContinue')}`;
|
||||
}
|
||||
case 'question': {
|
||||
return config.question || $t('ai-platform.agent.chatPanel.inputPrompt');
|
||||
}
|
||||
default: {
|
||||
return $t('ai-platform.agent.chatPanel.waitingInput');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理交互提交
|
||||
function handleInteractionSubmit(messageId: string, value: any) {
|
||||
const msgIndex = chatMessages.value.findIndex((m) => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
const msg = chatMessages.value[msgIndex];
|
||||
if (msg) {
|
||||
chatMessages.value[msgIndex] = { ...msg, interaction: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
const userMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: formatInteractionValue(value),
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMsg);
|
||||
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
|
||||
doSendMessage(String(value), false);
|
||||
}
|
||||
|
||||
// 处理交互取消
|
||||
function handleInteractionCancel(messageId: string) {
|
||||
const msgIndex = chatMessages.value.findIndex((m) => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
const msg = chatMessages.value[msgIndex];
|
||||
if (msg) {
|
||||
chatMessages.value[msgIndex] = { ...msg, interaction: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
const userMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content:
|
||||
waitingConfig.value?.cancel_text ||
|
||||
$t('ai-platform.agent.chatPanel.cancelText'),
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMsg);
|
||||
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
|
||||
doSendMessage('false', false);
|
||||
}
|
||||
|
||||
// 格式化交互值显示
|
||||
function formatInteractionValue(value: any) {
|
||||
if (value === true) {
|
||||
return (
|
||||
waitingConfig.value?.confirm_text ||
|
||||
$t('ai-platform.agent.chatPanel.confirm')
|
||||
);
|
||||
}
|
||||
if (value === false) {
|
||||
return (
|
||||
waitingConfig.value?.cancel_text ||
|
||||
$t('ai-platform.agent.chatPanel.cancelText')
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const options = waitingConfig.value?.options || [];
|
||||
const labels = value.map((v: string) => {
|
||||
const opt = options.find((o: any) => o.value === v);
|
||||
return opt?.label || v;
|
||||
});
|
||||
return labels.join(', ');
|
||||
}
|
||||
if (waitingConfig.value?.type === 'choice') {
|
||||
const options = waitingConfig.value?.options || [];
|
||||
const opt = options.find((o: any) => o.value === value);
|
||||
return opt?.label || value;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
chatBoxRef.value?.scrollToBottom();
|
||||
}
|
||||
|
||||
// 清空对话
|
||||
function clearChat() {
|
||||
chatMessages.value = [];
|
||||
conversationId.value = null;
|
||||
streamingContent.value = '';
|
||||
streamingSteps.value = [];
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
}
|
||||
|
||||
// 添加消息(语音消息用)
|
||||
function handleAddMessage(message: any) {
|
||||
const agentMsg: AgentMessage = {
|
||||
id: message.id || generateId(),
|
||||
role: message.role || 'user',
|
||||
content: message.content || '',
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
voice: message.voice,
|
||||
};
|
||||
chatMessages.value.push(agentMsg);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 更新消息(语音识别完成后用)
|
||||
function handleUpdateMessage(messageId: string, updates: any) {
|
||||
const msgIndex = chatMessages.value.findIndex((m) => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
const msg = chatMessages.value[msgIndex]!;
|
||||
chatMessages.value[msgIndex] = { ...msg, ...updates };
|
||||
|
||||
// 如果语音识别成功,触发发送
|
||||
if (updates.content && !updates.content.startsWith('[')) {
|
||||
doSendMessage(updates.content, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消息反馈
|
||||
async function handleChatFeedback(
|
||||
messageId: string,
|
||||
feedback: 'dislike' | 'like',
|
||||
) {
|
||||
if (messageId.startsWith('temp-') || messageId.startsWith('msg-')) return;
|
||||
|
||||
try {
|
||||
await feedbackAgentMessageApi(messageId, { feedback });
|
||||
const msg = chatMessages.value.find((m) => m.id === messageId);
|
||||
if (msg) {
|
||||
msg.feedback = feedback;
|
||||
}
|
||||
ElMessage.success($t('ai-platform.agent.chatPanel.feedbackThanks'));
|
||||
} catch (error) {
|
||||
console.error('反馈失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设计面板确认
|
||||
function handleDesignConfirm(data: Record<string, any>) {
|
||||
showDesignPanel.value = false;
|
||||
|
||||
// 添加用户确认消息
|
||||
const userMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: $t('ai-platform.agent.chatPanel.confirmDesign'),
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMsg);
|
||||
|
||||
// 重置等待状态
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
currentDesign.value = null;
|
||||
|
||||
// 继续工作流,传递编辑后的数据
|
||||
doSendMessage(JSON.stringify(data), false);
|
||||
}
|
||||
|
||||
// 设计面板取消
|
||||
function handleDesignCancel() {
|
||||
showDesignPanel.value = false;
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
currentDesign.value = null;
|
||||
}
|
||||
|
||||
// 应用设计面板确认
|
||||
function handleAppDesignConfirm(data: Record<string, any>) {
|
||||
showAppDesignPanel.value = false;
|
||||
|
||||
// 添加用户确认消息
|
||||
const userMsg: AgentMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: $t('ai-platform.agent.chatPanel.confirmAppDesign'),
|
||||
status: 'completed',
|
||||
reasoning_steps: [],
|
||||
tool_calls: [],
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
elapsed_time: 0,
|
||||
error_message: '',
|
||||
feedback: '',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
chatMessages.value.push(userMsg);
|
||||
|
||||
// 重置等待状态
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
currentAppDesign.value = null;
|
||||
|
||||
// 继续工作流,传递编辑后的数据
|
||||
doSendMessage(JSON.stringify(data), false);
|
||||
}
|
||||
|
||||
// 应用设计面板取消
|
||||
function handleAppDesignCancel() {
|
||||
showAppDesignPanel.value = false;
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
currentAppDesign.value = null;
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
clearChat,
|
||||
scrollToBottom,
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelStream?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full overflow-hidden">
|
||||
<!-- 聊天区域 -->
|
||||
<div
|
||||
class="h-full overflow-hidden"
|
||||
:class="showDesignPanel ? 'w-[400px] flex-shrink-0' : 'flex-1'"
|
||||
>
|
||||
<ChatBox
|
||||
ref="chatBoxRef"
|
||||
:messages="formattedMessages"
|
||||
:loading="sending"
|
||||
:streaming-content="streamingContent"
|
||||
:assistant-name="agent?.name"
|
||||
:welcome-config="welcomeConfig"
|
||||
:disabled="disabled"
|
||||
:placeholder="
|
||||
placeholder || $t('ai-platform.agent.chatPanel.inputPlaceholder')
|
||||
"
|
||||
:enable-feedback="true"
|
||||
:enable-regenerate="false"
|
||||
:enable-attachment="enableAttachment ?? false"
|
||||
:enable-image="enableImage ?? false"
|
||||
:enable-voice="enableVoice ?? true"
|
||||
:show-clear-button="showClearButton ?? true"
|
||||
class="h-full"
|
||||
@send="handleSendMessage"
|
||||
@feedback="handleChatFeedback"
|
||||
@clear="clearChat"
|
||||
@interaction-submit="handleInteractionSubmit"
|
||||
@interaction-cancel="handleInteractionCancel"
|
||||
@add-message="handleAddMessage"
|
||||
@update-message="handleUpdateMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 设计面板 -->
|
||||
<div
|
||||
v-if="showDesignPanel && currentDesign"
|
||||
class="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<!-- 加载状态 - AI 工作中动画 -->
|
||||
<AiWorkingAnimation
|
||||
v-if="currentDesign.type === 'loading'"
|
||||
:title="currentDesign.title"
|
||||
:subtitle="$t('ai-platform.agent.chatPanel.analyzingAndGenerating')"
|
||||
class="ml-3 h-full min-h-[400px] flex-1 rounded-lg"
|
||||
/>
|
||||
<!-- 设计编辑器 -->
|
||||
<DesignEditorPanel
|
||||
v-else
|
||||
v-model:visible="showDesignPanel"
|
||||
:design="{
|
||||
type: currentDesign.type as any,
|
||||
title: currentDesign.title,
|
||||
data: currentDesign.data,
|
||||
nodeId: currentDesign.nodeId,
|
||||
form_fields: currentDesign.form_fields,
|
||||
table_configs: currentDesign.table_configs,
|
||||
}"
|
||||
@confirm="handleDesignConfirm"
|
||||
@close="handleDesignCancel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 应用设计面板 -->
|
||||
<div
|
||||
v-if="showAppDesignPanel && currentAppDesign"
|
||||
class="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<AppDesignPanel
|
||||
:visible="showAppDesignPanel"
|
||||
:design="currentAppDesign as any"
|
||||
@update:visible="showAppDesignPanel = $event"
|
||||
@confirm="handleAppDesignConfirm"
|
||||
@close="handleAppDesignCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuItem } from '#/components/zq-form/zq-menu-selector/types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElScrollbar,
|
||||
} from 'element-plus';
|
||||
|
||||
import { publishAgentToMenuApi } from '#/api/ai-platform/ai-platform';
|
||||
import { ZqIconPicker } from '#/components/zq-form/zq-icon-picker';
|
||||
import { ZqMenuSelector } from '#/components/zq-form/zq-menu-selector';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
agentCode?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
published: [];
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const appContextStore = useAppContextStore();
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// 发布表单
|
||||
const publishForm = ref({
|
||||
menu_name: '',
|
||||
menu_parent_id: undefined as string | undefined,
|
||||
menu_icon: 'lucide:bot',
|
||||
menu_order: 0,
|
||||
});
|
||||
|
||||
// 监听弹窗打开
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
// 重置表单,菜单名称默认为智能体名称
|
||||
publishForm.value = {
|
||||
menu_name: props.agentName,
|
||||
menu_parent_id: undefined,
|
||||
menu_icon: 'lucide:bot',
|
||||
menu_order: 0,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 菜单选择回调
|
||||
function handleMenuChange(menu: MenuItem | MenuItem[] | null) {
|
||||
publishForm.value.menu_parent_id =
|
||||
menu && !Array.isArray(menu) ? menu.id : undefined;
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
if (!publishForm.value.menu_name) {
|
||||
ElMessage.warning($t('ui.placeholder.input'));
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
await publishAgentToMenuApi(props.agentId, {
|
||||
menu_name: publishForm.value.menu_name,
|
||||
menu_parent_id: publishForm.value.menu_parent_id,
|
||||
menu_icon: publishForm.value.menu_icon,
|
||||
menu_order: publishForm.value.menu_order,
|
||||
});
|
||||
ElMessage.success($t('common.operationSuccess'));
|
||||
emit('published');
|
||||
dialogVisible.value = false;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || $t('common.operationFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('ai-platform.agent.publishToMenu')"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
align-center
|
||||
append-to-body
|
||||
>
|
||||
<ElScrollbar max-height="60vh">
|
||||
<div class="pr-4">
|
||||
<ElForm :model="publishForm" label-width="100px">
|
||||
<!-- 菜单配置 -->
|
||||
<div class="mb-4 font-medium">
|
||||
{{ $t('form-manager.publishDialog.menuConfig') }}
|
||||
</div>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('form-manager.publishDialog.menuName')"
|
||||
required
|
||||
>
|
||||
<ElInput
|
||||
v-model="publishForm.menu_name"
|
||||
:placeholder="$t('form-manager.placeholder.name')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('form-manager.publishDialog.parentMenu')">
|
||||
<ZqMenuSelector
|
||||
:model-value="publishForm.menu_parent_id || null"
|
||||
mode="dialog"
|
||||
:placeholder="
|
||||
$t('form-manager.publishDialog.parentMenuPlaceholder')
|
||||
"
|
||||
:application-id="appContextStore.currentApp?.id"
|
||||
@change="handleMenuChange"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('form-manager.publishDialog.menuIcon')">
|
||||
<ZqIconPicker
|
||||
v-model="publishForm.menu_icon"
|
||||
prefix="lucide"
|
||||
:auto-fetch-api="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('common.sort')">
|
||||
<ElInputNumber
|
||||
v-model="publishForm.menu_order"
|
||||
:min="0"
|
||||
:max="999"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 路由信息 -->
|
||||
<template v-if="agentCode">
|
||||
<div class="mb-4 mt-6 font-medium">
|
||||
{{ $t('form-manager.publishDialog.routeInfo') }}
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('form-manager.publishDialog.accessPath')">
|
||||
<ElInput
|
||||
:model-value="`/agent-chat/${agentCode}`"
|
||||
disabled
|
||||
class="text-muted-foreground"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" :loading="loading" @click="handlePublish">
|
||||
{{ $t('common.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 确保 IconPicker 的弹出层在 Dialog 之上 */
|
||||
.z-popup {
|
||||
z-index: 2100 !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,642 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AgentCreateInput,
|
||||
AgentListItem,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
Bot,
|
||||
CirclePlus,
|
||||
Download,
|
||||
Edit,
|
||||
Menu,
|
||||
MessageSquare,
|
||||
MoreVertical,
|
||||
Play,
|
||||
Square,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDialog,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
createAgentApi,
|
||||
deleteAgentApi,
|
||||
disableAgentApi,
|
||||
exportAgentConfigApi,
|
||||
getAgentListApi,
|
||||
publishAgentApi,
|
||||
unpublishAgentMenuApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
import PublishToMenuDialog from './components/PublishToMenuDialog.vue';
|
||||
import ImportDialog from './modules/import-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'AgentList' });
|
||||
|
||||
const router = useRouter();
|
||||
const appContextStore = useAppContextStore();
|
||||
|
||||
// 是否在主应用下(只有主应用才显示“子应用可见”开关)
|
||||
const isMainApp = !appContextStore.currentApp?.id;
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 列表数据
|
||||
const loading = ref(false);
|
||||
const agentList = ref<AgentListItem[]>([]);
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 发布到菜单弹窗
|
||||
const showPublishToMenuDialog = ref(false);
|
||||
const publishingAgent = ref<AgentListItem | null>(null);
|
||||
|
||||
// 导入弹窗
|
||||
const showImportDialog = ref(false);
|
||||
|
||||
// 创建对话框
|
||||
const createDialogVisible = ref(false);
|
||||
const createForm = reactive<AgentCreateInput>({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
mode: 'autonomous',
|
||||
is_global: false,
|
||||
});
|
||||
const createLoading = ref(false);
|
||||
|
||||
// 获取列表
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAgentListApi({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
name: searchKeyword.value || undefined,
|
||||
applicationId: appContextStore.currentApp?.id,
|
||||
});
|
||||
agentList.value = res.items;
|
||||
pagination.total = res.total;
|
||||
} catch (error) {
|
||||
console.error('获取智能体列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.current = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handlePageChange(page: number) {
|
||||
pagination.current = page;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// 每页条数变化
|
||||
function handleSizeChange(size: number) {
|
||||
pagination.pageSize = size;
|
||||
pagination.current = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// 打开创建对话框
|
||||
function openCreateDialog() {
|
||||
createForm.name = '';
|
||||
createForm.code = '';
|
||||
createForm.description = '';
|
||||
createForm.mode = 'autonomous';
|
||||
createForm.is_global = false;
|
||||
createDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 创建智能体
|
||||
async function handleCreate() {
|
||||
if (!createForm.name || !createForm.code) {
|
||||
ElMessage.warning($t('ai-platform.agent.fillNameAndCode'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 code 格式
|
||||
if (!/^[a-z]\w*$/i.test(createForm.code)) {
|
||||
ElMessage.warning($t('ai-platform.agent.codeFormatError'));
|
||||
return;
|
||||
}
|
||||
|
||||
createLoading.value = true;
|
||||
try {
|
||||
const agent = await createAgentApi({
|
||||
...createForm,
|
||||
application_id: appContextStore.currentApp?.id,
|
||||
});
|
||||
ElMessage.success($t('ai-platform.agent.createSuccess'));
|
||||
createDialogVisible.value = false;
|
||||
// 跳转到编辑页(支持子应用模式)
|
||||
router.push(
|
||||
appContextStore.getContextPath(`/ai-platform/agent/editor/${agent.id}`),
|
||||
);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || $t('ai-platform.agent.createFailed'));
|
||||
} finally {
|
||||
createLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑智能体
|
||||
function handleEdit(agent: AgentListItem) {
|
||||
router.push(
|
||||
appContextStore.getContextPath(`/ai-platform/agent/editor/${agent.id}`),
|
||||
);
|
||||
}
|
||||
|
||||
// 对话
|
||||
function handleChat(agent: AgentListItem) {
|
||||
router.push(
|
||||
appContextStore.getContextPath(`/ai-platform/agent/chat/${agent.id}`),
|
||||
);
|
||||
}
|
||||
|
||||
// 发布
|
||||
async function handlePublish(agent: AgentListItem) {
|
||||
try {
|
||||
await publishAgentApi(agent.id);
|
||||
ElMessage.success($t('ai-platform.agent.publishSuccess'));
|
||||
fetchList();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || $t('ai-platform.agent.publishFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
// 停用
|
||||
async function handleDisable(agent: AgentListItem) {
|
||||
try {
|
||||
await disableAgentApi(agent.id);
|
||||
ElMessage.success($t('ai-platform.agent.disableSuccess'));
|
||||
fetchList();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || $t('ai-platform.agent.operationFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleDelete(agent: AgentListItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('ai-platform.agent.deleteConfirm', { name: agent.name }),
|
||||
$t('ai-platform.agent.deleteTitle'),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
await deleteAgentApi(agent.id);
|
||||
ElMessage.success($t('ai-platform.agent.deleteSuccess'));
|
||||
fetchList();
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || $t('ai-platform.agent.deleteFailed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusType(status: string) {
|
||||
switch (status) {
|
||||
case 'disabled': {
|
||||
return 'danger';
|
||||
}
|
||||
case 'published': {
|
||||
return 'success';
|
||||
}
|
||||
default: {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string) {
|
||||
switch (status) {
|
||||
case 'disabled': {
|
||||
return $t('ai-platform.agent.status.disabled');
|
||||
}
|
||||
case 'published': {
|
||||
return $t('ai-platform.agent.status.published');
|
||||
}
|
||||
default: {
|
||||
return $t('ai-platform.agent.status.draft');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模式文本
|
||||
function getModeText(mode: string) {
|
||||
return mode === 'autonomous'
|
||||
? $t('ai-platform.agent.mode.autonomous')
|
||||
: $t('ai-platform.agent.mode.dialogFlow');
|
||||
}
|
||||
|
||||
// 发布到菜单
|
||||
function handlePublishToMenu(agent: AgentListItem) {
|
||||
publishingAgent.value = agent;
|
||||
showPublishToMenuDialog.value = true;
|
||||
}
|
||||
|
||||
// 发布到菜单成功回调
|
||||
function handlePublishedToMenu() {
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// 取消发布菜单
|
||||
async function handleUnpublishMenu(agent: AgentListItem) {
|
||||
try {
|
||||
await unpublishAgentMenuApi(agent.id);
|
||||
ElMessage.success($t('ai-platform.agent.unpublishMenuSuccess'));
|
||||
fetchList();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || $t('ai-platform.agent.operationFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function handleExport(agent: AgentListItem) {
|
||||
try {
|
||||
const blob = await exportAgentConfigApi(agent.id);
|
||||
downloadBlob(blob, `${agent.code}.json`);
|
||||
ElMessage.success($t('ai-platform.agent.importExport.exportSuccess'));
|
||||
} catch {
|
||||
ElMessage.error($t('ai-platform.agent.importExport.exportFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
function handleImported() {
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// 处理更多菜单命令
|
||||
function handleCommand(command: string, agent: AgentListItem) {
|
||||
switch (command) {
|
||||
case 'export': {
|
||||
handleExport(agent);
|
||||
break;
|
||||
}
|
||||
case 'chat': {
|
||||
handleChat(agent);
|
||||
break;
|
||||
}
|
||||
case 'disable': {
|
||||
handleDisable(agent);
|
||||
break;
|
||||
}
|
||||
case 'publish': {
|
||||
handlePublish(agent);
|
||||
break;
|
||||
}
|
||||
case 'publishToMenu': {
|
||||
handlePublishToMenu(agent);
|
||||
break;
|
||||
}
|
||||
case 'unpublishMenu': {
|
||||
handleUnpublishMenu(agent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-list-page">
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('ai-platform.agent.searchPlaceholder')"
|
||||
clearable
|
||||
class="w-64"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<ElButton @click="handleSearch">
|
||||
{{ $t('ai-platform.agent.search') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElButton @click="showImportDialog = true">
|
||||
<Upload class="mr-1 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.importExport.import') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="openCreateDialog">
|
||||
<CirclePlus class="mr-1 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.create') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 智能体卡片列表 -->
|
||||
<div
|
||||
v-if="agentList.length > 0"
|
||||
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
|
||||
>
|
||||
<ElCard
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
class="agent-card cursor-pointer transition-shadow"
|
||||
shadow="hover"
|
||||
:body-style="{ padding: '0' }"
|
||||
style="border: none"
|
||||
@click="handleEdit(agent)"
|
||||
>
|
||||
<div class="p-4">
|
||||
<!-- 头部:图标 + 右侧信息区 -->
|
||||
<div class="mb-4 flex gap-3">
|
||||
<div
|
||||
class="bg-primary/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg"
|
||||
>
|
||||
<Bot class="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<!-- name + 操作 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{{ agent.name }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center -space-x-1"
|
||||
@click.stop
|
||||
>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.agent.edit')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleEdit(agent)">
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.agent.delete')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleDelete(agent)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
@command="(cmd: string) => handleCommand(cmd, agent)"
|
||||
>
|
||||
<ElButton text size="small">
|
||||
<MoreVertical class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-if="agent.status === 'published'"
|
||||
command="chat"
|
||||
>
|
||||
<MessageSquare class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.chat') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="agent.status === 'draft'"
|
||||
command="publish"
|
||||
>
|
||||
<Play class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.publish') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="
|
||||
agent.status === 'published' && !agent.has_menu
|
||||
"
|
||||
command="publishToMenu"
|
||||
>
|
||||
<Menu class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.publishToMenu') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="
|
||||
agent.status === 'published' && agent.has_menu
|
||||
"
|
||||
command="unpublishMenu"
|
||||
>
|
||||
<Square class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.unpublishMenu') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="export">
|
||||
<Download class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.importExport.export') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="agent.status === 'published'"
|
||||
command="disable"
|
||||
>
|
||||
<Square class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.agent.disable') }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<!-- code -->
|
||||
<div class="text-muted-foreground font-mono text-xs">
|
||||
{{ agent.code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div
|
||||
class="text-muted-foreground mb-4 line-clamp-1 min-h-[18px] text-xs"
|
||||
>
|
||||
{{ agent.description || $t('ai-platform.agent.noDescription') }}
|
||||
</div>
|
||||
<!-- 标签 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<ElTag size="small" :type="getStatusType(agent.status) as any">
|
||||
{{ getStatusText(agent.status) }}
|
||||
</ElTag>
|
||||
<ElTag size="small" type="info">
|
||||
{{ getModeText(agent.mode) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 应用名称 + 创建时间 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<span v-if="agent.application_name">{{
|
||||
agent.application_name
|
||||
}}</span>
|
||||
<span v-else>{{ $t('ai-platform.agent.mainApp') }}</span>
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ agent.sys_create_datetime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-else :description="$t('ai-platform.agent.empty')" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<template #footer>
|
||||
<div class="flex w-full items-center justify-end">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:pager-count="7"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
size="small"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Page>
|
||||
|
||||
<ImportDialog v-model="showImportDialog" @imported="handleImported" />
|
||||
|
||||
<!-- 发布到菜单弹窗 -->
|
||||
<PublishToMenuDialog
|
||||
v-if="publishingAgent"
|
||||
v-model="showPublishToMenuDialog"
|
||||
:agent-id="publishingAgent.id"
|
||||
:agent-name="publishingAgent.name"
|
||||
:agent-code="publishingAgent.code"
|
||||
@published="handlePublishedToMenu"
|
||||
/>
|
||||
|
||||
<!-- 创建对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
:title="$t('ai-platform.agent.create')"
|
||||
width="500px"
|
||||
>
|
||||
<ElForm label-width="80px">
|
||||
<ElFormItem :label="$t('ai-platform.agent.form.name')" required>
|
||||
<ElInput
|
||||
v-model="createForm.name"
|
||||
:placeholder="$t('ai-platform.agent.form.namePlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('ai-platform.agent.form.code')" required>
|
||||
<ElInput
|
||||
v-model="createForm.code"
|
||||
:placeholder="$t('ai-platform.agent.form.codePlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('ai-platform.agent.form.mode')">
|
||||
<ElSelect v-model="createForm.mode" style="width: 100%">
|
||||
<ElOption
|
||||
:label="$t('ai-platform.agent.mode.autonomousLabel')"
|
||||
value="autonomous"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<div>{{ $t('ai-platform.agent.mode.autonomousLabel') }}</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.agent.mode.autonomousDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElOption>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.agent.mode.dialogFlowLabel')"
|
||||
value="dialog_flow"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<div>{{ $t('ai-platform.agent.mode.dialogFlowLabel') }}</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.agent.mode.dialogFlowDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('ai-platform.agent.form.description')">
|
||||
<ElInput
|
||||
v-model="createForm.description"
|
||||
:rows="3"
|
||||
:placeholder="$t('ai-platform.agent.form.descriptionPlaceholder')"
|
||||
type="textarea"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="isMainApp"
|
||||
:label="$t('ai-platform.agent.form.globalVisible')"
|
||||
>
|
||||
<ElSwitch v-model="createForm.is_global" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="createDialogVisible = false">
|
||||
{{ $t('ai-platform.agent.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton :loading="createLoading" type="primary" @click="handleCreate">
|
||||
{{ $t('ai-platform.agent.create') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-card :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
AgentImportCheckResult,
|
||||
AgentImportInput,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Upload } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
checkImportAgentApi,
|
||||
importAgentConfigApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
imported: [];
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const appContextStore = useAppContextStore();
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
type Step = 'check' | 'result' | 'upload';
|
||||
|
||||
const step = ref<Step>('upload');
|
||||
const loading = ref(false);
|
||||
const importData = ref<AgentImportInput | null>(null);
|
||||
const checkResult = ref<AgentImportCheckResult | null>(null);
|
||||
const newCode = ref('');
|
||||
|
||||
function resetState() {
|
||||
step.value = 'upload';
|
||||
loading.value = false;
|
||||
importData.value = null;
|
||||
checkResult.value = null;
|
||||
newCode.value = '';
|
||||
}
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) resetState();
|
||||
});
|
||||
|
||||
const canConfirmImport = computed(() => {
|
||||
if (!checkResult.value || !importData.value) return false;
|
||||
if (checkResult.value.code_exists && !newCode.value.trim()) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const showFooter = computed(() => step.value === 'result');
|
||||
|
||||
function readFileAsJson(file: File): Promise<AgentImportInput> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
resolve(JSON.parse(reader.result as string));
|
||||
} catch {
|
||||
reject(new Error('parse'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('read'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFileChange(file: any) {
|
||||
const rawFile = (file.raw || file) as File;
|
||||
if (!rawFile) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await readFileAsJson(rawFile);
|
||||
if (!data.name || !data.code) {
|
||||
ElMessage.error($t('ai-platform.agent.importExport.fileParseError'));
|
||||
return;
|
||||
}
|
||||
importData.value = data;
|
||||
newCode.value = data.code;
|
||||
step.value = 'check';
|
||||
await runCheck();
|
||||
} catch {
|
||||
ElMessage.error($t('ai-platform.agent.importExport.fileParseError'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCheck() {
|
||||
if (!importData.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
checkResult.value = await checkImportAgentApi({
|
||||
code: importData.value.code,
|
||||
});
|
||||
step.value = 'result';
|
||||
} catch (error: any) {
|
||||
ElMessage.error(
|
||||
error?.message || $t('ai-platform.agent.importExport.importFailed'),
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmImport() {
|
||||
if (!importData.value || !canConfirmImport.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const payload: AgentImportInput = {
|
||||
...importData.value,
|
||||
application_id: appContextStore.currentApp?.id,
|
||||
code: checkResult.value?.code_exists
|
||||
? newCode.value.trim()
|
||||
: importData.value.code,
|
||||
};
|
||||
await importAgentConfigApi(payload);
|
||||
ElMessage.success($t('ai-platform.agent.importExport.importSuccess'));
|
||||
dialogVisible.value = false;
|
||||
emit('imported');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(
|
||||
error?.message || $t('ai-platform.agent.importExport.importFailed'),
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('ai-platform.agent.importExport.importTitle')"
|
||||
:confirm-loading="loading"
|
||||
width="520px"
|
||||
:show-footer="showFooter"
|
||||
@confirm="handleConfirmImport"
|
||||
>
|
||||
<template #footer>
|
||||
<ElButton @click="resetState">
|
||||
{{ $t('ai-platform.agent.importExport.reselect') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="!canConfirmImport"
|
||||
@click="handleConfirmImport"
|
||||
>
|
||||
{{ $t('ai-platform.agent.importExport.confirmImport') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
|
||||
<div v-if="step === 'upload' || step === 'check'" class="py-4">
|
||||
<ElUpload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
:disabled="loading"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<div class="flex flex-col items-center py-6">
|
||||
<Upload class="text-muted-foreground mb-3 h-10 w-10" />
|
||||
<div class="text-sm">
|
||||
{{ $t('ai-platform.agent.importExport.dragOrClick') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.agent.importExport.onlyJson') }}
|
||||
</div>
|
||||
</div>
|
||||
</ElUpload>
|
||||
<div
|
||||
v-if="loading && step === 'check'"
|
||||
class="text-muted-foreground mt-4 text-center text-sm"
|
||||
>
|
||||
{{ $t('ai-platform.agent.importExport.checking') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="step === 'result' && importData && checkResult" class="space-y-4">
|
||||
<ElAlert
|
||||
:title="$t('ai-platform.agent.importExport.appTip')"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<ElAlert
|
||||
:title="$t('ai-platform.agent.importExport.refTip')"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.agent.importExport.agentInfo') }}
|
||||
</div>
|
||||
<ElDescriptions :column="1" border size="small">
|
||||
<ElDescriptionsItem :label="$t('ai-platform.agent.form.name')">
|
||||
{{ importData.name }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('ai-platform.agent.form.code')">
|
||||
{{ importData.code }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="importData.mode"
|
||||
:label="$t('ai-platform.agent.form.mode')"
|
||||
>
|
||||
{{ importData.mode }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="importData.description"
|
||||
:label="$t('ai-platform.agent.form.description')"
|
||||
>
|
||||
{{ importData.description }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="importData.model_name"
|
||||
:label="$t('ai-platform.agent.importExport.modelName')"
|
||||
>
|
||||
{{ importData.model_name }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="importData.workflow_code"
|
||||
:label="$t('ai-platform.agent.importExport.workflowCode')"
|
||||
>
|
||||
{{ importData.workflow_code }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="checkResult.code_exists">
|
||||
<ElAlert
|
||||
:title="$t('ai-platform.agent.importExport.codeConflictTip')"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="newCode"
|
||||
:placeholder="$t('ai-platform.agent.importExport.newCodePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<ElAlert
|
||||
v-else
|
||||
:title="$t('ai-platform.agent.importExport.codeAvailable')"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,690 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
KnowledgeBaseCreateInput,
|
||||
KnowledgeBaseListItem,
|
||||
ModelListItem,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
CircleHelp,
|
||||
Database,
|
||||
Edit,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
createKnowledgeBaseApi,
|
||||
deleteKnowledgeBaseApi,
|
||||
getActiveModelsApi,
|
||||
getKnowledgeBaseListApi,
|
||||
updateKnowledgeBaseApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
defineOptions({ name: 'KnowledgeBase' });
|
||||
|
||||
const router = useRouter();
|
||||
const appContextStore = useAppContextStore();
|
||||
|
||||
const isMainApp = !appContextStore.currentApp?.id;
|
||||
|
||||
// 表单
|
||||
const createForm = reactive<KnowledgeBaseCreateInput & { id?: string }>({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
embedding_model_id: undefined,
|
||||
embedding_dimensions: 1536,
|
||||
chunk_strategy: 'auto',
|
||||
chunk_size: 500,
|
||||
chunk_overlap: 50,
|
||||
retrieval_mode: 'hybrid',
|
||||
top_k: 5,
|
||||
score_threshold: 0.5,
|
||||
indexing_technique: 'high_quality',
|
||||
is_global: false,
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const isEditMode = ref(false);
|
||||
const formLoading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
|
||||
// Embedding 模型列表
|
||||
const embeddingModels = ref<ModelListItem[]>([]);
|
||||
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
const res = await getActiveModelsApi();
|
||||
embeddingModels.value = res.filter((m) => m.model_type === 'embedding');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 列表
|
||||
const searchKeyword = ref('');
|
||||
const loading = ref(false);
|
||||
const kbList = ref<KnowledgeBaseListItem[]>([]);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getKnowledgeBaseListApi({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
name: searchKeyword.value || undefined,
|
||||
applicationId: appContextStore.currentApp?.id,
|
||||
});
|
||||
kbList.value = res.items;
|
||||
pagination.total = res.total;
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge base list:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.current = page;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pagination.pageSize = size;
|
||||
pagination.current = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
isEditMode.value = false;
|
||||
createForm.id = undefined;
|
||||
createForm.name = '';
|
||||
createForm.code = '';
|
||||
createForm.description = '';
|
||||
createForm.embedding_model_id = undefined;
|
||||
createForm.embedding_dimensions = 1536;
|
||||
createForm.chunk_strategy = 'auto';
|
||||
createForm.chunk_size = 500;
|
||||
createForm.chunk_overlap = 50;
|
||||
createForm.retrieval_mode = 'hybrid';
|
||||
createForm.top_k = 5;
|
||||
createForm.score_threshold = 0.5;
|
||||
createForm.indexing_technique = 'high_quality';
|
||||
createForm.is_global = false;
|
||||
dialogTitle.value = $t('ai-platform.knowledge.create');
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEditInfo(item: KnowledgeBaseListItem) {
|
||||
isEditMode.value = true;
|
||||
createForm.id = item.id;
|
||||
createForm.name = item.name;
|
||||
createForm.code = item.code;
|
||||
createForm.description = item.description || '';
|
||||
createForm.is_global = item.is_global || false;
|
||||
dialogTitle.value = $t('ai-platform.knowledge.edit');
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
formLoading.value = true;
|
||||
try {
|
||||
if (isEditMode.value && createForm.id) {
|
||||
await updateKnowledgeBaseApi(createForm.id, {
|
||||
name: createForm.name,
|
||||
description: createForm.description,
|
||||
is_global: createForm.is_global,
|
||||
});
|
||||
ElMessage.success($t('ai-platform.knowledge.saveSuccess'));
|
||||
} else {
|
||||
const res = await createKnowledgeBaseApi({
|
||||
application_id: appContextStore.currentApp?.id,
|
||||
is_global: createForm.is_global,
|
||||
name: createForm.name,
|
||||
code: createForm.code,
|
||||
description: createForm.description,
|
||||
embedding_model_id: createForm.embedding_model_id,
|
||||
embedding_dimensions: createForm.embedding_dimensions,
|
||||
chunk_strategy: createForm.chunk_strategy,
|
||||
chunk_size: createForm.chunk_size,
|
||||
chunk_overlap: createForm.chunk_overlap,
|
||||
retrieval_mode: createForm.retrieval_mode,
|
||||
top_k: createForm.top_k,
|
||||
score_threshold: createForm.score_threshold,
|
||||
indexing_technique: createForm.indexing_technique,
|
||||
});
|
||||
ElMessage.success($t('ai-platform.knowledge.createSuccess'));
|
||||
// 跳转到详情页
|
||||
router.push(
|
||||
appContextStore.getContextPath(
|
||||
`/ai-platform/knowledge/detail/${res.id}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
fetchList();
|
||||
} catch {
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDetail(item: KnowledgeBaseListItem) {
|
||||
router.push(
|
||||
appContextStore.getContextPath(`/ai-platform/knowledge/detail/${item.id}`),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDelete(item: KnowledgeBaseListItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('ai-platform.knowledge.deleteConfirm'),
|
||||
$t('ai-platform.knowledge.delete'),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
await deleteKnowledgeBaseApi(item.id);
|
||||
ElMessage.success($t('ai-platform.knowledge.deleteSuccess'));
|
||||
fetchList();
|
||||
} catch {
|
||||
// cancelled
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommand(cmd: string, item: KnowledgeBaseListItem) {
|
||||
if (cmd === 'edit') {
|
||||
handleEditInfo(item);
|
||||
} else if (cmd === 'delete') {
|
||||
handleDelete(item);
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
active: 'success',
|
||||
disabled: 'info',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
if (status === 'active') return $t('ai-platform.knowledge.active');
|
||||
if (status === 'disabled') return $t('ai-platform.knowledge.disabled');
|
||||
return status;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
loadEmbeddingModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-knowledge-list-page">
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('ai-platform.knowledge.searchPlaceholder')"
|
||||
clearable
|
||||
class="w-64"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<ElButton @click="handleSearch">
|
||||
{{ $t('ai-platform.knowledge.search') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElButton type="primary" @click="handleCreate">
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
{{ $t('ai-platform.knowledge.create') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div
|
||||
v-if="kbList.length > 0"
|
||||
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
|
||||
>
|
||||
<ElCard
|
||||
v-for="item in kbList"
|
||||
:key="item.id"
|
||||
class="cursor-pointer transition-shadow"
|
||||
shadow="hover"
|
||||
:body-style="{ padding: '0' }"
|
||||
style="border: none"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<div class="p-4">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-4 flex gap-3">
|
||||
<div
|
||||
class="bg-primary/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg"
|
||||
>
|
||||
<Database class="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center -space-x-1"
|
||||
@click.stop
|
||||
>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.knowledge.edit')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleEditInfo(item)">
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.knowledge.delete')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
@command="(cmd: string) => handleCommand(cmd, item)"
|
||||
>
|
||||
<ElButton text size="small">
|
||||
<MoreVertical class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="edit">
|
||||
<Edit class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.knowledge.edit') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="delete">
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{{ $t('ai-platform.knowledge.delete') }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted-foreground font-mono text-xs">
|
||||
{{ item.code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div
|
||||
class="text-muted-foreground mb-4 line-clamp-1 min-h-[18px] text-xs"
|
||||
>
|
||||
{{
|
||||
item.description || $t('ai-platform.knowledge.noDescription')
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div class="mb-4 flex items-center gap-1">
|
||||
<ElTag size="small" :type="getStatusType(item.status) as any">
|
||||
{{ getStatusText(item.status) }}
|
||||
</ElTag>
|
||||
<ElTag v-if="item.embedding_model_name" size="small">
|
||||
{{ item.embedding_model_name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
|
||||
<!-- 统计 + 时间 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-muted-foreground flex gap-3 text-xs">
|
||||
<span>
|
||||
{{ $t('ai-platform.knowledge.documents') }}:
|
||||
{{ item.document_count }}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('ai-platform.knowledge.segments') }}:
|
||||
{{ item.segment_count }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ item.sys_create_datetime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-else :description="$t('common.noData')" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<template #footer>
|
||||
<div class="flex w-full items-center justify-end">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:pager-count="7"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
size="small"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Page>
|
||||
|
||||
<!-- 创建/编辑弹窗 -->
|
||||
<ZqDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:confirm-loading="formLoading"
|
||||
width="560px"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="createForm"
|
||||
label-position="top"
|
||||
:rules="{
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('ai-platform.knowledge.namePlaceholder'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('ai-platform.knowledge.codePlaceholder'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<ElFormItem :label="$t('ai-platform.knowledge.name')" prop="name">
|
||||
<ElInput
|
||||
v-model="createForm.name"
|
||||
:placeholder="$t('ai-platform.knowledge.namePlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="!isEditMode"
|
||||
:label="$t('ai-platform.knowledge.code')"
|
||||
prop="code"
|
||||
>
|
||||
<ElInput
|
||||
v-model="createForm.code"
|
||||
:placeholder="$t('ai-platform.knowledge.codePlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('ai-platform.knowledge.description')">
|
||||
<ElInput
|
||||
v-model="createForm.description"
|
||||
type="textarea"
|
||||
:placeholder="$t('ai-platform.knowledge.descriptionPlaceholder')"
|
||||
:rows="3"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 创建模式下显示更多配置 -->
|
||||
<template v-if="!isEditMode">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.embeddingModel') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.embeddingModelTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElSelect
|
||||
v-model="createForm.embedding_model_id"
|
||||
:placeholder="
|
||||
$t('ai-platform.knowledge.embeddingModelPlaceholder')
|
||||
"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="model in embeddingModels"
|
||||
:key="model.id"
|
||||
:label="model.display_name"
|
||||
:value="model.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.chunkStrategy') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.chunkStrategyTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElSelect v-model="createForm.chunk_strategy" style="width: 100%">
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.auto')"
|
||||
value="auto"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.recursive')"
|
||||
value="recursive"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.markdown')"
|
||||
value="markdown"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.fixed')"
|
||||
value="fixed"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.sentence')"
|
||||
value="sentence"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.chunkStrategies.qa')"
|
||||
value="qa"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{
|
||||
$t('ai-platform.knowledge.indexingTechnique')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="
|
||||
$t('ai-platform.knowledge.indexingTechniqueTooltip')
|
||||
"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElSelect
|
||||
v-model="createForm.indexing_technique"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.knowledge.indexingTechniques.high_quality')
|
||||
"
|
||||
value="high_quality"
|
||||
/>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.knowledge.indexingTechniques.economy')
|
||||
"
|
||||
value="economy"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.retrievalMode') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.retrievalModeTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElSelect v-model="createForm.retrieval_mode" style="width: 100%">
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.retrievalModes.hybrid')"
|
||||
value="hybrid"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.retrievalModes.vector')"
|
||||
value="vector"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.knowledge.retrievalModes.fulltext')"
|
||||
value="fulltext"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.chunkSize') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.chunkSizeTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElInputNumber
|
||||
v-model="createForm.chunk_size"
|
||||
:min="100"
|
||||
:max="4000"
|
||||
:step="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.chunkOverlap') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.chunkOverlapTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElInputNumber
|
||||
v-model="createForm.chunk_overlap"
|
||||
:min="0"
|
||||
:max="500"
|
||||
:step="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElFormItem v-if="isMainApp">
|
||||
<template #label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ $t('ai-platform.knowledge.globalVisible') }}</span>
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
:content="$t('ai-platform.knowledge.globalVisibleTooltip')"
|
||||
>
|
||||
<CircleHelp
|
||||
class="text-muted-foreground h-3.5 w-3.5 cursor-help"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<ElSwitch v-model="createForm.is_global" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ZqDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { ExecutionLogEntry } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElEmpty, ElScrollbar, ElTimeline, ElTimelineItem } from 'element-plus';
|
||||
|
||||
import RunStatusTag from './RunStatusTag.vue';
|
||||
|
||||
defineProps<{
|
||||
activeNodeId?: string;
|
||||
logs: ExecutionLogEntry[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [nodeId: string];
|
||||
}>();
|
||||
|
||||
function formatDuration(ms?: number) {
|
||||
if (!ms && ms !== 0) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElScrollbar class="h-full">
|
||||
<ElEmpty
|
||||
v-if="logs.length === 0"
|
||||
:description="$t('ai-platform.workflowRuns.detail.noLogs')"
|
||||
/>
|
||||
<ElTimeline v-else class="px-2 py-3">
|
||||
<ElTimelineItem
|
||||
v-for="(log, index) in logs"
|
||||
:key="`${log.node_id}-${index}`"
|
||||
:timestamp="formatDuration(log.elapsed_time)"
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:bg-muted w-full rounded border px-3 py-2 text-left transition-colors"
|
||||
:class="activeNodeId === log.node_id ? 'border-primary bg-primary/5' : 'border-border'"
|
||||
@click="emit('select', log.node_id)"
|
||||
>
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-foreground text-sm font-medium">
|
||||
{{ log.node_label || log.node_id }}
|
||||
</span>
|
||||
<RunStatusTag :status="log.status" />
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ log.node_type }}
|
||||
</div>
|
||||
<div
|
||||
v-if="log.error"
|
||||
class="text-destructive mt-1 line-clamp-2 text-xs"
|
||||
>
|
||||
{{ log.error }}
|
||||
</div>
|
||||
</button>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
const props = defineProps<{ status: string }>();
|
||||
|
||||
const tagType = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
case 'running':
|
||||
return 'primary';
|
||||
case 'waiting':
|
||||
return 'warning';
|
||||
case 'stopped':
|
||||
return 'info';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
});
|
||||
|
||||
const label = computed(() => {
|
||||
const key = `ai-platform.workflowRuns.status.${props.status}`;
|
||||
const translated = $t(key);
|
||||
return translated === key ? props.status : translated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTag :type="tagType" size="small">{{ label }}</ElTag>
|
||||
</template>
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import type { Edge, Node } from '@vue-flow/core';
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVueFlow, VueFlow } from '@vue-flow/core';
|
||||
import { MiniMap } from '@vue-flow/minimap';
|
||||
|
||||
import { workflowEdgeTypes, workflowNodeTypes } from '../../workflow/shared/workflowNodeTypes';
|
||||
|
||||
import '@vue-flow/minimap/dist/style.css';
|
||||
import '@vue-flow/core/dist/style.css';
|
||||
import '@vue-flow/core/dist/theme-default.css';
|
||||
|
||||
const props = defineProps<{
|
||||
edges: Edge[];
|
||||
fitNodeId?: string;
|
||||
nodes: Node[];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const { fitView } = useVueFlow();
|
||||
|
||||
async function refreshView(nodeId?: string) {
|
||||
await nextTick();
|
||||
if (!containerRef.value?.clientHeight) return;
|
||||
if (nodeId) {
|
||||
fitView({ nodes: [nodeId], padding: 0.4, duration: 200 });
|
||||
return;
|
||||
}
|
||||
fitView({ padding: 0.2, duration: 200 });
|
||||
}
|
||||
|
||||
function onNodesInitialized() {
|
||||
refreshView(props.fitNodeId);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fitNodeId,
|
||||
(nodeId) => {
|
||||
if (nodeId) refreshView(nodeId);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.nodes, props.edges],
|
||||
() => refreshView(props.fitNodeId),
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
if (!containerRef.value) return;
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (containerRef.value && containerRef.value.clientHeight > 0) {
|
||||
refreshView(props.fitNodeId);
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="workflow-readonly-canvas bg-muted/30 h-full w-full min-h-[320px]"
|
||||
>
|
||||
<VueFlow
|
||||
:nodes="props.nodes"
|
||||
:edges="props.edges"
|
||||
:node-types="workflowNodeTypes"
|
||||
:edge-types="workflowEdgeTypes"
|
||||
:nodes-draggable="false"
|
||||
:nodes-connectable="false"
|
||||
:elements-selectable="false"
|
||||
:pan-on-drag="[1, 2]"
|
||||
:zoom-on-scroll="true"
|
||||
@nodes-initialized="onNodesInitialized"
|
||||
>
|
||||
<MiniMap pannable zoomable />
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workflow-readonly-canvas :deep(.vue-flow) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__container) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__node) {
|
||||
cursor: default;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.nopan) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__panel.top) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__edge button) {
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { Column } from 'element-plus';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { getWorkflowListApi } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
type TagType = 'danger' | 'info' | 'primary' | 'success' | 'warning';
|
||||
|
||||
export function getStatusOptions() {
|
||||
return [
|
||||
{ label: $t('ai-platform.workflowRuns.status.pending'), value: 'pending', type: 'info' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.running'), value: 'running', type: 'primary' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.waiting'), value: 'waiting', type: 'warning' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.completed'), value: 'completed', type: 'success' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.failed'), value: 'failed', type: 'danger' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.stopped'), value: 'stopped', type: 'info' as TagType },
|
||||
];
|
||||
}
|
||||
|
||||
export function getTriggerOptions() {
|
||||
return [
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.editor_draft'), value: 'editor_draft', type: 'warning' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.editor_published'), value: 'editor_published', type: 'primary' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.agent'), value: 'agent', type: 'success' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.api'), value: 'api', type: 'info' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.form_button'), value: 'form_button', type: 'info' as TagType },
|
||||
];
|
||||
}
|
||||
|
||||
export function getTagType(value: string, options: Array<{ type?: TagType; value: string }>): TagType {
|
||||
const option = options.find((item) => item.value === value);
|
||||
return option?.type || 'info';
|
||||
}
|
||||
|
||||
export function getTagLabel(value: string, options: Array<{ label: string; value: string }>): string {
|
||||
const option = options.find((item) => item.value === value);
|
||||
return option?.label || value;
|
||||
}
|
||||
|
||||
export function formatDuration(ms?: number) {
|
||||
if (!ms && ms !== 0) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
export function formatTime(value?: string) {
|
||||
if (!value) return '-';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'workflowId',
|
||||
label: $t('ai-platform.workflowRuns.filters.workflow'),
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const res = await getWorkflowListApi({ page: 1, pageSize: 200 });
|
||||
return (res.items || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allWorkflows'),
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: $t('ai-platform.workflowRuns.filters.status'),
|
||||
componentProps: {
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allStatus'),
|
||||
options: getStatusOptions(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'triggerType',
|
||||
label: $t('ai-platform.workflowRuns.filters.trigger'),
|
||||
componentProps: {
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allTriggers'),
|
||||
options: getTriggerOptions(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useZqTableColumns(): Column[] {
|
||||
return [
|
||||
{
|
||||
key: 'workflow_name',
|
||||
dataKey: 'workflow_name',
|
||||
title: $t('ai-platform.workflowRuns.columns.workflow'),
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: $t('ai-platform.workflowRuns.columns.status'),
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-status' },
|
||||
},
|
||||
{
|
||||
key: 'trigger_type',
|
||||
title: $t('ai-platform.workflowRuns.columns.trigger'),
|
||||
width: 140,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-trigger_type' },
|
||||
},
|
||||
{
|
||||
key: 'total_steps',
|
||||
dataKey: 'total_steps',
|
||||
title: $t('ai-platform.workflowRuns.columns.steps'),
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
key: 'total_tokens',
|
||||
dataKey: 'total_tokens',
|
||||
title: $t('ai-platform.workflowRuns.columns.tokens'),
|
||||
width: 90,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
key: 'elapsed_time',
|
||||
title: $t('ai-platform.workflowRuns.columns.duration'),
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-elapsed_time' },
|
||||
},
|
||||
{
|
||||
key: 'started_at',
|
||||
title: $t('ai-platform.workflowRuns.columns.startedAt'),
|
||||
width: 180,
|
||||
slots: { default: 'cell-started_at' },
|
||||
},
|
||||
{
|
||||
key: 'error_message',
|
||||
dataKey: 'error_message',
|
||||
title: $t('ai-platform.workflowRuns.columns.error'),
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: $t('ai-platform.workflowRuns.columns.actions'),
|
||||
width: 120,
|
||||
fixed: true,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import type { WorkflowRunListItem } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Eye } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
|
||||
import { getAllWorkflowRunsApi } from '#/api/ai-platform/ai-platform';
|
||||
import { useZqTable } from '#/components/zq-table';
|
||||
|
||||
import RunStatusTag from './components/RunStatusTag.vue';
|
||||
import {
|
||||
formatDuration,
|
||||
formatTime,
|
||||
getTagLabel,
|
||||
getTagType,
|
||||
getTriggerOptions,
|
||||
useSearchFormSchema,
|
||||
useZqTableColumns,
|
||||
} from './data';
|
||||
import DetailDialog from './modules/detail-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'WorkflowRunHistory' });
|
||||
|
||||
const detailRef = ref<InstanceType<typeof DetailDialog>>();
|
||||
const triggerOptions = getTriggerOptions();
|
||||
|
||||
const fetchRunList = async (params: any) => {
|
||||
const res = await getAllWorkflowRunsApi({
|
||||
page: params.page.currentPage,
|
||||
pageSize: params.page.pageSize,
|
||||
workflowId: params.form?.workflowId || undefined,
|
||||
status: params.form?.status || undefined,
|
||||
triggerType: params.form?.triggerType || undefined,
|
||||
});
|
||||
return {
|
||||
items: res.items,
|
||||
total: res.total,
|
||||
};
|
||||
};
|
||||
|
||||
const [Grid] = useZqTable({
|
||||
gridOptions: {
|
||||
columns: useZqTableColumns(),
|
||||
border: true,
|
||||
stripe: true,
|
||||
showIndex: true,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
ajax: {
|
||||
query: fetchRunList,
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
},
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
custom: true,
|
||||
},
|
||||
},
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: true,
|
||||
submitOnChange: true,
|
||||
},
|
||||
});
|
||||
|
||||
function openDetail(row: WorkflowRunListItem) {
|
||||
detailRef.value?.open(row.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<DetailDialog ref="detailRef" />
|
||||
|
||||
<Grid>
|
||||
<template #cell-status="{ row }">
|
||||
<RunStatusTag :status="row.status" />
|
||||
</template>
|
||||
|
||||
<template #cell-trigger_type="{ row }">
|
||||
<ElTag
|
||||
:type="getTagType(row.trigger_type, triggerOptions)"
|
||||
size="small"
|
||||
>
|
||||
{{ getTagLabel(row.trigger_type, triggerOptions) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
|
||||
<template #cell-elapsed_time="{ row }">
|
||||
{{ formatDuration(row.elapsed_time) }}
|
||||
</template>
|
||||
|
||||
<template #cell-started_at="{ row }">
|
||||
{{ formatTime(row.started_at) }}
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<ElButton link type="primary" :icon="Eye" @click.stop="openDetail(row)">
|
||||
{{ $t('common.view') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Edge, Node } from '@vue-flow/core';
|
||||
|
||||
import type { WorkflowRun } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ExternalLink } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElAlert, ElButton, ElTag } from 'element-plus';
|
||||
|
||||
import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform';
|
||||
import { ZqDesc, ZqDescItem } from '#/components/zq-desc';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
import { definitionToFlowElements } from '../../workflow/shared/loadDefinition';
|
||||
import { applyRunReplay } from '../../workflow/shared/useRunReplay';
|
||||
import RunStatusTag from '../components/RunStatusTag.vue';
|
||||
import WorkflowReadonlyCanvas from '../components/WorkflowReadonlyCanvas.vue';
|
||||
import {
|
||||
formatDuration,
|
||||
formatTime,
|
||||
getTagLabel,
|
||||
getTagType,
|
||||
getTriggerOptions,
|
||||
} from '../data';
|
||||
|
||||
const router = useRouter();
|
||||
const appContextStore = useAppContextStore();
|
||||
const triggerOptions = getTriggerOptions();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const runId = ref('');
|
||||
const run = ref<WorkflowRun | null>(null);
|
||||
const nodes = ref<Node[]>([]);
|
||||
const edges = ref<Edge[]>([]);
|
||||
const activeNodeId = ref('');
|
||||
const definitionFallback = ref(false);
|
||||
|
||||
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
||||
|
||||
async function loadDetail() {
|
||||
if (!runId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
definitionFallback.value = false;
|
||||
activeNodeId.value = '';
|
||||
run.value = null;
|
||||
nodes.value = [];
|
||||
edges.value = [];
|
||||
|
||||
try {
|
||||
const detail = await getWorkflowRunDetailApi(runId.value);
|
||||
run.value = detail;
|
||||
|
||||
let definition = detail.definition_snapshot;
|
||||
if (!definition?.nodes?.length) {
|
||||
definitionFallback.value = true;
|
||||
try {
|
||||
const workflow = await getWorkflowDetailApi(detail.workflow_id);
|
||||
definition = detail.use_draft
|
||||
? workflow.definition
|
||||
: workflow.published_definition || workflow.definition;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
definition = { nodes: [], edges: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const base = definitionToFlowElements(definition || { nodes: [], edges: [] });
|
||||
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
||||
nodes.value = replayed.nodes;
|
||||
edges.value = replayed.edges;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function open(id: string) {
|
||||
runId.value = id;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
loadDetail();
|
||||
}
|
||||
|
||||
function openEditor() {
|
||||
if (!run.value?.workflow_id) return;
|
||||
visible.value = false;
|
||||
router.push(
|
||||
appContextStore.getContextPath(
|
||||
`/ai-platform/workflow/editor/${run.value.workflow_id}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function triggerLabel(value?: string) {
|
||||
if (!value) return '-';
|
||||
return getTagLabel(value, triggerOptions);
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="visible"
|
||||
class="workflow-run-detail-dialog"
|
||||
:title="dialogTitle"
|
||||
:loading="loading"
|
||||
default-fullscreen
|
||||
:draggable="false"
|
||||
:show-footer="false"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2">
|
||||
{{ dialogTitle }}
|
||||
<RunStatusTag v-if="run" :status="run.status" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<ElButton
|
||||
v-if="run?.workflow_id"
|
||||
:icon="ExternalLink"
|
||||
@click="openEditor"
|
||||
>
|
||||
{{ $t('ai-platform.workflowRuns.detail.openEditor') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
|
||||
<div v-if="run" class="run-detail-layout flex h-full min-h-0 flex-col gap-4">
|
||||
<ElAlert
|
||||
v-if="definitionFallback"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="$t('ai-platform.workflowRuns.detail.snapshotMissing')"
|
||||
/>
|
||||
|
||||
<ZqDesc :column="4">
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.runId')">
|
||||
{{ runId }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.trigger')">
|
||||
<ElTag
|
||||
:type="getTagType(run.trigger_type, triggerOptions)"
|
||||
size="small"
|
||||
>
|
||||
{{ triggerLabel(run.trigger_type) }}
|
||||
</ElTag>
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.steps')">
|
||||
{{ run.total_steps }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.tokens')">
|
||||
{{ run.total_tokens }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.duration')">
|
||||
{{ formatDuration(run.elapsed_time) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.startedAt')">
|
||||
{{ formatTime(run.started_at) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.completedAt')">
|
||||
{{ formatTime(run.completed_at) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.version')">
|
||||
{{
|
||||
run.use_draft
|
||||
? $t('ai-platform.workflowRuns.detail.draft')
|
||||
: run.workflow_version || '-'
|
||||
}}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem
|
||||
v-if="run.error_message"
|
||||
:label="$t('ai-platform.workflowRuns.columns.error')"
|
||||
:span="4"
|
||||
>
|
||||
<span class="text-destructive">{{ run.error_message }}</span>
|
||||
</ZqDescItem>
|
||||
</ZqDesc>
|
||||
|
||||
<div class="run-detail-canvas border-border overflow-hidden rounded-lg border">
|
||||
<WorkflowReadonlyCanvas
|
||||
:nodes="nodes"
|
||||
:edges="edges"
|
||||
:fit-node-id="activeNodeId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.workflow-run-detail-dialog.is-fullscreen .el-dialog__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .zq-dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__wrap,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__view,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__view > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .run-detail-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .run-detail-canvas {
|
||||
flex: 1;
|
||||
min-height: 320px;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,900 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkflowStreamEvent } from '#/api/ai-platform/ai-platform';
|
||||
import type { ChatMessage, ReasoningStep } from '#/components/ChatBox/index';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { Settings2, X } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
resumeWorkflowStreamApi,
|
||||
runWorkflowStreamApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
import { ChatBox } from '#/components/ChatBox/index';
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: any[];
|
||||
workflowId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'node-start',
|
||||
'node-complete',
|
||||
'node-streaming',
|
||||
'loop-iteration',
|
||||
'clear-status',
|
||||
]);
|
||||
|
||||
// 状态
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const running = ref(false);
|
||||
const streamingContent = ref('');
|
||||
let cancelStream: (() => void) | null = null;
|
||||
|
||||
// 多变量输入对话框
|
||||
const variablesDialogVisible = ref(false);
|
||||
const variablesForm = ref<Record<string, any>>({});
|
||||
const pendingMessage = ref('');
|
||||
|
||||
// 对话流交互状态
|
||||
const waitingForInput = ref(false);
|
||||
const waitingConfig = ref<any>(null);
|
||||
const currentRunId = ref('');
|
||||
|
||||
// 设计预览编辑面板状态
|
||||
// 应用设计面板状态
|
||||
// 应用设置面板状态
|
||||
// 仪表盘基础信息面板状态
|
||||
// 仪表盘设计面板状态
|
||||
// 仪表盘发布面板状态
|
||||
// 系统总结面板状态
|
||||
// 查找 Start 节点并提取变量
|
||||
const startNode = computed(() => props.nodes.find((n) => n.type === 'start'));
|
||||
const variables = computed(() => startNode.value?.data?.variables || []);
|
||||
const hasMultipleVariables = computed(() => variables.value.length > 1);
|
||||
|
||||
// 初始化变量表单
|
||||
watch(
|
||||
variables,
|
||||
(vars) => {
|
||||
const newForm: Record<string, any> = {};
|
||||
vars.forEach((v: any) => {
|
||||
newForm[v.variable] =
|
||||
variablesForm.value[v.variable] || v.default_value || '';
|
||||
});
|
||||
variablesForm.value = newForm;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 生成消息 ID
|
||||
const generateId = () =>
|
||||
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// 发送消息
|
||||
const handleSend = (text: string) => {
|
||||
if (!text || running.value) return;
|
||||
|
||||
// 如果工作流正在等待用户输入,调用恢复而不是重新运行
|
||||
if (waitingForInput.value && currentRunId.value) {
|
||||
// 移除当前消息的交互配置
|
||||
const lastAssistantMsg = messages.value.findLast(
|
||||
(m) => m.role === 'assistant',
|
||||
);
|
||||
if (lastAssistantMsg?.interaction) {
|
||||
const msgIndex = messages.value.findIndex(
|
||||
(m) => m.id === lastAssistantMsg.id,
|
||||
);
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex] = {
|
||||
...lastAssistantMsg,
|
||||
interaction: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
const userMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.value.push(userMsg);
|
||||
|
||||
// 重置等待状态
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
|
||||
// 恢复工作流
|
||||
resumeWorkflow(text);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有多个变量,先弹出表单
|
||||
if (hasMultipleVariables.value) {
|
||||
pendingMessage.value = text;
|
||||
// 将用户输入填入第一个变量(通常是 user_input)
|
||||
const firstVar = variables.value[0];
|
||||
if (firstVar) {
|
||||
variablesForm.value[firstVar.variable] = text;
|
||||
}
|
||||
variablesDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 单变量或无变量,直接运行
|
||||
runWorkflow(text, { user_input: text });
|
||||
};
|
||||
|
||||
// 确认变量表单后运行
|
||||
const handleVariablesConfirm = () => {
|
||||
variablesDialogVisible.value = false;
|
||||
runWorkflow(pendingMessage.value, variablesForm.value);
|
||||
};
|
||||
|
||||
// 当前正在处理的消息 ID
|
||||
const currentAssistantMsgId = ref<null | string>(null);
|
||||
|
||||
// 当前执行步骤
|
||||
const currentSteps = ref<ReasoningStep[]>([]);
|
||||
|
||||
// 获取节点显示名称
|
||||
const getNodeLabel = (
|
||||
nodeId?: string,
|
||||
nodeType?: string,
|
||||
fallbackLabel?: string,
|
||||
) => {
|
||||
if (!nodeId) return fallbackLabel || nodeType || '';
|
||||
|
||||
// 优先从前端 nodes 中获取配置的 label
|
||||
const node = props.nodes.find((n) => n.id === nodeId);
|
||||
const label = node?.data?.label;
|
||||
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
// 如果前端没有找到,使用后端传来的 fallbackLabel
|
||||
if (fallbackLabel) {
|
||||
return fallbackLabel;
|
||||
}
|
||||
|
||||
// 最后使用 nodeType
|
||||
return nodeType || '';
|
||||
};
|
||||
|
||||
// 运行工作流
|
||||
const runWorkflow = (userMessage: string, inputs: Record<string, any>) => {
|
||||
if (!props.workflowId) return;
|
||||
|
||||
// 添加用户消息
|
||||
const userMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.value.push(userMsg);
|
||||
|
||||
// 添加 AI 消息(pending 状态)
|
||||
const assistantMsgId = generateId();
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: assistantMsgId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
status: 'pending',
|
||||
reasoning_steps: [],
|
||||
};
|
||||
messages.value.push(assistantMsg);
|
||||
currentAssistantMsgId.value = assistantMsgId;
|
||||
currentSteps.value = [];
|
||||
streamingContent.value = '';
|
||||
|
||||
running.value = true;
|
||||
|
||||
// 流式运行(编辑器调试模式,使用草稿版本)
|
||||
// 系统变量(application_id 等)由 API 层自动注入
|
||||
cancelStream = runWorkflowStreamApi(
|
||||
props.workflowId,
|
||||
inputs,
|
||||
(event: WorkflowStreamEvent) => {
|
||||
handleStreamEvent(event, assistantMsgId);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Stream error:', error);
|
||||
updateAssistantMessage(assistantMsgId, {
|
||||
status: 'failed',
|
||||
error_message:
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.execFailed'),
|
||||
});
|
||||
running.value = false;
|
||||
ElMessage.error(
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.workflowExecFailed'),
|
||||
);
|
||||
},
|
||||
() => {
|
||||
cancelStream = null;
|
||||
},
|
||||
true, // useDraft: 编辑器调试使用草稿版本
|
||||
);
|
||||
};
|
||||
|
||||
// 更新助手消息
|
||||
const updateAssistantMessage = (
|
||||
msgId: string,
|
||||
updates: Partial<ChatMessage>,
|
||||
) => {
|
||||
const index = messages.value.findIndex((m) => m.id === msgId);
|
||||
if (index !== -1) {
|
||||
const currentMsg = messages.value[index];
|
||||
if (currentMsg) {
|
||||
messages.value[index] = { ...currentMsg, ...updates } as ChatMessage;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理流式事件
|
||||
const handleStreamEvent = (event: WorkflowStreamEvent, msgId: string) => {
|
||||
switch (event.type) {
|
||||
case 'answer': {
|
||||
// 消息内容 - 检查是否有空的初始消息可以更新
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
// 更新初始消息(typing indicator -> 实际内容)
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: event.content || '',
|
||||
});
|
||||
} else {
|
||||
// 已有内容,创建新消息
|
||||
const answerMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: event.content || '',
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(answerMsg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'complete': {
|
||||
// 从 outputs 中提取结束节点的输出内容
|
||||
// 只检查 output 字段(结束节点配置的输出内容)
|
||||
let endOutput = '';
|
||||
if (
|
||||
event.outputs &&
|
||||
typeof event.outputs.output === 'string' &&
|
||||
event.outputs.output.trim()
|
||||
) {
|
||||
endOutput = event.outputs.output;
|
||||
}
|
||||
|
||||
// 查找初始消息(用于显示执行状态的消息)
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
const initialMsgHasContent = initialMsg?.content?.trim();
|
||||
|
||||
// 只有当结束节点有明确的输出内容时才显示
|
||||
if (endOutput) {
|
||||
// 检查初始消息是否还是 typing indicator(没有内容)
|
||||
if (initialMsg && !initialMsgHasContent) {
|
||||
// 更新初始消息为结束节点输出
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: endOutput,
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.total_tokens,
|
||||
});
|
||||
} else {
|
||||
// 已有内容,追加新消息
|
||||
const endMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: endOutput,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.total_tokens,
|
||||
};
|
||||
messages.value.push(endMsg);
|
||||
}
|
||||
} else if (!initialMsgHasContent) {
|
||||
// 没有结束节点输出,且初始消息还是 typing indicator
|
||||
// 检查是否有其他助手消息(由 answer 或 node_event 创建)
|
||||
const otherAssistantMessages = messages.value.filter(
|
||||
(m) => m.role === 'assistant' && m.id !== msgId && m.content?.trim(),
|
||||
);
|
||||
if (otherAssistantMessages.length > 0) {
|
||||
// 有其他消息,移除空的初始消息
|
||||
const initialMsgIndex = messages.value.findIndex(
|
||||
(m) => m.id === msgId,
|
||||
);
|
||||
if (initialMsgIndex !== -1) {
|
||||
messages.value.splice(initialMsgIndex, 1);
|
||||
}
|
||||
} else {
|
||||
// 没有任何消息,显示一个完成提示
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.total_tokens,
|
||||
content: $t(
|
||||
'ai-platform.workflow.editor.chatPanel.workflowComplete',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
// 如果初始消息已有内容(由 answer/node_event 更新),不需要额外处理
|
||||
|
||||
running.value = false;
|
||||
streamingContent.value = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'failed',
|
||||
error_message:
|
||||
event.message ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.execFailed'),
|
||||
});
|
||||
running.value = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'llm_chunk': {
|
||||
// LLM 流式输出
|
||||
emit('node-streaming', {
|
||||
node_id: event.node_id,
|
||||
content: event.content,
|
||||
accumulated_content: event.accumulated_content,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_complete': {
|
||||
// 循环执行完成
|
||||
const loopEvent = event as any;
|
||||
const outputVar = loopEvent.output_variable || 'loop_results';
|
||||
const outputs: Record<string, any> = {
|
||||
total_iterations: loopEvent.total_iterations,
|
||||
results_count: loopEvent.results_count,
|
||||
};
|
||||
// 将 loop_results 添加到输出中
|
||||
if (loopEvent.loop_results) {
|
||||
outputs[outputVar] = loopEvent.loop_results;
|
||||
}
|
||||
|
||||
emit('node-complete', {
|
||||
node_id: loopEvent.node_id,
|
||||
node_type: 'loop',
|
||||
status: 'success',
|
||||
elapsed_time: 0,
|
||||
tokens_used: 0,
|
||||
outputs,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_complete': {
|
||||
// 循环迭代完成
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'complete',
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
output: event.output,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_error': {
|
||||
// 循环迭代错误
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'error',
|
||||
iteration: event.iteration,
|
||||
error: event.error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_start': {
|
||||
// 循环迭代开始
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'start',
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
item: event.item,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_complete': {
|
||||
// 查找已存在的 node_start 步骤并更新
|
||||
const existingIndex = currentSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id && s.type === 'node_start',
|
||||
);
|
||||
const nodeLabel = getNodeLabel(
|
||||
event.node_id,
|
||||
event.node_type,
|
||||
event.node_label,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
// 如果没有找到对应的 start,添加新的 complete 步骤
|
||||
currentSteps.value.push({
|
||||
type: 'node_complete',
|
||||
content: nodeLabel,
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// 更新已存在的步骤
|
||||
currentSteps.value[existingIndex] = {
|
||||
...currentSteps.value[existingIndex],
|
||||
type: 'node_complete',
|
||||
content: nodeLabel,
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
};
|
||||
}
|
||||
updateAssistantMessage(msgId, {
|
||||
reasoning_steps: [...currentSteps.value],
|
||||
});
|
||||
|
||||
emit('node-complete', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: event.status,
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used,
|
||||
error_message: event.error_message,
|
||||
outputs: event.outputs,
|
||||
inputs: event.inputs,
|
||||
loop_iteration: event.loop_iteration,
|
||||
warnings: event.warnings,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_event': {
|
||||
// 节点事件(如消息节点发送消息)
|
||||
const nodeEvent = event.event;
|
||||
if (nodeEvent?.type === 'message') {
|
||||
// 检查是否有空的初始消息可以更新
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
// 更新初始消息(typing indicator -> 实际内容)
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: nodeEvent.content || '',
|
||||
});
|
||||
} else {
|
||||
// 已有内容,创建新消息
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: nodeEvent.content || '',
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_start': {
|
||||
// 检查是否已存在相同 node_id 的步骤(避免重复)
|
||||
const existingIndex = currentSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
// 添加新的执行步骤
|
||||
const nodeLabel = getNodeLabel(
|
||||
event.node_id,
|
||||
event.node_type,
|
||||
event.node_label,
|
||||
);
|
||||
currentSteps.value.push({
|
||||
type: 'node_start',
|
||||
content: nodeLabel,
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
updateAssistantMessage(msgId, {
|
||||
reasoning_steps: [...currentSteps.value],
|
||||
});
|
||||
|
||||
emit('node-start', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
node_label: event.node_label,
|
||||
inputs: event.inputs,
|
||||
loop_iteration: event.loop_iteration,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start': {
|
||||
currentRunId.value = event.run_id || '';
|
||||
// 保持 pending 状态以显示 typing indicator(三个点)
|
||||
// 只有当有实际内容时才更新状态
|
||||
break;
|
||||
}
|
||||
|
||||
case 'waiting_input': {
|
||||
waitingForInput.value = true;
|
||||
waitingConfig.value = event.waiting_config || event.config;
|
||||
|
||||
const configData = event.waiting_config || (event as any).config;
|
||||
|
||||
if (configData?.type === 'design_preview') {
|
||||
const waitingContent = `**${configData.title}**\n\n${configData.message || $t('ai-platform.workflow.editor.chatPanel.designPreviewHint')}\n\n????????/??/??????????`;
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: waitingContent,
|
||||
});
|
||||
} else {
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: waitingContent,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
} else {
|
||||
const waitingContent = getWaitingPrompt(configData);
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: waitingContent,
|
||||
interaction: configData as any,
|
||||
});
|
||||
} else {
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: waitingContent,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
interaction: configData as any,
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取等待提示文本
|
||||
const getWaitingPrompt = (config: any) => {
|
||||
if (!config) return $t('ai-platform.workflow.editor.chatPanel.waitingInput');
|
||||
switch (config.type) {
|
||||
case 'choice': {
|
||||
return (
|
||||
config.question ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.pleaseSelect')
|
||||
);
|
||||
}
|
||||
case 'confirm': {
|
||||
return `**${config.title || $t('ai-platform.workflow.editor.chatPanel.confirm')}**\n\n${config.content || $t('ai-platform.workflow.editor.chatPanel.pleaseConfirm')}`;
|
||||
}
|
||||
case 'question': {
|
||||
return (
|
||||
config.question ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.pleaseInput')
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return $t('ai-platform.workflow.editor.chatPanel.waitingInput');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理交互提交(来自 ChatBox)
|
||||
const handleInteractionSubmit = (messageId: string, value: any) => {
|
||||
// 找到消息并移除交互配置
|
||||
const msgIndex = messages.value.findIndex((m) => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
const msg = messages.value[msgIndex];
|
||||
if (msg) {
|
||||
messages.value[msgIndex] = { ...msg, interaction: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户回复消息
|
||||
const userMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content: formatInteractionValue(value),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.value.push(userMsg);
|
||||
|
||||
// 调用后端 API 继续工作流执行
|
||||
if (currentRunId.value) {
|
||||
resumeWorkflow(value);
|
||||
}
|
||||
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
};
|
||||
|
||||
// 处理交互取消(来自 ChatBox)
|
||||
const handleInteractionCancel = (messageId: string) => {
|
||||
// 找到消息并移除交互配置
|
||||
const msgIndex = messages.value.findIndex((m) => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
const msg = messages.value[msgIndex];
|
||||
if (msg) {
|
||||
messages.value[msgIndex] = { ...msg, interaction: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户回复消息
|
||||
const userMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'user',
|
||||
content:
|
||||
waitingConfig.value?.cancel_text ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.cancel'),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.value.push(userMsg);
|
||||
|
||||
// 调用后端 API 继续工作流执行(传入 false)
|
||||
if (currentRunId.value) {
|
||||
resumeWorkflow(false);
|
||||
}
|
||||
|
||||
waitingForInput.value = false;
|
||||
waitingConfig.value = null;
|
||||
};
|
||||
|
||||
// 恢复工作流执行
|
||||
const resumeWorkflow = (userInput: any) => {
|
||||
if (!currentRunId.value) return;
|
||||
|
||||
// 添加 AI 消息(pending 状态)
|
||||
const assistantMsgId = generateId();
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: assistantMsgId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
status: 'pending',
|
||||
};
|
||||
messages.value.push(assistantMsg);
|
||||
currentAssistantMsgId.value = assistantMsgId;
|
||||
|
||||
running.value = true;
|
||||
|
||||
cancelStream = resumeWorkflowStreamApi(
|
||||
currentRunId.value,
|
||||
userInput,
|
||||
(event: WorkflowStreamEvent) => {
|
||||
handleStreamEvent(event, assistantMsgId);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Resume stream error:', error);
|
||||
updateAssistantMessage(assistantMsgId, {
|
||||
status: 'failed',
|
||||
error_message:
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.resumeFailed'),
|
||||
});
|
||||
running.value = false;
|
||||
ElMessage.error(
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.resumeWorkflowFailed'),
|
||||
);
|
||||
},
|
||||
() => {
|
||||
cancelStream = null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 格式化交互值显示
|
||||
const formatInteractionValue = (value: any) => {
|
||||
if (value === true) {
|
||||
return (
|
||||
waitingConfig.value?.confirm_text ||
|
||||
$t('ai-platform.workflow.editor.chatPanel.confirm')
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const options = waitingConfig.value?.options || [];
|
||||
const labels = value.map((v: string) => {
|
||||
const opt = options.find((o: any) => o.value === v);
|
||||
return opt?.label || v;
|
||||
});
|
||||
return labels.join(', ');
|
||||
}
|
||||
if (waitingConfig.value?.type === 'choice') {
|
||||
const options = waitingConfig.value?.options || [];
|
||||
const opt = options.find((o: any) => o.value === value);
|
||||
return opt?.label || value;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 停止运行
|
||||
const handleStop = () => {
|
||||
cancelStream?.();
|
||||
cancelStream = null;
|
||||
running.value = false;
|
||||
|
||||
// 更新当前正在处理的 AI 消息状态
|
||||
if (currentAssistantMsgId.value) {
|
||||
updateAssistantMessage(currentAssistantMsgId.value, {
|
||||
status: 'failed',
|
||||
error_message: $t('ai-platform.workflow.editor.chatPanel.userStopped'),
|
||||
});
|
||||
currentAssistantMsgId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 清空对话
|
||||
const handleClear = () => {
|
||||
messages.value = [];
|
||||
streamingContent.value = '';
|
||||
emit('clear-status');
|
||||
};
|
||||
|
||||
// 处理添加消息(来自 ChatBox 内部,如语音消息)
|
||||
const handleAddMessage = (message: ChatMessage) => {
|
||||
messages.value.push(message);
|
||||
};
|
||||
|
||||
// 处理更新消息(来自 ChatBox 内部,如语音识别完成)
|
||||
const handleUpdateMessage = (
|
||||
messageId: string,
|
||||
updates: Partial<ChatMessage>,
|
||||
) => {
|
||||
const idx = messages.value.findIndex((m) => m.id === messageId);
|
||||
if (idx !== -1) {
|
||||
messages.value[idx] = { ...messages.value[idx]!, ...updates };
|
||||
}
|
||||
};
|
||||
|
||||
// 处理设计预览确认
|
||||
onUnmounted(() => {
|
||||
cancelStream?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<!-- 聊天面板 -->
|
||||
<div
|
||||
class="border-border bg-card flex h-full w-96 flex-col border-l shadow-xl"
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<div
|
||||
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||
>
|
||||
<div class="text-foreground flex items-center gap-2 font-medium">
|
||||
{{ $t('ai-platform.workflow.editor.chatPanel.runPreview') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- 变量设置按钮(多变量时显示) -->
|
||||
<ElButton
|
||||
v-if="hasMultipleVariables"
|
||||
link
|
||||
:icon="Settings2"
|
||||
:title="$t('ai-platform.workflow.editor.chatPanel.configVariables')"
|
||||
@click="variablesDialogVisible = true"
|
||||
/>
|
||||
<ElButton link :icon="X" @click="$emit('close')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ChatBox 组件 -->
|
||||
<ChatBox
|
||||
ref="chatBoxRef"
|
||||
:messages="messages"
|
||||
:loading="running"
|
||||
:streaming-content="streamingContent"
|
||||
:show-avatar="false"
|
||||
:assistant-name="
|
||||
$t('ai-platform.workflow.editor.chatPanel.assistantName')
|
||||
"
|
||||
:placeholder="$t('ai-platform.workflow.editor.chatPanel.placeholder')"
|
||||
:enable-attachment="false"
|
||||
:enable-image="false"
|
||||
:enable-voice="true"
|
||||
:show-clear-button="true"
|
||||
:empty-text="$t('ai-platform.workflow.editor.chatPanel.emptyText')"
|
||||
class="flex-1 rounded-none border-0"
|
||||
@send="handleSend"
|
||||
@stop="handleStop"
|
||||
@clear="handleClear"
|
||||
@interaction-submit="handleInteractionSubmit"
|
||||
@interaction-cancel="handleInteractionCancel"
|
||||
@add-message="handleAddMessage"
|
||||
@update-message="handleUpdateMessage"
|
||||
/>
|
||||
|
||||
<!-- 多变量输入对话框 -->
|
||||
<ElDialog
|
||||
v-model="variablesDialogVisible"
|
||||
:title="$t('ai-platform.workflow.editor.chatPanel.configVariables')"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem
|
||||
v-for="v in variables"
|
||||
:key="v.variable"
|
||||
:label="v.variable"
|
||||
:required="v.required"
|
||||
>
|
||||
<ElInput
|
||||
v-model="variablesForm[v.variable]"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.chatPanel.inputVariable', {
|
||||
variable: v.variable,
|
||||
})
|
||||
"
|
||||
:type="v.type === 'text' ? 'textarea' : 'text'"
|
||||
:rows="v.type === 'text' ? 3 : undefined"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="variablesDialogVisible = false">
|
||||
{{ $t('ai-platform.workflow.editor.chatPanel.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleVariablesConfirm">
|
||||
{{ $t('ai-platform.workflow.editor.chatPanel.startRun') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
|
||||
<!-- 设计编辑面板 -->
|
||||
|
||||
<!-- 应用设计面板 -->
|
||||
|
||||
<!-- 应用设置面板 -->
|
||||
|
||||
<!-- 仪表盘基础信息面板 -->
|
||||
|
||||
<!-- 仪表盘设计面板 -->
|
||||
|
||||
<!-- 仪表盘发布面板 -->
|
||||
|
||||
<!-- 系统总结面板 -->
|
||||
</div>
|
||||
</template>
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 确认模式选择组件
|
||||
* 支持三种模式:始终确认、从不确认、变量控制
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElRadioButton, ElRadioGroup } from 'element-plus';
|
||||
|
||||
import SmartInput from './SmartInput.vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean | string;
|
||||
currentNodeId: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean | string];
|
||||
}>();
|
||||
|
||||
// 确认模式
|
||||
type ConfirmMode = 'always' | 'never' | 'variable';
|
||||
|
||||
// 当前模式
|
||||
const mode = ref<ConfirmMode>('always');
|
||||
|
||||
// 变量值(当模式为 variable 时使用)
|
||||
const variableValue = ref('');
|
||||
|
||||
// 解析 modelValue 到 mode 和 variableValue
|
||||
const parseModelValue = (value: boolean | string) => {
|
||||
if (typeof value === 'boolean') {
|
||||
mode.value = value ? 'always' : 'never';
|
||||
variableValue.value = '';
|
||||
} else if (typeof value === 'string') {
|
||||
if (value === 'always' || value === 'true') {
|
||||
mode.value = 'always';
|
||||
variableValue.value = '';
|
||||
} else if (value === 'never' || value === 'false') {
|
||||
mode.value = 'never';
|
||||
variableValue.value = '';
|
||||
} else if (value.includes('{{')) {
|
||||
mode.value = 'variable';
|
||||
variableValue.value = value;
|
||||
} else {
|
||||
// 默认当作变量引用
|
||||
mode.value = 'variable';
|
||||
variableValue.value = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
parseModelValue(props.modelValue);
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
parseModelValue(newValue);
|
||||
},
|
||||
);
|
||||
|
||||
// 计算输出值
|
||||
const outputValue = computed(() => {
|
||||
switch (mode.value) {
|
||||
case 'always': {
|
||||
return true;
|
||||
}
|
||||
case 'never': {
|
||||
return false;
|
||||
}
|
||||
case 'variable': {
|
||||
return variableValue.value || true;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听内部值变化,更新 modelValue
|
||||
watch(
|
||||
[mode, variableValue],
|
||||
() => {
|
||||
emit('update:modelValue', outputValue.value);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="confirm-mode-select space-y-2">
|
||||
<ElRadioGroup v-model="mode" size="small">
|
||||
<ElRadioButton value="always">
|
||||
{{ $t('ai-platform.workflow.editor.confirmMode.always') }}
|
||||
</ElRadioButton>
|
||||
<ElRadioButton value="never">
|
||||
{{ $t('ai-platform.workflow.editor.confirmMode.never') }}
|
||||
</ElRadioButton>
|
||||
<ElRadioButton value="variable">
|
||||
{{ $t('ai-platform.workflow.editor.confirmMode.variable') }}
|
||||
</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
|
||||
<!-- 变量输入(仅在变量模式下显示) -->
|
||||
<div v-if="mode === 'variable'" class="mt-2">
|
||||
<SmartInput
|
||||
v-model="variableValue"
|
||||
:current-node-id="currentNodeId"
|
||||
:placeholder="$t('ai-platform.workflow.editor.confirmMode.placeholder')"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.editor.confirmMode.hint') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
import { Copy, Edit3, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'duplicate', 'delete', 'rename']);
|
||||
|
||||
// 点击外部关闭
|
||||
const handleClickOutside = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('click', handleClickOutside));
|
||||
onUnmounted(() => document.removeEventListener('click', handleClickOutside));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border-border bg-popover fixed z-50 w-36 rounded-lg border py-1 shadow-xl"
|
||||
:style="{ left: `${x}px`, top: `${y}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
class="text-popover-foreground hover:bg-accent flex cursor-pointer items-center gap-2 px-3 py-2 text-xs"
|
||||
@click="
|
||||
$emit('rename');
|
||||
$emit('close');
|
||||
"
|
||||
>
|
||||
<Edit3 class="size-3.5" />
|
||||
<span>{{ $t('ai-platform.workflow.editor.contextMenu.rename') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-popover-foreground hover:bg-accent flex cursor-pointer items-center gap-2 px-3 py-2 text-xs"
|
||||
@click="
|
||||
$emit('duplicate');
|
||||
$emit('close');
|
||||
"
|
||||
>
|
||||
<Copy class="size-3.5" />
|
||||
<span>{{ $t('ai-platform.workflow.editor.contextMenu.duplicate') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-border my-1 h-px"></div>
|
||||
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-xs text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950"
|
||||
@click="
|
||||
$emit('delete');
|
||||
$emit('close');
|
||||
"
|
||||
>
|
||||
<Trash2 class="size-3.5" />
|
||||
<span>{{ $t('ai-platform.workflow.editor.contextMenu.delete') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ElAlert } from 'element-plus';
|
||||
|
||||
export interface DbConfigInfo {
|
||||
database?: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
dbConfig?: DbConfigInfo | null;
|
||||
showDefaultWriteWarning?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="dbConfig" class="space-y-2">
|
||||
<div class="bg-muted rounded px-3 py-2">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<span class="text-foreground font-medium">{{ dbConfig.dbName }}</span>
|
||||
<span v-if="dbConfig.database"> · {{ dbConfig.database }}</span>
|
||||
<span v-if="dbConfig.schema">.{{ dbConfig.schema }}</span>
|
||||
<span class="ml-2">({{ dbConfig.dbType }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElAlert
|
||||
v-if="showDefaultWriteWarning && dbConfig.dbName === 'default'"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-xs">{{
|
||||
$t('ai-platform.workflow.editor.dbConnection.defaultWriteWarning')
|
||||
}}</span>
|
||||
</template>
|
||||
</ElAlert>
|
||||
</div>
|
||||
</template>
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElAlert, ElFormItem, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { getDatabaseConfigsApi } from '#/api/core/database-manager';
|
||||
|
||||
import type { DbConfigInfo } from './DbConfigSummary.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string;
|
||||
writeMode?: boolean;
|
||||
}>(),
|
||||
{
|
||||
modelValue: 'default',
|
||||
writeMode: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
change: [payload: Pick<DbConfigInfo, 'database' | 'dbName' | 'dbType'>];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const options = ref<
|
||||
Array<{
|
||||
db_name: string;
|
||||
db_type: string;
|
||||
display_name?: string;
|
||||
name: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
async function loadOptions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
options.value = await getDatabaseConfigsApi();
|
||||
} catch (error) {
|
||||
console.error('Failed to load database configs:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(value: string) {
|
||||
emit('update:modelValue', value);
|
||||
const selected = options.value.find((item) => item.db_name === value);
|
||||
emit('change', {
|
||||
dbName: value,
|
||||
dbType: selected?.db_type || 'postgresql',
|
||||
database: selected?.database || '',
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
emit('update:modelValue', 'default');
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.editor.dbConnection.connectionLabel')"
|
||||
>
|
||||
<ElSelect
|
||||
:model-value="modelValue"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
filterable
|
||||
@change="handleChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.db_name"
|
||||
:label="item.display_name || item.name"
|
||||
:value="item.db_name"
|
||||
>
|
||||
<span>{{ item.display_name || item.name }}</span>
|
||||
<span class="text-muted-foreground ml-2 text-xs">({{ item.db_type }})</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<ElAlert
|
||||
v-if="writeMode && modelValue === 'default'"
|
||||
class="mt-2"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-xs">{{
|
||||
$t('ai-platform.workflow.editor.dbConnection.defaultWriteWarning')
|
||||
}}</span>
|
||||
</template>
|
||||
</ElAlert>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElFormItem, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { getDatabasesApi } from '#/api/core/database-manager';
|
||||
|
||||
import SmartInput from './SmartInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
currentNodeId?: string;
|
||||
dbName?: string;
|
||||
dbType?: string;
|
||||
modelValue?: string;
|
||||
}>(),
|
||||
{
|
||||
currentNodeId: '',
|
||||
dbName: 'default',
|
||||
dbType: 'postgresql',
|
||||
modelValue: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const options = ref<Array<{ name: string }>>([]);
|
||||
|
||||
const supportsDatabaseCatalog = computed(() =>
|
||||
['postgresql', 'mysql', 'sqlserver'].includes(
|
||||
(props.dbType || 'postgresql').toLowerCase(),
|
||||
),
|
||||
);
|
||||
|
||||
async function loadOptions(dbName: string) {
|
||||
if (!dbName || !supportsDatabaseCatalog.value) {
|
||||
options.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
options.value = await getDatabasesApi(dbName);
|
||||
} catch (error) {
|
||||
console.error('Failed to load databases:', error);
|
||||
options.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectChange(value: string) {
|
||||
emit('update:modelValue', value || '');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.dbName, props.dbType] as const,
|
||||
([dbName]) => {
|
||||
loadOptions(dbName || 'default');
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.editor.dbConnection.databaseLabel')"
|
||||
>
|
||||
<template v-if="supportsDatabaseCatalog">
|
||||
<ElSelect
|
||||
:model-value="modelValue"
|
||||
:loading="loading"
|
||||
class="mb-2 w-full"
|
||||
clearable
|
||||
filterable
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.dbConnection.databasePlaceholder')
|
||||
"
|
||||
@change="handleSelectChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
<SmartInput
|
||||
:model-value="modelValue"
|
||||
:current-node-id="currentNodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.dbConnection.databaseInputPlaceholder')
|
||||
"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-else
|
||||
:model-value="modelValue"
|
||||
:current-node-id="currentNodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.dbConnection.databaseOraclePlaceholder')
|
||||
"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
supportsDatabaseCatalog
|
||||
? $t('ai-platform.workflow.editor.dbConnection.databaseHint')
|
||||
: $t('ai-platform.workflow.editor.dbConnection.databaseOracleHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { DbConfigInfo } from './DbConfigSummary.vue';
|
||||
|
||||
defineProps<{
|
||||
dbConfig?: DbConfigInfo | null;
|
||||
showDefaultBadge?: boolean;
|
||||
}>();
|
||||
|
||||
function formatTablePath(dbConfig: DbConfigInfo, table?: string) {
|
||||
const parts: string[] = [];
|
||||
if (dbConfig.database) parts.push(dbConfig.database);
|
||||
if (dbConfig.schema) parts.push(dbConfig.schema);
|
||||
if (table) parts.push(table);
|
||||
return parts.join('.') || table || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="dbConfig" class="space-y-1">
|
||||
<div class="text-muted-foreground flex items-center gap-1 text-[10px]">
|
||||
<span class="font-medium">{{ dbConfig.dbName }}</span>
|
||||
<span
|
||||
v-if="showDefaultBadge && dbConfig.dbName === 'default'"
|
||||
class="rounded bg-amber-100 px-1 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.dbConnection.defaultBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="dbConfig.database || dbConfig.schema"
|
||||
class="text-muted-foreground truncate font-mono text-[10px]"
|
||||
>
|
||||
{{ formatTablePath(dbConfig) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,479 @@
|
||||
<script lang="ts">
|
||||
// 迭代结果类型
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Loader2,
|
||||
Maximize,
|
||||
XCircle,
|
||||
Zap,
|
||||
} 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 {
|
||||
iteration?: number;
|
||||
status: 'failed' | 'success';
|
||||
elapsed_time?: number;
|
||||
tokens_used?: number;
|
||||
error_message?: string;
|
||||
output?: any;
|
||||
inputs?: any;
|
||||
}
|
||||
|
||||
export interface NodeExecutionResult {
|
||||
status: 'failed' | 'pending' | 'running' | 'success';
|
||||
streaming_content?: string;
|
||||
output?: any;
|
||||
elapsed_time?: number;
|
||||
tokens_used?: number;
|
||||
error_message?: string;
|
||||
inputs?: {
|
||||
previous_output?: any;
|
||||
variables?: Record<string, any>;
|
||||
};
|
||||
loop_iteration?: number;
|
||||
iterations?: IterationResult[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
nodeType?: string;
|
||||
result: NodeExecutionResult;
|
||||
}>();
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const expanded = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
|
||||
const hasIterations = computed(
|
||||
() => props.result.iterations && props.result.iterations.length > 0,
|
||||
);
|
||||
const selectedIteration = ref(0);
|
||||
|
||||
watch(
|
||||
() => props.result.iterations?.length,
|
||||
(newLen) => {
|
||||
if (newLen && newLen > 0) {
|
||||
selectedIteration.value = newLen - 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const displayResult = computed(() => {
|
||||
if (hasIterations.value && props.result.iterations) {
|
||||
const iter = props.result.iterations[selectedIteration.value];
|
||||
if (iter) {
|
||||
return {
|
||||
status: iter.status,
|
||||
output: iter.output,
|
||||
elapsed_time: iter.elapsed_time,
|
||||
tokens_used: iter.tokens_used,
|
||||
error_message: iter.error_message,
|
||||
inputs: iter.inputs,
|
||||
streaming_content: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
return props.result;
|
||||
});
|
||||
|
||||
const formattedOutput = computed(() => {
|
||||
if (displayResult.value.output === undefined || displayResult.value.output === null) {
|
||||
return '{}';
|
||||
}
|
||||
if (typeof displayResult.value.output === 'string') {
|
||||
return displayResult.value.output;
|
||||
}
|
||||
return JSON.stringify(displayResult.value.output, null, 2);
|
||||
});
|
||||
|
||||
const outputPreview = computed(() => {
|
||||
const output = formattedOutput.value;
|
||||
if (!output) return $t('ai-platform.workflow.editor.nodeResultCard.noOutput');
|
||||
if (output.length <= 100) return output;
|
||||
return `${output.slice(0, 100)}...`;
|
||||
});
|
||||
|
||||
const hasDetails = computed(() => {
|
||||
return formattedOutput.value.length > 100 || !!displayResult.value.inputs;
|
||||
});
|
||||
|
||||
const canOpenDetail = computed(() => {
|
||||
return (
|
||||
(displayResult.value.status === 'success' ||
|
||||
displayResult.value.status === 'failed') &&
|
||||
(displayResult.value.output !== undefined ||
|
||||
!!displayResult.value.inputs ||
|
||||
!!displayResult.value.error_message)
|
||||
);
|
||||
});
|
||||
|
||||
const statusConfig = computed(() => {
|
||||
const status = displayResult.value.status;
|
||||
switch (status) {
|
||||
case 'failed': {
|
||||
return {
|
||||
icon: XCircle,
|
||||
text: $t('ai-platform.workflow.editor.nodeResultCard.failed'),
|
||||
bgClass: 'bg-red-50 dark:bg-red-950',
|
||||
borderClass: 'border-red-300 dark:border-red-700',
|
||||
textClass: 'text-red-600 dark:text-red-400',
|
||||
iconClass: 'text-red-500 dark:text-red-400',
|
||||
};
|
||||
}
|
||||
case 'pending': {
|
||||
return {
|
||||
icon: Clock,
|
||||
text: $t('ai-platform.workflow.editor.nodeResultCard.pending'),
|
||||
bgClass: 'bg-muted',
|
||||
borderClass: 'border-border',
|
||||
textClass: 'text-muted-foreground',
|
||||
iconClass: 'text-muted-foreground',
|
||||
};
|
||||
}
|
||||
case 'running': {
|
||||
return {
|
||||
icon: Loader2,
|
||||
text: $t('ai-platform.workflow.editor.nodeResultCard.running'),
|
||||
bgClass: 'bg-blue-50 dark:bg-blue-950',
|
||||
borderClass: 'border-blue-300 dark:border-blue-700',
|
||||
textClass: 'text-blue-600 dark:text-blue-400',
|
||||
iconClass: 'text-blue-500 dark:text-blue-400 animate-spin',
|
||||
};
|
||||
}
|
||||
case 'success': {
|
||||
return {
|
||||
icon: CheckCircle2,
|
||||
text: $t('ai-platform.workflow.editor.nodeResultCard.success'),
|
||||
bgClass: 'bg-green-50 dark:bg-green-950',
|
||||
borderClass: 'border-green-300 dark:border-green-700',
|
||||
textClass: 'text-green-600 dark:text-green-400',
|
||||
iconClass: 'text-green-500 dark:text-green-400',
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
icon: Clock,
|
||||
text: $t('ai-platform.workflow.editor.nodeResultCard.unknown'),
|
||||
bgClass: 'bg-muted',
|
||||
borderClass: 'border-border',
|
||||
textClass: 'text-muted-foreground',
|
||||
iconClass: 'text-muted-foreground',
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function toggleExpand() {
|
||||
if (hasDetails.value) {
|
||||
expanded.value = !expanded.value;
|
||||
}
|
||||
}
|
||||
|
||||
function openDetailDialog() {
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
const formattedInputs = computed(() => {
|
||||
if (!displayResult.value.inputs) {
|
||||
return JSON.stringify({ variables: {}, previous_output: null }, null, 2);
|
||||
}
|
||||
return JSON.stringify(displayResult.value.inputs, null, 2);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nodrag nopan w-[280px] max-w-[280px]" v-bind="$attrs">
|
||||
<ZqDialog
|
||||
v-model="detailVisible"
|
||||
width="90%"
|
||||
:show-footer="false"
|
||||
:draggable="false"
|
||||
:content-height="'calc(100vh - 200px)'"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<component
|
||||
:is="statusConfig.icon"
|
||||
class="size-4"
|
||||
:class="statusConfig.iconClass"
|
||||
/>
|
||||
<span class="text-sm font-medium" :class="statusConfig.textClass">
|
||||
{{ statusConfig.text }}
|
||||
</span>
|
||||
<span
|
||||
v-if="displayResult.elapsed_time !== undefined"
|
||||
class="text-muted-foreground text-sm"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.elapsed') }}
|
||||
{{ displayResult.elapsed_time }}ms
|
||||
</span>
|
||||
<span
|
||||
v-if="displayResult.tokens_used"
|
||||
class="text-muted-foreground text-sm"
|
||||
>
|
||||
消耗 Token:{{ displayResult.tokens_used }}
|
||||
</span>
|
||||
<div v-if="hasIterations" class="flex items-center gap-1.5">
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.iteration') }}
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="(iter, idx) in result.iterations"
|
||||
:key="idx"
|
||||
type="button"
|
||||
class="flex size-6 items-center justify-center rounded text-xs font-medium transition-colors"
|
||||
:class="[
|
||||
selectedIteration === idx
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-accent text-muted-foreground',
|
||||
iter.status === 'failed' ? 'ring-1 ring-red-500' : '',
|
||||
]"
|
||||
@click.stop="selectedIteration = idx"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid h-full min-h-[520px] grid-cols-2 gap-4">
|
||||
<div class="flex min-h-0 flex-col overflow-hidden">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-0 flex-col overflow-hidden">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayResult.status === 'failed' && displayResult.error_message"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="mb-2 text-sm font-medium text-red-600 dark:text-red-400">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.errorInfo') }}
|
||||
</div>
|
||||
<div class="overflow-auto rounded border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950">
|
||||
<pre class="whitespace-pre-wrap break-all font-mono text-sm text-red-600 dark:text-red-400">{{
|
||||
displayResult.error_message
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
|
||||
<div
|
||||
class="nodrag nopan w-full overflow-hidden rounded-lg border transition-all duration-200"
|
||||
:class="[statusConfig.bgClass, statusConfig.borderClass]"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<div
|
||||
v-if="hasIterations"
|
||||
class="flex items-center gap-1 border-b px-3 py-1.5"
|
||||
:class="statusConfig.borderClass"
|
||||
>
|
||||
<span class="text-muted-foreground mr-1 text-[10px]">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.iteration') }}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="(iter, idx) in result.iterations"
|
||||
:key="idx"
|
||||
type="button"
|
||||
class="nodrag nopan flex size-5 items-center justify-center rounded text-[10px] font-medium transition-colors"
|
||||
:class="[
|
||||
selectedIteration === idx
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-accent text-muted-foreground',
|
||||
iter.status === 'failed' ? 'ring-1 ring-red-500' : '',
|
||||
]"
|
||||
@click.stop="selectedIteration = idx"
|
||||
@pointerdown.stop
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-3 py-2">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
:class="{ 'cursor-pointer hover:opacity-80': hasDetails }"
|
||||
@click.stop="toggleExpand"
|
||||
>
|
||||
<component
|
||||
:is="statusConfig.icon"
|
||||
class="size-4 shrink-0"
|
||||
:class="statusConfig.iconClass"
|
||||
/>
|
||||
<span class="truncate text-xs font-medium" :class="statusConfig.textClass">
|
||||
{{ statusConfig.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="nodrag nopan flex shrink-0 items-center gap-1.5">
|
||||
<div
|
||||
v-if="displayResult.elapsed_time !== undefined"
|
||||
class="text-muted-foreground flex items-center gap-1 text-xs"
|
||||
>
|
||||
<Clock class="size-3" />
|
||||
<span>{{ displayResult.elapsed_time }}ms</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayResult.tokens_used"
|
||||
class="text-muted-foreground flex items-center gap-1 text-xs"
|
||||
>
|
||||
<Zap class="size-3" />
|
||||
<span>{{ displayResult.tokens_used }}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="canOpenDetail"
|
||||
type="button"
|
||||
class="nodrag nopan hover:bg-accent flex size-6 shrink-0 items-center justify-center rounded"
|
||||
:title="$t('ai-platform.workflow.editor.nodeResultCard.fullscreen')"
|
||||
@click.stop="openDetailDialog"
|
||||
@pointerdown.stop
|
||||
@mousedown.stop
|
||||
>
|
||||
<Maximize class="text-muted-foreground size-3.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="hasDetails"
|
||||
type="button"
|
||||
class="nodrag nopan flex size-6 shrink-0 items-center justify-center rounded"
|
||||
@click.stop="toggleExpand"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<component
|
||||
:is="expanded ? ChevronUp : ChevronDown"
|
||||
class="text-muted-foreground size-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!expanded &&
|
||||
displayResult.status !== 'pending' &&
|
||||
displayResult.status !== 'running'
|
||||
"
|
||||
class="border-t px-3 py-2"
|
||||
:class="statusConfig.borderClass"
|
||||
>
|
||||
<div
|
||||
v-if="displayResult.status === 'failed'"
|
||||
class="text-xs text-red-600 dark:text-red-400"
|
||||
>
|
||||
{{
|
||||
displayResult.error_message ||
|
||||
$t('ai-platform.workflow.editor.nodeResultCard.execFailed')
|
||||
}}
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground overflow-hidden text-xs">
|
||||
<pre class="whitespace-pre-wrap break-all font-sans">{{
|
||||
outputPreview
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
displayResult.status === 'running' && displayResult.streaming_content
|
||||
"
|
||||
class="border-t px-3 py-2"
|
||||
:class="statusConfig.borderClass"
|
||||
>
|
||||
<div class="text-muted-foreground overflow-hidden text-xs">
|
||||
<pre class="whitespace-pre-wrap break-all font-sans">{{
|
||||
displayResult.streaming_content
|
||||
}}</pre>
|
||||
<span
|
||||
class="inline-block h-4 w-1 animate-pulse bg-blue-500 dark:bg-blue-400"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="expanded"
|
||||
class="space-y-3 border-t px-3 py-2"
|
||||
:class="statusConfig.borderClass"
|
||||
>
|
||||
<div v-if="displayResult.inputs">
|
||||
<div class="text-muted-foreground mb-1 text-xs font-medium">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.input') }}
|
||||
</div>
|
||||
<div class="bg-card/50 max-h-32 overflow-auto rounded p-2 text-xs">
|
||||
<pre class="text-muted-foreground whitespace-pre-wrap break-all font-mono">{{
|
||||
JSON.stringify(displayResult.inputs, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="displayResult.output !== undefined">
|
||||
<div class="text-muted-foreground mb-1 text-xs font-medium">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.output') }}
|
||||
</div>
|
||||
<div class="bg-card/50 max-h-48 overflow-auto rounded p-2 text-xs">
|
||||
<pre class="text-muted-foreground whitespace-pre-wrap break-all font-mono">{{
|
||||
formattedOutput
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayResult.status === 'failed' && displayResult.error_message"
|
||||
>
|
||||
<div class="mb-1 text-xs font-medium text-red-500 dark:text-red-400">
|
||||
{{ $t('ai-platform.workflow.editor.nodeResultCard.errorInfo') }}
|
||||
</div>
|
||||
<div
|
||||
class="rounded bg-red-100/50 p-2 text-xs text-red-600 dark:bg-red-900/50 dark:text-red-400"
|
||||
>
|
||||
{{ displayResult.error_message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
|
||||
import { X } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { ElButton, ElScrollbar } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
node: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'update:node']);
|
||||
|
||||
const { updateNodeData } = useVueFlow();
|
||||
|
||||
// 全屏状态
|
||||
const isFullscreen = ref(false);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
};
|
||||
|
||||
// 动态加载面板组件
|
||||
const components: Record<string, any> = {
|
||||
start: defineAsyncComponent(() => import('../panels/StartPanel.vue')),
|
||||
llm: defineAsyncComponent(() => import('../panels/LLMPanel.vue')),
|
||||
end: defineAsyncComponent(() => import('../panels/EndPanel.vue')),
|
||||
http: defineAsyncComponent(() => import('../panels/HttpPanel.vue')),
|
||||
code: defineAsyncComponent(() => import('../panels/CodePanel.vue')),
|
||||
condition: defineAsyncComponent(() => import('../panels/ConditionPanel.vue')),
|
||||
template: defineAsyncComponent(() => import('../panels/TemplatePanel.vue')),
|
||||
parallel: defineAsyncComponent(() => import('../panels/ParallelPanel.vue')),
|
||||
merge: defineAsyncComponent(() => import('../panels/MergePanel.vue')),
|
||||
db_insert: defineAsyncComponent(() => import('../panels/DbInsertPanel.vue')),
|
||||
db_update: defineAsyncComponent(() => import('../panels/DbUpdatePanel.vue')),
|
||||
db_query: defineAsyncComponent(() => import('../panels/DbQueryPanel.vue')),
|
||||
db_delete: defineAsyncComponent(() => import('../panels/DbDeletePanel.vue')),
|
||||
db_sql: defineAsyncComponent(() => import('../panels/DbSqlPanel.vue')),
|
||||
// 对话流节点
|
||||
question: defineAsyncComponent(() => import('../panels/QuestionPanel.vue')),
|
||||
choice: defineAsyncComponent(() => import('../panels/ChoicePanel.vue')),
|
||||
message: defineAsyncComponent(() => import('../panels/MessagePanel.vue')),
|
||||
confirm: defineAsyncComponent(() => import('../panels/ConfirmPanel.vue')),
|
||||
// 意图识别节点
|
||||
intent: defineAsyncComponent(() => import('../panels/IntentPanel.vue')),
|
||||
// Snowflake Cortex 节点
|
||||
snowflake_cortex_llm: defineAsyncComponent(
|
||||
() => import('../panels/SnowflakeCortexLLMPanel.vue'),
|
||||
),
|
||||
snowflake_cortex_analyst: defineAsyncComponent(
|
||||
() => import('../panels/SnowflakeCortexAnalystPanel.vue'),
|
||||
),
|
||||
// 循环节点
|
||||
loop: defineAsyncComponent(() => import('../panels/LoopPanel.vue')),
|
||||
// 子流程节点
|
||||
subflow: defineAsyncComponent(() => import('../panels/SubflowPanel.vue')),
|
||||
// Text-to-SQL 节点
|
||||
text_to_sql: defineAsyncComponent(
|
||||
() => import('../panels/TextToSqlPanel.vue'),
|
||||
),
|
||||
// 知识库节点
|
||||
knowledge_retrieval: defineAsyncComponent(
|
||||
() => import('../panels/KnowledgeRetrievalPanel.vue'),
|
||||
),
|
||||
};
|
||||
|
||||
const CurrentPanel = computed(() => {
|
||||
if (!props.node) return null;
|
||||
return components[props.node.type] || null;
|
||||
});
|
||||
|
||||
const handleUpdate = (newData: any) => {
|
||||
// 使用 VueFlow 的 updateNodeData 方法安全更新节点数据
|
||||
// 这样不会触发节点重新渲染导致消失
|
||||
if (props.node?.id) {
|
||||
updateNodeData(props.node.id, newData);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="bg-card z-20 flex h-full flex-col rounded-[8px] py-3 transition-all duration-300"
|
||||
:class="isFullscreen ? 'w-full' : 'w-96'"
|
||||
>
|
||||
<div class="flex items-center justify-between px-4 pb-4 pt-1">
|
||||
<div class="text-foreground font-medium">
|
||||
{{ node.data.label || node.label }}
|
||||
{{ $t('ai-platform.workflow.editor.panel.propertyConfig') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- <ElButton
|
||||
link
|
||||
:icon="isFullscreen ? Minimize : Maximize"
|
||||
@click="toggleFullscreen"
|
||||
/> -->
|
||||
<ElButton link :icon="X" @click="$emit('close')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElScrollbar class="flex-1">
|
||||
<div class="p-4">
|
||||
<component
|
||||
:is="CurrentPanel"
|
||||
v-if="CurrentPanel"
|
||||
:key="node.id"
|
||||
:data="node.data"
|
||||
:node-id="node.id"
|
||||
:node-type="node.type"
|
||||
@update="handleUpdate"
|
||||
/>
|
||||
<div v-else class="text-muted-foreground py-10 text-center">
|
||||
{{ $t('ai-platform.workflow.editor.panel.noConfig') }}
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Play } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
} from 'element-plus';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
nodes: any[];
|
||||
workflowName?: string;
|
||||
}>(),
|
||||
{
|
||||
workflowName: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submit', inputs: Record<string, any>): void;
|
||||
}>();
|
||||
|
||||
const inputs = ref<Record<string, any>>({});
|
||||
|
||||
const startNode = computed(() => props.nodes.find((n) => n.type === 'start'));
|
||||
const variables = computed(() => startNode.value?.data?.variables || []);
|
||||
|
||||
watch(
|
||||
variables,
|
||||
(vars) => {
|
||||
const newInputs: Record<string, any> = {};
|
||||
vars.forEach((v: any) => {
|
||||
newInputs[v.variable] = inputs.value[v.variable] || v.default_value || '';
|
||||
});
|
||||
inputs.value = newInputs;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
inputs.value = {};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleRun = () => {
|
||||
emit('submit', { ...inputs.value });
|
||||
emit('update:visible', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="props.visible"
|
||||
:title="workflowName || $t('ai-platform.workflow.editor.runPanel.runPreview')"
|
||||
:width="'480px'"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
>
|
||||
<div class="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider">
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.inputVariables') }}
|
||||
</div>
|
||||
<ElForm label-position="top" size="default">
|
||||
<ElFormItem
|
||||
v-for="v in variables"
|
||||
:key="v.variable"
|
||||
:label="v.label || v.variable"
|
||||
:required="v.required"
|
||||
>
|
||||
<ElInput
|
||||
v-model="inputs[v.variable]"
|
||||
:placeholder="$t('ai-platform.workflow.editor.runPanel.inputPlaceholder', { type: v.type })"
|
||||
:type="v.type === 'number' ? 'number' : 'text'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-center">
|
||||
<ElButton type="primary" :icon="Play" @click="handleRun">
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.startRun') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,786 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkflowStreamEvent } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { CheckCircle2, Clock, Loader2, Play, X, XCircle } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElScrollbar,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { runWorkflowStreamApi } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: any[];
|
||||
workflowId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'run-start',
|
||||
'run-complete',
|
||||
'clear-status',
|
||||
'node-start',
|
||||
'node-complete',
|
||||
'node-message',
|
||||
'waiting-for-input',
|
||||
'loop-iteration',
|
||||
]);
|
||||
|
||||
const running = ref(false);
|
||||
const result = ref<any>(null);
|
||||
const inputs = ref<Record<string, any>>({});
|
||||
const runId = ref<string>('');
|
||||
let cancelStream: (() => void) | null = null;
|
||||
|
||||
// 实时节点状态追踪
|
||||
interface NodeExecutionState {
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
node_label: string;
|
||||
status: 'failed' | 'pending' | 'running' | 'success';
|
||||
start_time?: number;
|
||||
elapsed_time?: number;
|
||||
tokens_used?: number;
|
||||
error_message?: string;
|
||||
inputs?: {
|
||||
previous_output?: any;
|
||||
variables?: Record<string, any>;
|
||||
};
|
||||
outputs?: {
|
||||
output?: any;
|
||||
output_variables?: Record<string, any>;
|
||||
};
|
||||
// LLM 流式输出
|
||||
streaming_content?: string;
|
||||
// 并行执行
|
||||
parallel_branches?: string[];
|
||||
parallel_results?: Record<string, any>;
|
||||
}
|
||||
const nodeStates = ref<Map<string, NodeExecutionState>>(new Map());
|
||||
const currentNodeId = ref<string>('');
|
||||
let timerInterval: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
// 查找 Start 节点并提取变量
|
||||
const startNode = computed(() => props.nodes.find((n) => n.type === 'start'));
|
||||
const variables = computed(() => startNode.value?.data?.variables || []);
|
||||
|
||||
// 获取节点状态列表(按执行顺序)
|
||||
const nodeStateList = computed(() => {
|
||||
return [...nodeStates.value.values()];
|
||||
});
|
||||
|
||||
// 计算总体统计
|
||||
const totalStats = computed(() => {
|
||||
const states = nodeStateList.value;
|
||||
const completed = states.filter(
|
||||
(s) => s.status === 'success' || s.status === 'failed',
|
||||
);
|
||||
const totalTime = completed.reduce(
|
||||
(sum, s) => sum + (s.elapsed_time || 0),
|
||||
0,
|
||||
);
|
||||
const totalTokenCount = completed.reduce(
|
||||
(sum, s) => sum + (s.tokens_used || 0),
|
||||
0,
|
||||
);
|
||||
return {
|
||||
total: states.length,
|
||||
completed: completed.length,
|
||||
success: states.filter((s) => s.status === 'success').length,
|
||||
failed: states.filter((s) => s.status === 'failed').length,
|
||||
running: states.filter((s) => s.status === 'running').length,
|
||||
totalTime,
|
||||
totalTokenCount,
|
||||
};
|
||||
});
|
||||
|
||||
// 启动计时器更新当前运行节点的耗时
|
||||
const startTimer = () => {
|
||||
stopTimer();
|
||||
timerInterval = setInterval(() => {
|
||||
if (currentNodeId.value) {
|
||||
const state = nodeStates.value.get(currentNodeId.value);
|
||||
if (state && state.status === 'running' && state.start_time) {
|
||||
state.elapsed_time = Date.now() - state.start_time;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化输入(使用默认值)
|
||||
watch(
|
||||
variables,
|
||||
(vars) => {
|
||||
const newInputs: Record<string, any> = {};
|
||||
vars.forEach((v: any) => {
|
||||
// 保留已有值,否则使用默认值
|
||||
newInputs[v.variable] = inputs.value[v.variable] || v.default_value || '';
|
||||
});
|
||||
inputs.value = newInputs;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 组件卸载时取消流和计时器
|
||||
onUnmounted(() => {
|
||||
cancelStream?.();
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
// 格式化耗时显示
|
||||
const formatTime = (ms: number | undefined) => {
|
||||
if (ms === undefined) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
if (!props.workflowId) return;
|
||||
running.value = true;
|
||||
result.value = null;
|
||||
nodeStates.value.clear();
|
||||
currentNodeId.value = '';
|
||||
emit('run-start');
|
||||
startTimer();
|
||||
|
||||
// 编辑器调试模式,使用草稿版本
|
||||
// 系统变量(application_id 等)由 API 层自动注入
|
||||
cancelStream = runWorkflowStreamApi(
|
||||
props.workflowId,
|
||||
inputs.value,
|
||||
(event: WorkflowStreamEvent) => {
|
||||
// 处理不同类型的事件
|
||||
switch (event.type) {
|
||||
case 'complete': {
|
||||
stopTimer();
|
||||
result.value = {
|
||||
status: 'completed',
|
||||
outputs: event.outputs,
|
||||
elapsed_time: event.elapsed_time,
|
||||
total_tokens: event.total_tokens,
|
||||
execution_log: nodeStateList.value,
|
||||
};
|
||||
running.value = false;
|
||||
emit('run-complete', result.value);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
stopTimer();
|
||||
// 将当前运行中的节点标记为失败(优先使用事件中的 node_id)
|
||||
const failedNodeId = event.node_id || currentNodeId.value;
|
||||
if (failedNodeId) {
|
||||
const state = nodeStates.value.get(failedNodeId);
|
||||
if (state && state.status === 'running') {
|
||||
state.status = 'failed';
|
||||
state.error_message =
|
||||
event.message ||
|
||||
$t('ai-platform.workflow.editor.runPanel.execFailed');
|
||||
state.elapsed_time = state.start_time
|
||||
? Date.now() - state.start_time
|
||||
: 0;
|
||||
emit('node-complete', {
|
||||
node_id: failedNodeId,
|
||||
node_type: state.node_type,
|
||||
status: 'failed',
|
||||
error_message:
|
||||
event.message ||
|
||||
$t('ai-platform.workflow.editor.runPanel.execFailed'),
|
||||
});
|
||||
}
|
||||
currentNodeId.value = '';
|
||||
}
|
||||
result.value = {
|
||||
status: 'failed',
|
||||
error_message: event.message,
|
||||
execution_log: nodeStateList.value,
|
||||
};
|
||||
running.value = false;
|
||||
emit('run-complete', result.value);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'llm_chunk': {
|
||||
// LLM 流式输出
|
||||
const state = nodeStates.value.get(event.node_id || '');
|
||||
if (state) {
|
||||
state.streaming_content = event.accumulated_content || '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_complete': {
|
||||
// 循环执行完成
|
||||
const loopEvent = event as any;
|
||||
const outputVar = loopEvent.output_variable || 'loop_results';
|
||||
const outputs: Record<string, any> = {
|
||||
total_iterations: loopEvent.total_iterations,
|
||||
results_count: loopEvent.results_count,
|
||||
};
|
||||
// 将 loop_results 添加到输出中
|
||||
if (loopEvent.loop_results) {
|
||||
outputs[outputVar] = loopEvent.loop_results;
|
||||
}
|
||||
|
||||
emit('node-complete', {
|
||||
node_id: loopEvent.node_id,
|
||||
node_type: 'loop',
|
||||
status: 'success',
|
||||
elapsed_time: 0,
|
||||
tokens_used: 0,
|
||||
outputs,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_complete': {
|
||||
// 循环迭代完成
|
||||
emit('loop-iteration', {
|
||||
type: 'complete',
|
||||
node_id: event.node_id,
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
output: event.output,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_error': {
|
||||
// 循环迭代错误
|
||||
emit('loop-iteration', {
|
||||
type: 'error',
|
||||
node_id: event.node_id,
|
||||
iteration: event.iteration,
|
||||
error: event.error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_start': {
|
||||
// 循环迭代开始
|
||||
emit('loop-iteration', {
|
||||
type: 'start',
|
||||
node_id: event.node_id,
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
item: event.item,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_complete': {
|
||||
// 更新节点完成状态(包含输出数据)
|
||||
const state = nodeStates.value.get(event.node_id || '');
|
||||
if (state) {
|
||||
state.status = event.status === 'success' ? 'success' : 'failed';
|
||||
state.elapsed_time = event.elapsed_time;
|
||||
state.tokens_used = event.tokens_used;
|
||||
state.error_message = event.error_message;
|
||||
state.outputs = event.outputs;
|
||||
}
|
||||
currentNodeId.value = '';
|
||||
emit('node-complete', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: event.status,
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used,
|
||||
error_message: event.error_message,
|
||||
warnings: event.warnings,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_event': {
|
||||
// 节点事件(如消息节点发送消息)
|
||||
const nodeEvent = event.event;
|
||||
if (nodeEvent?.type === 'message') {
|
||||
// 显示消息
|
||||
emit('node-message', {
|
||||
node_id: event.node_id,
|
||||
content: nodeEvent.content,
|
||||
message_type: nodeEvent.message_type,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_start': {
|
||||
// 记录节点开始状态(包含输入数据)
|
||||
currentNodeId.value = event.node_id || '';
|
||||
nodeStates.value.set(event.node_id || '', {
|
||||
node_id: event.node_id || '',
|
||||
node_type: event.node_type || '',
|
||||
node_label: event.node_label || event.node_id || '',
|
||||
status: 'running',
|
||||
start_time: Date.now(),
|
||||
elapsed_time: 0,
|
||||
inputs: event.inputs,
|
||||
});
|
||||
emit('node-start', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'parallel_complete': {
|
||||
// 并行执行完成
|
||||
const state = nodeStates.value.get(event.node_id || '');
|
||||
if (state) {
|
||||
state.status = 'success';
|
||||
state.elapsed_time = Date.now() - (state.start_time || Date.now());
|
||||
state.parallel_results = event.branch_results;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'parallel_start': {
|
||||
// 并行执行开始
|
||||
const branches = event.branches || [];
|
||||
nodeStates.value.set(event.node_id || '', {
|
||||
node_id: event.node_id || '',
|
||||
node_type: 'parallel',
|
||||
node_label: $t('ai-platform.workflow.editor.runPanel.parallelExec'),
|
||||
status: 'running',
|
||||
start_time: Date.now(),
|
||||
elapsed_time: 0,
|
||||
parallel_branches: branches,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start': {
|
||||
// 工作流开始执行
|
||||
runId.value = event.run_id || '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'waiting_input': {
|
||||
// 等待用户输入(对话流节点)
|
||||
stopTimer();
|
||||
const state = nodeStates.value.get(event.node_id || '');
|
||||
if (state) {
|
||||
state.status = 'waiting' as any;
|
||||
state.elapsed_time = state.start_time
|
||||
? Date.now() - state.start_time
|
||||
: 0;
|
||||
}
|
||||
// 发送等待事件给父组件处理
|
||||
emit('waiting-for-input', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
run_id: runId.value,
|
||||
waiting_config: event.waiting_config,
|
||||
loop_iteration: event.loop_iteration,
|
||||
});
|
||||
running.value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Stream error:', error);
|
||||
stopTimer();
|
||||
// 将当前运行中的节点标记为失败
|
||||
if (currentNodeId.value) {
|
||||
const state = nodeStates.value.get(currentNodeId.value);
|
||||
if (state && state.status === 'running') {
|
||||
state.status = 'failed';
|
||||
state.error_message =
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.runPanel.execFailed');
|
||||
emit('node-complete', {
|
||||
node_id: currentNodeId.value,
|
||||
node_type: state.node_type,
|
||||
status: 'failed',
|
||||
error_message:
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.runPanel.execFailed'),
|
||||
});
|
||||
}
|
||||
currentNodeId.value = '';
|
||||
}
|
||||
ElMessage.error(
|
||||
error.message ||
|
||||
$t('ai-platform.workflow.editor.runPanel.workflowExecFailed'),
|
||||
);
|
||||
result.value = {
|
||||
status: 'failed',
|
||||
error_message: error.message,
|
||||
execution_log: nodeStateList.value,
|
||||
};
|
||||
running.value = false;
|
||||
},
|
||||
() => {
|
||||
// Stream completed
|
||||
cancelStream = null;
|
||||
},
|
||||
true, // useDraft: 编辑器调试使用草稿版本
|
||||
);
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
cancelStream?.();
|
||||
cancelStream = null;
|
||||
stopTimer();
|
||||
running.value = false;
|
||||
// 将当前运行中的节点标记为已停止
|
||||
if (currentNodeId.value) {
|
||||
const state = nodeStates.value.get(currentNodeId.value);
|
||||
if (state && state.status === 'running') {
|
||||
state.status = 'failed';
|
||||
state.error_message = $t(
|
||||
'ai-platform.workflow.editor.runPanel.userStopped',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border-border bg-card z-30 flex h-full w-96 flex-col border-l shadow-xl"
|
||||
>
|
||||
<div
|
||||
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||
>
|
||||
<div class="text-foreground flex items-center gap-2 font-medium">
|
||||
<Play class="text-primary size-4" />
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.runPreview') }}
|
||||
</div>
|
||||
<ElButton link :icon="X" @click="$emit('close')" />
|
||||
</div>
|
||||
|
||||
<ElScrollbar class="flex-1">
|
||||
<div class="space-y-6 p-4">
|
||||
<!-- 输入区域 -->
|
||||
<div>
|
||||
<div
|
||||
class="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.inputVariables') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="variables.length === 0"
|
||||
class="border-border bg-muted text-muted-foreground rounded border border-dashed p-3 text-center text-sm"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.noVariablesHint') }}
|
||||
</div>
|
||||
|
||||
<ElForm v-else label-position="top" size="default">
|
||||
<ElFormItem
|
||||
v-for="v in variables"
|
||||
:key="v.variable"
|
||||
:label="v.label || v.variable"
|
||||
:required="v.required"
|
||||
>
|
||||
<ElInput
|
||||
v-model="inputs[v.variable]"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.runPanel.inputPlaceholder', {
|
||||
type: v.type,
|
||||
})
|
||||
"
|
||||
:type="v.type === 'number' ? 'number' : 'text'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="mt-2 flex gap-2">
|
||||
<ElButton
|
||||
v-if="!running"
|
||||
type="primary"
|
||||
class="flex-1"
|
||||
@click="handleRun"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.startRun') }}
|
||||
</ElButton>
|
||||
<ElButton v-else type="danger" class="flex-1" @click="handleStop">
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.stopRun') }}
|
||||
</ElButton>
|
||||
<ElButton v-if="result && !running" @click="emit('clear-status')">
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.clearStatus') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时执行状态 -->
|
||||
<div v-if="running || nodeStateList.length > 0" class="mt-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div
|
||||
class="text-muted-foreground text-xs font-semibold uppercase tracking-wider"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.execStatus') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="running"
|
||||
class="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<Loader2 class="size-3 animate-spin" />
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.running') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="mb-3 flex gap-2 text-xs">
|
||||
<ElTag size="small" type="info">
|
||||
{{ totalStats.completed }}/{{ totalStats.total }}
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.nodes') }}
|
||||
</ElTag>
|
||||
<ElTag v-if="totalStats.totalTokenCount > 0" size="small">
|
||||
消耗 Token:{{ totalStats.totalTokenCount }}
|
||||
</ElTag>
|
||||
<ElTag v-if="totalStats.totalTime > 0" size="small">
|
||||
{{ formatTime(totalStats.totalTime) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
|
||||
<!-- 节点列表 -->
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="state in nodeStateList"
|
||||
:key="state.node_id"
|
||||
class="rounded-lg border p-2 text-xs transition-all"
|
||||
:class="[
|
||||
state.status === 'running'
|
||||
? 'border-blue-300 bg-blue-50 dark:border-blue-700 dark:bg-blue-950'
|
||||
: '',
|
||||
state.status === 'success'
|
||||
? 'border-green-200 bg-green-50 dark:border-green-700 dark:bg-green-950'
|
||||
: '',
|
||||
state.status === 'failed'
|
||||
? 'border-red-200 bg-red-50 dark:border-red-700 dark:bg-red-950'
|
||||
: '',
|
||||
state.status === 'pending' ? 'border-border bg-muted' : '',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 状态图标 -->
|
||||
<Loader2
|
||||
v-if="state.status === 'running'"
|
||||
class="size-3.5 animate-spin text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
<CheckCircle2
|
||||
v-else-if="state.status === 'success'"
|
||||
class="size-3.5 text-green-600 dark:text-green-400"
|
||||
/>
|
||||
<XCircle
|
||||
v-else-if="state.status === 'failed'"
|
||||
class="size-3.5 text-red-600 dark:text-red-400"
|
||||
/>
|
||||
<Clock v-else class="text-muted-foreground size-3.5" />
|
||||
|
||||
<!-- 节点名称 -->
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="[
|
||||
state.status === 'running'
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: '',
|
||||
state.status === 'success'
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: '',
|
||||
state.status === 'failed'
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: '',
|
||||
state.status === 'pending' ? 'text-muted-foreground' : '',
|
||||
]"
|
||||
>
|
||||
{{ state.node_label }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">({{ state.node_type }})</span>
|
||||
</div>
|
||||
|
||||
<!-- 耗时 -->
|
||||
<span
|
||||
class="font-mono"
|
||||
:class="[
|
||||
state.status === 'running'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-muted-foreground',
|
||||
]"
|
||||
>
|
||||
{{ formatTime(state.elapsed_time) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Token 和错误信息 -->
|
||||
<div
|
||||
v-if="state.tokens_used || state.error_message"
|
||||
class="mt-1 pl-5"
|
||||
>
|
||||
<span v-if="state.tokens_used" class="text-muted-foreground">
|
||||
消耗 Token:{{ state.tokens_used }}
|
||||
</span>
|
||||
<span
|
||||
v-if="state.error_message"
|
||||
class="text-red-500 dark:text-red-400"
|
||||
>
|
||||
{{ state.error_message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- LLM 流式输出内容 -->
|
||||
<div
|
||||
v-if="state.node_type === 'llm' && state.streaming_content"
|
||||
class="mt-2 rounded border border-blue-200 bg-blue-50/50 p-2 dark:border-blue-700 dark:bg-blue-900/50"
|
||||
>
|
||||
<div
|
||||
class="mb-1 flex items-center gap-1 text-[10px] font-medium text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<Loader2
|
||||
v-if="state.status === 'running'"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.editor.runPanel.llmOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-foreground max-h-40 overflow-auto whitespace-pre-wrap break-all font-mono text-xs"
|
||||
>
|
||||
{{ state.streaming_content }}
|
||||
<span
|
||||
v-if="state.status === 'running'"
|
||||
class="ml-0.5 inline-block h-3 w-1 animate-pulse bg-blue-500 dark:bg-blue-400"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 并行执行状态 -->
|
||||
<div
|
||||
v-if="state.node_type === 'parallel'"
|
||||
class="mt-2 rounded border border-purple-200 bg-purple-50/50 p-2 dark:border-purple-700 dark:bg-purple-900/50"
|
||||
>
|
||||
<div
|
||||
class="mb-1 flex items-center gap-1 text-[10px] font-medium text-purple-600 dark:text-purple-400"
|
||||
>
|
||||
<Loader2
|
||||
v-if="state.status === 'running'"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
<span>{{
|
||||
$t(
|
||||
'ai-platform.workflow.editor.runPanel.parallelBranches',
|
||||
{ count: state.parallel_branches?.length || 0 },
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="state.parallel_branches"
|
||||
class="mt-1 flex flex-wrap gap-1"
|
||||
>
|
||||
<span
|
||||
v-for="branch in state.parallel_branches"
|
||||
:key="branch"
|
||||
class="rounded bg-purple-100 px-1.5 py-0.5 text-[10px] text-purple-700 dark:bg-purple-800 dark:text-purple-300"
|
||||
>
|
||||
{{ branch }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="state.parallel_results" class="mt-2">
|
||||
<div class="text-muted-foreground mb-1 text-[10px]">
|
||||
{{
|
||||
$t('ai-platform.workflow.editor.runPanel.branchResults')
|
||||
}}
|
||||
</div>
|
||||
<pre
|
||||
class="text-muted-foreground bg-card border-border max-h-32 overflow-auto whitespace-pre-wrap break-all rounded border p-2 font-mono text-[10px]"
|
||||
>{{ JSON.stringify(state.parallel_results, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入输出详情(可折叠) -->
|
||||
<ElCollapse v-if="state.inputs || state.outputs" class="mt-2">
|
||||
<ElCollapseItem
|
||||
v-if="state.inputs"
|
||||
:title="$t('ai-platform.workflow.editor.runPanel.input')"
|
||||
name="inputs"
|
||||
>
|
||||
<pre
|
||||
class="text-muted-foreground bg-muted max-h-32 overflow-auto whitespace-pre-wrap break-all rounded p-2 font-mono text-[10px]"
|
||||
>{{ JSON.stringify(state.inputs, null, 2) }}</pre>
|
||||
</ElCollapseItem>
|
||||
<ElCollapseItem
|
||||
v-if="state.outputs"
|
||||
:title="$t('ai-platform.workflow.editor.runPanel.output')"
|
||||
name="outputs"
|
||||
>
|
||||
<pre
|
||||
class="text-muted-foreground bg-muted max-h-32 overflow-auto whitespace-pre-wrap break-all rounded p-2 font-mono text-[10px]"
|
||||
>{{ JSON.stringify(state.outputs, null, 2) }}</pre>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果区域 -->
|
||||
<div
|
||||
v-if="result"
|
||||
class="animate-in fade-in slide-in-from-bottom-4 mt-4 duration-300"
|
||||
>
|
||||
<div
|
||||
class="text-muted-foreground mb-2 text-xs font-semibold uppercase tracking-wider"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.runPanel.runResult') }}
|
||||
</div>
|
||||
|
||||
<div class="border-border bg-muted overflow-hidden rounded-lg border">
|
||||
<div
|
||||
class="border-border bg-muted/50 text-muted-foreground flex justify-between border-b px-3 py-2 text-xs"
|
||||
>
|
||||
<span
|
||||
:class="
|
||||
result.status === 'completed'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
"
|
||||
>
|
||||
{{
|
||||
result.status === 'completed'
|
||||
? $t('ai-platform.workflow.editor.runPanel.success')
|
||||
: $t('ai-platform.workflow.editor.runPanel.failed')
|
||||
}}
|
||||
</span>
|
||||
<span v-if="result.elapsed_time">{{ result.elapsed_time }}ms</span>
|
||||
</div>
|
||||
<div v-if="result.outputs" class="p-3">
|
||||
<pre
|
||||
class="text-foreground max-h-64 overflow-auto whitespace-pre-wrap break-all font-mono text-xs"
|
||||
>{{ JSON.stringify(result.outputs, null, 2) }}</pre
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="result.error_message"
|
||||
class="p-3 text-xs text-red-500 dark:text-red-400"
|
||||
>
|
||||
{{ result.error_message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
+1139
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElInput, ElPopover } from 'element-plus';
|
||||
|
||||
import VariableSelector from './VariableSelector.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
currentNodeId?: string;
|
||||
modelValue: string;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
size?: 'default' | 'large' | 'small';
|
||||
type?: 'text' | 'textarea';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const showSelector = ref(false);
|
||||
const inputRef = ref();
|
||||
|
||||
const handleInput = (val: string) => {
|
||||
emit('update:modelValue', val);
|
||||
};
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
emit('change', val);
|
||||
};
|
||||
|
||||
// 监听键盘事件,输入 '{' 时触发
|
||||
const handleKeyup = (e: KeyboardEvent) => {
|
||||
if (e.key === '{' && props.currentNodeId) {
|
||||
showSelector.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const insertVariable = (variable: string) => {
|
||||
const val = props.modelValue || '';
|
||||
const el = inputRef.value?.textarea || inputRef.value?.input;
|
||||
|
||||
if (el) {
|
||||
const start = el.selectionStart || 0;
|
||||
const end = el.selectionEnd || 0;
|
||||
const newVal = val.slice(0, start) + variable + val.slice(end);
|
||||
|
||||
emit('update:modelValue', newVal);
|
||||
emit('change', newVal);
|
||||
|
||||
nextTick(() => {
|
||||
el.focus();
|
||||
const newPos = start + variable.length;
|
||||
el.setSelectionRange(newPos, newPos);
|
||||
});
|
||||
} else {
|
||||
const newVal = val + variable;
|
||||
emit('update:modelValue', newVal);
|
||||
emit('change', newVal);
|
||||
}
|
||||
showSelector.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="group relative w-full min-w-48">
|
||||
<ElInput
|
||||
ref="inputRef"
|
||||
:model-value="modelValue"
|
||||
:type="type"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
class="smart-input-textarea"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
@keyup="handleKeyup"
|
||||
/>
|
||||
|
||||
<ElPopover
|
||||
v-if="currentNodeId"
|
||||
v-model:visible="showSelector"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="260"
|
||||
:show-arrow="false"
|
||||
popper-class="!p-0"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="text-muted-foreground/60 hover:bg-primary/10 hover:text-primary absolute z-10 flex h-5 cursor-pointer items-center rounded px-1 transition-all"
|
||||
:class="
|
||||
type === 'textarea'
|
||||
? 'right-2 top-1'
|
||||
: 'right-1 top-1/2 -translate-y-1/2'
|
||||
"
|
||||
:title="$t('ai-platform.workflow.editor.smartInput.insertVariable')"
|
||||
@click.stop
|
||||
>
|
||||
<span class="font-mono text-[10px] font-bold leading-none">{ }</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VariableSelector
|
||||
:current-node-id="currentNodeId"
|
||||
@select="insertVariable"
|
||||
/>
|
||||
</ElPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Element Plus 风格的滚动条样式 */
|
||||
.smart-input-textarea :deep(.el-textarea__inner) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--el-border-color-darker) transparent;
|
||||
}
|
||||
|
||||
.smart-input-textarea :deep(.el-textarea__inner::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.smart-input-textarea :deep(.el-textarea__inner::-webkit-scrollbar-thumb) {
|
||||
background-color: var(--el-border-color-darker);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.smart-input-textarea
|
||||
:deep(.el-textarea__inner::-webkit-scrollbar-thumb:hover) {
|
||||
background-color: var(--el-border-color-dark);
|
||||
}
|
||||
|
||||
.smart-input-textarea :deep(.el-textarea__inner::-webkit-scrollbar-track) {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 数据库表选择对话框
|
||||
* 参考 data-source-config.vue 实现
|
||||
*/
|
||||
import type { ColumnInfo } from '#/api/core/database-manager';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { Database, RefreshCw } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElScrollbar,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
ElTree,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
getDatabaseConfigsApi,
|
||||
getDatabasesApi,
|
||||
getSchemasApi,
|
||||
getTableColumnsApi,
|
||||
getTablesApi,
|
||||
} from '#/api/core/database-manager';
|
||||
import { DatabaseTreeNodeIcon } from '#/components/database-tree-node-icon';
|
||||
import { DatabaseTreeNodeLabel } from '#/components/database-tree-node-label';
|
||||
|
||||
// 树节点类型
|
||||
interface TreeNode {
|
||||
id: string;
|
||||
label: string;
|
||||
type: 'connection' | 'database' | 'schema' | 'table';
|
||||
isLeaf: boolean;
|
||||
meta?: {
|
||||
database?: string;
|
||||
dbName?: string;
|
||||
dbType?: string;
|
||||
schema?: string;
|
||||
table?: string;
|
||||
};
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
// 表字段类型
|
||||
interface TableField {
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
nullable: boolean;
|
||||
isPrimaryKey: boolean;
|
||||
}
|
||||
|
||||
// 选中的表信息
|
||||
export interface SelectedTable {
|
||||
tableName: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
fields: TableField[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
currentTable?: string;
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [table: SelectedTable];
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
// 对话框可见性
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
},
|
||||
);
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val);
|
||||
});
|
||||
|
||||
// 树相关
|
||||
const treeData = ref<TreeNode[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 树配置
|
||||
const treeProps = {
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
};
|
||||
|
||||
// 当前选中的表节点
|
||||
const selectedTableNode = ref<null | TreeNode>(null);
|
||||
const previewFields = ref<TableField[]>([]);
|
||||
const previewLoading = ref(false);
|
||||
|
||||
// 加载数据库配置(根节点)
|
||||
async function loadDatabaseConfigs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const configs = await getDatabaseConfigsApi();
|
||||
treeData.value = configs.map((config) => ({
|
||||
id: `conn-${config.db_name}`,
|
||||
label: config.display_name || config.name,
|
||||
type: 'connection' as const,
|
||||
isLeaf: false,
|
||||
meta: {
|
||||
dbName: config.db_name,
|
||||
dbType: config.db_type,
|
||||
isSystem: config.is_system,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load database configs:', error);
|
||||
ElMessage.error(
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.loadConfigFailed'),
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据库列表
|
||||
async function loadDatabases(dbName: string, dbType?: string) {
|
||||
try {
|
||||
const databases = await getDatabasesApi(dbName);
|
||||
return databases.map((db) => ({
|
||||
id: `db-${dbName}-${db.name}`,
|
||||
label: db.name,
|
||||
type: 'database' as const,
|
||||
isLeaf: false,
|
||||
meta: {
|
||||
dbName,
|
||||
dbType,
|
||||
database: db.name,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load databases:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 加载Schema列表
|
||||
async function loadSchemas(dbName: string, database: string, dbType?: string) {
|
||||
try {
|
||||
const schemas = await getSchemasApi(dbName, database);
|
||||
return schemas.map((schema) => ({
|
||||
id: `schema-${dbName}-${database}-${schema.name}`,
|
||||
label: schema.name,
|
||||
type: 'schema' as const,
|
||||
isLeaf: false,
|
||||
meta: {
|
||||
dbName,
|
||||
dbType,
|
||||
database,
|
||||
schema: schema.name,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load schemas:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 加载表列表
|
||||
async function loadTables(
|
||||
dbName: string,
|
||||
database: string,
|
||||
schema?: string,
|
||||
dbType?: string,
|
||||
) {
|
||||
try {
|
||||
const tableList = await getTablesApi(dbName, database, schema);
|
||||
return tableList.map((table) => ({
|
||||
id: `table-${dbName}-${database}-${schema || ''}-${table.table_name}`,
|
||||
label: table.table_name,
|
||||
type: 'table' as const,
|
||||
isLeaf: true,
|
||||
meta: {
|
||||
dbName,
|
||||
dbType,
|
||||
database,
|
||||
schema: schema || table.schema_name,
|
||||
table: table.table_name,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load tables:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 懒加载子节点
|
||||
async function loadNode(node: any, resolve: any) {
|
||||
if (node.level === 0) {
|
||||
resolve(treeData.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeData = node.data as TreeNode;
|
||||
const { type, meta } = nodeData;
|
||||
|
||||
if (!meta) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (type === 'connection' && meta.dbName) {
|
||||
const nodes = await loadDatabases(meta.dbName, meta.dbType);
|
||||
resolve(nodes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'database' && meta.dbName && meta.database) {
|
||||
const dbType = meta.dbType?.toLowerCase();
|
||||
if (dbType === 'postgresql' || dbType === 'sqlserver') {
|
||||
const nodes = await loadSchemas(
|
||||
meta.dbName,
|
||||
meta.database,
|
||||
meta.dbType,
|
||||
);
|
||||
resolve(nodes);
|
||||
} else {
|
||||
const nodes = await loadTables(
|
||||
meta.dbName,
|
||||
meta.database,
|
||||
undefined,
|
||||
meta.dbType,
|
||||
);
|
||||
resolve(nodes);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'schema' && meta.dbName && meta.database) {
|
||||
const nodes = await loadTables(
|
||||
meta.dbName,
|
||||
meta.database,
|
||||
meta.schema,
|
||||
meta.dbType,
|
||||
);
|
||||
resolve(nodes);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve([]);
|
||||
} catch (error) {
|
||||
console.error('Failed to load node:', error);
|
||||
resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
// 节点点击 - 预览表字段
|
||||
async function handleNodeClick(data: TreeNode) {
|
||||
if (data.type === 'table' && data.meta) {
|
||||
selectedTableNode.value = data;
|
||||
previewLoading.value = true;
|
||||
|
||||
try {
|
||||
const columns = await getTableColumnsApi(
|
||||
data.meta.dbName!,
|
||||
data.meta.table!,
|
||||
data.meta.database,
|
||||
data.meta.schema,
|
||||
);
|
||||
previewFields.value = columns.map((col: ColumnInfo) => ({
|
||||
name: col.column_name,
|
||||
type: col.data_type,
|
||||
comment: col.description || '',
|
||||
nullable: col.is_nullable,
|
||||
isPrimaryKey: col.is_primary_key,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load columns:', error);
|
||||
previewFields.value = [];
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 双击选中表
|
||||
async function handleNodeDblClick(data: TreeNode) {
|
||||
if (data.type === 'table' && data.meta) {
|
||||
await selectTable(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 选中表
|
||||
async function selectTable(node: TreeNode) {
|
||||
if (node.type !== 'table' || !node.meta) return;
|
||||
|
||||
// 如果还没加载字段,先加载
|
||||
let fields = previewFields.value;
|
||||
if (selectedTableNode.value?.id !== node.id || fields.length === 0) {
|
||||
try {
|
||||
const columns = await getTableColumnsApi(
|
||||
node.meta.dbName!,
|
||||
node.meta.table!,
|
||||
node.meta.database,
|
||||
node.meta.schema,
|
||||
);
|
||||
fields = columns.map((col: ColumnInfo) => ({
|
||||
name: col.column_name,
|
||||
type: col.data_type,
|
||||
comment: col.description || '',
|
||||
nullable: col.is_nullable,
|
||||
isPrimaryKey: col.is_primary_key,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load columns:', error);
|
||||
fields = [];
|
||||
}
|
||||
}
|
||||
|
||||
emit('select', {
|
||||
tableName: node.meta.table!,
|
||||
dbName: node.meta.dbName!,
|
||||
dbType: node.meta.dbType || '',
|
||||
database: node.meta.database || '',
|
||||
schema: node.meta.schema,
|
||||
fields,
|
||||
});
|
||||
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
function handleConfirm() {
|
||||
if (selectedTableNode.value) {
|
||||
selectTable(selectedTableNode.value);
|
||||
} else {
|
||||
ElMessage.warning(
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.selectTableFirst'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新树
|
||||
async function refreshTree() {
|
||||
await loadDatabaseConfigs();
|
||||
ElMessage.success(
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.refreshSuccess'),
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadDatabaseConfigs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('ai-platform.workflow.editor.tableSelectDialog.title')"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
class="table-select-dialog"
|
||||
>
|
||||
<div class="flex h-[500px] gap-3">
|
||||
<!-- 左侧:数据库树形结构 -->
|
||||
<div
|
||||
class="border-border bg-card flex h-full w-[280px] flex-col rounded-lg border"
|
||||
>
|
||||
<div
|
||||
class="border-border flex items-center justify-between border-b px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElIcon><Database /></ElIcon>
|
||||
<span class="text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.database')
|
||||
}}</span>
|
||||
</div>
|
||||
<ElTooltip
|
||||
:content="
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.refresh')
|
||||
"
|
||||
>
|
||||
<ElButton
|
||||
:icon="RefreshCw"
|
||||
link
|
||||
size="small"
|
||||
@click="refreshTree"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 树形结构 -->
|
||||
<ElScrollbar class="flex-1">
|
||||
<div v-if="loading" class="flex h-32 items-center justify-center">
|
||||
<span class="text-muted-foreground text-sm">{{
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.loading')
|
||||
}}</span>
|
||||
</div>
|
||||
<ElTree
|
||||
v-else
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
:load="loadNode"
|
||||
node-key="id"
|
||||
lazy
|
||||
highlight-current
|
||||
class="p-2"
|
||||
@node-click="handleNodeClick"
|
||||
@node-dblclick="handleNodeDblClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex items-center gap-2">
|
||||
<DatabaseTreeNodeIcon
|
||||
:type="data.type"
|
||||
:db-type="data.meta?.dbType"
|
||||
:size="16"
|
||||
/>
|
||||
<DatabaseTreeNodeLabel
|
||||
:label="data.label"
|
||||
:type="data.type"
|
||||
:is-system="data.meta?.isSystem"
|
||||
:db-name="data.meta?.dbName"
|
||||
/>
|
||||
<ElTag
|
||||
v-if="
|
||||
data.type === 'table' && data.meta?.table === currentTable
|
||||
"
|
||||
size="small"
|
||||
type="success"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.current')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:字段预览 -->
|
||||
<div class="border-border bg-card flex flex-1 flex-col rounded-lg border">
|
||||
<div class="border-border flex items-center gap-2 border-b px-3 py-2">
|
||||
<span class="text-sm font-medium">
|
||||
{{
|
||||
selectedTableNode
|
||||
? $t(
|
||||
'ai-platform.workflow.editor.tableSelectDialog.fieldList',
|
||||
{ table: selectedTableNode.label },
|
||||
)
|
||||
: $t(
|
||||
'ai-platform.workflow.editor.tableSelectDialog.fieldPreview',
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ElScrollbar class="flex-1">
|
||||
<div
|
||||
v-if="!selectedTableNode"
|
||||
class="flex h-full items-center justify-center"
|
||||
>
|
||||
<span class="text-muted-foreground text-sm">{{
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.clickToPreview')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="previewLoading"
|
||||
class="flex h-32 items-center justify-center"
|
||||
>
|
||||
<span class="text-muted-foreground text-sm">{{
|
||||
$t('ai-platform.workflow.editor.tableSelectDialog.loading')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-else class="p-2">
|
||||
<div
|
||||
v-for="field in previewFields"
|
||||
:key="field.name"
|
||||
class="border-border/50 flex items-center justify-between border-b px-2 py-2 text-sm last:border-b-0"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="font-mono font-medium"
|
||||
:class="{ 'text-primary': field.isPrimaryKey }"
|
||||
>
|
||||
{{ field.name }}
|
||||
</span>
|
||||
<ElTag v-if="field.isPrimaryKey" size="small" type="warning">
|
||||
PK
|
||||
</ElTag>
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
field.type
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="previewFields.length === 0"
|
||||
class="text-muted-foreground py-8 text-center text-sm"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.tableSelectDialog.noFields') }}
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
<!-- 选中的表信息 -->
|
||||
<div
|
||||
v-if="selectedTableNode?.meta"
|
||||
class="border-border bg-muted border-t px-3 py-2"
|
||||
>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<span class="font-medium">{{
|
||||
selectedTableNode.meta.database
|
||||
}}</span>
|
||||
<span v-if="selectedTableNode.meta.schema">.{{ selectedTableNode.meta.schema }}</span>
|
||||
<span class="text-primary font-medium">.{{ selectedTableNode.meta.table }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('ai-platform.workflow.editor.tableSelectDialog.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="!selectedTableNode"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.tableSelectDialog.confirmSelect') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-tree-node__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 懒加载时 loading 替换箭头位置 */
|
||||
:deep(.el-tree-node__content:has(.el-tree-node__loading-icon) > .el-tree-node__expand-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__loading-icon) {
|
||||
order: -1;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
+1297
File diff suppressed because it is too large
Load Diff
+761
@@ -0,0 +1,761 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
BookOpen,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Code,
|
||||
Combine,
|
||||
Database,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
LayoutTemplate,
|
||||
ListChecks,
|
||||
MessageSquareText,
|
||||
Play,
|
||||
Search,
|
||||
Snowflake,
|
||||
Square,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { ElInput, ElScrollbar, ElTooltip } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
currentNodeId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
const { nodes, edges } = useVueFlow();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const collapsedNodes = ref<Set<string>>(new Set());
|
||||
|
||||
// 定义不同节点的图标
|
||||
const icons: any = {
|
||||
start: Play,
|
||||
llm: Bot,
|
||||
http: Globe,
|
||||
code: Code,
|
||||
template: LayoutTemplate,
|
||||
end: Square,
|
||||
db_query: Database,
|
||||
db_insert: Database,
|
||||
db_update: Database,
|
||||
db_delete: Database,
|
||||
db_sql: Database,
|
||||
// 对话流节点
|
||||
question: HelpCircle,
|
||||
choice: ListChecks,
|
||||
message: MessageSquareText,
|
||||
confirm: CircleHelp,
|
||||
// Snowflake Cortex 节点
|
||||
snowflake_cortex_llm: Snowflake,
|
||||
snowflake_cortex_analyst: Snowflake,
|
||||
// 循环节点
|
||||
loop: Play,
|
||||
// 合并节点
|
||||
merge: Combine,
|
||||
// 表单节点
|
||||
// 应用节点
|
||||
// 仪表盘节点
|
||||
// 系统总结节点
|
||||
// 子流程节点
|
||||
subflow: Play,
|
||||
// 知识库检索节点
|
||||
knowledge_retrieval: BookOpen,
|
||||
};
|
||||
|
||||
// 获取当前节点的父循环节点(如果存在)
|
||||
const getParentLoopNode = (nodeId: string) => {
|
||||
const currentNode = nodes.value.find((n) => n.id === nodeId);
|
||||
if (currentNode?.parentNode) {
|
||||
const parentNode = nodes.value.find((n) => n.id === currentNode.parentNode);
|
||||
if (parentNode?.type === 'loop') {
|
||||
return parentNode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取当前节点的所有上游节点(通过边的连接关系递归查找)
|
||||
const getUpstreamNodeIds = (
|
||||
nodeId: string,
|
||||
visited = new Set<string>(),
|
||||
): Set<string> => {
|
||||
// 找到所有指向当前节点的边
|
||||
const incomingEdges = edges.value.filter((e) => e.target === nodeId);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const sourceId = edge.source;
|
||||
if (!visited.has(sourceId)) {
|
||||
visited.add(sourceId);
|
||||
// 递归查找上游节点的上游
|
||||
getUpstreamNodeIds(sourceId, visited);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前节点是循环体内的子节点,也要包含父循环节点及其上游节点
|
||||
const parentLoop = getParentLoopNode(nodeId);
|
||||
if (parentLoop && !visited.has(parentLoop.id)) {
|
||||
visited.add(parentLoop.id);
|
||||
// 递归查找父循环节点的上游
|
||||
getUpstreamNodeIds(parentLoop.id, visited);
|
||||
}
|
||||
|
||||
return visited;
|
||||
};
|
||||
|
||||
// 获取所有可用的前序节点(只显示上游节点)
|
||||
const availableNodes = computed(() => {
|
||||
// 获取当前节点的所有上游节点 ID
|
||||
const upstreamIds = getUpstreamNodeIds(props.currentNodeId);
|
||||
|
||||
// 只保留上游节点
|
||||
return nodes.value
|
||||
.filter((n) => upstreamIds.has(n.id) && n.type !== 'end')
|
||||
.map((node) => {
|
||||
let variables: any[] = [];
|
||||
|
||||
// 根据节点类型获取输出变量 schema
|
||||
switch (node.type) {
|
||||
case 'choice': {
|
||||
const choiceVar = node.data.variable_name || 'user_choice';
|
||||
variables = [
|
||||
{
|
||||
key: choiceVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.userChoice',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${choiceVar}_labels`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.choiceText',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'code': {
|
||||
// 默认提供 result 变量
|
||||
variables = [
|
||||
{
|
||||
key: 'result',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.execResult',
|
||||
),
|
||||
isSystem: true,
|
||||
},
|
||||
];
|
||||
// 加上用户自定义的输出变量
|
||||
const customOutputs = (node.data.outputs || [])
|
||||
.filter((v: any) => v.variable && v.variable !== 'result')
|
||||
.map((v: any) => ({
|
||||
key: v.variable,
|
||||
label: v.variable,
|
||||
}));
|
||||
variables.push(...customOutputs);
|
||||
break;
|
||||
}
|
||||
case 'condition': {
|
||||
// 条件节点不产生输出变量
|
||||
break;
|
||||
}
|
||||
case 'confirm': {
|
||||
const confirmVar = node.data.variable_name || 'confirmed';
|
||||
variables = [
|
||||
{
|
||||
key: confirmVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.confirmed',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'db_delete': {
|
||||
const deleteOutputVar = node.data.output_variable || 'delete_result';
|
||||
variables = [
|
||||
{
|
||||
key: deleteOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.deleteResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${deleteOutputVar}_count`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.affectedRows',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'db_insert': {
|
||||
const insertOutputVar = node.data.output_variable || 'insert_result';
|
||||
variables = [
|
||||
{
|
||||
key: insertOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.insertResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${insertOutputVar}_id`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.insertId',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'db_query': {
|
||||
const queryOutputVar = node.data.output_variable || 'query_result';
|
||||
variables = [
|
||||
{
|
||||
key: queryOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.queryResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${queryOutputVar}_count`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.resultCount',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'db_sql': {
|
||||
const sqlOutputVar = node.data.output_variable || 'sql_result';
|
||||
variables = [
|
||||
{
|
||||
key: sqlOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.sqlResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${sqlOutputVar}_count`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.resultCount',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'db_update': {
|
||||
const updateOutputVar = node.data.output_variable || 'update_result';
|
||||
variables = [
|
||||
{
|
||||
key: updateOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.updateResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${updateOutputVar}_count`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.affectedRows',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'http': {
|
||||
variables = [
|
||||
{
|
||||
key: 'body',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.responseBody',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status_code',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.statusCode',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'knowledge_retrieval': {
|
||||
// 知识库检索节点固定输出变量
|
||||
variables = [
|
||||
{
|
||||
key: 'knowledge_results',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.knowledgeResults',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'knowledge_context',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.knowledgeContext',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'knowledge_results_total',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.knowledgeResultsTotal',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'llm': {
|
||||
// 使用配置的输出变量名,默认为 llm_response
|
||||
const outputVar = node.data.output_variable || 'llm_response';
|
||||
const outputMode = node.data.output_mode || 'text';
|
||||
const outputSchema = node.data.output_schema || [];
|
||||
|
||||
if (outputMode === 'structured' && outputSchema.length > 0) {
|
||||
// 结构化输出模式:展示每个定义的字段
|
||||
variables = [
|
||||
{
|
||||
key: outputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.jsonString',
|
||||
),
|
||||
isStructured: true,
|
||||
},
|
||||
{
|
||||
key: `${outputVar}_data`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.structuredData',
|
||||
),
|
||||
isStructured: true,
|
||||
},
|
||||
];
|
||||
// 添加每个字段作为独立变量
|
||||
for (const field of outputSchema) {
|
||||
if (field.name) {
|
||||
const fieldLabel = field.description || field.name;
|
||||
const fieldType = field.type || 'string';
|
||||
variables.push({
|
||||
key: `${outputVar}_${field.name}`,
|
||||
label: `${fieldLabel} (${fieldType})`,
|
||||
isField: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
variables.push({
|
||||
key: `${outputVar}_tokens`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.tokenUsage',
|
||||
),
|
||||
});
|
||||
} else {
|
||||
// 普通文本输出模式
|
||||
variables = [
|
||||
{
|
||||
key: outputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.llmResponse',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${outputVar}_tokens`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.tokenUsage',
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 循环节点
|
||||
case 'loop': {
|
||||
const itemVar = node.data.item_variable_name || 'item';
|
||||
const indexVar = node.data.index_variable_name || 'index';
|
||||
variables = [
|
||||
{
|
||||
key: itemVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.currentLoopItem',
|
||||
),
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
key: indexVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.currentIndex',
|
||||
),
|
||||
isSystem: true,
|
||||
},
|
||||
];
|
||||
// 如果是 for_each 模式,还可以引用原始数组
|
||||
if (node.data.loop_mode === 'for_each') {
|
||||
const outputVar = node.data.output_variable || 'loop_results';
|
||||
variables.push({
|
||||
key: outputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.loopResults',
|
||||
),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'merge': {
|
||||
const mergeOutputVar = node.data.output_variable || 'merged_result';
|
||||
variables = [
|
||||
{
|
||||
key: mergeOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.mergeResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'branch_count',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.branchCount',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'message': {
|
||||
// 消息节点不产生输出变量
|
||||
break;
|
||||
}
|
||||
case 'parallel': {
|
||||
// 并行节点不产生输出变量
|
||||
break;
|
||||
}
|
||||
// 对话流节点
|
||||
case 'question': {
|
||||
const questionVar = node.data.variable_name || 'user_input';
|
||||
variables = [
|
||||
{
|
||||
key: questionVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.userAnswer',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${questionVar}_valid`,
|
||||
label: $t('ai-platform.workflow.editor.variableSelector.isValid'),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'snowflake_cortex_analyst': {
|
||||
const analystResultVar =
|
||||
node.data.output_variable || 'analyst_result';
|
||||
const analystSqlVar = node.data.sql_variable || 'analyst_sql';
|
||||
variables = [
|
||||
{
|
||||
key: analystResultVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.queryResultData',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: analystSqlVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.generatedSql',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${analystResultVar}_raw`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.rawResponse',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
// Snowflake Cortex 节点
|
||||
case 'snowflake_cortex_llm': {
|
||||
const cortexLlmVar = node.data.output_variable || 'cortex_response';
|
||||
variables = [
|
||||
{
|
||||
key: cortexLlmVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.cortexLlmResponse',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'start': {
|
||||
// 默认的系统变量
|
||||
variables = [
|
||||
{
|
||||
key: 'user_input',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.userInput',
|
||||
),
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
key: 'application_id',
|
||||
label: $t('ai-platform.workflow.editor.variableSelector.appId'),
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
key: 'application_code',
|
||||
label: $t('ai-platform.workflow.editor.variableSelector.appCode'),
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
key: 'form_code',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.formCode',
|
||||
),
|
||||
isSystem: true,
|
||||
},
|
||||
];
|
||||
// 加上自定义变量
|
||||
variables.push(
|
||||
...(node.data.variables || []).map((v: any) => ({
|
||||
key: v.variable,
|
||||
label: v.label || v.variable,
|
||||
isSystem: false,
|
||||
})),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'subflow': {
|
||||
const resultVar = node.data.result_variable || 'subflow_result';
|
||||
variables = [
|
||||
{
|
||||
key: resultVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.subflowFullResult',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${resultVar}.success`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.execSuccess',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${resultVar}.outputs`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.subflowOutputVars',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${resultVar}.total_tokens`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.tokenUsage',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 如果配置了结果传递,添加传递的变量
|
||||
const resultPassMode = node.data.result_pass_mode || 'all';
|
||||
if (resultPassMode === 'all') {
|
||||
// all 模式:提示所有子流程输出变量都可用
|
||||
variables.push({
|
||||
key: '*',
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.allSubflowVars',
|
||||
),
|
||||
isSystem: true,
|
||||
});
|
||||
} else if (resultPassMode === 'selected') {
|
||||
// selected 模式:显示选择的变量
|
||||
const resultVars = node.data.result_vars || [];
|
||||
resultVars.forEach((varName: string) => {
|
||||
variables.push({
|
||||
key: varName,
|
||||
label: `${varName} (${$t('ai-platform.workflow.editor.variableSelector.fromSubflow')})`,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'template': {
|
||||
const templateOutputVar =
|
||||
node.data.output_variable || 'template_result';
|
||||
variables = [
|
||||
{
|
||||
key: templateOutputVar,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.renderResult',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'text_to_sql': {
|
||||
const textToSqlOutputVar =
|
||||
node.data.output_variable || 'text_to_sql_result';
|
||||
variables = [
|
||||
{
|
||||
key: `${textToSqlOutputVar}_sql`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.generatedSqlStatement',
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `${textToSqlOutputVar}_thought`,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.sqlThought',
|
||||
),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// 其他未知节点类型,尝试从 output_variable 获取
|
||||
if (node.data.output_variable) {
|
||||
variables = [
|
||||
{
|
||||
key: node.data.output_variable,
|
||||
label: $t(
|
||||
'ai-platform.workflow.editor.variableSelector.output',
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
label: node.data.label || node.label,
|
||||
type: node.type,
|
||||
variables,
|
||||
};
|
||||
})
|
||||
.filter((n) => n.variables.length > 0);
|
||||
});
|
||||
|
||||
const filteredNodes = computed(() => {
|
||||
if (!searchQuery.value) return availableNodes.value;
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return availableNodes.value.filter((node) => {
|
||||
const matchNode = node.label.toLowerCase().includes(query);
|
||||
const matchVar = node.variables.some(
|
||||
(v: any) =>
|
||||
v.key.toLowerCase().includes(query) ||
|
||||
v.label.toLowerCase().includes(query),
|
||||
);
|
||||
return matchNode || matchVar;
|
||||
});
|
||||
});
|
||||
|
||||
const toggleNodeCollapse = (nodeId: string) => {
|
||||
if (collapsedNodes.value.has(nodeId)) {
|
||||
collapsedNodes.value.delete(nodeId);
|
||||
} else {
|
||||
collapsedNodes.value.add(nodeId);
|
||||
}
|
||||
};
|
||||
|
||||
const isNodeCollapsed = (nodeId: string) => {
|
||||
return collapsedNodes.value.has(nodeId);
|
||||
};
|
||||
|
||||
const handleSelect = (nodeId: string, variable: string) => {
|
||||
emit('select', `{{${nodeId}.${variable}}}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[390px] w-full flex-col rounded-lg">
|
||||
<div class="border-border bg-muted shrink-0 border-b p-2">
|
||||
<ElInput
|
||||
v-model="searchQuery"
|
||||
size="small"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.editor.variableSelector.searchPlaceholder')
|
||||
"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</div>
|
||||
<ElScrollbar class="flex-1">
|
||||
<div
|
||||
v-if="filteredNodes.length === 0"
|
||||
class="text-muted-foreground p-4 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.variableSelector.noVariables') }}
|
||||
</div>
|
||||
<div v-else class="p-2">
|
||||
<div v-for="node in filteredNodes" :key="node.id" class="mb-1">
|
||||
<div
|
||||
class="bg-muted/50 text-foreground hover:bg-muted flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-xs font-medium transition-colors"
|
||||
@click="toggleNodeCollapse(node.id)"
|
||||
>
|
||||
<component
|
||||
:is="isNodeCollapsed(node.id) ? ChevronRight : ChevronDown"
|
||||
class="text-muted-foreground size-3 shrink-0"
|
||||
/>
|
||||
<component
|
||||
:is="icons[node.type]"
|
||||
class="text-muted-foreground size-3 shrink-0"
|
||||
/>
|
||||
<span class="flex-1 truncate">{{ node.label }}</span>
|
||||
<span class="text-muted-foreground text-[10px]">{{
|
||||
node.variables.length
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="!isNodeCollapsed(node.id)" class="mt-1 space-y-0.5">
|
||||
<ElTooltip
|
||||
v-for="v in node.variables"
|
||||
:key="v.key"
|
||||
:content="v.label"
|
||||
placement="left"
|
||||
:show-after="500"
|
||||
>
|
||||
<div
|
||||
class="hover:bg-accent hover:text-primary flex cursor-pointer flex-col rounded px-2 py-1.5 text-xs transition-colors"
|
||||
:class="{
|
||||
'bg-primary/5 text-primary': v.isSystem,
|
||||
'bg-green-500/5 text-green-600 dark:text-green-400':
|
||||
v.isField,
|
||||
'bg-blue-500/5 text-blue-600 dark:text-blue-400':
|
||||
v.isStructured,
|
||||
'text-muted-foreground':
|
||||
!v.isSystem && !v.isField && !v.isStructured,
|
||||
}"
|
||||
@click="handleSelect(node.id, v.key)"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-mono text-[10px]">{{ v.key }}</span>
|
||||
<span
|
||||
v-if="v.isSystem"
|
||||
class="bg-primary/20 text-primary rounded px-1 text-[9px]"
|
||||
>{{
|
||||
$t('ai-platform.workflow.editor.variableSelector.system')
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
v-if="v.isField"
|
||||
class="rounded bg-green-500/20 px-1 text-[9px] text-green-600 dark:text-green-400"
|
||||
>{{
|
||||
$t('ai-platform.workflow.editor.variableSelector.field')
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
v-if="v.isStructured"
|
||||
class="rounded bg-blue-500/20 px-1 text-[9px] text-blue-600 dark:text-blue-400"
|
||||
>{{
|
||||
$t(
|
||||
'ai-platform.workflow.editor.variableSelector.structured',
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
class="text-muted-foreground mt-0.5 truncate text-[10px]"
|
||||
>{{ v.label }}</span>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkflowVersion } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Clock, RotateCcw } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDrawer,
|
||||
ElEmpty,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElScrollbar,
|
||||
ElTag,
|
||||
ElTimeline,
|
||||
ElTimelineItem,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
getWorkflowVersionsApi,
|
||||
rollbackWorkflowApi,
|
||||
} from '#/api/ai-platform/ai-platform';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
workflowId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'rollback', workflow: any): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const versions = ref<WorkflowVersion[]>([]);
|
||||
|
||||
const loadVersions = async () => {
|
||||
if (!props.workflowId) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await getWorkflowVersionsApi(props.workflowId, {
|
||||
pageSize: 50,
|
||||
});
|
||||
versions.value = result.items;
|
||||
} catch (error) {
|
||||
console.error('Failed to load version history:', error);
|
||||
ElMessage.error(
|
||||
$t('ai-platform.workflow.editor.versionHistory.loadFailed'),
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadVersions();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const handleRollback = async (version: WorkflowVersion) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('ai-platform.workflow.editor.versionHistory.rollbackConfirm', {
|
||||
version: version.version,
|
||||
}),
|
||||
$t('ai-platform.workflow.editor.versionHistory.rollbackTitle'),
|
||||
{
|
||||
confirmButtonText: $t(
|
||||
'ai-platform.workflow.editor.versionHistory.rollbackBtn',
|
||||
),
|
||||
cancelButtonText: $t('ai-platform.workflow.editor.cancelBtn'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
|
||||
const result = await rollbackWorkflowApi(props.workflowId, version.version);
|
||||
ElMessage.success(
|
||||
$t('ai-platform.workflow.editor.versionHistory.rollbackSuccess', {
|
||||
version: version.version,
|
||||
}),
|
||||
);
|
||||
emit('rollback', result);
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
if (error === 'cancel') return;
|
||||
console.error('Rollback failed:', error);
|
||||
ElMessage.error(
|
||||
$t('ai-platform.workflow.editor.versionHistory.rollbackFailed'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer
|
||||
:model-value="visible"
|
||||
:title="$t('ai-platform.workflow.editor.versionHistory.title')"
|
||||
direction="rtl"
|
||||
size="400px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-loading="loading" class="h-full">
|
||||
<ElScrollbar v-if="versions.length > 0" class="h-full">
|
||||
<ElTimeline class="px-4">
|
||||
<ElTimelineItem
|
||||
v-for="version in versions"
|
||||
:key="version.id"
|
||||
:timestamp="formatDate(version.published_at)"
|
||||
placement="top"
|
||||
>
|
||||
<div class="border-border bg-card rounded-lg border p-4 shadow-sm">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<ElTag type="primary" size="small">
|
||||
v{{ version.version }}
|
||||
</ElTag>
|
||||
<span
|
||||
v-if="version.published_by"
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
{{ version.published_by }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton
|
||||
size="small"
|
||||
:icon="RotateCcw"
|
||||
@click="handleRollback(version)"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.editor.versionHistory.rollback')
|
||||
}}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="version.description"
|
||||
class="text-muted-foreground mb-2 text-sm"
|
||||
>
|
||||
{{ version.description }}
|
||||
</p>
|
||||
<p v-else class="text-muted-foreground/50 mb-2 text-sm italic">
|
||||
{{
|
||||
$t('ai-platform.workflow.editor.versionHistory.noDescription')
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center gap-4 text-xs"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.editor.versionHistory.runCount', {
|
||||
count: version.run_count,
|
||||
})
|
||||
}}</span>
|
||||
<span>{{
|
||||
$t(
|
||||
'ai-platform.workflow.editor.versionHistory.successCount',
|
||||
{ count: version.success_count },
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</ElScrollbar>
|
||||
|
||||
<ElEmpty
|
||||
v-else
|
||||
:description="$t('ai-platform.workflow.editor.versionHistory.empty')"
|
||||
class="mt-20"
|
||||
>
|
||||
<template #image>
|
||||
<Clock class="text-muted-foreground/30 size-16" />
|
||||
</template>
|
||||
</ElEmpty>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
data?: {
|
||||
status?: EdgeStatus;
|
||||
};
|
||||
id: string;
|
||||
markerEnd?: string;
|
||||
source: string;
|
||||
sourcePosition: any;
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
style?: any;
|
||||
target: string;
|
||||
targetPosition: any;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
}>();
|
||||
|
||||
// 边的执行状态类型
|
||||
type EdgeStatus = 'completed' | 'failed' | 'idle' | 'running';
|
||||
|
||||
const isHovered = ref(false);
|
||||
|
||||
// 根据状态计算边的样式
|
||||
const edgeStyle = computed(() => {
|
||||
const status = props.data?.status || 'idle';
|
||||
const baseStyle = { ...props.style };
|
||||
|
||||
switch (status) {
|
||||
case 'completed': {
|
||||
return {
|
||||
...baseStyle,
|
||||
stroke: '#22c55e',
|
||||
strokeWidth: 2.5,
|
||||
};
|
||||
}
|
||||
case 'failed': {
|
||||
return {
|
||||
...baseStyle,
|
||||
stroke: '#ef4444',
|
||||
strokeWidth: 2.5,
|
||||
};
|
||||
}
|
||||
case 'running': {
|
||||
return {
|
||||
...baseStyle,
|
||||
stroke: '#3b82f6',
|
||||
strokeWidth: 2.5,
|
||||
strokeDasharray: '5,5',
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return baseStyle;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 是否显示流动动画
|
||||
const isRunning = computed(() => props.data?.status === 'running');
|
||||
|
||||
// 计算贝塞尔曲线路径和中点
|
||||
const pathData = computed(() => {
|
||||
const [path, labelX, labelY] = getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
});
|
||||
return { path, labelX, labelY };
|
||||
});
|
||||
|
||||
// 从父组件注入的添加节点函数
|
||||
const onEdgeAddClick =
|
||||
inject<
|
||||
(
|
||||
edgeId: string,
|
||||
source: string,
|
||||
target: string,
|
||||
position: { x: number; y: number },
|
||||
) => void
|
||||
>('onEdgeAddClick');
|
||||
|
||||
const handleAddClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (onEdgeAddClick) {
|
||||
onEdgeAddClick(props.id, props.source, props.target, {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g>
|
||||
<!-- 主边路径 - 直接使用 path 以支持动画 -->
|
||||
<path
|
||||
:id="id"
|
||||
:d="pathData.path"
|
||||
fill="none"
|
||||
:stroke="edgeStyle.stroke || '#94a3b8'"
|
||||
:stroke-width="edgeStyle.strokeWidth || 2"
|
||||
:stroke-dasharray="edgeStyle.strokeDasharray"
|
||||
:marker-end="markerEnd"
|
||||
:class="{ 'edge-path-running': isRunning }"
|
||||
/>
|
||||
|
||||
<!-- 悬停检测区域 - 更宽的透明路径 -->
|
||||
<path
|
||||
:d="pathData.path"
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
stroke-width="30"
|
||||
class="cursor-pointer"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
/>
|
||||
|
||||
<!-- 中间的添加按钮 -->
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
class="nodrag nopan"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${pathData.labelX}px, ${pathData.labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
zIndex: 1000,
|
||||
}"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<button
|
||||
v-show="isHovered"
|
||||
class="bg-primary text-primary-foreground border-card flex size-6 items-center justify-center rounded-full border-2 shadow-lg transition-all duration-200 hover:scale-110"
|
||||
title="在此处插入节点"
|
||||
@click="handleAddClick"
|
||||
>
|
||||
<Plus class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 执行中的边 - 流动动画 */
|
||||
.edge-path-running {
|
||||
animation: edge-dash-flow 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes edge-dash-flow {
|
||||
to {
|
||||
stroke-dashoffset: -10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutTemplate } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.app_create.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="app_create"
|
||||
>
|
||||
<template #icon><LayoutTemplate class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.app_create.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div v-if="data.name" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.app_create.appName') }}</span>
|
||||
<span class="max-w-[120px] truncate">{{ data.name }}</span>
|
||||
</div>
|
||||
<div v-if="data.code" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.app_create.appCode') }}</span>
|
||||
<span class="font-mono">{{ data.code }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_create.updateIfExists')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.update_if_exists
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">app_id, app_code</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutDashboard } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.app_design.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="app_design"
|
||||
>
|
||||
<template #icon><LayoutDashboard class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.app_design.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div v-if="data.design_title" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_design.designTitle')
|
||||
}}</span>
|
||||
<span class="max-w-[120px] truncate">{{ data.design_title }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_design.needConfirm')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.require_confirmation !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">design_content</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { Settings } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.app_settings.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="app_settings"
|
||||
>
|
||||
<template #icon><Settings class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.app_settings.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div v-if="data.app_name" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_settings.appName')
|
||||
}}</span>
|
||||
<span class="max-w-[120px] truncate">{{ data.app_name }}</span>
|
||||
</div>
|
||||
<div v-if="data.app_layout" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_settings.layoutMode')
|
||||
}}</span>
|
||||
<span>{{ data.app_layout }}</span>
|
||||
</div>
|
||||
<div v-if="data.theme_builtin_type" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_settings.builtinTheme')
|
||||
}}</span>
|
||||
<span>{{ data.theme_builtin_type }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_settings.needConfirm')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.require_confirmation !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { Save } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.app_update.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="app_update"
|
||||
>
|
||||
<template #icon><Save class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.app_update.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div v-if="data.settings" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.app_update.settingsSource')
|
||||
}}</span>
|
||||
<span class="max-w-[120px] truncate font-mono">{{
|
||||
data.settings
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="data.application_id" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.app_update.app') }}</span>
|
||||
<span class="max-w-[120px] truncate">{{ data.application_id }}</span>
|
||||
</div>
|
||||
<div v-else class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.app_update.target') }}</span>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.app_update.mainApp') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">update_success, config_id</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,400 @@
|
||||
<script setup lang="ts">
|
||||
import type { NodeExecutionResult } from '../components/NodeResultCard.vue';
|
||||
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import { CheckCircle2, Loader2, Plus, XCircle } from '@vben/icons';
|
||||
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { ElTooltip } from 'element-plus';
|
||||
|
||||
import NodeResultCard from '../components/NodeResultCard.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
// 新增:完整的执行结果(用于节点下方展示)
|
||||
executionResult?: NodeExecutionResult;
|
||||
executionStatus?: {
|
||||
elapsed_time?: number;
|
||||
error?: string;
|
||||
status: 'failed' | 'running' | 'success';
|
||||
tokens_used?: number;
|
||||
};
|
||||
icon?: any;
|
||||
inputs?: Array<{ id: string; label?: string }>;
|
||||
label?: string;
|
||||
nodeId?: string;
|
||||
nodeType?: string;
|
||||
outputs?: Array<{ id: string; label?: string }>;
|
||||
selected?: boolean;
|
||||
}>();
|
||||
|
||||
const addButtonRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 从父组件注入的添加节点函数
|
||||
const onNodeAddClick =
|
||||
inject<(nodeId: string, position: { x: number; y: number }) => void>(
|
||||
'onNodeAddClick',
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
const rect = addButtonRef.value?.getBoundingClientRect();
|
||||
if (rect && onNodeAddClick && props.nodeId) {
|
||||
onNodeAddClick(props.nodeId, { x: rect.right + 10, y: rect.top });
|
||||
}
|
||||
};
|
||||
|
||||
// 节点类型配色 - 支持 dark 模式,使用 Tailwind dark: 前缀
|
||||
const nodeColorConfig = computed(() => {
|
||||
const configs: Record<
|
||||
string,
|
||||
{ headerBg: string; iconBg: string; iconColor: string }
|
||||
> = {
|
||||
// 流程控制
|
||||
start: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-950 dark:to-green-950',
|
||||
iconBg: 'bg-emerald-200 dark:bg-emerald-800',
|
||||
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
||||
},
|
||||
end: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-rose-50 to-red-50 dark:from-rose-950 dark:to-red-950',
|
||||
iconBg: 'bg-rose-200 dark:bg-rose-800',
|
||||
iconColor: 'text-rose-600 dark:text-rose-400',
|
||||
},
|
||||
condition: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-amber-50 to-yellow-50 dark:from-amber-950 dark:to-yellow-950',
|
||||
iconBg: 'bg-amber-200 dark:bg-amber-800',
|
||||
iconColor: 'text-amber-600 dark:text-amber-400',
|
||||
},
|
||||
parallel: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-cyan-50 to-sky-50 dark:from-cyan-950 dark:to-sky-950',
|
||||
iconBg: 'bg-cyan-200 dark:bg-cyan-800',
|
||||
iconColor: 'text-cyan-600 dark:text-cyan-400',
|
||||
},
|
||||
merge: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-slate-50 to-gray-100 dark:from-slate-900 dark:to-gray-900',
|
||||
iconBg: 'bg-slate-300 dark:bg-slate-700',
|
||||
iconColor: 'text-slate-600 dark:text-slate-400',
|
||||
},
|
||||
|
||||
// AI 相关
|
||||
llm: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-950 dark:to-purple-950',
|
||||
iconBg: 'bg-violet-200 dark:bg-violet-800',
|
||||
iconColor: 'text-violet-600 dark:text-violet-400',
|
||||
},
|
||||
intent: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-fuchsia-50 to-pink-50 dark:from-fuchsia-950 dark:to-pink-950',
|
||||
iconBg: 'bg-fuchsia-200 dark:bg-fuchsia-800',
|
||||
iconColor: 'text-fuchsia-600 dark:text-fuchsia-400',
|
||||
},
|
||||
|
||||
// 代码和模板
|
||||
code: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950',
|
||||
iconBg: 'bg-orange-200 dark:bg-orange-800',
|
||||
iconColor: 'text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
template: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-teal-50 to-emerald-50 dark:from-teal-950 dark:to-emerald-950',
|
||||
iconBg: 'bg-teal-200 dark:bg-teal-800',
|
||||
iconColor: 'text-teal-600 dark:text-teal-400',
|
||||
},
|
||||
|
||||
// HTTP
|
||||
http: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950 dark:to-indigo-950',
|
||||
iconBg: 'bg-blue-200 dark:bg-blue-800',
|
||||
iconColor: 'text-blue-600 dark:text-blue-400',
|
||||
},
|
||||
|
||||
// 数据库
|
||||
db_query: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-indigo-50 to-blue-50 dark:from-indigo-950 dark:to-blue-950',
|
||||
iconBg: 'bg-indigo-200 dark:bg-indigo-800',
|
||||
iconColor: 'text-indigo-600 dark:text-indigo-400',
|
||||
},
|
||||
db_insert: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950',
|
||||
iconBg: 'bg-green-200 dark:bg-green-800',
|
||||
iconColor: 'text-green-600 dark:text-green-400',
|
||||
},
|
||||
db_update: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-yellow-50 to-amber-50 dark:from-yellow-950 dark:to-amber-950',
|
||||
iconBg: 'bg-yellow-200 dark:bg-yellow-800',
|
||||
iconColor: 'text-yellow-600 dark:text-yellow-400',
|
||||
},
|
||||
db_delete: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-red-50 to-rose-50 dark:from-red-950 dark:to-rose-950',
|
||||
iconBg: 'bg-red-200 dark:bg-red-800',
|
||||
iconColor: 'text-red-600 dark:text-red-400',
|
||||
},
|
||||
db_sql: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-purple-50 to-violet-50 dark:from-purple-950 dark:to-violet-950',
|
||||
iconBg: 'bg-purple-200 dark:bg-purple-800',
|
||||
iconColor: 'text-purple-600 dark:text-purple-400',
|
||||
},
|
||||
|
||||
// Text-to-SQL
|
||||
text_to_sql: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-cyan-50 to-teal-50 dark:from-cyan-950 dark:to-teal-950',
|
||||
iconBg: 'bg-cyan-200 dark:bg-cyan-800',
|
||||
iconColor: 'text-cyan-600 dark:text-cyan-400',
|
||||
},
|
||||
|
||||
// 对话流
|
||||
question: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-sky-50 to-blue-50 dark:from-sky-950 dark:to-blue-950',
|
||||
iconBg: 'bg-sky-200 dark:bg-sky-800',
|
||||
iconColor: 'text-sky-600 dark:text-sky-400',
|
||||
},
|
||||
choice: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-lime-50 to-green-50 dark:from-lime-950 dark:to-green-950',
|
||||
iconBg: 'bg-lime-200 dark:bg-lime-800',
|
||||
iconColor: 'text-lime-600 dark:text-lime-400',
|
||||
},
|
||||
message: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-pink-50 to-rose-50 dark:from-pink-950 dark:to-rose-950',
|
||||
iconBg: 'bg-pink-200 dark:bg-pink-800',
|
||||
iconColor: 'text-pink-600 dark:text-pink-400',
|
||||
},
|
||||
confirm: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-950 dark:to-red-950',
|
||||
iconBg: 'bg-orange-200 dark:bg-orange-800',
|
||||
iconColor: 'text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
|
||||
// Snowflake
|
||||
snowflake_cortex_llm: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-950 dark:to-blue-950',
|
||||
iconBg: 'bg-cyan-200 dark:bg-cyan-800',
|
||||
iconColor: 'text-cyan-600 dark:text-cyan-400',
|
||||
},
|
||||
snowflake_cortex_analyst: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950 dark:to-cyan-950',
|
||||
iconBg: 'bg-blue-200 dark:bg-blue-800',
|
||||
iconColor: 'text-blue-600 dark:text-blue-400',
|
||||
},
|
||||
|
||||
// 子流程
|
||||
subflow: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950 dark:to-purple-950',
|
||||
iconBg: 'bg-indigo-200 dark:bg-indigo-800',
|
||||
iconColor: 'text-indigo-600 dark:text-indigo-400',
|
||||
},
|
||||
|
||||
// 知识库
|
||||
knowledge_retrieval: {
|
||||
headerBg:
|
||||
'bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-teal-950 dark:to-cyan-950',
|
||||
iconBg: 'bg-teal-200 dark:bg-teal-800',
|
||||
iconColor: 'text-teal-600 dark:text-teal-400',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
configs[props.nodeType || ''] || {
|
||||
headerBg: 'bg-gradient-to-r from-card to-muted',
|
||||
iconBg: 'bg-card border border-border',
|
||||
iconColor: 'text-primary',
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<!-- 节点主体 -->
|
||||
<div
|
||||
class="bg-card group relative w-[280px] max-w-[280px] rounded-xl border shadow-sm transition-all duration-200 hover:shadow-md"
|
||||
:class="[
|
||||
selected ? 'border-primary ring-primary ring-1' : '',
|
||||
executionStatus?.status === 'running'
|
||||
? 'animate-pulse-border !border-blue-500 shadow-lg shadow-blue-500/20 ring-2 ring-blue-500/50'
|
||||
: '',
|
||||
executionStatus?.status === 'success'
|
||||
? '!border-green-500 ring-1 ring-green-500'
|
||||
: '',
|
||||
executionStatus?.status === 'failed'
|
||||
? '!border-red-500 ring-2 ring-red-500'
|
||||
: '',
|
||||
!selected && !executionStatus?.status ? 'border-border' : '',
|
||||
]"
|
||||
>
|
||||
<!-- Status Badge (Absolute Top Right) -->
|
||||
<div
|
||||
v-if="executionStatus"
|
||||
class="absolute -top-3 right-4 z-20 flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] shadow-sm"
|
||||
:class="[
|
||||
executionStatus.status === 'running'
|
||||
? 'border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
||||
: '',
|
||||
executionStatus.status === 'success'
|
||||
? 'border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-400'
|
||||
: '',
|
||||
executionStatus.status === 'failed'
|
||||
? 'border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<Loader2
|
||||
v-if="executionStatus.status === 'running'"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
<CheckCircle2
|
||||
v-if="executionStatus.status === 'success'"
|
||||
class="size-3"
|
||||
/>
|
||||
<ElTooltip
|
||||
v-if="executionStatus.status === 'failed' && executionStatus.error"
|
||||
:content="executionStatus.error"
|
||||
placement="top"
|
||||
>
|
||||
<XCircle class="size-3 cursor-help" />
|
||||
</ElTooltip>
|
||||
<XCircle
|
||||
v-else-if="executionStatus.status === 'failed'"
|
||||
class="size-3"
|
||||
/>
|
||||
|
||||
<span v-if="executionStatus.status === 'running'">运行中...</span>
|
||||
<span v-else-if="executionStatus.elapsed_time !== undefined">{{ executionStatus.elapsed_time }}ms</span>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="border-border/50 flex items-center gap-3 rounded-t-xl border-b px-4 py-3"
|
||||
:class="nodeColorConfig.headerBg"
|
||||
>
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg shadow-sm"
|
||||
:class="[nodeColorConfig.iconBg, nodeColorConfig.iconColor]"
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="text-foreground truncate text-sm font-semibold">
|
||||
{{ label }}
|
||||
</div>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
<slot name="desc"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Token Usage -->
|
||||
<div
|
||||
v-if="executionStatus?.tokens_used"
|
||||
class="text-muted-foreground bg-muted border-border flex items-center rounded border px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{{ executionStatus.tokens_used }} T
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="text-muted-foreground min-w-0 space-y-2 overflow-hidden p-4 text-xs">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- Input Handles (Left) -->
|
||||
<div v-if="inputs && inputs.length > 0">
|
||||
<Handle
|
||||
v-for="(input, index) in inputs"
|
||||
:key="input.id"
|
||||
type="target"
|
||||
:position="Position.Left"
|
||||
:id="input.id"
|
||||
class="!bg-primary !z-20 !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: `${((index + 1) * 100) / (inputs.length + 1)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<!-- Default single input if not specified but needed logic can be handled in parent -->
|
||||
|
||||
<!-- Output Handles (Right) -->
|
||||
<div v-if="outputs && outputs.length > 0">
|
||||
<Handle
|
||||
v-for="(output, index) in outputs"
|
||||
:key="output.id"
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
:id="output.id"
|
||||
class="!bg-primary !z-20 !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: `${((index + 1) * 100) / (outputs.length + 1)}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 悬停时显示的添加按钮 - 放在节点更右侧,不覆盖 Handle -->
|
||||
<!-- 分支类型节点(condition/intent/parallel/choice)不显示通用添加按钮,需要从具体分支 Handle 拖线 -->
|
||||
<div
|
||||
v-if="
|
||||
nodeType !== 'end' &&
|
||||
!['condition', 'intent', 'parallel', 'choice'].includes(
|
||||
nodeType || '',
|
||||
)
|
||||
"
|
||||
ref="addButtonRef"
|
||||
class="bg-primary hover:bg-primary/90 absolute -right-8 top-1/2 flex size-6 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full text-white opacity-0 shadow-md transition-opacity hover:scale-110 group-hover:opacity-100"
|
||||
title="添加下一个节点"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<Plus class="pointer-events-none size-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点下方的执行结果卡片(独立显示,不在节点内部) -->
|
||||
<NodeResultCard
|
||||
v-if="executionResult"
|
||||
:result="executionResult"
|
||||
:node-type="nodeType"
|
||||
class="mt-3"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义 Handle 样式 */
|
||||
:deep(.vue-flow__handle) {
|
||||
z-index: 20;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* 运行中边框呼吸动画 */
|
||||
@keyframes pulse-border {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-border {
|
||||
animation: pulse-border 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { ListChecks } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.choice.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="choice"
|
||||
>
|
||||
<template #icon><ListChecks class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.choice.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="rounded bg-purple-50 px-2 py-1.5 text-xs dark:bg-purple-950">
|
||||
<div class="mb-1 font-medium text-purple-700 dark:text-purple-300">
|
||||
{{ $t('ai-platform.workflow.nodes.common.question') }}
|
||||
</div>
|
||||
<div class="line-clamp-2 text-purple-600 dark:text-purple-400">
|
||||
{{
|
||||
data.question ||
|
||||
$t('ai-platform.workflow.nodes.common.configureQuestion')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.workflow.nodes.choice.options') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="data.options && data.options.length > 0"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<span
|
||||
v-for="(opt, idx) in data.options.slice(0, 3)"
|
||||
:key="idx"
|
||||
class="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-xs"
|
||||
>
|
||||
{{ opt.label || opt.value }}
|
||||
</span>
|
||||
<span
|
||||
v-if="data.options.length > 3"
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
+{{ data.options.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.workflow.nodes.choice.noOptions') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center justify-between text-xs"
|
||||
>
|
||||
<span>{{
|
||||
data.multiple
|
||||
? $t('ai-platform.workflow.nodes.choice.multiple')
|
||||
: $t('ai-platform.workflow.nodes.choice.single')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.variable_name || 'user_choice'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { Code } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.code.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="code"
|
||||
>
|
||||
<template #icon><Code class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.code.desc') }}</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">{{
|
||||
$t('ai-platform.workflow.nodes.code.language')
|
||||
}}</span>
|
||||
<span
|
||||
class="text-primary bg-primary/10 rounded px-2 font-mono text-[10px]"
|
||||
>
|
||||
{{ data.language || 'python3' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="data.inputs && data.inputs.length > 0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.nodes.code.input') }}
|
||||
{{ data.inputs.map((i: any) => i.variable).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { GitBranch } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
const props = defineProps(['selected', 'data', 'id']);
|
||||
|
||||
// 确保有默认分支
|
||||
const branches = computed(() => {
|
||||
return props.data.branches || [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.condition.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:node-id="id"
|
||||
node-type="condition"
|
||||
>
|
||||
<template #icon><GitBranch class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.condition.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-3 pt-1">
|
||||
<!-- IF 分支 -->
|
||||
<div
|
||||
v-for="(branch, index) in branches"
|
||||
:key="branch.id"
|
||||
class="relative flex h-8 items-center justify-between rounded border border-blue-100 bg-blue-50 px-3 text-xs text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300"
|
||||
>
|
||||
<span class="font-medium">IF {{ index + 1 }}</span>
|
||||
<span class="text-muted-foreground max-w-[100px] truncate">{{
|
||||
branch.name || $t('ai-platform.workflow.nodes.condition.unnamed')
|
||||
}}</span>
|
||||
|
||||
<!-- Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
:id="branch.id"
|
||||
class="!border-card !h-3 !w-3 !border-2 !bg-blue-500 transition-transform hover:scale-125"
|
||||
style="right: -17px; top: 50%; transform: translateY(-50%)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ELSE 分支 -->
|
||||
<div
|
||||
class="border-border bg-muted text-muted-foreground relative flex h-8 items-center justify-between rounded border px-3 text-xs"
|
||||
>
|
||||
<span class="font-medium">ELSE</span>
|
||||
|
||||
<!-- Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
id="else"
|
||||
class="!border-card !bg-muted-foreground !h-3 !w-3 !border-2 transition-transform hover:scale-125"
|
||||
style="right: -17px; top: 50%; transform: translateY(-50%)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleHelp } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.confirm.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="confirm"
|
||||
>
|
||||
<template #icon><CircleHelp class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.confirm.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="rounded bg-amber-50 px-2 py-1.5 text-xs dark:bg-amber-950">
|
||||
<div class="mb-1 font-medium text-amber-700 dark:text-amber-300">
|
||||
{{
|
||||
data.title || $t('ai-platform.workflow.nodes.confirm.confirmText')
|
||||
}}
|
||||
</div>
|
||||
<div class="line-clamp-2 text-amber-600 dark:text-amber-400">
|
||||
{{
|
||||
data.content ||
|
||||
$t('ai-platform.workflow.nodes.confirm.configureContent')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
class="rounded bg-green-100 px-2 py-0.5 text-green-700 dark:bg-green-900 dark:text-green-300"
|
||||
>
|
||||
{{
|
||||
data.confirm_text ||
|
||||
$t('ai-platform.workflow.nodes.confirm.confirmText')
|
||||
}}
|
||||
</span>
|
||||
<span class="bg-muted text-muted-foreground rounded px-2 py-0.5">
|
||||
{{
|
||||
data.cancel_text ||
|
||||
$t('ai-platform.workflow.nodes.confirm.cancelText')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{ data.variable_name || 'confirmed' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { FileText } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.dashboard_basic_info.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="dashboard_basic_info"
|
||||
>
|
||||
<template #icon><FileText class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.dashboard_basic_info.desc') }}
|
||||
</template>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-if="data.name" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_basic_info.name')
|
||||
}}</span>
|
||||
{{ data.name }}
|
||||
</div>
|
||||
<div v-if="data.code" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_basic_info.code')
|
||||
}}</span>
|
||||
{{ data.code }}
|
||||
</div>
|
||||
<div v-if="data.category" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_basic_info.category')
|
||||
}}</span>
|
||||
{{ data.category }}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.common.output') }}
|
||||
dashboard_basic_info
|
||||
</div>
|
||||
</template>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { FilePlus } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.dashboard_create.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="dashboard_create"
|
||||
>
|
||||
<template #icon><FilePlus class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.dashboard_create.desc') }}
|
||||
</template>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div
|
||||
v-if="data.dashboard_basic_info"
|
||||
class="text-muted-foreground truncate"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_create.basicInfo')
|
||||
}}</span>
|
||||
{{ $t('ai-platform.workflow.nodes.common.configured') }}
|
||||
</div>
|
||||
<div v-if="data.page_config" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_create.pageConfig')
|
||||
}}</span>
|
||||
{{ $t('ai-platform.workflow.nodes.common.configured') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_create.updateIfExists')
|
||||
}}</span>
|
||||
{{
|
||||
data.update_if_exists !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.common.output') }} dashboard_id,
|
||||
dashboard_code
|
||||
</div>
|
||||
</template>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutDashboard } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.dashboard_design.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="dashboard_design"
|
||||
>
|
||||
<template #icon><LayoutDashboard class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.dashboard_design.desc') }}
|
||||
</template>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-if="data.design_title" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_design.title')
|
||||
}}</span>
|
||||
{{ data.design_title }}
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_design.needConfirm')
|
||||
}}</span>
|
||||
{{
|
||||
data.require_confirmation !== false
|
||||
? $t('ai-platform.workflow.nodes.dashboard_design.need')
|
||||
: $t('ai-platform.workflow.nodes.dashboard_design.notNeed')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.common.output') }} page_config
|
||||
</div>
|
||||
</template>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { Upload } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.dashboard_publish.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="dashboard_publish"
|
||||
>
|
||||
<template #icon><Upload class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.dashboard_publish.desc') }}
|
||||
</template>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-if="data.dashboard_id" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_publish.dashboard')
|
||||
}}</span>
|
||||
{{ data.dashboard_id }}
|
||||
</div>
|
||||
<div v-if="data.menu_name" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_publish.menuName')
|
||||
}}</span>
|
||||
{{ data.menu_name }}
|
||||
</div>
|
||||
<div v-if="data.menu_icon" class="text-muted-foreground truncate">
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.dashboard_publish.icon')
|
||||
}}</span>
|
||||
{{ data.menu_icon }}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.common.output') }} menu_id, route_path
|
||||
</div>
|
||||
</template>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import DbNodeConnectionBadge from '../components/DbNodeConnectionBadge.vue';
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.db_delete.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="db_delete"
|
||||
>
|
||||
<template #icon><Trash2 class="size-5 text-red-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.db_delete.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<DbNodeConnectionBadge
|
||||
:db-config="data.db_config"
|
||||
show-default-badge
|
||||
/>
|
||||
|
||||
<!-- 表名 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border border-red-200 bg-red-50 px-1.5 py-0.5 text-[10px] font-bold text-red-600"
|
||||
>
|
||||
DELETE
|
||||
</span>
|
||||
<span
|
||||
class="text-foreground flex-1 truncate font-mono text-xs"
|
||||
:title="data.table"
|
||||
>
|
||||
{{ data.table || $t('ai-platform.workflow.nodes.db_delete.noTable') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 条件预览 -->
|
||||
<div
|
||||
v-if="data.where_conditions && data.where_conditions.length > 0"
|
||||
class="rounded bg-red-50 px-2 py-1.5"
|
||||
>
|
||||
<div class="text-[10px] text-red-700">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.common.conditions', {
|
||||
count: data.where_conditions.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rounded bg-red-100 px-2 py-1.5">
|
||||
<div class="text-[10px] font-medium text-red-600">
|
||||
{{ $t('ai-platform.workflow.nodes.db_delete.mustSetCondition') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { DatabaseZap } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import DbNodeConnectionBadge from '../components/DbNodeConnectionBadge.vue';
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
|
||||
const getFieldCount = (fieldMapping: Record<string, string> | undefined) => {
|
||||
if (!fieldMapping) return 0;
|
||||
return Object.keys(fieldMapping).length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.db_insert.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="db_insert"
|
||||
>
|
||||
<template #icon><DatabaseZap class="size-5 text-green-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.db_insert.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<DbNodeConnectionBadge
|
||||
:db-config="data.db_config"
|
||||
show-default-badge
|
||||
/>
|
||||
|
||||
<!-- 表名 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border border-green-200 bg-green-50 px-1.5 py-0.5 text-[10px] font-bold text-green-600 dark:border-green-700 dark:bg-green-950 dark:text-green-400"
|
||||
>
|
||||
INSERT
|
||||
</span>
|
||||
<span
|
||||
class="text-foreground flex-1 truncate font-mono text-xs"
|
||||
:title="data.table"
|
||||
>
|
||||
{{ data.table || $t('ai-platform.workflow.nodes.db_insert.noTable') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- {{ $t('ai-platform.workflow.nodes.db_insert.fieldMapping') }}预览 -->
|
||||
<div
|
||||
v-if="data.field_mapping && getFieldCount(data.field_mapping) > 0"
|
||||
class="bg-muted rounded px-2 py-1.5"
|
||||
>
|
||||
<div class="text-muted-foreground mb-1 text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.db_insert.fieldMapping') }} ({{
|
||||
getFieldCount(data.field_mapping)
|
||||
}})
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<div
|
||||
v-for="(value, field) in data.field_mapping"
|
||||
:key="field"
|
||||
class="flex items-center gap-1 text-[10px]"
|
||||
>
|
||||
<span class="text-muted-foreground font-medium">{{ field }}</span>
|
||||
<span class="text-muted-foreground">←</span>
|
||||
<span
|
||||
class="truncate font-mono text-green-600 dark:text-green-400"
|
||||
:title="value"
|
||||
>{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UPSERT 标记 -->
|
||||
<div v-if="data.upsert" class="text-[10px] text-blue-600">
|
||||
{{ $t('ai-platform.workflow.nodes.db_insert.upsert') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import DbNodeConnectionBadge from '../components/DbNodeConnectionBadge.vue';
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.db_query.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="db_query"
|
||||
>
|
||||
<template #icon><Search class="size-5 text-blue-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.db_query.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<DbNodeConnectionBadge :db-config="data.db_config" />
|
||||
|
||||
<!-- 表名 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border border-blue-200 bg-blue-50 px-1.5 py-0.5 text-[10px] font-bold text-blue-600 dark:border-blue-700 dark:bg-blue-950 dark:text-blue-400"
|
||||
>
|
||||
SELECT
|
||||
</span>
|
||||
<span
|
||||
class="text-foreground flex-1 truncate font-mono text-xs"
|
||||
:title="data.table"
|
||||
>
|
||||
{{ data.table || $t('ai-platform.workflow.nodes.db_query.noTable') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 返回字段 -->
|
||||
<div
|
||||
v-if="data.return_fields && data.return_fields !== '*'"
|
||||
class="bg-muted rounded px-2 py-1.5"
|
||||
>
|
||||
<div class="text-muted-foreground text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.db_query.returnMode') }}
|
||||
{{ data.return_fields }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 条件预览 -->
|
||||
<div
|
||||
v-if="data.where_conditions && data.where_conditions.length > 0"
|
||||
class="rounded bg-blue-50 px-2 py-1.5 dark:bg-blue-950"
|
||||
>
|
||||
<div class="text-[10px] text-blue-700 dark:text-blue-300">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.common.conditions', {
|
||||
count: data.where_conditions.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 限制条数 -->
|
||||
<div
|
||||
v-if="data.limit && data.limit !== 100"
|
||||
class="text-muted-foreground text-[10px]"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.db_query.limit', { count: data.limit })
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { Database } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.db_sql.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="db_sql"
|
||||
>
|
||||
<template #icon><Database class="size-5 text-purple-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.db_sql.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="flex min-w-0 items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
class="shrink-0 rounded border border-purple-200 bg-purple-50 px-1.5 py-0.5 text-[10px] font-bold text-purple-600"
|
||||
>
|
||||
SQL
|
||||
</span>
|
||||
<span class="text-muted-foreground shrink-0 text-[10px]">
|
||||
{{
|
||||
data.sql_type === 'query'
|
||||
? $t('ai-platform.workflow.nodes.db_sql.query')
|
||||
: $t('ai-platform.workflow.nodes.db_sql.execute')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="data.db_config?.dbName"
|
||||
class="flex min-w-0 shrink items-center justify-end gap-1"
|
||||
>
|
||||
<span
|
||||
v-if="data.sql_type === 'execute' && data.db_config.dbName === 'default'"
|
||||
class="shrink-0 rounded bg-amber-100 px-1 text-[10px] text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.editor.dbConnection.defaultBadge') }}
|
||||
</span>
|
||||
<span
|
||||
class="text-muted-foreground truncate text-[10px] font-medium"
|
||||
:title="data.db_config.dbName"
|
||||
>
|
||||
{{ data.db_config.dbName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { DatabaseBackup } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import DbNodeConnectionBadge from '../components/DbNodeConnectionBadge.vue';
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
|
||||
const getFieldCount = (fieldMapping: Record<string, string> | undefined) => {
|
||||
if (!fieldMapping) return 0;
|
||||
return Object.keys(fieldMapping).length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.db_update.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="db_update"
|
||||
>
|
||||
<template #icon><DatabaseBackup class="size-5 text-orange-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.db_update.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<DbNodeConnectionBadge
|
||||
:db-config="data.db_config"
|
||||
show-default-badge
|
||||
/>
|
||||
|
||||
<!-- 表名 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border border-orange-200 bg-orange-50 px-1.5 py-0.5 text-[10px] font-bold text-orange-600"
|
||||
>
|
||||
UPDATE
|
||||
</span>
|
||||
<span
|
||||
class="text-foreground flex-1 truncate font-mono text-xs"
|
||||
:title="data.table"
|
||||
>
|
||||
{{ data.table || $t('ai-platform.workflow.nodes.db_update.noTable') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 字段映射预览 -->
|
||||
<div
|
||||
v-if="data.field_mapping && getFieldCount(data.field_mapping) > 0"
|
||||
class="bg-muted rounded px-2 py-1.5"
|
||||
>
|
||||
<div class="text-muted-foreground mb-1 text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.db_update.updateFields') }} ({{
|
||||
getFieldCount(data.field_mapping)
|
||||
}})
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<div
|
||||
v-for="(value, field) in data.field_mapping"
|
||||
:key="field"
|
||||
class="flex items-center gap-1 text-[10px]"
|
||||
>
|
||||
<span class="text-muted-foreground font-medium">{{ field }}</span>
|
||||
<span class="text-muted-foreground">←</span>
|
||||
<span class="truncate font-mono text-orange-600" :title="value">{{
|
||||
value
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 条件预览 -->
|
||||
<div
|
||||
v-if="data.where_conditions && data.where_conditions.length > 0"
|
||||
class="rounded bg-yellow-50 px-2 py-1.5"
|
||||
>
|
||||
<div class="text-[10px] text-yellow-700">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.common.conditions', {
|
||||
count: data.where_conditions.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { Square } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="$t('ai-platform.workflow.nodes.end.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:node-id="id"
|
||||
node-type="end"
|
||||
>
|
||||
<template #icon><Square class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.end.desc') }}</template>
|
||||
|
||||
<div class="bg-muted text-muted-foreground rounded p-2 text-xs">
|
||||
<div v-if="data.outputs && data.outputs.length > 0">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariablesCount', {
|
||||
count: data.outputs.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('ai-platform.workflow.nodes.common.noOutputVariables') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { FileText } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.form_basic_info.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_basic_info"
|
||||
>
|
||||
<template #icon><FileText class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_basic_info.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_basic_info.formName')
|
||||
}}</span>
|
||||
<span class="max-w-[120px] truncate">{{
|
||||
data.name || $t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_basic_info.formCode')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.code ||
|
||||
(data.auto_generate_code
|
||||
? $t('ai-platform.workflow.nodes.form_basic_info.autoGenerate')
|
||||
: '-')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_basic_info.formType')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.form_type === 'workflow'
|
||||
? $t('ai-platform.workflow.nodes.form_basic_info.workflow')
|
||||
: $t('ai-platform.workflow.nodes.form_basic_info.normal')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">form_basic_info</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { FilePlus } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.form_create.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_create"
|
||||
>
|
||||
<template #icon><FilePlus class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_create.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_create.updateIfExists')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.update_if_exists !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">form_id, form_code</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Code, FilePlus, FileText, Search, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected?: boolean;
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
// 根据节点类型获取显示信息
|
||||
const nodeInfo = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'form_data_create': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.create'),
|
||||
icon: FilePlus,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.createDesc'),
|
||||
};
|
||||
}
|
||||
case 'form_data_delete': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.delete'),
|
||||
icon: Trash2,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.deleteDesc'),
|
||||
};
|
||||
}
|
||||
case 'form_data_list': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.list'),
|
||||
icon: Search,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.listDesc'),
|
||||
};
|
||||
}
|
||||
case 'form_data_read': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.read'),
|
||||
icon: FileText,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.readDesc'),
|
||||
};
|
||||
}
|
||||
case 'form_data_update': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.update'),
|
||||
icon: FileText,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.updateDesc'),
|
||||
};
|
||||
}
|
||||
case 'form_schema_to_llm': {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.schemaToLLM'),
|
||||
icon: Code,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.schemaToLLMDesc'),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
label: $t('ai-platform.workflow.nodes.form_data.label'),
|
||||
icon: FileText,
|
||||
desc: $t('ai-platform.workflow.nodes.form_data.desc'),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const label = computed(() => props.data?.label || nodeInfo.value.label);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="label"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
:node-type="type"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="nodeInfo.icon" class="size-5" />
|
||||
</template>
|
||||
<template #desc>{{ nodeInfo.desc }}</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- 表单编码 -->
|
||||
<div
|
||||
v-if="type !== 'form_schema_to_llm'"
|
||||
class="flex items-center justify-between rounded bg-blue-50 px-2 py-1 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_data.formCode')
|
||||
}}</span>
|
||||
<span class="font-mono text-xs">{{
|
||||
data.form_code ||
|
||||
$t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 表单转LLM特殊显示 -->
|
||||
<div
|
||||
v-if="type === 'form_schema_to_llm'"
|
||||
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_data.formCode')
|
||||
}}</span>
|
||||
<span class="font-mono text-xs">{{
|
||||
data.form_code ||
|
||||
$t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 配置信息 -->
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div v-if="type === 'form_data_read'" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
|
||||
<span>{{
|
||||
data.record_id
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="type === 'form_data_update'" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
|
||||
<span>{{
|
||||
data.record_id
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="type === 'form_data_update'" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_data.updateData')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.data ? $t('ai-platform.workflow.nodes.common.configured') : '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="type === 'form_data_create'" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_data.createData')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.data ? $t('ai-platform.workflow.nodes.common.configured') : '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="type === 'form_data_delete'" class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
|
||||
<span>{{
|
||||
data.record_id
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="type === 'form_data_list'" class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_data.filterCondition')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.filters
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.output_variable ||
|
||||
$t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { DatabaseZap } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.form_db_create.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_database_create"
|
||||
>
|
||||
<template #icon><DatabaseZap class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_db_create.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-green-50 px-2 py-1 text-green-700 dark:bg-green-950 dark:text-green-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_db_create.operation')
|
||||
}}</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_create.createTable')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_create.tableExists')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.if_exists === 'replace'
|
||||
? $t('ai-platform.workflow.nodes.form_db_create.dropRecreate')
|
||||
: data.if_exists === 'error'
|
||||
? $t('ai-platform.workflow.nodes.form_db_create.error')
|
||||
: $t('ai-platform.workflow.nodes.form_db_create.skip')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_create.autoCreateSchema')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.create_schema_if_not_exists !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">creation_result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { Database } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.form_db_design.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_database_design"
|
||||
>
|
||||
<template #icon><Database class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_db_design.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-blue-50 px-2 py-1 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_db_design.database')
|
||||
}}</span>
|
||||
<span>{{ data.db_config || 'default' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_design.mainTable')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.main_table
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: $t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_design.subTable')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.sub_tables
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: $t('ai-platform.workflow.nodes.form_db_design.noSubTable')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_db_design.systemFields')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.auto_add_system_fields !== false
|
||||
? $t('ai-platform.workflow.nodes.form_db_design.autoAdd')
|
||||
: $t('ai-platform.workflow.nodes.form_db_design.noAdd')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">database_design</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { TableProperties } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label || $t('ai-platform.workflow.nodes.form_list_design.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_list_design"
|
||||
>
|
||||
<template #icon><TableProperties class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_list_design.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-indigo-50 px-2 py-1 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_list_design.containerType')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.container_type === 'drawer'
|
||||
? $t('ai-platform.workflow.nodes.form_list_design.drawer')
|
||||
: data.container_type === 'dialog'
|
||||
? $t('ai-platform.workflow.nodes.form_list_design.dialog')
|
||||
: data.container_type === 'page'
|
||||
? $t('ai-platform.workflow.nodes.form_list_design.page')
|
||||
: $t('ai-platform.workflow.nodes.form_list_design.drawer')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_list_design.pageSize')
|
||||
}}</span>
|
||||
<span>{{ data.page_size || 20 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_list_design.autoQueryFields')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.auto_query_fields !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_list_design.autoColumns')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.auto_columns !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">list_config</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { Send } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.form_publish.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_publish"
|
||||
>
|
||||
<template #icon><Send class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_publish.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_publish.menuName')
|
||||
}}</span>
|
||||
<span class="max-w-[120px] truncate">{{
|
||||
data.menu_name ||
|
||||
$t('ai-platform.workflow.nodes.form_publish.notSet')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_publish.menuIcon')
|
||||
}}</span>
|
||||
<span>{{ data.menu_icon || 'lucide:file-text' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">menu_id, route_path</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutTemplate } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.form_ui_design.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="form_ui_design"
|
||||
>
|
||||
<template #icon><LayoutTemplate class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.form_ui_design.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.form_ui_design.layoutMode')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.layout_mode === 'auto'
|
||||
? $t('ai-platform.workflow.nodes.form_ui_design.auto')
|
||||
: data.layout_mode === 'single'
|
||||
? $t('ai-platform.workflow.nodes.form_ui_design.singleCol')
|
||||
: data.layout_mode === 'double'
|
||||
? $t('ai-platform.workflow.nodes.form_ui_design.doubleCol')
|
||||
: data.layout_mode === 'triple'
|
||||
? $t('ai-platform.workflow.nodes.form_ui_design.tripleCol')
|
||||
: $t('ai-platform.workflow.nodes.form_ui_design.auto')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_ui_design.formName')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.form_name ||
|
||||
$t('ai-platform.workflow.nodes.form_ui_design.auto')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_ui_design.enableGrouping')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.enable_grouping !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.form_ui_design.hideSystemFields')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.hide_system_fields !== false
|
||||
? $t('ai-platform.workflow.nodes.common.yes')
|
||||
: $t('ai-platform.workflow.nodes.common.no')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
|
||||
<span class="font-mono">form_ui_design</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { Globe } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
|
||||
const getMethodColor = (method: string) => {
|
||||
const map: Record<string, string> = {
|
||||
GET: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-700',
|
||||
POST: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-700',
|
||||
PUT: 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-700',
|
||||
DELETE:
|
||||
'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-700',
|
||||
};
|
||||
return (
|
||||
map[method?.toUpperCase()] || 'text-muted-foreground bg-muted border-border'
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.http.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="http"
|
||||
>
|
||||
<template #icon><Globe class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.http.desc') }}</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border px-1.5 py-0.5 text-[10px] font-bold uppercase"
|
||||
:class="getMethodColor(data.method || 'GET')"
|
||||
>
|
||||
{{ data.method || 'GET' }}
|
||||
</span>
|
||||
<span
|
||||
class="text-muted-foreground flex-1 truncate font-mono"
|
||||
:title="data.url"
|
||||
>
|
||||
{{ data.url || 'https://api.example.com' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Brain } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
const props = defineProps(['selected', 'data', 'id']);
|
||||
|
||||
// 获取意图列表
|
||||
const intents = computed(() => {
|
||||
return props.data.intents || [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.intent.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:node-id="id"
|
||||
node-type="intent"
|
||||
>
|
||||
<template #icon><Brain class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.intent.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 pt-1">
|
||||
<!-- 意图分支 -->
|
||||
<div
|
||||
v-for="intent in intents"
|
||||
:key="intent.name"
|
||||
class="relative flex h-8 items-center justify-between rounded border border-purple-100 bg-purple-50 px-3 text-xs text-purple-700"
|
||||
>
|
||||
<span class="font-medium">{{ intent.name }}</span>
|
||||
<span class="text-muted-foreground max-w-[100px] truncate">{{
|
||||
intent.description || ''
|
||||
}}</span>
|
||||
|
||||
<!-- Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
:id="intent.branch_id || intent.name"
|
||||
class="!h-3 !w-3 !border-2 !border-white !bg-purple-500 transition-transform hover:scale-125"
|
||||
style="right: -17px; top: 50%; transform: translateY(-50%)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- OTHER 分支 -->
|
||||
<div
|
||||
class="border-border bg-muted text-muted-foreground relative flex h-8 items-center justify-between rounded border px-3 text-xs"
|
||||
>
|
||||
<span class="font-medium">OTHER</span>
|
||||
<span class="text-muted-foreground">{{
|
||||
$t('ai-platform.workflow.nodes.intent.noMatch')
|
||||
}}</span>
|
||||
|
||||
<!-- Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
id="other"
|
||||
class="!h-3 !w-3 !border-2 !border-white !bg-gray-400 transition-transform hover:scale-125"
|
||||
style="right: -17px; top: 50%; transform: translateY(-50%)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 置信度阈值提示 -->
|
||||
<div
|
||||
v-if="data.confidence_threshold"
|
||||
class="text-muted-foreground text-center text-[10px]"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.nodes.intent.confidenceThreshold') }}
|
||||
{{ data.confidence_threshold }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { BookOpen } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="
|
||||
data.label ||
|
||||
$t('ai-platform.workflow.nodes.knowledge_retrieval_node.label')
|
||||
"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="knowledge_retrieval"
|
||||
>
|
||||
<template #icon><BookOpen class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.knowledge_retrieval_node.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
v-if="data.knowledge_base_ids?.length"
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.knowledge_retrieval_node.kbCount', {
|
||||
count: data.knowledge_base_ids.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-if="data.top_k" class="text-muted-foreground text-xs">
|
||||
{{
|
||||
$t(
|
||||
'ai-platform.workflow.nodes.knowledge_retrieval_node.topKThreshold',
|
||||
{ topK: data.top_k, threshold: data.score_threshold || 0.5 },
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="data.retrieval_mode"
|
||||
class="text-muted-foreground text-xs capitalize"
|
||||
>
|
||||
{{
|
||||
data.retrieval_mode === 'vector'
|
||||
? $t('ai-platform.workflow.nodes.knowledge_retrieval_node.vector')
|
||||
: data.retrieval_mode === 'fulltext'
|
||||
? $t(
|
||||
'ai-platform.workflow.nodes.knowledge_retrieval_node.fulltext',
|
||||
)
|
||||
: $t('ai-platform.workflow.nodes.knowledge_retrieval_node.hybrid')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { Bot } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.llm.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="llm"
|
||||
>
|
||||
<template #icon><Bot class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.llm.desc') }}</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-blue-50 px-2 py-1 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
||||
>
|
||||
<span class="font-medium">Model</span>
|
||||
<span>{{
|
||||
data.model_name ||
|
||||
data.model ||
|
||||
$t('ai-platform.workflow.nodes.common.selectModel')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.llm.systemPrompt') }}</span>
|
||||
<span>{{
|
||||
data.system_prompt
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.llm.userPrompt') }}</span>
|
||||
<span>{{
|
||||
data.user_prompt
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.output_variable || 'llm_response'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,306 @@
|
||||
<script setup lang="ts">
|
||||
import type { NodeExecutionResult } from '../components/NodeResultCard.vue';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { RefreshCw } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Handle, Position, useVueFlow } from '@vue-flow/core';
|
||||
|
||||
import NodeResultCard from '../components/NodeResultCard.vue';
|
||||
|
||||
const props = defineProps(['selected', 'data', 'id']);
|
||||
|
||||
const { getNodes, updateNodeInternals, updateNodeData, updateNode } =
|
||||
useVueFlow();
|
||||
|
||||
// 容器元素引用
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
let resizeObserver: null | ResizeObserver = null;
|
||||
|
||||
// 使用 ResizeObserver 监听 DOM 尺寸变化
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (props.id) {
|
||||
updateNodeInternals([props.id]);
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
// 获取循环模式
|
||||
const loopMode = computed(() => props.data?.loop_mode || 'for_each');
|
||||
const loopModeText = computed(() => {
|
||||
return loopMode.value === 'for_each'
|
||||
? $t('ai-platform.workflow.nodes.loop.forEach')
|
||||
: $t('ai-platform.workflow.nodes.loop.whileLoop');
|
||||
});
|
||||
|
||||
// 获取配置信息
|
||||
const itemsVariable = computed(() => props.data?.items_variable || '');
|
||||
const maxIterations = computed(() => props.data?.max_iterations || 100);
|
||||
|
||||
// 执行状态
|
||||
const executionStatus = computed(() => props.data?.executionStatus);
|
||||
const isRunning = computed(() => executionStatus.value?.status === 'running');
|
||||
const isSuccess = computed(() => executionStatus.value?.status === 'success');
|
||||
const isFailed = computed(() => executionStatus.value?.status === 'failed');
|
||||
const currentIteration = computed(() => executionStatus.value?.iteration ?? 0);
|
||||
const totalIterations = computed(() => executionStatus.value?.total);
|
||||
|
||||
// 执行结果(用于显示日志)
|
||||
const executionResult = computed<NodeExecutionResult | undefined>(() => {
|
||||
return props.data?.executionResult;
|
||||
});
|
||||
|
||||
// 默认最小尺寸
|
||||
const minWidth = 400;
|
||||
const minHeight = 350;
|
||||
const padding = 20;
|
||||
|
||||
// 获取子节点列表
|
||||
const childNodes = computed(() => {
|
||||
const allNodes = getNodes.value;
|
||||
return allNodes.filter((n) => n.parentNode === props.id);
|
||||
});
|
||||
|
||||
// 子节点数量
|
||||
const childCount = computed(() => childNodes.value.length);
|
||||
|
||||
// 实时计算容器尺寸(根据子节点位置动态调整)
|
||||
const containerWidth = computed(() => {
|
||||
if (childNodes.value.length === 0) {
|
||||
return minWidth;
|
||||
}
|
||||
|
||||
let maxRight = 0;
|
||||
for (const child of childNodes.value) {
|
||||
const childWidth = child.dimensions?.width || 280;
|
||||
const right = child.position.x + childWidth + padding;
|
||||
maxRight = Math.max(maxRight, right);
|
||||
}
|
||||
|
||||
return Math.max(minWidth, maxRight);
|
||||
});
|
||||
|
||||
const containerHeight = computed(() => {
|
||||
if (childNodes.value.length === 0) {
|
||||
return minHeight;
|
||||
}
|
||||
|
||||
let maxBottom = 0;
|
||||
for (const child of childNodes.value) {
|
||||
const childHeight = child.dimensions?.height || 150;
|
||||
const bottom = child.position.y + childHeight + padding;
|
||||
maxBottom = Math.max(maxBottom, bottom);
|
||||
}
|
||||
|
||||
return Math.max(minHeight, maxBottom);
|
||||
});
|
||||
|
||||
// 监听尺寸变化,触发节点内部更新以重新计算连线位置,并同步尺寸到 data 和 style
|
||||
watch(
|
||||
[containerWidth, containerHeight],
|
||||
([width, height]) => {
|
||||
if (props.id) {
|
||||
// 同步尺寸到 data,供 onDrop 判断使用
|
||||
updateNodeData(props.id, { width, height });
|
||||
// 设置节点的 style 属性,限制 VueFlow 节点包装元素的尺寸
|
||||
updateNode(props.id, {
|
||||
style: { width: `${width}px`, height: `${height}px` },
|
||||
});
|
||||
updateNodeInternals([props.id]);
|
||||
}
|
||||
},
|
||||
{ flush: 'post', immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="loop-container bg-card rounded-xl border shadow-sm transition-all duration-200 hover:shadow-md"
|
||||
:class="[
|
||||
selected ? 'border-primary ring-primary ring-1' : '',
|
||||
isRunning
|
||||
? 'animate-pulse-border !border-blue-500 shadow-lg shadow-blue-500/20 ring-2 ring-blue-500/50'
|
||||
: '',
|
||||
isSuccess ? '!border-green-500 ring-1 ring-green-500' : '',
|
||||
isFailed ? '!border-red-500 ring-2 ring-red-500' : '',
|
||||
!selected && !executionStatus?.status ? 'border-border' : '',
|
||||
]"
|
||||
:style="{
|
||||
width: `${containerWidth}px`,
|
||||
height: `${containerHeight}px`,
|
||||
}"
|
||||
>
|
||||
<!-- 循环节点头部 - 与 BaseNode 保持一致的样式 -->
|
||||
<div
|
||||
class="border-border/50 flex items-center gap-3 rounded-t-xl border-b bg-gradient-to-r from-indigo-50 to-violet-50 px-4 py-3 dark:from-indigo-950 dark:to-violet-950"
|
||||
>
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-indigo-200 text-indigo-600 shadow-sm dark:bg-indigo-800 dark:text-indigo-400"
|
||||
>
|
||||
<RefreshCw class="size-5" :class="{ 'animate-spin': isRunning }" />
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="text-foreground truncate text-sm font-semibold">
|
||||
{{ data?.label || $t('ai-platform.workflow.nodes.loop.label') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{ loopModeText }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 子节点数量 + 运行状态显示 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 子节点数量 -->
|
||||
<div
|
||||
v-if="childCount > 0"
|
||||
class="flex items-center rounded border border-indigo-300 bg-indigo-100 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:border-indigo-700 dark:bg-indigo-900 dark:text-indigo-400"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.loop.nodeCount', {
|
||||
count: childCount,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<!-- 运行状态 -->
|
||||
<div
|
||||
v-if="isRunning"
|
||||
class="flex items-center gap-1.5 rounded border border-blue-300 bg-blue-100 px-2 py-0.5 text-[10px] text-blue-600 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-400"
|
||||
>
|
||||
<span
|
||||
class="inline-block size-1.5 animate-pulse rounded-full bg-blue-500"
|
||||
></span>
|
||||
<span v-if="totalIterations">{{
|
||||
$t('ai-platform.workflow.nodes.loop.iteration', {
|
||||
current: currentIteration + 1,
|
||||
total: totalIterations,
|
||||
})
|
||||
}}</span>
|
||||
<span v-else>{{
|
||||
$t('ai-platform.workflow.nodes.loop.iterationNoTotal', {
|
||||
current: currentIteration + 1,
|
||||
})
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-muted-foreground bg-muted border-border flex items-center rounded border px-1.5 py-0.5 text-[10px]"
|
||||
>
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.loop.maxIterations', {
|
||||
max: maxIterations,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 循环配置信息 -->
|
||||
<div
|
||||
class="border-border/50 text-muted-foreground space-y-1 border-b px-4 py-2 text-xs"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{
|
||||
loopMode === 'for_each'
|
||||
? $t('ai-platform.workflow.nodes.loop.forEach')
|
||||
: $t('ai-platform.workflow.nodes.loop.loopCondition')
|
||||
}}</span>
|
||||
<span
|
||||
v-if="loopMode === 'for_each' && itemsVariable"
|
||||
class="max-w-[150px] truncate font-mono text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
{{ itemsVariable }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="loopMode === 'while'"
|
||||
class="text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.nodes.loop.whileLoop') }}
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">{{
|
||||
$t('ai-platform.workflow.nodes.common.notConfigured')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="loopMode === 'for_each'" class="text-muted-foreground">
|
||||
{{ $t('ai-platform.workflow.nodes.loop.availableVariables') }}
|
||||
<code class="bg-muted rounded px-1 text-indigo-600 dark:text-indigo-400">item</code>
|
||||
<code class="bg-muted rounded px-1 text-indigo-600 dark:text-indigo-400">index</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 循环体区域(子节点容器) -->
|
||||
<div
|
||||
class="loop-body relative p-4"
|
||||
:style="{ height: `${containerHeight - 120}px` }"
|
||||
>
|
||||
<div
|
||||
v-if="childCount === 0"
|
||||
class="border-border bg-muted/30 text-muted-foreground flex h-full min-h-[80px] items-center justify-center rounded-lg border-2 border-dashed text-sm"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.nodes.loop.dragNodeHere') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入 Handle -->
|
||||
<Handle
|
||||
type="target"
|
||||
:position="Position.Left"
|
||||
id="target"
|
||||
class="!bg-primary !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: '28px' }"
|
||||
/>
|
||||
|
||||
<!-- 输出 Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
id="source"
|
||||
class="!bg-primary !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: '28px' }"
|
||||
/>
|
||||
|
||||
<!-- 节点下方的执行结果卡片(独立显示,不在节点内部) -->
|
||||
<NodeResultCard
|
||||
v-if="executionResult"
|
||||
:result="executionResult"
|
||||
node-type="loop"
|
||||
class="mt-3"
|
||||
:style="{ width: `${containerWidth}px` }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loop-container {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
/* 确保节点尺寸与 VueFlow 计算的尺寸一致 */
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.loop-body {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
hsl(var(--muted) / 0.3) 10px,
|
||||
hsl(var(--muted) / 0.3) 20px
|
||||
);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loop-body > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { Combine } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.merge.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="merge"
|
||||
>
|
||||
<template #icon><Combine class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.merge.desc') }}</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
|
||||
>
|
||||
<span class="text-xs font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.merge.mergeMode')
|
||||
}}</span>
|
||||
<span class="text-xs">{{
|
||||
data.merge_mode === 'array'
|
||||
? $t('ai-platform.workflow.nodes.merge.array')
|
||||
: data.merge_mode === 'concat'
|
||||
? $t('ai-platform.workflow.nodes.merge.concat')
|
||||
: data.merge_mode === 'first'
|
||||
? $t('ai-platform.workflow.nodes.merge.first')
|
||||
: $t('ai-platform.workflow.nodes.merge.object')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 输出变量 -->
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.output_variable || 'merged_result'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { MessageSquareText } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.message.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="message"
|
||||
>
|
||||
<template #icon><MessageSquareText class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.message.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="rounded bg-blue-50 px-2 py-1.5 text-xs dark:bg-blue-950">
|
||||
<div class="mb-1 font-medium text-blue-700 dark:text-blue-300">
|
||||
{{ $t('ai-platform.workflow.nodes.message.content') }}
|
||||
</div>
|
||||
<div class="line-clamp-3 text-blue-600 dark:text-blue-400">
|
||||
{{
|
||||
data.content ||
|
||||
$t('ai-platform.workflow.nodes.message.configureContent')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center justify-between text-xs"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.message.messageType') }}</span>
|
||||
<span class="font-medium">{{ data.message_type || 'text' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { GitFork } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
const props = defineProps(['selected', 'data', 'id']);
|
||||
|
||||
// 获取分支配置
|
||||
const branches = computed(() => {
|
||||
return (
|
||||
props.data.branches || [
|
||||
{
|
||||
id: 'branch_1',
|
||||
name: $t('ai-platform.workflow.nodes.parallel.branch', { index: 1 }),
|
||||
},
|
||||
{
|
||||
id: 'branch_2',
|
||||
name: $t('ai-platform.workflow.nodes.parallel.branch', { index: 2 }),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.parallel.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:node-id="id"
|
||||
node-type="parallel"
|
||||
>
|
||||
<template #icon><GitFork class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.parallel.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 pt-1">
|
||||
<!-- 并行分支 -->
|
||||
<div
|
||||
v-for="(branch, index) in branches"
|
||||
:key="branch.id"
|
||||
class="relative flex h-8 items-center justify-between rounded border border-purple-100 bg-purple-50 px-3 text-xs text-purple-700"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.parallel.branch', { index: index + 1 })
|
||||
}}</span>
|
||||
<span class="text-muted-foreground max-w-[80px] truncate">{{
|
||||
branch.name || `branch_${index + 1}`
|
||||
}}</span>
|
||||
|
||||
<!-- Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
:id="branch.id"
|
||||
class="!h-3 !w-3 !border-2 !border-white !bg-purple-500 transition-transform hover:scale-125"
|
||||
style="right: -17px; top: 50%; transform: translateY(-50%)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { HelpCircle } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.question.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="question"
|
||||
>
|
||||
<template #icon><HelpCircle class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.question.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="rounded bg-purple-50 px-2 py-1.5 text-xs dark:bg-purple-950">
|
||||
<div class="mb-1 font-medium text-purple-700 dark:text-purple-300">
|
||||
{{ $t('ai-platform.workflow.nodes.common.question') }}
|
||||
</div>
|
||||
<div class="line-clamp-2 text-purple-600 dark:text-purple-400">
|
||||
{{
|
||||
data.question ||
|
||||
$t('ai-platform.workflow.nodes.common.configureQuestion')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground flex items-center justify-between text-xs"
|
||||
>
|
||||
<span>{{ $t('ai-platform.workflow.nodes.question.inputType') }}</span>
|
||||
<span class="font-medium">{{ data.input_type || 'text' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.variable_name || 'user_input'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { MessageSquareMore } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || 'Cortex Analyst'"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="snowflake_cortex_analyst"
|
||||
>
|
||||
<template #icon><MessageSquareMore class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.snowflake_cortex_analyst.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-indigo-50 px-2 py-1 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300"
|
||||
>
|
||||
<span class="font-medium">Analyst</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_analyst.nlQuery')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t(
|
||||
'ai-platform.workflow.nodes.snowflake_cortex_analyst.questionLabel',
|
||||
)
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.question
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t(
|
||||
'ai-platform.workflow.nodes.snowflake_cortex_analyst.semanticModel',
|
||||
)
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.semantic_model_file
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.output_variable || 'analyst_result'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { Snowflake } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
|
||||
const getFunctionLabel = (func: string) => {
|
||||
const map: Record<string, string> = {
|
||||
complete: $t('ai-platform.workflow.nodes.snowflake_cortex_llm.complete'),
|
||||
summarize: $t('ai-platform.workflow.nodes.snowflake_cortex_llm.summarize'),
|
||||
translate: $t('ai-platform.workflow.nodes.snowflake_cortex_llm.translate'),
|
||||
sentiment: $t('ai-platform.workflow.nodes.snowflake_cortex_llm.sentiment'),
|
||||
extract_answer: $t(
|
||||
'ai-platform.workflow.nodes.snowflake_cortex_llm.extractAnswer',
|
||||
),
|
||||
};
|
||||
return map[func] || func;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || 'Cortex LLM'"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="snowflake_cortex_llm"
|
||||
>
|
||||
<template #icon><Snowflake class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.snowflake_cortex_llm.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between rounded bg-cyan-50 px-2 py-1 text-cyan-700 dark:bg-cyan-950 dark:text-cyan-300"
|
||||
>
|
||||
<span class="font-medium">{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.function')
|
||||
}}</span>
|
||||
<span>{{
|
||||
getFunctionLabel(data.function) ||
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.selectFunction')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="data.function === 'complete'"
|
||||
class="text-muted-foreground space-y-1 text-xs"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.model')
|
||||
}}</span>
|
||||
<span>{{ data.complete?.model || 'mistral-large' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.prompt')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data.complete?.prompt
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="data.function === 'translate'"
|
||||
class="text-muted-foreground space-y-1 text-xs"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.translateLabel')
|
||||
}}</span>
|
||||
<span>{{ data.translate?.source_language || 'en' }} →
|
||||
{{ data.translate?.target_language || 'zh' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-muted-foreground text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.snowflake_cortex_llm.inputText')
|
||||
}}</span>
|
||||
<span>{{
|
||||
data[data.function]?.text
|
||||
? $t('ai-platform.workflow.nodes.common.configured')
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
|
||||
<div
|
||||
class="flex items-center justify-between text-green-700 dark:text-green-300"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.common.outputVariable')
|
||||
}}</span>
|
||||
<span class="font-mono">{{
|
||||
data.output_variable || 'cortex_result'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { Play } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="$t('ai-platform.workflow.nodes.start.label')"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="start"
|
||||
>
|
||||
<template #icon><Play class="size-5" /></template>
|
||||
<template #desc>{{ $t('ai-platform.workflow.nodes.start.desc') }}</template>
|
||||
|
||||
<div class="bg-muted text-muted-foreground rounded p-2 text-xs">
|
||||
<div v-if="data.variables && data.variables.length > 0">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.common.inputVariablesCount', {
|
||||
count: data.variables.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('ai-platform.workflow.nodes.common.noInputVariables') }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,404 @@
|
||||
<script setup lang="ts">
|
||||
import type { NodeExecutionResult } from '../components/NodeResultCard.vue';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
Workflow,
|
||||
XCircle,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { ElButton, ElProgress, ElScrollbar, ElTooltip } from 'element-plus';
|
||||
|
||||
import { getWorkflowDetailApi } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import NodeResultCard from '../components/NodeResultCard.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
}>();
|
||||
|
||||
// 展开状态
|
||||
const expanded = ref(false);
|
||||
|
||||
// 子流程定义(用于展开显示)
|
||||
const subflowDefinition = ref<any>(null);
|
||||
const loadingSubflow = ref(false);
|
||||
|
||||
// 执行状态
|
||||
const executionStatus = computed(() => props.data?.executionStatus);
|
||||
const isRunning = computed(() => executionStatus.value?.status === 'running');
|
||||
const isSuccess = computed(() => executionStatus.value?.status === 'success');
|
||||
const isFailed = computed(() => executionStatus.value?.status === 'failed');
|
||||
|
||||
// 执行结果
|
||||
const executionResult = computed<NodeExecutionResult | undefined>(() => {
|
||||
return props.data?.executionResult;
|
||||
});
|
||||
|
||||
// 子流程执行日志
|
||||
const subflowLogs = computed(() => {
|
||||
return (
|
||||
executionResult.value?.subflow_logs ||
|
||||
props.data?.subflow_result?.logs ||
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
// 子流程进度
|
||||
const progress = computed(() => {
|
||||
if (subflowLogs.value.length === 0) return 0;
|
||||
const completed = subflowLogs.value.filter(
|
||||
(l: any) => l.status === 'completed',
|
||||
).length;
|
||||
return Math.round((completed / subflowLogs.value.length) * 100);
|
||||
});
|
||||
|
||||
// 当前执行的节点
|
||||
const currentNodeLabel = computed(() => {
|
||||
const running = subflowLogs.value.find((l: any) => l.status === 'running');
|
||||
return running?.node_label || running?.node_type || '';
|
||||
});
|
||||
|
||||
// 切换展开
|
||||
const toggleExpand = async () => {
|
||||
expanded.value = !expanded.value;
|
||||
|
||||
// 展开时加载子流程定义
|
||||
if (expanded.value && !subflowDefinition.value && props.data?.subflow_id) {
|
||||
await loadSubflowDefinition();
|
||||
}
|
||||
};
|
||||
|
||||
// 加载子流程定义
|
||||
const loadSubflowDefinition = async () => {
|
||||
if (!props.data?.subflow_id || loadingSubflow.value) return;
|
||||
|
||||
loadingSubflow.value = true;
|
||||
try {
|
||||
const workflow = await getWorkflowDetailApi(props.data.subflow_id);
|
||||
subflowDefinition.value = workflow;
|
||||
} catch (error) {
|
||||
console.error('加载子流程定义失败:', error);
|
||||
} finally {
|
||||
loadingSubflow.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取节点执行状态
|
||||
const getNodeStatus = (nodeId: string) => {
|
||||
const log = subflowLogs.value.find((l: any) => l.node_id === nodeId);
|
||||
if (!log) return 'pending';
|
||||
return log.status;
|
||||
};
|
||||
|
||||
// 获取节点状态样式
|
||||
const getNodeStatusClass = (nodeId: string) => {
|
||||
const status = getNodeStatus(nodeId);
|
||||
return {
|
||||
'border-blue-500 bg-blue-50 dark:bg-blue-950': status === 'running',
|
||||
'border-green-500 bg-green-50 dark:bg-green-950': status === 'completed',
|
||||
'border-red-500 bg-red-50 dark:bg-red-950': status === 'failed',
|
||||
'border-border bg-muted/30': status === 'pending',
|
||||
};
|
||||
};
|
||||
|
||||
// 子流程节点列表(排除开始和结束节点)
|
||||
const subflowNodes = computed(() => {
|
||||
if (!subflowDefinition.value?.definition?.nodes) return [];
|
||||
return subflowDefinition.value.definition.nodes.filter(
|
||||
(n: any) => n.type !== 'start' && n.type !== 'end',
|
||||
);
|
||||
});
|
||||
|
||||
// 监听子流程ID变化,重新加载
|
||||
watch(
|
||||
() => props.data?.subflow_id,
|
||||
(newId, oldId) => {
|
||||
if (newId !== oldId) {
|
||||
subflowDefinition.value = null;
|
||||
if (expanded.value && newId) {
|
||||
loadSubflowDefinition();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="subflow-node-wrapper flex flex-col">
|
||||
<!-- 节点主体 -->
|
||||
<div
|
||||
class="bg-card group relative min-w-[320px] rounded-xl border shadow-sm transition-all duration-200 hover:shadow-md"
|
||||
:class="[
|
||||
selected ? 'border-primary ring-primary ring-1' : '',
|
||||
isRunning
|
||||
? 'animate-pulse-border !border-blue-500 shadow-lg shadow-blue-500/20 ring-2 ring-blue-500/50'
|
||||
: '',
|
||||
isSuccess ? '!border-green-500 ring-1 ring-green-500' : '',
|
||||
isFailed ? '!border-red-500 ring-2 ring-red-500' : '',
|
||||
!selected && !executionStatus?.status ? 'border-border' : '',
|
||||
]"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
<div
|
||||
v-if="executionStatus"
|
||||
class="absolute -top-3 right-4 z-10 flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] shadow-sm"
|
||||
:class="[
|
||||
isRunning
|
||||
? 'border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
||||
: '',
|
||||
isSuccess
|
||||
? 'border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-400'
|
||||
: '',
|
||||
isFailed
|
||||
? 'border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<Loader2 v-if="isRunning" class="size-3 animate-spin" />
|
||||
<CheckCircle2 v-if="isSuccess" class="size-3" />
|
||||
<ElTooltip
|
||||
v-if="isFailed && executionStatus.error"
|
||||
:content="executionStatus.error"
|
||||
placement="top"
|
||||
>
|
||||
<XCircle class="size-3 cursor-help" />
|
||||
</ElTooltip>
|
||||
<XCircle v-else-if="isFailed" class="size-3" />
|
||||
<span v-if="isRunning">运行中...</span>
|
||||
<span v-else-if="executionStatus.elapsed_time !== undefined">
|
||||
{{ executionStatus.elapsed_time }}ms
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="border-border/50 flex items-center gap-3 rounded-t-xl border-b bg-gradient-to-r from-indigo-50 to-purple-50 px-4 py-3 dark:from-indigo-950 dark:to-purple-950"
|
||||
>
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-indigo-200 text-indigo-600 shadow-sm dark:bg-indigo-800 dark:text-indigo-400"
|
||||
>
|
||||
<Workflow class="size-5" />
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="text-foreground truncate text-sm font-semibold">
|
||||
{{ data?.label || $t('ai-platform.workflow.nodes.subflow.label') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground truncate text-[10px]">
|
||||
{{
|
||||
data?.subflow_name ||
|
||||
$t('ai-platform.workflow.nodes.subflow.desc')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开/折叠按钮 -->
|
||||
<ElButton
|
||||
v-if="data?.subflow_id"
|
||||
link
|
||||
size="small"
|
||||
class="!p-1"
|
||||
@click.stop="toggleExpand"
|
||||
>
|
||||
<ChevronDown v-if="expanded" class="size-4" />
|
||||
<ChevronRight v-else class="size-4" />
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="text-muted-foreground space-y-2 p-4 text-xs">
|
||||
<!-- 子流程信息 -->
|
||||
<div
|
||||
v-if="data?.subflow_name"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.subflow.subflowLabel')
|
||||
}}</span>
|
||||
<span class="font-medium text-indigo-600 dark:text-indigo-400">
|
||||
{{ data.subflow_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground">
|
||||
{{ $t('ai-platform.workflow.nodes.subflow.notSelected') }}
|
||||
</div>
|
||||
|
||||
<!-- 变量传递模式 -->
|
||||
<div
|
||||
v-if="data?.var_pass_mode"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.nodes.subflow.varPassMode')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{
|
||||
data.var_pass_mode === 'all'
|
||||
? $t('ai-platform.workflow.nodes.subflow.all')
|
||||
: data.var_pass_mode === 'selected'
|
||||
? $t('ai-platform.workflow.nodes.subflow.selected')
|
||||
: $t('ai-platform.workflow.nodes.subflow.none')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 执行进度 -->
|
||||
<div v-if="isRunning && subflowLogs.length > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.nodes.subflow.progress') }}</span>
|
||||
<span>{{ progress }}%</span>
|
||||
</div>
|
||||
<ElProgress
|
||||
:percentage="progress"
|
||||
:show-text="false"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
<div v-if="currentNodeLabel" class="text-muted-foreground truncate">
|
||||
{{
|
||||
$t('ai-platform.workflow.nodes.subflow.current', {
|
||||
label: currentNodeLabel,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开的子流程内容 -->
|
||||
<div v-if="expanded" class="border-border border-t">
|
||||
<div class="bg-muted/30 px-4 py-2">
|
||||
<div class="text-muted-foreground mb-2 text-xs font-medium">
|
||||
{{ $t('ai-platform.workflow.nodes.subflow.subflowNodes') }}
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div
|
||||
v-if="loadingSubflow"
|
||||
class="flex items-center justify-center py-4"
|
||||
>
|
||||
<Loader2 class="size-4 animate-spin text-indigo-500" />
|
||||
<span class="text-muted-foreground ml-2 text-xs">{{
|
||||
$t('ai-platform.workflow.nodes.subflow.loading')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 子流程节点列表 -->
|
||||
<ElScrollbar v-else-if="subflowNodes.length > 0" max-height="200px">
|
||||
<div class="space-y-2 pr-2">
|
||||
<div
|
||||
v-for="node in subflowNodes"
|
||||
:key="node.id"
|
||||
class="flex items-center gap-2 rounded border px-3 py-2 text-xs transition-colors"
|
||||
:class="getNodeStatusClass(node.id)"
|
||||
>
|
||||
<!-- 状态图标 -->
|
||||
<div class="shrink-0">
|
||||
<Loader2
|
||||
v-if="getNodeStatus(node.id) === 'running'"
|
||||
class="size-3 animate-spin text-blue-500"
|
||||
/>
|
||||
<CheckCircle2
|
||||
v-else-if="getNodeStatus(node.id) === 'completed'"
|
||||
class="size-3 text-green-500"
|
||||
/>
|
||||
<XCircle
|
||||
v-else-if="getNodeStatus(node.id) === 'failed'"
|
||||
class="size-3 text-red-500"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="border-muted-foreground/30 size-3 rounded-full border-2"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 节点信息 -->
|
||||
<div class="flex-1 truncate">
|
||||
<span class="font-medium">{{
|
||||
node.data?.label || node.type
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 执行时间 -->
|
||||
<div
|
||||
v-if="
|
||||
subflowLogs.find((l: any) => l.node_id === node.id)
|
||||
?.elapsed_time
|
||||
"
|
||||
class="text-muted-foreground text-[10px]"
|
||||
>
|
||||
{{
|
||||
subflowLogs.find((l: any) => l.node_id === node.id)
|
||||
?.elapsed_time
|
||||
}}ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
|
||||
<!-- 无节点 -->
|
||||
<div v-else class="text-muted-foreground py-4 text-center text-xs">
|
||||
{{
|
||||
data?.subflow_id
|
||||
? $t('ai-platform.workflow.nodes.subflow.emptySubflow')
|
||||
: $t('ai-platform.workflow.nodes.subflow.selectSubflow')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Handle -->
|
||||
<Handle
|
||||
type="target"
|
||||
:position="Position.Left"
|
||||
id="target"
|
||||
class="!bg-primary !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: '28px' }"
|
||||
/>
|
||||
|
||||
<!-- Output Handle -->
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
id="source"
|
||||
class="!bg-primary !h-3 !w-3 !border-2 !border-white transition-transform hover:scale-125"
|
||||
:style="{ top: '28px' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 执行结果卡片 -->
|
||||
<NodeResultCard
|
||||
v-if="executionResult"
|
||||
:result="executionResult"
|
||||
node-type="subflow"
|
||||
class="mt-3"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.subflow-node-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 运行中边框呼吸动画 */
|
||||
@keyframes pulse-border {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(99, 102, 241, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-border {
|
||||
animation: pulse-border 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutTemplate } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || $t('ai-platform.workflow.nodes.template.label')"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="template"
|
||||
>
|
||||
<template #icon><LayoutTemplate class="size-5" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.template.desc') }}
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="bg-muted text-muted-foreground line-clamp-3 break-all rounded p-2 font-mono text-xs"
|
||||
v-text="data.template || '{{ input }}'"
|
||||
></div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { DatabaseZap } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import BaseNode from './BaseNode.vue';
|
||||
|
||||
defineProps(['selected', 'data', 'id']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseNode
|
||||
:selected="selected"
|
||||
:execution-status="data.executionStatus"
|
||||
:execution-result="data.executionResult"
|
||||
:label="data.label || 'Text-to-SQL'"
|
||||
:inputs="[{ id: 'target' }]"
|
||||
:outputs="[{ id: 'source' }]"
|
||||
:node-id="id"
|
||||
node-type="text_to_sql"
|
||||
>
|
||||
<template #icon><DatabaseZap class="size-5 text-cyan-600" /></template>
|
||||
<template #desc>
|
||||
{{ $t('ai-platform.workflow.nodes.text_to_sql.desc') }}
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- 数据库连接 -->
|
||||
<div v-if="data.db_connection" class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded border border-cyan-200 bg-cyan-50 px-1.5 py-0.5 text-[10px] font-bold text-cyan-600 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-400"
|
||||
>
|
||||
DB
|
||||
</span>
|
||||
<span class="text-muted-foreground text-[10px]">
|
||||
{{ data.db_connection }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 问题预览 -->
|
||||
<div v-if="data.user_question" class="bg-muted rounded px-2 py-1.5">
|
||||
<div
|
||||
class="text-muted-foreground line-clamp-2 text-[10px]"
|
||||
:title="data.user_question"
|
||||
>
|
||||
{{ data.user_question }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground text-[10px]">
|
||||
{{ $t('ai-platform.workflow.nodes.text_to_sql.notConfigured') }}
|
||||
</div>
|
||||
|
||||
<!-- {{ $t('ai-platform.workflow.nodes.text_to_sql.chartRecommend') }}开关 -->
|
||||
<div v-if="data.enable_chart" class="flex items-center gap-1">
|
||||
<span
|
||||
class="rounded border border-green-200 bg-green-50 px-1.5 py-0.5 text-[10px] text-green-600 dark:border-green-800 dark:bg-green-950 dark:text-green-400"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.nodes.text_to_sql.chartRecommend') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseNode>
|
||||
</template>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.appCreate.label'),
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
app_type: 'mixed',
|
||||
update_if_exists: false,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.appCreate.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.appNameOutput')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appCreate.appNamePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appCreate.appNameHint')
|
||||
}}<code v-pre>{{llm-1.app_name}}</code>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.appCodeOutput')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.code"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appCreate.appCodePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appCreate.appCodeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appDesc')">
|
||||
<SmartInput
|
||||
v-model="form.description"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appCreate.appDescPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appIcon')">
|
||||
<SmartInput
|
||||
v-model="form.icon"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appCreate.appIconPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appType')">
|
||||
<ElSelect
|
||||
v-model="form.app_type"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appCreate.appTypePlaceholder')
|
||||
"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.mixedApp')"
|
||||
value="mixed"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.formApp')"
|
||||
value="form"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.workflowApp')"
|
||||
value="workflow"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.dashboardApp')"
|
||||
value="dashboard"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.screenApp')"
|
||||
value="screen"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.appCreate.updateIfExists')"
|
||||
>
|
||||
<ElSwitch v-model="form.update_if_exists" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appCreate.updateIfExistsHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">app_id</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.createdAppId')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">app_code</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.appCodeOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">app_name</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.appNameOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">success</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appCreate.successOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.appDesign.label'),
|
||||
design_content: '',
|
||||
design_title: $t(
|
||||
'ai-platform.workflow.panels.appDesign.designTitlePlaceholder',
|
||||
),
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.appDesign.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appDesign.designContent')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.design_content"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appDesign.designContentPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appDesign.designContentHint')
|
||||
}}<code v-pre>{{llm-1.output}}</code>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.appDesign.designTitle')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.design_title"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appDesign.designTitlePlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">design_content</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appDesign.designContentOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">design_title</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appDesign.designTitleOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">confirmed</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appDesign.confirmedOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.appSettings.label'),
|
||||
app_name: '',
|
||||
home_path: '',
|
||||
logo_source: '',
|
||||
theme_builtin_type: 'default',
|
||||
theme_color_primary: '',
|
||||
app_layout: 'sidebar-nav',
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 布局选项
|
||||
const layoutOptions = [
|
||||
{
|
||||
label: $t('ai-platform.appSettings.layoutOptions.sidebar-nav'),
|
||||
value: 'sidebar-nav',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.appSettings.layoutOptions.sidebar-mixed-nav'),
|
||||
value: 'sidebar-mixed-nav',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.appSettings.layoutOptions.header-nav'),
|
||||
value: 'header-nav',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.appSettings.layoutOptions.mixed-nav'),
|
||||
value: 'mixed-nav',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.appSettings.layoutOptions.full-content'),
|
||||
value: 'full-content',
|
||||
},
|
||||
];
|
||||
|
||||
// 主题选项
|
||||
const themeOptions = [
|
||||
{
|
||||
label: $t('ai-platform.appSettings.themeOptions.default'),
|
||||
value: 'default',
|
||||
},
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.violet'), value: 'violet' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.pink'), value: 'pink' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.rose'), value: 'rose' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.sky'), value: 'sky' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.cyan'), value: 'cyan' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.green'), value: 'green' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.orange'), value: 'orange' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.yellow'), value: 'yellow' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.zinc'), value: 'zinc' },
|
||||
{
|
||||
label: $t('ai-platform.appSettings.themeOptions.neutral'),
|
||||
value: 'neutral',
|
||||
},
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.slate'), value: 'slate' },
|
||||
{ label: $t('ai-platform.appSettings.themeOptions.gray'), value: 'gray' },
|
||||
{
|
||||
label: $t('ai-platform.appSettings.themeOptions.deep-blue'),
|
||||
value: 'deep-blue',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.appSettings.themeOptions.deep-green'),
|
||||
value: 'deep-green',
|
||||
},
|
||||
];
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.appSettings.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.appSettings.appName')">
|
||||
<SmartInput
|
||||
v-model="form.app_name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.appSettings.appNamePlaceholder')"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appSettings.varRefHint') }}
|
||||
<code v-pre>{{llm-1.output}}</code>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.appSettings.homePath')">
|
||||
<SmartInput
|
||||
v-model="form.home_path"
|
||||
:current-node-id="nodeId"
|
||||
placeholder="e.g. /dashboard"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appSettings.homePathHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.appSettings.logoSource')">
|
||||
<SmartInput
|
||||
v-model="form.logo_source"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.appSettings.logoSourcePlaceholder')"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appSettings.logoHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.appSettings.builtinTheme')">
|
||||
<ElSelect v-model="form.theme_builtin_type" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in themeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.appSettings.primaryColor')">
|
||||
<SmartInput
|
||||
v-model="form.theme_color_primary"
|
||||
:current-node-id="nodeId"
|
||||
placeholder="e.g. hsl(212 100% 45%)"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appSettings.primaryColorHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.appSettings.layout')">
|
||||
<ElSelect v-model="form.app_layout" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in layoutOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">settings</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appSettings.settingsOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">confirmed</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appSettings.confirmedOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.appUpdate.label'),
|
||||
settings: '',
|
||||
application_id: '',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.appUpdate.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appUpdate.appSettingsLabel')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.settings"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appUpdate.appSettingsPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appUpdate.appSettingsHint')
|
||||
}}<code v-pre>{{app_settings-1.settings}}</code>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.appUpdate.appId')">
|
||||
<SmartInput
|
||||
v-model="form.application_id"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.appUpdate.appIdPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.appUpdate.appIdHint')
|
||||
}}<code v-pre>{{ application_id }}</code>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">update_success</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appUpdate.updateSuccess')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">config_id</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appUpdate.configId')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">update_message</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.appUpdate.updateMessage')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.choice.label'),
|
||||
question: '',
|
||||
variable_name: 'user_choice',
|
||||
options: [] as Array<{ description: string; label: string; value: string }>,
|
||||
multiple: false,
|
||||
min_select: 1,
|
||||
max_select: 1,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 初始化选项
|
||||
if (!form.value.options || form.value.options.length === 0) {
|
||||
form.value.options = [
|
||||
{
|
||||
value: 'option1',
|
||||
label: `${$t('ai-platform.workflow.panels.choice.option', { index: 1 })}`,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
value: 'option2',
|
||||
label: `${$t('ai-platform.workflow.panels.choice.option', { index: 2 })}`,
|
||||
description: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const addOption = () => {
|
||||
const idx = form.value.options.length + 1;
|
||||
form.value.options.push({
|
||||
value: `option${idx}`,
|
||||
label: `${$t('ai-platform.workflow.panels.choice.option', { index: idx })}`,
|
||||
description: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
if (form.value.options.length > 1) {
|
||||
form.value.options.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.choice.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.choice.questionContent')
|
||||
}}</span>
|
||||
<span class="text-destructive ml-1">*</span>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.question"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.choice.questionPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.variable_name" placeholder="user_choice" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.choice.outputHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span>{{ $t('ai-platform.workflow.panels.choice.optionList') }}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="addOption">
|
||||
{{ $t('ai-platform.workflow.panels.choice.addOption') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full space-y-2">
|
||||
<div
|
||||
v-for="(opt, index) in form.options"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-start gap-2 rounded border p-2"
|
||||
>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<ElInput
|
||||
v-model="opt.value"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.choice.optionValue')
|
||||
"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="opt.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.choice.displayText')
|
||||
"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<ElInput
|
||||
v-model="opt.description"
|
||||
:placeholder="$t('ai-platform.workflow.panels.choice.optionDesc')"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="form.options.length > 1"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.choice.allowMultiple')">
|
||||
<ElSwitch v-model="form.multiple" />
|
||||
</ElFormItem>
|
||||
|
||||
<div v-if="form.multiple" class="flex gap-4">
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.choice.minSelect')"
|
||||
class="flex-1"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.min_select"
|
||||
:min="1"
|
||||
:max="form.options.length"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.choice.maxSelect')"
|
||||
class="flex-1"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.max_select"
|
||||
:min="form.min_select"
|
||||
:max="form.options.length"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,269 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Maximize2, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { CodeEditor } from '#/components/zq-form/code-editor';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.code.label'),
|
||||
language: 'python3',
|
||||
code: '',
|
||||
timeout: 30,
|
||||
inputs: [] as any[],
|
||||
outputs: [] as any[],
|
||||
...props.data,
|
||||
});
|
||||
|
||||
if (!form.value.inputs) form.value.inputs = [];
|
||||
if (!form.value.outputs) form.value.outputs = [];
|
||||
|
||||
// 默认代码模板
|
||||
const defaultCode = `def main(inputs):
|
||||
# 在这里编写代码
|
||||
# inputs 包含所有上下文变量
|
||||
# 返回字典,result 字段将作为输出
|
||||
return {
|
||||
"result": "Hello " + inputs.get("name", "World")
|
||||
}`;
|
||||
|
||||
// 初始化默认代码
|
||||
if (!form.value.code) {
|
||||
form.value.code = defaultCode;
|
||||
}
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addInput = () => {
|
||||
form.value.inputs.push({ variable: '', default_value: '' });
|
||||
};
|
||||
|
||||
const removeInput = (index: number) => {
|
||||
form.value.inputs.splice(index, 1);
|
||||
};
|
||||
|
||||
const addOutput = () => {
|
||||
form.value.outputs.push({ variable: '', type: 'string' });
|
||||
};
|
||||
|
||||
const removeOutput = (index: number) => {
|
||||
form.value.outputs.splice(index, 1);
|
||||
};
|
||||
|
||||
const codeEditorVisible = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.code.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.code.timeout')">
|
||||
<ElInputNumber
|
||||
v-model="form.timeout"
|
||||
:min="1"
|
||||
:max="300"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="border-border rounded-lg border p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.common.inputVars')
|
||||
}}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="addInput">
|
||||
{{ $t('ai-platform.workflow.panels.common.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.inputs"
|
||||
:key="index"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.variable"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.variableName')"
|
||||
size="small"
|
||||
class="!w-28"
|
||||
/>
|
||||
<SmartInput
|
||||
v-model="item.default_value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.defaultValue')"
|
||||
size="small"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="removeInput(index)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.inputs.length === 0"
|
||||
class="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.code.noInputVars') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-border rounded-lg border p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.common.outputVars')
|
||||
}}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="addOutput">
|
||||
{{ $t('ai-platform.workflow.panels.common.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.outputs"
|
||||
:key="index"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.variable"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.variableName')"
|
||||
size="small"
|
||||
class="!w-28"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="removeOutput(index)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.outputs.length === 0"
|
||||
class="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.code.noOutputVars') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.code.codePython')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.workflow.panels.code.expandEditor')"
|
||||
>
|
||||
<ElButton link type="primary" @click="codeEditorVisible = true">
|
||||
<Maximize2 class="size-4" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<CodeEditor
|
||||
v-model="form.code"
|
||||
language="python"
|
||||
height="280px"
|
||||
:placeholder="$t('ai-platform.workflow.panels.code.codePlaceholder')"
|
||||
:fold-gutter="true"
|
||||
:bracket-matching="true"
|
||||
:autocompletion="true"
|
||||
:line-numbers="false"
|
||||
/>
|
||||
</div>
|
||||
</ElForm>
|
||||
|
||||
<ZqDialog
|
||||
v-model="codeEditorVisible"
|
||||
class="code-editor-dialog"
|
||||
:title="$t('ai-platform.workflow.panels.code.codePython')"
|
||||
width="960px"
|
||||
:show-footer="false"
|
||||
>
|
||||
<div class="code-editor-dialog-content">
|
||||
<CodeEditor
|
||||
v-model="form.code"
|
||||
language="python"
|
||||
height="100%"
|
||||
:placeholder="$t('ai-platform.workflow.panels.code.codePlaceholder')"
|
||||
:fold-gutter="true"
|
||||
:bracket-matching="true"
|
||||
:autocompletion="true"
|
||||
:line-numbers="true"
|
||||
/>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 代码编辑器弹窗:内容区与编辑器占满 dialog(含全屏) */
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__wrap,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__view,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__view > div,
|
||||
.code-editor-dialog .code-editor-dialog-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-editor-dialog:not(.is-fullscreen) .zq-dialog-body {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.code-editor-dialog:not(.is-fullscreen) .zq-dialog-body .el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-editor-dialog .code-editor-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-editor-dialog.is-fullscreen .zq-dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-editor-dialog.is-fullscreen .zq-dialog-body .el-scrollbar {
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,290 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Plus, Trash2, X } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const label = ref(
|
||||
props.data.label || $t('ai-platform.workflow.panels.condition.label'),
|
||||
);
|
||||
const branches = ref<any[]>(props.data.branches || []);
|
||||
|
||||
const operators = [
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.equals'),
|
||||
value: 'equals',
|
||||
group: 'compare',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.notEquals'),
|
||||
value: 'not_equals',
|
||||
group: 'compare',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.gt'),
|
||||
value: 'gt',
|
||||
group: 'number',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.gte'),
|
||||
value: 'gte',
|
||||
group: 'number',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.lt'),
|
||||
value: 'lt',
|
||||
group: 'number',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.lte'),
|
||||
value: 'lte',
|
||||
group: 'number',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.contains'),
|
||||
value: 'contains',
|
||||
group: 'string',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.notContains'),
|
||||
value: 'not_contains',
|
||||
group: 'string',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.startsWith'),
|
||||
value: 'starts_with',
|
||||
group: 'string',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.endsWith'),
|
||||
value: 'ends_with',
|
||||
group: 'string',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.isEmpty'),
|
||||
value: 'is_empty',
|
||||
group: 'empty',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.condition.isNotEmpty'),
|
||||
value: 'is_not_empty',
|
||||
group: 'empty',
|
||||
},
|
||||
];
|
||||
|
||||
if (branches.value.length === 0) {
|
||||
branches.value.push({
|
||||
id: `branch-${Date.now()}`,
|
||||
name: `${$t('ai-platform.workflow.panels.condition.branch', { index: 1 })}`,
|
||||
conditions: [],
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
[label, branches],
|
||||
([labelVal, branchesVal]) => {
|
||||
emit('update', { ...props.data, label: labelVal, branches: branchesVal });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addBranch = () => {
|
||||
branches.value.push({
|
||||
id: `branch-${Date.now()}`,
|
||||
name: `${$t('ai-platform.workflow.panels.condition.branch', { index: branches.value.length + 1 })}`,
|
||||
conditions: [],
|
||||
});
|
||||
};
|
||||
|
||||
const removeBranch = (index: number) => {
|
||||
branches.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const addCondition = (branchIndex: number) => {
|
||||
if (!branches.value[branchIndex].conditions) {
|
||||
branches.value[branchIndex].conditions = [];
|
||||
}
|
||||
branches.value[branchIndex].conditions.push({
|
||||
variable: '',
|
||||
operator: 'contains',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeCondition = (branchIndex: number, conditionIndex: number) => {
|
||||
branches.value[branchIndex].conditions.splice(conditionIndex, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.condition.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 使用提示 -->
|
||||
<ElAlert type="info" :closable="false" show-icon>
|
||||
<template #title>
|
||||
<span class="text-xs font-medium">{{
|
||||
$t('ai-platform.workflow.panels.common.usageTips')
|
||||
}}</span>
|
||||
</template>
|
||||
<ul class="mt-1 list-inside list-disc text-xs">
|
||||
<li>
|
||||
<b>{{ $t('ai-platform.workflow.panels.condition.booleanHint') }}</b>: <code class="bg-muted rounded px-1">true</code> /
|
||||
<code class="bg-muted rounded px-1">false</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{
|
||||
$t('ai-platform.workflow.panels.condition.emptyStringHint')
|
||||
}}</b>: <code class="bg-muted rounded px-1">is_empty</code> /
|
||||
<code class="bg-muted rounded px-1">is_not_empty</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ $t('ai-platform.workflow.panels.condition.mainAppHint') }}</b>:
|
||||
<code class="bg-muted rounded px-1" v-pre>{{
|
||||
start.application_id
|
||||
}}</code>
|
||||
== <code class="bg-muted rounded px-1">main</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{
|
||||
$t('ai-platform.workflow.panels.condition.numberCompareHint')
|
||||
}}</b>: {{ $t('ai-platform.workflow.panels.condition.numberCompare') }}
|
||||
</li>
|
||||
</ul>
|
||||
</ElAlert>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.condition.branchList')
|
||||
}}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="addBranch">
|
||||
{{ $t('ai-platform.workflow.panels.condition.addBranch') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(branch, index) in branches"
|
||||
:key="branch.id"
|
||||
class="border-border bg-muted relative rounded border p-3"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-blue-600"
|
||||
>IF {{ index + 1 }}</span
|
||||
>
|
||||
<ElButton
|
||||
v-if="branches.length > 1"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="removeBranch(index)"
|
||||
/>
|
||||
</div>
|
||||
<ElInput
|
||||
v-model="branch.name"
|
||||
:placeholder="$t('ai-platform.workflow.panels.parallel.branchName')"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<!-- Conditions -->
|
||||
<div class="border-border mt-3 space-y-2 border-t pt-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-[10px] font-medium">{{
|
||||
$t('ai-platform.workflow.panels.condition.conditions')
|
||||
}}</span>
|
||||
<ElButton
|
||||
size="small"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
class="!text-[10px]"
|
||||
@click="addCondition(index)"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addCondition') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!branch.conditions || branch.conditions.length === 0"
|
||||
class="text-muted-foreground text-center text-[10px]"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.condition.noCondition') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(cond, cIdx) in branch.conditions"
|
||||
:key="cIdx"
|
||||
class="flex flex-col gap-2 rounded border bg-white p-2 shadow-sm"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<SmartInput
|
||||
v-model="cond.variable"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.variable')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="X"
|
||||
@click="removeCondition(index, cIdx as number)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<ElSelect
|
||||
v-model="cond.operator"
|
||||
size="small"
|
||||
style="width: 100px; flex-shrink: 0"
|
||||
>
|
||||
<ElOption
|
||||
v-for="op in operators"
|
||||
:key="op.value"
|
||||
:label="op.label"
|
||||
:value="op.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<SmartInput
|
||||
v-if="!['is_empty', 'is_not_empty'].includes(cond.operator)"
|
||||
v-model="cond.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.value')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="border-border bg-muted text-muted-foreground rounded border p-3 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.condition.elseBranch') }}
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getModelListApi } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.confirm.label'),
|
||||
title: $t('ai-platform.workflow.panels.confirm.titlePlaceholder'),
|
||||
content: '',
|
||||
confirm_text: $t('ai-platform.workflow.panels.confirm.confirmPlaceholder'),
|
||||
cancel_text: $t('ai-platform.workflow.panels.confirm.cancelPlaceholder'),
|
||||
variable_name: 'confirmed',
|
||||
use_llm_intent: false,
|
||||
llm_model_id: '',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 模型列表
|
||||
const modelList = ref<any[]>([]);
|
||||
const loadingModels = ref(false);
|
||||
|
||||
// 加载模型列表
|
||||
const loadModels = async () => {
|
||||
loadingModels.value = true;
|
||||
try {
|
||||
const res = await getModelListApi({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
is_active: true,
|
||||
});
|
||||
modelList.value = res.items || [];
|
||||
} catch (error) {
|
||||
console.error('加载模型列表失败', error);
|
||||
} finally {
|
||||
loadingModels.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (form.value.use_llm_intent) {
|
||||
loadModels();
|
||||
}
|
||||
});
|
||||
|
||||
// 当启用 LLM 意图识别时加载模型列表
|
||||
watch(
|
||||
() => form.value.use_llm_intent,
|
||||
(val) => {
|
||||
if (val && modelList.value.length === 0) {
|
||||
loadModels();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.confirm.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.confirm.dialogTitle')">
|
||||
<SmartInput
|
||||
v-model="form.title"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.confirm.titlePlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<span>{{ $t('ai-platform.workflow.panels.confirm.content') }}</span>
|
||||
<span class="text-destructive ml-1">*</span>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.content"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.confirm.contentPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.confirm.contentHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.confirm.confirmBtnText')"
|
||||
class="flex-1"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.confirm_text"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.confirm.confirmPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.confirm.cancelBtnText')"
|
||||
class="flex-1"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.cancel_text"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.confirm.cancelPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.variable_name" placeholder="confirmed" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.confirm.outputHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- LLM 意图识别配置 -->
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.confirm.smartIntent')
|
||||
}}</span>
|
||||
<ElSwitch v-model="form.use_llm_intent" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.confirm.smartIntentHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="form.use_llm_intent"
|
||||
:label="$t('ai-platform.workflow.panels.confirm.intentModel')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.llm_model_id"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.selectModel')"
|
||||
:loading="loadingModels"
|
||||
filterable
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="model in modelList"
|
||||
:key="model.id"
|
||||
:label="`${model.display_name} (${model.provider_name})`"
|
||||
:value="model.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.confirm.intentModelHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
nodeId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.label'),
|
||||
name: '',
|
||||
code: '',
|
||||
category: 'dashboard',
|
||||
description: '',
|
||||
sort: 0,
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(newVal) => {
|
||||
emit('update', newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const categoryOptions = [
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catDashboard'),
|
||||
value: 'dashboard',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catPortal'),
|
||||
value: 'portal',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catDataboard'),
|
||||
value: 'databoard',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catOther'),
|
||||
value: 'other',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<ElForm :model="form" label-position="top" size="small">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardBasicInfo.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardBasicInfo.dashboardName')
|
||||
"
|
||||
required
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.name"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardBasicInfo.dashboardNamePlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardBasicInfo.dashboardCode')
|
||||
"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.code"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardBasicInfo.dashboardCodePlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardBasicInfo.category')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.category"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardBasicInfo.categoryPlaceholder',
|
||||
)
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in categoryOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardBasicInfo.description')
|
||||
"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardBasicInfo.descPlaceholder')
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardBasicInfo.sort')"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElSwitch } from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
nodeId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dashboardCreate.label'),
|
||||
dashboard_basic_info: '',
|
||||
page_config: '',
|
||||
application_id: '',
|
||||
update_if_exists: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(newVal) => {
|
||||
emit('update', newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<ElForm :model="form" label-position="top" size="small">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardCreate.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardCreate.basicInfo')"
|
||||
required
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.dashboard_basic_info"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardCreate.basicInfoPlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardCreate.pageConfig')"
|
||||
required
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.page_config"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardCreate.pageConfigPlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardCreate.appId')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.application_id"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardCreate.appIdPlaceholder')
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardCreate.updateIfExists')
|
||||
"
|
||||
>
|
||||
<ElSwitch v-model="form.update_if_exists" />
|
||||
<span class="text-muted-foreground ml-2 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.dashboardCreate.updateIfExistsHint')
|
||||
}}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="bg-muted/50 rounded-lg p-3">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<p class="mb-2 font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}:
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1">
|
||||
<li>
|
||||
<code>dashboard_id</code> -
|
||||
{{
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardCreate.dashboardIdOutput',
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<code>dashboard_code</code> -
|
||||
{{
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardCreate.dashboardCodeOutput',
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<code>page_meta</code> -
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.dashboardCreate.pageMetaOutput')
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
nodeId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dashboardDesign.label'),
|
||||
dashboard_code: '',
|
||||
design_title: $t('ai-platform.workflow.panels.dashboardDesign.designTitle'),
|
||||
design_suggestion: '',
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(newVal) => {
|
||||
emit('update', newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<ElForm :model="form" label-position="top" size="small">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardDesign.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardDesign.dashboardCode')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.dashboard_code"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardDesign.dashboardCodePlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardDesign.designTitleLabel')
|
||||
"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.design_title"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardDesign.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.dashboardDesign.designSuggestion')
|
||||
"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.design_suggestion"
|
||||
type="textarea"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardDesign.designSuggestionPlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="bg-muted/50 rounded-lg p-3">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<p class="mb-2 font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.usage') }}:
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1">
|
||||
<li>
|
||||
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip1') }}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip2') }}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip3') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
nodeId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dashboardPublish.label'),
|
||||
dashboard_id: '',
|
||||
menu_name: '',
|
||||
menu_parent_id: '',
|
||||
menu_icon: 'lucide:layout-dashboard',
|
||||
menu_order: 0,
|
||||
application_id: '',
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(newVal) => {
|
||||
emit('update', newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<ElForm :model="form" label-position="top" size="small">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardPublish.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.dashboardId')"
|
||||
required
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.dashboard_id"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardPublish.dashboardIdPlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuName')"
|
||||
required
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.menu_name"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardPublish.menuNamePlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.parentMenuId')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.menu_parent_id"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardPublish.parentMenuIdPlaceholder',
|
||||
)
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuIcon')"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.menu_icon"
|
||||
placeholder="lucide:layout-dashboard"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuSort')"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.menu_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.dashboardPublish.appId')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.application_id"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dashboardPublish.appIdPlaceholder')
|
||||
"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="bg-muted/50 rounded-lg p-3">
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<p class="mb-2 font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}:
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1">
|
||||
<li>
|
||||
<code>menu_id</code> -
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.dashboardPublish.menuIdOutput')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<code>route_path</code> -
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.dashboardPublish.routePathOutput')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<code>publish_result</code> -
|
||||
{{
|
||||
$t(
|
||||
'ai-platform.workflow.panels.dashboardPublish.publishResultOutput',
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectedTable } from '../components/TableSelectDialog.vue';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Database, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
import DbConfigSummary from '../components/DbConfigSummary.vue';
|
||||
import TableSelectDialog from '../components/TableSelectDialog.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dbDelete.label'),
|
||||
operation: 'delete',
|
||||
table: '',
|
||||
db_config: null as null | {
|
||||
database: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
schema?: string;
|
||||
},
|
||||
table_fields: [] as Array<{
|
||||
isPrimaryKey: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
}>,
|
||||
where_conditions: [] as Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>,
|
||||
output_variable: 'db_result',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 表选择对话框
|
||||
const tableSelectVisible = ref(false);
|
||||
|
||||
// 确保数组存在
|
||||
if (!form.value.where_conditions) form.value.where_conditions = [];
|
||||
if (!form.value.table_fields) form.value.table_fields = [];
|
||||
|
||||
// 条件操作符选项
|
||||
const operatorOptions = [
|
||||
{ value: '=', label: $t('ai-platform.workflow.panels.dbQuery.eqOp') },
|
||||
{ value: '!=', label: $t('ai-platform.workflow.panels.dbQuery.neOp') },
|
||||
{ value: '>', label: $t('ai-platform.workflow.panels.dbQuery.gtOp') },
|
||||
{ value: '>=', label: $t('ai-platform.workflow.panels.dbQuery.gteOp') },
|
||||
{ value: '<', label: $t('ai-platform.workflow.panels.dbQuery.ltOp') },
|
||||
{ value: '<=', label: $t('ai-platform.workflow.panels.dbQuery.lteOp') },
|
||||
{ value: 'in', label: $t('ai-platform.workflow.panels.dbQuery.inOp') },
|
||||
];
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addCondition = () => {
|
||||
form.value.where_conditions.push({ field: '', operator: '=', value: '' });
|
||||
};
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
form.value.where_conditions.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理表选择
|
||||
const handleTableSelect = (table: SelectedTable) => {
|
||||
form.value.table = table.tableName;
|
||||
form.value.db_config = {
|
||||
dbName: table.dbName,
|
||||
database: table.database,
|
||||
schema: table.schema,
|
||||
dbType: table.dbType,
|
||||
};
|
||||
form.value.table_fields = table.fields.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
isPrimaryKey: f.isPrimaryKey,
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElAlert type="warning" :closable="false" show-icon>
|
||||
<template #title>
|
||||
<span class="text-xs font-medium">{{
|
||||
$t('ai-platform.workflow.panels.dbDelete.dangerWarning')
|
||||
}}</span>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbDelete.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.targetTable')">
|
||||
<div class="flex w-full gap-2">
|
||||
<ElInput
|
||||
v-model="form.table"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.common.tablePlaceholder')
|
||||
"
|
||||
readonly
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<Database class="text-muted-foreground h-4 w-4" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="tableSelectVisible = true"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.selectTable') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<DbConfigSummary
|
||||
:db-config="form.db_config"
|
||||
show-default-write-warning
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表字段参考 -->
|
||||
<div v-if="form.table_fields.length > 0">
|
||||
<div class="text-foreground mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.tableFieldsRef') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<ElTag
|
||||
v-for="field in form.table_fields"
|
||||
:key="field.name"
|
||||
:type="field.isPrimaryKey ? 'warning' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ field.name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除条件 -->
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.dbDelete.deleteConditions') }}
|
||||
<span class="text-red-500">*</span>
|
||||
</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addCondition"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addCondition') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.where_conditions"
|
||||
:key="index"
|
||||
class="flex items-center gap-2 rounded border border-red-100 bg-red-50 p-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.field')"
|
||||
size="small"
|
||||
class="w-24"
|
||||
/>
|
||||
<ElSelect v-model="item.operator" size="small" class="w-24">
|
||||
<ElOption
|
||||
v-for="op in operatorOptions"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
:label="op.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
<SmartInput
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.value')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="form.where_conditions.length === 0" class="py-4 text-center">
|
||||
<ElAlert type="error" :closable="false" show-icon>
|
||||
<template #title>
|
||||
{{ $t('ai-platform.workflow.panels.dbDelete.mustAddCondition') }}
|
||||
</template>
|
||||
</ElAlert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" placeholder="db_result" />
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表选择对话框 -->
|
||||
<TableSelectDialog
|
||||
v-model:visible="tableSelectVisible"
|
||||
:current-table="form.table"
|
||||
@select="handleTableSelect"
|
||||
/>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,365 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectedTable } from '../components/TableSelectDialog.vue';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Database, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
import DbConfigSummary from '../components/DbConfigSummary.vue';
|
||||
import TableSelectDialog from '../components/TableSelectDialog.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dbInsert.label'),
|
||||
operation: 'insert',
|
||||
table: '',
|
||||
db_config: null as null | {
|
||||
database: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
schema?: string;
|
||||
},
|
||||
table_fields: [] as Array<{
|
||||
isPrimaryKey: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
}>,
|
||||
field_mapping: [] as Array<{ field: string; value: string }>,
|
||||
where_conditions: [] as Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>,
|
||||
upsert: false,
|
||||
output_variable: 'db_result',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 表选择对话框
|
||||
const tableSelectVisible = ref(false);
|
||||
|
||||
// 确保数组存在
|
||||
if (!form.value.field_mapping) form.value.field_mapping = [];
|
||||
if (!form.value.where_conditions) form.value.where_conditions = [];
|
||||
if (!form.value.table_fields) form.value.table_fields = [];
|
||||
|
||||
// 将对象格式转换为数组格式(兼容旧数据)
|
||||
if (props.data.field_mapping && !Array.isArray(props.data.field_mapping)) {
|
||||
form.value.field_mapping = Object.entries(props.data.field_mapping).map(
|
||||
([field, value]) => ({
|
||||
field,
|
||||
value: value as string,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 条件操作符选项
|
||||
const operatorOptions = [
|
||||
{ value: '=', label: $t('ai-platform.workflow.panels.dbQuery.eqOp') },
|
||||
{ value: '!=', label: $t('ai-platform.workflow.panels.dbQuery.neOp') },
|
||||
];
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
// 将数组格式转换为对象格式存储
|
||||
const fieldMappingObj: Record<string, string> = {};
|
||||
val.field_mapping.forEach((item: any) => {
|
||||
if (item.field) {
|
||||
fieldMappingObj[item.field] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
emit('update', {
|
||||
...val,
|
||||
operation: val.upsert ? 'upsert' : 'insert',
|
||||
field_mapping: fieldMappingObj,
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 添加字段映射
|
||||
const addFieldMapping = () => {
|
||||
form.value.field_mapping.push({ field: '', value: '' });
|
||||
};
|
||||
|
||||
// 删除字段映射
|
||||
const removeFieldMapping = (index: number) => {
|
||||
form.value.field_mapping.splice(index, 1);
|
||||
};
|
||||
|
||||
// 添加条件
|
||||
const addCondition = () => {
|
||||
form.value.where_conditions.push({ field: '', operator: '=', value: '' });
|
||||
};
|
||||
|
||||
// 删除条件
|
||||
const removeCondition = (index: number) => {
|
||||
form.value.where_conditions.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理表选择
|
||||
const handleTableSelect = (table: SelectedTable) => {
|
||||
form.value.table = table.tableName;
|
||||
form.value.db_config = {
|
||||
dbName: table.dbName,
|
||||
database: table.database,
|
||||
schema: table.schema,
|
||||
dbType: table.dbType,
|
||||
};
|
||||
form.value.table_fields = table.fields.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
isPrimaryKey: f.isPrimaryKey,
|
||||
}));
|
||||
};
|
||||
|
||||
// 快速添加字段映射
|
||||
const addFieldFromTable = (fieldName: string) => {
|
||||
// 检查是否已存在
|
||||
if (
|
||||
form.value.field_mapping.some(
|
||||
(f: { field: string }) => f.field === fieldName,
|
||||
)
|
||||
)
|
||||
return;
|
||||
form.value.field_mapping.push({ field: fieldName, value: '' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbInsert.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.targetTable')">
|
||||
<div class="flex w-full gap-2">
|
||||
<ElInput
|
||||
v-model="form.table"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.common.tablePlaceholder')
|
||||
"
|
||||
readonly
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<Database class="text-muted-foreground h-4 w-4" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="tableSelectVisible = true"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.selectTable') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<DbConfigSummary
|
||||
:db-config="form.db_config"
|
||||
show-default-write-warning
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表字段快捷选择 -->
|
||||
<div v-if="form.table_fields.length > 0" class="mb-4">
|
||||
<div class="text-foreground mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.tableFields') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<ElTag
|
||||
v-for="field in form.table_fields"
|
||||
:key="field.name"
|
||||
:type="field.isPrimaryKey ? 'warning' : 'info'"
|
||||
size="small"
|
||||
class="cursor-pointer"
|
||||
@click="addFieldFromTable(field.name)"
|
||||
>
|
||||
{{ field.name }}
|
||||
<span class="ml-1 text-[10px] opacity-60">{{ field.type }}</span>
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.clickFieldToAdd') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段映射 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.common.fieldMapping')
|
||||
}}</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addFieldMapping"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addField') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.field_mapping"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-center gap-2 rounded border p-2"
|
||||
>
|
||||
<ElSelect
|
||||
v-if="form.table_fields.length > 0"
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.selectField')"
|
||||
size="small"
|
||||
class="max-w-36"
|
||||
filterable
|
||||
allow-create
|
||||
>
|
||||
<ElOption
|
||||
v-for="f in form.table_fields"
|
||||
:key="f.name"
|
||||
:value="f.name"
|
||||
:label="f.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.dbField')"
|
||||
size="small"
|
||||
class="min-w-32"
|
||||
/>
|
||||
<span class="text-muted-foreground">←</span>
|
||||
<SmartInput
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.varValue')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeFieldMapping(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.field_mapping.length === 0"
|
||||
class="text-muted-foreground py-4 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.clickAddField') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UPSERT 选项 -->
|
||||
<ElFormItem>
|
||||
<ElCheckbox v-model="form.upsert">
|
||||
{{ $t('ai-platform.workflow.panels.common.upsert') }}
|
||||
</ElCheckbox>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.upsertHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- UPSERT 条件 -->
|
||||
<div v-if="form.upsert" class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.judgeCondition') }}
|
||||
<span class="text-red-500">*</span>
|
||||
</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addCondition"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addCondition') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.where_conditions"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-center gap-2 rounded border p-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.field')"
|
||||
size="small"
|
||||
class="w-24"
|
||||
/>
|
||||
<ElSelect v-model="item.operator" size="small" class="w-20">
|
||||
<ElOption
|
||||
v-for="op in operatorOptions"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
:label="op.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
<SmartInput
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.value')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.where_conditions.length === 0"
|
||||
class="py-4 text-center text-xs text-red-500 dark:text-red-400"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addJudgeConditionHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" placeholder="db_result" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.insertResultSavedTo') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表选择对话框 -->
|
||||
<TableSelectDialog
|
||||
v-model:visible="tableSelectVisible"
|
||||
:current-table="form.table"
|
||||
@select="handleTableSelect"
|
||||
/>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectedTable } from '../components/TableSelectDialog.vue';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Database, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
import DbConfigSummary from '../components/DbConfigSummary.vue';
|
||||
import TableSelectDialog from '../components/TableSelectDialog.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dbQuery.label'),
|
||||
operation: 'select',
|
||||
table: '',
|
||||
db_config: null as null | {
|
||||
database: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
schema?: string;
|
||||
},
|
||||
table_fields: [] as Array<{
|
||||
isPrimaryKey: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
}>,
|
||||
where_conditions: [] as Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>,
|
||||
return_fields: '*',
|
||||
limit: 100,
|
||||
order_by: '',
|
||||
output_variable: 'db_result',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 表选择对话框
|
||||
const tableSelectVisible = ref(false);
|
||||
|
||||
// 确保数组存在
|
||||
if (!form.value.where_conditions) form.value.where_conditions = [];
|
||||
if (!form.value.table_fields) form.value.table_fields = [];
|
||||
|
||||
// 条件操作符选项
|
||||
const operatorOptions = [
|
||||
{ value: '=', label: $t('ai-platform.workflow.panels.dbQuery.eqOp') },
|
||||
{ value: '!=', label: $t('ai-platform.workflow.panels.dbQuery.neOp') },
|
||||
{ value: '>', label: $t('ai-platform.workflow.panels.dbQuery.gtOp') },
|
||||
{ value: '>=', label: $t('ai-platform.workflow.panels.dbQuery.gteOp') },
|
||||
{ value: '<', label: $t('ai-platform.workflow.panels.dbQuery.ltOp') },
|
||||
{ value: '<=', label: $t('ai-platform.workflow.panels.dbQuery.lteOp') },
|
||||
{ value: 'like', label: $t('ai-platform.workflow.panels.dbQuery.likeOp') },
|
||||
{ value: 'in', label: $t('ai-platform.workflow.panels.dbQuery.inOp') },
|
||||
{
|
||||
value: 'is_null',
|
||||
label: $t('ai-platform.workflow.panels.dbQuery.isNullOp'),
|
||||
},
|
||||
{
|
||||
value: 'is_not_null',
|
||||
label: $t('ai-platform.workflow.panels.dbQuery.isNotNullOp'),
|
||||
},
|
||||
];
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
// 将 return_fields 字符串转换为数组
|
||||
let returnFields = val.return_fields;
|
||||
if (typeof returnFields === 'string') {
|
||||
returnFields =
|
||||
returnFields === '*' || returnFields.trim() === ''
|
||||
? ['*']
|
||||
: returnFields
|
||||
.split(',')
|
||||
.map((f: string) => f.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
emit('update', {
|
||||
...val,
|
||||
return_fields: returnFields,
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addCondition = () => {
|
||||
form.value.where_conditions.push({ field: '', operator: '=', value: '' });
|
||||
};
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
form.value.where_conditions.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理表选择
|
||||
const handleTableSelect = (table: SelectedTable) => {
|
||||
form.value.table = table.tableName;
|
||||
form.value.db_config = {
|
||||
dbName: table.dbName,
|
||||
database: table.database,
|
||||
schema: table.schema,
|
||||
dbType: table.dbType,
|
||||
};
|
||||
form.value.table_fields = table.fields.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
isPrimaryKey: f.isPrimaryKey,
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbQuery.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.targetTable')">
|
||||
<div class="flex w-full gap-2">
|
||||
<ElInput
|
||||
v-model="form.table"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.common.tablePlaceholder')
|
||||
"
|
||||
readonly
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<Database class="text-muted-foreground h-4 w-4" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="tableSelectVisible = true"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.selectTable') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<DbConfigSummary :db-config="form.db_config" />
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表字段快捷选择 -->
|
||||
<div v-if="form.table_fields.length > 0" class="mb-4">
|
||||
<div class="text-foreground mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.availableFields') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<ElTag
|
||||
v-for="field in form.table_fields"
|
||||
:key="field.name"
|
||||
:type="field.isPrimaryKey ? 'warning' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ field.name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查询条件 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.common.queryConditions')
|
||||
}}</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addCondition"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addCondition') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.where_conditions"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-center gap-2 rounded border p-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.field')"
|
||||
size="small"
|
||||
class="w-24"
|
||||
/>
|
||||
<ElSelect v-model="item.operator" size="small" class="w-28">
|
||||
<ElOption
|
||||
v-for="op in operatorOptions"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
:label="op.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
<SmartInput
|
||||
v-if="!['is_null', 'is_not_null'].includes(item.operator)"
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.value')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.where_conditions.length === 0"
|
||||
class="text-muted-foreground py-2 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.noConditionQueryAll') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.returnFields')">
|
||||
<ElInput
|
||||
v-model="form.return_fields"
|
||||
placeholder="* or id,name,created_at"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.returnFieldsHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.orderBy')">
|
||||
<ElInput v-model="form.order_by" placeholder="e.g. created_at DESC" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.limitRows')">
|
||||
<ElInputNumber v-model="form.limit" :min="1" :max="1000" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" placeholder="db_result" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.resultSavedTo') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表选择对话框 -->
|
||||
<TableSelectDialog
|
||||
v-model:visible="tableSelectVisible"
|
||||
:current-table="form.table"
|
||||
@select="handleTableSelect"
|
||||
/>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,364 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Maximize2, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
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';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
import DbConfigSummary from '../components/DbConfigSummary.vue';
|
||||
import DbConnectionSelect from '../components/DbConnectionSelect.vue';
|
||||
|
||||
interface SqlParam {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string; nodeType?: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const paramTypeOptions = getParamTypeOptions();
|
||||
|
||||
const initialDbConfig = {
|
||||
dbName: 'default',
|
||||
dbType: 'postgresql',
|
||||
...(props.data?.db_config || {}),
|
||||
};
|
||||
|
||||
function normalizeParam(raw: Partial<SqlParam> & { default?: string }): SqlParam {
|
||||
return {
|
||||
name: raw.name ?? '',
|
||||
type: raw.type ?? 'string',
|
||||
value: raw.value ?? raw.default ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dbSql.label'),
|
||||
sql_type: 'query',
|
||||
sql: '',
|
||||
params: [] as SqlParam[],
|
||||
output_variable: 'sql_result',
|
||||
...props.data,
|
||||
db_config: initialDbConfig,
|
||||
});
|
||||
|
||||
form.value.params = (form.value.params || []).map(normalizeParam);
|
||||
if (!form.value.db_config) {
|
||||
form.value.db_config = initialDbConfig;
|
||||
}
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const sqlEditorVisible = ref(false);
|
||||
|
||||
const addParam = () => {
|
||||
form.value.params.push({
|
||||
name: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeParam = (index: number) => {
|
||||
form.value.params.splice(index, 1);
|
||||
};
|
||||
|
||||
const sqlExamples = [
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dbSql.queryExample'),
|
||||
value: 'SELECT * FROM users WHERE status = :status LIMIT :limit',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dbSql.insertExample'),
|
||||
value:
|
||||
'INSERT INTO logs (user_id, action, created_at) VALUES (:user_id, :action, NOW())',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dbSql.updateExample'),
|
||||
value: 'UPDATE orders SET status = :status WHERE id = :id',
|
||||
},
|
||||
{
|
||||
label: $t('ai-platform.workflow.panels.dbSql.statsExample'),
|
||||
value: 'SELECT COUNT(*) as total, status FROM orders GROUP BY status',
|
||||
},
|
||||
];
|
||||
|
||||
const insertExample = (sql: string) => {
|
||||
form.value.sql = sql;
|
||||
};
|
||||
|
||||
function handleConnectionChange(payload: { dbName: string; dbType: string }) {
|
||||
form.value.db_config = {
|
||||
...form.value.db_config,
|
||||
dbName: payload.dbName,
|
||||
dbType: payload.dbType,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElAlert type="warning" :closable="false" show-icon>
|
||||
<template #title>
|
||||
<span class="text-xs font-medium">{{
|
||||
$t('ai-platform.workflow.panels.dbSql.cautionWarning')
|
||||
}}</span>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbSql.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<DbConnectionSelect
|
||||
v-model="form.db_config.dbName"
|
||||
:write-mode="form.sql_type === 'execute'"
|
||||
@change="handleConnectionChange"
|
||||
/>
|
||||
<DbConfigSummary
|
||||
:db-config="form.db_config"
|
||||
:show-default-write-warning="form.sql_type === 'execute'"
|
||||
/>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.dbSql.execType')">
|
||||
<ElRadioGroup v-model="form.sql_type">
|
||||
<ElRadioButton value="query">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.queryType') }}
|
||||
</ElRadioButton>
|
||||
<ElRadioButton value="execute">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.executeType') }}
|
||||
</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.dbSql.sqlStatement')
|
||||
}}</span>
|
||||
<ElTooltip
|
||||
:content="$t('ai-platform.workflow.panels.code.expandEditor')"
|
||||
>
|
||||
<ElButton link type="primary" @click="sqlEditorVisible = true">
|
||||
<Maximize2 class="size-4" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<CodeEditor
|
||||
v-model="form.sql"
|
||||
language="sql"
|
||||
height="280px"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbSql.sqlPlaceholder')"
|
||||
:fold-gutter="true"
|
||||
:bracket-matching="true"
|
||||
:autocompletion="true"
|
||||
:line-numbers="false"
|
||||
class="font-mono"
|
||||
/>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-1">
|
||||
<span class="text-muted-foreground mr-2 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.examples') }}:
|
||||
</span>
|
||||
<button
|
||||
v-for="example in sqlExamples"
|
||||
:key="example.label"
|
||||
type="button"
|
||||
class="text-xs text-blue-500 hover:text-blue-700 hover:underline"
|
||||
@click="insertExample(example.value)"
|
||||
>
|
||||
{{ example.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.sqlParamHint') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.dbSql.paramList')
|
||||
}}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="addParam">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.addParam') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.params.length === 0"
|
||||
class="text-muted-foreground py-4 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.noParams') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-3 space-y-3">
|
||||
<div
|
||||
v-for="(param, index) in form.params"
|
||||
:key="index"
|
||||
class="border-border bg-muted rounded border p-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElInput
|
||||
v-model="param.name"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbSql.paramName')"
|
||||
size="small"
|
||||
style="width: 138px"
|
||||
/>
|
||||
<ElSelect
|
||||
v-model="param.type"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbSql.paramType')"
|
||||
size="small"
|
||||
style="width: 158px"
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in paramTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="db-sql-param-value shrink-0">
|
||||
<SmartInput
|
||||
v-model="param.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.dbSql.paramValuePlaceholder')
|
||||
"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="removeParam(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted-foreground mt-2 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.dbSql.paramsHint') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" placeholder="sql_result" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
<span v-if="form.sql_type === 'query'">{{
|
||||
$t('ai-platform.workflow.panels.dbSql.queryResultHint')
|
||||
}}</span>
|
||||
<span v-else>{{
|
||||
$t('ai-platform.workflow.panels.dbSql.affectedRowsHint')
|
||||
}}</span>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<ZqDialog
|
||||
v-model="sqlEditorVisible"
|
||||
class="code-editor-dialog"
|
||||
:title="$t('ai-platform.workflow.panels.dbSql.sqlStatement')"
|
||||
width="960px"
|
||||
:show-footer="false"
|
||||
>
|
||||
<div class="code-editor-dialog-content">
|
||||
<CodeEditor
|
||||
v-model="form.sql"
|
||||
language="sql"
|
||||
height="100%"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbSql.sqlPlaceholder')"
|
||||
:fold-gutter="true"
|
||||
:bracket-matching="true"
|
||||
:autocompletion="true"
|
||||
:line-numbers="true"
|
||||
class="font-mono"
|
||||
/>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.db-sql-param-value {
|
||||
width: 142px;
|
||||
}
|
||||
|
||||
.db-sql-param-value :deep(> div) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 与 CodePanel 共用:编辑器弹窗占满 dialog(含全屏) */
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__wrap,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__view,
|
||||
.code-editor-dialog .zq-dialog-body .el-scrollbar__view > div,
|
||||
.code-editor-dialog .code-editor-dialog-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-editor-dialog:not(.is-fullscreen) .zq-dialog-body {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.code-editor-dialog:not(.is-fullscreen) .zq-dialog-body .el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-editor-dialog .code-editor-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-editor-dialog.is-fullscreen .zq-dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-editor-dialog.is-fullscreen .zq-dialog-body .el-scrollbar {
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectedTable } from '../components/TableSelectDialog.vue';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Database, Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
import DbConfigSummary from '../components/DbConfigSummary.vue';
|
||||
import TableSelectDialog from '../components/TableSelectDialog.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.dbUpdate.label'),
|
||||
operation: 'update',
|
||||
table: '',
|
||||
db_config: null as null | {
|
||||
database: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
schema?: string;
|
||||
},
|
||||
table_fields: [] as Array<{
|
||||
isPrimaryKey: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
}>,
|
||||
field_mapping: [] as Array<{ field: string; value: string }>,
|
||||
where_conditions: [] as Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>,
|
||||
output_variable: 'db_result',
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 表选择对话框
|
||||
const tableSelectVisible = ref(false);
|
||||
|
||||
// 确保数组存在
|
||||
if (!form.value.field_mapping) form.value.field_mapping = [];
|
||||
if (!form.value.where_conditions) form.value.where_conditions = [];
|
||||
if (!form.value.table_fields) form.value.table_fields = [];
|
||||
|
||||
// 将对象格式转换为数组格式(兼容旧数据)
|
||||
if (props.data.field_mapping && !Array.isArray(props.data.field_mapping)) {
|
||||
form.value.field_mapping = Object.entries(props.data.field_mapping).map(
|
||||
([field, value]) => ({
|
||||
field,
|
||||
value: value as string,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 条件操作符选项
|
||||
const operatorOptions = [
|
||||
{ value: '=', label: $t('ai-platform.workflow.panels.dbQuery.eqOp') },
|
||||
{ value: '!=', label: $t('ai-platform.workflow.panels.dbQuery.neOp') },
|
||||
{ value: '>', label: $t('ai-platform.workflow.panels.dbQuery.gtOp') },
|
||||
{ value: '>=', label: $t('ai-platform.workflow.panels.dbQuery.gteOp') },
|
||||
{ value: '<', label: $t('ai-platform.workflow.panels.dbQuery.ltOp') },
|
||||
{ value: '<=', label: $t('ai-platform.workflow.panels.dbQuery.lteOp') },
|
||||
{ value: 'like', label: $t('ai-platform.workflow.panels.dbQuery.likeOp') },
|
||||
{ value: 'in', label: $t('ai-platform.workflow.panels.dbQuery.inOp') },
|
||||
];
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
const fieldMappingObj: Record<string, string> = {};
|
||||
val.field_mapping.forEach((item: any) => {
|
||||
if (item.field) {
|
||||
fieldMappingObj[item.field] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
emit('update', {
|
||||
...val,
|
||||
field_mapping: fieldMappingObj,
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addFieldMapping = () => {
|
||||
form.value.field_mapping.push({ field: '', value: '' });
|
||||
};
|
||||
|
||||
const removeFieldMapping = (index: number) => {
|
||||
form.value.field_mapping.splice(index, 1);
|
||||
};
|
||||
|
||||
const addCondition = () => {
|
||||
form.value.where_conditions.push({ field: '', operator: '=', value: '' });
|
||||
};
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
form.value.where_conditions.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理表选择
|
||||
const handleTableSelect = (table: SelectedTable) => {
|
||||
form.value.table = table.tableName;
|
||||
form.value.db_config = {
|
||||
dbName: table.dbName,
|
||||
database: table.database,
|
||||
schema: table.schema,
|
||||
dbType: table.dbType,
|
||||
};
|
||||
form.value.table_fields = table.fields.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
isPrimaryKey: f.isPrimaryKey,
|
||||
}));
|
||||
};
|
||||
|
||||
// 快速添加字段映射
|
||||
const addFieldFromTable = (fieldName: string) => {
|
||||
if (
|
||||
form.value.field_mapping.some(
|
||||
(f: { field: string }) => f.field === fieldName,
|
||||
)
|
||||
)
|
||||
return;
|
||||
form.value.field_mapping.push({ field: fieldName, value: '' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.dbUpdate.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.targetTable')">
|
||||
<div class="flex w-full gap-2">
|
||||
<ElInput
|
||||
v-model="form.table"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.common.tablePlaceholder')
|
||||
"
|
||||
readonly
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<Database class="text-muted-foreground h-4 w-4" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="tableSelectVisible = true"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.selectTable') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<DbConfigSummary
|
||||
:db-config="form.db_config"
|
||||
show-default-write-warning
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表字段快捷选择 -->
|
||||
<div v-if="form.table_fields.length > 0" class="mb-4">
|
||||
<div class="text-foreground mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.common.tableFields') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<ElTag
|
||||
v-for="field in form.table_fields"
|
||||
:key="field.name"
|
||||
:type="field.isPrimaryKey ? 'warning' : 'info'"
|
||||
size="small"
|
||||
class="cursor-pointer"
|
||||
@click="addFieldFromTable(field.name)"
|
||||
>
|
||||
{{ field.name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.common.clickFieldToAdd') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新字段 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.dbUpdate.updateFields')
|
||||
}}</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addFieldMapping"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addField') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.field_mapping"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-center gap-2 rounded border p-2"
|
||||
>
|
||||
<ElSelect
|
||||
v-if="form.table_fields.length > 0"
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.selectField')"
|
||||
size="small"
|
||||
class="w-32"
|
||||
filterable
|
||||
allow-create
|
||||
>
|
||||
<ElOption
|
||||
v-for="f in form.table_fields"
|
||||
:key="f.name"
|
||||
:value="f.name"
|
||||
:label="f.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.dbField')"
|
||||
size="small"
|
||||
class="w-32"
|
||||
/>
|
||||
<span class="text-muted-foreground">←</span>
|
||||
<SmartInput
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.varValue')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeFieldMapping(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.field_mapping.length === 0"
|
||||
class="text-muted-foreground py-4 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.clickAddField') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新条件 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.dbUpdate.updateConditions') }}
|
||||
<span class="text-red-500">*</span>
|
||||
</span>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addCondition"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.common.addCondition') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in form.where_conditions"
|
||||
:key="index"
|
||||
class="border-border bg-muted flex items-center gap-2 rounded border p-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="item.field"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.field')"
|
||||
size="small"
|
||||
class="w-24"
|
||||
/>
|
||||
<ElSelect v-model="item.operator" size="small" class="w-24">
|
||||
<ElOption
|
||||
v-for="op in operatorOptions"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
:label="op.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
<SmartInput
|
||||
v-model="item.value"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.value')"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
size="small"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.where_conditions.length === 0"
|
||||
class="text-muted-foreground py-4 text-center text-xs"
|
||||
>
|
||||
<ElAlert type="error" :closable="false" show-icon>
|
||||
<template #title>
|
||||
{{ $t('ai-platform.workflow.panels.dbUpdate.mustAddCondition') }}
|
||||
</template>
|
||||
</ElAlert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" placeholder="db_result" />
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 表选择对话框 -->
|
||||
<TableSelectDialog
|
||||
v-model:visible="tableSelectVisible"
|
||||
:current-table="form.table"
|
||||
@select="handleTableSelect"
|
||||
/>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElInput, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
outputs: props.data.outputs || [],
|
||||
output: props.data.output || '',
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', { ...props.data, ...val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const handleAdd = () => {
|
||||
form.value.outputs.push({
|
||||
variable: '',
|
||||
type: 'string',
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
form.value.outputs.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 输出内容 -->
|
||||
<div class="border-border border-b pb-4">
|
||||
<div class="text-foreground mb-2 text-sm font-medium">
|
||||
{{ $t('ai-platform.workflow.panels.end.outputContent') }}
|
||||
</div>
|
||||
<SmartInput
|
||||
v-model="form.output"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('ai-platform.workflow.panels.end.outputPlaceholder')"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.end.outputHint') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-foreground text-sm font-medium">{{
|
||||
$t('ai-platform.workflow.panels.common.outputVars')
|
||||
}}</span>
|
||||
<ElButton link type="primary" :icon="Plus" @click="handleAdd">
|
||||
{{ $t('ai-platform.workflow.panels.common.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.outputs.length === 0"
|
||||
class="text-muted-foreground py-4 text-center text-xs"
|
||||
>
|
||||
{{ $t('ai-platform.workflow.panels.end.noOutputVars') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="(item, index) in form.outputs"
|
||||
:key="index"
|
||||
class="border-border bg-muted rounded border p-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElInput
|
||||
v-model="item.variable"
|
||||
:placeholder="$t('ai-platform.workflow.panels.common.variableName')"
|
||||
size="small"
|
||||
/>
|
||||
<ElSelect
|
||||
v-model="item.type"
|
||||
:placeholder="$t('ai-platform.workflow.panels.end.type')"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
>
|
||||
<ElOption label="String" value="string" />
|
||||
<ElOption label="Number" value="number" />
|
||||
<ElOption label="JSON" value="json" />
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Trash2"
|
||||
@click="handleDelete(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formBasicInfo.label'),
|
||||
name: '',
|
||||
code: '',
|
||||
form_type: 'normal',
|
||||
description: '',
|
||||
sort: 0,
|
||||
auto_generate_code: false,
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.formName')"
|
||||
required
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formName')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formNamePlaceholder') +
|
||||
' {{llm_basic_name}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formBasicInfo.formNameHint')
|
||||
}}{{ llm_basic_name }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.formCode')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.code"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formCodePlaceholder') +
|
||||
' {{llm_basic_code}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formBasicInfo.formCodeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.autoGenCode')"
|
||||
>
|
||||
<ElSwitch v-model="form.auto_generate_code" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formBasicInfo.autoGenCodeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.formType')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.form_type"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formTypePlaceholder')
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.normalForm')"
|
||||
value="normal"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.workflowForm')"
|
||||
value="workflow"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formBasicInfo.formDesc')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.description"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formDescPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formBasicInfo.sort')">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
:min="0"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.sortPlaceholder')
|
||||
"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formBasicInfo.sortHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_basic_info</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.fullConfigOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_name</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formNameOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_code</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formCodeOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_type</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formTypeOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_description</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.formDescOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_sort</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formBasicInfo.sortOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElSwitch } from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formCreate.label'),
|
||||
form_basic_info: '',
|
||||
database_design: '',
|
||||
form_ui_design: '',
|
||||
list_config: '',
|
||||
application_id: '',
|
||||
update_if_exists: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.formCreate.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formBasicInfo')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_basic_info"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="{{form_basic_info.form_basic_info}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.basicInfoHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.dbDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.database_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="{{form_database_design.database_design}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.dbDesignHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.uiDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_ui_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="{{form_ui_design.form_ui_design}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.uiDesignHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.listDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.list_config"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="{{form_list_design.list_config}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.listDesignHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ $t('ai-platform.workflow.panels.formCreate.appId') }}</span>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.llm.optional')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.application_id"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formCreate.appIdPlaceholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formCreate.updateIfExists')"
|
||||
>
|
||||
<ElSwitch v-model="form.update_if_exists" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.updateIfExistsHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_id</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formIdOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_code</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formCodeOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_meta</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formMetaOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工作流说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
|
||||
{{ $t('ai-platform.workflow.panels.formCreate.fullWorkflow') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formBasicInfo')
|
||||
}}</span>
|
||||
<span>→</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.dbDesign')
|
||||
}}</span>
|
||||
<span>→</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.dbCreate')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-1">
|
||||
<span>→</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.uiDesign')
|
||||
}}</span>
|
||||
<span>→</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.listDesign')
|
||||
}}</span>
|
||||
<span>→</span>
|
||||
<span class="font-medium text-blue-600 dark:text-blue-400">{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formCreateStep')
|
||||
}}</span>
|
||||
<span>→</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formCreate.formPublish')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getFormListApi } from '#/api/online-dev/form-manager';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: any;
|
||||
nodeId: string;
|
||||
nodeType: string;
|
||||
}>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
// 根据节点类型设置默认值
|
||||
const getDefaultLabel = () => {
|
||||
switch (props.nodeType) {
|
||||
case 'form_data_create': {
|
||||
return $t('ai-platform.workflow.panels.formData.createLabel');
|
||||
}
|
||||
case 'form_data_delete': {
|
||||
return $t('ai-platform.workflow.panels.formData.deleteLabel');
|
||||
}
|
||||
case 'form_data_list': {
|
||||
return $t('ai-platform.workflow.panels.formData.listLabel');
|
||||
}
|
||||
case 'form_data_read': {
|
||||
return $t('ai-platform.workflow.panels.formData.readLabel');
|
||||
}
|
||||
case 'form_data_update': {
|
||||
return $t('ai-platform.workflow.panels.formData.updateLabel');
|
||||
}
|
||||
case 'form_schema_to_llm': {
|
||||
return $t('ai-platform.workflow.panels.formData.schemaToLlmLabel');
|
||||
}
|
||||
default: {
|
||||
return $t('ai-platform.workflow.panels.formData.label');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultOutputVariable = () => {
|
||||
switch (props.nodeType) {
|
||||
case 'form_data_create': {
|
||||
return 'form_data_create_result';
|
||||
}
|
||||
case 'form_data_delete': {
|
||||
return 'form_data_delete_result';
|
||||
}
|
||||
case 'form_data_list': {
|
||||
return 'form_data_list';
|
||||
}
|
||||
case 'form_data_read': {
|
||||
return 'form_data';
|
||||
}
|
||||
case 'form_data_update': {
|
||||
return 'form_data_update_result';
|
||||
}
|
||||
case 'form_schema_to_llm': {
|
||||
return 'llm_output_schema';
|
||||
}
|
||||
default: {
|
||||
return 'form_data_result';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const form = ref({
|
||||
label: getDefaultLabel(),
|
||||
form_code: '',
|
||||
id: '',
|
||||
data: '',
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
filters: '',
|
||||
sort: [],
|
||||
hard_delete: false,
|
||||
is_array: false,
|
||||
include_fields: [],
|
||||
exclude_fields: [],
|
||||
output_variable: getDefaultOutputVariable(),
|
||||
...props.data,
|
||||
});
|
||||
|
||||
// 表单输入模式:variable(变量输入)或 select(下拉选择)
|
||||
const formInputMode = ref<'select' | 'variable'>('variable');
|
||||
|
||||
// 表单列表
|
||||
const formList = ref<any[]>([]);
|
||||
const formLoading = ref(false);
|
||||
|
||||
// 加载表单列表
|
||||
const loadFormList = async () => {
|
||||
try {
|
||||
formLoading.value = true;
|
||||
const res = await getFormListApi({ page: 1, pageSize: 100, status: 'published' });
|
||||
formList.value = res.items || [];
|
||||
} catch (error) {
|
||||
console.error('加载表单列表失败:', error);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadFormList();
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 判断是否显示某个字段
|
||||
const showField = (field: string) => {
|
||||
const nodeType = props.nodeType;
|
||||
switch (field) {
|
||||
case 'data': {
|
||||
return ['form_data_create', 'form_data_update'].includes(nodeType);
|
||||
}
|
||||
case 'exclude_fields':
|
||||
case 'include_fields':
|
||||
case 'is_array': {
|
||||
return nodeType === 'form_schema_to_llm';
|
||||
}
|
||||
case 'filters':
|
||||
case 'page':
|
||||
case 'page_size':
|
||||
case 'sort': {
|
||||
return nodeType === 'form_data_list';
|
||||
}
|
||||
case 'hard_delete': {
|
||||
return nodeType === 'form_data_delete';
|
||||
}
|
||||
case 'id': {
|
||||
return ['form_data_delete', 'form_data_read', 'form_data_update'].includes(nodeType);
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput v-model="form.label" :placeholder="getDefaultLabel()" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.formCode')">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ElRadioGroup v-model="formInputMode" size="small">
|
||||
<ElRadioButton value="variable">{{ $t('ai-platform.workflow.panels.formData.variableInput') }}</ElRadioButton>
|
||||
<ElRadioButton value="select">{{ $t('ai-platform.workflow.panels.formData.selectFromList') }}</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
|
||||
<!-- 变量输入模式 -->
|
||||
<template v-if="formInputMode === 'variable'">
|
||||
<SmartInput
|
||||
v-model="form.form_code"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formData.formCodeInputPlaceholder') +
|
||||
' {{form_code}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.formCodeInputHint') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 下拉选择模式 -->
|
||||
<template v-else>
|
||||
<ElSelect
|
||||
v-model="form.form_code"
|
||||
:placeholder="$t('ai-platform.workflow.panels.formData.selectFormPlaceholder')"
|
||||
filterable
|
||||
:loading="formLoading"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="f in formList"
|
||||
:key="f.code"
|
||||
:label="`${f.name} (${f.code})`"
|
||||
:value="f.code"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.selectedFormHint', { code: form.form_code || '-' }) }}
|
||||
</div>
|
||||
</template>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 记录ID(读取/更新/删除) -->
|
||||
<ElFormItem v-if="showField('id')" :label="$t('ai-platform.workflow.panels.formData.recordId')">
|
||||
<SmartInput
|
||||
v-model="form.id"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formData.recordIdPlaceholder') +
|
||||
' {{record_id}}'
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 数据(创建/更新) -->
|
||||
<ElFormItem v-if="showField('data')">
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ $t('ai-platform.workflow.panels.formData.data') }}</span>
|
||||
<span class="text-muted-foreground text-xs">(JSON)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.data"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
:placeholder="$t('ai-platform.workflow.panels.formData.dataPlaceholder') + ' {{llm_response}}'"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.dataHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 分页(列表查询) -->
|
||||
<template v-if="showField('page')">
|
||||
<div class="flex gap-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.pageNum')" class="flex-1">
|
||||
<ElInputNumber v-model="form.page" :min="1" class="w-full" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.pageSize')" class="flex-1">
|
||||
<ElInputNumber v-model="form.page_size" :min="1" :max="1000" class="w-full" />
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 过滤条件(列表查询) -->
|
||||
<ElFormItem v-if="showField('filters')">
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ $t('ai-platform.workflow.panels.formData.filterConditions') }}</span>
|
||||
<span class="text-muted-foreground text-xs">(JSON)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.filters"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="{"status": "active", "name": {"type": "like", "value": "Zhang"}}"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 硬删除(删除) -->
|
||||
<ElFormItem v-if="showField('hard_delete')">
|
||||
<div class="flex items-center gap-2">
|
||||
<ElSwitch v-model="form.hard_delete" />
|
||||
<span class="text-sm">{{ $t('ai-platform.workflow.panels.formData.hardDelete') }}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.hardDeleteHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 数组模式(表单转LLM结构) -->
|
||||
<ElFormItem v-if="showField('is_array')">
|
||||
<div class="flex items-center gap-2">
|
||||
<ElSwitch v-model="form.is_array" />
|
||||
<span class="text-sm">{{ $t('ai-platform.workflow.panels.formData.generateArray') }}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.generateArrayHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
|
||||
<ElInput v-model="form.output_variable" :placeholder="getDefaultOutputVariable()" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formData.outputVarHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formDbCreate.label'),
|
||||
database_design: '',
|
||||
db_database: '',
|
||||
db_schema: '',
|
||||
if_exists: 'skip',
|
||||
create_schema_if_not_exists: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbCreate.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert type="warning" :closable="false" show-icon class="mb-4">
|
||||
<template #title>
|
||||
<span class="text-sm">{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.cautionWarning')
|
||||
}}</span>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.dbDesignConfig')"
|
||||
required
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.dbDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.database_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formDbCreate.dbDesignConfigPlaceholder',
|
||||
) + ' {{database_design}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbCreate.dbDesignConfigHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.database')"
|
||||
>
|
||||
<SmartInput
|
||||
v-model="form.db_database"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbCreate.databasePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbCreate.databaseHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formDbCreate.schema')">
|
||||
<SmartInput
|
||||
v-model="form.db_schema"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbCreate.schemaPlaceholder') +
|
||||
' {{schema_name}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbCreate.schemaHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.ifExists')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.if_exists"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbCreate.ifExistsPlaceholder')
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formDbCreate.skipRecommended')
|
||||
"
|
||||
value="skip"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.throwError')"
|
||||
value="error"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.replaceDanger')"
|
||||
value="replace"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbCreate.ifExistsHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formDbCreate.autoCreateSchema')"
|
||||
>
|
||||
<ElSwitch v-model="form.create_schema_if_not_exists" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.autoCreateSchemaHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">creation_result</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.resultOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">all_tables_ready</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.allReadyOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">created_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.successCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">error_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbCreate.errorCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 警告提示 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-orange-700 dark:text-orange-300">
|
||||
{{ $t('ai-platform.workflow.panels.formDbCreate.notes') }}
|
||||
</div>
|
||||
<ul class="text-muted-foreground list-disc space-y-1 pl-4 text-xs">
|
||||
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note1') }}</li>
|
||||
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note2') }}</li>
|
||||
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note3') }}</li>
|
||||
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note4') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import DbConnectionSelect from '../components/DbConnectionSelect.vue';
|
||||
import DbDatabaseSelect from '../components/DbDatabaseSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formDbDesign.label'),
|
||||
design_mode: 'main', // 'main' or 'sub'
|
||||
// 主表配置
|
||||
table_name: '',
|
||||
fields: '',
|
||||
// 从表配置
|
||||
sub_table_name: '',
|
||||
sub_fields: '',
|
||||
parent_table: '',
|
||||
foreign_key: '',
|
||||
// 通用配置
|
||||
db_config: 'default',
|
||||
db_database: '',
|
||||
db_schema: '',
|
||||
db_type: 'postgresql',
|
||||
auto_add_system_fields: true,
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const resolvedDbTypeLabel = computed(() => form.value.db_type || 'postgresql');
|
||||
|
||||
function handleConnectionChange(payload: {
|
||||
database?: string;
|
||||
dbName: string;
|
||||
dbType: string;
|
||||
}) {
|
||||
const connectionChanged = form.value.db_config !== payload.dbName;
|
||||
form.value.db_config = payload.dbName;
|
||||
form.value.db_type = payload.dbType;
|
||||
if (connectionChanged) {
|
||||
form.value.db_database = payload.database || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formDbDesign.designMode')"
|
||||
>
|
||||
<ElRadioGroup v-model="form.design_mode">
|
||||
<ElRadioButton value="main">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.mainTable') }}
|
||||
</ElRadioButton>
|
||||
<ElRadioButton value="sub">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.subTable') }}
|
||||
</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.designModeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 主表配置 -->
|
||||
<template v-if="form.design_mode === 'main'">
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.tableName')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.table_name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.tableNamePlaceholder') +
|
||||
' {{llm_table_name}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.tableNameHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.fieldList')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(array)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.fields"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.fieldListPlaceholder') +
|
||||
' {{llm_fields}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.fieldListHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
|
||||
<!-- 从表配置 -->
|
||||
<template v-else>
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.subTableName')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.sub_table_name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.subTableNamePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.subTableNameHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.relatedMainTable')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.parent_table"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formDbDesign.relatedMainTablePlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.relatedMainTableHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.foreignKey')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.foreign_key"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.foreignKeyPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.foreignKeyHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.fieldList')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(array)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.sub_fields"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formDbDesign.subFieldListPlaceholder',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.subFieldListHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
|
||||
<DbConnectionSelect
|
||||
v-model="form.db_config"
|
||||
write-mode
|
||||
@change="handleConnectionChange"
|
||||
/>
|
||||
|
||||
<DbDatabaseSelect
|
||||
v-model="form.db_database"
|
||||
:db-name="form.db_config"
|
||||
:db-type="form.db_type"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.dbSchema')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.db_schema"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.dbSchemaPlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.dbSchemaHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.formDbDesign.dbType')">
|
||||
<ElInput :model-value="resolvedDbTypeLabel" readonly />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.dbTypeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formDbDesign.autoAddSystemFields')
|
||||
"
|
||||
>
|
||||
<ElSwitch v-model="form.auto_add_system_fields" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.autoAddSystemFieldsHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">database_design</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.fullDesignOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">table_name</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.tableNameOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">field_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formDbDesign.fieldCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段配置示例 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
|
||||
{{ $t('ai-platform.workflow.panels.formDbDesign.fieldConfigExample') }}
|
||||
</div>
|
||||
<pre class="text-muted-foreground overflow-x-auto text-[10px]">
|
||||
[
|
||||
{
|
||||
"name": "name",
|
||||
"type": "varchar",
|
||||
"maxLength": 100,
|
||||
"comment": "Name",
|
||||
"nullable": false
|
||||
},
|
||||
{
|
||||
"name": "age",
|
||||
"type": "int",
|
||||
"comment": "Age",
|
||||
"nullable": true
|
||||
}
|
||||
]</pre
|
||||
>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formListDesign.label'),
|
||||
form_ui_design: '',
|
||||
database_design: '',
|
||||
auto_query_fields: true,
|
||||
auto_columns: true,
|
||||
container_type: 'drawer',
|
||||
page_size: 20,
|
||||
show_index: true,
|
||||
show_selection: true,
|
||||
enable_export: true,
|
||||
enable_import: false,
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formListDesign.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.uiDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_ui_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formListDesign.uiDesignConfigPlaceholder',
|
||||
) + ' {{form_ui_design.form_ui_design}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.uiDesignConfigHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.dbDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs"
|
||||
>(object,
|
||||
{{ $t('ai-platform.workflow.panels.llm.optional') }})</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.database_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formListDesign.dbDesignConfigPlaceholder',
|
||||
) + ' {{form_database_design.database_design}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.dbDesignConfigHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.autoGenQuery')"
|
||||
>
|
||||
<ElSwitch v-model="form.auto_query_fields" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formListDesign.autoGenQueryHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.autoGenColumns')"
|
||||
>
|
||||
<ElSwitch v-model="form.auto_columns" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.autoGenColumnsHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.containerType')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.container_type"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formListDesign.containerTypePlaceholder',
|
||||
)
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.drawer')"
|
||||
value="drawer"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.dialog')"
|
||||
value="dialog"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.page')"
|
||||
value="page"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formListDesign.containerTypeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.pageSize')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.page_size"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formListDesign.pageSizePlaceholder')
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formListDesign.perPage', {
|
||||
count: 10,
|
||||
})
|
||||
"
|
||||
:value="10"
|
||||
/>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formListDesign.perPage', {
|
||||
count: 20,
|
||||
})
|
||||
"
|
||||
:value="20"
|
||||
/>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formListDesign.perPage', {
|
||||
count: 50,
|
||||
})
|
||||
"
|
||||
:value="50"
|
||||
/>
|
||||
<ElOption
|
||||
:label="
|
||||
$t('ai-platform.workflow.panels.formListDesign.perPage', {
|
||||
count: 100,
|
||||
})
|
||||
"
|
||||
:value="100"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.showIndex')"
|
||||
>
|
||||
<ElSwitch v-model="form.show_index" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.showSelection')"
|
||||
>
|
||||
<ElSwitch v-model="form.show_selection" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.enableExport')"
|
||||
>
|
||||
<ElSwitch v-model="form.enable_export" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formListDesign.enableImport')"
|
||||
>
|
||||
<ElSwitch v-model="form.enable_import" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">list_config</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.fullConfigOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">query_field_count</span>
|
||||
<span>{{
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formListDesign.queryFieldCountOutput',
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">column_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formListDesign.columnCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能推断说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-800 dark:bg-indigo-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-indigo-700 dark:text-indigo-300">
|
||||
{{ $t('ai-platform.workflow.panels.formListDesign.inferRules') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formListDesign.inferRule1') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formListDesign.inferRule2') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formListDesign.inferRule3') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formListDesign.inferRule4') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formListDesign.inferRule5') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formPublish.label'),
|
||||
form_id: '',
|
||||
form_code: '',
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="$t('ai-platform.workflow.panels.formPublish.placeholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formPublish.formId')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_id"
|
||||
:current-node-id="nodeId"
|
||||
placeholder="{{form_create.form_id}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formPublish.formIdHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formPublish.formCode')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('ai-platform.workflow.panels.llm.optional')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_code"
|
||||
:current-node-id="nodeId"
|
||||
placeholder="{{form_create.form_code}}"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formPublish.formCodeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">menu_id</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formPublish.menuIdOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">route_path</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formPublish.routePathOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">publish_result</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formPublish.publishResultOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工作流说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.usage') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
<p>{{ $t('ai-platform.workflow.panels.formPublish.usageTip1') }}</p>
|
||||
<p class="mt-1">
|
||||
{{ $t('ai-platform.workflow.panels.formPublish.usageTip2') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
|
||||
import SmartInput from '../components/SmartInput.vue';
|
||||
|
||||
const props = defineProps<{ data: any; nodeId: string }>();
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const form = ref({
|
||||
label: $t('ai-platform.workflow.panels.formUIDesign.label'),
|
||||
database_design: '',
|
||||
form_name: '',
|
||||
form_code: '',
|
||||
layout_mode: 'auto',
|
||||
label_width: 120,
|
||||
enable_grouping: true,
|
||||
hide_system_fields: true,
|
||||
require_confirmation: true,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
(val) => {
|
||||
emit('update', val);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-position="top" size="small" class="space-y-4">
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
|
||||
<ElInput
|
||||
v-model="form.label"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formUIDesign.placeholder')
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.dbDesignConfig')
|
||||
}}</span>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-muted-foreground text-xs">(object)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.database_design"
|
||||
:current-node-id="nodeId"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="
|
||||
$t(
|
||||
'ai-platform.workflow.panels.formUIDesign.dbDesignConfigPlaceholder',
|
||||
) + ' {{form_database_design.database_design}}'
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.dbDesignConfigHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.formName')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_name"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formUIDesign.formNamePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.formNameHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.formCode')
|
||||
}}</span>
|
||||
<span class="text-muted-foreground text-xs">(string)</span>
|
||||
</div>
|
||||
</template>
|
||||
<SmartInput
|
||||
v-model="form.form_code"
|
||||
:current-node-id="nodeId"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formUIDesign.formCodePlaceholder')
|
||||
"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.formCodeHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutMode')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="form.layout_mode"
|
||||
:placeholder="
|
||||
$t('ai-platform.workflow.panels.formUIDesign.layoutModePlaceholder')
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutAuto')"
|
||||
value="auto"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutSingle')"
|
||||
value="single"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutDouble')"
|
||||
value="double"
|
||||
/>
|
||||
<ElOption
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutTriple')"
|
||||
value="triple"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.layoutAutoHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.labelWidth')"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.label_width"
|
||||
:min="60"
|
||||
:max="200"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.labelWidthHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.enableGrouping')"
|
||||
>
|
||||
<ElSwitch v-model="form.enable_grouping" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.enableGroupingHint') }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
:label="$t('ai-platform.workflow.panels.formUIDesign.hideSystemFields')"
|
||||
>
|
||||
<ElSwitch v-model="form.hide_system_fields" />
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.hideSystemFieldsHint')
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
|
||||
<ConfirmModeSelect
|
||||
v-model="form.require_confirmation"
|
||||
:current-node-id="nodeId"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 输出变量说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
|
||||
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_ui_design</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.fullConfigOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">form_code</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.formCodeOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">field_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.fieldCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-mono">group_count</span>
|
||||
<span>{{
|
||||
$t('ai-platform.workflow.panels.formUIDesign.groupCountOutput')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能映射说明 -->
|
||||
<div
|
||||
class="mt-4 rounded border border-purple-200 bg-purple-50 p-3 dark:border-purple-800 dark:bg-purple-950"
|
||||
>
|
||||
<div class="mb-2 font-medium text-purple-700 dark:text-purple-300">
|
||||
{{ $t('ai-platform.workflow.panels.formUIDesign.mappingRules') }}
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule1') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule2') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule3') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule4') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule5') }}</div>
|
||||
<div>• {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule6') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user