operations-learning-reports.component.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { CommonModule } from '@angular/common';
  2. import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild, inject, signal } from '@angular/core';
  3. import { FormsModule } from '@angular/forms';
  4. import { ActivatedRoute, RouterLink } from '@angular/router';
  5. import html2pdf from 'html2pdf.js';
  6. import { ChevronLeft, ChevronRight, Download, Eye, FileChartColumn, LoaderCircle, LucideAngularModule, RefreshCw, Search, X } from 'lucide-angular';
  7. import { LearningReportDetail, LearningReportListItem, LearningReportPage } from '../admin.models';
  8. import { OperationsService } from '../operations.service';
  9. const localMonth = (): string => {
  10. const now = new Date();
  11. return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  12. };
  13. @Component({
  14. selector: 'app-operations-learning-reports',
  15. standalone: true,
  16. imports: [CommonModule, FormsModule, RouterLink, LucideAngularModule],
  17. templateUrl: './operations-learning-reports.component.html',
  18. styleUrls: ['./operations-professional.scss', './operations-learning-reports.component.scss'],
  19. changeDetection: ChangeDetectionStrategy.OnPush,
  20. })
  21. export class OperationsLearningReportsComponent implements OnInit {
  22. @ViewChild('pdfContent') pdfContent?: ElementRef<HTMLElement>;
  23. readonly data = signal<LearningReportPage | null>(null);
  24. readonly detail = signal<LearningReportDetail | null>(null);
  25. readonly loading = signal(true);
  26. readonly refreshing = signal(false);
  27. readonly detailLoading = signal(false);
  28. readonly exportingId = signal('');
  29. readonly error = signal('');
  30. readonly message = signal('');
  31. readonly page = signal(1);
  32. readonly pageSize = 20;
  33. readonly month = signal(localMonth());
  34. readonly dateFrom = signal('');
  35. readonly dateTo = signal('');
  36. readonly search = signal('');
  37. readonly coachSearch = signal('');
  38. readonly courseSearch = signal('');
  39. readonly status = signal('');
  40. readonly studentObjectId = signal('');
  41. readonly icons = { ChevronLeft, ChevronRight, Download, Eye, FileChartColumn, LoaderCircle, RefreshCw, Search, X };
  42. private readonly operations = inject(OperationsService);
  43. private readonly route = inject(ActivatedRoute);
  44. ngOnInit(): void {
  45. this.studentObjectId.set(this.route.snapshot.queryParamMap.get('studentObjectId') || '');
  46. const requestedReport = this.route.snapshot.queryParamMap.get('report') || '';
  47. void this.load(false).then(() => requestedReport ? this.open(requestedReport) : undefined);
  48. }
  49. private filters(): Record<string, unknown> {
  50. return {
  51. month: this.month(), dateFrom: this.dateFrom(), dateTo: this.dateTo(), status: this.status(),
  52. coachSearch: this.coachSearch(), courseSearch: this.courseSearch(), studentObjectId: this.studentObjectId(),
  53. };
  54. }
  55. async load(refresh = false): Promise<void> {
  56. if (refresh) this.refreshing.set(true); else this.loading.set(true);
  57. this.error.set(''); this.message.set('');
  58. try {
  59. const page = await this.operations.learningReports(this.page(), this.pageSize, this.search(), this.filters(), refresh);
  60. this.data.set(page);
  61. if (refresh) this.message.set(page.sourceStatus.state === 'synced' ? '学习记录已同步到最新水位' : page.sourceStatus.message);
  62. } catch (error) {
  63. this.error.set(error instanceof Error ? error.message : '学习报表加载失败');
  64. } finally {
  65. this.loading.set(false); this.refreshing.set(false);
  66. }
  67. }
  68. async applyFilters(): Promise<void> { this.page.set(1); await this.load(false); }
  69. async changePage(next: number): Promise<void> {
  70. const totalPages = Math.max(1, Math.ceil((this.data()?.total || 0) / this.pageSize));
  71. if (next < 1 || next > totalPages || next === this.page()) return;
  72. this.page.set(next); await this.load(false);
  73. }
  74. async open(targetId: string): Promise<void> {
  75. this.detailLoading.set(true); this.error.set('');
  76. try { this.detail.set(await this.operations.learningReportDetail(targetId)); }
  77. catch (error) { this.error.set(error instanceof Error ? error.message : '报表详情加载失败'); }
  78. finally { this.detailLoading.set(false); }
  79. }
  80. close(): void { this.detail.set(null); }
  81. async download(row: LearningReportListItem | LearningReportDetail): Promise<void> {
  82. if (!row.canExport || this.exportingId()) return;
  83. this.exportingId.set(row.targetId); this.error.set('');
  84. try {
  85. const report = await this.operations.learningReportExport(row.targetId);
  86. this.detail.set(report);
  87. await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
  88. await document.fonts?.ready;
  89. const element = this.pdfContent?.nativeElement;
  90. if (!element) throw new Error('报表打印区域尚未准备完成');
  91. const safeName = `${report.studentName}-${report.studyDate}-${report.generalId}`.replace(/[^a-zA-Z0-9一-龥_-]/g, '_');
  92. let hasBlankTrailingPage = false;
  93. const worker = html2pdf().set({
  94. margin: [9, 9, 9, 9], filename: `小树陪练-单词学习报表-${safeName}.pdf`,
  95. image: { type: 'jpeg', quality: 1 },
  96. html2canvas: { scale: 1.5, logging: false, useCORS: true, backgroundColor: '#ffffff' },
  97. jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
  98. pagebreak: { mode: ['css', 'legacy'], avoid: ['.report-summary article', '.word-row', '.review-item', '.comment-card'] },
  99. }).from(element).toCanvas().get('canvas', (canvas) => {
  100. const pageHeight = Math.floor(canvas.width * (279 / 192));
  101. const trailingSlice = canvas.height % pageHeight;
  102. hasBlankTrailingPage = trailingSlice > 0 && trailingSlice < 32;
  103. }).toPdf();
  104. await worker.get('pdf', (pdf) => {
  105. const pageCount = pdf.internal.getNumberOfPages();
  106. if (hasBlankTrailingPage && pageCount > 1) pdf.deletePage(pageCount);
  107. }).save();
  108. this.message.set('学习报表 PDF 已生成');
  109. } catch (error) {
  110. this.error.set(error instanceof Error ? error.message : 'PDF 生成失败');
  111. } finally { this.exportingId.set(''); }
  112. }
  113. totalPages(): number { return Math.max(1, Math.ceil((this.data()?.total || 0) / this.pageSize)); }
  114. trackReport(_index: number, row: LearningReportListItem): string { return row.targetId; }
  115. }