|
|
@@ -1,7 +1,8 @@
|
|
|
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
|
|
import { IonicModule } from '@ionic/angular';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { Chart, registerables } from 'chart.js';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { Chart, registerables, ChartTypeRegistry } from 'chart.js';
|
|
|
import { FocusDataService } from '../services/focus-data.service';
|
|
|
|
|
|
Chart.register(...registerables);
|
|
|
@@ -11,54 +12,242 @@ Chart.register(...registerables);
|
|
|
templateUrl: './tab3.page.html',
|
|
|
styleUrls: ['./tab3.page.scss'],
|
|
|
standalone: true,
|
|
|
- imports: [IonicModule, CommonModule]
|
|
|
+ imports: [IonicModule, CommonModule, FormsModule]
|
|
|
})
|
|
|
export class Tab3Page implements OnInit {
|
|
|
- @ViewChild('focusChart', { static: true }) focusChart!: ElementRef;
|
|
|
- chart: any;
|
|
|
- focusRecords: { date: string, duration: number }[] = [];
|
|
|
+ @ViewChild('focusChart') focusChart!: ElementRef;
|
|
|
+ @ViewChild('taskChart') taskChart!: ElementRef;
|
|
|
+ @ViewChild('categoryChart') categoryChart!: ElementRef;
|
|
|
+
|
|
|
+ focusRecords: any[] = [];
|
|
|
+ tasks: any[] = [];
|
|
|
+ isLoading: boolean = false;
|
|
|
+ selectedChart: string = 'focus'; // 'focus' | 'task' | 'category'
|
|
|
+ timeRange: string = 'week';
|
|
|
+
|
|
|
+ stats = {
|
|
|
+ totalFocusTime: 0,
|
|
|
+ totalTasks: 0,
|
|
|
+ completedTasks: 0,
|
|
|
+ mostFrequentCategory: '',
|
|
|
+ averageFocusTime: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ activityTypes = [
|
|
|
+ { id: 'study', name: '学习', icon: 'school-outline' },
|
|
|
+ { id: 'work', name: '工作', icon: 'briefcase-outline' },
|
|
|
+ { id: 'sleep', name: '睡眠', icon: 'moon-outline' },
|
|
|
+ { id: 'sport', name: '运动', icon: 'barbell-outline' },
|
|
|
+ { id: 'reading', name: '阅读', icon: 'book-outline' },
|
|
|
+ { id: 'coding', name: '编程', icon: 'code-slash-outline' },
|
|
|
+ { id: 'meditation', name: '冥想', icon: 'leaf-outline' },
|
|
|
+ { id: 'music', name: '音乐', icon: 'musical-notes-outline' },
|
|
|
+ { id: 'language', name: '语言', icon: 'language-outline' },
|
|
|
+ { id: 'writing', name: '写作', icon: 'pencil-outline' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ charts: { [key: string]: Chart } = {};
|
|
|
|
|
|
constructor(private focusDataService: FocusDataService) {}
|
|
|
|
|
|
async ngOnInit() {
|
|
|
- this.focusRecords = await this.focusDataService.getFocusRecords();
|
|
|
- console.log('Focus Records in Tab3:', this.focusRecords);
|
|
|
- this.loadChartData();
|
|
|
+ await this.loadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ ionViewWillEnter() {
|
|
|
+ this.loadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadData() {
|
|
|
+ this.isLoading = true;
|
|
|
+ try {
|
|
|
+ // 并行加载数据
|
|
|
+ const [focusRecords, tasks] = await Promise.all([
|
|
|
+ this.focusDataService.getFocusRecords(),
|
|
|
+ this.focusDataService.getTasks()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ this.focusRecords = focusRecords;
|
|
|
+ this.tasks = tasks;
|
|
|
+ this.calculateStats();
|
|
|
+ this.updateCharts();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载数据失败:', error);
|
|
|
+ } finally {
|
|
|
+ this.isLoading = false;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- loadChartData() {
|
|
|
- const labels = this.focusRecords.map(record => record.date);
|
|
|
- const dataValues = this.focusRecords.map(record => record.duration);
|
|
|
+ calculateStats() {
|
|
|
+ // 计算专注时间统计
|
|
|
+ this.stats.totalFocusTime = this.focusRecords.reduce((sum, record) =>
|
|
|
+ sum + record.duration, 0);
|
|
|
+ this.stats.averageFocusTime = this.focusRecords.length ?
|
|
|
+ Math.round(this.stats.totalFocusTime / this.focusRecords.length) : 0;
|
|
|
+
|
|
|
+ // 计算任务统计
|
|
|
+ this.stats.totalTasks = this.tasks.length;
|
|
|
+ this.stats.completedTasks = this.tasks.filter(task => task.completed).length;
|
|
|
+
|
|
|
+ // 计算最常见类别
|
|
|
+ const categoryCount = [...this.focusRecords, ...this.tasks].reduce((acc, item) => {
|
|
|
+ const category = item.category;
|
|
|
+ acc[category] = (acc[category] || 0) + 1;
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+
|
|
|
+ this.stats.mostFrequentCategory = Object.entries(categoryCount)
|
|
|
+ .sort(([,a]: any, [,b]: any) => b - a)[0][0];
|
|
|
+ }
|
|
|
|
|
|
- const data = {
|
|
|
- labels: labels,
|
|
|
+ updateCharts() {
|
|
|
+ this.updateFocusChart();
|
|
|
+ this.updateTaskChart();
|
|
|
+ this.updateCategoryChart();
|
|
|
+ }
|
|
|
+
|
|
|
+ updateFocusChart() {
|
|
|
+ const filteredRecords = this.filterDataByTimeRange(this.focusRecords, 'startTime');
|
|
|
+ const groupedData = this.groupByDate(filteredRecords, 'startTime', 'duration');
|
|
|
+
|
|
|
+ this.createOrUpdateChart('focus', this.focusChart, {
|
|
|
+ labels: Object.keys(groupedData),
|
|
|
datasets: [{
|
|
|
- label: 'Focus Time (minutes)',
|
|
|
- data: dataValues,
|
|
|
- backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
|
- borderColor: 'rgba(75, 192, 192, 1)',
|
|
|
+ label: '专注时间(分钟)',
|
|
|
+ data: Object.values(groupedData),
|
|
|
+ backgroundColor: 'rgba(var(--ion-color-primary-rgb), 0.4)',
|
|
|
+ borderColor: 'var(--ion-color-primary)',
|
|
|
borderWidth: 1
|
|
|
}]
|
|
|
- };
|
|
|
+ }, 'bar');
|
|
|
+ }
|
|
|
|
|
|
- this.createChart(data);
|
|
|
+ updateTaskChart() {
|
|
|
+ const filteredTasks = this.filterDataByTimeRange(this.tasks, 'createdAt');
|
|
|
+ const completed = filteredTasks.filter(task => task.completed).length;
|
|
|
+ const uncompleted = filteredTasks.length - completed;
|
|
|
+
|
|
|
+ this.createOrUpdateChart('task', this.taskChart, {
|
|
|
+ labels: ['已完成', '未完成'],
|
|
|
+ datasets: [{
|
|
|
+ data: [completed, uncompleted],
|
|
|
+ backgroundColor: [
|
|
|
+ 'rgba(var(--ion-color-success-rgb), 0.8)',
|
|
|
+ 'rgba(var(--ion-color-medium-rgb), 0.4)'
|
|
|
+ ]
|
|
|
+ }]
|
|
|
+ }, 'doughnut');
|
|
|
+ }
|
|
|
+
|
|
|
+ updateCategoryChart() {
|
|
|
+ const filteredData = [
|
|
|
+ ...this.filterDataByTimeRange(this.focusRecords, 'startTime'),
|
|
|
+ ...this.filterDataByTimeRange(this.tasks, 'createdAt')
|
|
|
+ ];
|
|
|
+
|
|
|
+ const categoryData = filteredData.reduce((acc, item) => {
|
|
|
+ const category = item.category;
|
|
|
+ acc[category] = (acc[category] || 0) + 1;
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+
|
|
|
+ this.createOrUpdateChart('category', this.categoryChart, {
|
|
|
+ labels: Object.keys(categoryData).map(id =>
|
|
|
+ this.activityTypes.find(type => type.id === id)?.name || id
|
|
|
+ ),
|
|
|
+ datasets: [{
|
|
|
+ data: Object.values(categoryData),
|
|
|
+ backgroundColor: [
|
|
|
+ 'rgba(var(--ion-color-primary-rgb), 0.8)',
|
|
|
+ 'rgba(var(--ion-color-secondary-rgb), 0.8)',
|
|
|
+ 'rgba(var(--ion-color-tertiary-rgb), 0.8)',
|
|
|
+ 'rgba(var(--ion-color-success-rgb), 0.8)',
|
|
|
+ 'rgba(var(--ion-color-warning-rgb), 0.8)'
|
|
|
+ ]
|
|
|
+ }]
|
|
|
+ }, 'pie');
|
|
|
}
|
|
|
|
|
|
- createChart(data: any) {
|
|
|
- if (this.chart) {
|
|
|
- this.chart.destroy();
|
|
|
+ private filterDataByTimeRange(data: any[], dateField: string) {
|
|
|
+ const now = new Date();
|
|
|
+ let startDate: Date;
|
|
|
+
|
|
|
+ switch (this.timeRange) {
|
|
|
+ case 'week':
|
|
|
+ startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
+ break;
|
|
|
+ case 'month':
|
|
|
+ startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
|
|
+ break;
|
|
|
+ case 'year':
|
|
|
+ startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ startDate = new Date(0); // 所有数据
|
|
|
}
|
|
|
|
|
|
- this.chart = new Chart(this.focusChart.nativeElement, {
|
|
|
- type: 'bar',
|
|
|
+ return data.filter(item => new Date(item[dateField]) >= startDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ private groupByDate(data: any[], dateField: string, valueField: string) {
|
|
|
+ return data.reduce((acc, item) => {
|
|
|
+ const date = new Date(item[dateField]).toLocaleDateString();
|
|
|
+ acc[date] = (acc[date] || 0) + item[valueField];
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+
|
|
|
+ private createOrUpdateChart(id: string, canvas: ElementRef, data: any, type: string) {
|
|
|
+ if (this.charts[id]) {
|
|
|
+ this.charts[id].destroy();
|
|
|
+ }
|
|
|
+
|
|
|
+ const ctx = canvas.nativeElement.getContext('2d');
|
|
|
+ this.charts[id] = new Chart(ctx, {
|
|
|
+ type: type as keyof ChartTypeRegistry,
|
|
|
data: data,
|
|
|
options: {
|
|
|
- scales: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'bottom',
|
|
|
+ labels: {
|
|
|
+ color: 'var(--ion-text-color)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: type === 'bar' ? {
|
|
|
y: {
|
|
|
- beginAtZero: true
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(var(--ion-color-medium-rgb), 0.1)'
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ color: 'var(--ion-text-color)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ grid: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ color: 'var(--ion-text-color)'
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ } : undefined
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
-}
|
|
|
+
|
|
|
+ onTimeRangeChange(event: any) {
|
|
|
+ this.timeRange = event.detail.value;
|
|
|
+ this.updateCharts();
|
|
|
+ }
|
|
|
+
|
|
|
+ onChartTypeChange(event: any) {
|
|
|
+ this.selectedChart = event.detail.value;
|
|
|
+ // 给图表一点时间重新渲染
|
|
|
+ setTimeout(() => this.updateCharts(), 100);
|
|
|
+ }
|
|
|
+}
|