feat:代码变更diff可视化功能实现
This commit is contained in:
366
src/views/changePanel.ts
Normal file
366
src/views/changePanel.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/**
|
||||
* 代码变更面板组件
|
||||
* 功能:显示 AI 修改的文件列表和 diff 对比
|
||||
* 依赖:utils/diffRenderer
|
||||
* 使用场景:对话结束后展示代码变更供用户审查
|
||||
*/
|
||||
|
||||
import { getDiffStyles } from '../utils/diffRenderer';
|
||||
|
||||
/**
|
||||
* 获取变更面板的 HTML 内容
|
||||
*/
|
||||
export function getChangePanelContent(): string {
|
||||
return `
|
||||
<div class="change-panel" id="changePanel" style="display: none;">
|
||||
<div class="change-panel-header">
|
||||
<div class="change-panel-title">
|
||||
<span class="change-icon">📝</span>
|
||||
<span>代码变更</span>
|
||||
<span class="change-count" id="changeCount">0</span>
|
||||
</div>
|
||||
<button class="change-toggle-btn" id="changePanelToggle" onclick="toggleChangePanel()">
|
||||
<span class="toggle-icon">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="change-panel-body" id="changePanelBody" style="display: none;">
|
||||
<div class="change-list" id="changeList">
|
||||
<!-- 变更列表将动态插入 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更面板的样式
|
||||
*/
|
||||
export function getChangePanelStyles(): string {
|
||||
return `
|
||||
.change-panel {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.change-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.change-panel-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.change-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.change-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.change-count {
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.change-toggle-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.change-toggle-btn:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.change-panel-body {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.change-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.change-item-header:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.change-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.change-type-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.change-type-create {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.change-type-modify {
|
||||
background: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.change-type-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.change-file-path {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.change-item-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.change-action-btn {
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.change-action-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.change-item-diff {
|
||||
padding: 12px;
|
||||
background: var(--vscode-editor-background);
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
display: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.change-item-diff.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${getDiffStyles()}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变更面板的脚本
|
||||
*/
|
||||
export function getChangePanelScript(): string {
|
||||
return `
|
||||
// 切换变更面板展开/收起
|
||||
function toggleChangePanel() {
|
||||
const body = document.getElementById('changePanelBody');
|
||||
const toggleIcon = document.querySelector('.toggle-icon');
|
||||
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
toggleIcon.classList.add('expanded');
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
toggleIcon.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开文件 diff(在 VS Code 中打开)
|
||||
function openFileDiff(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'openFileDiff',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 采纳变更
|
||||
function acceptChange(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'acceptChange',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 拒绝变更
|
||||
function rejectChange(changeId) {
|
||||
vscode.postMessage({
|
||||
command: 'rejectChange',
|
||||
changeId: changeId
|
||||
});
|
||||
}
|
||||
|
||||
// 显示变更面板(从后端接收变更列表)
|
||||
window.showChangesPanel = function(changes) {
|
||||
const changePanel = document.getElementById('changePanel');
|
||||
const changeList = document.getElementById('changeList');
|
||||
const changeCount = document.getElementById('changeCount');
|
||||
|
||||
if (!changePanel || !changeList || !changeCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
changeCount.textContent = changes.length;
|
||||
|
||||
// 清空现有列表
|
||||
changeList.innerHTML = '';
|
||||
|
||||
// 渲染每个变更项
|
||||
changes.forEach(change => {
|
||||
const changeItem = createChangeItem(change);
|
||||
changeList.appendChild(changeItem);
|
||||
});
|
||||
|
||||
// 显示面板
|
||||
changePanel.style.display = 'block';
|
||||
};
|
||||
|
||||
// 创建单个变更项的 DOM 元素
|
||||
function createChangeItem(change) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'change-item';
|
||||
item.id = 'change-item-' + change.changeId;
|
||||
|
||||
const typeLabel = change.changeType === 'create' ? '新建' :
|
||||
change.changeType === 'modify' ? '修改' : '删除';
|
||||
|
||||
item.innerHTML = \`
|
||||
<div class="change-item-header" onclick="openFileDiff('\${change.changeId}')">
|
||||
<div class="change-item-info">
|
||||
<span class="change-type-badge change-type-\${change.changeType}">\${typeLabel}</span>
|
||||
<span class="change-file-path">\${change.filePath}</span>
|
||||
</div>
|
||||
<div class="change-item-actions">
|
||||
<button class="change-action-btn accept-btn" onclick="event.stopPropagation(); acceptChange('\${change.changeId}')">采纳</button>
|
||||
<button class="change-action-btn reject-btn" onclick="event.stopPropagation(); rejectChange('\${change.changeId}')">拒绝</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// 处理采纳变更的响应
|
||||
window.handleChangeAccepted = function(changeId, success, error) {
|
||||
if (success) {
|
||||
// 从列表中移除该变更项
|
||||
const item = document.getElementById('change-item-' + changeId);
|
||||
if (item) {
|
||||
item.remove();
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
updateChangeCount();
|
||||
} else {
|
||||
console.error('采纳变更失败:', error);
|
||||
alert('采纳变更失败: ' + (error || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拒绝变更的响应
|
||||
window.handleChangeRejected = function(changeId, success, error) {
|
||||
if (success) {
|
||||
// 从列表中移除该变更项
|
||||
const item = document.getElementById('change-item-' + changeId);
|
||||
if (item) {
|
||||
item.remove();
|
||||
}
|
||||
|
||||
// 更新变更数量
|
||||
updateChangeCount();
|
||||
} else {
|
||||
console.error('拒绝变更失败:', error);
|
||||
alert('拒绝变更失败: ' + (error || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新变更数量
|
||||
function updateChangeCount() {
|
||||
const changeList = document.getElementById('changeList');
|
||||
const changeCount = document.getElementById('changeCount');
|
||||
const changePanel = document.getElementById('changePanel');
|
||||
|
||||
if (changeList && changeCount) {
|
||||
const count = changeList.children.length;
|
||||
changeCount.textContent = count;
|
||||
|
||||
// 如果没有变更了,隐藏面板
|
||||
if (count === 0 && changePanel) {
|
||||
changePanel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -34,6 +34,11 @@ import {
|
||||
getExampleShowcaseStyles,
|
||||
getExampleShowcaseScript,
|
||||
} from "./exampleShowcase";
|
||||
import {
|
||||
getChangePanelContent,
|
||||
getChangePanelStyles,
|
||||
getChangePanelScript,
|
||||
} from "./changePanel";
|
||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
@ -49,6 +54,8 @@ export function getInputAreaContent(
|
||||
<div class="input-area centered" id="inputArea">
|
||||
<div class="input-group">
|
||||
<div class="input-wrapper">
|
||||
<!-- 代码变更面板 -->
|
||||
${getChangePanelContent()}
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="input-top-toolbar">
|
||||
${getContextButtonContent()}
|
||||
@ -94,6 +101,7 @@ export function getInputAreaStyles(): string {
|
||||
${getContextCompressStyles()}
|
||||
${getOptimizeButtonStyles()}
|
||||
${getExampleShowcaseStyles()}
|
||||
${getChangePanelStyles()}
|
||||
.input-area {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
padding-top: 15px;
|
||||
@ -300,6 +308,7 @@ export function getInputAreaScript(): string {
|
||||
${getContextButtonScript()}
|
||||
${getContextCompressScript()}
|
||||
${getOptimizeButtonScript()}
|
||||
${getChangePanelScript()}
|
||||
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
|
||||
@ -882,6 +882,27 @@ export function getWebviewContent(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'showChanges':
|
||||
// 显示代码变更
|
||||
if (typeof showChangesPanel === 'function') {
|
||||
showChangesPanel(message.changes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'changeAccepted':
|
||||
// 变更已采纳
|
||||
if (typeof handleChangeAccepted === 'function') {
|
||||
handleChangeAccepted(message.changeId, message.success, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'changeRejected':
|
||||
// 变更已拒绝
|
||||
if (typeof handleChangeRejected === 'function') {
|
||||
handleChangeRejected(message.changeId, message.success, message.error);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user