18079408532 1 rok temu
rodzic
commit
f03f302c80
35 zmienionych plików z 1632 dodań i 355 usunięć
  1. 8 1
      src/app/app.component.ts
  2. 4 3
      src/app/app.routes.ts
  3. 57 0
      src/app/components/task-modal/task-modal.component.ts
  4. 81 0
      src/app/models/Task.ts
  5. 13 0
      src/app/picture/picture.page.html
  6. 0 0
      src/app/picture/picture.page.scss
  7. 17 0
      src/app/picture/picture.page.spec.ts
  8. 20 0
      src/app/picture/picture.page.ts
  9. 30 0
      src/app/services/auth.service.ts
  10. 16 0
      src/app/services/focus-data.service.ts
  11. 49 0
      src/app/services/ncloud.ts
  12. 67 69
      src/app/tab1/tab1.page.html
  13. 54 95
      src/app/tab1/tab1.page.ts
  14. 90 49
      src/app/tab2/tab2.page.html
  15. 65 67
      src/app/tab2/tab2.page.scss
  16. 30 2
      src/app/tab2/tab2.page.ts
  17. 10 4
      src/app/tab3/tab3.page.html
  18. 1 1
      src/app/tab3/tab3.page.scss
  19. 28 62
      src/app/tab3/tab3.page.ts
  20. 77 0
      src/app/tab4/tab4.page.html
  21. 125 0
      src/app/tab4/tab4.page.scss
  22. 18 0
      src/app/tab4/tab4.page.spec.ts
  23. 99 0
      src/app/tab4/tab4.page.ts
  24. 4 0
      src/app/tabs/tabs.page.html
  25. 6 2
      src/app/tabs/tabs.routes.ts
  26. 389 0
      src/lib/ncloud.ts
  27. 29 0
      src/lib/user/modal-user-edit/modal-user-edit.component.html
  28. 0 0
      src/lib/user/modal-user-edit/modal-user-edit.component.scss
  29. 22 0
      src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts
  30. 66 0
      src/lib/user/modal-user-edit/modal-user-edit.component.ts
  31. 36 0
      src/lib/user/modal-user-login/modal-user-login.component.html
  32. 0 0
      src/lib/user/modal-user-login/modal-user-login.component.scss
  33. 22 0
      src/lib/user/modal-user-login/modal-user-login.component.spec.ts
  34. 96 0
      src/lib/user/modal-user-login/modal-user-login.component.ts
  35. 3 0
      tsconfig.json

+ 8 - 1
src/app/app.component.ts

@@ -1,5 +1,8 @@
 import { Component } from '@angular/core';
 import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+import { trash, add } from 'ionicons/icons';
+import * as Parse from 'parse';
 
 @Component({
   selector: 'app-root',
@@ -8,5 +11,9 @@ import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
   imports: [IonApp, IonRouterOutlet],
 })
 export class AppComponent {
-  constructor() {}
+  constructor() {
+    addIcons({ trash, add });
+    Parse.initialize('YOUR_APP_ID', 'YOUR_JS_KEY', undefined);
+    (Parse as any).serverURL = 'http://dev.fmode.cn:1337/parse';
+  }
 }

+ 4 - 3
src/app/app.routes.ts

@@ -3,10 +3,11 @@ import { Routes } from '@angular/router';
 export const routes: Routes = [
   {
     path: '',
-    loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes),
+    loadChildren: () => import('./tabs/tabs.routes').then(m => m.routes)
   },
   {
-    path: 'countdown',
-    loadComponent: () => import('./countdown/countdown.page').then(m => m.CountdownPage)
+    path: 'login',
+    loadComponent: () => import('../lib/user/modal-user-login/modal-user-login.component')
+      .then(m => m.ModalUserLoginComponent)
   }
 ];

+ 57 - 0
src/app/components/task-modal/task-modal.component.ts

