From d26782a63328e8489dd6ff302d6558465ee55626 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:29:45 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4=E4=BA=86=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=85=B3=E7=B3=BB=E5=9B=BE=E7=9A=84=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=B7=B3=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 122 +++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/public/script.js b/src/public/script.js index 0eee4b4..9c959b8 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -5,6 +5,7 @@ let searchTerm = ""; let sortOption = "date-asc"; let globalAnalysisResult = null; // 新增:儲存全局分析結果 let svg, g, simulation; // << 修改:定義 D3 相關變量 +let isGraphInitialized = false; // << 新增:追蹤圖表是否已初始化 // 新增:i18n 全局變量 let currentLang = "en"; // 預設語言 @@ -765,6 +766,7 @@ function renderDependencyGraph() { svg = null; g = null; simulation = null; + isGraphInitialized = false; // << 新增:重置初始化標誌 return; } @@ -865,7 +867,38 @@ function renderDependencyGraph() { simulation.force("center", d3.forceCenter(width / 2, height / 2)); } - // 3. 更新連結 + // --- 預先運算穩定的節點位置 --- + // 複製節點和連結以進行穩定化計算 + const stableNodes = [...nodes]; + const stableLinks = [...links]; + + // 暫時創建一個模擬器來計算穩定的位置 + const stableSim = d3 + .forceSimulation(stableNodes) + .force("link", d3.forceLink(stableLinks).id(d => d.id).distance(100)) + .force("charge", d3.forceManyBody().strength(-300)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collide", d3.forceCollide().radius(30)); + + // 預熱模擬獲得穩定位置 + for (let i = 0; i < 10; i++) { + stableSim.tick(); + } + + // 將穩定位置複製回原始節點 + stableNodes.forEach((stableNode) => { + const originalNode = nodes.find(n => n.id === stableNode.id); + if (originalNode) { + originalNode.x = stableNode.x; + originalNode.y = stableNode.y; + } + }); + + // 停止臨時模擬器 + stableSim.stop(); + // --- 預先運算結束 --- + + // 3. 更新連結 (無動畫) const linkSelection = g .select(".links") // 選擇放置連結的 g 元素 .selectAll("line.link") @@ -874,62 +907,43 @@ function renderDependencyGraph() { (d) => `${d.source.id || d.source}-${d.target.id || d.target}` ); // Key function 基於 source/target ID - // Exit - 移除舊連結 - linkSelection - .exit() - .transition("exit") - .duration(300) - .attr("stroke-opacity", 0) - .remove(); + // Exit - 直接移除舊連結 + linkSelection.exit().remove(); - // Enter - 添加新連結 + // Enter - 添加新連結 (無動畫) const linkEnter = linkSelection .enter() .append("line") .attr("class", "link") .attr("stroke", "#999") .attr("marker-end", "url(#arrowhead)") - .attr("stroke-opacity", 0); // 初始透明 - - // Update + Enter - 更新所有連結的屬性 (合併 enter 和 update 選擇集) - const linkUpdate = linkSelection.merge(linkEnter); - - linkUpdate - .transition("update") - .duration(500) .attr("stroke-opacity", 0.6) .attr("stroke-width", 1.5); - // 4. 更新節點 + // 立即設置連結位置 + linkEnter + .attr("x1", d => d.source.x || 0) + .attr("y1", d => d.source.y || 0) + .attr("x2", d => d.target.x || 0) + .attr("y2", d => d.target.y || 0); + + // 4. 更新節點 (無動畫) const nodeSelection = g .select(".nodes") // 選擇放置節點的 g 元素 .selectAll("g.node-item") .data(nodes, (d) => d.id); // 使用 ID 作為 key - // Exit - 移除舊節點 - nodeSelection - .exit() - .transition("exit") - .duration(300) - .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(0)`) // 從當前位置縮放消失 - .attr("opacity", 0) - .remove(); + // Exit - 直接移除舊節點 + nodeSelection.exit().remove(); - // Enter - 添加新節點組 + // Enter - 添加新節點組 (無動畫,直接在最終位置創建) const nodeEnter = nodeSelection .enter() .append("g") .attr("class", (d) => `node-item status-${getStatusClass(d.status)}`) // 使用輔助函數設置 class .attr("data-id", (d) => d.id) - // 初始位置:從模擬計算的位置(如果存在)或隨機位置出現,初始縮放為0 - .attr( - "transform", - (d) => - `translate(${d.x || Math.random() * width}, ${ - d.y || Math.random() * height - }) scale(0)` - ) - .attr("opacity", 0) + // 直接使用預計算的位置,無需縮放或透明度過渡 + .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`) .call(drag(simulation)); // 添加拖拽 // 添加圓形到 Enter 選擇集 @@ -937,8 +951,8 @@ function renderDependencyGraph() { .append("circle") .attr("r", 10) .attr("stroke", "#fff") - .attr("stroke-width", 1.5); - // 顏色將在 merge 後通過 update 過渡設置 + .attr("stroke-width", 1.5) + .attr("fill", getNodeColor); // 直接設置顏色 // 添加文字到 Enter 選擇集 nodeEnter @@ -960,28 +974,14 @@ function renderDependencyGraph() { event.stopPropagation(); }); - // Update + Enter - 合併並更新所有節點 - const nodeUpdate = nodeSelection.merge(nodeEnter); + // Update - 立即更新現有節點 (無動畫) + nodeSelection + .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`) + .attr("class", (d) => `node-item status-${getStatusClass(d.status)}`); - // 過渡到最終位置和狀態 - nodeUpdate - .transition("update") - .duration(500) - .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(1)`) // 移動到模擬位置並恢復大小 - .attr("opacity", 1); - - // 更新節點顏色 (單獨過渡) - nodeUpdate + nodeSelection .select("circle") - .transition("color") - .duration(500) - .attr("fill", getNodeColor); // 使用已有的 getNodeColor 函數 - - // 更新節點狀態 Class (即時更新,無需過渡) - nodeUpdate.attr( - "class", - (d) => `node-item status-${getStatusClass(d.status)}` - ); + .attr("fill", getNodeColor); // << 新增:重新定義 drag 函數 >> function drag(simulation) { @@ -1012,10 +1012,10 @@ function renderDependencyGraph() { } // << drag 函數定義結束 >> - // 5. 更新力導向模擬 - simulation.nodes(nodes); // 在處理完 enter/exit 後更新模擬節點 + // 5. 更新力導向模擬,但不啟動 + simulation.nodes(nodes); // 更新模擬節點 simulation.force("link").links(links); // 更新模擬連結 - simulation.alpha(0.3).restart(); // 重新激活模擬 + // 注意:移除了 restart() 調用,防止刷新時的動畫跳變 } // Tick 函數: 更新節點和連結位置 From f9a90a8aa641a0e46979960cab7b1c9cbf416d4d Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:41:41 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0reset=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=8C=E4=BE=BF=E4=BA=8E=E5=9C=A8=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=E6=97=B6=E5=BF=AB=E9=80=9F=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/index.html | 5 ++ src/public/locales/en.json | 1 + src/public/locales/zh-TW.json | 1 + src/public/script.js | 137 +++++++++++++++++++++++----------- src/public/style.css | 26 +++++++ 5 files changed, 125 insertions(+), 45 deletions(-) diff --git a/src/public/index.html b/src/public/index.html index 72836d1..e67c189 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -46,6 +46,11 @@
${translate( - "error_loading_graph_d3" // Use a specific key - )}
`; + dependencyGraphElement.innerHTML = `${translate("error_loading_graph_d3")}
`; } } return; } + updateDimensions(); + // 如果沒有任務,清空圖表並顯示提示 if (tasks.length === 0) { - dependencyGraphElement.innerHTML = `${translate( - "dependency_graph_placeholder_empty" - )}
`; - // 重置 SVG 和 simulation 變數,以便下次正確初始化 + dependencyGraphElement.innerHTML = `${translate("dependency_graph_placeholder_empty")}
`; svg = null; g = null; simulation = null; - isGraphInitialized = false; // << 新增:重置初始化標誌 return; } @@ -775,10 +838,9 @@ function renderDependencyGraph() { id: task.id, name: task.name, status: task.status, - // 保留現有位置以便平滑過渡 x: simulation?.nodes().find((n) => n.id === task.id)?.x, y: simulation?.nodes().find((n) => n.id === task.id)?.y, - fx: simulation?.nodes().find((n) => n.id === task.id)?.fx, // 保留固定位置 + fx: simulation?.nodes().find((n) => n.id === task.id)?.fx, fy: simulation?.nodes().find((n) => n.id === task.id)?.fy, })); @@ -788,44 +850,29 @@ function renderDependencyGraph() { task.dependencies.forEach((dep) => { const sourceId = typeof dep === "object" ? dep.taskId : dep; const targetId = task.id; - if ( - nodes.some((n) => n.id === sourceId) && - nodes.some((n) => n.id === targetId) - ) { - // 確保 link 的 source/target 是 ID,以便力導向識別 + if (nodes.some((n) => n.id === sourceId) && nodes.some((n) => n.id === targetId)) { links.push({ source: sourceId, target: targetId }); } else { - console.warn( - `Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.` - ); + console.warn(`Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.`); } }); } }); - // 2. D3 繪圖設置與更新 - const width = dependencyGraphElement.clientWidth; - const height = dependencyGraphElement.clientHeight || 400; - if (!svg) { // --- 首次渲染 --- console.log("First render of dependency graph"); - dependencyGraphElement.innerHTML = ""; // 清空 placeholder + dependencyGraphElement.innerHTML = ""; - svg = d3 - .select(dependencyGraphElement) + svg = d3.select(dependencyGraphElement) .append("svg") .attr("viewBox", [0, 0, width, height]) .attr("preserveAspectRatio", "xMidYMid meet"); - g = svg.append("g"); // 主要組,用於縮放和平移 + g = svg.append("g"); - // 添加縮放和平移 - svg.call( - d3.zoom().on("zoom", (event) => { - g.attr("transform", event.transform); - }) - ); + // 初始化並添加縮放行為 + initZoom(); // 添加箭頭定義 g.append("defs") @@ -842,19 +889,12 @@ function renderDependencyGraph() { .attr("fill", "#999"); // 初始化力導向模擬 - simulation = d3 - .forceSimulation() // 初始化時不傳入 nodes - .force( - "link", - d3 - .forceLink() - .id((d) => d.id) - .distance(100) // 指定 id 訪問器 - ) + simulation = d3.forceSimulation() + .force("link", d3.forceLink().id((d) => d.id).distance(100)) .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(30)) - .on("tick", ticked); // 綁定 tick 事件處理函數 + .on("tick", ticked); // 添加用於存放連結和節點的組 g.append("g").attr("class", "links"); @@ -862,7 +902,6 @@ function renderDependencyGraph() { } else { // --- 更新渲染 --- console.log("Updating dependency graph"); - // 更新 SVG 尺寸和中心力 (如果窗口大小改變) svg.attr("viewBox", [0, 0, width, height]); simulation.force("center", d3.forceCenter(width / 2, height / 2)); } @@ -1191,5 +1230,13 @@ function getStatusClass(status) { return status ? status.replace(/_/g, "-") : "unknown"; // 替換所有下劃線 } +// 新增:更新寬高的函數 +function updateDimensions() { + if (dependencyGraphElement) { + width = dependencyGraphElement.clientWidth; + height = dependencyGraphElement.clientHeight || 400; + } +} + // 函數:啟用節點拖拽 (保持不變) // ... drag ... diff --git a/src/public/style.css b/src/public/style.css index f019803..a7681e1 100644 --- a/src/public/style.css +++ b/src/public/style.css @@ -123,6 +123,32 @@ main { border-bottom: 1px solid rgba(255, 255, 255, 0.05); } +.reset-view-btn { + background: none; + border: none; + color: var(--text-color); + cursor: pointer; + padding: 8px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.reset-view-btn:hover { + background-color: rgba(255, 255, 255, 0.1); + transform: rotate(-30deg); +} + +.reset-view-btn svg { + transition: transform 0.3s ease; +} + +.reset-view-btn.resetting svg { + transform: rotate(-360deg); +} + h2 { font-size: 1.2rem; font-weight: 500; From 0f22b416825de901149cc16927d5deb6cd28b22a Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:54:37 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=88=9D=E5=A7=8B=E5=B8=83=E5=B1=80=EF=BC=8C=E6=A0=B9?= =?UTF-8?q?=E6=8D=AE=E5=87=BA=E5=85=A5=E5=BA=A6=E6=96=BD=E5=8A=A0=E6=B0=B4?= =?UTF-8?q?=E5=B9=B3=E5=8A=9B=EF=BC=8C=E6=8F=90=E5=8D=87=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=E7=BB=93=E6=9E=84=E6=B8=85=E6=99=B0?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/public/script.js b/src/public/script.js index 64e83bf..18452f7 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -894,6 +894,23 @@ function renderDependencyGraph() { .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(30)) + // 新增:水平分布力 + .force("x", d3.forceX().x(d => { + // 计算节点的入度和出度 + const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; + const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length; + + if (inDegree === 0) { + // 入度为0的节点(起始节点)靠左 + return width * 0.2; + } else if (outDegree === 0) { + // 出度为0的节点(终止节点)靠右 + return width * 0.8; + } else { + // 其他节点在中间 + return width * 0.5; + } + }).strength(0.2)) // 调整力的强度,可以根据需要调整 .on("tick", ticked); // 添加用於存放連結和節點的組 @@ -1054,6 +1071,20 @@ function renderDependencyGraph() { // 5. 更新力導向模擬,但不啟動 simulation.nodes(nodes); // 更新模擬節點 simulation.force("link").links(links); // 更新模擬連結 + + // 更新水平分布力的目標位置 + simulation.force("x").x(d => { + const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; + const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length; + + if (inDegree === 0) { + return width * 0.2; + } else if (outDegree === 0) { + return width * 0.8; + } else { + return width * 0.5; + } + }); // 注意:移除了 restart() 調用,防止刷新時的動畫跳變 } From 2b2d002f318e14e8ace92d0d3e3b2c43cd172211 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 02:46:00 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E8=8A=82=E7=82=B9=E5=BA=A6=E6=95=B0=E7=9A=84=E5=9E=82?= =?UTF-8?q?=E7=9B=B4=E5=88=86=E5=B8=83=E5=8A=9B=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E8=BF=87=E4=BA=8E=E5=88=86=E6=95=A3=E6=88=96=E8=81=9A=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/public/script.js b/src/public/script.js index 18452f7..b674d9c 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -910,7 +910,17 @@ function renderDependencyGraph() { // 其他节点在中间 return width * 0.5; } - }).strength(0.2)) // 调整力的强度,可以根据需要调整 + }).strength(0.2)) + // 新增:基于节点度数的垂直分布力 + .force("y", d3.forceY().y(height / 2).strength(d => { + // 计算节点的总度数(入度+出度) + const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; + const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length; + const totalDegree = inDegree + outDegree; + + // 度数越大,力越大(基础力0.05,每个连接增加0.03,最大0.3) + return Math.min(0.05 + totalDegree * 0.03, 0.3); + })) .on("tick", ticked); // 添加用於存放連結和節點的組 From c843cbc820362f407a39511682c1da8a535bafa9 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:39:37 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BC=A9?= =?UTF-8?q?=E7=95=A5=E5=9B=BE=E5=8A=9F=E8=83=BD=E5=8F=8A=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=85=B3=E7=B3=BB=E5=9B=BE=E5=92=8C=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E8=81=94=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 209 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 195 insertions(+), 14 deletions(-) diff --git a/src/public/script.js b/src/public/script.js index b674d9c..cf2a34f 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -490,6 +490,9 @@ function renderTasks() { ); } + // 保存筛选后的任务ID集合,用于图形渲染 + const filteredTaskIds = new Set(filteredTasks.map(task => task.id)); + filteredTasks.sort((a, b) => { switch (sortOption) { case "name-asc": @@ -507,6 +510,9 @@ function renderTasks() { } }); + // 更新图形的显示状态 + updateGraphVisibility(filteredTaskIds); + // --- 簡單粗暴的替換 (會導致閃爍) --- // TODO: 實現 DOM Diffing 或更智慧的更新策略 if (filteredTasks.length === 0) { @@ -559,9 +565,77 @@ function renderTasks() { } } -// 選擇任務 +// 新增:更新图形可见性的函数 +function updateGraphVisibility(filteredTaskIds) { + if (!g) return; + + // 更新节点的样式 + g.select(".nodes") + .selectAll("g.node-item") + .style("opacity", d => filteredTaskIds.has(d.id) ? 1 : 0.2) + .style("filter", d => filteredTaskIds.has(d.id) ? "none" : "grayscale(80%)"); + + // 更新连接的样式 + g.select(".links") + .selectAll("line.link") + .style("opacity", d => { + const sourceVisible = filteredTaskIds.has(d.source.id || d.source); + const targetVisible = filteredTaskIds.has(d.target.id || d.target); + return (sourceVisible && targetVisible) ? 0.6 : 0.1; + }) + .style("stroke", d => { + const sourceVisible = filteredTaskIds.has(d.source.id || d.source); + const targetVisible = filteredTaskIds.has(d.target.id || d.target); + return (sourceVisible && targetVisible) ? "#999" : "#ccc"; + }); + + // 更新缩略图中的节点和连接样式 + const minimapContent = svg.select(".minimap-content"); + + minimapContent.selectAll(".minimap-node") + .style("opacity", d => filteredTaskIds.has(d.id) ? 1 : 0.2) + .style("filter", d => filteredTaskIds.has(d.id) ? "none" : "grayscale(80%)"); + + minimapContent.selectAll(".minimap-link") + .style("opacity", d => { + const sourceVisible = filteredTaskIds.has(d.source.id || d.source); + const targetVisible = filteredTaskIds.has(d.target.id || d.target); + return (sourceVisible && targetVisible) ? 0.6 : 0.1; + }) + .style("stroke", d => { + const sourceVisible = filteredTaskIds.has(d.source.id || d.source); + const targetVisible = filteredTaskIds.has(d.target.id || d.target); + return (sourceVisible && targetVisible) ? "#999" : "#ccc"; + }); +} + +// 新增:将节点移动到视图中心的函数 +function centerNode(nodeId) { + if (!svg || !g || !simulation) return; + + const node = simulation.nodes().find(n => n.id === nodeId); + if (!node) return; + + // 获取当前视图的变换状态 + const transform = d3.zoomTransform(svg.node()); + + // 计算需要的变换以将节点居中 + const scale = transform.k; // 保持当前缩放级别 + const x = width / 2 - node.x * scale; + const y = height / 2 - node.y * scale; + + // 使用过渡动画平滑地移动到新位置 + svg.transition() + .duration(750) // 750ms的过渡时间 + .call(zoom.transform, d3.zoomIdentity + .translate(x, y) + .scale(scale) + ); +} + +// 修改选择任务的函数 function selectTask(taskId) { - // 清除舊的選中狀態和高亮 + // 清除旧的选中状态和高亮 if (selectedTaskId) { const previousElement = document.querySelector( `.task-item[data-id="${selectedTaskId}"]` @@ -571,7 +645,7 @@ function selectTask(taskId) { } } - // 如果再次點擊同一個任務,則取消選中 + // 如果再次点击同一个任务,则取消选中 if (selectedTaskId === taskId) { selectedTaskId = null; taskDetailsContent.innerHTML = `${translate( @@ -583,7 +657,7 @@ function selectTask(taskId) { selectedTaskId = taskId; - // 添加新的選中狀態 + // 添加新的选中状态 const selectedElement = document.querySelector( `.task-item[data-id="${taskId}"]` ); @@ -591,7 +665,7 @@ function selectTask(taskId) { selectedElement.classList.add("selected"); } - // 獲取並顯示任務詳情 + // 获取并显示任务详情 const task = tasks.find((t) => t.id === taskId); if (!task) { @@ -757,8 +831,9 @@ function selectTask(taskId) { // --- 原來的 innerHTML 賦值已移除 --- - // 只調用高亮函數 - highlightNode(taskId); // 只調用 highlightNode + // 高亮节点并将其移动到中心 + highlightNode(taskId); + centerNode(taskId); } // 新增:重置視圖功能 @@ -803,6 +878,7 @@ function initZoom() { .scaleExtent([0.1, 4]) // 設置縮放範圍 .on("zoom", (event) => { g.attr("transform", event.transform); + updateMinimap(); // 在縮放時更新縮略圖 }); if (svg) { @@ -869,6 +945,37 @@ function renderDependencyGraph() { .attr("viewBox", [0, 0, width, height]) .attr("preserveAspectRatio", "xMidYMid meet"); + // 添加縮略圖背景 + const minimapSize = Math.min(width, height) * 0.2; // 縮略圖大小為主視圖的20% + const minimapMargin = 40; + + // 創建縮略圖容器 + const minimap = svg.append("g") + .attr("class", "minimap") + .attr("transform", `translate(${width - minimapSize - minimapMargin}, ${height - minimapSize - minimapMargin*(height/width)})`); + + // 添加縮略圖背景 + minimap.append("rect") + .attr("width", minimapSize) + .attr("height", minimapSize) + .attr("fill", "rgba(0, 0, 0, 0.2)") + .attr("stroke", "#666") + .attr("stroke-width", 1) + .attr("rx", 4) + .attr("ry", 4); + + // 創建縮略圖內容組 + minimap.append("g") + .attr("class", "minimap-content"); + + // 添加視口指示器 + minimap.append("rect") + .attr("class", "minimap-viewport") + .attr("fill", "none") + .attr("stroke", "var(--accent-color)") + .attr("stroke-width", 1.5) + .attr("pointer-events", "none"); + g = svg.append("g"); // 初始化並添加縮放行為 @@ -896,29 +1003,29 @@ function renderDependencyGraph() { .force("collide", d3.forceCollide().radius(30)) // 新增:水平分布力 .force("x", d3.forceX().x(d => { - // 计算节点的入度和出度 + // 計算節點的入度和出度 const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length; if (inDegree === 0) { - // 入度为0的节点(起始节点)靠左 + // 入度為0的節點(起始節點)靠左 return width * 0.2; } else if (outDegree === 0) { - // 出度为0的节点(终止节点)靠右 + // 出度為0的節點(終止節點)靠右 return width * 0.8; } else { - // 其他节点在中间 + // 其他節點在中間 return width * 0.5; } }).strength(0.2)) - // 新增:基于节点度数的垂直分布力 + // 新增:基于節點度數的垂直分布力 .force("y", d3.forceY().y(height / 2).strength(d => { - // 计算节点的总度数(入度+出度) + // 計算節點的總度數(入度+出度) const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length; const totalDegree = inDegree + outDegree; - // 度数越大,力越大(基础力0.05,每个连接增加0.03,最大0.3) + // 度數越大,力越大(基礎力0.05,每個連接增加0.03,最大0.3) return Math.min(0.05 + totalDegree * 0.03, 0.3); })) .on("tick", ticked); @@ -1115,6 +1222,9 @@ function ticked() { .selectAll("g.node-item") // << 修改:添加座標後備值 >> .attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`); + + // 更新縮略圖 + updateMinimap(); } // 函數:根據節點數據返回顏色 (示例) @@ -1279,5 +1389,76 @@ function updateDimensions() { } } +// 添加縮略圖更新函數 +function updateMinimap() { + if (!svg || !simulation) return; + + const minimapSize = Math.min(width, height) * 0.2; + const nodes = simulation.nodes(); + const links = simulation.force("link").links(); + + // 計算當前圖的邊界 + const xExtent = d3.extent(nodes, d => d.x); + const yExtent = d3.extent(nodes, d => d.y); + const graphWidth = xExtent[1] - xExtent[0] || width; + const graphHeight = yExtent[1] - yExtent[0] || height; + const scale = Math.min(minimapSize / graphWidth, minimapSize / graphHeight) * 0.9; + + // 創建縮放函數 + const minimapX = d3.scaleLinear() + .domain([xExtent[0] - graphWidth * 0.05, xExtent[1] + graphWidth * 0.05]) + .range([0, minimapSize]); + const minimapY = d3.scaleLinear() + .domain([yExtent[0] - graphHeight * 0.05, yExtent[1] + graphHeight * 0.05]) + .range([0, minimapSize]); + + // 更新縮略圖中的連接 + const minimapContent = svg.select(".minimap-content"); + const minimapLinks = minimapContent.selectAll(".minimap-link") + .data(links); + + minimapLinks.enter() + .append("line") + .attr("class", "minimap-link") + .merge(minimapLinks) + .attr("x1", d => minimapX(d.source.x)) + .attr("y1", d => minimapY(d.source.y)) + .attr("x2", d => minimapX(d.target.x)) + .attr("y2", d => minimapY(d.target.y)) + .attr("stroke", "#999") + .attr("stroke-width", 0.5) + .attr("stroke-opacity", 0.6); + + minimapLinks.exit().remove(); + + // 更新縮略圖中的節點 + const minimapNodes = minimapContent.selectAll(".minimap-node") + .data(nodes); + + minimapNodes.enter() + .append("circle") + .attr("class", "minimap-node") + .attr("r", 2) + .merge(minimapNodes) + .attr("cx", d => minimapX(d.x)) + .attr("cy", d => minimapY(d.y)) + .attr("fill", getNodeColor); + + minimapNodes.exit().remove(); + + // 更新視口指示器 + const transform = d3.zoomTransform(svg.node()); + const viewportWidth = width / transform.k; + const viewportHeight = height / transform.k; + const viewportX = -transform.x / transform.k; + const viewportY = -transform.y / transform.k; + + svg.select(".minimap-viewport") + .attr("x", minimapX(viewportX)) + .attr("y", minimapY(viewportY)) + .attr("width", minimapX(viewportX + viewportWidth) - minimapX(viewportX)) + .attr("height", minimapY(viewportY + viewportHeight) - minimapY(viewportY)); +} + // 函數:啟用節點拖拽 (保持不變) // ... drag ... From c676799e36149946e3ca80b02c31726740b47d98 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:04:24 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=B8=AD=E6=96=87=E5=AD=97=E7=AC=A6=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E4=B8=80=E8=87=B4=E6=80=A7=E5=92=8C=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/public/script.js b/src/public/script.js index cf2a34f..f0e2da9 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -490,7 +490,7 @@ function renderTasks() { ); } - // 保存筛选后的任务ID集合,用于图形渲染 + // 儲存篩選後的任務 ID 集合,用於圖形渲染 const filteredTaskIds = new Set(filteredTasks.map(task => task.id)); filteredTasks.sort((a, b) => { @@ -510,7 +510,7 @@ function renderTasks() { } }); - // 更新图形的显示状态 + // 更新圖形的顯示狀態 updateGraphVisibility(filteredTaskIds); // --- 簡單粗暴的替換 (會導致閃爍) --- From 378e4393ccadc17e3aa8a88162e35c230788c181 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:44:47 +0800 Subject: [PATCH 7/9] =?UTF-8?q?docs:=E6=9B=B4=E6=96=B0=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=92=8C=E7=89=88=E6=9C=AC=E8=87=B3=201.0.20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++++++ docs/zh/CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f72c6..803f6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ # Changelog +## [1.0.20] + +### Added + +- Added reset button and thumbnail view +- Enhanced interaction between dependency graph and task list, making the dependency graph respond to filtering and task list selection + +### Changed + +- Removed initial animation of dependency graph to avoid animation jumps +- Optimized initial state of dependency graph + ## [1.0.19] ### Added diff --git a/docs/zh/CHANGELOG.md b/docs/zh/CHANGELOG.md index 919e553..150ccfd 100644 --- a/docs/zh/CHANGELOG.md +++ b/docs/zh/CHANGELOG.md @@ -2,6 +2,18 @@ # 更新日誌 +## [1.0.20] + +### 新增 + +- 新增重置按鈕和縮略圖 +- 增強關係依賴圖和任務列表交互,篩選和點擊任務列表使關係依賴圖響應變化 + +### 變更 + +- 移除關係依賴圖開頭動畫,避免動畫跳變 +- 優化關係依賴圖初始狀態 + ## [1.0.19] ### 新增 diff --git a/package.json b/package.json index 799e85b..d27c799 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-shrimp-task-manager", - "version": "1.0.19", + "version": "1.0.20", "description": "Shrimp Task Manager is a task tool built for AI Agents, emphasizing chain-of-thought, reflection, and style consistency. It converts natural language into structured dev tasks with dependency tracking and iterative refinement, enabling agent-like developer behavior in reasoning AI systems", "main": "dist/index.js", "type": "module", From 28f9a7c15dfd8665d46475fb3d209143b2c37e5a Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:00:32 +0800 Subject: [PATCH 8/9] =?UTF-8?q?chore=EF=BC=9A=E7=BB=9F=E4=B8=80=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=B3=A8=E9=87=8A=E4=B8=BA=E4=B8=AD=E6=96=87=E7=B9=81?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/public/script.js b/src/public/script.js index f0e2da9..cee20ce 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -1001,7 +1001,7 @@ function renderDependencyGraph() { .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide().radius(30)) - // 新增:水平分布力 + // 新增:水平分布力,用於優化節點在水平方向的分布,根據節點的入度和出度來決定節點的水平位置,入度為0的節點(起始節點)靠左,出度為0的節點(終止節點)靠右,其他節點則分布在中間位置 .force("x", d3.forceX().x(d => { // 計算節點的入度和出度 const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length; @@ -1034,7 +1034,7 @@ function renderDependencyGraph() { g.append("g").attr("class", "links"); g.append("g").attr("class", "nodes"); } else { - // --- 更新渲染 --- + // --- 更新圖表渲染 --- console.log("Updating dependency graph"); svg.attr("viewBox", [0, 0, width, height]); simulation.force("center", d3.forceCenter(width / 2, height / 2)); From 54438960b5c3e151e8ce139fd85dce53b9efebcc Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:32:57 +0800 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E7=BC=A9?= =?UTF-8?q?=E7=95=A5=E5=9B=BE=E8=A7=86=E5=8F=A3=E6=8C=87=E7=A4=BA=E5=99=A8?= =?UTF-8?q?=E8=A7=86=E8=A7=89=E6=95=88=E6=9E=9C=EF=BC=8C=E6=9B=B4=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=E8=BD=BB=E5=BE=AE=E7=9A=84=E5=8D=8A=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E7=99=BD=E8=89=B2=E5=8C=BA=E5=9F=9F=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=E6=83=85=E5=86=B5=E4=B8=8B=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E9=95=BF=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/script.js | 26 ++++++++++++++------------ src/public/style.css | 8 ++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/public/script.js b/src/public/script.js index cee20ce..259c932 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -970,11 +970,7 @@ function renderDependencyGraph() { // 添加視口指示器 minimap.append("rect") - .attr("class", "minimap-viewport") - .attr("fill", "none") - .attr("stroke", "var(--accent-color)") - .attr("stroke-width", 1.5) - .attr("pointer-events", "none"); + .attr("class", "minimap-viewport"); g = svg.append("g"); @@ -1397,19 +1393,25 @@ function updateMinimap() { const nodes = simulation.nodes(); const links = simulation.force("link").links(); - // 計算當前圖的邊界 + // 計算當前圖的邊界(添加padding) + const padding = 20; // 添加內邊距 const xExtent = d3.extent(nodes, d => d.x); const yExtent = d3.extent(nodes, d => d.y); - const graphWidth = xExtent[1] - xExtent[0] || width; - const graphHeight = yExtent[1] - yExtent[0] || height; - const scale = Math.min(minimapSize / graphWidth, minimapSize / graphHeight) * 0.9; + const graphWidth = (xExtent[1] - xExtent[0]) || width; + const graphHeight = (yExtent[1] - yExtent[0]) || height; - // 創建縮放函數 + // 計算縮放比例,確保考慮padding + const scale = Math.min( + minimapSize / (graphWidth + padding * 2), + minimapSize / (graphHeight + padding * 2) + ) * 0.9; // 0.9作為安全係數 + + // 創建縮放函數,加入padding const minimapX = d3.scaleLinear() - .domain([xExtent[0] - graphWidth * 0.05, xExtent[1] + graphWidth * 0.05]) + .domain([xExtent[0] - padding, xExtent[1] + padding]) .range([0, minimapSize]); const minimapY = d3.scaleLinear() - .domain([yExtent[0] - graphHeight * 0.05, yExtent[1] + graphHeight * 0.05]) + .domain([yExtent[0] - padding, yExtent[1] + padding]) .range([0, minimapSize]); // 更新縮略圖中的連接 diff --git a/src/public/style.css b/src/public/style.css index a7681e1..6383001 100644 --- a/src/public/style.css +++ b/src/public/style.css @@ -485,3 +485,11 @@ g.node-item.highlighted circle { flex-grow: 1; /* 讓搜索框佔據更多空間 */ min-width: 150px; } + +/* 新增:缩略图视口指示器样式 */ +.minimap-viewport { + fill: rgba(255, 255, 255, 0.025); + stroke: rgba(255, 255, 255, 0.1); + stroke-width: 1; + pointer-events: none; +}