首页更新
This commit is contained in:
@@ -153,10 +153,11 @@ export function getCreationList(params: CreationListParams, requestOptions?: Req
|
||||
}) as Promise<CreationListResponse>;
|
||||
}
|
||||
|
||||
export function getExecutionList(requestOptions?: RequestOptions) {
|
||||
export function getExecutionList(params?: Record<string, any>, requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/flow/execution/list',
|
||||
method: 'get',
|
||||
params,
|
||||
requestOptions,
|
||||
}) as Promise<ExecutionListResponse>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<div class="chat-list">
|
||||
<div class="chat-divider">今天 15:14</div>
|
||||
|
||||
<div v-for="msg in messages" :key="msg.id" class="message-row" :class="{ 'is-user': msg.isUser }">
|
||||
<div class="bubble-wrap">
|
||||
<div class="bubble">{{ msg.content }}</div>
|
||||
@@ -12,35 +10,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
time: string;
|
||||
isUser: boolean;
|
||||
}
|
||||
|
||||
const messages = ref<Message[]>([
|
||||
{
|
||||
id: 1,
|
||||
content: '你好!我叫知子,很高兴为您服务。您今天想聊些什么呢?',
|
||||
time: '09:30',
|
||||
isUser: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '你好!我想了解下这个系统都能做什么。',
|
||||
time: '09:31',
|
||||
isUser: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
|
||||
time: '09:31',
|
||||
isUser: false,
|
||||
},
|
||||
]);
|
||||
interface Props {
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -53,19 +34,6 @@ const messages = ref<Message[]>([
|
||||
padding: 28px 0 10px;
|
||||
}
|
||||
|
||||
.chat-divider {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1;
|
||||
padding: 4px 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(226, 233, 244, 0.8);
|
||||
letter-spacing: 0.2px;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -95,7 +63,6 @@ const messages = ref<Message[]>([
|
||||
border: 1px solid rgba(226, 233, 244, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.message-row.is-user & {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
|
||||
|
||||
@@ -118,7 +118,13 @@ const toggleWorkflow = (id: string) => {
|
||||
emit('workflow-select', newId);
|
||||
};
|
||||
|
||||
const clearWorkflow = () => {
|
||||
const resetAll = () => {
|
||||
message.value = '';
|
||||
selectedWorkflowId.value = null;
|
||||
emit('workflow-select', null);
|
||||
};
|
||||
|
||||
const clearWorkflow = () => {
|
||||
selectedWorkflowId.value = null;
|
||||
emit('workflow-select', null);
|
||||
};
|
||||
@@ -126,6 +132,8 @@ const clearWorkflow = () => {
|
||||
onMounted(() => {
|
||||
fetchWorkflows();
|
||||
});
|
||||
|
||||
defineExpose({ resetAll, clearWorkflow, selectedWorkflowId });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -130,50 +130,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话区域 -->
|
||||
<div v-if="activeHistoryId && activeMenu === 'chat'" class="content-body">
|
||||
<div class="chat-container">
|
||||
<ChatList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 默认占位 -->
|
||||
<div v-if="!workflowDetail && !(activeHistoryId && activeMenu === 'chat')" class="content-body placeholder-body">
|
||||
<!-- 默认占位 — 无工作流时显示引导 -->
|
||||
<div v-else class="content-body placeholder-body">
|
||||
<div class="placeholder-content">
|
||||
<div class="placeholder-icon">
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="8" y="12" width="64" height="48" rx="8" fill="url(#grad1)" fill-opacity="0.18"/>
|
||||
<rect x="8" y="12" width="64" height="48" rx="8" stroke="url(#grad1)" stroke-width="1.2"/>
|
||||
<path d="M28 32h24M28 40h16M28 48h20" stroke="url(#grad1)" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M62 72c0-8.837-7.163-16-16-16s-16 7.163-16 16" stroke="url(#grad2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="46" cy="48" r="4" fill="url(#grad2)"/>
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0" y1="0" x2="80" y2="60" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3b82f6"/>
|
||||
<stop offset="1" stop-color="#8b5cf6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad2" x1="0" y1="0" x2="80" y2="60" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8b5cf6"/>
|
||||
<stop offset="1" stop-color="#ec4899"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="icon-ring ring-outer">
|
||||
<div class="icon-ring ring-inner">
|
||||
<div class="icon-ring ring-core">
|
||||
<svg class="icon-sparkle" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2l1.09 6.26L18 4.5l-3.41 4.74L20 10l-5.41.76L18 15.5l-4.91-2.76L12 19l-1.09-6.26L6 15.5l3.41-4.74L4 10l5.41-.76L6 4.5l4.91 2.74L12 2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="placeholder-title">开始内容创作</h2>
|
||||
<p class="placeholder-desc">选择下方的工作流,填写参数后即可快速生成内容</p>
|
||||
<div class="placeholder-hints">
|
||||
<div class="hint-item">
|
||||
<span class="hint-dot"></span>
|
||||
<span>点击工作流胶囊快速切换</span>
|
||||
|
||||
<h2 class="placeholder-title">请选择下方工作流进行创作</h2>
|
||||
<p class="placeholder-desc">点击工作流胶囊快速切换,填写参数后一键生成内容</p>
|
||||
|
||||
<div class="placeholder-steps">
|
||||
<div class="step-item">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-text">
|
||||
<span class="step-label">选择工作流</span>
|
||||
<span class="step-hint">点击胶囊切换不同创作模式</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<span class="hint-dot"></span>
|
||||
<span>在「对话记录」中查看历史</span>
|
||||
<div class="step-arrow">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<span class="hint-dot"></span>
|
||||
<span>在「工作空间」中管理作品</span>
|
||||
<div class="step-item">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-text">
|
||||
<span class="step-label">填写参数</span>
|
||||
<span class="step-hint">按需配置内容与样式选项</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-arrow">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-text">
|
||||
<span class="step-label">一键生成</span>
|
||||
<span class="step-hint">AI 自动产出,结果同步到工作空间</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-footer">
|
||||
<span class="footer-hint">还可从左侧「对话记录」查看历史或「工作空间」管理作品</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,19 +189,18 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { UploadFilled, Document } from '@element-plus/icons-vue';
|
||||
import ChatList from './ChatList.vue';
|
||||
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
|
||||
interface Props {
|
||||
activeMenu: string;
|
||||
workflowDetail: any;
|
||||
activeHistoryId?: string | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
activeHistoryId: null,
|
||||
});
|
||||
activeHistoryId: null,
|
||||
});
|
||||
defineEmits<{
|
||||
(e: 'close-workflow'): void;
|
||||
}>();
|
||||
@@ -204,7 +209,6 @@ const formValues = reactive<Record<string, any>>({});
|
||||
const fieldFiles = reactive<Record<string, { name: string; url: string }[]>>({});
|
||||
const uploadingFields = reactive<Record<string, boolean>>({});
|
||||
const templates = ref<any[]>([]);
|
||||
|
||||
const getFieldKey = (node: any, field: any): string => {
|
||||
if (field?.__isHttpBodyChild) {
|
||||
return `${node.id}_body_${field.bodyKey}`;
|
||||
@@ -586,11 +590,84 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
|
||||
.chat-container { width: min(1060px, 82%); margin: 0 auto; padding: 20px 0 130px; }
|
||||
|
||||
.placeholder-body { display: flex; align-items: center; justify-content: center; }
|
||||
.placeholder-content { text-align: center; padding: 40px 20px; max-width: 400px; }
|
||||
.placeholder-icon { margin-bottom: 24px; display: flex; justify-content: center; }
|
||||
.placeholder-title { font-size: 20px; font-weight: 700; color: #1e293b; margin: 0 0 8px; letter-spacing: -0.3px; }
|
||||
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 28px; line-height: 1.6; }
|
||||
.placeholder-hints { display: flex; flex-direction: column; gap: 10px; align-items: center; }
|
||||
.hint-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #64748b; }
|
||||
.hint-dot { width: 6px; height: 6px; border-radius: 50%; background: #cbd5e1; flex-shrink: 0; }
|
||||
.placeholder-content { text-align: center; padding: 20px; max-width: 480px; }
|
||||
|
||||
/* ===== 图标:层叠圆环 + 星形 ===== */
|
||||
.placeholder-icon { margin-bottom: 28px; display: flex; justify-content: center; }
|
||||
.icon-ring {
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.ring-outer {
|
||||
width: 88px; height: 88px;
|
||||
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.10) 0%, rgba(139, 92, 246, 0.06) 100%);
|
||||
border: 1.5px solid rgba(59, 130, 246, 0.15);
|
||||
&:hover { transform: scale(1.05); }
|
||||
}
|
||||
.ring-inner {
|
||||
width: 62px; height: 62px;
|
||||
background: radial-gradient(circle at 35% 30%, rgba(59, 130, 246, 0.14) 0%, rgba(139, 92, 246, 0.08) 100%);
|
||||
border: 1.5px solid rgba(59, 130, 246, 0.20);
|
||||
}
|
||||
.ring-core {
|
||||
width: 42px; height: 42px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
.icon-sparkle { color: #fff; }
|
||||
|
||||
/* ===== 标题与描述 ===== */
|
||||
.placeholder-title { font-size: 18px; font-weight: 700; color: #0f172a; margin: 0 0 6px; letter-spacing: -0.2px; line-height: 1.4; }
|
||||
.placeholder-desc { font-size: 14px; color: #94a3b8; margin: 0 0 32px; line-height: 1.5; }
|
||||
|
||||
/* ===== 步骤引导 ===== */
|
||||
.placeholder-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.step-num {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #f0f0ff 100%);
|
||||
border: 1.5px solid #bfdbfe;
|
||||
color: #3b82f6;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
.step-item:hover & {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
.step-text { display: flex; flex-direction: column; gap: 1px; }
|
||||
.step-label { font-size: 13px; font-weight: 600; color: #334155; line-height: 1.3; white-space: nowrap; }
|
||||
.step-hint { font-size: 11px; color: #94a3b8; line-height: 1.3; white-space: nowrap; }
|
||||
.step-arrow {
|
||||
color: #cbd5e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 底部辅助提示 ===== */
|
||||
.placeholder-footer { margin-top: 4px; }
|
||||
.footer-hint { font-size: 12px; color: #cbd5e1; line-height: 1.5; }
|
||||
</style>
|
||||
@@ -38,6 +38,9 @@
|
||||
<el-icon class="item-icon"><ChatLineRound /></el-icon>
|
||||
<div class="item-body">
|
||||
<div class="item-title">{{ item.title }}</div>
|
||||
<div v-if="item.status === 'executing'" class="status-badge status-running">执行中</div>
|
||||
<div v-if="item.status === 'completed'" class="status-badge status-done">执行完成</div>
|
||||
<div v-if="item.status === 'failed'" class="status-badge status-failed">执行失败</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
</div>
|
||||
<button class="item-del-btn" @click.stop="handleDeleteHistory(item.id)">
|
||||
@@ -53,7 +56,7 @@
|
||||
<div v-show="activeTab === 'workspace'" class="panel" v-loading="treeLoading">
|
||||
<el-empty v-if="!treeLoading && treeNodes.length === 0" :image-size="48" description="暂无内容" />
|
||||
<div v-else class="panel-list">
|
||||
<div v-for="dateNode in visibleWorkspace" :key="dateNode.id" class="tree-date-group">
|
||||
<div v-for="dateNode in treeNodes" :key="dateNode.id" class="tree-date-group">
|
||||
<div class="tree-date-label">{{ dateNode.label }}</div>
|
||||
<div v-for="flowNode in dateNode.children" :key="flowNode.id" class="tree-flow-group">
|
||||
<div class="tree-flow-label">
|
||||
@@ -73,9 +76,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="workspaceShowCount < treeNodes.length" class="load-more-btn" @click="workspaceShowCount += WORKSPACE_PAGE">
|
||||
<button v-if="props.hasMoreWorkspace" class="load-more-btn" @click="emit('load-more-workspace')">
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
展开更多 ({{ treeNodes.length - workspaceShowCount }} 条)
|
||||
展开更多
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { Delete, ChatDotRound, ChatLineRound, EditPen, ArrowDown, Promotion, Document, VideoPlay, Headset, Picture, FolderOpened } from '@element-plus/icons-vue';
|
||||
|
||||
interface TreeNode {
|
||||
@@ -101,6 +104,7 @@ interface HistoryItem {
|
||||
id: string;
|
||||
title: string;
|
||||
time: string;
|
||||
status?: 'idle' | 'executing' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -109,6 +113,7 @@ interface Props {
|
||||
historyList: HistoryItem[];
|
||||
treeNodes: TreeNode[];
|
||||
treeLoading: boolean;
|
||||
hasMoreWorkspace: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -118,19 +123,15 @@ interface Emits {
|
||||
(e: 'delete-history', id: string): void;
|
||||
(e: 'preview-node', data: TreeNode): void;
|
||||
(e: 'download-node', data: TreeNode): void;
|
||||
(e: 'delete-node', data: TreeNode): void;
|
||||
(e: 'delete-node', data: TreeNode): void;
|
||||
(e: 'load-more-workspace'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const WORKSPACE_PAGE = 5;
|
||||
|
||||
const activeTab = ref<'history' | 'workspace'>('history');
|
||||
const workspaceShowCount = ref(WORKSPACE_PAGE);
|
||||
|
||||
const visibleHistory = computed(() => props.historyList);
|
||||
const visibleWorkspace = computed(() => props.treeNodes.slice(0, workspaceShowCount.value));
|
||||
|
||||
const getFileIcon = (fileType?: string) => {
|
||||
if (!fileType) return Document;
|
||||
@@ -504,4 +505,33 @@ const handleDeleteNode = (data: TreeNode) => emit('delete-node', data);
|
||||
background: #dbeafe;
|
||||
}
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 11px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
line-height: 1.5;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
||||
|
||||
+241
-106
@@ -6,6 +6,8 @@
|
||||
:history-list="historyList"
|
||||
:tree-nodes="treeNodes"
|
||||
:tree-loading="treeLoading"
|
||||
:has-more-workspace="hasMoreWorkspace"
|
||||
@load-more-workspace="loadMoreWorkspace"
|
||||
@menu-change="handleMenuChange"
|
||||
@new-chat="handleCreateHistory"
|
||||
@select-history="handleSelectHistory"
|
||||
@@ -15,8 +17,8 @@
|
||||
@delete-node="handleDeleteNode"
|
||||
/>
|
||||
<div class="main-wrapper">
|
||||
<MainContent ref="mainContentRef" :active-menu="activeMenu" :workflow-detail="selectedWorkflowDetail" :active-history-id="activeHistoryId" />
|
||||
<InputBar @send="handleSend" @workflow-select="handleWorkflowSelect" />
|
||||
<MainContent ref="mainContentRef" :active-menu="activeMenu" :workflow-detail="selectedWorkflowDetail" :active-history-id="activeHistoryId" @close-workflow="handleCloseWorkflow" />
|
||||
<InputBar ref="inputBarRef" @send="handleSend" @workflow-select="handleWorkflowSelect" />
|
||||
</div>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
@@ -44,13 +46,22 @@ import Sidebar from './components/Sidebar.vue';
|
||||
import MainContent from './components/MainContent.vue';
|
||||
import InputBar from './components/InputBar.vue';
|
||||
import type { ExecutionTreeItem } from '/@/api/settings/creation';
|
||||
import { getExecutionList, getWorkflowDetail, deleteExecutionResult, deleteExecutionSession, getSessionList, downloadToFile, executeFlow } from '/@/api/settings/creation';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { getExecutionList, getWorkflowDetail, getExecutionDetail, deleteExecutionResult, deleteExecutionSession, getSessionList, downloadToFile, executeFlow } from '/@/api/settings/creation';
|
||||
|
||||
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
title: string;
|
||||
time: string;
|
||||
status?: 'idle' | 'executing' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
time: string;
|
||||
isUser: boolean;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
@@ -74,6 +85,7 @@ const previewUrl = ref('');
|
||||
const previewMode = ref<'iframe' | 'image' | 'video' | 'audio'>('iframe');
|
||||
|
||||
const historyList = ref<HistoryItem[]>([]);
|
||||
const sessionMessages = ref<Map<string, ChatMessage[]>>(new Map());
|
||||
|
||||
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||
|
||||
@@ -135,7 +147,7 @@ const downloadNode = async (data: TreeNode) => {
|
||||
try {
|
||||
// 优先通过后端代理下载,避免跨域问题
|
||||
const res = await downloadToFile({ fileURL: data.fileUrl });
|
||||
const blob = new Blob([res]);
|
||||
const blob = res.data as Blob;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
@@ -175,22 +187,23 @@ const handleDeleteNode = async (data: TreeNode) => {
|
||||
// 后端已提示错误,继续本地删除
|
||||
}
|
||||
}
|
||||
const removeNode = (nodes: TreeNode[], targetId: string): boolean => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === targetId) {
|
||||
nodes.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children && removeNode(nodes[i].children!, targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
removeNode(treeNodes.value, data.id);
|
||||
ElMessage.success('已删除');
|
||||
};
|
||||
|
||||
const removeNode = (nodes: TreeNode[], targetId: string): boolean => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === targetId) {
|
||||
nodes.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children && removeNode(nodes[i].children!, targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getSessionData = (res: any): any[] => {
|
||||
if (!res?.data) return [];
|
||||
if (Array.isArray(res.data)) return res.data;
|
||||
@@ -199,6 +212,8 @@ const getSessionData = (res: any): any[] => {
|
||||
};
|
||||
|
||||
const getList = async () => {
|
||||
workspacePage.value = 1;
|
||||
hasMoreWorkspace.value = true;
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const [execRes, sessionRes] = await Promise.all([
|
||||
@@ -208,7 +223,7 @@ const getList = async () => {
|
||||
imgAddressPrefix.value = execRes.data?.imgAddressPrefix || '';
|
||||
treeNodes.value = buildTreeNodes(execRes.data?.tree || []);
|
||||
const d = new Date(); const todayStr = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||||
historyList.value = getSessionData(sessionRes).map((s) => {
|
||||
const sessions = getSessionData(sessionRes).map((s) => {
|
||||
const rawTime = s.createdAt || s.createDate || '';
|
||||
let displayTime = rawTime;
|
||||
if (rawTime) {
|
||||
@@ -221,10 +236,18 @@ const getList = async () => {
|
||||
}
|
||||
return {
|
||||
id: s.id,
|
||||
sessionId: s.sessionId,
|
||||
title: s.flowName || '未命名会话',
|
||||
time: displayTime,
|
||||
};
|
||||
});
|
||||
historyList.value = sessions;
|
||||
// 为每个后端会话初始化消息存储
|
||||
sessions.forEach((s) => {
|
||||
if (!sessionMessages.value.has(s.id)) {
|
||||
sessionMessages.value.set(s.id, []);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
treeNodes.value = [];
|
||||
historyList.value = [];
|
||||
@@ -236,9 +259,13 @@ const getList = async () => {
|
||||
|
||||
const selectedWorkflowDetail = ref<any>(null);
|
||||
const mainContentRef = ref<any>(null);
|
||||
const workspacePage = ref(1);
|
||||
const hasMoreWorkspace = ref(true);
|
||||
const workspaceLoading = ref(false);
|
||||
const inputBarRef = ref<any>(null);
|
||||
|
||||
const getSessionId = () => {
|
||||
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
return `session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
};
|
||||
|
||||
const handleWorkflowSelect = async (workflowId: string | null) => {
|
||||
@@ -254,101 +281,203 @@ const handleWorkflowSelect = async (workflowId: string | null) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreWorkspace = async () => {
|
||||
if (workspaceLoading.value) return;
|
||||
workspaceLoading.value = true;
|
||||
const nextPage = workspacePage.value + 1;
|
||||
try {
|
||||
const res = await getExecutionList({ pageNum: nextPage, pageSize: 5 });
|
||||
const newTree = res.data?.tree || [];
|
||||
if (newTree.length === 0) {
|
||||
hasMoreWorkspace.value = false;
|
||||
return;
|
||||
}
|
||||
workspacePage.value = nextPage;
|
||||
// 追加新数据到现有 treeNodes
|
||||
const newNodes = buildTreeNodes(newTree);
|
||||
treeNodes.value = [...treeNodes.value, ...newNodes];
|
||||
} catch {
|
||||
// 错误已由全局拦截器处理
|
||||
} finally {
|
||||
workspaceLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseWorkflow = () => {
|
||||
selectedWorkflowDetail.value = null;
|
||||
inputBarRef.value?.resetAll();
|
||||
};
|
||||
|
||||
const handleMenuChange = (menu: string) => {
|
||||
activeMenu.value = menu;
|
||||
};
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
if (!selectedWorkflowDetail.value) return;
|
||||
const mc = mainContentRef.value;
|
||||
if (!mc) return;
|
||||
const formatTime = (d: Date) =>
|
||||
String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||||
|
||||
if (!mc.validateFormFields()) return;
|
||||
|
||||
try {
|
||||
// 1. 构建节点输入参数
|
||||
const nodeInputParams = selectedWorkflowDetail.value.nodeInputParams?.map((node: any) => {
|
||||
const nodeParam: any = { ...node };
|
||||
|
||||
if (node.formConfig && Array.isArray(node.formConfig)) {
|
||||
nodeParam.formConfig = node.formConfig.map((field: any) => {
|
||||
// HTTP body 处理
|
||||
if (String(node.nodeCode || '').toLowerCase() === 'http' && field.field === 'body' && field.value && typeof field.value === 'object') {
|
||||
const bodyValue = { ...field.value };
|
||||
Object.entries(bodyValue).forEach(([bodyKey, bodyItem]: [string, any]) => {
|
||||
if (!bodyItem || bodyItem.showInForm !== true) return;
|
||||
const bodyFieldKey = `${node.id}_body_${bodyKey}`;
|
||||
const userVal = mc.formValues[bodyFieldKey];
|
||||
bodyValue[bodyKey] = {
|
||||
...bodyItem,
|
||||
value: userVal !== undefined ? userVal : bodyItem.value,
|
||||
};
|
||||
});
|
||||
return { ...field, value: bodyValue };
|
||||
}
|
||||
|
||||
const fieldKey = `${node.id}_${field.field || field.label}`;
|
||||
return {
|
||||
...field,
|
||||
value: mc.formValues[fieldKey] !== undefined ? mc.formValues[fieldKey] : field.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return nodeParam;
|
||||
}) || [];
|
||||
|
||||
// 2. 构建 flowContent
|
||||
const updatedFlowContent = {
|
||||
...selectedWorkflowDetail.value.flowContent,
|
||||
nodes: nodeInputParams,
|
||||
};
|
||||
|
||||
// 3. 收集文件
|
||||
const fileUrls: string[] = [];
|
||||
Object.values(mc.fieldFiles).forEach((files: any) => {
|
||||
files.forEach((f: any) => {
|
||||
if (f.url && !fileUrls.includes(f.url)) fileUrls.push(f.url);
|
||||
});
|
||||
});
|
||||
|
||||
// 4. 构建请求参数
|
||||
const params = {
|
||||
flowId: selectedWorkflowDetail.value.id,
|
||||
flowContent: updatedFlowContent,
|
||||
nodeInputParams: nodeInputParams,
|
||||
sessionId: getSessionId(),
|
||||
desc: message,
|
||||
flowName: selectedWorkflowDetail.value.flowName || '',
|
||||
fileUrl: fileUrls,
|
||||
resultUrl: selectedWorkflowDetail.value.resultUrl || '',
|
||||
templates: mc.templates || [],
|
||||
};
|
||||
|
||||
// 5. 执行
|
||||
await executeFlow(params);
|
||||
ElMessage.success('执行完成');
|
||||
} catch {
|
||||
// 错误由全局拦截器处理
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectHistory = (id: string) => {
|
||||
activeHistoryId.value = id;
|
||||
activeMenu.value = 'chat';
|
||||
ElMessage.success('已切换会话');
|
||||
const addMessage = (msg: ChatMessage) => {
|
||||
const id = activeHistoryId.value;
|
||||
if (!id) return;
|
||||
const list = sessionMessages.value.get(id);
|
||||
if (list) list.push(msg);
|
||||
else sessionMessages.value.set(id, [msg]);
|
||||
};
|
||||
|
||||
const handleCreateHistory = () => {
|
||||
const id = 'chat-' + Date.now();
|
||||
const handleSend = async (message: string) => {
|
||||
if (!selectedWorkflowDetail.value) {
|
||||
ElMessage.warning('请先选择一个工作流');
|
||||
return;
|
||||
}
|
||||
const mc = mainContentRef.value;
|
||||
if (!mc) return;
|
||||
|
||||
if (!mc.validateFormFields()) return;
|
||||
|
||||
// 获取当前会话的 sessionId
|
||||
const curSession = historyList.value.find((h) => h.id === activeHistoryId.value);
|
||||
if (!curSession) return;
|
||||
|
||||
// 添加用户消息
|
||||
addMessage({
|
||||
id: 'msg-' + Date.now() + '-user',
|
||||
content: message || '执行工作流',
|
||||
time: formatTime(new Date()),
|
||||
isUser: true,
|
||||
});
|
||||
|
||||
curSession.status = 'executing';
|
||||
|
||||
const sessionId = curSession.sessionId || getSessionId();
|
||||
|
||||
try {
|
||||
// 1. 构建节点输入参数
|
||||
const nodeInputParams = selectedWorkflowDetail.value.nodeInputParams?.map((node: any) => {
|
||||
const nodeParam: any = { ...node };
|
||||
|
||||
if (node.formConfig && Array.isArray(node.formConfig)) {
|
||||
nodeParam.formConfig = node.formConfig.map((field: any) => {
|
||||
// HTTP body 处理
|
||||
if (String(node.nodeCode || '').toLowerCase() === 'http' && field.field === 'body' && field.value && typeof field.value === 'object') {
|
||||
const bodyValue = { ...field.value };
|
||||
Object.entries(bodyValue).forEach(([bodyKey, bodyItem]: [string, any]) => {
|
||||
if (!bodyItem || bodyItem.showInForm !== true) return;
|
||||
const bodyFieldKey = `${node.id}_body_${bodyKey}`;
|
||||
const userVal = mc.formValues[bodyFieldKey];
|
||||
bodyValue[bodyKey] = {
|
||||
...bodyItem,
|
||||
value: userVal !== undefined ? userVal : bodyItem.value,
|
||||
};
|
||||
});
|
||||
return { ...field, value: bodyValue };
|
||||
}
|
||||
|
||||
const fieldKey = `${node.id}_${field.field || field.label}`;
|
||||
return {
|
||||
...field,
|
||||
value: mc.formValues[fieldKey] !== undefined ? mc.formValues[fieldKey] : field.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return nodeParam;
|
||||
}) || [];
|
||||
|
||||
// 2. 构建 flowContent
|
||||
const updatedFlowContent = {
|
||||
...selectedWorkflowDetail.value.flowContent,
|
||||
nodes: nodeInputParams,
|
||||
};
|
||||
|
||||
// 3. 收集文件
|
||||
const fileUrls: string[] = [];
|
||||
Object.values(mc.fieldFiles).forEach((files: any) => {
|
||||
files.forEach((f: any) => {
|
||||
if (f.url && !fileUrls.includes(f.url)) fileUrls.push(f.url);
|
||||
});
|
||||
});
|
||||
|
||||
// 4. 构建请求参数
|
||||
const params = {
|
||||
flowId: selectedWorkflowDetail.value.id,
|
||||
flowContent: updatedFlowContent,
|
||||
nodeInputParams: nodeInputParams,
|
||||
sessionId: sessionId,
|
||||
desc: message,
|
||||
flowName: selectedWorkflowDetail.value.flowName || '',
|
||||
fileUrl: fileUrls,
|
||||
resultUrl: selectedWorkflowDetail.value.resultUrl || '',
|
||||
templates: mc.templates || [],
|
||||
};
|
||||
|
||||
// 5. 执行
|
||||
await executeFlow(params);
|
||||
// 标记完成
|
||||
const doneSession = historyList.value.find((h) => h.id === activeHistoryId.value);
|
||||
if (doneSession) doneSession.status = 'completed';
|
||||
addMessage({
|
||||
id: 'msg-' + Date.now() + '-done',
|
||||
content: '✅ 执行完成,可前往工作空间查看产出',
|
||||
time: formatTime(new Date()),
|
||||
isUser: false,
|
||||
});
|
||||
ElMessage.success('执行完成');
|
||||
// 刷新工作空间树
|
||||
getExecutionList().then((res) => {
|
||||
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
||||
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
||||
});
|
||||
} catch {
|
||||
const failSession = historyList.value.find((h) => h.id === activeHistoryId.value);
|
||||
if (failSession) failSession.status = 'failed';
|
||||
addMessage({
|
||||
id: 'msg-' + Date.now() + '-fail',
|
||||
content: '❌ 执行失败,请重试或联系管理员',
|
||||
time: formatTime(new Date()),
|
||||
isUser: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectHistory = async (id: string) => {
|
||||
activeHistoryId.value = id;
|
||||
activeMenu.value = 'chat';
|
||||
// 加载该会话的工作流详情,渲染可编辑表单
|
||||
const session = historyList.value.find((h) => h.id === id);
|
||||
if (session && !session.id.startsWith('virtual_')) {
|
||||
try {
|
||||
const res = await getExecutionDetail(session.id);
|
||||
if (res.data) {
|
||||
selectedWorkflowDetail.value = res.data;
|
||||
}
|
||||
} catch {
|
||||
// 无关联执行数据,仅展示消息
|
||||
}
|
||||
} else {
|
||||
selectedWorkflowDetail.value = null;
|
||||
}
|
||||
// 清除 completed/failed 标记
|
||||
if (session?.status === 'completed' || session?.status === 'failed') {
|
||||
session.status = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const createNewSession = () => {
|
||||
const sessionId = `virtual_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
historyList.value.unshift({
|
||||
id,
|
||||
id: sessionId,
|
||||
sessionId,
|
||||
title: '新会话 ' + (historyList.value.length + 1),
|
||||
time: '刚刚',
|
||||
});
|
||||
activeHistoryId.value = id;
|
||||
activeHistoryId.value = sessionId;
|
||||
activeMenu.value = 'chat';
|
||||
ElMessage.success('已新建会话');
|
||||
selectedWorkflowDetail.value = null;
|
||||
inputBarRef.value?.resetAll();
|
||||
sessionMessages.value.set(sessionId, []); // 清空消息
|
||||
};
|
||||
|
||||
const handleCreateHistory = () => {
|
||||
createNewSession();
|
||||
};
|
||||
|
||||
const handleDeleteHistory = async (id: string) => {
|
||||
@@ -362,7 +491,7 @@ const handleDeleteHistory = async (id: string) => {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const isVirtual = id.startsWith('chat-');
|
||||
const isVirtual = id.startsWith('virtual_');
|
||||
if (!isVirtual) {
|
||||
try {
|
||||
await deleteExecutionSession({ id });
|
||||
@@ -373,14 +502,20 @@ const handleDeleteHistory = async (id: string) => {
|
||||
const idx = historyList.value.findIndex((item) => item.id === id);
|
||||
if (idx < 0) return;
|
||||
historyList.value.splice(idx, 1);
|
||||
if (activeHistoryId.value === id && historyList.value.length > 0) {
|
||||
activeHistoryId.value = historyList.value[0].id;
|
||||
sessionMessages.value.delete(id);
|
||||
if (activeHistoryId.value === id) {
|
||||
if (historyList.value.length > 0) {
|
||||
activeHistoryId.value = historyList.value[0].id;
|
||||
} else {
|
||||
createNewSession();
|
||||
}
|
||||
}
|
||||
ElMessage.success('已删除会话');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
// 不自动创建 / 选中会话,默认显示占位引导页
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -429,4 +564,4 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
min-height: 60vh;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user