test news page

This commit is contained in:
锦恢 2025-07-06 05:37:23 +08:00
parent 5bf0623327
commit 1244ee474e
12 changed files with 508 additions and 1073 deletions

View File

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

View File

@ -109,12 +109,6 @@ body {
} }
.openmcp-logo {
width: 84px;
height: 84px;
border-radius: 16px;
}
.margin-bottom { .margin-bottom {
margin-bottom: 32px; margin-bottom: 32px;
} }

View File

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

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

View File

@ -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": [
{ {

View File

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

@ -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",

View File

@ -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",

View File

@ -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]));
// xx
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
// offsetXnode.xnode.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>

View File

@ -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 = {

896
yarn.lock

File diff suppressed because it is too large Load Diff