diff --git a/apps/showcase_ui/src/app/components/automation-panel/automation-panel.component.ts b/apps/showcase_ui/src/app/components/automation-panel/automation-panel.component.ts
index bb3b070..b02c0dc 100644
--- a/apps/showcase_ui/src/app/components/automation-panel/automation-panel.component.ts
+++ b/apps/showcase_ui/src/app/components/automation-panel/automation-panel.component.ts
@@ -1,11 +1,11 @@
import { Component, DestroyRef, computed, effect, inject, output, signal } from '@angular/core';
+import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { AgentService } from '../../services/agent.service';
import { SystemService } from '../../services/system.service';
-import { renderMarkdownToHtml } from '../../utils/markdown-parser.util';
import { TestResultBadgeComponent } from '../test-result-badge/test-result-badge.component';
import { TestResultCardComponent } from '../test-result-badge/test-result-card.component';
import { CaseReviewResponse } from '../../core/models/session.model';
@@ -22,7 +22,7 @@ interface ArchiveProgress {
}
@Component({
- selector: 'app-automation-panel', standalone: true, imports: [CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent],
+ selector: 'app-automation-panel', standalone: true, imports: [ReportMarkdownComponent, CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent],
templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss'
})
export class AutomationPanelComponent {
@@ -61,7 +61,6 @@ export class AutomationPanelComponent {
}
formats() { return [ ...(this.word ? ['docx'] : []), ...(this.excel ? ['xlsx'] : []) ]; }
label(s: string) { return ({draft:'待开始',running:'执行中',paused:'已暂停',task_paused:'等待指导',resuming:'等待设备恢复',completed:'执行完成',failed:'失败',cancelled:'已取消',pending:'待执行',queued:'排队中',interrupted:'中断',skipped:'跳过',finalizing:'正在生成报告',deleting:'删除中,可重试清理'} as Record
)[s] || s; }
- renderReport(content: string) { return renderMarkdownToHtml(content || ''); }
reviewSaved(review: CaseReviewResponse) { this.agent.applyCaseReview(review); if (this.report()?.id === review.id) this.report.update(value => ({...value, ...review})); }
openBatchTask(id: string) { this.agent.selectSession(id, true); }
showBatchQueue(id: string) { this.agent.batchFilter.set(id); this.agent.activeTab.set('tasks'); this.queueRequested.emit(); }
diff --git a/apps/showcase_ui/src/app/components/report-markdown/report-markdown.component.spec.ts b/apps/showcase_ui/src/app/components/report-markdown/report-markdown.component.spec.ts
new file mode 100644
index 0000000..344e940
--- /dev/null
+++ b/apps/showcase_ui/src/app/components/report-markdown/report-markdown.component.spec.ts
@@ -0,0 +1,36 @@
+import { TestBed } from '@angular/core/testing';
+import { ReportMarkdownComponent } from './report-markdown.component';
+
+describe('报告 Markdown 展示', () => {
+ it('renders report headings, emphasis, lists, tables and code in both preview and full view', () => {
+ const fixture = TestBed.createComponent(ReportMarkdownComponent);
+ fixture.componentRef.setInput('content', '### 双端布局报告\n- **手机**:`/zh`\n\n| 区域 | 结论 |\n| --- | --- |\n| 页底 | 已确认 |\n\n```text\n<原始证据>\n```');
+ fixture.componentRef.setInput('compact', true);
+ fixture.detectChanges();
+ const view: HTMLElement = fixture.nativeElement;
+ expect(view.querySelector('h3')?.textContent).toBe('双端布局报告');
+ expect(view.querySelector('li strong')?.textContent).toBe('手机');
+ expect(view.querySelector('li code')?.textContent).toBe('/zh');
+ expect(view.querySelector('td')?.textContent).toBe('页底');
+ expect(view.querySelector('pre code')?.textContent).toBe('<原始证据>');
+ expect(view.querySelector('.compact')).not.toBeNull();
+ fixture.componentRef.setInput('compact', false);
+ fixture.detectChanges();
+ expect(view.querySelector('.compact')).toBeNull();
+ expect(view.querySelectorAll('td').length).toBe(2);
+ fixture.destroy();
+ });
+
+ it('keeps HTML in model output inert and updates when the selected report changes', () => {
+ const fixture = TestBed.createComponent(ReportMarkdownComponent);
+ fixture.componentRef.setInput('content', '
\n');
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelector('img,script')).toBeNull();
+ expect(fixture.nativeElement.textContent).toContain('![]()
`,
+ styles: [`
+ :host { display: block; min-width: 0; max-width: 100%; }
+ .report-prose { color: inherit; font-size: 13px; font-weight: 400; line-height: 1.7; white-space: normal; overflow-wrap: anywhere; word-break: normal; }
+ .compact { max-height: 170px; overflow: hidden; }
+ :host ::ng-deep .report-prose {
+ > div, p { margin: 4px 0; }
+ > :first-child { margin-top: 0; }
+ > :last-child { margin-bottom: 0; }
+ h1,h2,h3,h4,h5,h6 { margin: 16px 0 6px; font-weight: 600; line-height: 1.5; color: #0f172a; }
+ h1 { font-size: 18px; } h2 { font-size: 16px; } h3,h4,h5,h6 { font-size: 14px; }
+ strong { font-weight: 600; }
+ ul,ol { margin: 6px 0; padding-left: 24px; }
+ li { margin: 4px 0; }
+ blockquote { margin: 10px 0; padding: 6px 12px; border-left: 3px solid #93c5fd; background: #f8fafc; }
+ code { font-family: Consolas, monospace; font-size: .92em; border-radius: 4px; padding: 1px 4px; background: #f1f5f9; }
+ pre { max-width: 100%; overflow: auto; padding: 10px 12px; border-radius: 6px; background: #f1f5f9; white-space: pre; }
+ pre code { padding: 0; background: none; }
+ .md-table-scroll { max-width: 100%; overflow-x: auto; }
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
+ th,td { border: 1px solid #cbd5e1; padding: 7px 9px; text-align: left; vertical-align: top; }
+ th { background: #eff6ff; font-weight: 600; }
+ .md-para-gap { height: 4px; margin: 0; }
+ hr { border: 0; border-top: 1px solid #e2e8f0; margin: 12px 0; }
+ }
+ `]
+})
+export class ReportMarkdownComponent {
+ content = input('');
+ compact = input(false);
+ html = computed(() => renderMarkdownToHtml(this.content()));
+}
diff --git a/apps/showcase_ui/src/app/components/test-result-badge/test-result-card.component.ts b/apps/showcase_ui/src/app/components/test-result-badge/test-result-card.component.ts
index a4e8c01..e6e0e57 100644
--- a/apps/showcase_ui/src/app/components/test-result-badge/test-result-card.component.ts
+++ b/apps/showcase_ui/src/app/components/test-result-badge/test-result-card.component.ts
@@ -3,18 +3,19 @@ import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { CaseReviewResponse, CaseTestResult } from '../../core/models/session.model';
+import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
import { TestResultBadgeComponent } from './test-result-badge.component';
@Component({
selector: 'app-test-result-card',
standalone: true,
- imports: [TestResultBadgeComponent, DatePipe, FormsModule],
+ imports: [ReportMarkdownComponent, TestResultBadgeComponent, DatePipe, FormsModule],
template: `