message-dashboard.component.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
  2. import { CommonModule, DatePipe } from '@angular/common';
  3. import { HttpClient } from '@angular/common/http';
  4. import { Router } from '@angular/router';
  5. import { Observable, forkJoin } from 'rxjs'; // Import Observable, forkJoin
  6. import { finalize, catchError, map } from 'rxjs/operators'; // Import operators
  7. import { of } from 'rxjs'; // Import 'of' to create an Observable from a value
  8. // NG-ZORRO 组件
  9. import { NzCardModule } from 'ng-zorro-antd/card';
  10. import { NzGridModule } from 'ng-zorro-antd/grid';
  11. import { NzStatisticModule } from 'ng-zorro-antd/statistic';
  12. import { NzTableModule } from 'ng-zorro-antd/table';
  13. import { NzTabsModule } from 'ng-zorro-antd/tabs';
  14. import { NzTagModule } from 'ng-zorro-antd/tag';
  15. import { NzSpinModule } from 'ng-zorro-antd/spin';
  16. import { NzMessageService } from 'ng-zorro-antd/message';
  17. import { NzDividerModule } from 'ng-zorro-antd/divider';
  18. import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
  19. import { FormsModule } from '@angular/forms';
  20. import { NzSelectModule } from 'ng-zorro-antd/select';
  21. import { NzButtonModule } from 'ng-zorro-antd/button';
  22. import { NzIconModule } from 'ng-zorro-antd/icon';
  23. import { NzEmptyModule } from 'ng-zorro-antd/empty';
  24. import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
  25. import { NzProgressModule } from 'ng-zorro-antd/progress';
  26. import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
  27. // Chart.js
  28. import { BaseChartDirective } from 'ng2-charts';
  29. import {
  30. Chart,
  31. ChartConfiguration,
  32. ChartOptions,
  33. registerables,
  34. } from 'chart.js';
  35. @Component({
  36. selector: 'app-message-dashboard',
  37. standalone: true,
  38. imports: [
  39. CommonModule,
  40. NzCardModule,
  41. NzGridModule,
  42. NzStatisticModule,
  43. NzTableModule,
  44. NzTabsModule,
  45. NzTagModule,
  46. NzSpinModule,
  47. NzDividerModule,
  48. NzDatePickerModule,
  49. FormsModule,
  50. NzSelectModule,
  51. NzButtonModule,
  52. NzIconModule,
  53. NzEmptyModule,
  54. NzToolTipModule,
  55. NzProgressModule,
  56. NzDescriptionsModule,
  57. DatePipe,
  58. BaseChartDirective,
  59. ],
  60. templateUrl: './message-dashboard.component.html',
  61. styleUrls: ['./message-dashboard.component.css'],
  62. })
  63. export class MessageDashboardComponent implements OnInit {
  64. isLoading = false;
  65. overallStats: any = null;
  66. strategyStats: any[] = [];
  67. templateStats: any[] = [];
  68. dailyTrends: any[] = [];
  69. avgDeliveryTime: any = null;
  70. activeTab: number = 0;
  71. // 日期范围
  72. dateRange: Date[] = [];
  73. // 组合图表配置
  74. public combinedChartData: ChartConfiguration<'bar'>['data'] = {
  75. labels: [],
  76. datasets: [
  77. {
  78. label: '总发送量',
  79. data: [],
  80. backgroundColor: '#1890ff',
  81. yAxisID: 'y',
  82. },
  83. {
  84. label: '成功发送',
  85. data: [],
  86. backgroundColor: '#52c41a',
  87. yAxisID: 'y',
  88. },
  89. {
  90. label: '已送达',
  91. data: [],
  92. backgroundColor: '#13c2c2',
  93. yAxisID: 'y',
  94. },
  95. {
  96. label: '已打开',
  97. data: [],
  98. backgroundColor: '#722ed1',
  99. yAxisID: 'y',
  100. },
  101. {
  102. label: '展示数',
  103. data: [],
  104. backgroundColor: '#faad14',
  105. yAxisID: 'y',
  106. },
  107. {
  108. label: '失败数',
  109. data: [],
  110. backgroundColor: '#f5222d',
  111. yAxisID: 'y',
  112. },
  113. ],
  114. };
  115. public rateChartData: ChartConfiguration<'line'>['data'] = {
  116. labels: [],
  117. datasets: [
  118. {
  119. label: '发送成功率',
  120. data: [],
  121. borderColor: '#1890ff',
  122. backgroundColor: 'rgba(24, 144, 255, 0.1)',
  123. yAxisID: 'y1',
  124. tension: 0.3,
  125. fill: true,
  126. },
  127. {
  128. label: '送达率',
  129. data: [],
  130. borderColor: '#52c41a',
  131. backgroundColor: 'rgba(82, 196, 26, 0.1)',
  132. yAxisID: 'y1',
  133. tension: 0.3,
  134. fill: true,
  135. },
  136. {
  137. label: '展示率',
  138. data: [],
  139. borderColor: '#faad14',
  140. backgroundColor: 'rgba(250, 173, 20, 0.1)',
  141. yAxisID: 'y1',
  142. tension: 0.3,
  143. fill: true,
  144. },
  145. {
  146. label: '点击率',
  147. data: [],
  148. borderColor: '#722ed1',
  149. backgroundColor: 'rgba(114, 46, 209, 0.1)',
  150. yAxisID: 'y1',
  151. tension: 0.3,
  152. fill: true,
  153. },
  154. ],
  155. };
  156. public combinedChartOptions: ChartOptions<'bar' | 'line'> = {
  157. responsive: true,
  158. maintainAspectRatio: false,
  159. plugins: {
  160. tooltip: {
  161. mode: 'index',
  162. intersect: false,
  163. },
  164. legend: {
  165. position: 'top',
  166. },
  167. },
  168. scales: {
  169. y: {
  170. type: 'linear',
  171. display: true,
  172. position: 'left',
  173. title: {
  174. display: true,
  175. text: '消息数量',
  176. },
  177. beginAtZero: true,
  178. },
  179. y1: {
  180. type: 'linear',
  181. display: true,
  182. position: 'right',
  183. title: {
  184. display: true,
  185. text: '百分比(%)',
  186. },
  187. min: 0,
  188. max: 100,
  189. grid: {
  190. drawOnChartArea: false,
  191. },
  192. },
  193. },
  194. };
  195. constructor(
  196. private http: HttpClient,
  197. private message: NzMessageService,
  198. private router: Router,
  199. private cd: ChangeDetectorRef // 注入 ChangeDetectorRef
  200. ) {
  201. Chart.register(...registerables);
  202. }
  203. ngOnInit(): void {
  204. this.loadAllStatistics();
  205. }
  206. loadAllStatistics(): void {
  207. this.isLoading = true;
  208. // Use forkJoin to run all requests in parallel and wait for all to complete
  209. forkJoin({
  210. overall: this.http
  211. .get(`/api/message-records/statistics/overall?t=${new Date()}`)
  212. .pipe(
  213. map((res: any) => res?.data || null),
  214. catchError((err) => {
  215. console.error('Failed to load overall statistics:', err);
  216. this.message.error('加载整体统计失败');
  217. return of(null); // Return a default value to prevent forkJoin from failing
  218. })
  219. ),
  220. strategies: this.http
  221. .get(`/api/message-records/statistics/by-strategy?t=${new Date()}`)
  222. .pipe(
  223. map((res: any) => res?.data || []),
  224. catchError((err) => {
  225. console.error('Failed to load strategy statistics:', err);
  226. this.message.error('加载策略统计失败');
  227. return of([]);
  228. })
  229. ),
  230. templates: this.http
  231. .get(`/api/message-records/statistics/by-template?t=${new Date()}`)
  232. .pipe(
  233. map((res: any) => res?.data || []),
  234. catchError((err) => {
  235. console.error('Failed to load template statistics:', err);
  236. this.message.error('加载模板统计失败');
  237. return of([]);
  238. })
  239. ),
  240. dailyTrends: this.http
  241. .get(`/api/message-records/statistics/daily-trends?t=${new Date()}`)
  242. .pipe(
  243. map((res: any) => res?.data || []),
  244. catchError((err) => {
  245. console.error('Failed to load daily trends:', err);
  246. this.message.error('加载每日趋势失败');
  247. return of([]);
  248. })
  249. ),
  250. // avgTime: this.http
  251. // .get('/api/message-records/statistics/avg-delivery-time')
  252. // .pipe(
  253. // map((res: any) => res?.data || null),
  254. // catchError((err) => {
  255. // console.error('Failed to load average delivery time:', err);
  256. // this.message.error('加载平均送达时间失败');
  257. // return of(null);
  258. // })
  259. // ),
  260. })
  261. .pipe(
  262. finalize(() => {
  263. this.isLoading = false;
  264. // 在所有数据都赋值完毕后,手动触发变更检测
  265. this.cd.detectChanges();
  266. })
  267. )
  268. .subscribe((results: any) => {
  269. // Assign data to component properties
  270. this.overallStats = results.overall;
  271. this.strategyStats = results.strategies;
  272. this.templateStats = results.templates;
  273. this.dailyTrends = results.dailyTrends;
  274. // this.avgDeliveryTime = results.avgTime;
  275. // Update charts based on the latest data
  276. this.updateChartData();
  277. });
  278. }
  279. private updateChartData(): void {
  280. // 更新组合图表数据
  281. this.combinedChartData = {
  282. labels: this.dailyTrends.map((t) => this.formatDate(t.date)),
  283. datasets: [
  284. {
  285. ...this.combinedChartData.datasets[0],
  286. data: this.dailyTrends.map((t) => t.totalRecords || 0),
  287. },
  288. {
  289. ...this.combinedChartData.datasets[1],
  290. data: this.dailyTrends.map((t) => t.sent || 0),
  291. },
  292. {
  293. ...this.combinedChartData.datasets[2],
  294. data: this.dailyTrends.map((t) => t.delivered || 0),
  295. },
  296. {
  297. ...this.combinedChartData.datasets[3],
  298. data: this.dailyTrends.map((t) => t.opened || 0),
  299. },
  300. {
  301. ...this.combinedChartData.datasets[4],
  302. data: this.dailyTrends.map((t) => t.displayCount || 0),
  303. },
  304. {
  305. ...this.combinedChartData.datasets[5],
  306. data: this.dailyTrends.map((t) => t.failed || 0),
  307. },
  308. ],
  309. };
  310. // 更新比率图表数据
  311. this.rateChartData = {
  312. labels: this.dailyTrends.map((t) => this.formatDate(t.date)),
  313. datasets: [
  314. {
  315. ...this.rateChartData.datasets[0],
  316. data: this.dailyTrends.map((t) => (t.sentSuccessRate || 0) * 100),
  317. },
  318. {
  319. ...this.rateChartData.datasets[1],
  320. data: this.dailyTrends.map((t) => (t.deliveredRate || 0) * 100),
  321. },
  322. {
  323. ...this.rateChartData.datasets[2],
  324. data: this.dailyTrends.map((t) => (t.displayRate || 0) * 100),
  325. },
  326. {
  327. ...this.rateChartData.datasets[3],
  328. data: this.dailyTrends.map((t) => (t.clickThroughRate || 0) * 100),
  329. },
  330. ],
  331. };
  332. }
  333. refreshData(): void {
  334. this.loadAllStatistics();
  335. }
  336. navigateToStrategy(strategyName: string): void {
  337. this.router.navigate(['/message-strategy'], {
  338. queryParams: { strategyName: strategyName },
  339. });
  340. }
  341. navigateToTemplate(templateName: string): void {
  342. this.router.navigate(['/message-template'], {
  343. queryParams: { templateName: templateName },
  344. });
  345. }
  346. formatPercentage(value: number): string {
  347. return (value * 100).toFixed(2) + '%';
  348. }
  349. formatPercentageToNumber(value: number): number {
  350. return Number((value * 100).toFixed(2));
  351. }
  352. formatSeconds(seconds: number): string {
  353. if (seconds < 60) {
  354. return seconds.toFixed(2) + '秒';
  355. } else {
  356. return (seconds / 60).toFixed(2) + '分钟';
  357. }
  358. }
  359. preciseRound(num: number, decimalPlaces: number): number {
  360. if (decimalPlaces === 0) return Math.round(num);
  361. const multiplier = Math.pow(10, decimalPlaces);
  362. return Number(Math.round(num * multiplier) / multiplier);
  363. }
  364. private formatDate(date: string | null): string {
  365. if (!date) return '未知日期';
  366. return new Date(date).toLocaleDateString();
  367. }
  368. }