import { Component, OnInit } from '@angular/core'; import { CommonModule, NgFor, NgIf, DatePipe } from '@angular/common'; import { DashboardService, NewArtworkTab } from '../services/dashboard.service'; import { ArtDoneRateComponent } from './art-done-rate.component'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzStatisticModule } from 'ng-zorro-antd/statistic'; import { NzSpinModule } from 'ng-zorro-antd/spin'; import { NzTableModule } from 'ng-zorro-antd/table'; import { NzPageHeaderModule } from 'ng-zorro-antd/page-header'; import { NzTabsModule } from 'ng-zorro-antd/tabs'; import { NzMessageService } from 'ng-zorro-antd/message'; @Component({ selector: 'app-dashboard', standalone: true, imports: [ CommonModule, NgFor, NgIf, DatePipe, ArtDoneRateComponent, NzCardModule, NzIconModule, NzStatisticModule, NzSpinModule, NzTableModule, NzPageHeaderModule, NzTabsModule, ], providers: [NzMessageService], template: `
数据看板

最后更新时间:{{ lastUpdateTime | date: 'yyyy-MM-dd HH:mm:ss' }}

{{ hoveredDauDate }}:{{ hoveredDauValue }}
{{ dauTrendStartDate }} Max: {{ dauMax }} Min: {{ dauMin }} {{ dauTrendEndDate }}
{{ tab.date }} · 当日DAU {{ tab.dau }}
作品 名称 区块数 点击率 完成率 点击数 完成数 道具使用数 作品缩略图 {{ work.name }} {{ work.areaCount || 0 }} {{ work.clickRate * 100 | number: '1.1-2' }}% {{ work.clickCount }} {{ work.completionCount }} {{ work.tipCount || 0 }}
当日无上新作品
`, styles: [ ` .dashboard-container { padding: 24px; background: #f5f7fa; min-height: 100%; } .stack-card { margin-bottom: 16px; } .range-buttons { margin: 12px 0 8px; display: flex; gap: 8px; } .range-btn { border: 1px solid #d9d9d9; background: #fff; border-radius: 6px; padding: 4px 10px; cursor: pointer; } .range-btn.active { border-color: #1677ff; color: #1677ff; background: #e6f4ff; } .chart-wrapper { margin-top: 8px; border: 1px solid #f0f0f0; border-radius: 8px; background: #fcfdff; } .line-chart { width: 100%; height: 180px; } .chart-meta { margin-top: 8px; display: flex; justify-content: space-between; color: #8c8c8c; font-size: 12px; } .chart-hover-tip { margin-top: 6px; color: #262626; font-size: 12px; } .tab-subtitle { margin-bottom: 10px; color: #595959; font-size: 12px; } .work-thumbnail { width: 52px; height: 52px; border-radius: 4px; object-fit: cover; border: 1px solid #f0f0f0; cursor: zoom-in; } .empty-tip { margin-top: 10px; color: #8c8c8c; font-size: 12px; } `, ], }) export class DashboardComponent implements OnInit { isLoading = true; lastUpdateTime = new Date(); activeUsersToday = 0; selectedDauRange = 30; dauTrendData: number[] = []; dauTrendLabels: string[] = []; dauChartPoints = ''; dauMin = 0; dauMax = 0; dauTrendStartDate = ''; dauTrendEndDate = ''; hoveredDauIndex = -1; hoveredDauX = 0; hoveredDauY = 0; hoveredDauDate = ''; hoveredDauValue = 0; artworkTabs: NewArtworkTab[] = []; activeArtworkTabIndex = 0; private readonly chartWidth = 600; private readonly chartHeight = 180; private readonly chartPadding = 16; constructor( private message: NzMessageService, private dashboardService: DashboardService, ) {} ngOnInit(): void { this.loadDashboardData(); } refreshData(): void { this.loadDashboardData(); } changeDauRange(days: number): void { if (this.selectedDauRange === days) return; this.selectedDauRange = days; this.loadUserCardData(); } private buildDauChartPoints(values: number[]): string { if (!values.length) return ''; const width = 600; const height = 180; const padding = 16; const minVal = Math.min(...values); const maxVal = Math.max(...values); const span = Math.max(1, maxVal - minVal); const stepX = values.length > 1 ? (width - padding * 2) / (values.length - 1) : 0; return values .map((value, idx) => { const x = padding + idx * stepX; const y = height - padding - ((value - minVal) / span) * (height - padding * 2); return `${x},${y}`; }) .join(' '); } private getDauY(value: number): number { const minVal = this.dauMin; const maxVal = this.dauMax; const span = Math.max(1, maxVal - minVal); return this.chartHeight - this.chartPadding - ((value - minVal) / span) * (this.chartHeight - this.chartPadding * 2); } onChartMouseMove(event: MouseEvent): void { if (this.dauTrendData.length === 0) return; const rect = (event.target as SVGRectElement).getBoundingClientRect(); const localX = ((event.clientX - rect.left) / rect.width) * this.chartWidth; let index = 0; if (this.dauTrendData.length > 1) { const stepX = (this.chartWidth - this.chartPadding * 2) / (this.dauTrendData.length - 1); index = Math.round((localX - this.chartPadding) / stepX); index = Math.max(0, Math.min(this.dauTrendData.length - 1, index)); this.hoveredDauX = this.chartPadding + index * stepX; } else { this.hoveredDauX = this.chartWidth / 2; } this.hoveredDauIndex = index; this.hoveredDauDate = this.dauTrendLabels[index] || ''; this.hoveredDauValue = this.dauTrendData[index] || 0; this.hoveredDauY = this.getDauY(this.hoveredDauValue); } onChartMouseLeave(): void { this.hoveredDauIndex = -1; } getArtworkDetailUrl(resId: string): string { return `https://color2.jccytech.cn/app/zh/pages/detail/${resId}`; } openArtworkImage(imageUrl: string): void { if (!imageUrl) return; window.open(imageUrl, '_blank', 'noopener,noreferrer'); } private applyDauData(dau: { today: number; trend: Array<{ date: string; dau: number }> }): void { this.activeUsersToday = dau.today; this.dauTrendLabels = dau.trend.map((item) => item.date); this.dauTrendData = dau.trend.map((item) => item.dau); this.dauMin = this.dauTrendData.length ? Math.min(...this.dauTrendData) : 0; this.dauMax = this.dauTrendData.length ? Math.max(...this.dauTrendData) : 0; this.dauTrendStartDate = this.dauTrendLabels[0] || ''; this.dauTrendEndDate = this.dauTrendLabels[this.dauTrendLabels.length - 1] || ''; this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData); } private loadUserCardData(): void { this.isLoading = true; this.dashboardService.getKpi(this.selectedDauRange).subscribe({ next: (response) => { if (!response.success) { this.message.error('获取日活数据失败'); this.isLoading = false; return; } this.applyDauData(response.data.dau); this.lastUpdateTime = new Date(); this.isLoading = false; }, error: (error) => { console.error('Failed to load DAU data:', error); this.message.error('加载日活数据出错'); this.isLoading = false; }, }); } private loadArtworkTabsData(): Promise { return new Promise((resolve) => { this.dashboardService.getNewArtworkTabs(7, 20).subscribe({ next: (response) => { if (response.success) { this.artworkTabs = response.data.tabs || []; this.activeArtworkTabIndex = 0; } else { this.message.error('获取上新作品数据失败'); } resolve(); }, error: (error) => { console.error('Failed to load artwork tabs:', error); this.message.error('加载上新作品数据出错'); resolve(); }, }); }); } loadDashboardData(): void { this.isLoading = true; this.dashboardService.getKpi(this.selectedDauRange).subscribe({ next: async (kpiResponse) => { if (!kpiResponse.success) { this.message.error('获取日活数据失败'); this.isLoading = false; return; } this.applyDauData(kpiResponse.data.dau); await this.loadArtworkTabsData(); this.lastUpdateTime = new Date(); this.isLoading = false; }, error: (error) => { console.error('Failed to load dashboard data:', error); this.message.error('加载看板数据出错'); this.isLoading = false; }, }); } }