@@ -0,0 +1,57 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule, ModalController } from '@ionic/angular';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+  selector: 'app-task-modal',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>新建任务</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismiss()">取消</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content class="ion-padding">
+      <ion-item>
+        <ion-label position="stacked">标题</ion-label>
+        <ion-input [(ngModel)]="task.title"></ion-input>
+      </ion-item>
+
+      <ion-item>
+        <ion-label position="stacked">开始时间</ion-label>
+        <ion-datetime-button datetime="startTime"></ion-datetime-button>
+        <ion-modal [keepContentsMounted]="true">
+          <ng-template>
+            <ion-datetime id="startTime" [(ngModel)]="task.startTime"></ion-datetime>
+          </ng-template>
+        </ion-modal>
+      </ion-item>
+
+      <ion-button expand="block" (click)="confirm()" class="ion-margin-top">
+        创建任务
+      </ion-button>
+    </ion-content>
+  `,
+  standalone: true,
+  imports: [IonicModule, CommonModule, FormsModule]
+})
+export class TaskModalComponent {
+  task = {
+    title: '',
+    startTime: new Date().toISOString()
+  };
+
+  constructor(private modalCtrl: ModalController) {}
+
+  dismiss() {
+    this.modalCtrl.dismiss(null, 'cancel');
+  }
+
+  confirm() {
+    this.modalCtrl.dismiss(this.task, 'confirm');
+  }
+} 

+ 81 - 0
src/app/models/Task.ts

@@ -0,0 +1,81 @@
+import { CloudObject, CloudQuery } from '../../lib/ncloud';
+import * as Parse from 'parse';
+
+export class Task extends CloudObject {
+  protected override parseObject: Parse.Object;
+  override id: string | null = null;
+  title: string = '';
+  content: string = '';
+  startTime: string = new Date().toISOString();
+  endTime: string = new Date().toISOString();
+  completed: boolean = false;
+  userId: string | null = null;
+
+  constructor() {
+    super('Task');
+    this.parseObject = new Parse.Object('Task');
+  }
+
+  static async getUserTasks(userId: string): Promise<Task[]> {
+    try {
+      if (!userId) {
+        throw new Error('userId is required');
+      }
+      
+      const query = new Parse.Query('Task');
+      query.equalTo('userId', userId);
+      
+      const results = await query.find();
+      return results.map(result => {
+        const task = new Task();
+        task.parseObject = result;
+        Object.assign(task, result.toJSON());
+        return task;
+      });
+    } catch (error) {
+      console.error('Failed to get user tasks:', error);
+      throw error;
+    }
+  }
+
+  static async create(data: Partial<Task>): Promise<Task> {
+    try {
+      const task = new Task();
+      
+      // 处理日期
+      if (typeof data.startTime === 'string') {
+        data.startTime = new Date(data.startTime).toISOString();
+      }
+      if (typeof data.endTime === 'string') {
+        data.endTime = new Date(data.endTime).toISOString();
+      }
+
+      // 设置数据到 parseObject
+      Object.keys(data).forEach(key => {
+        task.parseObject.set(key, (data as any)[key]);
+      });
+
+      // 保存到数据库
+      await task.parseObject.save();
+      
+      // 设置本地数据
+      Object.assign(task, data);
+      task.id = task.parseObject.id;
+      
+      console.log('Task created:', task);  // 添加日志
+      return task;
+    } catch (error) {
+      console.error('Failed to create task:', error);
+      throw error;
+    }
+  }
+
+  async delete(): Promise<void> {
+    try {
+      await this.parseObject.destroy();
+    } catch (error) {
+      console.error('Delete failed:', error);
+      throw error;
+    }
+  }
+} 

+ 13 - 0
src/app/picture/picture.page.html

@@ -0,0 +1,13 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>picture</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">picture</ion-title>
+    </ion-toolbar>
+  </ion-header>
+</ion-content>

+ 0 - 0
src/app/picture/picture.page.scss


+ 17 - 0
src/app/picture/picture.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { PicturePage } from './picture.page';
+
+describe('PicturePage', () => {
+  let component: PicturePage;
+  let fixture: ComponentFixture<PicturePage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(PicturePage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 20 - 0
src/app/picture/picture.page.ts

@@ -0,0 +1,20 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+
+@Component({
+  selector: 'app-picture',
+  templateUrl: './picture.page.html',
+  styleUrls: ['./picture.page.scss'],
+  standalone: true,
+  imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule]
+})
+export class PicturePage implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}

+ 30 - 0
src/app/services/auth.service.ts

@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core';
+import * as Parse from 'parse';
+import { CloudUser } from '../../lib/ncloud';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthService {
+  currentUser: CloudUser | null = null;
+
+  constructor() {
+    this.checkAuthState();
+  }
+
+  async checkAuthState() {
+    const parseUser = Parse.User.current();
+    if (parseUser) {
+      this.currentUser = parseUser as unknown as CloudUser;
+    }
+    return this.currentUser;
+  }
+
+  setCurrentUser(user: CloudUser | null) {
+    this.currentUser = user;
+  }
+
+  isLoggedIn(): boolean {
+    return !!this.currentUser;
+  }
+} 

+ 16 - 0
src/app/services/focus-data.service.ts

@@ -0,0 +1,16 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class FocusDataService {
+  private focusRecords: { date: string, duration: number }[] = [];
+
+  addFocusRecord(date: string, duration: number) {
+    this.focusRecords.push({ date, duration });
+  }
+
+  getFocusRecords() {
+    return this.focusRecords;
+  }
+} 

+ 49 - 0
src/app/services/ncloud.ts

@@ -0,0 +1,49 @@
+import * as Parse from 'parse';
+
+export class CloudObject {
+  protected parseObject: Parse.Object;
+
+  constructor() {
+    this.parseObject = new Parse.Object(this.constructor.name);
+  }
+
+  async save(): Promise<void> {
+    try {
+      await this.parseObject.save(this);
+    } catch (error) {
+      console.error('Save failed:', error);
+      throw error;
+    }
+  }
+
+  setParseObject(parseObject: Parse.Object): void {
+    this.parseObject = parseObject;
+  }
+}
+
+export class CloudQuery {
+  private query: Parse.Query;
+
+  constructor(className: string) {
+    this.query = new Parse.Query(className);
+  }
+
+  equalTo(key: string, value: any): CloudQuery {
+    this.query.equalTo(key, value);
+    return this;
+  }
+
+  async find(): Promise<CloudObject[]> {
+    try {
+      const results = await this.query.find();
+      return results.map((result: Parse.Object) => {
+        const obj = new CloudObject();
+        obj.setParseObject(result);
+        return obj;
+      });
+    } catch (error) {
+      console.error('Query failed:', error);
+      throw error;
+    }
+  }
+} 

+ 67 - 69
src/app/tab1/tab1.page.html

@@ -1,79 +1,77 @@
-<ion-header>
-  <ion-toolbar color="light">
-    <ion-title>任务清单</ion-title>
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>
+      任务列表
+    </ion-title>
     <ion-buttons slot="end">
-      <ion-button (click)="openAddTaskModal()">
-        <ion-icon slot="icon-only" name="add-outline"></ion-icon>
+      <ion-button (click)="setOpen(true)">
+        <ion-icon name="add"></ion-icon>
       </ion-button>
     </ion-buttons>
   </ion-toolbar>
 </ion-header>
 
-<ion-content>
-  <ion-list lines="none">
-    <ion-item-sliding *ngFor="let task of tasks; let i = index">
-      <ion-item class="task-item" [style.--task-color]="getTaskColor(i)">
-        <ion-checkbox slot="start" [(ngModel)]="task.completed" (ionChange)="toggleTaskCompletion(task)"></ion-checkbox>
-        <ion-label class="task-label">
-          <h2 [class.strikethrough]="task.completed">{{ task.title }}</h2>
-          <p>开始: {{ task.startTime | date:'yyyy-MM-dd HH:mm' }}</p>
-          <p>截止: {{ task.endTime | date:'yyyy-MM-dd HH:mm' }}</p>
-        </ion-label>
-        <ion-badge slot="end" [color]="getTaskStatus(task)">{{ getTaskStatusText(task) }}</ion-badge>
-      </ion-item>
-      
-      <ion-item-options side="end">
-        <ion-item-option color="danger" (click)="deleteTask(task.id)">
-          <ion-icon slot="icon-only" name="trash"></ion-icon>
-          删除
-        </ion-item-option>
-      </ion-item-options>
-    </ion-item-sliding>
+<ion-content [fullscreen]="true">
+  <ion-list>
+    <ion-item *ngFor="let task of tasks">
+      <ion-checkbox slot="start" 
+                    [(ngModel)]="task.completed"
+                    (ionChange)="toggleComplete(task)">
+      </ion-checkbox>
+      <ion-label>
+        <h2>{{task.title}}</h2>
+        <p>{{task.content}}</p>
+        <p *ngIf="task.startTime">开始时间: {{task.startTime | date}}</p>
+        <p *ngIf="task.endTime">结束时间: {{task.endTime | date}}</p>
+      </ion-label>
+    </ion-item>
   </ion-list>
 
-  <div *ngIf="tasks.length === 0" class="ion-text-center ion-padding">
-    <p>暂无任务,点击右上角添加新任务</p>
-  </div>
-</ion-content>
-
-<ion-modal [isOpen]="isModalOpen">
-  <ng-template>
-    <ion-header>
-      <ion-toolbar color="light">
-        <ion-title>新建任务</ion-title>
-        <ion-buttons slot="end">
-          <ion-button (click)="cancelModal()">取消</ion-button>
-        </ion-buttons>
-      </ion-toolbar>
-    </ion-header>
-
-    <ion-content class="ion-padding">
-      <ion-item>
-        <ion-label position="stacked">任务名称</ion-label>
-        <ion-input [(ngModel)]="newTask.title" placeholder="请输入任务名称"></ion-input>
-      </ion-item>
+  <ion-modal [isOpen]="isModalOpen">
+    <ng-template>
+      <ion-header>
+        <ion-toolbar>
+          <ion-title>新建任务</ion-title>
+          <ion-buttons slot="end">
+            <ion-button (click)="setOpen(false)">关闭</ion-button>
+          </ion-buttons>
+        </ion-toolbar>
+      </ion-header>
+      <ion-content class="ion-padding">
+        <ion-item>
+          <ion-label position="stacked">标题</ion-label>
+          <ion-input [(ngModel)]="newTask.title" placeholder="输入任务标题"></ion-input>
+        </ion-item>
+        
+        <ion-item>
+          <ion-label position="stacked">描述</ion-label>
+          <ion-textarea [(ngModel)]="newTask.content" placeholder="输入任务描述"></ion-textarea>
+        </ion-item>
+        
+        <ion-item>
+          <ion-label position="stacked">开始时间</ion-label>
+          <ion-datetime-button datetime="startTime"></ion-datetime-button>
+          <ion-modal [keepContentsMounted]="true">
+            <ng-template>
+              <ion-datetime id="startTime" [(ngModel)]="newTask.startTime"></ion-datetime>
+            </ng-template>
+          </ion-modal>
+        </ion-item>
 
-      <ion-item>
-        <ion-label position="stacked">开始时间</ion-label>
-        <ion-datetime
-          [(ngModel)]="newTask.startTime"
-          presentation="date-time"
-          locale="zh-CN">
-        </ion-datetime>
-      </ion-item>
+        <ion-item>
+          <ion-label position="stacked">结束时间</ion-label>
+          <ion-datetime-button datetime="endTime"></ion-datetime-button>
+          <ion-modal [keepContentsMounted]="true">
+            <ng-template>
+              <ion-datetime id="endTime" [(ngModel)]="newTask.endTime"></ion-datetime>
+            </ng-template>
+          </ion-modal>
+        </ion-item>
 
-      <ion-item>
-        <ion-label position="stacked">截止时间</ion-label>
-        <ion-datetime
-          [(ngModel)]="newTask.endTime"
-          presentation="date-time"
-          locale="zh-CN">
-        </ion-datetime>
-      </ion-item>
-
-      <ion-button expand="block" (click)="addTask()" class="ion-margin-top">
-        创建任务
-      </ion-button>
-    </ion-content>
-  </ng-template>
-</ion-modal>
+        <ion-button expand="block" (click)="addTask()" class="ion-margin-top">
+          创建任务
+        </ion-button>
+      </ion-content>
+    </ng-template>
+  </ion-modal>
+</ion-content>

+ 54 - 95
src/app/tab1/tab1.page.ts

@@ -1,126 +1,85 @@
 import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { Task } from '../../models/Task';
+import { AuthService } from '../services/auth.service';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { IonicModule } from '@ionic/angular';
-import { addIcons } from 'ionicons';
-import { addOutline, trash } from 'ionicons/icons';
-
-interface Task {
-  id: number;
-  title: string;
-  completed: boolean;
-  startTime: string;
-  endTime: string;
-}
+import * as Parse from 'parse';
 
 @Component({
   selector: 'app-tab1',
-  templateUrl: './tab1.page.html',
-  styleUrls: ['./tab1.page.scss'],
+  templateUrl: 'tab1.page.html',
+  styleUrls: ['tab1.page.scss'],
   standalone: true,
   imports: [
-    IonicModule,
     CommonModule,
-    FormsModule
+    FormsModule,
+    IonicModule
   ]
 })
 export class Tab1Page implements OnInit {
   tasks: Task[] = [];
+  newTask: Task = new Task();
   isModalOpen = false;
-  newTask: Task = {
-    id: 0,
-    title: '',
-    completed: false,
-    startTime: new Date().toISOString(),
-    endTime: new Date().toISOString(),
-  };
 
-  constructor() {
-    // 注册图标
-    addIcons({
-      'add-outline': addOutline,
-      'trash': trash
-    });
-  }
+  constructor(
+    private router: Router,
+    private auth: AuthService
+  ) {}
 
-  ngOnInit() {
-    const savedTasks = localStorage.getItem('tasks');
-    if (savedTasks) {
-      this.tasks = JSON.parse(savedTasks);
-    }
+  async ngOnInit() {
+    await this.loadTasks();
   }
 
-  openAddTaskModal() {
-    this.isModalOpen = true;
-    this.newTask = {
-      id: Date.now(),
-      title: '',
-      completed: false,
-      startTime: new Date().toISOString(),
-      endTime: new Date().toISOString(),
-    };
-  }
-
-  cancelModal() {
-    this.isModalOpen = false;
-  }
-
-  addTask() {
-    if (this.newTask.title.trim()) {
-      this.tasks.push({ ...this.newTask });
-      this.saveTasks();
-      this.isModalOpen = false;
+  async loadTasks() {
+    try {
+      const currentUser = Parse.User.current();
+      if (currentUser) {
+        this.tasks = await Task.getUserTasks(currentUser.id);
+      }
+    } catch (error: any) {
+      console.error('Error loading tasks:', error);
     }
   }
 
-  deleteTask(taskId: number) {
-    this.tasks = this.tasks.filter(task => task.id !== taskId);
-    this.saveTasks();
-  }
-
-  toggleTaskCompletion(task: Task) {
-    task.completed = !task.completed;
-    this.saveTasks();
-  }
-
-  getTaskStatus(task: Task): string {
-    const now = new Date().getTime();
-    const startTime = new Date(task.startTime).getTime();
-    const endTime = new Date(task.endTime).getTime();
-
-    if (task.completed) {
-      return 'success';
-    } else if (now < startTime) {
-      return 'primary';
-    } else if (now >= startTime && now <= endTime) {
-      return 'warning';
-    } else {
-      return 'danger';
+  async addTask() {
+    const currentUser = Parse.User.current();
+    if (!currentUser) {
+      alert('请先登录后再添加任务');
+      return;
     }
-  }
-
-  getTaskStatusText(task: Task): string {
-    const now = new Date().getTime();
-    const startTime = new Date(task.startTime).getTime();
-    const endTime = new Date(task.endTime).getTime();
 
-    if (task.completed) {
-      return '已完成';
-    } else if (now < startTime) {
-      return '未开始';
-    } else if (now >= startTime && now <= endTime) {
-      return '进行中';
-    } else {
-      return '已逾期';
+    try {
+      await Task.create({
+        title: this.newTask.title,
+        content: this.newTask.content,
+        startTime: this.newTask.startTime,
+        endTime: this.newTask.endTime,
+        completed: false,
+        userId: currentUser.id
+      });
+      
+      await this.loadTasks();
+      this.isModalOpen = false;
+      this.newTask = new Task();
+    } catch (error: any) {
+      console.error('Failed to create task:', error);
     }
   }
 
-  getTaskColor(index: number): string {
-    const colors = ['#ffebee', '#e3f2fd', '#e8f5e9', '#fff3e0', '#f3e5f5'];
-    return colors[index % colors.length];
+  setOpen(isOpen: boolean) {
+    this.isModalOpen = isOpen;
   }
 
-  private saveTasks() {
-    localStorage.setItem('tasks', JSON.stringify(this.tasks));
+  async toggleComplete(task: Task) {
+    try {
+      if (!task.id) return;
+      task.completed = !task.completed;
+      await task.save();
+      await this.loadTasks();
+    } catch (error: any) {
+      console.error('Error toggling task completion:', error);
+    }
   }
 }

+ 90 - 49
src/app/tab2/tab2.page.html

@@ -1,61 +1,102 @@
-<ion-header>
+<ion-header [translucent]="true">
   <ion-toolbar>
-    <ion-title>自律计时</ion-title>
+    <ion-title>
+      专注计时
+    </ion-title>
   </ion-toolbar>
 </ion-header>
 
-<ion-content>
-  <div class="timer-container">
-    <!-- 计时器显示 -->
-    <div class="timer-display">
-      <h1>{{ currentTime }}</h1>
-    </div>
+<ion-content [fullscreen]="true" class="ion-padding">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">专注计时</ion-title>
+    </ion-toolbar>
+  </ion-header>
 
-    <!-- 活动类型选择 -->
-    <div class="type-selector">
-      <ion-segment [(ngModel)]="selectedType" *ngIf="!isTimerRunning">
-        <ion-segment-button *ngFor="let type of activityTypes" [value]="type.id">
-          <ion-icon [name]="type.icon"></ion-icon>
-          <ion-label>{{ type.name }}</ion-label>
-        </ion-segment-button>
-      </ion-segment>
-    </div>
+  <!-- 活动类型选择 -->
+  <div class="type-selection">
+    <ion-button 
+      *ngFor="let type of activityTypes"
+      [class.selected]="selectedType === type.id"
+      (click)="selectType(type.id)"
+      fill="outline"
+      class="type-button">
+      <ion-icon [name]="type.icon" slot="start"></ion-icon>
+      {{ type.name }}
+    </ion-button>
+  </div>
+
+  <!-- 计时器显示 -->
+  <div class="timer-display">
+    <h1>{{ currentTime }}</h1>
+  </div>
 
-    <!-- 控制按钮 -->
-    <div class="timer-controls">
-      <ion-button expand="block" (click)="startTimer()" *ngIf="!isTimerRunning" [disabled]="!selectedType">
-        开始计时
-      </ion-button>
-      <ion-button expand="block" color="danger" (click)="stopTimer()" *ngIf="isTimerRunning">
-        结束计时
-      </ion-button>
-    </div>
+  <!-- 控制按钮 -->
+  <div class="control-buttons">
+    <ion-button 
+      *ngIf="!isTimerRunning" 
+      (click)="startTimer()" 
+      [disabled]="!selectedType"
+      expand="block"
+      color="primary"
+      class="control-button">
+      开始
+    </ion-button>
+    <ion-button 
+      *ngIf="isTimerRunning" 
+      (click)="stopTimer()"
+      expand="block"
+      color="danger"
+      class="control-button">
+      停止
+    </ion-button>
+  </div>
 
-    <!-- 倒计时部分 -->
-    <div class="countdown-section">
-      <h2>专注倒计时</h2>
-      <ion-item>
-        <ion-label position="stacked">设置时长(分钟)</ion-label>
-        <ion-input type="number" [(ngModel)]="countdownMinutes" min="1" max="180"></ion-input>
-      </ion-item>
-      <ion-button expand="block" (click)="startCountdown()" [disabled]="!selectedType">
-        开始专注
-      </ion-button>
-    </div>
+  <!-- 倒计时设置 -->
+  <div class="countdown-setting">
+    <ion-card>
+      <ion-card-content>
+        <ion-item lines="none">
+          <ion-label position="stacked">倒计时(分钟)</ion-label>
+          <ion-input 
+            type="number" 
+            [(ngModel)]="countdownMinutes"
+            [disabled]="isTimerRunning"
+            placeholder="请输入时间">
+          </ion-input>
+        </ion-item>
+        <ion-button 
+          (click)="startCountdown()" 
+          [disabled]="!countdownMinutes || isTimerRunning"
+          expand="block"
+          class="countdown-button">
+          开始倒计时
+        </ion-button>
+      </ion-card-content>
+    </ion-card>
   </div>
 
-  <!-- 历史记录 -->
-  <div class="records-container">
-    <h2 class="ion-padding">历史记录</h2>
+  <!-- 记录列表 -->
+  <div class="records-list">
     <ion-list>
-      <ion-item *ngFor="let record of records">
-        <ion-icon slot="start" [name]="getTypeIcon(record.type)"></ion-icon>
-        <ion-label>
-          <h2>{{ getTypeName(record.type) }}</h2>
-          <p>{{ record.startTime | date:'yyyy-MM-dd HH:mm' }}</p>
-          <p>时长: {{ formatDuration(record.duration) }}</p>
-        </ion-label>
-      </ion-item>
+      <ion-list-header>
+        <ion-label>今日记录</ion-label>
+      </ion-list-header>
+      <ion-item-sliding *ngFor="let record of records; let i = index">
+        <ion-item>
+          <ion-icon [name]="getTypeIcon(record.type)" slot="start" class="record-icon"></ion-icon>
+          <ion-label>
+            <h2>{{ getTypeName(record.type) }}</h2>
+            <p>{{ formatDuration(record.duration) }}</p>
+          </ion-label>
+        </ion-item>
+        
+        <ion-item-options side="end">
+          <ion-item-option color="danger" (click)="deleteRecord(i)">
+            <ion-icon slot="icon-only" name="trash"></ion-icon>
+          </ion-item-option>
+        </ion-item-options>
+      </ion-item-sliding>
     </ion-list>
   </div>
-</ion-content>
+</ion-content>

+ 65 - 67
src/app/tab2/tab2.page.scss

@@ -1,99 +1,97 @@
-.timer-container {
-  padding: 20px;
-  text-align: center;
+.type-selection {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+  margin: 20px 0;
+
+  .type-button {
+    min-width: 120px;
+    
+    &.selected {
+      --background: var(--ion-color-primary);
+      --color: var(--ion-color-primary-contrast);
+    }
+  }
 }
 
 .timer-display {
-  margin: 30px 0;
+  text-align: center;
+  margin: 40px 0;
   
   h1 {
-    font-size: 48px;
-    font-weight: 300;
+    font-size: 4rem;
+    font-weight: bold;
     color: var(--ion-color-primary);
+    margin: 0;
+    font-family: monospace;
   }
 }
 
-.type-selector {
-  margin-bottom: 20px;
-  
-  ion-segment {
-    background: var(--ion-color-light);
-    border-radius: 8px;
-    
-    ion-segment-button {
-      --padding-top: 8px;
-      --padding-bottom: 8px;
-      
-      ion-icon {
-        font-size: 24px;
-        margin-bottom: 4px;
-      }
-      
-      ion-label {
-        font-size: 12px;
-      }
-    }
+.control-buttons {
+  margin: 20px auto;
+  max-width: 300px;
+
+  .control-button {
+    margin: 10px 0;
+    --border-radius: 25px;
+    height: 50px;
+    font-size: 1.2rem;
   }
 }
 
-.timer-controls {
+.countdown-setting {
   margin: 20px 0;
   
-  ion-button {
-    height: 48px;
-    --border-radius: 24px;
-    font-weight: 500;
-  }
-}
+  ion-card {
+    margin: 0 10px;
+    border-radius: 15px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 
-.countdown-section {
-  margin-top: 20px;
-  padding: 0 20px;
-  
-  h2 {
-    font-size: 18px;
-    color: var(--ion-color-dark);
-    margin-bottom: 16px;
-  }
-  
-  ion-item {
-    --background: var(--ion-color-light);
-    border-radius: 8px;
-    margin-bottom: 16px;
+    ion-card-content {
+      padding: 16px;
+    }
   }
-  
-  ion-button {
-    margin-top: 8px;
+
+  .countdown-button {
+    margin-top: 16px;
+    --border-radius: 10px;
   }
 }
 
-.records-container {
-  h2 {
-    margin: 0;
-    font-size: 18px;
-    font-weight: 500;
-    color: var(--ion-color-dark);
+.records-list {
+  margin-top: 20px;
+
+  ion-list-header {
+    font-size: 1.1rem;
+    font-weight: 600;
+    padding-top: 20px;
   }
-  
+
   ion-item {
     --padding-start: 16px;
     --inner-padding-end: 16px;
-    
-    ion-icon {
-      font-size: 24px;
-      color: var(--ion-color-primary);
-    }
-    
+    --background: transparent;
+
     h2 {
-      font-size: 16px;
       font-weight: 500;
       margin-bottom: 4px;
     }
-    
+
     p {
-      font-size: 14px;
       color: var(--ion-color-medium);
-      margin: 2px 0;
     }
   }
+
+  .record-icon {
+    font-size: 24px;
+    color: var(--ion-color-primary);
+  }
+}
+
+// 深色模式适配
+@media (prefers-color-scheme: dark) {
+  .timer-display h1 {
+    color: var(--ion-color-primary-tint);
+  }
 }

+ 30 - 2
src/app/tab2/tab2.page.ts

@@ -18,6 +18,7 @@ import {
   trashOutline,
   addOutline
 } from 'ionicons/icons';
+import { AlertController } from '@ionic/angular';
 
 interface TimerRecord {
   id: number;
@@ -57,7 +58,10 @@ export class Tab2Page {
   timer: any;
   countdownMinutes: number = 25; // 默认25分钟
 
-  constructor(private router: Router) {
+  constructor(
+    private router: Router,
+    private alertController: AlertController
+  ) {
     // 注册图标
     addIcons({
       'school-outline': schoolOutline,
@@ -168,4 +172,28 @@ export class Tab2Page {
     const mins = minutes % 60;
     return hours > 0 ? `${hours}小时${mins}分钟` : `${mins}分钟`;
   }
-}
+
+  async deleteRecord(index: number) {
+    // 创建一个警告对话框
+    const alert = await this.alertController.create({
+      header: '确认删除',
+      message: '你确定要删除这条记录吗?',
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel',
+        },
+        {
+          text: '删除',
+          role: 'destructive',
+          handler: () => {
+            // 从数组中删除指定索引的记录
+            this.records.splice(index, 1);
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+}

+ 10 - 4
src/app/tab3/tab3.page.html

@@ -4,8 +4,14 @@
   </ion-toolbar>
 </ion-header>
 
-<ion-content>
-  <div class="chart-container">
-    <canvas #barCanvas></canvas>
-  </div>
+<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>

+ 1 - 1
src/app/tab3/tab3.page.scss

@@ -4,4 +4,4 @@
   justify-content: center;
   align-items: center;
   height: 100%;
-}
+}

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

@@ -1,8 +1,8 @@
 import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
 import { IonicModule } from '@ionic/angular';
+import { CommonModule } from '@angular/common';
 import { Chart, registerables } from 'chart.js';
+import { FocusDataService } from '../services/focus-data.service';
 
 Chart.register(...registerables);
 
@@ -11,82 +11,48 @@ Chart.register(...registerables);
   templateUrl: './tab3.page.html',
   styleUrls: ['./tab3.page.scss'],
   standalone: true,
-  imports: [IonicModule, CommonModule, FormsModule]
+  imports: [IonicModule, CommonModule]
 })
 export class Tab3Page implements OnInit {
-  @ViewChild('barCanvas') private barCanvas!: ElementRef;
-  barChart: any;
+  @ViewChild('focusChart', { static: true }) focusChart!: ElementRef;
+  chart: any;
+  focusRecords: { date: string, duration: number }[] = [];
 
-  constructor() { }
+  constructor(private focusDataService: FocusDataService) {}
 
   ngOnInit() {
+    this.focusRecords = this.focusDataService.getFocusRecords();
+    console.log('Focus Records in Tab3:', this.focusRecords); // 检查数据是否正确获取
+    this.loadChartData();
   }
 
-  ionViewDidEnter() {
-    this.loadData();
-  }
-
-  loadData() {
-    const tab1Records = JSON.parse(localStorage.getItem('tasks') || '[]');
-    const tab2Records = JSON.parse(localStorage.getItem('timerRecords') || '[]');
-
-    const combinedRecords = [...tab1Records, ...tab2Records];
-    const activityDurations: { [key: string]: number } = {};
-
-    combinedRecords.forEach(record => {
-      const type = record.type;
-      const duration = record.duration || 0;
-      if (activityDurations[type]) {
-        activityDurations[type] += duration;
-      } else {
-        activityDurations[type] = duration;
-      }
-    });
-
-    const translatedLabels = this.translateLabels(Object.keys(activityDurations));
-
-    this.createBarChart(
-      translatedLabels,
-      Object.values(activityDurations)
-    );
-  }
+  loadChartData() {
+    const labels = this.focusRecords.map(record => record.date);
+    const dataValues = this.focusRecords.map(record => record.duration);
 
-  translateLabels(labels: string[]): string[] {
-    const translations: { [key: string]: string } = {
-      'study': '学习',
-      'work': '工作',
-      'sleep': '睡眠',
-      'sport': '运动',
-      'reading': '阅读',
-      'coding': '编程',
-      'meditation': '冥想',
-      'music': '音乐',
-      'language': '语言',
-      'writing': '写作'
+    const data = {
+      labels: labels,
+      datasets: [{
+        label: 'Focus Time (minutes)',
+        data: dataValues,
+        backgroundColor: 'rgba(75, 192, 192, 0.2)',
+        borderColor: 'rgba(75, 192, 192, 1)',
+        borderWidth: 1
+      }]
     };
 
-    return labels.map(label => translations[label] || label);
+    this.createChart(data);
   }
 
-  createBarChart(labels: string[], data: number[]) {
-    if (this.barChart) {
-      this.barChart.destroy();
+  createChart(data: any) {
+    if (this.chart) {
+      this.chart.destroy();
     }
 
-    this.barChart = new Chart(this.barCanvas.nativeElement, {
+    this.chart = new Chart(this.focusChart.nativeElement, {
       type: 'bar',
-      data: {
-        labels: labels,
-        datasets: [{
-          label: '活动时长(分钟)',
-          data: data,
-          backgroundColor: 'rgba(54, 162, 235, 0.2)',
-          borderColor: 'rgba(54, 162, 235, 1)',
-          borderWidth: 1
-        }]
-      },
+      data: data,
       options: {
-        responsive: true,
         scales: {
           y: {
             beginAtZero: true

+ 77 - 0
src/app/tab4/tab4.page.html

@@ -0,0 +1,77 @@
+<ion-header [translucent]="true">
+  <ion-toolbar class="custom-toolbar">
+    <ion-title class="custom-title">
+      我的
+    </ion-title>
+  </ion-toolbar>
+</ion-header>
+<ion-content [fullscreen]="true">
+ 
+  <!-- 用户登录状态 -->
+  <ion-card>
+    <!-- 未登录 -->
+     @if(!currentUser?.id){
+        <ion-content>
+          <ion-card class="login-card">
+              <ion-card-header>
+                  <ion-card-title>请登录</ion-card-title>
+                  <ion-card-subtitle>暂无信息</ion-card-subtitle>
+                  <ion-card-content>欢迎来到"生活智伴"!在这里,我们致力于成为您生活中的智慧伙伴。通过科学的生活规划和健康管理,让您的每一天都充满活力与平衡。我们提供个性化的生活建议,帮助您建立健康的生活方式,享受高质量的生活。让我们一起探索智慧生活的美好,开启健康快乐的人生旅程!登录后,您将获得更多贴心的生活建议和个性化服务。现在就加入我们,让生活更有智慧,让每一天都更加美好!</ion-card-content>
+              </ion-card-header>
+              <div class="image-container">
+                <img src="assets/images/lifepartner.jpg" alt="" class="responsive-image">
+            </div>
+          </ion-card>
+      </ion-content>
+      }
+        <!-- 已登录 -->
+     @if(currentUser?.id){
+      <ion-card-header class="card-header">
+        <img [src]="currentUser?.get('avatar')" onerror="this.src='https://app.fmode.cn/dev/jxnu/202226701019/头像示例.png';" alt="图片加载失败" class="avatar" />
+        <div class="user-info">
+            <ion-card-title>账号:{{currentUser?.get("username")}}</ion-card-title>
+            <ion-card-subtitle>
+                姓名: {{currentUser?.get("realname") || "-"}} 
+                性别: {{currentUser?.get("gender") || "-"}} 
+                年龄: {{currentUser?.get("age") || "-"}}
+            </ion-card-subtitle>
+        </div>
+    </ion-card-header>
+      }
+      <ion-card-content>
+      @if(!currentUser?.id){
+        <ion-button expand="block" (click)="signup()" color="success">注册</ion-button>
+        <ion-button expand="block" (click)="login()" color="success">登录</ion-button>
+      }
+     @if(currentUser?.id){
+      <ion-button expand="block" (click)="editUser()" color="success">编辑资料</ion-button>
+      <ion-button expand="block" (click)="logout()" color="medium">登出</ion-button>
+    }
+    </ion-card-content>
+  </ion-card>
+  @if(currentUser?.id){
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>个性头像生成器</ion-card-title>
+        <ion-card-subtitle>点击创建个性化头像</ion-card-subtitle>
+      </ion-card-header>
+      <ion-card-content>
+        <ion-button expand="block" (click)="goToAvatar()" color="success">前往生成</ion-button>
+      </ion-card-content>
+    </ion-card>
+  }
+  @if(currentUser?.id){
+  <ion-card class="memo-card">
+    <h2 class="memo-title">健康备忘录</h2>
+    <p class="memo-description">写下您问诊的医生名或者心动的科普知识,便于您下次查找(点击标签可删除)</p>
+
+    <h2 class="memo-title">收藏夹</h2>
+    <ul class="tag-list">
+        @for(tag of editTags; track tag;){
+            <li class="tag-item">{{tag}}</li>
+        }
+    </ul>
+  </ion-card>
+  }
+
+</ion-content>

+ 125 - 0
src/app/tab4/tab4.page.scss

@@ -0,0 +1,125 @@
+.custom-toolbar {
+    --background: rgba(255, 255, 255, 0.8); /* 使工具栏背景透明 */
+    display: flex; /* 使用 Flexbox 布局 */
+    justify-content: center; /* 水平居中 */
+    align-items: center; /* 垂直居中 */
+    padding: 0; /* 去掉默认内边距 */
+}
+
+.custom-title {
+    font-size: 1.3em; /* 字体大小 */
+    font-weight: bold; /* 加粗 */
+    color: #000000; 
+    text-align: center; /* 文字居中对齐 */
+    margin: 0; /* 去掉默认外边距 */
+    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); /* 添加文字阴影效果 */
+    /* 添加其他美化效果 */
+    font-family: "微软雅黑"; /* 自定义字体 */
+}
+
+ion-card {
+    background-color: #e0f7fa; /* 浅蓝色背景,给人以清新和健康的感觉 */
+    border-radius: 10px; /* 圆角边框 */
+    padding: 20px; /* 内边距 */
+    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 轻微的阴影效果 */
+}
+
+ion-card-title {
+    font-size: 1.5em; /* 标题字体大小 */
+    font-weight: bold; /* 加粗 */
+    color: #00796b; /* 深绿色字体,象征健康 */
+    margin: 0; /* 去掉默认的外边距 */
+}
+
+ion-card-subtitle {
+    font-size: 1.2em; /* 副标题字体大小 */
+    color: #004d40; /* 更深的绿色字体 */
+    margin-top: 5px; /* 顶部外边距 */
+}
+
+ion-card:hover {
+    background-color: #b2ebf2; /* 悬停时的背景色变化 */
+    transition: background-color 0.3s; /* 背景色变化的过渡效果 */
+}
+.memo-card {
+    background-color: #e0f7fa; /* 浅蓝色背景,给人以清新和健康的感觉 */
+    border-radius: 10px; /* 圆角边框 */
+    padding: 20px; /* 内边距 */
+    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 轻微的阴影效果 */
+}
+
+.memo-title {
+    font-size: 1.8em; /* 标题字体大小 */
+    font-weight: bold; /* 加粗 */
+    color: #00796b; /* 深绿色字体,象征健康 */
+    margin: 15px 0; /* 顶部和底部外边距 */
+}
+
+.memo-description {
+    font-size: 1.1em; /* 描述字体大小 */
+    color: #004d40; /* 更深的绿色字体 */
+    margin-bottom: 20px; /* 底部外边距 */
+}
+
+.tag-list {
+    list-style-type: none; /* 去掉默认的列表样式 */
+    padding: 0; /* 去掉内边距 */
+}
+
+.tag-item {
+    background-color: #b2ebf2; /* 标签背景色 */
+    color: #00796b; /* 标签字体颜色 */
+    border-radius: 5px; /* 标签圆角 */
+    padding: 10px; /* 标签内边距 */
+    margin: 5px 0; /* 标签外边距 */
+    transition: background-color 0.3s; /* 背景色���化的过渡效果 */
+    cursor: pointer; /* 鼠标悬停时显示为可点击 */
+}
+
+.tag-item:hover {
+    background-color: #80deea; /* 悬停时的背景色变化 */
+}
+.card-header {
+    display: flex; /* 使用 Flexbox 布局 */
+    align-items: center; /* 垂直居中对齐 */
+    padding: 10px; /* 内边距 */
+}
+
+.avatar {
+    width: 50px; /* 头像宽度 */
+    height: 50px; /* 头像高度 */
+    border-radius: 50%; /* 圆形头像 */
+    margin-right: 15px; /* 头像与文本之间的间距 */
+    object-fit: cover; /* 确保图片覆盖区域并保持比例 */
+}
+
+.user-info {
+    flex: 1; /* 使用户信息部分占据剩余空间 */
+}
+
+ion-content {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 68vh; /* 使内容区域高度为视口高度 */
+}
+
+.login-card {
+    width: 94%; /* 可以根据需要调整宽度 */
+    max-width: 400px; /* 设置最大宽度以避免过宽 */
+    text-align: center; /* 文本居中 */
+    padding: 10px; /* 添加内边距 */
+}
+
+.image-container {
+    width: 100%; /* 图片容器宽度100% */
+    display: flex; /* 使用flex布局 */
+    justify-content: center; /* 水平居中 */
+    margin-top: 10px; /* 上方间距 */
+}
+
+.responsive-image {
+    width: 100%;
+    height: auto;
+    object-fit: cover;
+}

+ 18 - 0
src/app/tab4/tab4.page.spec.ts

@@ -0,0 +1,18 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Tab4Page } from './tab4.page';
+
+describe('Tab4Page', () => {
+  let component: Tab4Page;
+  let fixture: ComponentFixture<Tab4Page>;
+
+  beforeEach(async () => {
+    fixture = TestBed.createComponent(Tab4Page);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 99 - 0
src/app/tab4/tab4.page.ts

@@ -0,0 +1,99 @@
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, AlertController } from '@ionic/angular/standalone';
+
+import { Router } from '@angular/router';
+import { openUserLoginModal } from '../../lib/user/modal-user-login/modal-user-login.component';
+import { CloudUser } from '../../lib/ncloud';
+import { openUserEditModal } from '../../lib/user/modal-user-edit/modal-user-edit.component';
+import * as Parse from 'parse';
+import { AuthService } from '../services/auth.service';
+
+@Component({
+  selector: 'app-tab4',
+  templateUrl: 'tab4.page.html',
+  styleUrls: ['tab4.page.scss'],
+  standalone: true,
+  imports: [
+    IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard, IonCardContent, IonButton, IonCardHeader, 
+    IonCardTitle, IonCardSubtitle,
+  ],
+})
+export class Tab4Page {
+  goToCollection(){
+    console.log("goToCollection");
+  }
+
+  goToAvatar(){
+    console.log(['route'])
+    this.router.navigate(['/tabs/picture'])
+  }
+
+  currentUser:CloudUser|undefined
+  constructor(
+    private router: Router,
+    private modalCtrl:ModalController,
+    private alertController: AlertController,
+    private auth: AuthService
+  ) {
+    this.currentUser = new CloudUser();
+  }
+
+  async login(){
+    try {
+      let user = await openUserLoginModal(this.modalCtrl);
+      if(user?.id){
+        this.currentUser = user;
+        this.auth.setCurrentUser(user);
+      } else {
+        // 登录失败时显示提示
+        const alert = await this.alertController.create({
+          header: '登录失败',
+          message: '用户名或密码错误,请重试',
+          buttons: ['确定']
+        });
+        await alert.present();
+      }
+    } catch (error) {
+      // 处理其他错误
+      const alert = await this.alertController.create({
+        header: '登录错误',
+        message: '登录过程中发生错误,请稍后重试',
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
+
+  async signup(){
+    // 弹出注册窗口
+    let user = await openUserLoginModal(this.modalCtrl,"signup");
+    if(user?.id){
+      this.currentUser = user
+    }
+  }
+
+  logout(){
+    this.currentUser?.logout();
+    this.auth.setCurrentUser(null);
+  }
+
+  editUser(){
+    openUserEditModal(this.modalCtrl)
+  }
+
+  editTags:Array<String>=[]
+  async setTagsValue(ev:any){
+    let currentUser = new CloudUser();
+    let userPrompt = ``
+    if(!currentUser?.id){
+      console.log("用户未登录,请登录后重试");
+      let user = await openUserLoginModal(this.modalCtrl);
+      if(!user?.id){
+        return
+      }
+      currentUser = user;
+    }
+    this.editTags=ev;
+  }
+}

+ 4 - 0
src/app/tabs/tabs.page.html

@@ -14,5 +14,9 @@
       <ion-icon name="stats-chart-outline"></ion-icon>
       <ion-label>数据统计</ion-label>
     </ion-tab-button>
+    <ion-tab-button tab="tab4" href="/tabs/tab4">
+      <ion-icon aria-hidden="true" name="square"></ion-icon>
+      <ion-label>我的</ion-label>
+    </ion-tab-button>
   </ion-tab-bar>
 </ion-tabs>

+ 6 - 2
src/app/tabs/tabs.routes.ts

@@ -8,8 +8,7 @@ export const routes: Routes = [
     children: [
       {
         path: 'tab1',
-        loadComponent: () =>
-          import('../tab1/tab1.page').then((m) => m.Tab1Page),
+        loadComponent: () => import('../tab1/tab1.page').then(m => m.Tab1Page)
       },
       {
         path: 'tab2',
@@ -21,6 +20,11 @@ export const routes: Routes = [
         loadComponent: () =>
           import('../tab3/tab3.page').then((m) => m.Tab3Page),
       },
+      {
+        path: 'tab4',
+        loadComponent: () =>
+          import('../tab4/tab4.page').then((m) => m.Tab4Page),
+      },
       {
         path: '',
         redirectTo: '/tabs/tab1',

+ 389 - 0
src/lib/ncloud.ts

@@ -0,0 +1,389 @@
+// CloudObject.ts
+export class CloudObject {
+    className: string;
+    id: string | null = null;
+    createdAt:any;
+    updatedAt:any;
+    data: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    toPointer() {
+        return { "__type": "Pointer", "className": this.className, "objectId": this.id };
+    }
+
+    set(json: Record<string, any>) {
+        Object.keys(json).forEach(key => {
+            if (["objectId", "id", "createdAt", "updatedAt", "ACL"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}`;
+
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "content-type": "application/json;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        return this;
+    }
+
+    async destroy() {
+        if (!this.id) return;
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${this.id}`, {
+            headers: {
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "DELETE",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result) {
+            this.id = null;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+    // 作用是将查询参数转换为对象
+    include(...fileds:string[]) {
+        this.queryParams["include"] = fileds;
+    }
+    greaterThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = `http://dev.fmode.cn:1337/parse/classes/${this.className}/${id}?`;
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        // return json || {};
+        const exists = json?.results?.[0] || null;
+        if (exists) {
+            let existsObject = this.dataToObj(exists)
+            return existsObject;
+        }
+        return null
+
+    }
+
+    async find() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key=>{
+            let paramStr = JSON.stringify(this.queryParams[key]); // 作用是将对象转换为JSON字符串
+            if(key=="include"){
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if(key=="where"){
+                paramStr = JSON.stringify(this.queryParams[key]);
+
+            }
+            if(queryStr) {
+                url += `${key}=${paramStr}`;
+            }else{
+                url += `&${key}=${paramStr}`;
+            }
+        })
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        let list = json?.results || []
+        let objList = list.map((item:any)=>this.dataToObj(item))
+        return objList || [];
+    }
+
+
+    async first() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        // const exists = json?.results?.[0] || null;
+        // if (exists) {
+        //     let existsObject = this.dataToObj(exists)
+        //     return existsObject;
+        // }
+        // return null
+        let list = json?.results || []
+        let objList = list.map((item:any)=>this.dataToObj(item))
+        return objList || [];
+    }
+
+    dataToObj(exists:any):CloudObject{
+        let existsObject = new CloudObject(this.className);
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}
+
+// CloudUser.ts
+export class CloudUser extends CloudObject {
+    constructor() {
+        super("_User"); // 假设用户类在Parse中是"_User"
+        // 读取用户缓存信息
+        let userCacheStr = localStorage.getItem("NCloud/dev/User")
+        if(userCacheStr){
+            let userData = JSON.parse(userCacheStr)
+            // 设置用户信息
+            this.id = userData?.objectId;
+            this.sessionToken = userData?.sessionToken;
+            this.data = userData; // 保存用户数据
+        }
+    }
+
+    sessionToken:string|null = ""
+    /** 获取当前用户信息 */
+    async current() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return null;
+        }
+        return this;
+        // const response = await fetch(`http://dev.fmode.cn:1337/parse/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
+    }
+
+    /** 登录 */
+    async login(username: string, password: string):Promise<CloudUser|null> {
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/login`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify({ username, password }),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+        
+        // 设置用户信息
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/logout`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "x-parse-session-token": this.sessionToken
+            },
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return false;
+        }
+
+        // 清除用户信息
+        localStorage.removeItem("NCloud/dev/User")
+        this.id = null;
+        this.sessionToken = null;
+        this.data = {};
+        return true;
+    }
+
+    /** 注册 */
+    async signUp(username: string, password: string, additionalData: Record<string, any> = {}) {
+        const userData = {
+            username,
+            password,
+            ...additionalData // 合并额外的用户数据
+        };
+
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/users`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify(userData),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+
+    override async save() {
+        let method = "POST";
+        let url = `http://dev.fmode.cn:1337/parse/users`;
+    
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+    
+        let data:any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions:any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+    
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(this.data))
+        return this;
+    }
+}

