feat: Phase 3 — 前端交互功能完成

This commit is contained in:
2026-06-18 23:37:19 +08:00
parent 886e0d598d
commit 7ac3a3726b
10 changed files with 1387 additions and 261 deletions
+93 -8
View File
@@ -1,4 +1,4 @@
// Git Graph visualization using D3.js
// Git Graph visualization using D3.js with interactive base/head selection
const GitGraph = {
init(containerId, repoId) {
this.container = document.getElementById(containerId);
@@ -7,6 +7,11 @@ const GitGraph = {
this.refs = [];
this.edges = [];
// Selection state
this.selectedBase = null;
this.selectedHead = null;
this.onSelectionChange = null; // callback(baseHash, headHash)
if (!this.container) {
console.error('Graph container not found:', containerId);
return;
@@ -31,6 +36,13 @@ const GitGraph = {
}
},
// Set selection programmatically (e.g. from dropdowns)
setSelection(baseHash, headHash) {
this.selectedBase = baseHash;
this.selectedHead = headHash;
this._updateSelectionVisuals();
},
render() {
if (this.commits.length === 0) {
this.container.innerHTML = '<div class="text-gray-500 p-4">No commits found</div>';
@@ -57,7 +69,7 @@ const GitGraph = {
const nodeMap = {};
const laneWidth = 24;
// Assign lanes to commits (simple algorithm: each branch gets its own lane)
// Assign lanes to commits
const lanes = this.assignLanes();
this.commits.forEach((commit, i) => {
@@ -86,12 +98,15 @@ const GitGraph = {
.attr('stroke-width', 2);
// Draw commit nodes
const self = this;
const nodes = g.selectAll('.node')
.data(this.commits)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d, i) => `translate(${nodeMap[d.hash]?.x || 20},${nodeMap[d.hash]?.y || i * nodeHeight + 20})`);
.attr('data-hash', d => d.hash)
.attr('transform', (d, i) => `translate(${nodeMap[d.hash]?.x || 20},${nodeMap[d.hash]?.y || i * nodeHeight + 20})`)
.style('cursor', 'pointer');
nodes.append('circle')
.attr('r', 6)
@@ -158,16 +173,86 @@ const GitGraph = {
.attr('height', textBBox.height + 6);
});
// Add hover effect
// Hover effect
nodes.on('mouseover', function(event, d) {
d3.select(this).select('circle')
.attr('r', 8)
.attr('fill', '#2563eb');
}).on('mouseout', function() {
d3.select(this).select('circle')
.attr('r', 6)
.attr('fill', '#3b82f6');
}).on('mouseout', function(event, d) {
const node = d3.select(this);
const hash = d.hash;
if (hash === self.selectedBase) {
node.select('circle').attr('r', 9).attr('fill', '#10b981').attr('stroke', '#059669');
} else if (hash === self.selectedHead) {
node.select('circle').attr('r', 9).attr('fill', '#f59e0b').attr('stroke', '#d97706');
} else {
node.select('circle').attr('r', 6).attr('fill', '#3b82f6').attr('stroke', '#1e40af');
}
});
// Click handler for base/head selection
nodes.on('click', function(event, d) {
const hash = d.hash;
if (!self.selectedBase) {
// First click: set base
self.selectedBase = hash;
} else if (!self.selectedHead) {
// Second click: set head
if (hash === self.selectedBase) {
// Clicked same node, deselect base
self.selectedBase = null;
} else {
self.selectedHead = hash;
}
} else {
// Both selected: reset and start new selection
self.selectedBase = hash;
self.selectedHead = null;
}
self._updateSelectionVisuals();
// Notify callback
if (self.onSelectionChange) {
self.onSelectionChange(self.selectedBase, self.selectedHead);
}
});
// Store refs for later updates
this._nodes = nodes;
this._nodeMap = nodeMap;
// Apply any pre-existing selection
this._updateSelectionVisuals();
},
// Update visual state of selected nodes
_updateSelectionVisuals() {
if (!this._nodes) return;
const self = this;
this._nodes.each(function(d) {
const node = d3.select(this);
const circle = node.select('circle');
const hash = d.hash;
if (hash === self.selectedBase) {
circle.attr('r', 9).attr('fill', '#10b981').attr('stroke', '#059669');
} else if (hash === self.selectedHead) {
circle.attr('r', 9).attr('fill', '#f59e0b').attr('stroke', '#d97706');
} else {
circle.attr('r', 6).attr('fill', '#3b82f6').attr('stroke', '#1e40af');
}
});
},
// Get the ref name (branch/tag) for a commit hash, or short hash if none
getRefName(hash) {
const ref = this.refs.find(r => r.hash === hash);
if (ref) return ref.name;
const commit = this.commits.find(c => c.hash === hash);
return commit ? commit.short_hash : hash.substring(0, 7);
},
assignLanes() {