finish work
This commit is contained in:
parent
fba7ed29f0
commit
27c3f24089
@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="showDiagram" width="800px" append-to-body class="no-padding-dialog">
|
<el-dialog v-model="showDiagram" width="800px" append-to-body class="no-padding-dialog">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center;">
|
||||||
<span>Tool Diagram</span>
|
<span>Tool Diagram</span>
|
||||||
|
 
|
||||||
|
<el-button size="small" type="primary" @click="() => {}">重置</el-button>
|
||||||
<el-button size="small" type="primary" @click="() => {}">开启自检程序</el-button>
|
<el-button size="small" type="primary" @click="() => {}">开启自检程序</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar height="80vh">
|
<el-scrollbar height="80vh">
|
||||||
<Diagram />
|
<!-- <Diagram /> -->
|
||||||
|
<SwimPool />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<!-- <el-button @click="showDiagram = true" type="primary" style="margin-bottom: 16px;">
|
<!-- <el-button @click="showDiagram = true" type="primary" style="margin-bottom: 16px;">
|
||||||
@ -18,7 +21,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Diagram from './diagram.vue';
|
import Diagram from './diagram.vue';
|
||||||
|
import SwimPool from './swim-pool.vue';
|
||||||
const showDiagram = ref(true);
|
const showDiagram = ref(true);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="svgContainer" class="diagram-container"></div>
|
<div style="display: flex; align-items: center; gap: 16px;">
|
||||||
|
<div ref="svgContainer" class="diagram-container"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -105,7 +107,7 @@ function renderSvg() {
|
|||||||
.append('svg')
|
.append('svg')
|
||||||
.attr('width', width)
|
.attr('width', width)
|
||||||
.attr('height', height)
|
.attr('height', height)
|
||||||
.style('user-select', 'none');
|
.style('user-select', 'none') as any;
|
||||||
} else {
|
} else {
|
||||||
svg.attr('width', width).attr('height', height);
|
svg.attr('width', width).attr('height', height);
|
||||||
svg.selectAll('defs').remove();
|
svg.selectAll('defs').remove();
|
||||||
@ -116,21 +118,21 @@ function renderSvg() {
|
|||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'arrow')
|
.attr('id', 'arrow')
|
||||||
.attr('viewBox', '0 0 10 10')
|
.attr('viewBox', '0 0 8 8')
|
||||||
.attr('refX', 8)
|
.attr('refX', 6)
|
||||||
.attr('refY', 5)
|
.attr('refY', 4)
|
||||||
.attr('markerWidth', 6)
|
.attr('markerWidth', 5)
|
||||||
.attr('markerHeight', 6)
|
.attr('markerHeight', 5)
|
||||||
.attr('orient', 'auto')
|
.attr('orient', 'auto')
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
|
.attr('d', 'M 0 0 L 8 4 L 0 8 z')
|
||||||
.attr('fill', 'var(--main-color)');
|
.attr('fill', 'var(--main-color)');
|
||||||
|
|
||||||
// Draw edges with enter animation
|
// Draw edges with enter animation
|
||||||
const allSections: { id: string, section: any }[] = [];
|
const allSections: { id: string, section: any }[] = [];
|
||||||
(state.edges || []).forEach(edge => {
|
(state.edges || []).forEach(edge => {
|
||||||
const sections = edge.sections || [];
|
const sections = edge.sections || [];
|
||||||
sections.forEach((section, idx) => {
|
sections.forEach((section: any, idx: number) => {
|
||||||
allSections.push({
|
allSections.push({
|
||||||
id: (edge.id || '') + '-' + (section.id || idx),
|
id: (edge.id || '') + '-' + (section.id || idx),
|
||||||
section
|
section
|
||||||
@ -201,6 +203,7 @@ function renderSvg() {
|
|||||||
|
|
||||||
nodeGroup.exit().remove();
|
nodeGroup.exit().remove();
|
||||||
|
|
||||||
|
// 节点 enter
|
||||||
const nodeGroupEnter = nodeGroup.enter()
|
const nodeGroupEnter = nodeGroup.enter()
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', 'node')
|
.attr('class', 'node')
|
||||||
@ -235,7 +238,7 @@ function renderSvg() {
|
|||||||
.attr('width', d => d.width)
|
.attr('width', d => d.width)
|
||||||
.attr('height', d => d.height)
|
.attr('height', d => d.height)
|
||||||
.attr('rx', 16)
|
.attr('rx', 16)
|
||||||
.attr('fill', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-color)')
|
.attr('fill', 'var(--main-color)')
|
||||||
.attr('opacity', d => state.selectedNodeId === d.id ? 0.25 : 0.12)
|
.attr('opacity', d => state.selectedNodeId === d.id ? 0.25 : 0.12)
|
||||||
.attr('stroke', 'var(--main-color)')
|
.attr('stroke', 'var(--main-color)')
|
||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2);
|
||||||
@ -263,9 +266,35 @@ function renderSvg() {
|
|||||||
.ease(d3.easeCubicInOut)
|
.ease(d3.easeCubicInOut)
|
||||||
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
||||||
|
|
||||||
|
// 高亮选中节点动画
|
||||||
|
nodeGroup.select('rect')
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr('opacity', d => state.selectedNodeId === d.id ? 0.55 : 0.12)
|
||||||
|
.attr('stroke-width', d => state.selectedNodeId === d.id ? 4 : 2)
|
||||||
|
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-color)')
|
||||||
|
.attr('fill', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-color)');
|
||||||
|
|
||||||
// 渲染结束后保存当前快照
|
// 渲染结束后保存当前快照
|
||||||
prevNodes = state.nodes.map(n => ({ ...n }));
|
prevNodes = state.nodes.map(n => ({ ...n }));
|
||||||
prevEdges = (state.edges || []).map(e => ({ ...e, sections: e.sections ? e.sections.map(s => ({ ...s })) : [] }));
|
prevEdges = (state.edges || []).map(e => ({ ...e, sections: e.sections ? e.sections.map((s: any) => ({ ...s })) : [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置连接为链表结构
|
||||||
|
function resetConnections() {
|
||||||
|
if (!state.nodes.length) return;
|
||||||
|
const edges = [];
|
||||||
|
for (let i = 0; i < state.nodes.length - 1; ++i) {
|
||||||
|
const prev = state.nodes[i];
|
||||||
|
const next = state.nodes[i + 1];
|
||||||
|
edges.push({
|
||||||
|
id: prev.id + '-' + next.id,
|
||||||
|
sources: [prev.id],
|
||||||
|
targets: [next.id]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.edges = edges;
|
||||||
|
recomputeLayout().then(renderSvg);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
128
renderer/src/components/main-panel/tool/swim-pool.vue
Normal file
128
renderer/src/components/main-panel/tool/swim-pool.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="swim-pool">
|
||||||
|
<transition-group name="lane-move" tag="div">
|
||||||
|
<div v-for="(lane, laneIdx) in lanes" :key="lane.id" class="swim-lane" @dragover.prevent
|
||||||
|
@drop="onDrop(laneIdx)">
|
||||||
|
<div class="lane-title">Group {{ laneIdx + 1 }}</div>
|
||||||
|
<div v-for="tool in lane.tools" :key="tool.name" class="tool-card" draggable="true"
|
||||||
|
@dragstart="onDragStart(tool, laneIdx)">
|
||||||
|
{{ tool.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ToolItem } from '@/hook/type';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
// 工具类型
|
||||||
|
interface Tool {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lane {
|
||||||
|
id: string;
|
||||||
|
tools: ToolItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具列表
|
||||||
|
const tools = ref<ToolItem[]>([]);
|
||||||
|
|
||||||
|
// 泳道列表
|
||||||
|
const lanes = ref<Lane[]>([]);
|
||||||
|
|
||||||
|
// 获取所有工具
|
||||||
|
const getAllTools = async () => {
|
||||||
|
const items = [];
|
||||||
|
for (const client of mcpClientAdapter.clients) {
|
||||||
|
const clientTools = await client.getTools();
|
||||||
|
items.push(...clientTools.values());
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
tools.value = await getAllTools();
|
||||||
|
console.log(tools.value);
|
||||||
|
|
||||||
|
lanes.value = tools.value.map((tool, idx) => ({
|
||||||
|
id: `lane-${idx}`,
|
||||||
|
tools: [tool]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拖拽信息
|
||||||
|
let dragInfo: { tool: ToolItem | null; fromLane: number } = { tool: null, fromLane: -1 };
|
||||||
|
|
||||||
|
// 拖拽开始
|
||||||
|
function onDragStart(tool: ToolItem, fromLane: number) {
|
||||||
|
dragInfo = { tool, fromLane };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽释放
|
||||||
|
function onDrop(toLane: number) {
|
||||||
|
if (dragInfo.tool && dragInfo.fromLane !== -1 && dragInfo.fromLane !== toLane) {
|
||||||
|
// 从原泳道移除
|
||||||
|
lanes.value[dragInfo.fromLane].tools = lanes.value[dragInfo.fromLane].tools.filter(
|
||||||
|
t => t.name !== dragInfo.tool!.name
|
||||||
|
);
|
||||||
|
// 加入新泳道
|
||||||
|
lanes.value[toLane].tools.push(dragInfo.tool);
|
||||||
|
|
||||||
|
// 如果原泳道已空,删除该泳道
|
||||||
|
if (lanes.value[dragInfo.fromLane].tools.length === 0) {
|
||||||
|
lanes.value.splice(dragInfo.fromLane, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新排序,确保泳道顺序和索引一致
|
||||||
|
lanes.value = lanes.value.map((lane, idx) => ({
|
||||||
|
...lane,
|
||||||
|
// 可选:如果你希望泳道 id 也随序号变化
|
||||||
|
id: `lane-${idx}`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dragInfo = { tool: null, fromLane: -1 };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.swim-pool {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swim-lane {
|
||||||
|
border: 1px solid var(--sidebar);
|
||||||
|
min-height: 60px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lane-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
border: 1px solid var(--main-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画样式 */
|
||||||
|
.lane-move-move {
|
||||||
|
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user