+ 29 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,29 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      用户名:{{currentUser?.get("username")}}
+    </ion-card-title>
+    <ion-card-subtitle>请输入您的详细资料</ion-card-subtitle>
+   </ion-card-header>
+ <ion-card-content>
+
+   <ion-item>
+     <ion-input [value]="userData['realname']" (ionChange)="userDataChange('realname',$event)" label="姓名" placeholder="请您输入真实姓名"></ion-input>
+   </ion-item>
+   <ion-item>
+     <ion-input type="number" [value]="userData['age']" (ionChange)="userDataChange('age',$event)" label="年龄" placeholder="请您输入年龄"></ion-input>
+    </ion-item>
+  <ion-item>
+     <ion-input [value]="userData['gender']" (ionChange)="userDataChange('gender',$event)" label="性别" placeholder="请您输入男/女"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$event)" label="头像" placeholder="请您输入头像地址(地址错误则会显示默认头像)"></ion-input>
+     </ion-item>
+
+   <ion-button expand="block" (click)="save()">保存</ion-button>
+   <ion-button expand="block" (click)="cancel()">取消</ion-button>
+ 
+
+</ion-card-content>
+</ion-card>

+ 0 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.scss


+ 22 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserEditComponent } from './modal-user-edit.component';
+
+describe('ModalUserEditComponent', () => {
+  let component: ModalUserEditComponent;
+  let fixture: ComponentFixture<ModalUserEditComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserEditComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserEditComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 66 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -0,0 +1,66 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from '../../ncloud';
+
+@Component({
+  selector: 'app-modal-user-edit',
+  templateUrl: './modal-user-edit.component.html',
+  styleUrls: ['./modal-user-edit.component.scss'],
+  standalone: true,
+  imports: [
+    IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserEditComponent  implements OnInit {
+
+  currentUser:CloudUser|undefined
+  userData:any = {}
+  userDataChange(key:string,ev:any){
+    let value = ev?.detail?.value
+    if(value){
+      this.userData[key] = value
+    }
+  }
+  constructor(private modalCtrl:ModalController) { 
+    this.currentUser = new CloudUser();
+    this.userData = this.currentUser.data;
+  }
+
+  ngOnInit() {}
+
+  async save(){
+    Object.keys(this.userData).forEach(key=>{
+      if(key=="age"){
+        this.userData[key] = Number(this.userData[key])
+      }
+    })
+
+    this.currentUser?.set(this.userData)
+    await this.currentUser?.save()
+    this.modalCtrl.dismiss(this.currentUser,"confirm")
+  }
+  cancel(){
+    this.modalCtrl.dismiss(null,"cancel")
+
+  }
+}
+
+export async function openUserEditModal(modalCtrl:ModalController):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserEditComponent,
+    breakpoints:[0.7,1.0],
+    initialBreakpoint:0.7
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 36 - 0
src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,36 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      <ion-segment [value]="type" (ionChange)="typeChange($event)">
+        <ion-segment-button value="login">
+          <ion-label>登录</ion-label>
+        </ion-segment-button>
+        <ion-segment-button value="signup">
+          <ion-label>注册</ion-label>
+        </ion-segment-button>
+      </ion-segment>
+    </ion-card-title>
+    <ion-card-subtitle>请输入账号密码</ion-card-subtitle>
+  </ion-card-header>
+  <ion-card-content>
+    <ion-item>
+      <ion-input [value]="username" (ionChange)="usernameChange($event)" label="账号" placeholder="请您输入账号/手机号"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="password" (ionChange)="passwordChange($event)" label="密码" type="password" value="password"></ion-input>
+    </ion-item>
+
+    @if(type=="signup"){
+      <ion-item>
+        <ion-input [value]="password2" (ionChange)="password2Change($event)" label="密码二次" type="password" value="password"></ion-input>
+      </ion-item>
+    }
+    @if(type=="login"){
+      <ion-button expand="block" (click)="login()">登录</ion-button>
+    }
+    @if(type=="signup"){
+      <ion-button expand="block" (click)="signup()">注册</ion-button>
+    }
+  </ion-card-content>
+</ion-card>

+ 0 - 0
src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
src/lib/user/modal-user-login/modal-user-login.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserLoginComponent } from './modal-user-login.component';
+
+describe('ModalUserLoginComponent', () => {
+  let component: ModalUserLoginComponent;
+  let fixture: ComponentFixture<ModalUserLoginComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserLoginComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserLoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 96 - 0
src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -0,0 +1,96 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from '../../ncloud';
+
+@Component({
+  selector: 'app-modal-user-login',
+  templateUrl: './modal-user-login.component.html',
+  styleUrls: ['./modal-user-login.component.scss'],
+  standalone: true,
+  imports: [
+    IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserLoginComponent  implements OnInit {
+  @Input()
+  type:"login"|"signup" = "login"
+  typeChange(ev:any){
+    this.type = ev?.detail?.value || ev?.value || 'login'
+  }
+  username:string = ""
+  usernameChange(ev:any){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) {
+    console.log(this.type)
+   }
+
+  ngOnInit() {}
+
+  async login(){
+    if(!this.username || !this.password){
+      console.log("请输入完整")
+      return
+    }
+    let user:any = new CloudUser();
+    user = await user.login(this.username,this.password);
+    if(user?.id){
+       this.modalCtrl.dismiss(user,"confirm") // 
+       console.log("登录成功")
+    }else{
+      console.log("登录失败")
+    }
+  }
+
+  async signup(){
+    if(!this.username || !this.password || !this.password2){
+      console.log("请输入完整")
+      return
+    }
+    if(this.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 3 - 0
tsconfig.json

@@ -3,6 +3,9 @@
   "compileOnSave": false,
   "compilerOptions": {
     "baseUrl": "./src",
+    "paths": {
+      "src/*": ["*"]
+    },
     "outDir": "./dist/out-tsc",
     "forceConsistentCasingInFileNames": true,
     "strict": true,