Files

704 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>代码审查报告 - UI 预览</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
font-size: 13px;
line-height: 1.5;
max-width: 960px;
margin: 0 auto;
min-height: 100vh;
padding: 0;
}
/* ─── Header ─── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 12px;
border-bottom: 1px solid #21262d;
}
.header h2 {
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
color: #e6edf3;
}
.header .meta {
color: #8b949e;
font-size: 12px;
margin-top: 2px;
}
/* ─── Buttons ─── */
.btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border: 1px solid #30363d;
background: #21262d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: background .15s, border-color .15s;
}
.btn:hover { background: #30363d; }
.btn-primary {
background: #238636;
color: #fff;
border-color: rgba(240,246,252,0.1);
}
.btn-primary:hover { background: #2ea043; }
/* ─── Banner ─── */
.banner {
margin: 12px 16px 0;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.banner-warning {
background: rgba(200, 160, 0, 0.15);
border: 1px solid rgba(200, 160, 0, 0.35);
color: #cca700;
}
.banner-error {
background: rgba(248, 81, 73, 0.15);
border: 1px solid rgba(248, 81, 73, 0.35);
color: #f48771;
}
/* ─── Errors box ─── */
.errors-box {
margin: 12px 16px 0;
padding: 10px 12px;
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
border-radius: 6px;
}
.errors-box-title {
font-weight: 600;
color: #f48771;
font-size: 12px;
margin-bottom: 4px;
}
.errors-box-item {
font-size: 12px;
color: #f48771;
padding: 2px 0;
}
/* ─── Summary Stats ─── */
.summary {
display: flex;
gap: 8px;
padding: 12px 16px 0;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 100px;
padding: 10px 12px;
border: 1px solid #30363d;
border-radius: 8px;
background: #161b22;
text-align: center;
}
.stat-card .num {
font-size: 24px;
font-weight: 700;
line-height: 1.2;
}
.stat-card .label {
font-size: 11px;
color: #8b949e;
margin-top: 1px;
}
.stat-error .num { color: #f48771; }
.stat-warning .num { color: #d29922; }
.stat-info .num { color: #58a6ff; }
.stat-total .num { color: #e6edf3; }
/* ─── Tab Bar ─── */
.tab-bar {
display: flex;
align-items: stretch;
margin: 16px 16px 0;
border-bottom: 1px solid #21262d;
}
.tab {
position: relative;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: #8b949e;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color .15s, border-color .15s;
user-select: none;
white-space: nowrap;
}
.tab:hover { color: #e6edf3; }
.tab.active {
color: #e6edf3;
border-bottom-color: #f78166;
}
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 9px;
font-size: 11px;
font-weight: 500;
line-height: 1;
}
.tab-count-error { background: rgba(248, 81, 73, 0.2); color: #f48771; }
.tab-count-warning { background: rgba(200, 160, 0, 0.2); color: #cca700; }
.tab-count-info { background: rgba(88, 166, 255, 0.2); color: #58a6ff; }
/* ─── Tab Content ─── */
.tab-content { display: none; padding: 8px 16px 20px; }
.tab-content.active { display: block; }
/* ─── Section header ─── */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-top: 12px;
}
.section-header:first-child { padding-top: 0; }
.section-header-title {
font-size: 12px;
font-weight: 600;
color: #8b949e;
text-transform: uppercase;
letter-spacing: .03em;
}
/* ─── Item ─── */
.item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
margin-top: 6px;
border: 1px solid #21262d;
border-radius: 8px;
background: #161b22;
cursor: pointer;
transition: border-color .15s, background .15s;
}
.item:first-child { margin-top: 0; }
.item:hover {
border-color: #58a6ff;
background: #1c2128;
}
.item.expanded {
border-color: #58a6ff;
}
.item-severity {
flex-shrink: 0;
width: 5px;
align-self: stretch;
border-radius: 3px;
margin: -10px 0 -10px -12px;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.item-severity-error { background: #f48771; }
.item-severity-warning { background: #cca700; }
.item-severity-info { background: #58a6ff; }
.item-body { flex: 1; min-width: 0; }
.item-row1 {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
}
.item-icon {
flex-shrink: 0;
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.icon-error { background: #f48771; }
.icon-warning { background: #cca700; }
.icon-info { background: #58a6ff; }
.item-badge {
display: inline-flex;
align-items: center;
padding: 0 7px;
height: 20px;
border-radius: 5px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .03em;
flex-shrink: 0;
line-height: 1;
}
.badge-linter { background: rgba(139, 148, 158, 0.15); color: #8b949e; }
.badge-custom { background: rgba(191, 133, 255, 0.15); color: #ce93d8; }
.badge-ai { background: rgba(79, 195, 247, 0.15); color: #4dd0e1; }
.item-message {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #e6edf3;
font-size: 14px;
}
.item-line {
flex-shrink: 0;
font-size: 11px;
font-weight: 600;
color: #8b949e;
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
background: rgba(139, 148, 158, 0.08);
padding: 1px 6px;
border-radius: 4px;
line-height: 20px;
}
.item-rule {
flex-shrink: 0;
font-size: 12px;
color: #8b949e;
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-fix {
flex-shrink: 0;
padding: 2px 8px;
border: 1px solid #30363d;
background: #21262d;
color: #c9d1d9;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: background .15s;
line-height: 18px;
}
.item-fix:hover { background: #30363d; }
.item-fix:disabled { opacity: .4; cursor: not-allowed; }
/* ─── Detail / Expanded ─── */
.item-detail { display: none; margin-top: 8px; padding-top: 8px; border-top: 1px solid #21262d; }
.item.expanded .item-detail { display: block; animation: fadeSlideIn .2s ease; }
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.detail-text { color: #8b949e; font-size: 13px; line-height: 1.7; }
.detail-text code { font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
.detail-suggestion {
margin-top: 8px;
padding: 8px 12px;
background: rgba(88, 166, 255, 0.08);
border: 1px solid rgba(88, 166, 255, 0.2);
border-radius: 6px;
font-size: 13px;
color: #79c0ff;
}
.detail-diff {
margin-top: 8px;
padding: 10px 12px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
}
.detail-diff del {
color: #f48771;
background: rgba(248, 81, 73, 0.15);
display: block;
text-decoration: none;
}
.detail-diff ins {
color: #3fb950;
background: rgba(63, 185, 80, 0.15);
display: block;
text-decoration: none;
}
.detail-original {
margin-top: 6px;
font-size: 12px;
color: #8b949e;
font-style: italic;
}
.detail-category {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: rgba(139,148,158,0.1);
color: #8b949e;
margin-top: 6px;
}
/* ─── Empty state ─── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
color: #8b949e;
text-align: center;
font-style: italic;
font-size: 13px;
}
/* ─── Actions bar ─── */
.actions {
display: flex;
gap: 8px;
padding: 16px 16px 20px;
border-top: 1px solid #21262d;
}
</style>
</head>
<body>
<div class="header">
<div>
<h2>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
<rect x="9" y="3" width="6" height="4" rx="1"/>
<path d="M9 14l2 2 4-4"/>
</svg>
代码审查报告
</h2>
<div class="meta">
src/UserService.java &middot; Java &middot; 8.3s
</div>
</div>
</div>
<div class="banner banner-warning">⚠ AI 审查未完成,报告仅包含静态分析结果</div>
<div class="errors-box">
<div class="errors-box-title">✖ 执行错误</div>
<div class="errors-box-item">未配置 sql 语言的 linter,请在设置中添加 vscode-code-reviewer.linters.sql</div>
<div class="errors-box-item">AI 审查失败: API 请求超时</div>
</div>
<div class="summary">
<div class="stat-card stat-total"><div class="num">12</div><div class="label">总计问题</div></div>
<div class="stat-card stat-error"><div class="num">3</div><div class="label">错误</div></div>
<div class="stat-card stat-warning"><div class="num">7</div><div class="label">警告</div></div>
<div class="stat-card stat-info"><div class="num">2</div><div class="label">建议</div></div>
</div>
<div class="tab-bar">
<div class="tab active" data-tab="linter" onclick="switchTab('linter')">
🔧 ESLint + PMD
<span class="tab-count tab-count-error">2</span>
<span class="tab-count tab-count-warning">4</span>
<span class="tab-count tab-count-info">1</span>
</div>
<div class="tab" data-tab="custom" onclick="switchTab('custom')">
📋 自定义规则
<span class="tab-count tab-count-error">1</span>
<span class="tab-count tab-count-warning">1</span>
</div>
<div class="tab" data-tab="ai" onclick="switchTab('ai')">
🤖 AI 审查
<span class="tab-count tab-count-warning">2</span>
<span class="tab-count tab-count-info">1</span>
</div>
</div>
<div class="tab-content active" id="tab-linter">
<div class="section-header">
<span class="section-header-title">ESLint + PMD · 7 个问题</span>
<button class="btn">🔧 全部修复</button>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-error"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-error"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:AvoidUsingHardCodedPassword</span>
<span class="item-message">硬编码密码:密码不应硬编码在代码中</span>
<span class="item-line">L13</span>
<button class="item-fix" onclick="event.stopPropagation(); this.disabled=true; this.textContent='⏳...';">🔧 修复</button>
</div>
<div class="item-detail">
<div class="detail-original">原文: Hard-coded password detected (DB_PASS) in field definition</div>
<div class="detail-suggestion">将密码移至环境变量或配置中心,通过 system.getenv() 读取,避免硬编码在源码中</div>
</div>
</div>
</div>
<div class="item static-item expanded" onclick="toggleItem(this)">
<div class="item-severity item-severity-error"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-error"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:SQLInjection</span>
<span class="item-message">SQL 注入:直接拼接用户输入可能导致 SQL 注入攻击</span>
<span class="item-line">L18</span>
<button class="item-fix" onclick="event.stopPropagation(); this.disabled=true; this.textContent='⏳...';">🔧 修复</button>
</div>
<div class="item-detail">
<div class="detail-original">原文: SQL injection detected: string concatenation in query</div>
<div class="detail-suggestion">改用 PreparedStatement 参数化查询,避免字符串拼接</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:CloseResource</span>
<span class="item-message">资源未关闭:Statement/ResultSet 未在 finally 中关闭</span>
<span class="item-line">L19</span>
<button class="item-fix" onclick="event.stopPropagation(); this.disabled=true; this.textContent='⏳...';">🔧 修复</button>
</div>
<div class="item-detail">
<div class="detail-original">原文: Ensure that resources like Statement and ResultSet are closed</div>
<div class="detail-suggestion">使用 try-with-resources 自动关闭 Statement 和 ResultSet,或确保在 finally 块中关闭它们</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:MethodNamingConventions</span>
<span class="item-message">方法命名不规范:FetchUserById 应以小写字母开头</span>
<span class="item-line">L37</span>
</div>
<div class="item-detail">
<div class="detail-original">原文: Method name 'FetchUserById' does not follow camelCase naming convention</div>
<div class="detail-suggestion">将方法名改为 fetchUserById,遵循 Java camelCase 命名规范</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:FieldNamingConventions</span>
<span class="item-message">字段命名:api_key 应使用大写常量命名</span>
<span class="item-line">L44</span>
</div>
<div class="item-detail">
<div class="detail-original">原文: Field name 'api_key' does not follow naming convention</div>
<div class="detail-suggestion">对于 static final 常量,应将名称改为 API_KEY 并使用下划线分隔</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">eslint:no-console</span>
<span class="item-message">生产代码不应保留 console 语句</span>
<span class="item-line">L57</span>
</div>
<div class="item-detail">
<div class="detail-original">原文: Unexpected console statement (no-console)</div>
<div class="detail-suggestion">移除 console.log 调试语句,或使用专门的日志框架替代</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-info"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-info"></span>
<span class="item-badge badge-linter">Linter</span>
<span class="item-rule">pmd:UnusedLocalVariable</span>
<span class="item-message">未使用的局部变量:unusedVar 已声明但未使用</span>
<span class="item-line">L56</span>
</div>
<div class="item-detail">
<div class="detail-original">原文: Avoid unused local variables such as 'unusedVar'</div>
<div class="detail-suggestion">删除未使用的变量声明,或确认是否需要该变量</div>
</div>
</div>
</div>
</div>
<div class="tab-content" id="tab-custom">
<div class="section-header">
<span class="section-header-title">自定义规则 · 2 个问题</span>
</div>
<div class="item static-item expanded" onclick="toggleItem(this)">
<div class="item-severity item-severity-error"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-error"></span>
<span class="item-badge badge-custom">自定义</span>
<span class="item-rule">no-hardcoded-credentials</span>
<span class="item-message">禁止在代码中硬编码数据库密码和 API Key</span>
<span class="item-line">L13</span>
</div>
<div class="item-detail">
<div class="detail-text">规则 no-hardcoded-credentials 触发: 禁止在代码中硬编码数据库密码和 API Key</div>
</div>
</div>
</div>
<div class="item static-item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-custom">自定义</span>
<span class="item-rule">avoid-console-log</span>
<span class="item-message">生产代码不应使用 console.log 调试</span>
<span class="item-line">L57</span>
</div>
<div class="item-detail">
<div class="detail-text">规则 avoid-console-log 触发: 生产代码不应使用 console.log 调试</div>
</div>
</div>
</div>
</div>
<div class="tab-content" id="tab-ai">
<div class="section-header">
<span class="section-header-title">AI 审查建议 · 3 条</span>
</div>
<div class="item expanded" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-ai">AI</span>
<span class="item-rule">sql-injection</span>
<span class="item-message">SQL注入风险:用户状态参数直接拼接 SQL 查询</span>
<span class="item-line">L18</span>
</div>
<div class="item-detail">
<div class="detail-text">getActiveUsers 方法中,status 参数直接拼接到 SQL 查询字符串中,攻击者可传入恶意值导致 SQL 注入。同时缺少对 Connection 为 null 的防御性检查。</div>
<span class="detail-category">🎯 安全漏洞</span>
<div class="detail-suggestion">使用 PreparedStatement 参数化查询;添加 Connection 非空校验;使用 try-with-resources 管理资源</div>
<div class="detail-diff"><del>- String query = "SELECT username FROM users WHERE status = '" + status + "'";</del>
<ins>+ String query = "SELECT username FROM users WHERE status = ?";</ins>
<ins>+ PreparedStatement ps = conn.prepareStatement(query);</ins>
<ins>+ ps.setString(1, status);</ins>
<ins>+ ResultSet rs = ps.executeQuery();</ins></div>
</div>
</div>
</div>
<div class="item" onclick="toggleItem(this)">
<div class="item-severity item-severity-warning"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-warning"></span>
<span class="item-badge badge-ai">AI</span>
<span class="item-rule">hardcoded-secret</span>
<span class="item-message">硬编码密钥:数据库密码和 API Key 直接写在源码中</span>
<span class="item-line">L13</span>
</div>
<div class="item-detail">
<div class="detail-text">DB_PASS 和 api_key 都是敏感凭据,硬编码在类中会导致密钥泄露。任何有权访问代码的人(包括通过反编译 .class 文件)都可获取这些凭据。</div>
<span class="detail-category">🎯 安全漏洞</span>
<div class="detail-suggestion">使用环境变量或密钥管理服务(如 Vault)存储敏感信息,在运行时注入</div>
<div class="detail-diff"><del>- private static final String DB_PASS = "admin123";</del>
<ins>+ private static final String DB_PASS = System.getenv("DB_PASS");</ins></div>
</div>
</div>
</div>
<div class="item" onclick="toggleItem(this)">
<div class="item-severity item-severity-info"></div>
<div class="item-body">
<div class="item-row1">
<span class="item-icon icon-info"></span>
<span class="item-badge badge-ai">AI</span>
<span class="item-rule">unused-method</span>
<span class="item-message">未使用的公共方法:unusedMethod 疑似死代码</span>
<span class="item-line">L55</span>
</div>
<div class="item-detail">
<div class="detail-text">unusedMethod 方法仅在 main 中未被调用,且其内部存在 DEBUG 打印和未使用的局部变量,可能是开发阶段遗留的测试代码,建议确认后删除。</div>
<span class="detail-category">🎯 代码设计</span>
</div>
</div>
</div>
</div>
<div class="actions">
<button class="btn btn-primary">🔄 重新审查</button>
<button class="btn">📄 导出报告</button>
<button class="btn">⚙️ 设置</button>
</div>
<script>
function switchTab(tabId) {
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
document.querySelector('.tab[data-tab="' + tabId + '"]').classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
}
function toggleItem(el) {
if (event.target.closest('button')) return;
el.classList.toggle('expanded');
}
</script>
</body>
</html>