| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
- import { CommonModule, DatePipe } from '@angular/common';
- import { HttpClient } from '@angular/common/http';
- import { Router } from '@angular/router';
- import { Observable, forkJoin } from 'rxjs'; // Import Observable, forkJoin
- import { finalize, catchError, map } from 'rxjs/operators'; // Import operators
- import { of } from 'rxjs'; // Import 'of' to create an Observable from a value
- // NG-ZORRO 组件
- import { NzCardModule } from 'ng-zorro-antd/card';
- import { NzGridModule } from 'ng-zorro-antd/grid';
- import { NzStatisticModule } from 'ng-zorro-antd/statistic';
- import { NzTableModule } from 'ng-zorro-antd/table';
- import { NzTabsModule } from 'ng-zorro-antd/tabs';
- import { NzTagModule } from 'ng-zorro-antd/tag';
- import { NzSpinModule } from 'ng-zorro-antd/spin';
- import { NzMessageService } from 'ng-zorro-antd/message';
- import { NzDividerModule } from 'ng-zorro-antd/divider';
- import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
- import { FormsModule } from '@angular/forms';
- import { NzSelectModule } from 'ng-zorro-antd/select';
- import { NzButtonModule } from 'ng-zorro-antd/button';
- import { NzIconModule } from 'ng-zorro-antd/icon';
- import { NzEmptyModule } from 'ng-zorro-antd/empty';
- import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
- import { NzProgressModule } from 'ng-zorro-antd/progress';
- import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
- // Chart.js
- import { BaseChartDirective } from 'ng2-charts';
- import {
- Chart,
- ChartConfiguration,
- ChartOptions,
- registerables,
- } from 'chart.js';
- @Component({
- selector: 'app-message-dashboard',
- standalone: true,
- imports: [
- CommonModule,
- NzCardModule,
- NzGridModule,
- NzStatisticModule,
- NzTableModule,
- NzTabsModule,
- NzTagModule,
- NzSpinModule,
- NzDividerModule,
- NzDatePickerModule,
- FormsModule,
- NzSelectModule,
- NzButtonModule,
- NzIconModule,
- NzEmptyModule,
- NzToolTipModule,
- NzProgressModule,
- NzDescriptionsModule,
- DatePipe,
- BaseChartDirective,
- ],
- templateUrl: './message-dashboard.component.html',
- styleUrls: ['./message-dashboard.component.css'],
- })
- export class MessageDashboardComponent implements OnInit {
- isLoading = false;
- overallStats: any = null;
- strategyStats: any[] = [];
- templateStats: any[] = [];
- dailyTrends: any[] = [];
- avgDeliveryTime: any = null;
- activeTab: number = 0;
- // 日期范围
- dateRange: Date[] = [];
- // 组合图表配置
- public combinedChartData: ChartConfiguration<'bar'>['data'] = {
- labels: [],
- datasets: [
- {
- label: '总发送量',
- data: [],
- backgroundColor: '#1890ff',
- yAxisID: 'y',
- },
- {
- label: '成功发送',
- data: [],
- backgroundColor: '#52c41a',
- yAxisID: 'y',
- },
- {
- label: '已送达',
- data: [],
- backgroundColor: '#13c2c2',
- yAxisID: 'y',
- },
- {
- label: '已打开',
- data: [],
- backgroundColor: '#722ed1',
- yAxisID: 'y',
- },
- {
- label: '展示数',
- data: [],
- backgroundColor: '#faad14',
- yAxisID: 'y',
- },
- {
- label: '失败数',
- data: [],
- backgroundColor: '#f5222d',
- yAxisID: 'y',
- },
- ],
- };
- public rateChartData: ChartConfiguration<'line'>['data'] = {
- labels: [],
- datasets: [
- {
- label: '发送成功率',
- data: [],
- borderColor: '#1890ff',
- backgroundColor: 'rgba(24, 144, 255, 0.1)',
- yAxisID: 'y1',
- tension: 0.3,
- fill: true,
- },
- {
- label: '送达率',
- data: [],
- borderColor: '#52c41a',
- backgroundColor: 'rgba(82, 196, 26, 0.1)',
- yAxisID: 'y1',
- tension: 0.3,
- fill: true,
- },
- {
- label: '展示率',
- data: [],
- borderColor: '#faad14',
- backgroundColor: 'rgba(250, 173, 20, 0.1)',
- yAxisID: 'y1',
- tension: 0.3,
- fill: true,
- },
- {
- label: '点击率',
- data: [],
- borderColor: '#722ed1',
- backgroundColor: 'rgba(114, 46, 209, 0.1)',
- yAxisID: 'y1',
- tension: 0.3,
- fill: true,
- },
- ],
- };
- public combinedChartOptions: ChartOptions<'bar' | 'line'> = {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- tooltip: {
- mode: 'index',
- intersect: false,
- },
- legend: {
- position: 'top',
- },
- },
- scales: {
- y: {
- type: 'linear',
- display: true,
- position: 'left',
- title: {
- display: true,
- text: '消息数量',
- },
- beginAtZero: true,
- },
- y1: {
- type: 'linear',
- display: true,
- position: 'right',
- title: {
- display: true,
- text: '百分比(%)',
- },
- min: 0,
- max: 100,
- grid: {
- drawOnChartArea: false,
- },
- },
- },
- };
- constructor(
- private http: HttpClient,
- private message: NzMessageService,
- private router: Router,
- private cd: ChangeDetectorRef // 注入 ChangeDetectorRef
- ) {
- Chart.register(...registerables);
- }
- ngOnInit(): void {
- this.loadAllStatistics();
- }
- loadAllStatistics(): void {
- this.isLoading = true;
- // Use forkJoin to run all requests in parallel and wait for all to complete
- forkJoin({
- overall: this.http
- .get(`/api/message-records/statistics/overall?t=${new Date()}`)
- .pipe(
- map((res: any) => res?.data || null),
- catchError((err) => {
- console.error('Failed to load overall statistics:', err);
- this.message.error('加载整体统计失败');
- return of(null); // Return a default value to prevent forkJoin from failing
- })
- ),
- strategies: this.http
- .get(`/api/message-records/statistics/by-strategy?t=${new Date()}`)
- .pipe(
- map((res: any) => res?.data || []),
- catchError((err) => {
- console.error('Failed to load strategy statistics:', err);
- this.message.error('加载策略统计失败');
- return of([]);
- })
- ),
- templates: this.http
- .get(`/api/message-records/statistics/by-template?t=${new Date()}`)
- .pipe(
- map((res: any) => res?.data || []),
- catchError((err) => {
- console.error('Failed to load template statistics:', err);
- this.message.error('加载模板统计失败');
- return of([]);
- })
- ),
- dailyTrends: this.http
- .get(`/api/message-records/statistics/daily-trends?t=${new Date()}`)
- .pipe(
- map((res: any) => res?.data || []),
- catchError((err) => {
- console.error('Failed to load daily trends:', err);
- this.message.error('加载每日趋势失败');
- return of([]);
- })
- ),
- // avgTime: this.http
- // .get('/api/message-records/statistics/avg-delivery-time')
- // .pipe(
- // map((res: any) => res?.data || null),
- // catchError((err) => {
- // console.error('Failed to load average delivery time:', err);
- // this.message.error('加载平均送达时间失败');
- // return of(null);
- // })
- // ),
- })
- .pipe(
- finalize(() => {
- this.isLoading = false;
- // 在所有数据都赋值完毕后,手动触发变更检测
- this.cd.detectChanges();
- })
- )
- .subscribe((results: any) => {
- // Assign data to component properties
- this.overallStats = results.overall;
- this.strategyStats = results.strategies;
- this.templateStats = results.templates;
- this.dailyTrends = results.dailyTrends;
- // this.avgDeliveryTime = results.avgTime;
- // Update charts based on the latest data
- this.updateChartData();
- });
- }
- private updateChartData(): void {
- // 更新组合图表数据
- this.combinedChartData = {
- labels: this.dailyTrends.map((t) => this.formatDate(t.date)),
- datasets: [
- {
- ...this.combinedChartData.datasets[0],
- data: this.dailyTrends.map((t) => t.totalRecords || 0),
- },
- {
- ...this.combinedChartData.datasets[1],
- data: this.dailyTrends.map((t) => t.sent || 0),
- },
- {
- ...this.combinedChartData.datasets[2],
- data: this.dailyTrends.map((t) => t.delivered || 0),
- },
- {
- ...this.combinedChartData.datasets[3],
- data: this.dailyTrends.map((t) => t.opened || 0),
- },
- {
- ...this.combinedChartData.datasets[4],
- data: this.dailyTrends.map((t) => t.displayCount || 0),
- },
- {
- ...this.combinedChartData.datasets[5],
- data: this.dailyTrends.map((t) => t.failed || 0),
- },
- ],
- };
- // 更新比率图表数据
- this.rateChartData = {
- labels: this.dailyTrends.map((t) => this.formatDate(t.date)),
- datasets: [
- {
- ...this.rateChartData.datasets[0],
- data: this.dailyTrends.map((t) => (t.sentSuccessRate || 0) * 100),
- },
- {
- ...this.rateChartData.datasets[1],
- data: this.dailyTrends.map((t) => (t.deliveredRate || 0) * 100),
- },
- {
- ...this.rateChartData.datasets[2],
- data: this.dailyTrends.map((t) => (t.displayRate || 0) * 100),
- },
- {
- ...this.rateChartData.datasets[3],
- data: this.dailyTrends.map((t) => (t.clickThroughRate || 0) * 100),
- },
- ],
- };
- }
- refreshData(): void {
- this.loadAllStatistics();
- }
- navigateToStrategy(strategyName: string): void {
- this.router.navigate(['/message-strategy'], {
- queryParams: { strategyName: strategyName },
- });
- }
- navigateToTemplate(templateName: string): void {
- this.router.navigate(['/message-template'], {
- queryParams: { templateName: templateName },
- });
- }
- formatPercentage(value: number): string {
- return (value * 100).toFixed(2) + '%';
- }
- formatPercentageToNumber(value: number): number {
- return Number((value * 100).toFixed(2));
- }
- formatSeconds(seconds: number): string {
- if (seconds < 60) {
- return seconds.toFixed(2) + '秒';
- } else {
- return (seconds / 60).toFixed(2) + '分钟';
- }
- }
- preciseRound(num: number, decimalPlaces: number): number {
- if (decimalPlaces === 0) return Math.round(num);
- const multiplier = Math.pow(10, decimalPlaces);
- return Number(Math.round(num * multiplier) / multiplier);
- }
- private formatDate(date: string | null): string {
- if (!date) return '未知日期';
- return new Date(date).toLocaleDateString();
- }
- }
|