fix: 贴片模板显示逻辑改为按节点实例patchLayout开关判断
- currentWorkflowHasPatchLayout 改为检查节点实例的 patchLayout 字段 - InputBar 工作流对接真实接口,禁用技能选择 - home/index.vue 移除 mock 数据,工作空间对接真实接口 - 工作空间增加预览/下载/删除功能
This commit is contained in:
@@ -2,17 +2,12 @@
|
|||||||
<div class="input-shell">
|
<div class="input-shell">
|
||||||
<div class="input-card" :class="{ 'is-focused': isFocused }">
|
<div class="input-card" :class="{ 'is-focused': isFocused }">
|
||||||
<!-- 已选中状态标签栏 -->
|
<!-- 已选中状态标签栏 -->
|
||||||
<div v-if="selectedWorkflowId !== null || selectedSkill !== 'default'" class="selected-tags">
|
<div v-if="selectedWorkflowId !== null" class="selected-tags">
|
||||||
<div v-if="selectedWorkflowId !== null" class="selected-tag workflow-tag">
|
<div class="selected-tag workflow-tag">
|
||||||
<el-icon><Promotion /></el-icon>
|
<el-icon><Promotion /></el-icon>
|
||||||
<span>{{ commonWorkflows.find((w) => w.id === selectedWorkflowId)?.name }}</span>
|
<span>{{ commonWorkflows.find((w) => w.id === selectedWorkflowId)?.name }}</span>
|
||||||
<el-icon class="tag-close" @click="selectedWorkflowId = null"><Close /></el-icon>
|
<el-icon class="tag-close" @click="selectedWorkflowId = null"><Close /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedSkill !== 'default'" class="selected-tag skill-tag">
|
|
||||||
<el-icon><MagicStick /></el-icon>
|
|
||||||
<span>{{ skillLabels[selectedSkill] }}</span>
|
|
||||||
<el-icon class="tag-close" @click="selectedSkill = 'default'"><Close /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文本输入区 -->
|
<!-- 文本输入区 -->
|
||||||
@@ -31,28 +26,15 @@
|
|||||||
<!-- 底部工具栏 -->
|
<!-- 底部工具栏 -->
|
||||||
<div class="input-toolbar">
|
<div class="input-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<!-- 上传附件 -->
|
|
||||||
<button class="tool-icon-btn" title="上传文件" @click="handleAttachment">
|
<button class="tool-icon-btn" title="上传文件" @click="handleAttachment">
|
||||||
<el-icon><Paperclip /></el-icon>
|
<el-icon><Paperclip /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 选择技能 -->
|
<!-- 技能暂已禁用 -->
|
||||||
<el-dropdown trigger="click" placement="top-start" @command="handleSkillSelect">
|
<button class="tool-icon-btn" title="技能暂不可用" disabled>
|
||||||
<button class="tool-icon-btn" :class="{ active: selectedSkill !== 'default' }" title="选择技能">
|
<el-icon><MagicStick /></el-icon>
|
||||||
<el-icon><MagicStick /></el-icon>
|
<span class="tool-label">技能</span>
|
||||||
<span class="tool-label">技能</span>
|
</button>
|
||||||
</button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="default">
|
|
||||||
<el-icon><CircleClose /></el-icon>不使用技能
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item divided command="code-review">代码审查</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="refactor">代码重构</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="writing">内容写作</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
@@ -81,9 +63,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { Top, MagicStick, Promotion, Close, CircleClose, Paperclip } from '@element-plus/icons-vue';
|
import { Top, MagicStick, Promotion, Close, Paperclip } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getWorkflowList } from '/@/api/settings/creation';
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'send', message: string): void;
|
(e: 'send', message: string): void;
|
||||||
@@ -98,36 +81,30 @@ interface Workflow {
|
|||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
const selectedSkill = ref('default');
|
|
||||||
const selectedWorkflowId = ref<number | null>(null);
|
const selectedWorkflowId = ref<number | null>(null);
|
||||||
|
const commonWorkflows = ref<Workflow[]>([]);
|
||||||
|
|
||||||
const skillLabels: Record<string, string> = {
|
const fetchWorkflows = async () => {
|
||||||
default: '选择技能',
|
try {
|
||||||
'code-review': '代码审查',
|
const res = await getWorkflowList();
|
||||||
refactor: '代码重构',
|
const workflows = res.data?.listFlowUserRes?.list || [];
|
||||||
writing: '内容写作',
|
commonWorkflows.value = workflows.map((w) => ({
|
||||||
|
id: Number(w.id),
|
||||||
|
name: w.flowName || '未命名',
|
||||||
|
prefix: '[工作流] ' + (w.flowName || '') + ':\n',
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
commonWorkflows.value = [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const skillPrefixes: Record<string, string> = {
|
|
||||||
default: '',
|
|
||||||
'code-review': '[技能:代码审查] 请帮我审查下面这段代码,找出潜在问题并给出改进建议:\n',
|
|
||||||
refactor: '[技能:代码重构] 请帮我重构下面这段代码,提升可读性和可维护性:\n',
|
|
||||||
writing: '[技能:内容写作] 请根据下面的需求帮我创作内容:\n',
|
|
||||||
};
|
|
||||||
|
|
||||||
const commonWorkflows: Workflow[] = [
|
|
||||||
{ id: 1, name: '审查', prefix: '[工作流:审查] 完成后请自动生成单元测试:\n' },
|
|
||||||
{ id: 2, name: '文档再编码', prefix: '[工作流:文档再编码] 请先编写接口文档,再实现代码:\n' },
|
|
||||||
{ id: 3, name: '代码重构', prefix: '[工作流:代码重构] 请帮我重构这段代码:\n' },
|
|
||||||
{ id: 4, name: '需求分析', prefix: '[工作流:需求分析] 请帮我分析这个需求并拆解任务:\n' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
const msg = message.value.trim();
|
const msg = message.value.trim();
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
const skillPrefix = skillPrefixes[selectedSkill.value] || '';
|
const workflowPrefix = selectedWorkflowId.value !== null
|
||||||
const workflowPrefix = selectedWorkflowId.value !== null ? commonWorkflows.find((w) => w.id === selectedWorkflowId.value)?.prefix || '' : '';
|
? commonWorkflows.value.find((w) => w.id === selectedWorkflowId.value)?.prefix || ''
|
||||||
const finalMessage = (skillPrefix + workflowPrefix + msg).trim();
|
: '';
|
||||||
|
const finalMessage = (workflowPrefix + msg).trim();
|
||||||
emit('send', finalMessage);
|
emit('send', finalMessage);
|
||||||
message.value = '';
|
message.value = '';
|
||||||
selectedWorkflowId.value = null;
|
selectedWorkflowId.value = null;
|
||||||
@@ -137,13 +114,13 @@ const handleAttachment = () => {
|
|||||||
ElMessage.info('附件上传功能开发中...');
|
ElMessage.info('附件上传功能开发中...');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSkillSelect = (key: string | number | object | null) => {
|
|
||||||
selectedSkill.value = String(key ?? 'default');
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleWorkflow = (id: number) => {
|
const toggleWorkflow = (id: number) => {
|
||||||
selectedWorkflowId.value = selectedWorkflowId.value === id ? null : id;
|
selectedWorkflowId.value = selectedWorkflowId.value === id ? null : id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchWorkflows();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -196,12 +173,6 @@ const toggleWorkflow = (id: number) => {
|
|||||||
border: 1px solid #bfdbfe;
|
border: 1px solid #bfdbfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.skill-tag {
|
|
||||||
background: #f5f3ff;
|
|
||||||
color: #7c3aed;
|
|
||||||
border: 1px solid #ddd6fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-close {
|
.tag-close {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
+41
-67
@@ -12,6 +12,7 @@
|
|||||||
@delete-history="handleDeleteHistory"
|
@delete-history="handleDeleteHistory"
|
||||||
@preview-node="previewNode"
|
@preview-node="previewNode"
|
||||||
@download-node="downloadNode"
|
@download-node="downloadNode"
|
||||||
|
@delete-node="handleDeleteNode"
|
||||||
/>
|
/>
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<MainContent :active-menu="activeMenu" />
|
<MainContent :active-menu="activeMenu" />
|
||||||
@@ -80,7 +81,7 @@ const historyList = ref<HistoryItem[]>([
|
|||||||
|
|
||||||
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||||
|
|
||||||
const joinUrl = (b: string, p: string) => `${b.replace(/\/$/, '')}${p.startsWith('/') ? p : `/${p}`}`;
|
const joinUrl = (b: string, p: string) => `${b.replace(/\/$/, '')}${p.startsWith('/') ? p : '/' + p}`;
|
||||||
const buildAssetUrl = (p?: string) =>
|
const buildAssetUrl = (p?: string) =>
|
||||||
!p
|
!p
|
||||||
? ''
|
? ''
|
||||||
@@ -94,18 +95,18 @@ const buildAssetUrl = (p?: string) =>
|
|||||||
|
|
||||||
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
||||||
tree.map((d, di) => ({
|
tree.map((d, di) => ({
|
||||||
id: `date-${di}`,
|
id: 'date-' + di,
|
||||||
label: d.createDate,
|
label: d.createDate,
|
||||||
nodeType: 'date',
|
nodeType: 'date',
|
||||||
children: (d.flows || []).map((f, fi) => ({
|
children: (d.flows || []).map((f, fi) => ({
|
||||||
id: `flow-${di}-${fi}`,
|
id: 'flow-' + di + '-' + fi,
|
||||||
label: f.flowName || '未命名工作流',
|
label: f.flowName || '未命名工作流',
|
||||||
nodeType: 'contentType',
|
nodeType: 'contentType',
|
||||||
workflowId: f.Id,
|
workflowId: f.Id,
|
||||||
sessionId: f.sessionId,
|
sessionId: f.sessionId,
|
||||||
children: (f.items || []).map((item, ii) => ({
|
children: (f.items || []).map((item, ii) => ({
|
||||||
id: `item-${di}-${fi}-${ii}`,
|
id: 'item-' + di + '-' + fi + '-' + ii,
|
||||||
label: item.label || `作品${ii + 1}`,
|
label: item.label || '作品' + (ii + 1),
|
||||||
nodeType: 'title',
|
nodeType: 'title',
|
||||||
fileUrl: item.content,
|
fileUrl: item.content,
|
||||||
fileType: item.type,
|
fileType: item.type,
|
||||||
@@ -115,70 +116,12 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 模拟mock数据
|
|
||||||
const mockTreeData: ExecutionTreeItem[] = [
|
|
||||||
{
|
|
||||||
createDate: '今天',
|
|
||||||
flows: [
|
|
||||||
{
|
|
||||||
Id: 1,
|
|
||||||
flowName: '代码审查任务',
|
|
||||||
sessionId: 'session-1',
|
|
||||||
items: [
|
|
||||||
{ label: '审查结果.md', content: 'https://placekitten.com/800/600', type: 'text/markdown', timestamp: '' },
|
|
||||||
{ label: '流程图.png', content: 'https://placekitten.com/800/600', type: 'image/png', timestamp: '' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: 2,
|
|
||||||
flowName: '需求分析',
|
|
||||||
sessionId: 'session-2',
|
|
||||||
items: [{ label: '需求拆解.txt', content: '# 需求分析结果\n\n- 功能点一\n- 功能点二\n- 需要优化', type: 'text/plain', timestamp: '' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
createDate: '昨天',
|
|
||||||
flows: [
|
|
||||||
{
|
|
||||||
Id: 3,
|
|
||||||
flowName: '生成演示视频',
|
|
||||||
sessionId: 'session-3',
|
|
||||||
items: [{ label: '输出视频.mp4', content: 'https://www.w3schools.com/html/mov_bbb.mp4', type: 'video/mp4', timestamp: '' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getList = async () => {
|
|
||||||
treeLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getExecutionList();
|
|
||||||
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
|
||||||
const data = res.data?.tree || [];
|
|
||||||
// 如果后端没有数据,使用mock数据展示
|
|
||||||
treeNodes.value = data.length > 0 ? buildTreeNodes(data) : buildTreeNodes(mockTreeData);
|
|
||||||
} catch {
|
|
||||||
// 出错时使用mock数据展示
|
|
||||||
treeNodes.value = buildTreeNodes(mockTreeData);
|
|
||||||
imgAddressPrefix.value = '';
|
|
||||||
} finally {
|
|
||||||
treeLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPreviewMode = (fileType?: string): 'image' | 'video' | 'audio' | 'iframe' => {
|
const getPreviewMode = (fileType?: string): 'image' | 'video' | 'audio' | 'iframe' => {
|
||||||
if (!fileType) return 'iframe';
|
if (!fileType) return 'iframe';
|
||||||
const lower = fileType.toLowerCase();
|
const lower = fileType.toLowerCase();
|
||||||
if (lower.includes('image') || lower.includes('jpg') || lower.includes('png') || lower.includes('gif')) {
|
if (lower.includes('image') || lower.includes('jpg') || lower.includes('png') || lower.includes('gif')) return 'image';
|
||||||
return 'image';
|
if (lower.includes('video') || lower.includes('mp4') || lower.includes('webm')) return 'video';
|
||||||
}
|
if (lower.includes('audio') || lower.includes('mp3') || lower.includes('wav')) return 'audio';
|
||||||
if (lower.includes('video') || lower.includes('mp4') || lower.includes('webm')) {
|
|
||||||
return 'video';
|
|
||||||
}
|
|
||||||
if (lower.includes('audio') || lower.includes('mp3') || lower.includes('wav')) {
|
|
||||||
return 'audio';
|
|
||||||
}
|
|
||||||
return 'iframe';
|
return 'iframe';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +143,37 @@ const downloadNode = (data: TreeNode) => {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteNode = (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 getList = async () => {
|
||||||
|
treeLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getExecutionList();
|
||||||
|
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
||||||
|
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
||||||
|
} catch {
|
||||||
|
treeNodes.value = [];
|
||||||
|
imgAddressPrefix.value = '';
|
||||||
|
} finally {
|
||||||
|
treeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMenuChange = (menu: string) => {
|
const handleMenuChange = (menu: string) => {
|
||||||
activeMenu.value = menu;
|
activeMenu.value = menu;
|
||||||
};
|
};
|
||||||
@@ -218,7 +192,7 @@ const handleCreateHistory = () => {
|
|||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
historyList.value.unshift({
|
historyList.value.unshift({
|
||||||
id,
|
id,
|
||||||
title: `新会话 ${historyList.value.length + 1}`,
|
title: '新会话 ' + (historyList.value.length + 1),
|
||||||
time: '刚刚',
|
time: '刚刚',
|
||||||
});
|
});
|
||||||
activeHistoryId.value = id;
|
activeHistoryId.value = id;
|
||||||
|
|||||||
@@ -1098,21 +1098,10 @@ const currentNodePatchLayout = computed(() => {
|
|||||||
});
|
});
|
||||||
return patchLayout;
|
return patchLayout;
|
||||||
});
|
});
|
||||||
// 判断当前工作流是否包含支持贴片布局的节点
|
// 判断当前工作流是否包含开启了贴片布局的节点
|
||||||
const currentWorkflowHasPatchLayout = computed(() => {
|
const currentWorkflowHasPatchLayout = computed(() => {
|
||||||
const nodes = currentWorkflowForCreation.value?.nodeInputParams || [];
|
const nodes = currentWorkflowForCreation.value?.nodeInputParams || [];
|
||||||
if (!nodes.length || !nodeLibraryGroups.value.length) return false;
|
return nodes.some((node: any) => node.patchLayout === true);
|
||||||
return nodes.some((node: any) => {
|
|
||||||
if (!node.nodeCode) return false;
|
|
||||||
for (const group of nodeLibraryGroups.value) {
|
|
||||||
for (const item of group.items || []) {
|
|
||||||
if (item.nodeCode === node.nodeCode && item.patchLayout) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
// 获取当前节点的模型类型
|
// 获取当前节点的模型类型
|
||||||
const currentNodeModelType = computed(() => {
|
const currentNodeModelType = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user