feat: Plan卡片支持Markdown渲染和智能步骤解析

- 添加renderPlanMarkdown函数,支持标题、列表、表格、代码块等
- 添加renderPlanSteps函数,智能解析JSON格式步骤对象
- 步骤显示模块名、描述、输入输出、逻辑等详细信息
- 添加plan-summary和step-details样式
This commit is contained in:
XiaoFeng
2026-01-09 17:02:00 +08:00
parent 178f3a7498
commit 5546791549

View File

@ -4,6 +4,7 @@
* 功能说明: * 功能说明:
* - 显示执行计划的卡片界面 * - 显示执行计划的卡片界面
* - 包含计划标题、摘要和步骤列表 * - 包含计划标题、摘要和步骤列表
* - 摘要支持 Markdown 格式渲染
* - 提供确认执行、修改计划、取消等操作按钮 * - 提供确认执行、修改计划、取消等操作按钮
*/ */
@ -43,11 +44,62 @@ export function getPlanCardStyles(): string {
padding: 16px; padding: 16px;
} }
.plan-summary { .plan-summary {
color: var(--vscode-descriptionForeground); color: var(--vscode-foreground);
margin-bottom: 12px; margin-bottom: 12px;
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.6;
} }
/* Markdown 渲染样式 */
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
margin: 16px 0 8px 0;
font-weight: 600;
color: var(--vscode-foreground);
}
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
.plan-summary h2 { font-size: 16px; }
.plan-summary h3 { font-size: 14px; }
.plan-summary h4 { font-size: 13px; }
.plan-summary p { margin: 8px 0; }
.plan-summary ul, .plan-summary ol {
margin: 8px 0;
padding-left: 24px;
}
.plan-summary li { margin: 4px 0; }
.plan-summary code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.plan-summary pre {
background: var(--vscode-textCodeBlock-background);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.plan-summary pre code {
background: none;
padding: 0;
}
.plan-summary table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 12px;
}
.plan-summary th, .plan-summary td {
border: 1px solid var(--vscode-input-border);
padding: 6px 10px;
text-align: left;
}
.plan-summary th {
background: var(--vscode-sideBar-background);
font-weight: 600;
}
.plan-summary strong { font-weight: 600; }
.plan-summary em { font-style: italic; }
.plan-steps { .plan-steps {
font-size: 13px; font-size: 13px;
} }
@ -58,6 +110,15 @@ export function getPlanCardStyles(): string {
border-radius: 4px; border-radius: 4px;
line-height: 1.5; line-height: 1.5;
} }
.plan-step strong {
color: var(--vscode-textLink-foreground);
}
.step-details {
margin-top: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
.plan-step:last-child { .plan-step:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -158,6 +219,117 @@ export function getPlanCardStyles(): string {
*/ */
export function getPlanCardScript(): string { export function getPlanCardScript(): string {
return ` return `
// 简单的 Markdown 渲染函数
function renderPlanMarkdown(text) {
if (!text) return '';
let html = text;
// 转义 HTML 特殊字符(保留换行)
html = html.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 代码块 (\`\`\`code\`\`\`)
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
// 行内代码 (\`code\`)
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
// 表格处理
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
const headers = header.split('|').map(h => h.trim()).filter(h => h);
const rows = body.trim().split('\\n').map(row =>
row.split('|').map(cell => cell.trim()).filter(cell => cell)
);
let table = '<table><thead><tr>';
headers.forEach(h => table += '<th>' + h + '</th>');
table += '</tr></thead><tbody>';
rows.forEach(row => {
table += '<tr>';
row.forEach(cell => table += '<td>' + cell + '</td>');
table += '</tr>';
});
table += '</tbody></table>';
return table;
});
// 标题
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 粗体和斜体
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
// 无序列表
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
// 有序列表
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
// 段落(连续的非空行)
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
// 清理多余的空行
html = html.replace(/<p><\\/p>/g, '');
html = html.replace(/\\n{2,}/g, '\\n');
return html;
}
// 解析并渲染步骤列表
function renderPlanSteps(steps) {
if (!steps || steps.length === 0) return '';
// 尝试解析 JSON 格式的步骤
let parsedSteps = steps;
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
if (steps.length === 1 && typeof steps[0] === 'string') {
const str = steps[0].trim();
if (str.startsWith('[') && str.endsWith(']')) {
try {
parsedSteps = JSON.parse(str);
} catch (e) {
// 解析失败,保持原样
}
}
}
return parsedSteps.map((step, i) => {
// 如果是对象,格式化显示
if (typeof step === 'object' && step !== null) {
const name = step.name || step.id || ('步骤 ' + (i + 1));
const desc = step.description || '';
const inputs = step.inputs || '';
const outputs = step.outputs || '';
const logic = step.logic || '';
let content = '<strong>' + name + '</strong>';
if (desc) content += '' + desc;
let details = [];
if (inputs) details.push('输入: ' + inputs);
if (outputs) details.push('输出: ' + outputs);
if (logic) details.push('逻辑: ' + logic);
if (details.length > 0) {
content += '<div class="step-details">' + details.join(' | ') + '</div>';
}
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
}
// 普通字符串
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
}).join('');
}
// 渲染计划卡片(在 updateSegmentsRealtime 中使用) // 渲染计划卡片(在 updateSegmentsRealtime 中使用)
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) { function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
@ -170,9 +342,8 @@ export function getPlanCardScript(): string {
segmentDiv.classList.add('answered'); segmentDiv.classList.add('answered');
} }
const stepsHtml = (segment.planSteps || []).map((step, i) => // 解析并渲染步骤
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\` const stepsHtml = renderPlanSteps(segment.planSteps || []);
).join('');
// 选项按钮 // 选项按钮
const options = ['确认执行', '修改计划', '取消']; const options = ['确认执行', '修改计划', '取消'];
@ -181,6 +352,9 @@ export function getPlanCardScript(): string {
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`; return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join(''); }).join('');
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
<div class="plan-header"> <div class="plan-header">
@ -188,7 +362,7 @@ export function getPlanCardScript(): string {
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div> <div class="plan-summary">\${summaryHtml}</div>
<div class="plan-steps">\${stepsHtml}</div> <div class="plan-steps">\${stepsHtml}</div>
</div> </div>
<div class="plan-actions"> <div class="plan-actions">
@ -250,9 +424,10 @@ export function getPlanCardScript(): string {
// 渲染计划卡片(在 renderSegments 中使用) // 渲染计划卡片(在 renderSegments 中使用)
function renderPlanCardStatic(segment, segmentDiv) { function renderPlanCardStatic(segment, segmentDiv) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) => const stepsHtml = renderPlanSteps(segment.planSteps || []);
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join(''); // 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
@ -261,7 +436,7 @@ export function getPlanCardScript(): string {
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div> <div class="plan-summary">\${summaryHtml}</div>
<div class="plan-steps">\${stepsHtml}</div> <div class="plan-steps">\${stepsHtml}</div>
</div> </div>
<div class="plan-actions"> <div class="plan-actions">