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/6] =?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/6] =?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 @@

Dependency View

+

diff --git a/src/public/locales/en.json b/src/public/locales/en.json index 7d75188..3c18bb7 100644 --- a/src/public/locales/en.json +++ b/src/public/locales/en.json @@ -3,6 +3,7 @@ "status_indicator_online": "ONLINE", "dependency_view_title": "Dependency View", "dependency_graph_placeholder": "Dependency relationship for all tasks", + "reset_view_btn_title": "Reset View", "task_list_title": "Task List", "search_placeholder": "Search tasks...", "sort_option_date_desc": "Creation Time (New-Old)", diff --git a/src/public/locales/zh-TW.json b/src/public/locales/zh-TW.json index d9b99bd..e33c706 100644 --- a/src/public/locales/zh-TW.json +++ b/src/public/locales/zh-TW.json @@ -3,6 +3,7 @@ "status_indicator_online": "在線", "dependency_view_title": "依賴關係", "dependency_graph_placeholder": "所有任務的依賴關係", + "reset_view_btn_title": "重置視圖", "task_list_title": "任務列表", "search_placeholder": "搜索任務...", "sort_option_date_desc": "創建時間 (新-舊)", diff --git a/src/public/script.js b/src/public/script.js index 9c959b8..64e83bf 100644 --- a/src/public/script.js +++ b/src/public/script.js @@ -4,8 +4,10 @@ let selectedTaskId = null; let searchTerm = ""; let sortOption = "date-asc"; let globalAnalysisResult = null; // 新增:儲存全局分析結果 -let svg, g, simulation; // << 修改:定義 D3 相關變量 +let svg, g, simulation; +let width, height; // << 新增:將寬高定義為全局變量 let isGraphInitialized = false; // << 新增:追蹤圖表是否已初始化 +let zoom; // << 新增:保存縮放行為對象 // 新增:i18n 全局變量 let currentLang = "en"; // 預設語言 @@ -26,6 +28,7 @@ const globalAnalysisResultElement = document.getElementById( "global-analysis-result" ); // 假設 HTML 中有這個元素 const langSwitcher = document.getElementById("lang-switcher"); // << 新增:獲取切換器元素 +const resetViewBtn = document.getElementById("reset-view-btn"); // << 新增:獲取重置按鈕元素 // 初始化 document.addEventListener("DOMContentLoaded", () => { @@ -33,6 +36,7 @@ document.addEventListener("DOMContentLoaded", () => { initI18n(); // << 新增:初始化 i18n updateCurrentTime(); setInterval(updateCurrentTime, 1000); + updateDimensions(); // << 新增:初始化時更新尺寸 // 事件監聽器 // statusFilter.addEventListener("change", renderTasks); // 將由 changeLanguage 觸發或在 applyTranslations 後觸發 @@ -40,6 +44,11 @@ document.addEventListener("DOMContentLoaded", () => { statusFilter.addEventListener("change", renderTasks); } + // 新增:重置視圖按鈕事件監聽 + if (resetViewBtn) { + resetViewBtn.addEventListener("click", resetView); + } + // 新增:搜索和排序事件監聽 const searchInput = document.getElementById("search-input"); const sortOptions = document.getElementById("sort-options"); @@ -67,6 +76,16 @@ document.addEventListener("DOMContentLoaded", () => { changeLanguage(e.target.value) ); } + + // 新增:視窗大小改變時更新尺寸 + window.addEventListener("resize", () => { + updateDimensions(); + if (svg && simulation) { + svg.attr("viewBox", [0, 0, width, height]); + simulation.force("center", d3.forceCenter(width / 2, height / 2)); + simulation.alpha(0.3).restart(); + } + }); }); // 新增:i18n 核心函數 @@ -742,31 +761,75 @@ function selectTask(taskId) { highlightNode(taskId); // 只調用 highlightNode } +// 新增:重置視圖功能 +function resetView() { + if (!svg || !simulation) return; + + // 添加重置動畫效果 + resetViewBtn.classList.add("resetting"); + + // 計算視圖中心 + const centerX = width / 2; + const centerY = height / 2; + + // 重置縮放和平移(使用 transform 過渡) + svg.transition() + .duration(750) + .call(zoom.transform, d3.zoomIdentity); + + // 重置所有節點位置到中心附近 + simulation.nodes().forEach(node => { + node.x = centerX + (Math.random() - 0.5) * 50; // 在中心點附近隨機分佈 + node.y = centerY + (Math.random() - 0.5) * 50; + node.fx = null; // 清除固定位置 + node.fy = null; + }); + + // 重置力導向模擬 + simulation + .force("center", d3.forceCenter(centerX, centerY)) + .alpha(1) // 完全重啟模擬 + .restart(); + + // 750ms 後移除動畫類 + setTimeout(() => { + resetViewBtn.classList.remove("resetting"); + }, 750); +} + +// 新增:初始化縮放行為 +function initZoom() { + zoom = d3.zoom() + .scaleExtent([0.1, 4]) // 設置縮放範圍 + .on("zoom", (event) => { + g.attr("transform", event.transform); + }); + + if (svg) { + svg.call(zoom); + } +} + // 渲染依賴關係圖 - 修改為全局視圖和 enter/update/exit 模式 function renderDependencyGraph() { if (!dependencyGraphElement || !window.d3) { console.warn("D3 or dependency graph element not found."); if (dependencyGraphElement) { - // 首次或D3丟失時顯示提示,不清空已有的圖 if (!dependencyGraphElement.querySelector("svg")) { - dependencyGraphElement.innerHTML = `

${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/6] =?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/6] =?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/6] =?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/6] =?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); // --- 簡單粗暴的替換 (會導致閃爍) ---