# Step 17 — Phase 5.3: 审查面板(Webview) **依赖**: Step 12, 15 **参考设计**: §7, §14 ## 目标 实现 Webview 审查报告面板:三 Tab 切换、统计卡片、问题列表、postMessage 通信、降级提示。 ## 新建文件 | # | 文件 | 说明 | |---|------|------| | 1 | `src/panel/webview.ts` | `ReviewPanel` 类(Webview 管理 + HTML 生成) | ## 面板参考 UI 预览文件: `docs/superpowers/specs/review-panel-preview.html` --- ## `src/panel/webview.ts` ```typescript import * as vscode from 'vscode'; import { MergedReport } from '../merger/merger'; interface PanelMessage { type: 'navigate' | 'rerun' | 'export' | 'settings' | 'fix' | 'fixAll'; line?: number; ruleId?: string; source?: 'linter' | 'custom' | 'ai'; } export class ReviewPanel { public static currentPanel: ReviewPanel | undefined; private readonly panel: vscode.WebviewPanel; private disposables: vscode.Disposable[] = []; private constructor( private readonly extensionUri: vscode.Uri, column: vscode.ViewColumn ) { this.panel = vscode.window.createWebviewPanel( 'codeReviewer.reviewPanel', '代码审查报告', column, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [], } ); this.panel.onDidDispose(() => this.dispose(), null, this.disposables); this.panel.webview.onDidReceiveMessage( (message: PanelMessage) => this.handleMessage(message), null, this.disposables ); } static createOrShow(extensionUri: vscode.Uri, column?: vscode.ViewColumn): ReviewPanel { if (ReviewPanel.currentPanel) { ReviewPanel.currentPanel.panel.reveal(column); return ReviewPanel.currentPanel; } ReviewPanel.currentPanel = new ReviewPanel(extensionUri, column ?? vscode.ViewColumn.Two); return ReviewPanel.currentPanel; } update(report: MergedReport): void { this.panel.webview.html = this.buildHtml(report); } private buildHtml(report: MergedReport): string { const total = report.linterCount + report.customRuleCount + report.aiCount; const errorCount = report.linterDiagnostics.filter(d => d.severity === 'error').length + report.customRuleDiagnostics.filter(d => d.severity === 'error').length + report.aiFindings.filter(f => f.severity === 'error').length; const warnCount = report.linterDiagnostics.filter(d => d.severity === 'warning').length + report.customRuleDiagnostics.filter(d => d.severity === 'warning').length + report.aiFindings.filter(f => f.severity === 'warning').length; const infoCount = total - errorCount - warnCount; const fileName = report.filePath.split(/[/\\]/).pop() ?? ''; const degradedBanner = report.degraded ? `` : ''; return ` 代码审查报告

📋 代码审查报告

${fileName} · ${report.language} · ${(report.duration / 1000).toFixed(1)}s
${degradedBanner}
${total}
总计
${errorCount}
错误
${warnCount}
警告
${infoCount}
建议
${this.buildLinterList(report)}
${this.buildCustomList(report)}
${this.buildAIList(report)}
`; } private buildLinterList(report: MergedReport): string { if (report.linterDiagnostics.length === 0) { return '
✅ 静态分析未发现问题
'; } return report.linterDiagnostics.map((d, i) => `
${this.sevIcon(d.severity)} ${this.escape(d.ruleId)} L${d.range.start.line + 1}
${this.escape(d.message)}
`).join(''); } private buildCustomList(report: MergedReport): string { if (report.customRuleDiagnostics.length === 0) { return '
✅ 自定义规则未发现问题
'; } return report.customRuleDiagnostics.map((d, i) => `
${this.sevIcon(d.severity)} ${this.escape(d.ruleId)} L${d.range.start.line + 1}
${this.escape(d.message)}
`).join(''); } private buildAIList(report: MergedReport): string { const total = report.translatedDiagnostics.length + report.aiFindings.length; if (total === 0) { return '
🤖 AI 审查未发现新问题
'; } const parts: string[] = []; for (const td of report.translatedDiagnostics) { parts.push(`
🔵 ${this.escape(td.originalRuleId)}
${this.escape(td.translatedMessage)}
${td.translatedSuggestion ? `
建议: ${this.escape(td.translatedSuggestion)}
` : ''}
`); } for (const f of report.aiFindings) { parts.push(`
${this.sevIcon(f.severity)} [${f.category}] ${this.escape(f.title)}
${this.escape(f.description)}
${f.suggestion ? `
建议: ${this.escape(f.suggestion)}
` : ''}
`); } return parts.join(''); } private sevIcon(severity: string): string { switch (severity) { case 'error': return '🔴'; case 'warning': return '🟡'; case 'info': return '🔵'; default: return '⚪'; } } private escape(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } private handleMessage(message: PanelMessage): void { switch (message.type) { case 'navigate': if (message.line !== undefined) { const editor = vscode.window.activeTextEditor; if (editor) { const line = Math.max(0, message.line); const range = new vscode.Range(line, 0, line, 0); editor.selection = new vscode.Selection(range.start, range.end); editor.revealRange(range, vscode.TextEditorRevealType.InCenter); } } break; case 'rerun': vscode.commands.executeCommand('codeReviewer.review'); break; case 'export': vscode.commands.executeCommand('codeReviewer.exportReport'); break; case 'settings': vscode.commands.executeCommand('codeReviewer.openSetup'); break; case 'fix': vscode.commands.executeCommand('codeReviewer.fixIssue', message); break; case 'fixAll': vscode.commands.executeCommand('codeReviewer.fixAll'); break; } } dispose(): void { ReviewPanel.currentPanel = undefined; this.panel.dispose(); for (const d of this.disposables) { d.dispose(); } this.disposables = []; } } ``` --- ## 额外修改:`commands.ts` 中集成审查面板调用 ```typescript import { ReviewPanel } from '../panel/webview'; // 在 'codeReviewer.review' 命令中,静态分析完成后: const panel = ReviewPanel.createOrShow(context.extensionUri); panel.update(report); // 'codeReviewer.exportReport' 命令: const markdown = reportToMarkdown(report); const doc = await vscode.workspace.openTextDocument({ content: markdown, language: 'markdown' }); await vscode.window.showTextDocument(doc); ``` --- ## 验收 - [ ] 审查面板可打开(Webview) - [ ] 三 Tab 切换正常工作 - [ ] 统计卡片数值正确 - [ ] 问题列表可点击跳转到代码位置 - [ ] 降级提示条在 AI 失败时显示 - [ ] 修复/重新审查/导出按钮发送正确消息 - [ ] `npm run compile` 通过 - [ ] `npm run lint` 通过