Ver Fonte

feat:完善了打卡页面。

xukang há 3 meses atrás
pai
commit
d3c72299fe

+ 1 - 1
TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.html

@@ -59,7 +59,7 @@
           <!-- 右侧按钮 -->
 
           <ion-col size="6">
-            <ion-button expand="full" (click)="saveChanges()" style="--background: #32548f;" shape="round"
+            <ion-button expand="full" (click)="saveChanges()" style="--background: #009b7d;" shape="round"
               class="confirm-btn">
               <ion-icon name="checkmark-circle-outline"></ion-icon>
               保存修改

+ 30 - 5
TFPower-app/src/app/tab2/tab2.page.html

@@ -51,22 +51,47 @@
           </div>
         </ion-card-content>
       </ion-card>
+
       <ion-card>
         <ion-card-header>
           <ion-card-title>打卡区域</ion-card-title>
+          <div class="power-label">
+            <strong>我的动能</strong>
+            <p class="stat-value">{{ user.get('power') }}</p>
+          </div>
         </ion-card-header>
         <ion-card-content>
-
           <ion-datetime [value]="realDate.toISOString()" (ionChange)="onDateChange($event)">
           </ion-datetime>
           <div class="card-info">
-            <p><strong>已打卡天数:</strong>{{ user.get('days') }}天</p>
-            <p><strong>连续打卡天数:</strong>{{ user.get('sucdays') }}天</p>
-            <p><strong>打卡日期:</strong>{{user.get('checkeddays')}}</p>
-            <ion-button expand="full" (click)="markAttendance()">打卡</ion-button>
+            <ion-button [disabled]="getButtonState(realDate).isDisabled"
+              (click)="getButtonState(realDate).buttonText === '补签' ? handleMakeupClick() : markAttendance()"
+              class="check">
+              {{ getButtonState(realDate).buttonText }}
+            </ion-button>
+          </div>
+          <div class="card-stats">
+            <div class="stat-item">
+              <p><strong>已打卡天数</strong></p>
+
+              <p class="stat-value">{{ user.get('days') }}</p>
+
+            </div>
+            <div class="stat-item">
+              <p><strong>连续打卡天数</strong></p>
+
+              <p class="stat-value">{{ user.get('sucdays') }}</p>
+
+            </div>
           </div>
+
         </ion-card-content>
       </ion-card>
