feat: 添加缩略图功能及更新逻辑,增强依赖关系图和任务列表的联系

This commit is contained in:
yitacls 2025-06-10 21:39:37 +08:00
parent 2b2d002f31
commit c843cbc820

View File

@ -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 = `<p class="placeholder">${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 ...