feat: 增加reset按钮,便于在图像丢失时快速回归

This commit is contained in:
yitacls 2025-06-10 01:41:41 +08:00
parent d26782a633
commit f9a90a8aa6
5 changed files with 125 additions and 45 deletions

View File

@ -46,6 +46,11 @@
<div class="dependency-view">
<div class="panel-header">
<h2 data-i18n-key="dependency_view_title">Dependency View</h2>
<button id="reset-view-btn" class="reset-view-btn" title="Reset View" data-i18n-key="reset_view_btn_title">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 5V2L8 6l4 4V7c3.31 0 6 2.69 6 6 0 2.97-2.17 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93 0-4.42-3.58-8-8-8zm-6 8c0-1.65.67-3.15 1.76-4.24L6.34 7.34C4.9 8.79 4 10.79 4 13c0 4.08 3.05 7.44 7 7.93v-2.02c-2.83-.48-5-2.94-5-5.91z"/>
</svg>
</button>
</div>
<div id="dependency-graph" class="dependency-graph">
<p class="placeholder"></p>

View File

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

View File

@ -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": "創建時間 (新-舊)",

View File

@ -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 = `<p class="placeholder">${translate(
"error_loading_graph_d3" // Use a specific key
)}</p>`;
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate("error_loading_graph_d3")}</p>`;
}
}
return;
}
updateDimensions();
// 如果沒有任務,清空圖表並顯示提示
if (tasks.length === 0) {
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate(
"dependency_graph_placeholder_empty"
)}</p>`;
// 重置 SVG 和 simulation 變數,以便下次正確初始化
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate("dependency_graph_placeholder_empty")}</p>`;
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 ...

View File

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