feat: Phase 3 — 前端交互功能完成
This commit is contained in:
+93
-8
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user