test news page
This commit is contained in:
parent
5bf0623327
commit
1244ee474e
@ -5,7 +5,6 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.svg">
|
<link rel="icon" href="/favicon.svg">
|
||||||
<link rel="stylesheet" href="/default-dark.css">
|
<link rel="stylesheet" href="/default-dark.css">
|
||||||
<link rel="stylesheet" href="/vscode.css">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>OpenMCP News Feature</title>
|
<title>OpenMCP News Feature</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -109,12 +109,6 @@ body {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.openmcp-logo {
|
|
||||||
width: 84px;
|
|
||||||
height: 84px;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin-bottom {
|
.margin-bottom {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import OmIcon from './OmIcon.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
version: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -9,7 +11,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="openmcp-header">
|
<header class="openmcp-header">
|
||||||
<img src="/favicon.svg" alt="OpenMCP Logo" width="72" height="72" class="openmcp-logo" />
|
<om-icon />
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
OpenMCP Client<sup><small>{{ props.version }}</small></sup>
|
OpenMCP Client<sup><small>{{ props.version }}</small></sup>
|
||||||
|
92
news/src/components/OmIcon.vue
Normal file
92
news/src/components/OmIcon.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<svg width="84" height="84" viewBox="0 0 612 612" fill="none" xmlns="http://www.w3.org/2000/svg" class="openmcp-logo">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
||||||
|
<stop offset="0" stop-color="#A1A7F6" />
|
||||||
|
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.2" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradient_2" gradientUnits="userSpaceOnUse" x1="110.5" y1="0" x2="110.5" y2="221">
|
||||||
|
<stop offset="0.468" stop-color="#BFBAF6" />
|
||||||
|
<stop offset="1" stop-color="#FFFFFF" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter color-interpolation-filters="sRGB" x="-219" y="-219" width="221" height="221" id="filter_3">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix_1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceAlpha" />
|
||||||
|
<feOffset dx="0" dy="4" />
|
||||||
|
<feGaussianBlur stdDeviation="2" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.251 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="gradient_4" gradientUnits="userSpaceOnUse" x1="55.5" y1="0" x2="55.5" y2="111">
|
||||||
|
<stop offset="0" stop-color="#FFFFFF" />
|
||||||
|
<stop offset="1" stop-color="#A8A7F3" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter color-interpolation-filters="sRGB" x="-109" y="-109" width="111" height="111" id="filter_5">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix_1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceAlpha" />
|
||||||
|
<feOffset dx="0" dy="4" />
|
||||||
|
<feGaussianBlur stdDeviation="2" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.251 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="gradient_6" gradientUnits="userSpaceOnUse" x1="174" y1="0" x2="174" y2="348">
|
||||||
|
<stop offset="0.182" stop-color="#A594F6" />
|
||||||
|
<stop offset="1" stop-color="#F4E5FF" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter color-interpolation-filters="sRGB" x="-346" y="-346" width="348" height="348" id="filter_7">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix_1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceAlpha" />
|
||||||
|
<feOffset dx="0" dy="4" />
|
||||||
|
<feGaussianBlur stdDeviation="2" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.251 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="gradient_8" gradientUnits="userSpaceOnUse" x1="57" y1="0" x2="57" y2="114">
|
||||||
|
<stop offset="0" stop-color="#FFFFFF" />
|
||||||
|
<stop offset="0.614" stop-color="#C7BAF8" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter color-interpolation-filters="sRGB" x="-112" y="-112" width="114" height="114" id="filter_9">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix_1" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceAlpha" />
|
||||||
|
<feOffset dx="0" dy="4" />
|
||||||
|
<feGaussianBlur stdDeviation="2" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.251 0" />
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(6 2)">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z"
|
||||||
|
fill="#5A00FF" fill-rule="evenodd" />
|
||||||
|
<path
|
||||||
|
d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z"
|
||||||
|
fill="url(#gradient_1)" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M0 110.5C0 49.4725 49.4725 0 110.5 0C171.527 0 221 49.4725 221 110.5C221 171.527 171.527 221 110.5 221C49.4725 221 0 171.527 0 110.5Z"
|
||||||
|
fill="url(#gradient_2)" fill-rule="evenodd" filter="url(#filter_3)" transform="translate(293 324)" />
|
||||||
|
<path
|
||||||
|
d="M0 55.5C0 24.8482 24.8482 0 55.5 0C86.1518 0 111 24.8482 111 55.5C111 86.1518 86.1518 111 55.5 111C24.8482 111 0 86.1518 0 55.5Z"
|
||||||
|
fill="url(#gradient_4)" fill-rule="evenodd" filter="url(#filter_5)" transform="translate(48 269)" />
|
||||||
|
<path
|
||||||
|
d="M0 174C0 77.9024 77.9024 0 174 0C270.098 0 348 77.9024 348 174C348 270.098 270.098 348 174 348C77.9024 348 0 270.098 0 174Z"
|
||||||
|
fill="url(#gradient_6)" fill-rule="evenodd" filter="url(#filter_7)" transform="translate(188 56)" />
|
||||||
|
<path
|
||||||
|
d="M0 57C0 25.5198 25.5198 0 57 0C88.4802 0 114 25.5198 114 57C114 88.4802 88.4802 114 57 114C25.5198 114 0 88.4802 0 57Z"
|
||||||
|
fill="url(#gradient_8)" fill-rule="evenodd" filter="url(#filter_9)" transform="translate(388 129)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.openmcp-logo {
|
||||||
|
width: 84px;
|
||||||
|
height: 84px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1.9",
|
"version": "0.1.9",
|
||||||
"changelogs": [
|
"changelogs": [
|
||||||
"Add mook feature: Automatically fill in test tool form data using random seeds or AI generation",
|
"Add mook functionality: Automatically fill in test tool form data using random seeds or AI generation.",
|
||||||
"Add tool self-check feature: Under openmcp's tool, click 'Tool Self-Check' on the right side of 'Tool Module' to enter self-check mode. In this mode, users can define the topological order of tool execution and perform automatic detection in one go.",
|
"Add tool self-check functionality: Under openmcp's tool, click 'Tool Self-Check' on the right side of 'Tool Module' to enter self-check mode. In this mode, users can define the topological order of tool execution and perform automatic detection in one go.",
|
||||||
"Fix issue #44: Complete platform adaptation for link redirection",
|
"Fix issue #44: Complete platform adaptation for link redirection.",
|
||||||
"Fix issue #36: Successful startup when not opening a folder",
|
"Fix issue #36: Ensure successful startup when not opening a folder.",
|
||||||
"Fix issue #45: Array type parameters not supported",
|
"Fix issue #45: Array type parameters are not supported.",
|
||||||
"Fix abnormal dialog style when pasting multi-line conversations into the dialog box"
|
"Fix the issue of abnormal dialog styles when pasting multi-line conversations into the dialog box."
|
||||||
],
|
],
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import './css/vscode.css';
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue'
|
import App from './App.vue';
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -52,6 +52,7 @@
|
|||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.25.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||||
"null-loader": "^4.0.1",
|
"null-loader": "^4.0.1",
|
||||||
|
"ompipe": "^1.0.2",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"rollup-plugin-visualizer": "^6.0.1",
|
"rollup-plugin-visualizer": "^6.0.1",
|
||||||
@ -10630,6 +10631,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ompipe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ompipe/-/ompipe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-D9SbKT2fqSkVxQtp0AffMdSYNZiScA9YOrS0iymyt6Q4wLWyUzvxhmFcJO8AxlwYLZY/IddwirgM46EfeNdq4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "4.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-exit-leak-free": {
|
"node_modules/on-exit-leak-free": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||||
|
@ -238,6 +238,7 @@
|
|||||||
"lint": "eslint src --ext ts",
|
"lint": "eslint src --ext ts",
|
||||||
"test": "node ./dist/test/e2e/runTest.js",
|
"test": "node ./dist/test/e2e/runTest.js",
|
||||||
"prepare:ocr": "rollup -c rollup.tesseract.js --bundleConfigAsCjs",
|
"prepare:ocr": "rollup -c rollup.tesseract.js --bundleConfigAsCjs",
|
||||||
|
"build:news": "npx tsx scripts/update-news-data.mts",
|
||||||
"build:task-loop": "npx vite build --config renderer/vite.config.task-loop.mjs && node renderer/scripts/task-loop.build.mjs"
|
"build:task-loop": "npx vite build --config renderer/vite.config.task-loop.mjs && node renderer/scripts/task-loop.build.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -281,6 +282,7 @@
|
|||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.25.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||||
"null-loader": "^4.0.1",
|
"null-loader": "^4.0.1",
|
||||||
|
"ompipe": "^1.0.2",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"rollup-plugin-visualizer": "^6.0.1",
|
"rollup-plugin-visualizer": "^6.0.1",
|
||||||
|
@ -1,547 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
|
||||||
<div ref="svgContainer" class="diagram-container"></div>
|
|
||||||
|
|
||||||
<!-- <template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
|
|
||||||
<div
|
|
||||||
v-if="state.hoverNodeId === node.id"
|
|
||||||
:style="getNodePopupStyle(node)"
|
|
||||||
class="node-popup"
|
|
||||||
>
|
|
||||||
<div>节点:{{ node.labels?.[0]?.text || node.id }}</div>
|
|
||||||
<div>宽: {{ node.width }}, 高: {{ node.height }}</div>
|
|
||||||
</div>
|
|
||||||
</template> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, nextTick, reactive, inject } from 'vue';
|
|
||||||
import * as d3 from 'd3';
|
|
||||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
||||||
import { mcpClientAdapter } from '@/views/connect/core';
|
|
||||||
import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
|
|
||||||
const svgContainer = ref<HTMLDivElement | null>(null);
|
|
||||||
let prevNodes: any[] = [];
|
|
||||||
let prevEdges: any[] = [];
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
nodes: [] as any[],
|
|
||||||
edges: [] as any[],
|
|
||||||
selectedNodeId: null as string | null,
|
|
||||||
draggingNodeId: null as string | null,
|
|
||||||
hoverNodeId: null as string | null,
|
|
||||||
offset: { x: 0, y: 0 },
|
|
||||||
dataView: new Map<string, NodeDataView>
|
|
||||||
});
|
|
||||||
|
|
||||||
const getAllTools = async () => {
|
|
||||||
const items = [];
|
|
||||||
for (const client of mcpClientAdapter.clients) {
|
|
||||||
const clientTools = await client.getTools();
|
|
||||||
items.push(...clientTools.values());
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
const recomputeLayout = async () => {
|
|
||||||
const elk = new ELK();
|
|
||||||
const elkGraph = {
|
|
||||||
id: 'root',
|
|
||||||
layoutOptions: {
|
|
||||||
'elk.direction': 'DOWN',
|
|
||||||
'elk.spacing.nodeNode': '40',
|
|
||||||
'elk.layered.spacing.nodeNodeBetweenLayers': '40'
|
|
||||||
},
|
|
||||||
children: state.nodes,
|
|
||||||
edges: state.edges
|
|
||||||
};
|
|
||||||
const layout = await elk.layout(elkGraph) as Node;
|
|
||||||
state.nodes.forEach((n, i) => {
|
|
||||||
const ln = layout.children?.find(c => c.id === n.id);
|
|
||||||
if (ln) {
|
|
||||||
n.x = ln.x;
|
|
||||||
n.y = ln.y;
|
|
||||||
n.width = ln.width;
|
|
||||||
n.height = ln.height;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state.edges = layout.edges || [];
|
|
||||||
return layout;
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawDiagram = async () => {
|
|
||||||
const tools = await getAllTools();
|
|
||||||
|
|
||||||
// 默认按照链表进行串联
|
|
||||||
const nodes = [] as Node[];
|
|
||||||
const edges = [] as Edge[];
|
|
||||||
|
|
||||||
for (let i = 0; i < tools.length - 1; ++i) {
|
|
||||||
const prev = tools[i];
|
|
||||||
const next = tools[i + 1];
|
|
||||||
edges.push({
|
|
||||||
id: prev.name + '-' + next.name,
|
|
||||||
sources: [prev.name],
|
|
||||||
targets: [next.name]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tool of tools) {
|
|
||||||
nodes.push({
|
|
||||||
id: tool.name,
|
|
||||||
width: 200,
|
|
||||||
height: 64, // 增加高度
|
|
||||||
labels: [{ text: tool.name || 'Tool' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
state.dataView.set(tool.name, {
|
|
||||||
tool,
|
|
||||||
status: 'waiting',
|
|
||||||
result: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
state.edges = edges;
|
|
||||||
state.nodes = nodes;
|
|
||||||
|
|
||||||
// 重新计算布局
|
|
||||||
await recomputeLayout();
|
|
||||||
|
|
||||||
// 绘制 svg
|
|
||||||
renderSvg();
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderSvg() {
|
|
||||||
const prevNodeMap = new Map(prevNodes.map(n => [n.id, n]));
|
|
||||||
const prevEdgeMap = new Map(prevEdges.map(e => [e.id, e]));
|
|
||||||
|
|
||||||
// 计算所有节点的最小x和最大x
|
|
||||||
const xs = state.nodes.map(n => (n.x || 0));
|
|
||||||
const minX = Math.min(...xs);
|
|
||||||
const maxX = Math.max(...xs.map((x, i) => x + (state.nodes[i].width || 160)));
|
|
||||||
const contentWidth = maxX - minX;
|
|
||||||
const svgWidth = Math.max(contentWidth + 120, 400); // 120为两侧留白
|
|
||||||
const offsetX = (svgWidth - contentWidth) / 2 - minX;
|
|
||||||
|
|
||||||
const height = Math.max(...state.nodes.map(n => (n.y || 0) + (n.height || 48)), 300) + 60;
|
|
||||||
|
|
||||||
// 不再全量清空,只清空 svg 元素
|
|
||||||
let svg = d3.select(svgContainer.value).select('svg');
|
|
||||||
if (svg.empty()) {
|
|
||||||
svg = d3
|
|
||||||
.select(svgContainer.value)
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', svgWidth)
|
|
||||||
.attr('height', height)
|
|
||||||
.style('user-select', 'none') as any;
|
|
||||||
} else {
|
|
||||||
svg.attr('width', svgWidth).attr('height', height);
|
|
||||||
svg.selectAll('defs').remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrow marker
|
|
||||||
svg
|
|
||||||
.append('defs')
|
|
||||||
.append('marker')
|
|
||||||
.attr('id', 'arrow')
|
|
||||||
.attr('viewBox', '0 0 8 8')
|
|
||||||
.attr('refX', 6)
|
|
||||||
.attr('refY', 4)
|
|
||||||
.attr('markerWidth', 5)
|
|
||||||
.attr('markerHeight', 5)
|
|
||||||
.attr('orient', 'auto')
|
|
||||||
.append('path')
|
|
||||||
.attr('d', 'M 0 0 L 8 4 L 0 8 z')
|
|
||||||
.attr('fill', 'var(--main-color)');
|
|
||||||
|
|
||||||
// 1. 创建/获取 main group
|
|
||||||
let mainGroup = svg.select('g.main-group');
|
|
||||||
if (mainGroup.empty()) {
|
|
||||||
mainGroup = svg.append('g').attr('class', 'main-group') as any;
|
|
||||||
}
|
|
||||||
mainGroup
|
|
||||||
.transition()
|
|
||||||
.duration(600)
|
|
||||||
.attr('transform', `translate(${offsetX}, 0)`);
|
|
||||||
|
|
||||||
// Draw edges with enter animation
|
|
||||||
const allSections: { id: string, section: any }[] = [];
|
|
||||||
(state.edges || []).forEach(edge => {
|
|
||||||
const sections = edge.sections || [];
|
|
||||||
sections.forEach((section: any, idx: number) => {
|
|
||||||
allSections.push({
|
|
||||||
id: (edge.id || '') + '-' + (section.id || idx),
|
|
||||||
section
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const edgeSelection = mainGroup.selectAll<SVGLineElement, any>('.edge')
|
|
||||||
.data(allSections, d => d.id);
|
|
||||||
|
|
||||||
edgeSelection.exit().remove();
|
|
||||||
|
|
||||||
const edgeEnter = edgeSelection.enter()
|
|
||||||
.append('line')
|
|
||||||
.attr('class', 'edge')
|
|
||||||
.attr('x1', d => {
|
|
||||||
const prev = prevEdgeMap.get(d.id);
|
|
||||||
return prev && prev.sections && prev.sections[0]
|
|
||||||
? prev.sections[0].startPoint.x + 30
|
|
||||||
: d.section.startPoint.x + 30;
|
|
||||||
})
|
|
||||||
.attr('y1', d => {
|
|
||||||
const prev = prevEdgeMap.get(d.id);
|
|
||||||
return prev && prev.sections && prev.sections[0]
|
|
||||||
? prev.sections[0].startPoint.y + 30
|
|
||||||
: d.section.startPoint.y + 30;
|
|
||||||
})
|
|
||||||
.attr('x2', d => {
|
|
||||||
const prev = prevEdgeMap.get(d.id);
|
|
||||||
return prev && prev.sections && prev.sections[0]
|
|
||||||
? prev.sections[0].endPoint.x + 30
|
|
||||||
: d.section.endPoint.x + 30;
|
|
||||||
})
|
|
||||||
.attr('y2', d => {
|
|
||||||
const prev = prevEdgeMap.get(d.id);
|
|
||||||
return prev && prev.sections && prev.sections[0]
|
|
||||||
? prev.sections[0].endPoint.y + 30
|
|
||||||
: d.section.endPoint.y + 30;
|
|
||||||
})
|
|
||||||
.attr('stroke', 'var(--main-color)')
|
|
||||||
.attr('stroke-width', 2.5)
|
|
||||||
.attr('marker-end', 'url(#arrow)')
|
|
||||||
.attr('opacity', 0);
|
|
||||||
|
|
||||||
edgeEnter
|
|
||||||
.transition()
|
|
||||||
.duration(600)
|
|
||||||
.attr('opacity', 1)
|
|
||||||
.attr('x1', d => d.section.startPoint.x + 30)
|
|
||||||
.attr('y1', d => d.section.startPoint.y + 30)
|
|
||||||
.attr('x2', d => d.section.endPoint.x + 30)
|
|
||||||
.attr('y2', d => d.section.endPoint.y + 30);
|
|
||||||
|
|
||||||
// update + 动画(注意这里不再 transition opacity)
|
|
||||||
edgeSelection.merge(edgeEnter)
|
|
||||||
.transition()
|
|
||||||
.duration(600)
|
|
||||||
.ease(d3.easeCubicInOut)
|
|
||||||
.attr('x1', d => d.section.startPoint.x + 30)
|
|
||||||
.attr('y1', d => d.section.startPoint.y + 30)
|
|
||||||
.attr('x2', d => d.section.endPoint.x + 30)
|
|
||||||
.attr('y2', d => d.section.endPoint.y + 30)
|
|
||||||
.attr('opacity', 1);
|
|
||||||
|
|
||||||
// --- 节点动画部分 ---
|
|
||||||
const nodeGroup = mainGroup.selectAll<SVGGElement, any>('.node')
|
|
||||||
.data(state.nodes, d => d.id);
|
|
||||||
|
|
||||||
nodeGroup.exit().remove();
|
|
||||||
|
|
||||||
// 节点 enter
|
|
||||||
const nodeGroupEnter = nodeGroup.enter()
|
|
||||||
.append('g')
|
|
||||||
.attr('class', 'node')
|
|
||||||
.attr('transform', d => {
|
|
||||||
const prev = prevNodeMap.get(d.id);
|
|
||||||
if (prev) {
|
|
||||||
return `translate(${(prev.x || 0) + 30}, ${(prev.y || 0) + 30})`;
|
|
||||||
}
|
|
||||||
return `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`;
|
|
||||||
})
|
|
||||||
.style('cursor', 'pointer')
|
|
||||||
.attr('opacity', 0)
|
|
||||||
.on('mousedown', null)
|
|
||||||
.on('mouseup', function (event, d) {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (state.selectedNodeId) {
|
|
||||||
|
|
||||||
const { canConnect, reason } = invalidConnectionDetector(state, d);
|
|
||||||
|
|
||||||
console.log(reason);
|
|
||||||
|
|
||||||
|
|
||||||
if (reason) {
|
|
||||||
ElMessage.warning(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canConnect) {
|
|
||||||
state.edges.push({
|
|
||||||
id: `e${state.selectedNodeId}_${d.id}_${Date.now()}`,
|
|
||||||
sources: [state.selectedNodeId],
|
|
||||||
targets: [d.id]
|
|
||||||
});
|
|
||||||
state.selectedNodeId = null;
|
|
||||||
recomputeLayout().then(renderSvg);
|
|
||||||
} else {
|
|
||||||
// 已存在则只取消选中
|
|
||||||
state.selectedNodeId = null;
|
|
||||||
renderSvg();
|
|
||||||
}
|
|
||||||
context.setCaption('');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
state.selectedNodeId = d.id;
|
|
||||||
renderSvg();
|
|
||||||
context.setCaption('选择另一个节点以定义测试拓扑');
|
|
||||||
}
|
|
||||||
state.draggingNodeId = null;
|
|
||||||
})
|
|
||||||
.on('mouseover', function (event, d) {
|
|
||||||
state.hoverNodeId = d.id;
|
|
||||||
d3.select(this).select('rect')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke', 'var(--main-color)')
|
|
||||||
.attr('stroke-width', 2);
|
|
||||||
})
|
|
||||||
.on('mouseout', function (event, d) {
|
|
||||||
state.hoverNodeId = null;
|
|
||||||
if (state.selectedNodeId === d.id) return;
|
|
||||||
d3.select(this).select('rect')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke', 'var(--main-light-color-10)')
|
|
||||||
.attr('stroke-width', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
nodeGroupEnter.append('rect')
|
|
||||||
.attr('width', d => d.width)
|
|
||||||
.attr('height', d => d.height)
|
|
||||||
.attr('rx', 16)
|
|
||||||
.attr('fill', 'var(--main-light-color-20)')
|
|
||||||
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
|
|
||||||
.attr('stroke-width', 2);
|
|
||||||
|
|
||||||
// 节点文字
|
|
||||||
nodeGroupEnter.append('text')
|
|
||||||
.attr('x', d => d.width / 2)
|
|
||||||
.attr('y', d => d.height / 2 - 6) // 上移一点
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('font-size', 16)
|
|
||||||
.attr('fill', 'var(--main-color)')
|
|
||||||
.attr('font-weight', 600)
|
|
||||||
.text(d => d.labels?.[0]?.text || 'Tool');
|
|
||||||
|
|
||||||
// 状态条
|
|
||||||
nodeGroupEnter.append('g')
|
|
||||||
.attr('class', 'node-status')
|
|
||||||
.each(function (d) {
|
|
||||||
const status = state.dataView.get(d.id)?.status || 'waiting';
|
|
||||||
const g = d3.select(this);
|
|
||||||
if (status === 'running') {
|
|
||||||
// 动画圆环+文字
|
|
||||||
g.append('circle')
|
|
||||||
.attr('cx', d.width / 2 - 32)
|
|
||||||
.attr('cy', d.height - 16)
|
|
||||||
.attr('r', 6) // 半径更小
|
|
||||||
.attr('fill', 'none')
|
|
||||||
.attr('stroke', 'var(--main-color)') // 使用主题色
|
|
||||||
.attr('stroke-width', 3)
|
|
||||||
.attr('stroke-dasharray', 20)
|
|
||||||
.attr('stroke-dashoffset', 0)
|
|
||||||
.append('animateTransform')
|
|
||||||
.attr('attributeName', 'transform')
|
|
||||||
.attr('attributeType', 'XML')
|
|
||||||
.attr('type', 'rotate')
|
|
||||||
.attr('from', `0 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
|
||||||
.attr('to', `360 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
|
||||||
.attr('dur', '1s')
|
|
||||||
.attr('repeatCount', 'indefinite');
|
|
||||||
g.append('text')
|
|
||||||
.attr('x', d.width / 2 - 16)
|
|
||||||
.attr('y', d.height - 12)
|
|
||||||
.attr('font-size', 13)
|
|
||||||
.attr('fill', 'var(--main-color)')
|
|
||||||
.text('running');
|
|
||||||
} else if (status === 'waiting') {
|
|
||||||
g.append('circle')
|
|
||||||
.attr('cx', d.width / 2 - 32)
|
|
||||||
.attr('cy', d.height - 16)
|
|
||||||
.attr('r', 6)
|
|
||||||
.attr('fill', 'none')
|
|
||||||
.attr('stroke', '#bdbdbd')
|
|
||||||
.attr('stroke-width', 3);
|
|
||||||
g.append('text')
|
|
||||||
.attr('x', d.width / 2 - 16)
|
|
||||||
.attr('y', d.height - 12)
|
|
||||||
.attr('font-size', 13)
|
|
||||||
.attr('fill', '#bdbdbd')
|
|
||||||
.text('waiting');
|
|
||||||
} else if (status === 'success') {
|
|
||||||
g.append('circle')
|
|
||||||
.attr('cx', d.width / 2 - 32)
|
|
||||||
.attr('cy', d.height - 16)
|
|
||||||
.attr('r', 6) // 保持和 waiting 一致
|
|
||||||
.attr('fill', 'none')
|
|
||||||
.attr('stroke', '#4caf50')
|
|
||||||
.attr('stroke-width', 3);
|
|
||||||
g.append('text')
|
|
||||||
.attr('x', d.width / 2 - 16)
|
|
||||||
.attr('y', d.height - 12)
|
|
||||||
.attr('font-size', 13)
|
|
||||||
.attr('fill', '#4caf50')
|
|
||||||
.text('success');
|
|
||||||
} else if (status === 'error') {
|
|
||||||
g.append('circle')
|
|
||||||
.attr('cx', d.width / 2 - 32)
|
|
||||||
.attr('cy', d.height - 16)
|
|
||||||
.attr('r', 6) // 保持和 waiting 一致
|
|
||||||
.attr('fill', 'none')
|
|
||||||
.attr('stroke', '#f44336')
|
|
||||||
.attr('stroke-width', 3);
|
|
||||||
g.append('text')
|
|
||||||
.attr('x', d.width / 2 - 16)
|
|
||||||
.attr('y', d.height - 12)
|
|
||||||
.attr('font-size', 13)
|
|
||||||
.attr('fill', '#f44336')
|
|
||||||
.text('error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 节点 enter 动画
|
|
||||||
nodeGroupEnter
|
|
||||||
.transition()
|
|
||||||
.duration(600)
|
|
||||||
.attr('opacity', 1)
|
|
||||||
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
|
||||||
|
|
||||||
// 节点 update 动画
|
|
||||||
nodeGroup
|
|
||||||
.transition()
|
|
||||||
.duration(600)
|
|
||||||
.ease(d3.easeCubicInOut)
|
|
||||||
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
|
||||||
|
|
||||||
// 高亮选中节点动画
|
|
||||||
nodeGroup.select('rect')
|
|
||||||
.transition()
|
|
||||||
.duration(400)
|
|
||||||
.attr('stroke-width', d => state.selectedNodeId === d.id ? 2 : 1)
|
|
||||||
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)');
|
|
||||||
|
|
||||||
// 边高亮
|
|
||||||
svg.selectAll<SVGLineElement, any>('.edge')
|
|
||||||
.on('mouseover', function () {
|
|
||||||
d3.select(this)
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke', 'var(--main-color)')
|
|
||||||
.attr('stroke-width', 4.5);
|
|
||||||
|
|
||||||
context.setCaption('点击边以删除');
|
|
||||||
|
|
||||||
})
|
|
||||||
.on('mouseout', function () {
|
|
||||||
d3.select(this)
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke', 'var(--main-color)')
|
|
||||||
.attr('stroke-width', 2.5);
|
|
||||||
|
|
||||||
context.setCaption('');
|
|
||||||
})
|
|
||||||
.on('click', function (event, d) {
|
|
||||||
// 只删除当前 edge
|
|
||||||
state.edges = state.edges.filter(e => {
|
|
||||||
// 多段 edge 情况
|
|
||||||
if (e.sections) {
|
|
||||||
// 只保留不是当前 section 的
|
|
||||||
return !e.sections.some((section: any, idx: number) =>
|
|
||||||
((e.id || '') + '-' + (section.id || idx)) === d.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 单段 edge 情况
|
|
||||||
return e.id !== d.id && e.id !== d.section?.id;
|
|
||||||
});
|
|
||||||
recomputeLayout().then(renderSvg);
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 渲染结束后保存当前快照
|
|
||||||
prevNodes = state.nodes.map(n => ({ ...n }));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = inject('context') as any;
|
|
||||||
context.reset = resetConnections;
|
|
||||||
context.state = state;
|
|
||||||
context.render = renderSvg;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(drawDiagram);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. 计算窗口位置
|
|
||||||
function getNodePopupStyle(node: any): any {
|
|
||||||
// 节点的 svg 坐标转为容器内绝对定位
|
|
||||||
// 注意:这里假设 offsetX、node.x、node.y 已经是最新的
|
|
||||||
console.log(node);
|
|
||||||
|
|
||||||
const left = (node.x || 0) + (node.width || 160) - 120; // 节点右侧
|
|
||||||
const top = (node.y || 0) + 30; // 节点顶部对齐
|
|
||||||
return {
|
|
||||||
position: 'absolute',
|
|
||||||
left: `${left}px`,
|
|
||||||
top: `${top}px`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.diagram-container {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-popup {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--background);
|
|
||||||
border: 1px solid var(--main-color);
|
|
||||||
width: 240px;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
white-space: nowrap;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 旋转动画 */
|
|
||||||
.status-running-circle {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from 'fs';
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://api.deepseek.com',
|
||||||
@ -116,6 +117,7 @@ ${content}
|
|||||||
return { version, changelogs };
|
return { version, changelogs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green('Starting to update contributors...'));
|
||||||
const contributors = await fetchAndMergeContributors();
|
const contributors = await fetchAndMergeContributors();
|
||||||
const { version, changelogs } = await getVersionAndContent();
|
const { version, changelogs } = await getVersionAndContent();
|
||||||
const newsData = {
|
const newsData = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user