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' }}
= 0">
{{ 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;
},
});
}
}