analysis-run-list.component.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { CommonModule } from '@angular/common';
  2. import {
  3. ChangeDetectionStrategy,
  4. ChangeDetectorRef,
  5. Component,
  6. DestroyRef,
  7. EventEmitter,
  8. Input,
  9. OnChanges,
  10. OnInit,
  11. Output,
  12. SimpleChanges,
  13. inject,
  14. } from '@angular/core';
  15. import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
  16. import { FormsModule } from '@angular/forms';
  17. import {
  18. Activity,
  19. AlertCircle,
  20. BrainCircuit,
  21. CheckCircle2,
  22. ChevronRight,
  23. Clock3,
  24. Eye,
  25. LoaderCircle,
  26. RefreshCw,
  27. Search,
  28. LucideAngularModule,
  29. } from 'lucide-angular';
  30. import { RUNTIME_CONFIG } from '../../../app/core/config/runtime-config';
  31. import {
  32. SaasAnalysisRun,
  33. SaasPlatformService,
  34. } from '../../../app/core/services/saas-platform.service';
  35. import { AnalysisRunDetailComponent } from './analysis-run-detail.component';
  36. import {
  37. ANALYSIS_RUN_STATUS_OPTIONS,
  38. AnalysisRunStatusFilter,
  39. analysisModeLabel,
  40. analysisRunStatusLabel,
  41. analysisRunStatusTone,
  42. analysisRunTypeLabel,
  43. analysisTargetKindLabel,
  44. analysisTargetLabel,
  45. formatAnalysisDate,
  46. readAnalysisRunInput,
  47. readAnalysisRunResult,
  48. } from './analysis-run.models';
  49. @Component({
  50. selector: 'app-analysis-run-list',
  51. standalone: true,
  52. imports: [CommonModule, FormsModule, LucideAngularModule, AnalysisRunDetailComponent],
  53. templateUrl: './analysis-run-list.component.html',
  54. styleUrl: './analysis-run-list.component.scss',
  55. changeDetection: ChangeDetectionStrategy.OnPush,
  56. })
  57. export class AnalysisRunListComponent implements OnInit, OnChanges {
  58. private readonly saasService = inject(SaasPlatformService);
  59. private readonly cdr = inject(ChangeDetectorRef);
  60. private readonly destroyRef = inject(DestroyRef);
  61. private initialized = false;
  62. private requestVersion = 0;
  63. @Input() workspaceId = RUNTIME_CONFIG.domesticWorkspaceId;
  64. @Output() readonly runSelected = new EventEmitter<SaasAnalysisRun>();
  65. readonly icons = {
  66. activity: Activity,
  67. alert: AlertCircle,
  68. brain: BrainCircuit,
  69. check: CheckCircle2,
  70. chevron: ChevronRight,
  71. clock: Clock3,
  72. eye: Eye,
  73. loader: LoaderCircle,
  74. refresh: RefreshCw,
  75. search: Search,
  76. };
  77. readonly statusOptions = ANALYSIS_RUN_STATUS_OPTIONS;
  78. runs: SaasAnalysisRun[] = [];
  79. statusFilter: AnalysisRunStatusFilter = '';
  80. searchTerm = '';
  81. loading = true;
  82. loadingMore = false;
  83. error = '';
  84. nextCursor: string | null = null;
  85. selectedRun: SaasAnalysisRun | null = null;
  86. loadedAt: string | null = null;
  87. ngOnInit(): void {
  88. this.initialized = true;
  89. this.loadRuns(true);
  90. }
  91. ngOnChanges(changes: SimpleChanges): void {
  92. if (this.initialized && changes['workspaceId']) this.loadRuns(true);
  93. }
  94. get visibleRuns(): SaasAnalysisRun[] {
  95. const query = this.searchTerm.trim().toLocaleLowerCase('zh-CN');
  96. if (!query) return this.runs;
  97. return this.runs.filter((run) => [
  98. run.id,
  99. run.targetKey,
  100. run.requestedBy,
  101. run.errorSummary ?? '',
  102. analysisRunTypeLabel(run.analysisType),
  103. analysisTargetKindLabel(run.targetKind),
  104. ].some((value) => value.toLocaleLowerCase('zh-CN').includes(query)));
  105. }
  106. get processingCount(): number {
  107. return this.runs.filter((run) => run.status === 'pending' || run.status === 'processing').length;
  108. }
  109. get completedCount(): number {
  110. return this.runs.filter((run) => run.status === 'completed' || run.status === 'partial').length;
  111. }
  112. get failedCount(): number {
  113. return this.runs.filter((run) => run.status === 'failed').length;
  114. }
  115. applyStatusFilter(): void {
  116. this.loadRuns(true);
  117. }
  118. refresh(): void {
  119. this.loadRuns(true);
  120. }
  121. loadMore(): void {
  122. if (!this.nextCursor || this.loadingMore) return;
  123. this.loadRuns(false);
  124. }
  125. selectRun(run: SaasAnalysisRun): void {
  126. this.selectedRun = run;
  127. this.runSelected.emit(run);
  128. }
  129. closeDetail(): void {
  130. this.selectedRun = null;
  131. }
  132. trackRun(_: number, run: SaasAnalysisRun): string {
  133. return run.id;
  134. }
  135. statusLabel(run: SaasAnalysisRun): string {
  136. return analysisRunStatusLabel(run.status);
  137. }
  138. statusTone(run: SaasAnalysisRun): string {
  139. return analysisRunStatusTone(run.status);
  140. }
  141. typeLabel(run: SaasAnalysisRun): string {
  142. return analysisRunTypeLabel(run.analysisType);
  143. }
  144. targetKindLabel(run: SaasAnalysisRun): string {
  145. return analysisTargetKindLabel(run.targetKind);
  146. }
  147. targetLabel(run: SaasAnalysisRun): string {
  148. return analysisTargetLabel(run);
  149. }
  150. sampleCount(run: SaasAnalysisRun): string {
  151. const input = readAnalysisRunInput(run);
  152. const result = readAnalysisRunResult(run);
  153. const value = input.sampleCount ?? result?.sampleCount ?? null;
  154. return value === null ? '未记录' : value.toLocaleString('zh-CN');
  155. }
  156. modeLabel(run: SaasAnalysisRun): string {
  157. return analysisModeLabel(readAnalysisRunResult(run)?.mode ?? null);
  158. }
  159. formatDate(value: string | null): string {
  160. return formatAnalysisDate(value);
  161. }
  162. private loadRuns(reset: boolean): void {
  163. const requestVersion = ++this.requestVersion;
  164. if (!this.workspaceId.trim()) {
  165. this.runs = [];
  166. this.loading = false;
  167. this.loadingMore = false;
  168. this.error = '工作空间 ID 未记录,运行列表未请求。';
  169. this.cdr.markForCheck();
  170. return;
  171. }
  172. const cursor = reset ? '' : this.nextCursor ?? '';
  173. if (reset) {
  174. this.loading = true;
  175. this.error = '';
  176. this.nextCursor = null;
  177. } else {
  178. this.loadingMore = true;
  179. }
  180. this.saasService.analyses(this.workspaceId, this.statusFilter, cursor, 50)
  181. .pipe(takeUntilDestroyed(this.destroyRef))
  182. .subscribe({
  183. next: (page) => {
  184. if (requestVersion !== this.requestVersion) return;
  185. this.runs = reset ? page.items : this.mergeRuns(this.runs, page.items);
  186. this.nextCursor = page.nextCursor;
  187. this.loadedAt = new Date().toISOString();
  188. this.loading = false;
  189. this.loadingMore = false;
  190. this.error = '';
  191. if (this.selectedRun) {
  192. this.selectedRun = this.runs.find((run) => run.id === this.selectedRun?.id) ?? this.selectedRun;
  193. }
  194. this.cdr.markForCheck();
  195. },
  196. error: (error: unknown) => {
  197. if (requestVersion !== this.requestVersion) return;
  198. this.loading = false;
  199. this.loadingMore = false;
  200. this.error = this.errorMessage(error);
  201. this.cdr.markForCheck();
  202. },
  203. });
  204. }
  205. private mergeRuns(current: SaasAnalysisRun[], incoming: SaasAnalysisRun[]): SaasAnalysisRun[] {
  206. const byId = new Map(current.map((run) => [run.id, run]));
  207. incoming.forEach((run) => byId.set(run.id, run));
  208. return [...byId.values()];
  209. }
  210. private errorMessage(error: unknown): string {
  211. if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
  212. return `分析运行加载失败:${error.message}`;
  213. }
  214. return '分析运行加载失败,请检查数据服务后重试。';
  215. }
  216. }