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] =?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;