+
+
+
+
+
       <ion-card>
         <!-- 未登录 -->
         @if(!currentUser?.id){

+ 70 - 11
TFPower-app/src/app/tab2/tab2.page.scss

@@ -16,7 +16,7 @@ ion-icon {
   margin-bottom: 5px;
 }
 
-ion-segment-button::part(indicator-background) {
+ion-segment-button.md::part(indicator-background) {
   background: #719e8c;
 }
 
@@ -46,7 +46,8 @@ ion-content {
 //表头
 .grid-header {
   font-weight: bold;
-  background-color: rgb(50, 84, 143);
+  background-color: #6dbdac;
+  color: rgb(7, 6, 6);
   text-align: center;
    display: flex;
     justify-content: center;
@@ -99,7 +100,7 @@ ion-content {
 .section-title {
   font-size: 20px;
   font-weight: bold;
-  color: #333;
+  color: rgb(7, 6, 6);
   margin-bottom: 20px;
 }
 @media (max-width: 1000px) {
@@ -219,16 +220,15 @@ ion-content {
 }
 .reverse
 {
-  --background: #32548f;
+  --background: #009b7d;
   --color: white;
   --border-radius: 15px;
-  --border-color: #000;
+  --border-color: rgb(28, 145, 57);
   --border-style: solid;
   --border-width: 1px;
   --box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
   --ripple-color: deeppink;
-  --padding-top: 10px;
-  --padding-bottom: 10px;
+ 
 }
 .plan-column {
   transition: background-color 0.3s ease, box-shadow 0.3s ease;  /* 平滑过渡效果 */
@@ -278,16 +278,75 @@ ion-content {
   flex: 1; /* 使信息部分占满剩余空间 */
 }
 
-.checkin .card-info p {
-  margin: 5px 0;
-}
+
 .datetime-container {
   display: flex;
   flex-direction: column;
   align-items: center;
-  margin-bottom: 20px;
 }
+/* 卡片内信息区域 */
+.card-info {
+  display: flex;
+  flex-direction: column; /* 垂直排列,按钮在上,统计信息在下 */
+  align-items: center;  /* 横向居中 */
+  justify-content: center; /* 纵向居中 */
+  gap: 20px;  /* 设置按钮和统计信息的间隔 */
+}
+
+/* 圆形打卡按钮 */
+.checkin ion-button {
 
+  --background: #009b7d;
+  --color: white;
+  --border-color: rgb(28, 145, 57);
+  --border-style: solid;
+  --border-width: 1px;
+  --box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
+  --ripple-color: deeppink;
+   width: 100px; 
+  height: 100px; 
+  font-size: 18px;
+  --border-radius: 50%; 
+  display: flex;
+  align-items: center;
+  justify-content: center;
+   --padding-start: 16px;
+  --padding-end: 16px;
+  --padding-top: 12px;
+  --padding-bottom: 12px;
+}
+
+
+.circle-btn:disabled {
+  background-color: #dcdcdc;
+  color: #aaa;
+}
+
+/* 卡片统计信息区域 */
+.card-stats {
+  display: flex;
+  justify-content: space-evenly;
+}
+
+/* 每个统计项 */
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* 统计值 */
+ion-card .power-label {
+  position: absolute;
+  top: 5px;
+  right: 30px;
+  font-size: 14px !important; /* 使用 !important 强制应用样式 */
+}
+
+.checkin .stat-value {
+  font-size: 24px !important;  /* 使用 !important 强制应用样式 */
+  color: #000000;
+}
 ion-card {
     border-radius: 15px; /* 圆角边框 */
     background-color: #f9f9f9; /* 卡片内部背景颜色 */

+ 136 - 31
TFPower-app/src/app/tab2/tab2.page.ts

@@ -4,7 +4,7 @@ import { addIcons } from 'ionicons';
 import { checkmarkCircle, sunny, infiniteOutline, alertCircleOutline, bicycleOutline, logoGitlab, trash, calendar, helpCircle, create } from 'ionicons/icons';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
-import { IonDatetime, IonSelect, IonItemOptions, IonThumbnail, IonCardSubtitle, IonImg, IonCard, IonButtons, IonItem, IonList, IonHeader, IonIcon, IonToolbar, IonContent, IonSegment, IonSegmentButton, IonGrid, IonRow, IonCol, IonButton, IonLabel, IonBadge, IonInput, ModalController, IonCardTitle, IonCardContent, IonCardHeader, IonSelectOption, IonItemSliding, IonItemOption } from '@ionic/angular/standalone';
+import { IonDatetime, IonSelect, IonItemOptions, IonThumbnail, IonCardSubtitle, IonImg, IonCard, IonButtons, IonItem, IonList, IonHeader, IonIcon, IonToolbar, IonContent, IonSegment, IonSegmentButton, IonGrid, IonRow, IonCol, IonButton, IonLabel, IonBadge, IonInput, ModalController, IonCardTitle, IonCardContent, IonCardHeader, IonSelectOption, IonItemSliding, IonItemOption, ToastController } from '@ionic/angular/standalone';
 import { FmodeChatCompletion, ImagineWork, DalleOptions, ChatPanelOptions, FmodeChat, FmodeChatMessage, MarkdownPreviewModule, openChatPanelModal } from "fmode-ng";
 import { AgentTaskStep } from './agent/agent.task';
 import { TaskPoemPictureDesc } from './agent/tasks/poem/poem-desc';
@@ -60,6 +60,7 @@ import { TestPageComponent } from './test-page/test-page.component';
   ]
 })
 export class Tab2Page implements OnInit {
+
   selectedTab: string = 'checkin';  // 默认选中的tab
   planList: any[] = [];
   coachList: any[] = [];
@@ -67,7 +68,7 @@ export class Tab2Page implements OnInit {
   currentUser: CloudUser | undefined
   actionTaskVisible = false;
   healthTaskVisible = false;
-  constructor(private router: Router, private modalCtrl: ModalController, private cdr: ChangeDetectorRef, private alertController: AlertController) {
+  constructor(private toastController: ToastController, private router: Router, private modalCtrl: ModalController, private cdr: ChangeDetectorRef, private alertController: AlertController) {
     addIcons({ alertCircleOutline, sunny, checkmarkCircle, calendar, helpCircle, trash, create, logoGitlab, bicycleOutline, infiniteOutline });
     this.currentUser = new CloudUser();
   }
@@ -102,13 +103,33 @@ export class Tab2Page implements OnInit {
     let query = new CloudQuery("Coach");
     this.coachList = await query.find();
   }
+
+  dailyReward = 5;
+  powerForMakup = 30;
+  power: number = 0;
+  realDate: Date = this.correctDate(new Date());
+  days: number = 0; // 总打卡天数
+  consecutiveDays: number = 0; // 连续打卡天数
+  checkInHistory: Set<string> = new Set(); // 已打卡日期集合
+
+  async showToast(message: string, color: string = 'success') {
+    const toast = await this.toastController.create({
+      message: message,
+      duration: 2000,
+      position: 'top',
+      color: color,
+    });
+    toast.present();
+  }
+
+  // 计算 BMI
   calculateBMI(height: number, weight: number): number {
-    // 身高转换为米
     const heightInMeters = height / 100;
-    // 计算 BMI
     const bmi = weight / (heightInMeters * heightInMeters);
     return parseFloat(bmi.toFixed(2));
   }
+
+  // 获取鼓励性话语
   getEncouragement(bmi: number): string {
     if (bmi < 18.5) {
       return '您的BMI偏低,注意保持健康的饮食哦!(๑•́ ₃ •̀๑)';
@@ -121,28 +142,23 @@ export class Tab2Page implements OnInit {
     }
   }
 
-
-  realDate: Date = this.correctDate(new Date());
-  days: number = 0; // 总打卡天数
-  consecutiveDays: number = 0; // 连续打卡天数
-  checkInHistory: Set<string> = new Set(); // 已打卡日期集合
-  //打卡页面
+  // 加载用户数据
   async loadPlanUser() {
     let currentUser = new CloudUser();
     const cloudQuery = new CloudQuery("fitUser");
     if (currentUser) {
       cloudQuery.equalTo("user", currentUser.toPointer());
       this.planUser = await cloudQuery.find();
-      // 假设从数据库加载已打卡的日期列表
       const checkedDays = this.planUser[0].get("checkeddays") || [];
       checkedDays.forEach((date: string) => {
-        this.checkInHistory.add(date); // 加载已打卡日期到 checkInHistory
+        this.checkInHistory.add(date);
       });
-      console.log(this.checkInHistory)
-      this.days = this.planUser[0].get("days"); // 获取已打卡天数
-      this.consecutiveDays = this.planUser[0].sucdays; // 获取连续打卡天数
+      this.days = this.planUser[0].get("days");
+      this.consecutiveDays = this.planUser[0].sucdays;
+      this.power = this.planUser[0].get('power');
     }
   }
+
   // 计算连续打卡天数
   calculateConsecutiveDays() {
     let currentDate = this.realDate;
@@ -160,49 +176,138 @@ export class Tab2Page implements OnInit {
     this.consecutiveDays = count;
   }
 
+  // 格式化日期
   formatDate(date: Date): string {
-
     return date.toISOString().split('T')[0];
   }
+
+  // 修正日期(解决时区问题)
   correctDate(date: string | Date): Date {
-    // 如果传入的已经是一个 Date 对象
     if (date instanceof Date) {
       return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
-    }
-    // 如果传入的是字符串(来自 ion-datetime),我们先将它转换为 Date 对象
-    else if (typeof date === 'string') {
+    } else if (typeof date === 'string') {
       const parsedDate = new Date(date);
       return new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000);
     }
     throw new Error("Invalid date format");
   }
 
+  // 判断是否今天已打卡
+  isCheckedInToday(): boolean {
+    const currentDate = this.realDate;
+    const formattedDate = this.formatDate(currentDate);
+    return this.checkInHistory.has(formattedDate);
+  }
+
+  // 获取连续打卡奖励
+  getConsecutiveReward(consecutiveDays: number): number {
+    if (consecutiveDays % 30 === 0) {
+      return 30;  // 第30天,奖励30
+    } else if (consecutiveDays % 15 === 0) {
+      return 20;  // 第15天,奖励20
+    } else if (consecutiveDays % 7 === 0) {
+      return 10;  // 第7天,奖励10
+    }
+    return 0;  // 不符合任何条件时不奖励
+  }
+
+  // 打卡操作
   async markAttendance() {
     const currentDate = this.realDate;
     const formattedDate = this.formatDate(currentDate);
-    if (!this.checkInHistory.has(formattedDate)) {
-      this.checkInHistory.add(formattedDate);
-      this.days = this.checkInHistory.size;
-      this.calculateConsecutiveDays();
+
+    // 如果今天已经打卡
+    if (this.checkInHistory.has(formattedDate)) {
+      this.showToast('今天已经打卡过了', 'warning');
+      return;
+    }
+
+    // 如果是未来日期
+    if (currentDate > this.correctDate(new Date())) {
+      this.showToast('不能打卡未来的日期', 'danger');
+      return;
+    }
+
+    // 正常打卡
+    this.checkInHistory.add(formattedDate);
+    this.days = this.checkInHistory.size;
+    this.calculateConsecutiveDays();
+
+    let currentUser = new CloudUser();
+    const cloudQuery = new CloudQuery("fitUser");
+    cloudQuery.equalTo("user", currentUser.toPointer());
+    const userData = await cloudQuery.find();
+
+    if (userData.length > 0) {
+      const user = userData[0];
+      let checkedDays = user.get("checkeddays") || [];
+      checkedDays.push(formattedDate);
+      user.set({ "checkeddays": checkedDays });
+      user.set({ "days": this.days });
+      user.set({ "sucdays": this.consecutiveDays });
+
+      // 计算奖励
+      let totalReward = this.dailyReward; // 每天签到奖励
+      totalReward += this.getConsecutiveReward(this.consecutiveDays); // 连续签到奖励
+      user.set({ "power": user.get("power") + totalReward }); // 增加总 power 奖励
+
+      await user.save();
+      this.loadPlanUser();
+      this.showToast('打卡成功,获得了 ' + totalReward + ' Power');
+    }
+  }
+
+  // 补签操作
+  async handleMakeupSignIn(user: CloudUser): Promise<string> {
+    if (user.get("power") >= this.powerForMakup) {
+      user.set({ "power": user.get("power") - this.powerForMakup });
+      await user.save();
+      return '补签成功!';
+    } else {
+      return '补签失败,您的动能不足!';
+    }
+  }
+
+  // 补签点击事件
+  async handleMakeupClick() {
+    const confirmed = window.confirm('补签将消耗 ' + this.powerForMakup + ' 动能,确定补签吗?');
+    if (confirmed) {
       let currentUser = new CloudUser();
       const cloudQuery = new CloudQuery("fitUser");
       cloudQuery.equalTo("user", currentUser.toPointer());
       const userData = await cloudQuery.find();
+
       if (userData.length > 0) {
         const user = userData[0];
-        let checkedDays = user.get("checkeddays") || [];
-        checkedDays.push(formattedDate);
-        user.set({ "checkeddays": checkedDays });
-        user.set({ "days": this.days });
-        user.set({ "sucdays": this.consecutiveDays });
-        await user.save();
-        this.loadPlanUser()
+        const resultMessage = await this.handleMakeupSignIn(user);
+        this.showToast(resultMessage, resultMessage.includes('成功') ? 'success' : 'danger');
+        this.loadPlanUser();
       }
     }
   }
+
+  // 日期变化时更新
   onDateChange(event: any) {
     this.realDate = new Date(this.correctDate(event.detail.value));
   }
+
+  // 获取按钮状态(判断打卡、补签等)
+  getButtonState(date: Date): { isDisabled: boolean, buttonText: string } {
+    const formattedDate = this.formatDate(date);
+
+    if (formattedDate > this.formatDate(this.correctDate(new Date()))) {
+      // 未来日期
+      return { isDisabled: true, buttonText: '无法签到' };
+    } else if (this.checkInHistory.has(formattedDate)) {
+      // 已经打卡过
+      return { isDisabled: true, buttonText: '今天已经打卡' };
+    } else if (formattedDate < this.formatDate(this.correctDate(new Date()))) {
+      // 过去的日期
+      return { isDisabled: false, buttonText: '补签' };
+    }
+    return { isDisabled: false, buttonText: '打卡' };
+  }
+
   async login() {
     let user = await openUserLoginModal(this.modalCtrl);
     if (user?.id) {

+ 1 - 1
TFPower-app/src/app/tab2/tag-input/tag-input.component.scss

@@ -13,7 +13,7 @@
 }
 ion-button
 {
-  --background: #32548f;
+  --background: #009b7d;
 }
 .tag-chip {
   display: flex;

+ 1 - 1
TFPower-app/src/app/tab2/test-page/test-page.component.html

@@ -10,7 +10,7 @@
 <ion-content [fullscreen]="true">
   <div class="content">
     <div class="module">
-      <h2>请输入您的健身目标</h2>
+      <h2>健身描述</h2>
       <app-tag-input (tagsChanged)="onTagsChanged($event)"></app-tag-input>
       <ion-item>
         <ion-label position="floating">详细描述(可选,越详细计划越清晰哦!)</ion-label>

+ 1 - 1
TFPower-app/src/app/tab2/test-page/test-page.component.scss

@@ -34,7 +34,7 @@ ion-col {
 }
 ion-button {
   margin-top: 20px;
-  --background: #32548f;
+  --background: #009b7d;
 }
 ion-card {
     border-radius: 15px;