This commit is contained in:
Dang Zerong
2026-03-11 21:16:47 +08:00
parent 459a8cb295
commit 14680f053e
8 changed files with 1557 additions and 39 deletions

View File

@@ -8,6 +8,11 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
body { background-color: #f5f7fa; }
/* Diff 语法高亮 */
#detail-code-diff .diff-add { color: #98c379; background: rgba(72, 120, 80, 0.2); }
#detail-code-diff .diff-del { color: #e06c75; background: rgba(180, 80, 80, 0.2); }
#detail-code-diff .diff-info { color: #61afef; }
#detail-code-diff .diff-header { color: #e5c07b; }
.sidebar {
min-height: 100vh;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
@@ -43,6 +48,38 @@
.issue-high { color: #d32f2f; }
.issue-medium { color: #f57c00; }
.issue-low { color: #388e3c; }
/* PR 详情 - Git 风格:左侧文件树 + 右侧文件内容 */
.pr-detail-file-layout { display: flex; height: 500px; border: 1px solid #dee2e6; border-radius: 6px; overflow: hidden; }
.pr-detail-file-tree { width: 280px; min-width: 280px; overflow-y: auto; background: #f8f9fa; border-right: 1px solid #dee2e6; }
.pr-detail-file-tree .tree-root { padding: 8px 0; }
.pr-detail-file-tree .tree-node { padding: 2px 8px; font-size: 13px; cursor: pointer; border-radius: 4px; }
.pr-detail-file-tree .tree-node:hover { background: #e9ecef; }
.pr-detail-file-tree .tree-node.active { background: #0d6efd; color: #fff; }
.pr-detail-file-tree .tree-folder { font-weight: 600; color: #495057; }
.pr-detail-file-tree .tree-file { padding-left: 12px; }
/* 代码在中间,右侧缺陷标注,虚线连接代码行与标注(参考审阅风格) */
.pr-detail-file-right { flex: 1; display: flex; overflow: hidden; min-width: 0; }
.pr-detail-file-wrapper { width: 100%; height: 100%; min-width: 0; overflow: hidden; }
.pr-detail-code-view { width: 100%; height: 100%; overflow: auto; background: #1e1e1e; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.5; }
.pr-detail-code-view .code-line { display: flex; min-height: 1.5em; align-items: stretch; }
.pr-detail-code-view .code-line .line-gutter { width: 48px; min-width: 48px; flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding-right: 8px; color: #6c757d; background: #252526; user-select: none; }
.pr-detail-code-view .code-line .line-gutter .line-num { margin-right: 4px; }
.pr-detail-code-view .code-line .line-gutter .gutter-icon { width: 14px; height: 14px; flex-shrink: 0; }
.pr-detail-code-view .code-line .line-gutter .gutter-icon.icon-error { color: #f14c4c; }
.pr-detail-code-view .code-line .line-gutter .gutter-icon.icon-warning { color: #cca700; }
.pr-detail-code-view .code-line .line-content { flex: 1; min-width: 0; color: #d4d4d4; padding: 0 12px; white-space: pre-wrap; word-break: break-all; overflow-wrap: break-word; }
.pr-detail-code-view .code-line.line-has-issue .line-content { background: rgba(220, 53, 69, 0.1); border-left: 2px solid #dc3545; padding-left: 10px; }
/* 虚线连接区:代码与标注之间 */
.pr-detail-code-view .code-line .line-connector { width: 20px; min-width: 20px; flex-shrink: 0; position: relative; background: #1e1e1e; }
.pr-detail-code-view .code-line.line-has-issue .line-connector::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; margin-top: -1px; height: 0; border-bottom: 1px dashed #f14c4c; }
.pr-detail-code-view .code-line .line-annotation { width: 180px; min-width: 180px; flex-shrink: 0; padding: 6px 8px; font-size: 11px; background: #252526; color: #9d9d9d; border-left: 1px solid #3c3c3c; white-space: normal; word-break: break-word; display: flex; align-items: center; font-family: inherit; }
.pr-detail-code-view .code-line.line-has-issue .line-annotation { background: #fff8f8; border-left: 2px solid #dc3545; color: #c62828; }
.pr-detail-code-view .code-line .line-annotation .anno-text { line-height: 1.4; }
.pr-detail-code-view .code-line .line-annotation .anno-icon { color: #dc3545; margin-right: 6px; flex-shrink: 0; }
.pr-detail-file-placeholder { color: #6c757d; padding: 24px; text-align: center; width: 100%; }
/* PR 详情弹窗加宽:占屏幕大部分宽度,代码区随之扩大 */
.modal-dialog-pr-wide { max-width: 92%; width: 92%; margin-left: auto; margin-right: auto; }
@media (min-width: 1400px) { .modal-dialog-pr-wide { max-width: 1400px; width: 92%; } }
</style>
</head>
<body>
@@ -208,9 +245,9 @@
</div>
</div>
<!-- PR 详情模态框 -->
<!-- PR 详情模态框:加宽整体以扩大代码区域 -->
<div class="modal fade" id="prDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-dialog modal-dialog-pr-wide">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detail-pr-title">PR 详情</h5>
@@ -229,16 +266,20 @@
<p><strong>安全漏洞:</strong> <span id="detail-security"></span></p>
</div>
</div>
<ul class="nav nav-tabs" id="detail-tabs" role="tablist">
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-scan-result">扫描结果</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-ai-review">AI 审查</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="tab-scan-result">
<pre id="detail-scan-result" style="max-height: 400px; overflow: auto; background: #f8f9fa; padding: 15px; border-radius: 5px;"></pre>
</div>
<div class="tab-pane fade" id="tab-ai-review">
<div id="detail-ai-review" style="white-space: pre-wrap; background: #f8f9fa; padding: 15px; border-radius: 5px;"></div>
<!-- 仅保留文件 Tab左侧文件树 + 右侧完整文件内容,最右侧为问题标注 -->
<div class="mt-3">
<div class="pr-detail-file-layout">
<div class="pr-detail-file-tree">
<div id="pr-file-tree-loading" class="p-3 text-center text-muted">加载文件列表中...</div>
<div id="pr-file-tree" class="tree-root" style="display: none;"></div>
</div>
<div class="pr-detail-file-right">
<div id="pr-file-content-placeholder" class="pr-detail-file-placeholder">点击左侧文件查看完整内容</div>
<div id="pr-file-content-wrapper" class="pr-detail-file-wrapper">
<div class="pr-detail-code-view" id="pr-file-content"></div>
</div>
<div id="pr-file-content-error" class="pr-detail-file-placeholder" style="display: none; color: #dc3545;"></div>
</div>
</div>
</div>
</div>
@@ -259,6 +300,44 @@
<script>
let currentPRId = null;
let currentPRInfo = null;
let currentDiffData = null;
let currentAIReview = null;
// 全局Markdown 简易格式化
function formatMarkdown(text) {
if (!text) return '';
return String(text)
.replace(/^### (.+)$/gm, '<h5 class="mt-3 mb-2">$1</h5>')
.replace(/^## (.+)$/gm, '<h4 class="mt-3 mb-2">$1</h4>')
.replace(/^# (.+)$/gm, '<h3 class="mt-3 mb-2">$1</h3>')
.replace(/^\*\*(.+):\*\*$/gm, '<strong>$1:</strong>')
.replace(/^- /gm, '<br>• ')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
}
// 将 AI 审查对象格式化为 HTML优点/问题/优化 或 raw_review
function formatReviewObject(review) {
if (!review || typeof review !== 'object') return '';
if (review.raw_review) return formatMarkdown(review.raw_review);
let html = '';
if (review['优点'] && review['优点'].length) {
html += '<div class="mb-2"><strong class="text-success">优点</strong><ul class="mb-0">';
review['优点'].forEach(s => { html += '<li>' + escapeHtml(s) + '</li>'; });
html += '</ul></div>';
}
if (review['问题'] && review['问题'].length) {
html += '<div class="mb-2"><strong class="text-danger">问题</strong><ul class="mb-0">';
review['问题'].forEach(s => { html += '<li>' + escapeHtml(s) + '</li>'; });
html += '</ul></div>';
}
if (review['优化'] && review['优化'].length) {
html += '<div class="mb-2"><strong class="text-primary">优化</strong><ul class="mb-0">';
review['优化'].forEach(s => { html += '<li>' + escapeHtml(s) + '</li>'; });
html += '</ul></div>';
}
return html || formatMarkdown(JSON.stringify(review, null, 2));
}
// 页面切换
function showPage(page) {
@@ -355,36 +434,513 @@
document.getElementById('detail-security').innerHTML = (pr.security_issues || 0) > 0 ?
'<span class="text-danger">' + pr.security_issues + '</span>' : '0';
// 扫描结果
let scanResult = pr.scan_result;
if (typeof scanResult === 'string') {
try { scanResult = JSON.parse(scanResult); } catch(e) {}
}
document.getElementById('detail-scan-result').textContent = JSON.stringify(scanResult, null, 2);
// AI 审查
let aiReview = pr.ai_review;
if (typeof aiReview === 'string') {
try { aiReview = JSON.parse(aiReview); } catch(e) {}
}
if (aiReview && aiReview.review) {
document.getElementById('detail-ai-review').textContent = aiReview.review;
} else {
document.getElementById('detail-ai-review').textContent = '无 AI 审查结果';
}
// 根据状态显示/隐藏按钮
const canOperate = pr.state === 'open';
document.getElementById('btn-merge').style.display = canOperate ? 'inline-block' : 'none';
document.getElementById('btn-reject').style.display = canOperate ? 'inline-block' : 'none';
// 显示模态框
new bootstrap.Modal(document.getElementById('prDetailModal')).show();
const modal = new bootstrap.Modal(document.getElementById('prDetailModal'));
modal.show();
// 加载文件树(左侧树,点击文件在右侧显示完整内容+标注)
loadPRFileTree(id);
} catch (e) {
alert('加载 PR 详情失败: ' + e.message);
}
}
// 加载 PR 文件列表并渲染左侧树,点击文件在右侧显示完整内容
async function loadPRFileTree(prId) {
const loadingEl = document.getElementById('pr-file-tree-loading');
const treeEl = document.getElementById('pr-file-tree');
const placeholderEl = document.getElementById('pr-file-content-placeholder');
const wrapperEl = document.getElementById('pr-file-content-wrapper');
const contentEl = document.getElementById('pr-file-content');
const errorEl = document.getElementById('pr-file-content-error');
if (loadingEl) loadingEl.style.display = 'block';
if (treeEl) treeEl.style.display = 'none';
if (placeholderEl) placeholderEl.style.display = 'block';
if (wrapperEl) wrapperEl.style.display = 'none';
if (contentEl) contentEl.innerHTML = '';
if (errorEl) errorEl.style.display = 'none';
try {
const res = await fetch('/api/prs/' + prId + '/files');
const data = await res.json();
if (data.error || !data.files || !data.files.length) {
if (treeEl) {
treeEl.style.display = 'block';
treeEl.innerHTML = '<div class="p-3 text-muted small">' + (data.error || '暂无变更文件') + '</div>';
}
if (loadingEl) loadingEl.style.display = 'none';
return;
}
const files = data.files;
const tree = buildFileTree(files);
if (treeEl) {
treeEl.innerHTML = renderFileTreeNodes(tree, prId);
treeEl.style.display = 'block';
}
if (loadingEl) loadingEl.style.display = 'none';
} catch (e) {
if (treeEl) {
treeEl.style.display = 'block';
treeEl.innerHTML = '<div class="p-3 text-danger small">加载失败: ' + escapeHtml(e.message) + '</div>';
}
if (loadingEl) loadingEl.style.display = 'none';
}
}
// 将扁平文件列表转为树结构 { name, children?, path? }
function buildFileTree(files) {
const root = { name: '', children: {} };
for (const f of files) {
const path = f.filename || f.path || f;
const parts = typeof path === 'string' ? path.split('/') : [String(path)];
let cur = root;
for (let i = 0; i < parts.length; i++) {
const isFile = i === parts.length - 1;
const name = parts[i];
if (isFile) {
if (!cur.children[name]) cur.children[name] = { name, path, file: true, status: f.status };
} else {
if (!cur.children[name]) cur.children[name] = { name, children: {} };
cur = cur.children[name];
}
}
}
return root;
}
// 递归渲染树节点 HTML
function renderFileTreeNodes(node, prId) {
const entries = Object.keys(node.children || {}).sort((a, b) => {
const aa = node.children[a];
const ab = node.children[b];
const aIsDir = !aa.file;
const bIsDir = !ab.file;
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
return a.localeCompare(b);
});
if (entries.length === 0) return '';
let html = '';
for (const key of entries) {
const child = node.children[key];
if (child.file) {
const status = child.status || '';
const statusCls = status === 'added' ? 'text-success' : status === 'removed' ? 'text-danger' : 'text-secondary';
html += '<div class="tree-node tree-file ' + statusCls + '" data-path="' + escapeHtml(child.path) + '" data-pr-id="' + prId + '"><i class="bi bi-file-code me-1"></i>' + escapeHtml(key) + '</div>';
} else {
html += '<div class="tree-folder tree-node">' + escapeHtml(key) + '</div>';
html += '<div style="padding-left: 12px;">' + renderFileTreeNodes(child, prId) + '</div>';
}
}
return html;
}
// 点击文件时加载并显示内容
document.addEventListener('click', function(ev) {
const node = ev.target.closest('.tree-file');
if (!node) return;
ev.preventDefault();
const path = node.getAttribute('data-path');
const prId = node.getAttribute('data-pr-id');
if (!path || !prId) return;
document.querySelectorAll('.pr-detail-file-tree .tree-node.active').forEach(el => el.classList.remove('active'));
node.classList.add('active');
loadPRFileContent(prId, path);
});
async function loadPRFileContent(prId, path) {
const placeholderEl = document.getElementById('pr-file-content-placeholder');
const wrapperEl = document.getElementById('pr-file-content-wrapper');
const contentEl = document.getElementById('pr-file-content');
const errorEl = document.getElementById('pr-file-content-error');
if (placeholderEl) placeholderEl.style.display = 'none';
if (errorEl) errorEl.style.display = 'none';
try {
const res = await fetch('/api/prs/' + prId + '/file?path=' + encodeURIComponent(path));
const data = await res.json();
if (data.error) {
errorEl.textContent = data.error;
errorEl.style.display = 'block';
return;
}
// 行号 -> 问题列表
const lineIssues = {};
const issues = data.scan_issues || [];
for (const issue of issues) {
const line = issue.line || 1;
if (!lineIssues[line]) lineIssues[line] = [];
lineIssues[line].push(issue);
}
// 代码在中间,右侧缺陷标注,虚线连接代码行与标注
const lines = (data.content || '').split('\n');
let html = '';
for (let i = 0; i < lines.length; i++) {
const lineNum = i + 1;
const rowIssues = lineIssues[lineNum] || [];
const hasIssue = rowIssues.length > 0;
const rowClass = hasIssue ? 'code-line line-has-issue' : 'code-line';
const lineText = lines[i];
const displayText = lineText === '' ? '\u00A0' : lineText;
let gutterHtml = '<span class="line-num">' + lineNum + '</span>';
if (hasIssue) {
const first = rowIssues[0];
const sev = first.severity || 'info';
const iconClass = (sev === 'error' || sev === 'high') ? 'gutter-icon icon-error' : 'gutter-icon icon-warning';
gutterHtml += '<span class="' + iconClass + '"><i class="bi bi-exclamation-circle-fill" style="font-size:12px;"></i></span>';
}
const reasonText = hasIssue ? rowIssues.map(function(iss) {
return (iss.scanner ? '[' + iss.scanner + '] ' : '') + (iss.message || '');
}).join('') : '';
html += '<div class="' + rowClass + '">';
html += '<div class="line-gutter">' + gutterHtml + '</div>';
html += '<span class="line-content">' + escapeHtml(displayText) + '</span>';
html += '<div class="line-connector"></div>';
html += '<div class="line-annotation">';
if (hasIssue && reasonText) {
html += '<span class="anno-icon"><i class="bi bi-exclamation-triangle-fill"></i></span>';
html += '<span class="anno-text">' + escapeHtml(reasonText) + '</span>';
}
html += '</div>';
html += '</div>';
}
contentEl.innerHTML = html;
if (wrapperEl) wrapperEl.style.display = 'block';
} catch (e) {
errorEl.textContent = '加载失败: ' + e.message;
errorEl.style.display = 'block';
}
}
// 渲染带代码片段的扫描结果
function renderScanDetailsWithCode(scanDetails) {
let html = '';
// 显示汇总信息
if (scanDetails.summary) {
html += '<div class="alert alert-info mb-3">';
html += `<strong>总问题数:</strong> ${scanDetails.total_issues} | `;
html += `<strong>扫描器:</strong> ${scanDetails.scanners ? scanDetails.scanners.length : 0}`;
html += '</div>';
}
// 遍历每个扫描器
if (scanDetails.scanners) {
for (const scanner of scanDetails.scanners) {
html += `<div class="card mb-3">
<div class="card-header bg-light">
<strong>${scanner.name}</strong>
<span class="badge bg-${scanner.total_issues > 0 ? 'danger' : 'success'}">
${scanner.total_issues} 个问题
</span>
</div>
<div class="card-body">`;
if (scanner.issues && scanner.issues.length > 0) {
for (const issue of scanner.issues) {
html += `<div class="issue-item mb-3 p-2 border rounded">`;
html += `<div class="fw-bold text-${issue.severity === 'error' ? 'danger' : 'warning'}">`;
html += `<i class="bi bi-exclamation-triangle"></i> ${issue.message || issue.description || '问题'}`;
html += ` <small class="text-muted">(${issue.file}:${issue.line || '?'})</small>`;
html += `</div>`;
// 显示代码片段
if (issue.code_context) {
const ctx = issue.code_context;
html += `<div class="code-context mt-2" style="background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px; font-size: 12px; overflow-x: auto;">`;
html += `<div class="text-muted small mb-1">文件: ${ctx.file || issue.file}</div>`;
if (ctx.context) {
// 显示上下文代码
for (const line of ctx.context) {
const lineClass = line.is_issue_line ? 'background: rgba(255, 200, 0, 0.2);' : '';
const prefix = line.is_issue_line ? '👉 ' : ' ';
html += `<div style="${lineClass}white-space: pre;">${prefix}${line.line_number}: ${escapeHtml(line.code)}</div>`;
}
} else if (ctx.preview) {
// 显示文件预览
html += `<pre style="margin: 0;">${escapeHtml(ctx.preview)}</pre>`;
if (ctx.has_more) {
html += `<div class="text-muted small">... (更多内容)</div>`;
}
}
html += `</div>`;
}
html += `</div>`;
}
} else {
html += '<div class="text-muted">没有问题</div>';
}
html += '</div></div>';
}
}
return html;
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 加载 PR 代码差异
async function loadPRDiff(prId) {
try {
const response = await fetch('/api/prs/' + prId + '/diff');
const data = await response.json();
const loadingEl = document.getElementById('detail-code-diff-loading');
const diffEl = document.getElementById('detail-code-diff');
if (loadingEl) loadingEl.style.display = 'none';
if (data.error) {
if (diffEl) {
diffEl.style.display = 'block';
diffEl.textContent = '加载失败: ' + data.error;
}
return;
}
// 格式化 diff 显示
const diff = data.diff || '';
if (!diff) {
if (diffEl) {
diffEl.style.display = 'block';
diffEl.textContent = '没有代码差异';
}
return;
}
// 添加语法高亮样式
const formattedDiff = diff
.replace(/^diff --git.*$/gm, match => '\n' + match)
.replace(/^@@.*@@$/gm, match => '\n' + match + '\n')
.replace(/^\+.*$/gm, match => match)
.replace(/^-.*$/gm, match => match);
if (diffEl) {
diffEl.style.display = 'block';
diffEl.textContent = diff;
}
} catch (e) {
const loadingEl = document.getElementById('detail-code-diff-loading');
if (loadingEl) loadingEl.style.display = 'none';
if (diffEl) {
diffEl.style.display = 'block';
diffEl.textContent = '加载失败: ' + e.message;
}
}
}
// 加载 AI 审查结果:一行一行展示(每行左侧代码片段 + 右侧审查结果)
async function loadAIReview(prId) {
try {
const prResponse = await fetch('/api/prs/' + prId);
const pr = await prResponse.json();
if (pr.error) {
showAIReviewError('加载失败: ' + pr.error);
return;
}
let aiReview = pr.ai_review;
if (typeof aiReview === 'string') {
try { aiReview = JSON.parse(aiReview); } catch(e) {}
}
currentAIReview = aiReview;
const diffResponse = await fetch('/api/prs/' + prId + '/diff');
const diffData = await diffResponse.json();
currentDiffData = diffData.diff || '';
const loadingEl = document.getElementById('ai-review-loading');
const emptyEl = document.getElementById('ai-review-empty');
const contentEl = document.getElementById('ai-review-content');
const summaryEl = document.getElementById('ai-review-summary');
const rowsEl = document.getElementById('ai-review-rows');
if (!loadingEl || !emptyEl || !contentEl || !rowsEl) return;
loadingEl.style.display = 'none';
// 兼容处理:优先使用 reviews 数组,若无则尝试从 summary 文本中解析文件审查
let reviews = [];
if (aiReview && Array.isArray(aiReview.reviews)) {
reviews = aiReview.reviews;
} else if (aiReview && aiReview.summary && typeof aiReview.summary === 'string') {
// 尝试从 summary 文本中解析每个文件的审查结果
// 格式如: "### 📄 app.py\n\n**优点:**\n- ..."
const summaryText = aiReview.summary;
// 按 "### 📄 " 分割,提取每个文件的审查
const fileBlocks = summaryText.split(/(?=### 📄 )/);
for (const block of fileBlocks) {
const match = block.match(/### 📄 ([^\n]+)\n([\s\S]*)/);
if (match) {
const fileName = match[1].trim();
const reviewContent = match[2].trim();
// 将文本格式转为对象结构
const reviewObj = parseReviewTextToObject(reviewContent);
reviews.push({ file: fileName, review: reviewObj });
}
}
// 如果解析不出文件,则将整个 summary 作为整体审查
if (reviews.length === 0) {
reviews = [{ file: '整体审查结果', review: { 'raw_review': summaryText } }];
}
}
// 辅助函数:将审查文本转为对象结构
function parseReviewTextToObject(text) {
const obj = {};
// 匹配 "**优点:**" / "**需要改进:**" / "**优化建议:**" 等
const sections = { '优点': [], '问题': [], '优化': [] };
let currentSection = null; // '优点' | '问题' | '优化'
const lines = text.split('\n');
for (const line of lines) {
let found = false;
if (/优点/.test(line) && /\*\*/.test(line)) {
currentSection = '优点';
found = true;
} else if (/(需要改进|问题)/.test(line) && /\*\*/.test(line)) {
currentSection = '问题';
found = true;
} else if (/(优化建议|优化)/.test(line) && /\*\*/.test(line)) {
currentSection = '优化';
found = true;
}
if (!found && currentSection) {
const m = line.match(/^- (.+)/);
if (m) {
sections[currentSection].push(m[1]);
}
}
}
// 清理空数组
if (sections['优点'].length) obj['优点'] = sections['优点'];
if (sections['问题'].length) obj['问题'] = sections['问题'];
if (sections['优化'].length) obj['优化'] = sections['优化'];
// 如果解析不出结构,直接返回原始文本
return Object.keys(obj).length ? obj : { 'raw_review': text };
}
if (reviews.length === 0) {
emptyEl.style.display = 'block';
contentEl.style.display = 'none';
return;
}
emptyEl.style.display = 'none';
contentEl.style.display = 'block';
if (summaryEl) {
summaryEl.textContent = aiReview.summary || ('已审查 ' + reviews.length + ' 个文件');
}
let rowsHtml = '';
const diff = currentDiffData || '';
for (const item of reviews) {
const fileDisplay = item.file || (item.path ? item.path.replace(/^.*[/\\]/, '') : '');
const fileForDiff = item.file || fileDisplay;
const codeSnippet = diff ? extractFileFromDiff(diff, fileForDiff) : '';
const codeDisplay = codeSnippet || '(该文件无 diff 或未在 PR 中修改)';
const reviewHtml = formatReviewObject(item.review);
rowsHtml += `
<div class="card mb-3 border">
<div class="card-header py-2 bg-light d-flex justify-content-between align-items-center">
<span><i class="bi bi-file-code me-1"></i> ${escapeHtml(fileDisplay)}</span>
</div>
<div class="card-body p-0">
<div class="row g-0">
<div class="col-md-6 border-end">
<div class="p-2 bg-dark text-light">
<pre class="mb-0 p-2" style="background:#1e1e1e;color:#d4d4d4;font-size:12px;max-height:280px;overflow:auto;white-space:pre;">${escapeHtml(codeDisplay)}</pre>
</div>
</div>
<div class="col-md-6">
<div class="p-3" style="min-height:120px;">${reviewHtml || '—'}</div>
</div>
</div>
</div>
</div>`;
}
rowsEl.innerHTML = rowsHtml;
} catch (e) {
showAIReviewError('加载失败: ' + e.message);
}
}
// 显示 AI 审查错误信息
function showAIReviewError(message) {
const loadingEl = document.getElementById('ai-review-loading');
const emptyEl = document.getElementById('ai-review-empty');
const contentEl = document.getElementById('ai-review-content');
if (loadingEl) loadingEl.style.display = 'none';
if (contentEl) contentEl.style.display = 'none';
if (emptyEl) {
emptyEl.style.display = 'block';
emptyEl.textContent = message;
}
}
// 从 diff 中提取指定文件的代码
function extractFileFromDiff(diff, targetFile) {
const lines = diff.split('\n');
let inTargetFile = false;
let result = [];
let currentFile = '';
for (const line of lines) {
// 检测新文件开始
const diffMatch = line.match(/diff --git a\/(.+) b\/(.+)/);
if (diffMatch) {
currentFile = diffMatch[1];
inTargetFile = currentFile.includes(targetFile) || targetFile.includes(currentFile);
continue;
}
// 如果在目标文件中,收集代码行
if (inTargetFile) {
// 遇到新文件,停止
if (line.startsWith('diff --git')) break;
// 跳过 diff 和 hunk 头部
if (line.startsWith('@@')) {
result.push(line);
continue;
}
// 添加实际代码(去掉 +, -, 前缀,但保留内容)
if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
result.push(line);
}
}
}
return result.join('\n');
}
// 监听 AI 审查标签点击
// 合并 PR
async function mergePR() {
if (!currentPRInfo) return;
@@ -443,6 +999,10 @@
}
// 初始化
window.onerror = function(msg, url, line, col, error) {
console.error('Global error:', msg, 'at line', line);
return false;
};
loadDashboard();
</script>
</body>