feat: Phase 2 — Git 核心功能,go-git 克隆、SSE 进度、D3.js 图形、diff2html 查看器
This commit is contained in:
+208
-40
@@ -1,51 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="h-full">
|
||||
{{template "head" .}}
|
||||
<body class="h-full bg-gray-50 text-gray-900">
|
||||
<div class="min-h-full flex flex-col">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
{{template "head" .}}
|
||||
<link rel="stylesheet" href="/static/lib/diff2html.min.css">
|
||||
<script src="/static/lib/d3.min.js"></script>
|
||||
<script src="/static/lib/diff2html.min.js"></script>
|
||||
<script src="/static/lib/highlight.min.js"></script>
|
||||
<script src="/static/js/graph.js"></script>
|
||||
<script src="/static/js/diff-viewer.js"></script>
|
||||
<style>
|
||||
.graph-container {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
.graph-container svg {
|
||||
display: block;
|
||||
}
|
||||
.ref-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ref-tag:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
{{template "nav" .}}
|
||||
<main class="flex-1">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">仓库详情</h1>
|
||||
<div class="flex gap-3">
|
||||
<a href="/repo/{{.ID}}/generate"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-green-700">
|
||||
生成 PR 描述
|
||||
</a>
|
||||
<a href="/repo/{{.ID}}/review"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700">
|
||||
AI 代码审查
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
<span id="repo-url" class="text-gray-500 text-lg font-normal"></span>
|
||||
</h1>
|
||||
<div class="flex gap-2">
|
||||
<a id="btn-generate" href="#" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
生成 PR 描述
|
||||
</a>
|
||||
<a id="btn-review" href="#" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||
AI 代码审查
|
||||
</a>
|
||||
</div>
|
||||
<!-- Repo Info -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div id="repo-info" class="text-sm text-gray-500">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="bg-white rounded-lg shadow-md">
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex -mb-px">
|
||||
<button onclick="showTab('graph')" id="tab-graph" class="px-6 py-3 text-sm font-medium border-b-2 border-blue-500 text-blue-600">
|
||||
Git Graph
|
||||
</button>
|
||||
<button onclick="showTab('diff')" id="tab-diff" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
Diff 视图
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Git Graph -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold mb-4">Git Graph</h2>
|
||||
<div id="git-graph" class="overflow-auto" style="min-height: 400px;">
|
||||
<p class="text-sm text-gray-400 text-center py-10">Git Graph 将在 Phase 3 实现</p>
|
||||
|
||||
<!-- Graph Tab -->
|
||||
<div id="panel-graph" class="p-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">Base:</label>
|
||||
<select id="graph-base" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">选择分支/标签</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">Head:</label>
|
||||
<select id="graph-head" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">选择分支/标签</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="git-graph" class="graph-container p-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Diff Tab -->
|
||||
<div id="panel-diff" class="p-4 hidden">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">Base:</label>
|
||||
<select id="diff-base" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">选择分支/标签</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">Head:</label>
|
||||
<select id="diff-head" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">选择分支/标签</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="loadDiff()" class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700">
|
||||
查看 Diff
|
||||
</button>
|
||||
<div class="flex gap-1 ml-auto">
|
||||
<button onclick="setDiffView('side-by-side')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">并排</button>
|
||||
<button onclick="setDiffView('unified')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">统一</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="diff-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{template "footer" .}}
|
||||
</div>
|
||||
<script>
|
||||
fetch('/api/repos').then(r => r.json()).then(repos => {
|
||||
const repo = repos.find(r => r.id == {{.ID}});
|
||||
const el = document.getElementById('repo-info');
|
||||
if (repo) {
|
||||
el.innerHTML = '<p><strong>URL:</strong> ' + repo.url + '</p>' +
|
||||
'<p class="mt-1"><strong>大小:</strong> ' + (repo.size_bytes / 1024 / 1024).toFixed(1) + ' MB</p>' +
|
||||
'<p class="mt-1"><strong>克隆时间:</strong> ' + repo.cloned_at + '</p>';
|
||||
} else {
|
||||
el.innerHTML = '<p class="text-red-500">仓库未找到</p>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const repoId = {{.ID}};
|
||||
let currentRefs = [];
|
||||
|
||||
// Load repo info and refs
|
||||
async function init() {
|
||||
try {
|
||||
// Load refs
|
||||
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
|
||||
if (refsResp.ok) {
|
||||
currentRefs = await refsResp.json();
|
||||
populateRefSelects(currentRefs);
|
||||
}
|
||||
|
||||
// Load repo info
|
||||
const reposResp = await fetch('/api/repos');
|
||||
if (reposResp.ok) {
|
||||
const repos = await reposResp.json();
|
||||
const repo = repos.find(r => r.id === repoId);
|
||||
if (repo) {
|
||||
document.getElementById('repo-url').textContent = repo.url;
|
||||
document.getElementById('btn-generate').href = `/repo/${repoId}/generate`;
|
||||
document.getElementById('btn-review').href = `/repo/${repoId}/review`;
|
||||
}
|
||||
}
|
||||
|
||||
// Init graph
|
||||
GitGraph.init('git-graph', repoId);
|
||||
DiffViewer.init('diff-container');
|
||||
} catch (err) {
|
||||
console.error('Init error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function populateRefSelects(refs) {
|
||||
const selects = ['graph-base', 'graph-head', 'diff-base', 'diff-head'];
|
||||
|
||||
selects.forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">选择分支/标签</option>';
|
||||
|
||||
// Branches first
|
||||
const branches = refs.filter(r => !r.is_tag);
|
||||
const tags = refs.filter(r => r.is_tag);
|
||||
|
||||
if (branches.length > 0) {
|
||||
const group = document.createElement('optgroup');
|
||||
group.label = '分支';
|
||||
branches.forEach(ref => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ref.name;
|
||||
opt.textContent = ref.name + (ref.is_head ? ' (HEAD)' : '');
|
||||
group.appendChild(opt);
|
||||
});
|
||||
select.appendChild(group);
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
const group = document.createElement('optgroup');
|
||||
group.label = '标签';
|
||||
tags.forEach(ref => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ref.name;
|
||||
opt.textContent = ref.name;
|
||||
group.appendChild(opt);
|
||||
});
|
||||
select.appendChild(group);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-select main/master as base, HEAD as head
|
||||
const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
|
||||
const headBranch = refs.find(r => r.is_head);
|
||||
|
||||
selects.forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
if (selectId.includes('base') && mainBranch) {
|
||||
select.value = mainBranch.name;
|
||||
} else if (selectId.includes('head') && headBranch) {
|
||||
select.value = headBranch.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showTab(tab) {
|
||||
document.getElementById('panel-graph').classList.toggle('hidden', tab !== 'graph');
|
||||
document.getElementById('panel-diff').classList.toggle('hidden', tab !== 'diff');
|
||||
document.getElementById('tab-graph').classList.toggle('border-blue-500', tab === 'graph');
|
||||
document.getElementById('tab-graph').classList.toggle('text-blue-600', tab === 'graph');
|
||||
document.getElementById('tab-diff').classList.toggle('border-blue-500', tab === 'diff');
|
||||
document.getElementById('tab-diff').classList.toggle('text-blue-600', tab === 'diff');
|
||||
}
|
||||
|
||||
function loadDiff() {
|
||||
const base = document.getElementById('diff-base').value;
|
||||
const head = document.getElementById('diff-head').value;
|
||||
if (!base || !head) {
|
||||
alert('请选择 Base 和 Head 分支');
|
||||
return;
|
||||
}
|
||||
DiffViewer.loadDiff(repoId, base, head);
|
||||
}
|
||||
|
||||
function setDiffView(mode) {
|
||||
DiffViewer.setViewMode(mode);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user