18079408532 1 年間 前
コミット
9ef9531c13

+ 87 - 45
src/app/services/focus-data.service.ts

@@ -1,77 +1,73 @@
 import { Injectable } from '@angular/core';
-import * as Parse from 'parse';
+import Parse from 'parse';
 
 @Injectable({
   providedIn: 'root'
 })
 export class FocusDataService {
-  async addFocusRecord(data: {
-    duration: number;
-    category: string;
-    startTime: Date;
-    endTime: Date;
-  }) {
+  constructor() {}
+
+  async getFocusRecords(): Promise<any[]> {
     try {
       const FocusRecord = Parse.Object.extend('FocusRecord');
-      const record = new FocusRecord();
+      const query = new Parse.Query(FocusRecord);
+      query.descending('startTime');
+      const results = await query.find();
       
-      record.set('duration', data.duration);
-      record.set('category', data.category);
-      record.set('startTime', data.startTime);
-      record.set('endTime', data.endTime);
-      record.set('completed', true);
-      record.set('userId', Parse.User.current()?.id);
-
-      await record.save();
-      return record;
+      return results.map(record => ({
+        id: record.id,
+        category: record.get('category'),
+        duration: record.get('duration'),
+        startTime: record.get('startTime'),
+        endTime: record.get('endTime')
+      }));
     } catch (error) {
-      console.error('保存专注记录失败:', error);
+      console.error('获取专注记录失败:', error);
       throw error;
     }
   }
 
-  async getFocusRecords() {
+  async getTasks(): Promise<any[]> {
     try {
-      const query = new Parse.Query('FocusRecord');
-      query.equalTo('userId', Parse.User.current()?.id);
+      const Task = Parse.Object.extend('Task');
+      const query = new Parse.Query(Task);
       query.descending('createdAt');
-      query.limit(30); // 获取最近30天的记录
-      
       const results = await query.find();
-      return results.map(record => ({
-        id: record.id,
-        date: record.get('startTime'),
-        duration: record.get('duration'),
-        category: record.get('category')
+      
+      return results.map(task => ({
+        id: task.id,
+        title: task.get('title'),
+        category: task.get('category'),
+        completed: task.get('completed'),
+        createdAt: task.get('createdAt')
       }));
     } catch (error) {
-      console.error('获取专注记录失败:', error);
-      return [];
+      console.error('获取任务数据失败:', error);
+      throw error;
     }
   }
 
-  async getWeeklyStats() {
+  async addFocusRecord(record: any): Promise<Parse.Object> {
     try {
-      const query = new Parse.Query('FocusRecord');
-      query.equalTo('userId', Parse.User.current()?.id);
-      query.greaterThan('startTime', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
-      query.ascending('startTime');
+      const FocusRecord = Parse.Object.extend('FocusRecord');
+      const newRecord = new FocusRecord();
       
-      const results = await query.find();
-      return results.map(record => ({
-        date: record.get('startTime'),
-        duration: record.get('duration'),
-        category: record.get('category')
-      }));
+      newRecord.set('category', record.category);
+      newRecord.set('duration', record.duration);
+      newRecord.set('startTime', record.startTime);
+      newRecord.set('endTime', record.endTime);
+      
+      return await newRecord.save();
     } catch (error) {
-      console.error('获取周统计失败:', error);
-      return [];
+      console.error('保存专注记录失败:', error);
+      throw error;
     }
   }
 
-  async deleteFocusRecord(recordId: string) {
+  async deleteFocusRecord(recordId: string): Promise<void> {
     try {
-      const query = new Parse.Query('FocusRecord');
+      const FocusRecord = Parse.Object.extend('FocusRecord');
+      const query = new Parse.Query(FocusRecord);
       const record = await query.get(recordId);
       await record.destroy();
     } catch (error) {
@@ -79,4 +75,50 @@ export class FocusDataService {
       throw error;
     }
   }
+
+  async addTask(task: any): Promise<string> {
+    try {
+      const Task = Parse.Object.extend('Task');
+      const newTask = new Task();
+      
+      newTask.set('title', task.title);
+      newTask.set('category', task.category);
+      newTask.set('completed', task.completed || false);
+      
+      const result = await newTask.save();
+      return result.id;
+    } catch (error) {
+      console.error('保存任务失败:', error);
+      throw error;
+    }
+  }
+
+  async updateTask(taskId: string, updates: any): Promise<void> {
+    try {
+      const Task = Parse.Object.extend('Task');
+      const query = new Parse.Query(Task);
+      const task = await query.get(taskId);
+      
+      Object.keys(updates).forEach(key => {
+        task.set(key, updates[key]);
+      });
+      
+      await task.save();
+    } catch (error) {
+      console.error('更新任务失败:', error);
+      throw error;
+    }
+  }
+
+  async deleteTask(taskId: string): Promise<void> {
+    try {
+      const Task = Parse.Object.extend('Task');
+      const query = new Parse.Query(Task);
+      const task = await query.get(taskId);
+      await task.destroy();
+    } catch (error) {
+      console.error('删除任务失败:', error);
+      throw error;
+    }
+  }
 }

+ 1 - 1
src/app/tab2/tab2.page.ts

@@ -154,7 +154,7 @@ export class Tab2Page implements OnInit {
           startTime: this.timerStartTime!.toISOString(),
           endTime: endTime.toISOString(),
           duration: duration,
-          parseId: savedRecord.id
+          parseId: savedRecord.id as string
         };
         
         this.records.unshift(record);

+ 22 - 12
src/app/tab3/tab3.page.html

@@ -1,17 +1,27 @@
 <ion-header>
   <ion-toolbar>
-    <ion-title>数据统计</ion-title>
+    <ion-title>统计分析</ion-title>
   </ion-toolbar>
 </ion-header>
 
-<ion-content class="ion-padding">
-  <canvas #focusChart></canvas>
-  <ion-list>
-    <ion-item *ngFor="let record of focusRecords">
-      <ion-label>
-        <h2>{{ record.date }}</h2>
-        <p>Duration: {{ record.duration }} minutes</p>
-      </ion-label>
-    </ion-item>
-  </ion-list>
-</ion-content>
+<ion-content>
+  <ion-segment [(ngModel)]="timeRange" (ionChange)="onTimeRangeChange($event)">
+    <ion-segment-button value="week">
+      <ion-label>周</ion-label>
+    </ion-segment-button>
+    <ion-segment-button value="month">
+      <ion-label>月</ion-label>
+    </ion-segment-button>
+    <ion-segment-button value="year">
+      <ion-label>年</ion-label>
+    </ion-segment-button>
+  </ion-segment>
+
+  <ion-progress-bar type="indeterminate" *ngIf="isLoading"></ion-progress-bar>
+
+  <div class="chart-container">
+    <canvas #focusChart></canvas>
+    <canvas #taskChart></canvas>
+    <canvas #categoryChart></canvas>
+  </div>
+</ion-content>

+ 66 - 4
src/app/tab3/tab3.page.scss

@@ -1,7 +1,69 @@
-.chart-container {
-  padding: 20px;
+ion-content {
+  --background: var(--ion-background-color);
+}
+
+.loading-container {
   display: flex;
-  justify-content: center;
+  flex-direction: column;
   align-items: center;
-  height: 100%;
+  justify-content: center;
+  height: 200px;
+  
+  ion-spinner {
+    width: 48px;
+    height: 48px;
+    margin-bottom: 16px;
+  }
+}
+
+.chart-container {
+  height: 300px;
+  margin: 16px;
+  padding: 16px;
+  background: var(--ion-card-background);
+  border-radius: 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+ion-card {
+  margin: 8px;
+  border-radius: 16px;
+  background: var(--ion-card-background);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  
+  ion-card-content {
+    padding: 16px;
+    text-align: center;
+
+    h2 {
+      font-size: 14px;
+      color: var(--ion-color-medium);
+      margin: 0 0 8px 0;
+    }
+
+    .stat-value {
+      font-size: 24px;
+      font-weight: bold;
+      color: var(--ion-color-primary);
+      margin: 0;
+    }
+  }
+}
+
+ion-segment {
+  margin: 16px;
+  padding: 4px;
+  background: var(--ion-card-background);
+  border-radius: 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+
+  ion-segment-button {
+    --color: var(--ion-color-medium);
+    --color-checked: var(--ion-color-primary);
+    --indicator-color: var(--ion-color-primary);
+  }
+}
+
+ion-header ion-toolbar {
+  --background: var(--ion-background-color);
 }

+ 217 - 28
src/app/tab3/tab3.page.ts

@@ -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);
+  }
+}