Merge commit 'c676799e36149946e3ca80b02c31726740b47d98'

This commit is contained in:
yitacls 2025-06-10 22:10:26 +08:00
commit 4fedd78923
5 changed files with 414 additions and 112 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,7 +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"; // 預設語言
@ -25,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", () => {
@ -32,6 +36,7 @@ document.addEventListener("DOMContentLoaded", () => {
initI18n(); // << 新增:初始化 i18n
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
updateDimensions(); // << 新增:初始化時更新尺寸
// 事件監聽器
// statusFilter.addEventListener("change", renderTasks); // 將由 changeLanguage 觸發或在 applyTranslations 後觸發
@ -39,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");
@ -66,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 核心函數
@ -470,6 +490,9 @@ function renderTasks() {
);
}
// 儲存篩選後的任務 ID 集合,用於圖形渲染
const filteredTaskIds = new Set(filteredTasks.map(task => task.id));
filteredTasks.sort((a, b) => {
switch (sortOption) {
case "name-asc":
@ -487,6 +510,9 @@ function renderTasks() {
}
});
// 更新圖形的顯示狀態
updateGraphVisibility(filteredTaskIds);
// --- 簡單粗暴的替換 (會導致閃爍) ---
// TODO: 實現 DOM Diffing 或更智慧的更新策略
if (filteredTasks.length === 0) {
@ -539,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}"]`
@ -551,7 +645,7 @@ function selectTask(taskId) {
}
}
// 如果再次點擊同一個任務,則取消選
// 如果再次点击同一个任务,则取消选
if (selectedTaskId === taskId) {
selectedTaskId = null;
taskDetailsContent.innerHTML = `<p class="placeholder">${translate(
@ -563,7 +657,7 @@ function selectTask(taskId) {
selectedTaskId = taskId;
// 添加新的選中狀態
// 添加新的选中状态
const selectedElement = document.querySelector(
`.task-item[data-id="${taskId}"]`
);
@ -571,7 +665,7 @@ function selectTask(taskId) {
selectedElement.classList.add("selected");
}
// 獲取並顯示任務詳
// 获取并显示任务详
const task = tasks.find((t) => t.id === taskId);
if (!task) {
@ -737,8 +831,59 @@ function selectTask(taskId) {
// --- 原來的 innerHTML 賦值已移除 ---
// 只調用高亮函數
highlightNode(taskId); // 只調用 highlightNode
// 高亮节点并将其移动到中心
highlightNode(taskId);
centerNode(taskId);
}
// 新增:重置視圖功能
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);
updateMinimap(); // 在縮放時更新縮略圖
});
if (svg) {
svg.call(zoom);
}
}
// 渲染依賴關係圖 - 修改為全局視圖和 enter/update/exit 模式
@ -746,22 +891,18 @@ 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;
@ -773,10 +914,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,
}));
@ -786,44 +926,60 @@ 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"); // 主要組,用於縮放和平移
// 添加縮略圖背景
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)})`);
// 添加縮放和平移
svg.call(
d3.zoom().on("zoom", (event) => {
g.attr("transform", event.transform);
})
);
// 添加縮略圖背景
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");
// 初始化並添加縮放行為
initZoom();
// 添加箭頭定義
g.append("defs")
@ -840,19 +996,39 @@ 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 事件處理函數
// 新增:水平分布力
.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))
// 新增:基于節點度數的垂直分布力
.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);
// 添加用於存放連結和節點的組
g.append("g").attr("class", "links");
@ -860,12 +1036,42 @@ 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));
}
// 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 +1080,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 +1124,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 +1147,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 +1185,24 @@ function renderDependencyGraph() {
}
// << drag 函數定義結束 >>
// 5. 更新力導向模擬
simulation.nodes(nodes); // 在處理完 enter/exit 後更新模擬節點
// 5. 更新力導向模擬,但不啟動
simulation.nodes(nodes); // 更新模擬節點
simulation.force("link").links(links); // 更新模擬連結
simulation.alpha(0.3).restart(); // 重新激活模擬
// 更新水平分布力的目標位置
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() 調用,防止刷新時的動畫跳變
}
// Tick 函數: 更新節點和連結位置
@ -1035,6 +1222,9 @@ function ticked() {
.selectAll("g.node-item")
// << 修改:添加座標後備值 >>
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`);
// 更新縮略圖
updateMinimap();
}
// 函數:根據節點數據返回顏色 (示例)
@ -1191,5 +1381,84 @@ function getStatusClass(status) {
return status ? status.replace(/_/g, "-") : "unknown"; // 替換所有下劃線
}
// 新增:更新寬高的函數
function updateDimensions() {
if (dependencyGraphElement) {
width = dependencyGraphElement.clientWidth;
height = dependencyGraphElement.clientHeight || 400;
}
}
// 添加縮略圖更新函數
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 ...

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;