finish work

This commit is contained in:
锦恢 2025-07-01 03:32:22 +08:00
parent fba7ed29f0
commit 27c3f24089
3 changed files with 174 additions and 14 deletions

View File

@ -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>
&ensp;
<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>

View File

@ -1,5 +1,7 @@
<template> <template>
<div style="display: flex; align-items: center; gap: 16px;">
<div ref="svgContainer" class="diagram-container"></div> <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(() => {

View 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>