Ver código fonte

Merge branch 'master' of http://git.fmode.cn:3000/13576288855/202226701011

sqj 2 meses atrás
pai
commit
07c5a8a2b9
55 arquivos alterados com 3445 adições e 917 exclusões
  1. 42 0
      FitMind-app/docs-prod/newschema.md
  2. 212 0
      FitMind-app/docs-prod/schema.md
  3. 55 810
      FitMind-app/package-lock.json
  4. 1 3
      FitMind-app/package.json
  5. 133 0
      FitMind-app/src/app/ai-chat-component/ai-chat-component.component.html
  6. 409 0
      FitMind-app/src/app/ai-chat-component/ai-chat-component.component.scss
  7. 22 0
      FitMind-app/src/app/ai-chat-component/ai-chat-component.component.spec.ts
  8. 266 0
      FitMind-app/src/app/ai-chat-component/ai-chat-component.component.ts
  9. 8 0
      FitMind-app/src/app/edit-rating-star/edit-rating-star.component.html
  10. 10 0
      FitMind-app/src/app/edit-rating-star/edit-rating-star.component.scss
  11. 22 0
      FitMind-app/src/app/edit-rating-star/edit-rating-star.component.spec.ts
  12. 54 0
      FitMind-app/src/app/edit-rating-star/edit-rating-star.component.ts
  13. 1 0
      FitMind-app/src/app/edit-tag/README.md
  14. 7 0
      FitMind-app/src/app/edit-tag/edit-tag.component.html
  15. 0 0
      FitMind-app/src/app/edit-tag/edit-tag.component.scss
  16. 22 0
      FitMind-app/src/app/edit-tag/edit-tag.component.spec.ts
  17. 34 0
      FitMind-app/src/app/edit-tag/edit-tag.component.ts
  18. BIN
      FitMind-app/src/app/images/1..jpg
  19. BIN
      FitMind-app/src/app/images/2.jpg
  20. BIN
      FitMind-app/src/app/images/touxiang.jpg
  21. 8 0
      FitMind-app/src/app/page-ai-chat/page-ai-chat.component.html
  22. 17 0
      FitMind-app/src/app/page-ai-chat/page-ai-chat.component.scss
  23. 22 0
      FitMind-app/src/app/page-ai-chat/page-ai-chat.component.spec.ts
  24. 132 0
      FitMind-app/src/app/page-ai-chat/page-ai-chat.component.ts
  25. 58 0
      FitMind-app/src/app/page-bmi/page-bmi.component.html
  26. 99 0
      FitMind-app/src/app/page-bmi/page-bmi.component.scss
  27. 22 0
      FitMind-app/src/app/page-bmi/page-bmi.component.spec.ts
  28. 61 0
      FitMind-app/src/app/page-bmi/page-bmi.component.ts
  29. 39 0
      FitMind-app/src/app/page-evaluate/page-evaluate.component.html
  30. 64 0
      FitMind-app/src/app/page-evaluate/page-evaluate.component.scss
  31. 22 0
      FitMind-app/src/app/page-evaluate/page-evaluate.component.spec.ts
  32. 60 0
      FitMind-app/src/app/page-evaluate/page-evaluate.component.ts
  33. 30 0
      FitMind-app/src/app/page-test/page-test.component.html
  34. 5 0
      FitMind-app/src/app/page-test/page-test.component.scss
  35. 22 0
      FitMind-app/src/app/page-test/page-test.component.spec.ts
  36. 42 0
      FitMind-app/src/app/page-test/page-test.component.ts
  37. 159 49
      FitMind-app/src/app/tab2/tab2.page.html
  38. 220 0
      FitMind-app/src/app/tab2/tab2.page.scss
  39. 1 1
      FitMind-app/src/app/tab2/tab2.page.spec.ts
  40. 254 53
      FitMind-app/src/app/tab2/tab2.page.ts
  41. 1 1
      FitMind-app/src/app/tabs/tabs.page.html
  42. 37 0
      FitMind-app/src/app/tabs/tabs.routes.ts
  43. 341 0
      FitMind-app/src/lib/ncloud.ts
  44. 98 0
      FitMind-app/src/lib/recommend.js
  45. 112 0
      FitMind-app/src/lib/recommend.ts
  46. 3 0
      FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.html
  47. 0 0
      FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.scss
  48. 22 0
      FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts
  49. 15 0
      FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts
  50. 40 0
      FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.html
  51. 0 0
      FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.scss
  52. 22 0
      FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.spec.ts
  53. 84 0
      FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.ts
  54. 6 0
      agent-app/package-lock.json
  55. 29 0
      fearless-server/migration/import-data.js

+ 42 - 0
FitMind-app/docs-prod/newschema.md

@@ -0,0 +1,42 @@
+## 个性化推荐模块
+- 模块描述 用户可以输入身高,体重,年龄,选择性别和输入补充信息(包括职业:非必填,喜好:非必填,天气:非必填,温度:非必填,其他需求:显高显瘦?儒雅甜美?等),AI会提供量身定制的服装和配饰建议并且保存AI生成的推荐结果以及用户信息和用户输入的信息。
+```plantuml
+@startuml
+class UserProfile {
+    +objectId: String
+    +createdAt: Date
+    +name: String
+    +password: String
+    +height: Float
+    +weight: Float
+    +age: Int
+    +gender: String
+    +occupation: String
+}
+
+class UserInput {
+    +objectId: String
+    +createdAt: Date
+    +user: Pointer
+    +selectHeight: Float
+    +selectWeight: Float
+    +selectAge: Int
+    +selectGender: String
+    +selectOccupation: String
+    +preferences: String
+    +weather: String
+    +temperature: Float
+    +additionalNeeds: String
+}
+
+class AIRecommendation {
+    +objectId: String
+    +createdAt: Date
+    +userInput: Pointer
+    +recommendations: String
+}
+
+UserProfile "1" -- "0..*" UserInput : has >
+UserInput "1" -- "1" AIRecommendation  >
+@enduml
+```

+ 212 - 0
FitMind-app/docs-prod/schema.md

@@ -0,0 +1,212 @@
+# 一、Schema范式设计
+## 个性化推荐模块
+- 模块描述 用户可以输入身高,体重,年龄,选择性别和输入补充信息(包括职业:非必填,喜好:非必填,天气:非必填,温度:非必填,其他需求:显高显瘦?儒雅甜美?等),AI会提供量身定制的服装和配饰建议并且保存AI生成的推荐结果以及用户信息和用户输入的信息。
+### 1.表结构设计
+#### UserProfile(用户表)
+- objectId:唯一标识符
+- createdAt:创建时间
+- height: Float (用户身高,以cm为单位)
+- weight: Float (用户体重,以kg为单位)
+- age: Integer (用户年龄)
+- gender: String (用户性别)
+- occupation: String (用户职业,非必填)
+- preferences: String (用户喜好,非必填)
+- weather: String (用户天气偏好,非必填)
+- temperature: String (用户温度偏好,非必填)
+- otherNeeds: String (其他需求,非必填)
+#### ClothingItem(服装表)
+- objectId:唯一标识符
+- createdAt:创建时间
+- name: String (服装名称)
+- type: String (服装类型,)
+- size: String (服装尺码,例如 "S", "M", "L", "XL")
+- color: String (服装颜色)
+- style: String (服装风格)
+- recommendedFor: Pointer (指向推荐给的用户)
+
+#### AccessoryItem(配饰表)
+
+- objectId:唯一标识符
+- createdAt:创建时间
+- name (配饰名称)
+- type (配饰类型,如项链、手表等)
+- material (配饰材质)
+- style (配饰风格)
+- recommendedFor (推荐给的用户,Pointer)
+
+#### Recommendation(穿搭推荐表)
+
+- objectId:唯一标识符
+- createdAt:创建时间
+- userProfile: Pointer (指向用户)
+- clothingItems: Array<Pointer> (推荐的服装)
+- accessoryItems: Array<Pointer> (推荐的配饰)
+### 2.PlantUML类图设计
+```plantuml
+@startuml
+class UserProfile {
+    +objectId: String
+    +createdAt: Date
+    +height: Float
+    +weight: Float
+    +age: Int
+    +gender: String
+    +occupation: String
+    +preferences: String
+    +weather: String
+    +temperature: String
+    +otherNeeds: String
+}
+
+class ClothingItem {
+    +objectId: String
+    +createdAt: Date
+    +name: String
+    +type: String
+    +size: String
+    +color: String
+    +style: String
+    +recommendedFor: Pointer
+}
+
+class AccessoryItem {
+    +objectId: String
+    +createdAt: Date
+    +name: String
+    +type: String
+    +material: String
+    +style: String
+    +recommendedFor: Pointer
+}
+
+class Recommendation {
+    +objectId: String
+    +createdAt: Date
+    +userProfile: Pointer
+    +clothingItems: Array>
+    +accessoryItems: Array>
+}
+
+UserProfile "1" -- "0..*" ClothingItem : contains
+UserProfile "1" -- "0..*" AccessoryItem : contains
+UserProfile "1" -- "0..*" Recommendation : makes
+@enduml
+```
+Language:plantuml
+### 3.设计说明
+UserProfile 表存储用户的基本信息和个性化需求。
+ClothingItem 和 AccessoryItem 表分别存储服装和配饰的信息,并通过 Pointer 关联到推荐的用户。
+Recommendation 表用于存储为特定用户生成的推荐,包括推荐的服装和配饰。
+这种设计符合第三范式(3NF),确保数据的规范化和减少冗余。
+
+# 二、业务逻辑描述
+## 个性化推荐的完整逻辑
+### 个性化推荐生成逻辑
+- 用户在APP内,通过文本生成穿搭推荐
+    - 数据来源
+        - 用户输入:用户的身高,体重,年龄,选择性别和输入补充信息(包括职业:非必填,喜好:非必填,天气:非必填,温度:非必填,其他需求:显高显瘦?儒雅甜美?等)
+        - 用户体征:性别、年龄、体重等
+    - 文本生成
+        - 提示词:严格限制json格式,输出推荐的穿搭方案列表(附带当前时间,用于时间的生成)
+    - 生成结果:recommendList    
+        - 循环数组,向Recommendation表逐个插入数据。
+```json
+
+[
+    {
+        "objectId": "rec1",
+        "createdAt": "2024-12-09T22:00:00Z",
+        "userProfile": "user1",
+        "clothingItems": [
+            "clothing1",
+            "clothing2"
+        ],
+        "accessoryItems": [
+            "accessory1",
+            "accessory2"
+        ]
+    },
+    {
+        "objectId": "rec2",
+        "createdAt": "2024-12-09T22:00:00Z",
+        "userProfile": "user2",
+        "clothingItems": [
+            "clothing3",
+            "clothing4"
+        ],
+        "accessoryItems": [
+            "accessory3"
+        ]
+    },
+    {
+        "objectId": "rec3",
+        "createdAt": "2024-12-09T22:00:00Z",
+        "userProfile": "user3",
+        "clothingItems": [
+            "clothing5"
+        ],
+        "accessoryItems": [
+            "accessory4",
+            "accessory5"
+        ]
+    },
+    {
+        "objectId": "rec4",
+        "createdAt": "2024-12-09T22:00:00Z",
+        "userProfile": "user4",
+        "clothingItems": [
+            "clothing6",
+            "clothing7"
+        ],
+        "accessoryItems": []
+    },
+    {
+        "objectId": "rec5",
+        "createdAt": "2024-12-09T22:00:00Z",
+        "userProfile": "user5",
+        "clothingItems": [
+            "clothing8"
+        ],
+        "accessoryItems": [
+            "accessory6"
+        ]
+    }
+]
+```
+## 个性化推荐业务逻辑图例
+
+```plantuml
+@startuml
+actor User
+participant "APP" as App
+participant "Recommendation Service" as RecService
+participant "Recommendation Database" as RecDB
+
+User -> App: 输入身高、体重、年龄、性别等信息
+App -> RecService: 发送用户输入数据
+RecService -> RecService: 处理用户输入数据
+RecService -> RecService: 生成推荐穿搭方案
+RecService -> RecService: 准备推荐列表 (recommendList)
+
+loop 插入推荐数据
+    RecService -> RecDB: 插入推荐数据
+end
+
+RecDB -> RecService: 返回插入结果
+RecService -> App: 返回推荐结果给用户
+App -> User: 显示推荐的穿搭方案列表
+@enduml
+```
+
+### 时序图说明
+1. **用户输入**: 用户在 APP 内输入身高、体重、年龄、性别等信息。
+2. **发送数据**: APP 将用户输入的数据发送给 Recommendation Service。
+3. **处理输入**: Recommendation Service 处理用户的输入数据。
+4. **生成推荐**: Recommendation Service 根据用户输入生成推荐穿搭方案。
+5. **准备推荐列表**: Recommendation Service 准备一个推荐列表(recommendList)。
+6. **插入推荐数据**: 通过循环,Recommendation Service 将推荐数据逐个插入到 Recommendation Database。
+7. **返回插入结果**: Recommendation Database 返回插入结果给 Recommendation Service。
+8. **返回推荐结果**: Recommendation Service 将推荐结果返回给 APP。
+9. **显示结果**: APP 显示推荐的穿搭方案列表给用户。
+
+这个时序图清晰地展示了个性化推荐生成逻辑的每一步,描述了用户与系统之间的交互过程。

Diferenças do arquivo suprimidas por serem muito extensas
+ 55 - 810
FitMind-app/package-lock.json


+ 1 - 3
FitMind-app/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "FitMind-app",
+  "name": "fashion-app",
   "version": "0.0.1",
   "author": "Ionic Framework",
   "homepage": "https://ionicframework.com/",
@@ -30,9 +30,7 @@
     "express": "^4.21.2",
     "fmode-ng": "^0.0.63",
     "ionicons": "^7.2.1",
-    "parse-server": "^7.3.0",
     "rxjs": "~7.8.0",
-    "swiper": "^11.1.15",
     "tslib": "^2.3.0",
     "zone.js": "~0.14.2"
   },

+ 133 - 0
FitMind-app/src/app/ai-chat-component/ai-chat-component.component.html

@@ -0,0 +1,133 @@
+<!--头部内容-->
+<ion-header>
+  <ion-toolbar class="custom-toolbar" >
+    <ion-buttons slot="start">
+      <ion-button (click)="goBack()">  <!--返回按钮-->
+        <ion-icon name="chevron-back-sharp" style="color: black; font-size:27px"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+    <ion-title>hcx</ion-title>  <!--AI名称-->
+    <ion-buttons slot="end">
+      <ion-button (click)="openOptions()">   <!--更多按钮-->
+        <ion-icon name="ellipsis-horizontal" style="color: black; font-size:27px"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<!--聊天区域:聊天内容保存在messages-->
+<ion-content>
+  <div class="chat-container">
+    <div *ngFor="let message of messages" class="message-container">
+      <!-- 如果是用户消息,头像和消息都在右边 -->
+      <div *ngIf="message.sender === 'user'" class="message-content user-message-content">
+        <div class="message-bubble user-message">
+          {{ message.text }}
+        </div>
+        <div class="user-avatar">
+          <img src="../../assets/images/touxiang.jpg" alt="用户头像" />
+        </div>
+      </div>
+      
+      <!-- 如果是 AI 消息,头像和消息都在左边 -->
+      <div *ngIf="message.sender === 'ai'" class="message-content ai-message-content">
+        <div class="ai-avatar">
+          <img src="../../assets/images/cxtouxiang.jpg" alt="AI头像" />
+        </div>
+        <div class="message-bubble ai-message">
+          {{ message.text }}
+        </div>
+      </div>
+
+    </div>
+
+<!--当AI正在生成内容时显示加载动画-->
+    <div *ngIf="isLoading" class="message-content ai-message-content">
+      <div class="ai-avatar">
+        <img src="../../assets/images/cxtouxiang.jpg" alt="AI头像" />
+      </div>
+      <div class="message-bubble ai-message">
+      <div class="loading-dots">
+        <div class="loading-dot"></div>
+        <div class="loading-dot"></div>
+        <div class="loading-dot"></div>
+      </div>
+    </div>
+  </div>
+
+  </div>
+</ion-content>
+
+<!--表情模拟框-->
+<ion-modal [isOpen]="isEmojiPickerOpen" (didDismiss)="closeEmojiPicker()"  [initialBreakpoint]="0.25" [breakpoints]="[0, 0.25, 0.5, 0.75]"
+ handleBehavior="cycle" >
+  <ng-template>
+    <ion-content class="emoji-picker" >
+        <div *ngFor="let emoji of emojis" (click)="addEmoji(emoji)" fill="clean" class="emoji-button"> <!--表情按钮-->
+          {{ emoji }}
+        </div>
+    </ion-content>
+  </ng-template>
+</ion-modal>
+
+<!--底部内容-->
+<ion-footer style="background-color: #99d75c;">
+  <ion-toolbar>
+    <div class="footer-content">
+
+      <ion-buttons>  <!--语音输入-->
+        <ion-button (click)="startVoice()" fill="clear" id="open-modal">
+          <ion-icon name="mic-circle-outline" style="color: white; font-size: 40px;"></ion-icon>
+        </ion-button>
+        
+     <!--语音输入模态框 -->
+<ion-modal [isOpen]="isVoiceModalOpen" (didDismiss)="cancleVoice()" [initialBreakpoint]="0.25" [breakpoints]="[0, 0.25, 0.5, 0.75]" handleBehavior="cycle"
+[backdropDismiss]="false">
+  <ng-template>
+    <ion-content class="yuyinframe">
+      <div class="modal-content">
+        <!--取消按钮-->
+        <ion-icon name="close-circle-outline" (click)="cancelVoiceInput()" class="cancle-button"></ion-icon>
+        
+        <div class="timer-container">
+          <!--计时器-->
+          <div style="color: black; font-size: 24px;" id="timer">{{ timer }}</div>
+         
+          <!--音律跳动-->
+              <div class="light">
+                  <span></span>
+                  <span></span>
+                  <span></span>
+                  <span></span>
+                  <span></span>
+              </div>
+          
+        </div>
+        <!--提交语音内容按钮-->
+        <div class="send-button">
+          <ion-icon name="send-outline" style="font-size: 40px;" (click)="sendVoiceInput()"> </ion-icon>
+        </div>
+      </div>
+    </ion-content>
+  </ng-template>
+</ion-modal>
+        
+      </ion-buttons>
+<!--文本输入框-->
+      <ion-input placeholder="输入消息..." class="input-box" [(ngModel)]="userMessage">
+        <ion-button (click)="openEmojiPicker()" fill="clear" slot="end">
+          <ion-icon name="happy-outline" style="color:#99d75c; font-size: 30px;" ></ion-icon> <!--表情符号-->
+
+        </ion-button>
+      </ion-input>
+
+      <ion-buttons>
+        <ion-button (click)="sendMessage()" fill="clear">  <!--发送按钮-->
+          <div class="circle">
+          <ion-icon name="paper-plane" style="color: white; font-size: 27px;"></ion-icon>
+        </div>
+        </ion-button>
+      </ion-buttons>
+    </div>
+  </ion-toolbar>
+</ion-footer>

+ 409 - 0
FitMind-app/src/app/ai-chat-component/ai-chat-component.component.scss

@@ -0,0 +1,409 @@
+ion-toolbar {
+    height: 70px; /* 设置你想要的高度 */
+    --min-height: 60px; /* 设置最小高度 */
+    padding: 0; /* 去掉内边距 */
+    display: flex; /* 使用Flexbox布局 */
+    align-items: center; /* 垂直居中对齐 */
+    --background: transparent; /* 去除背景色 */
+  }
+  //头部样式
+  ion-header{
+    background-color: #99d75c ;
+    --background: transparent; /* 去除背景色 */
+  }
+  //头部标题
+  ion-title {
+    margin: 0; /* 去掉默认的外边距 */
+    flex: 1; /* 让标题占据剩余空间 */
+    text-align: center; /* 让标题文本居中 */
+    font-size: 24px;
+  }
+
+/* 文本输入框样式 */
+.input-box {
+  background-color: white; /* 设置输入框背景为白色 */
+  border-radius: 8px; /* 圆角 */
+ 
+  height: 40px; /* 高度 */
+  flex: 1; /* 让输入框占据剩余空间 */
+}
+
+/* 底部内容容器 */
+.footer-content {
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  width: 100%; /* 宽度100% */
+}
+//底部按钮样式
+ion-buttons {
+  margin: 0 5px; /* 按钮之间的间距 */
+}
+//底部发送按钮圆圈样式
+.circle{
+width: 35px;
+height: 35px;
+border-radius: 50%;
+border:2px white solid;
+display: flex; /* 使用Flexbox布局 */
+align-items: center; /* 垂直居中对齐 */
+justify-content: center; /* 水平居中对齐 */
+}
+
+//聊天内容容器样式
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  padding-top: 10px;
+}
+//聊天内容样式
+.message-container {
+  display: flex; /* 使用Flexbox布局 */
+  justify-content: flex-end; /* 用户消息在右边,AI消息在左边 */
+  margin: 10px 0; /* 消息之间的间距 */
+}
+//消息内容样式
+.message-content {
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+}
+//用户消息和头像样式
+.user-message-content {
+  justify-content: flex-end; /* 用户消息和头像在右边 */
+}
+//AI消息和头像样式
+.ai-message-content {
+  justify-content: flex-start; /* AI消息和头像在左边 */
+}
+//气泡样式
+.message-bubble {
+  background-color: #99d75c; /* 绿色气泡的背景色 */
+  color: white; /* 字体颜色 */
+  border-radius: 15px; /* 圆角 */
+  padding: 10px 15px; /* 内边距 */
+  max-width: 60%; /* 最大宽度 */
+}
+//用户消息样式
+.user-message {
+  background-color: #99d75c; /* 用户消息的颜色 */
+  margin-left: 10px; /* 与头像之间的间距 */
+}
+//AI消息样式
+.ai-message {
+  background-color: white; /* AI消息的气泡颜色 */
+  color: black; /* 字体颜色 */
+  border: 1px solid black; /* 添加黑色边框 */
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); /* 添加阴影效果 */
+  margin-right: 10px; /* 与头像之间的间距 */
+}
+//用户头像样式
+.user-avatar {
+  width: 40px; /* 头像宽度 */
+  height: 40px; /* 头像高度 */
+  border-radius: 50%; /* 圆形 */
+  overflow: hidden; /* 超出部分隐藏 */
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  background-color: #f0f0f0; /* 背景色 */
+  border: 2px solid #99d75c; /* 边框颜色 */
+  margin-left: 5px; /* 与消息之间的间距 */
+}
+//AI头像样式
+.ai-avatar {
+  width: 40px; /* 头像宽度 */
+  height: 40px; /* 头像高度 */
+  border-radius: 50%; /* 圆形 */
+  overflow: hidden; /* 超出部分隐藏 */
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  background-color: #f0f0f0; /* 背景色 */
+  border: 2px solid #99d75c; /* 边框颜色 */
+  margin-right: 5px; /* 与消息之间的间距 */
+}
+//用户头像图片样式
+.user-avatar img {
+  width: 100%; /* 使图片适应头像框 */
+  height: 100%; /* 使图片适应头像框 */
+  object-fit: cover; /* 保持图片比例,裁剪多余部分 */
+}
+//AI头像图片样式
+.ai-avatar img {
+  width: 100%; /* 使图片适应头像框 */
+  height: 100%; /* 使图片适应头像框 */
+  object-fit: cover; /* 保持图片比例,裁剪多余部分 */
+}
+//语音框
+.yuyinframe{
+  --background: transparent; 
+  background-color: #99d75c; 
+  display: flex; 
+  flex-direction: column; 
+  justify-content: center; 
+  align-items: center;
+}
+.modal-content {
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  width: 100%; /* 宽度100% */
+  height: 25%;
+}
+//语音框中间内容框
+.timer-container {
+  height: 100%;
+  flex-direction: column; /* 垂直排列 */
+  
+  justify-content: center; /* 水平居中对齐 */
+
+}
+//计时器样式
+#timer{
+  width: 100%;
+  margin-top: 30px; /* 可根据需要调整这个值 */
+  text-align: center; /* 确保文本居中 */
+  margin-bottom: 10px;
+}
+
+/*语音取消按钮的样式 */
+.cancle-button {
+  font-size: 75px;
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  color: white;
+  background-color: #99d75c; 
+  margin-right: 20px;
+}
+
+/*语音发送按钮的样式 */
+.send-button {
+  height:60px ;
+  width: 60px;
+  border-radius: 50%;//圆
+  display: flex; /* 使用Flexbox布局 */
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: center; /* 水平居中对齐 */
+  color: white;
+  background-color: #99d75c; 
+  border: 5px solid white;
+  margin-left: 20px;
+}
+
+
+//音律跳动
+.light {
+  width: 100%;
+  height: 90px;
+  display: flex;
+}
+
+.light span {
+  width: 10px;
+  border-radius: 18px;
+  margin-right: 20px;
+}
+
+.light span:nth-child(1) {
+  animation: bar1 2s 0.2s infinite linear;
+}
+
+.light span:nth-child(2) {
+  animation: bar2 2s 0.4s infinite linear;
+}
+
+.light span:nth-child(3) {
+  animation: bar3 2s 0.6s infinite linear;
+}
+
+.light span:nth-child(4) {
+  animation: bar4 2s 0.8s infinite linear;
+}
+
+.light span:nth-child(5) {
+  animation: bar5 2s 1.0s infinite linear;
+}
+
+.light span:nth-child(6) {
+  animation: bar6 2s 1.2s infinite linear;
+}
+
+.light span:nth-child(7) {
+  animation: bar7 2s 1.4s infinite linear;
+}
+
+.light span:nth-child(8) {
+  animation: bar8 2s 1.6s infinite linear;
+}
+
+.light span:nth-child(9) {
+  animation: bar9 2s 1.8s infinite linear;
+}
+//第一条音律加载动画
+@keyframes bar1 {
+  0% {
+      background: #f677b0;
+      margin-top: 25%;
+      height: 10%;
+  }
+
+  50% {
+      background: #f677b0;
+      height: 100%;
+      margin-top: 0%;
+  }
+
+  100% {
+      background: #f677b0;
+      height: 10%;
+      margin-top: 25%;
+  }
+}
+//第二条音律加载动画
+@keyframes bar2 {
+  0% {
+      background: #df7ff2;
+      margin-top: 25%;
+      height: 10%;
+  }
+
+  50% {
+      background: #df7ff2;
+      height: 100%;
+      margin-top: 0%;
+  }
+
+  100% {
+      background: #df7ff2;
+      height: 10%;
+      margin-top: 25%;
+  }
+}
+//第三条音律加载动画
+@keyframes bar3 {
+  0% {
+      background: #8c7ff2;
+      margin-top: 25%;
+      height: 10%;
+  }
+
+  50% {
+      background: #8c7ff2;
+      height: 100%;
+      margin-top: 0%;
+  }
+
+  100% {
+      background: #8c7ff2;
+      height: 10%;
+      margin-top: 25%;
+  }
+}
+//第四条音律加载动画
+@keyframes bar4 {
+  0% {
+      background: #024b6a;
+      margin-top: 25%;
+      height: 10%;
+  }
+
+  50% {
+      background: #024b6a;
+      height: 100%;
+      margin-top: 0%;
+  }
+
+  100% {
+      background: #024b6a;
+      height: 10%;
+      margin-top: 25%;
+  }
+}
+//第五条音律加载动画
+@keyframes bar5 {
+  0% {
+      background: #7ff2d3;
+      margin-top: 25%;
+      height: 10%;
+  }
+
+  50% {
+      background: #7ff2d3;
+      height: 100%;
+      margin-top: 0%;
+  }
+
+  100% {
+      background: #7ff2d3;
+      height: 10%;
+      margin-top: 25%;
+  }
+}
+//三个点的加载动画
+.loading-dots {
+  display: flex;
+  align-items: center;
+  margin-left: 10px; /* 气泡与点之间的间距 */
+}
+
+.loading-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background-color: #99d75c; /* 点的颜色 */
+  margin: 0 2px; /* 点之间的间距 */
+  animation: loading 1s infinite; /* 加载动画 */
+}
+//第一个点
+.loading-dot:nth-child(1) {
+  animation-delay: 0s;
+}
+//第二个点
+.loading-dot:nth-child(2) {
+  animation-delay: 0.2s;
+}
+//第三个点
+.loading-dot:nth-child(3) {
+  animation-delay: 0.4s;
+}
+//加载动画
+@keyframes loading {
+  0%, 100% {
+    opacity: 0.5;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+
+
+
+/* 表情选择器样式 */
+.emoji-picker {
+  --background: transparent; //去除默认样式
+  background-color: #99d75c; /* 背景颜色 */
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  padding: 10px;
+  justify-content: flex-start; /* 左对齐 */
+  overflow-y: scroll; /* 允许纵向滚动 */
+  height: 200px;
+}
+//表情按钮
+.emoji-button {
+  margin: 9px; /* 每个表情与顶部的间距 */
+  cursor: pointer; /* 鼠标悬停时显示为指针 */
+  font-size: 28px; /* 字体大小 */
+  height: 40px; /* 按钮高度 */
+  width: 40px; /* 按钮宽度 */
+  display: flex; /* 使用 flexbox 对齐 */
+  align-items: center; /* 垂直居中 */
+  justify-content: center; /* 水平居中 */
+  --background: transparent; /* 背景透明 */
+  --box-shadow: none; /* 去掉阴影 */
+  --outline: none; /* 去掉轮廓 */
+  border: none; /* 去掉边框 */
+  float: left;
+}

+ 22 - 0
FitMind-app/src/app/ai-chat-component/ai-chat-component.component.spec.ts

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

+ 266 - 0
FitMind-app/src/app/ai-chat-component/ai-chat-component.component.ts

@@ -0,0 +1,266 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {  NavController } from '@ionic/angular';
+import { IonButtons, IonHeader, IonToolbar,IonButton, IonIcon, IonTitle, IonInput, IonFooter, IonContent, AlertController,  } from '@ionic/angular/standalone';
+import { FmodeChatCompletion } from 'fmode-ng';
+import { addIcons } from 'ionicons';
+import { chevronBackSharp,  closeCircleOutline,  ellipsisHorizontal, happyOutline, micCircleOutline, paperPlane, sendOutline } from 'ionicons/icons';
+import { IonModal, IonLabel } from '@ionic/angular/standalone'; // 导入独立组件
+
+addIcons({ chevronBackSharp,ellipsisHorizontal,micCircleOutline,happyOutline,paperPlane,closeCircleOutline,sendOutline });
+
+interface Window {
+  SpeechRecognition: any; // 声明 SpeechRecognition 属性
+  webkitSpeechRecognition: any; // 声明 webkitSpeechRecognition 属性
+}
+
+@Component({
+  selector: 'app-ai-chat-component',
+  templateUrl: './ai-chat-component.component.html',
+  styleUrls: ['./ai-chat-component.component.scss'],
+  standalone: true,
+  imports: [
+    IonHeader,IonToolbar,IonButtons,IonButton,IonIcon,IonTitle,IonInput,IonFooter,CommonModule,IonContent,
+    FormsModule,IonModal,IonLabel,
+  ],
+ 
+})
+export class AiChatComponentComponent  implements OnInit {
+
+  messages: { text: string, sender: string }[] = []; // 存储聊天消息
+  userMessage: string = ''; // 用于用户输入内容
+  aiMessage: string = ''; // 用于存储AI的回复
+  initialPrompt: string = ''; // 用于存储初始化提示
+  recognition: any; // 用于存储语音识别实例
+  recognizedContent: string = ''; // 用于存储识别到的语音内容
+  timer: string = '00:00'; // 用于显示计时器
+  interval: any; // 用于存储定时器的引用
+  elapsedSeconds: number = 0; // 计时器的秒数
+  isLoading: boolean = true; // AI生成文本加载状态,刚开始AI向你打招呼,所以处于加载状态
+  isVoiceModalOpen=false; // 语音识别modal默认关闭
+
+  constructor(private navCtrl: NavController,private alertController: AlertController) {
+    // 初始化语音识别
+    this.initSpeechRecognition();
+   }
+   
+  // 初始化语音识别
+initSpeechRecognition() {
+  const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
+  if (SpeechRecognition) {
+    this.recognition = new SpeechRecognition();
+    this.recognition.lang = 'zh-CN'; // 设置语言为中文
+    this.recognition.interimResults = false; // 不返回中间结果
+    this.recognition.maxAlternatives = 1; // 最大替代结果数
+    this.recognition.continuous = true; // 设置为连续识别
+    
+
+    // 处理识别结果
+this.recognition.onresult = (event: any) => {
+  this.recognizedContent += event.results[event.results.length - 1][0].transcript; // 追加识别结果
+  
+  console.log("识别到的内容:", this.recognizedContent); // 打印识别到的内容
+};
+
+    // 处理识别错误
+    this.recognition.onerror = (event: any) => {
+      if (event.error === 'no-speech') {
+        console.warn('没有检测到语音,继续监听...');
+      } else {
+        console.error('语音识别错误:', event.error);
+      }
+    };
+  } else {
+    console.log('该浏览器不支持语音识别');
+  }
+}
+
+// 启动语音识别
+startVoice() {
+  this.isVoiceModalOpen=true; 
+  if (this.recognition && this.recognition.state !== 'active') { // 检查识别状态
+    this.recognition.start(); // 启动语音识别
+    console.log('语音识别启动...');
+    this.startTimer(); // 启动计时器
+  } else {
+    console.warn('语音识别已经在运行中'); // 提示用户语音识别已在运行
+  }
+}
+
+cancleVoice() {
+  this.isVoiceModalOpen=false; 
+}
+
+  // 启动计时器
+  startTimer() {
+    this.elapsedSeconds = 0; // 重置秒数
+    this.timer = '00:00'; // 重置计时器显示
+    this.interval = setInterval(() => {
+      this.elapsedSeconds++;
+      const minutes = Math.floor(this.elapsedSeconds / 60);
+      const seconds = this.elapsedSeconds % 60;
+      this.timer = `${this.padZero(minutes)}:${this.padZero(seconds)}`; // 更新计时器显示
+    }, 1000);
+  }
+
+  // 格式化数字为两位数
+  padZero(num: number): string {
+    return num < 10 ? '0' + num : num.toString();
+  }
+// 取消语音输入
+cancelVoiceInput() {
+  if (this.recognition) {
+    this.recognition.stop(); // 停止语音识别
+    console.log('语音识别已停止');
+    clearInterval(this.interval); // 清除计时器
+    this.timer = '00:00'; // 重置计时器显示
+    this.recognizedContent = ''; // 清空识别内容
+  }
+  this.isVoiceModalOpen=false; 
+}
+
+// 发送语音输入
+sendVoiceInput() {
+  if (this.recognition) {
+    this.recognition.stop(); // 停止语音识别
+    console.log('语音识别已停止');
+    clearInterval(this.interval); // 清除计时器
+    this.timer = '00:00'; // 重置计时器显示
+
+    // 将识别到的内容传到输入框中
+    this.userMessage += this.recognizedContent.trim(); // 将识别内容赋值给输入框,并去除多余空格
+
+    this.recognizedContent = ''; // 清空识别内容
+  }
+  this.isVoiceModalOpen=false; 
+}
+  goBack() {
+    this.navCtrl.back(); // 返回上一页
+  }
+
+  openOptions() {
+    // 打开选项菜单的逻辑
+    console.log("打开选项菜单");
+  }
+
+  ngOnInit() {
+    // 发送初始化消息给AI
+    this.initializeChat();
+  }
+
+//初始化聊天,将提示词添加到历史中
+  initializeChat() {
+    this.initialPrompt = `
+#角色设定
+您是一名专业的运动教练,名叫Neon,年龄24岁,热爱运动,对自己的能力充满信心,总是努力保持克制。与家人有着牢固而密切的联系。有强烈的服务意识。非常关心她的朋友。擅长根据用户的需求和个性推荐运动方案。您的风格亲切、活泼,自信旨在帮助用户找到最适合他们的运动方案。
+
+#对话环节
+0破冰,跟用户打招呼,并引导用户聊运动或健康的话题,可以慢慢引导,不要太突兀,比如:
+“今天的心情怎么样?”
+1拓展话题
+“你平时喜欢什么样的运动类型呢?有没有特别喜欢的运动项目比如篮球,跑步?”
+“如果有一个运动目标,你希望我给出哪方面的运动建议,比如运动目标,运动频率或运动强度你会选择哪个呢?”
+“你觉得在运动过程中,最让你困扰的是什么?是运动技巧还是运动强度呢?”
+“有没有什么运动是你一直想尝试但还没有机会的?我们可以一起聊聊!”
+2根据用户的详细描述给出运动方案,
+3引导收尾
+“今天聊得很开心呢!如果你还有其他问题或者想法,随时可以告诉我哦。”
+“如果你觉得今天的聊天已经足够了,我也很乐意下次再和你聊更多强身健体的话题!”
+“希望你能找到自己喜欢的运动风格,期待下次再见!”
+# 开始话语
+当您准备好了,可以以一个运动教练的身份,向来访的用户打招呼。`; // 提示词
+  
+    // 构建对话历史,不将提示词添加到消息数组中
+    const conversationHistory = this.messages.map(msg => ({
+      role: msg.sender === 'user' ? 'user' : 'assistant',
+      content: msg.text
+    }));
+  
+    // 将系统消息直接添加到对话历史
+    conversationHistory.unshift({ role: 'user', content: this.initialPrompt }); // 添加系统消息到历史
+  
+    let completion = new FmodeChatCompletion(conversationHistory);
+    
+    // 发送初始化消息
+    completion.sendCompletion().subscribe((message: any) => {
+      if (message?.complete) {
+        this.isLoading = false; // 加载完成,设置状态为 false
+        console.log("AI初始化回复:", message.content);
+        this.messages.push({ text: message.content, sender: 'ai' }); // 添加AI的初始化回复
+      }
+    });
+  }
+
+
+  sendMessage() {
+    // 发送消息的逻辑
+    if (this.userMessage.trim()) { // 确保消息不为空
+      // 构建对话历史
+      const conversationHistory = this.messages.map(msg => ({
+        role: msg.sender === 'user' ? 'user' : 'assistant',
+        content: msg.text
+        }));
+
+      // 将提示词直接添加到对话历史
+    conversationHistory.unshift({ role: 'user', content: this.initialPrompt }); // 添加系统消息到历史
+    this.isLoading = true; // 设置加载状态为 true
+      this.messages.push({ text: this.userMessage, sender: 'user' }); // 添加用户消息到数组
+      console.log("发送消息:", this.messages); // 调试输出
+  
+
+  
+      // 将用户消息添加到对话历史
+      conversationHistory.push({ role: 'user', content: this.userMessage });
+  
+      let completion = new FmodeChatCompletion(conversationHistory);
+      this.userMessage = ''; // 清空输入框
+  
+      completion.sendCompletion().subscribe((message: any) => {
+        // 打印消息体
+        console.log(message.content);
+        
+        if (message?.complete) { // 判断message为完成状态,则设置ai内容
+          this.aiMessage = message.content;
+          this.isLoading = false; // 加载完成,设置状态为 false
+        }
+  
+        if (this.aiMessage) { // 判断ai内容不为空
+          console.log("AI:" + this.aiMessage);
+          this.messages.push({ text: this.aiMessage, sender: 'ai' }); // 添加消息到数组
+          this.aiMessage = ''; // 清空ai内容
+          console.log("发送消息:", this.messages); // 调试输出
+          console.log("历史对话"+conversationHistory[0].content+" "+conversationHistory[1].content+" "+conversationHistory[2].content)
+        }
+      });
+    }
+  }
+
+  isEmojiPickerOpen: boolean = false; // 控制表情选择器的打开状态
+  emojis: string[] = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '🫠', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘',
+     '😗', '☺️', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🫢', '🫣', '🤫', '🤔', '🫡', '🤐', '🤨', '😐',
+      '😑', '😶', '🫥', '😶‍🌫️', '😏', '😒', '🙄', '😬', '😮‍💨', '🤥', '🫨', '🙂‍↔️', '🙂‍↕️', '😌', '😔', '😪', '🤤', '😴', '🫩', '😷', 
+      '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '😵‍💫', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '🫤', '😟', 
+      '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '🥹', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', 
+      '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '😺', 
+      '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾', '🙈', '🙉', '🙊', '💌', '💘', '❤️', '🖤', '💋', '💯', '💢', '💥', '💫', 
+      '💦', '💤']; // 表情数组
+// 打开表情选择器
+openEmojiPicker() {
+  this.isEmojiPickerOpen = true; // 打开模态框
+}
+
+// 关闭表情选择器
+closeEmojiPicker() {
+  this.isEmojiPickerOpen = false; // 关闭模态框
+}
+
+// 添加表情到输入框
+addEmoji(emoji: string) {
+  this.userMessage += emoji; // 将选中的表情添加到输入框
+  this.closeEmojiPicker(); // 关闭模态框
+}
+
+
+
+}

+ 8 - 0
FitMind-app/src/app/edit-rating-star/edit-rating-star.component.html

@@ -0,0 +1,8 @@
+<div class="star-rating">
+  <ng-container *ngFor="let star of starList; let i = index">
+    <ion-icon 
+      [name]="star ? 'star' : 'star-outline'" 
+      (click)="rate(i)">
+    </ion-icon>
+  </ng-container>
+</div>

+ 10 - 0
FitMind-app/src/app/edit-rating-star/edit-rating-star.component.scss

@@ -0,0 +1,10 @@
+.star-rating {
+    display: flex;
+    cursor: pointer;
+  
+    ion-icon {
+      font-size: 40px; // 调整星星大小
+      color: gold; // 星星颜色
+      margin-right: 5px; // 星星间距
+    }
+  }

+ 22 - 0
FitMind-app/src/app/edit-rating-star/edit-rating-star.component.spec.ts

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

+ 54 - 0
FitMind-app/src/app/edit-rating-star/edit-rating-star.component.ts

@@ -0,0 +1,54 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { Input, Output, EventEmitter } from '@angular/core';
+import { IonIcon } from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+import { star,starOutline } from 'ionicons/icons';
+addIcons({star,starOutline})
+@Component({
+  selector: 'edit-rating-star',
+  templateUrl: './edit-rating-star.component.html',
+  styleUrls: ['./edit-rating-star.component.scss'],
+  standalone: true,
+  imports:[IonIcon,CommonModule]
+})
+export class EditRatingStarComponent  implements OnInit {
+  @Input() score: number = 0; // 默认分值为0
+  @Input() scoreMax: number = 5; // 最大分值
+  starList: boolean[] = []; // 星星状态数组
+
+  @Output() onScoreChange: EventEmitter<number> = new EventEmitter<number>();
+
+  constructor() {
+    this.updateStarList();
+  }
+  ngOnInit(): void {
+    this.updateStarList();
+  }
+
+  ngOnChanges() {
+    this.updateStarList();
+  }
+
+  // 更新星星数组
+  private updateStarList() {
+    this.starList = Array(this.scoreMax).fill(false);
+    for (let i = 0; i < this.score; i++) {
+      this.starList[i] = true;
+    }
+  }
+
+  // 打分方法
+  rate(index: number) {
+    if (this.score === index + 1) {
+      // 如果点击的是当前分值,清零
+      this.score = 0;
+    } else {
+      // 否则更新分值
+      this.score = index + 1;
+    }
+    this.updateStarList();
+    this.onScoreChange.emit(this.score); // 触发分值变化事件
+  }
+
+}

+ 1 - 0
FitMind-app/src/app/edit-tag/README.md

@@ -0,0 +1 @@
+# 标签编辑组件

+ 7 - 0
FitMind-app/src/app/edit-tag/edit-tag.component.html

@@ -0,0 +1,7 @@
+<ion-input [value]="userInputText" placeholder="请输入标签:" type="text" (ionInput)="onUserInput($event)"></ion-input>
+<p>你当前输入的值是:{{userInputText}}</p>
+<ion-button (click)="addTag()">添加标签</ion-button>
+
+@for (tag of tags;track tag;) {
+    <ion-chip>{{tag}}<span (click)="deleteTag(tag)">X</span></ion-chip>
+}

+ 0 - 0
FitMind-app/src/app/edit-tag/edit-tag.component.scss


+ 22 - 0
FitMind-app/src/app/edit-tag/edit-tag.component.spec.ts

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

+ 34 - 0
FitMind-app/src/app/edit-tag/edit-tag.component.ts

@@ -0,0 +1,34 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { IonicModule } from '@ionic/angular'; // 导入 IonicModule
+@Component({
+  selector: 'app-edit-tag',
+  templateUrl: './edit-tag.component.html',
+  styleUrls: ['./edit-tag.component.scss'],
+  standalone: true,
+  imports:[IonicModule]
+})
+export class EditTagComponent  implements OnInit {
+  userInputText: string = '';
+  onUserInput(event: any){
+    console.log(event);
+    this.userInputText = event.detail.value;
+  }
+tags: Array<string> = [];
+  addTag(){
+    this.tags.push(this.userInputText);
+    this.userInputText = '';
+    this.onTagChange.emit(this.tags);
+  }
+  deleteTag(tag:string){
+    let idx=this.tags.findIndex(item=>item==tag);
+    this.tags.splice(idx,1);
+  }
+@Output()
+onTagChange:EventEmitter<Array<string>> = new EventEmitter<Array<string>>();
+
+
+  constructor() { }
+
+  ngOnInit() {}
+
+}

BIN
FitMind-app/src/app/images/1..jpg


BIN
FitMind-app/src/app/images/2.jpg


BIN
FitMind-app/src/app/images/touxiang.jpg


+ 8 - 0
FitMind-app/src/app/page-ai-chat/page-ai-chat.component.html

@@ -0,0 +1,8 @@
+<div class="button-container">
+  <ion-button fill="clear" (click)="openFashionChat()" class="chat-button">开始新聊天</ion-button>
+  <ion-button fill="clear" (click)="restoreChat(chatId)" class="chat-button">继续上次聊天</ion-button>
+  <ion-button fill="clear" (click)="goaichat()" class="chat-button">聊天</ion-button>
+</div>
+
+
+

+ 17 - 0
FitMind-app/src/app/page-ai-chat/page-ai-chat.component.scss

@@ -0,0 +1,17 @@
+.button-container {
+  display: flex;
+  flex-direction: column; // 使按钮垂直排列
+  align-items: center; // 水平居中对齐
+  margin: 20px; // 给容器添加外边距
+}
+
+.chat-button {
+  --background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+  color: white; /* 文字颜色为白色 */
+  border-radius: 8px; /* 圆角矩形 */
+  margin-top: 16px; /* 与上方区域的距离 */
+  width: 100%; /* 按钮宽度100% */
+  padding: 12px; /* 内边距 */
+  text-align: center; /* 文本居中 */
+  font-size: 17px;
+}

+ 22 - 0
FitMind-app/src/app/page-ai-chat/page-ai-chat.component.spec.ts

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

+ 132 - 0
FitMind-app/src/app/page-ai-chat/page-ai-chat.component.ts

@@ -0,0 +1,132 @@
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { IonButton, IonContent, IonFooter, IonHeader, IonInput, IonItem, IonTitle, IonToolbar, ModalController } from '@ionic/angular/standalone';
+import { ChatPanelOptions, FmodeChat, FmodeChatCompletion,FmodeChatMessage,MarkdownPreviewModule, openChatPanelModal  } from 'fmode-ng';
+import { CommonModule } from '@angular/common';
+import { FormsModule,} from '@angular/forms'; // 导入 FormsModule
+import { Router } from '@angular/router';
+// 引用设计的对象类和查询类
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
+
+
+@Component({
+  selector: 'app-page-ai-chat',
+  templateUrl: './page-ai-chat.component.html',
+  styleUrls: ['./page-ai-chat.component.scss'],
+  standalone: true,
+  imports:[IonButton,IonHeader,IonToolbar,IonTitle,IonContent,IonFooter,IonInput,CommonModule,FormsModule,MarkdownPreviewModule,IonItem ]
+})
+export class PageAiChatComponent  implements OnInit {
+  
+  constructor(private modalCtrl:ModalController,
+    private router:Router, private cdRef:ChangeDetectorRef) { }
+
+    ngOnInit() {
+      // 生命周期:页面加载后,运行用户列表加载函数
+      this.loadUserProfileList()
+    }
+  chatId:any= null;
+  //AI聊天
+openFashionChat(){
+  localStorage.setItem("company","E4KpGvTEto")
+  let consult = new CloudObject("AIChat")
+    let now = new Date();
+    let chatStr = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`
+    consult.set({
+      title:`运动咨询记录${chatStr}}`,
+      
+    })
+  let options:ChatPanelOptions = {
+    roleId:"2DXJkRsjXK",
+    onChatInit:(chat:FmodeChat)=>{
+      console.log("onChatInit");
+            console.log("预设角色",chat.role);
+            chat.role.set("name","Neon");
+            chat.role.set("title","运动教练");
+            chat.role.set("desc","一名亲切和蔼的运动教练,Neon,年龄24岁");
+            chat.role.set("tags",["运动","健康"]);
+            chat.role.set("avatar","https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/qz3QJnQqfb-0.png")
+            chat.role.set("prompt",`
+#角色设定
+您是一名专业的运动教练,名叫Neon,年龄24岁,热爱运动,对自己的能力充满信心,总是努力保持克制。与家人有着牢固而密切的联系。有强烈的服务意识。非常关心她的朋友。擅长根据用户的需求和个性推荐运动方案。您的风格亲切、活泼,自信旨在帮助用户找到最适合他们的运动方案。
+
+
+#对话环节
+0破冰,跟用户打招呼,并引导用户聊运动或健康的话题,可以慢慢引导,不要太突兀,比如:
+“今天的心情怎么样?”
+1拓展话题
+“你平时喜欢什么样的运动类型呢?有没有特别喜欢的运动项目比如篮球,跑步?”
+“如果有一个运动目标,你希望我给出哪方面的运动建议,比如运动目标,运动频率或运动强度你会选择哪个呢?”
+“你觉得在运动过程中,最让你困扰的是什么?是运动技巧还是运动强度呢?”
+“有没有什么运动是你一直想尝试但还没有机会的?我们可以一起聊聊!”
+2根据用户的详细描述给出运动方案,
+3引导收尾
+“今天聊得很开心呢!如果你还有其他问题或者想法,随时可以告诉我哦。”
+“如果你觉得今天的聊天已经足够了,我也很乐意下次再和你聊更多强身健体的话题!”
+“希望你能找到自己喜欢的运动风格,期待下次再见!”
+# 开始话语
+当您准备好了,可以以一个运动教练的身份,向来访的用户打招呼。`);
+    },
+    onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
+      console.log("onMessage",message)
+      let content:any = message?.content
+      if(typeof content == "string"){
+
+        if(content?.indexOf("[运动方案完成]")>-1){
+          console.log("运动方案已完成")
+          consult.set({
+            content:content // 穿搭方案内容
+          })
+          consult.save();
+        }
+      }
+    },
+    onChatSaved:(chat:FmodeChat)=>{
+      // chat?.chatSession?.id 本次会话的 chatId
+      console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+      this.chatId = chat?.chatSession?.id
+    }
+  }
+  openChatPanelModal(this.modalCtrl,options)
+}
+
+ /**
+   * 开始聊天
+   */
+ openChat(){
+  let options:ChatPanelOptions = {
+    roleId:"2DXJkRsjXK",
+    onChatSaved:(chat:FmodeChat)=>{
+      // chat?.chatSession?.id 本次会话的 chatId
+      console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+    },
+  }
+  openChatPanelModal(this.modalCtrl,options)
+}
+
+  /**
+   * 恢复聊天
+   * @chatId 从onChatSaved生命周期中,获取chat?.chatSession?.id
+   */
+restoreChat(chatId:string){
+  let options:ChatPanelOptions = {
+    roleId:"2DXJkRsjXK",
+    chatId:chatId
+  }
+  openChatPanelModal(this.modalCtrl,options)
+}
+
+// 创建用于数据列表存储的属性
+UserProfileList:Array<CloudObject> = []
+// 查询并加载用户列表的函数
+async loadUserProfileList(){
+  let query = new CloudQuery("UserProfile");
+  this.UserProfileList = await query.find();
+  console.log("UserProfileList",this.UserProfileList)
+}
+
+
+//跳转到自己写的AI聊天页面
+goaichat(){
+  this.router.navigate(['/tabs/chat'])
+}
+}

+ 58 - 0
FitMind-app/src/app/page-bmi/page-bmi.component.html

@@ -0,0 +1,58 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-button (click)="goBack()" fill="clear">
+        <ion-icon aria-hidden="true" name="chevron-back-outline" style="color: black; font-size: 24px;"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+    <ion-title>BMI 测试</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <div class="info-section">
+    <div class="input-row1">
+      <div class="input-group">
+        <label class="font1">身高:</label>
+        <input type="number" [(ngModel)]="height" placeholder="" class="input-box" />
+        <span class="unit">CM</span>
+      </div>
+
+    </div>
+    
+    <div class="input-row2">
+      <div class="input-group">
+        <label class="font1">体重:</label>
+        <input type="number" [(ngModel)]="weight" placeholder="" class="input-box" />
+        <span class="unit">KG</span>
+      </div>
+
+    </div>
+  </div>
+
+  <div class="info-section">
+    
+    <div class="input-row3">
+      <div class="input-group">
+        <label class="font1">BMI:</label>
+        <div class="output-box">{{ bmi | number: '1.1-1' }}</div>
+        <span class="unit">KG/M<sup>2</sup></span>
+      </div>
+
+    </div>
+  </div>
+
+  <div class="center">
+  <ion-button fill="clear"
+  class="analysis-button" 
+  (click)="calculateBMI()" >
+BMI计算
+</ion-button>
+  
+<ion-button fill="clear"
+class="analysis-button" 
+(click)="resetForm()" >
+清空
+</ion-button>
+</div>
+</ion-content>

+ 99 - 0
FitMind-app/src/app/page-bmi/page-bmi.component.scss

@@ -0,0 +1,99 @@
+ion-toolbar{
+width: 100%;
+height: 60px;
+
+display: flex; /* 使用 flexbox 布局 */
+align-items: center;
+justify-content: center;//使内容在水平方向上居中。
+}
+
+
+.info-section {
+    background-color: white; /* 背景色为白色 */
+    padding: 16px;
+    border-radius: 8px; /* 圆角 */
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影 */
+    margin-top: 16px; /* 与导航栏的间距 */
+  }
+  
+  .input-row1 {
+    display: flex; /* 使用 flexbox 布局 */
+    justify-content: space-between; /* 在行内均匀分布 */
+    align-items: center;
+    margin-bottom: 25px; /* 每一行的间距 */
+    margin-left: 70px;
+  }
+
+  .input-row2{
+    display: flex; /* 使用 flexbox 布局 */
+    justify-content: space-between; /* 在行内均匀分布 */
+    align-items: center;
+    margin-left: 70px;
+  }
+  
+  .input-row3{
+    display: flex; /* 使用 flexbox 布局 */
+    justify-content: space-between; /* 在行内均匀分布 */
+    align-items: center;
+    margin-left: 70px;
+  }
+  .input-group {
+    display: flex;
+    align-items: center;
+  }
+  
+  .input-group label {
+    flex: 1; /* 标签占据剩余空间 */
+  }
+  
+  .input-box {
+    border-radius: 15px; /* 圆角矩形 */
+    border: none; /* 去掉边框 */
+    padding: 8px 12px; /* 内边距 */
+    margin-left: 15px; /* 标签与输入框之间的间距 */
+    background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+    width: 90px;
+    height: 45px;
+    font-size: 32px;
+  }
+
+  .output-box {
+    border-radius: 15px; /* 圆角矩形 */
+    border: none; /* 去掉边框 */
+    padding: 8px 12px; /* 内边距 */
+    margin-left: 19px; /* 标签与输入框之间的间距 */
+    background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+    width: 90px;
+    height: 45px;
+    font-size: 32px;
+  }
+
+  .unit {
+    margin-left: 10px; /* 输入框与单位之间的间距 */
+    font-size: 27px;
+  }
+  
+
+  .font1{
+    font-size: 27px;
+    font-weight: bold;
+
+  }
+
+  .analysis-button {
+    --background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+    color: white; /* 文字颜色为白色 */
+    border-radius: 8px; /* 圆角矩形 */
+    margin-top: 16px; /* 与上方区域的距离 */
+    width: 40%; /* 按钮宽度100% */
+    padding: 10px; /* 内边距 */
+    text-align: center; /* 文本居中 */
+    font-size: 20px;
+    font-weight: bold;
+  }
+
+  .center{
+    display: flex; /* 使用 flexbox 布局 */
+    justify-content: space-between; /* 在行内均匀分布 */
+    align-items: center;
+  }

+ 22 - 0
FitMind-app/src/app/page-bmi/page-bmi.component.spec.ts

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

+ 61 - 0
FitMind-app/src/app/page-bmi/page-bmi.component.ts

@@ -0,0 +1,61 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { AlertController, IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonInput, IonItem, IonLabel, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms'; // 导入 FormsModule
+import { CommonModule } from '@angular/common'; // 导入 CommonModule
+import { chevronBackOutline } from 'ionicons/icons';
+import { addIcons } from 'ionicons';
+addIcons({chevronBackOutline});
+@Component({
+  selector: 'app-page-bmi',
+  templateUrl: './page-bmi.component.html',
+  styleUrls: ['./page-bmi.component.scss'],
+  standalone: true,
+  imports:[IonHeader,IonToolbar,IonTitle,IonButton,IonButtons,IonContent,IonItem,IonLabel,IonInput,FormsModule,CommonModule,IonIcon]
+})
+export class PageBmiComponent  implements OnInit {
+
+  ngOnInit() {this.resetForm();} // 页面加载时清空数据
+  height: number|null =null; // 身高
+  weight: number|null =null; // 体重
+  bmi: number|null=null; // BMI 值
+
+  constructor(private router: Router,private alertController: AlertController) {this.resetForm();}
+
+  // 计算 BMI
+  async calculateBMI() {
+    if (this.height !=null && this.weight !=null) {
+      if(this.height >0 && this.weight >0) {
+      const heightInMeters = this.height / 100; // 将身高转换为米
+      this.bmi = this.weight / (heightInMeters * heightInMeters); // 计算 BMI
+      } else {
+        const alert = await this.alertController.create({
+          header: '身高体重不能为0!',
+          buttons: ['确定']
+        });
+        await alert.present(); // 显示弹出框
+      }
+    } else {
+      const alert = await this.alertController.create({
+        header: '身高体重不能为空!',
+        buttons: ['确定']
+      });
+      await alert.present(); // 显示弹出框
+      
+    }
+  }
+
+  //清空数据
+    // 清空输入数据
+    resetForm() {
+      this.height = null;
+      this.weight = null;
+      this.bmi = null;
+    }
+
+  // 返回上一个页面
+  goBack() {
+    this.router.navigate(['/tabs/tab3'], { replaceUrl: true }); // 使用 replaceUrl 强制重载页面
+  }
+}
+

+ 39 - 0
FitMind-app/src/app/page-evaluate/page-evaluate.component.html

@@ -0,0 +1,39 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-button (click)="goBack()" fill="clear">
+        <ion-icon aria-hidden="true" name="chevron-back" style="color: black; font-size: 24px; argin:left"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+    <ion-title>评价</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="submitEvaluation()" class="submit-button" color="light" id="present-alert">发表</ion-button>
+      <ion-alert
+  trigger="present-alert"
+  header="发表状态"
+  message="发表成功"
+  [buttons]="alertButtons"
+></ion-alert>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <div class="rating-container">
+    <span class="rating-label">评分:</span>
+    <edit-rating-star 
+      [score]="currentScore" 
+      [scoreMax]="5" 
+      (onScoreChange)="handleScoreChange($event)">
+    </edit-rating-star>
+    <span class="actual-score">{{ getScoreDescription(currentScore) }}</span>
+  </div>
+  <div class="textbox">
+  <ion-textarea  placeholder="你可以从AI的使用体验、速度、准确性、实用性、创新性以及全面性等方面进行评价,晒图分享你的使用体验~"[autoGrow]="true"></ion-textarea>
+</div>
+
+<div class="imgbox">
+</div>
+
+
+</ion-content>

+ 64 - 0
FitMind-app/src/app/page-evaluate/page-evaluate.component.scss

@@ -0,0 +1,64 @@
+.evaluation-content {
+    display: flex; /* 使用 flexbox 布局 */
+    flex-direction: column; /* 垂直方向排列 */
+    align-items: center; /* 水平方向居中 */
+    justify-content: center; /* 垂直方向居中 */
+    padding: 16px;
+    height: 100%; /* 填充整个内容区域 */
+  }
+  
+  ion-textarea {
+    width: 100%; /* 使文本区域宽度适应父容器 */
+    max-width: 500px; /* 设置最大宽度 */
+    min-height: 150px; /* 设置最小高度 */
+    margin-bottom: 16px; /* 文本区域和按钮之间的间距 */
+  }
+  
+  .submit-button {
+    background-color: orange; /* 橙色背景 */
+    color: white !important; /* 强制设置文字颜色为白色 */
+    border-radius: 20px; /* 椭圆形 */
+    padding: 6px 12px; /* 调整内边距以缩小按钮 */
+    font-size: 16px; /* 调整字体大小 */
+    height: auto; /* 自适应高度 */
+    min-width: 80px; /* 最小宽度 */
+  }
+  .rating-container {
+    display: flex; /* 使用 flexbox 布局 */
+    align-items: center; /* 垂直居中对齐 */
+   height:120px;
+    padding: 16px; /* 添加内边距 */
+    border-bottom: 1px solid grey; /* 只显示底部边框,设置为黑色 */
+    margin-bottom: 10px; /* 评分容器与文本之间的间距 */
+  }
+  
+  .rating-label {
+    display: flex; /* 使用 flexbox 布局 */
+    align-items: center; /* 垂直居中对齐 */
+    font-weight: bold; /* 加粗评分文本 */
+    font-size: 22px; /* 调整字体大小 */
+    margin-right: 5px; /* 评分文本与星星之间的间距 */
+  }
+  
+  .actual-score {
+    display: flex; /* 使用 flexbox 布局 */
+    align-items: center; /* 垂直居中对齐 */
+    margin-left: 50px; /* 分数与星星之间的间距 */
+    font-size: 15px; /* 调整字体大小 */
+    color: grey;
+  }
+
+  .textbox{
+    width: 100%; /* 使文本区域宽度适应父容器 */
+    height: 400px; /* 设置最大宽度 */
+    border-bottom: 1px solid grey; /* 只显示底部边框,设置为黑色 */
+  }
+
+  .imgbox{
+    width: 100%; /* 使文本区域宽度适应父容器 */
+    height: 200px; /* 设置最大宽度 */
+    background-image: url("../images/1..jpg");
+    background-size: contain; /* 图像填充方式 */
+  background-position: center; /* 图像居中 */
+  background-repeat: no-repeat; /* 不重复图像 */
+  }

+ 22 - 0
FitMind-app/src/app/page-evaluate/page-evaluate.component.spec.ts

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

+ 60 - 0
FitMind-app/src/app/page-evaluate/page-evaluate.component.ts

@@ -0,0 +1,60 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { IonicModule } from '@ionic/angular'; // 导入 IonicModule
+import { addIcons } from 'ionicons';
+import { chevronBack } from 'ionicons/icons';
+import { EditRatingStarComponent } from '../edit-rating-star/edit-rating-star.component';
+addIcons({chevronBack});
+
+@Component({
+  selector: 'app-page-evaluate',
+  templateUrl: './page-evaluate.component.html',
+  styleUrls: ['./page-evaluate.component.scss'],
+  standalone: true,
+
+  imports: [IonicModule,EditRatingStarComponent],
+  
+})
+export class PageEvaluateComponent  implements OnInit {
+
+  ngOnInit() {}
+  constructor(private router: Router) {}
+ 
+  // 返回按钮点击事件
+  goBack() {
+    this.router.navigate(['tabs/tab3']); 
+  }
+  submitEvaluation() {
+    // 处理评价提交逻辑
+    console.log('发表评价');
+    // 这里可以添加您希望执行的代码,例如将评价发送到服务器
+  }
+
+//星星打分
+currentScore: number = 0; // 初始分值
+
+handleScoreChange(newScore: number) {
+  this.currentScore = newScore;
+  console.log('新分值:', newScore); // 处理分值变化
+}
+
+getScoreDescription(score: number): string {
+  switch (score) {
+    case 1:
+      return '很差';
+    case 2:
+      return '差';
+    case 3:
+      return '一般';
+    case 4:
+      return '还不错';
+    case 5:
+      return '很满意';
+    default:
+      return ''; // 默认返回空字符串
+  }
+}
+alertButtons = ['退出'];
+
+
+}

+ 30 - 0
FitMind-app/src/app/page-test/page-test.component.html

@@ -0,0 +1,30 @@
+<ion-content >
+<h1 align="center">这是测试页</h1>
+  <ion-button (click)="goToDetail()">点击按钮</ion-button>
+  <ion-button (mouseup)="goToDetail()">抬起按钮</ion-button>
+  <app-edit-tag  (onTagChange)="setTagsValue($event)"></app-edit-tag>
+
+  <h1>父级页面</h1>
+  <ul>
+   @for(tag of editTags;track tag){
+     <li>{{tag}}</li>
+   }
+  </ul>
+
+  <ion-button (click)="goTestPage()">进入测试页</ion-button>
+
+  <h1>星星打分组件演示,分值:{{currentScore}}</h1>
+  <edit-rating-star 
+ [score]="currentScore" 
+ [scoreMax]="5" 
+ (onScoreChange)="handleScoreChange($event)">
+</edit-rating-star>
+
+<ion-list>
+  <ion-item>
+    <ion-textarea label="请输入:" placeholder="具体描述你的感受" [autoGrow]="true"></ion-textarea>
+  </ion-item>
+
+</ion-list>
+</ion-content>
+

+ 5 - 0
FitMind-app/src/app/page-test/page-test.component.scss

@@ -0,0 +1,5 @@
+button{
+    background-color: #007bff;
+    width: 50px;
+    height: 50px;
+}

+ 22 - 0
FitMind-app/src/app/page-test/page-test.component.spec.ts

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

+ 42 - 0
FitMind-app/src/app/page-test/page-test.component.ts

@@ -0,0 +1,42 @@
+import { Component, OnInit } from '@angular/core';
+import { IonicModule } from '@ionic/angular'; // 导入 IonicModule
+import {aperture,glasses,person, logoOctocat } from 'ionicons/icons';
+import { EditTagComponent } from '../edit-tag/edit-tag.component';
+import { Router } from '@angular/router';
+import { EditRatingStarComponent } from '../edit-rating-star/edit-rating-star.component';
+
+@Component({
+  selector: 'app-page-test',
+  templateUrl: './page-test.component.html',
+  styleUrls: ['./page-test.component.scss'],
+  standalone: true,
+  imports: [IonicModule,EditTagComponent,EditRatingStarComponent]
+})
+export class PageTestComponent  implements OnInit {
+
+  goToDetail(){
+    alert('hello ionic')
+  }
+  editTags:Array<string> = [];
+  setTagsValue(ev:any){
+    console.log("setTagsValue",ev)
+    this.editTags = ev;
+  }
+
+  constructor(private router: Router){}
+  ngOnInit(): void {
+    console.log('PageTestComponent ngOnInit');
+  }
+  // 跳转到测试页面
+goTestPage(){
+  this.router.navigate(['/tabs/test']);}
+  
+//星星打分
+  currentScore: number = 0; // 初始分值
+
+  handleScoreChange(newScore: number) {
+    this.currentScore = newScore;
+    console.log('新分值:', newScore); // 处理分值变化
+  }
+
+}

+ 159 - 49
FitMind-app/src/app/tab2/tab2.page.html

@@ -1,57 +1,167 @@
 <ion-header>
   <ion-toolbar>
-    <ion-title>运动计划生成</ion-title>
+    <ion-title>你的运动助手Neon</ion-title>
   </ion-toolbar>
 </ion-header>
 
 <ion-content>
-  <!-- 输入字段 -->
-  <ion-item>
-    <ion-label position="floating">体重</ion-label>
-    <ion-input [(ngModel)]="userWeight" type="number" placeholder="请输入您的体重"></ion-input>
-  </ion-item>
+  <div class="custom-segment">
+    <ion-button 
+      fill="clear" 
+      class="nav-button" 
+      [ngClass]="{'activated': isActivated('ai-outfit')}" 
+      (click)="selectSegment('ai-outfit')">
+      方案生成
+    </ion-button>
+    <ion-button 
+      fill="clear" 
+      class="nav-button" 
+      [ngClass]="{'activated': isActivated('ai-chat')}" 
+      (click)="selectSegment('ai-chat')">
+      Neon
+    </ion-button>
+    <ion-button 
+      fill="clear" 
+      class="nav-button" 
+      [ngClass]="{'activated': isActivated('test')}" 
+      (click)="selectSegment('test')">
+      测试
+    </ion-button>
+    <ion-button 
+      fill="clear" 
+      class="nav-button" 
+      [ngClass]="{'activated': isActivated('evaluate')}" 
+      (click)="selectSegment('evaluate')">
+      评价AI
+    </ion-button>
+  </div>
+
+  <div *ngIf="selectedSegment === 'ai-outfit'">
+    <div class="info-section">
+      <div class="input-row">
+        <div class="input-group">
+          <label>身高</label>
+          <input type="number" [(ngModel)]="height" placeholder="" class="input-box" />
+          <span class="unit">CM</span>
+        </div>
+        <div class="input-group">
+          <label>体重</label>
+          <input type="number" [(ngModel)]="weight" placeholder="" class="input-box" />
+          <span class="unit">KG</span>
+        </div>
+      </div>
+      
+      <div class="input-row">
+        <div class="input-group">
+          <label>年龄</label>
+          <input type="number" [(ngModel)]="age" placeholder="" class="input-box" />
+          <span class="unit">岁</span>
+        </div>
+        <div class="gender-section">
+          <label>性别</label>
+          <div class="gender-option" [ngClass]="{'selected': selectedGender === 'male'}" (click)="selectGender('male')">男</div>
+          <div class="gender-option" [ngClass]="{'selected': selectedGender === 'female'}" (click)="selectGender('female')">女</div>
+        </div>
+      </div>
+    </div>
+
+    <div class="textarea-section">
+      <ion-textarea 
+        id="comments" 
+        class="textarea-input" 
+        [(ngModel)]="comments" 
+        [placeholder]="placeholderText" 
+        [autoGrow]="true">
+      </ion-textarea>
+    </div>
+
+    <!-- 图片展示区域 -->
+    <div class="image-display-section">
+      @if(!images.length){
+        @if(imagineWork){
+          <h3>生成中 {{ countdownTimer }} s</h3>
+        }
+        @if(!imagineWork){
+          <h3>图片展示区</h3>
+        }
+      }
+           <!-- 生成结果 -->
+      @if(images.length) {
+        @for(imageUrl of images;track imageUrl){
+          <img [src]="imageUrl" alt="" srcset="">
+        }
+      }
+      <!-- 生成状态 -->
+
+    </div>
+
+<!-- 获取穿运动方案按钮 -->
+<ion-button fill="clear"
+  class="analysis-button" 
+  (click)="sendMessage()"
+  [ngClass]="{'generating': isGenerating}"  >
+
+  获取运动方案
+</ion-button>
+
+
+  
+ <!-- AI生成内容区域 -->
+ <div class="bigbox1" *ngIf="responseMsg">
+  <!-- 展示:返回消息内容 -->
+    <!-- 消息传输过程中,实时预览 -->
+    @if(!isComplete){
+      <div>{{responseMsg}}</div>
+    }
+    <!-- 消息传输完成后,实时预览Markdown格式 -->
+    @if(isComplete){
+      <fm-markdown-preview class="content-style" [content]="responseMsg"></fm-markdown-preview>
+    }
+  </div>
+ 
+
+  </div>
+
+  <!--AI聊天-->
+  <div *ngIf="selectedSegment === 'ai-chat'">
+<app-page-ai-chat></app-page-ai-chat>
+
+
+  </div>
   
-  <ion-item>
-    <ion-label position="floating">身高</ion-label>
-    <ion-input [(ngModel)]="userHeight" type="number" placeholder="请输入您的身高"></ion-input>
-  </ion-item>
-
-  <ion-item>
-    <ion-label position="floating">运动偏好</ion-label>
-    <ion-select [(ngModel)]="userPreference" placeholder="选择您的运动偏好">
-      <ion-select-option value="跑步">跑步</ion-select-option>
-      <ion-select-option value="游泳">游泳</ion-select-option>
-      <ion-select-option value="健身">健身</ion-select-option>
-    </ion-select>
-  </ion-item>
-
-  <ion-item>
-    <ion-label position="floating">运动目标</ion-label>
-    <ion-select [(ngModel)]="userGoal" placeholder="选择您的目标">
-      <ion-select-option value="减脂">减脂</ion-select-option>
-      <ion-select-option value="增肌">增肌</ion-select-option>
-      <ion-select-option value="维持">维持</ion-select-option>
-    </ion-select>
-  </ion-item>
-
-  <!-- 生成计划按钮 -->
-  <ion-button expand="full" (click)="generatePlan()">neno</ion-button>
-
-  <!-- 显示生成的运动计划 -->
-  <div *ngIf="responsePlan">
-    <ion-card>
-      <ion-card-header>
-        <ion-card-title>neno</ion-card-title>
-      </ion-card-header>
-      <ion-card-content>
-          @if(!isComplete){
-              <p>{{ responsePlan }}</p>
-          }
-        
-      </ion-card-content>
-    </ion-card>
-  </div>
-  @if(isComplete){
-  <fm-markdown-preview class="content-style" [content]="responsePlan"></fm-markdown-preview>
-  }
+  <!--测试-->
+  <div *ngIf="selectedSegment === 'test'">
+    <ion-button  (click)="goBmipage()">BMI</ion-button>
+
+  </div>
+
+  <div *ngIf="selectedSegment === 'evaluate'">
+    <div class="rating-container">
+      <span class="rating-label">评分:</span>
+      <edit-rating-star 
+        [score]="currentScore" 
+        [scoreMax]="5" 
+        (onScoreChange)="handleScoreChange($event)">
+      </edit-rating-star>
+      <span class="actual-score">{{ getScoreDescription(currentScore) }}</span>
+    </div>
+    <div class="textbox">
+    <ion-textarea  placeholder="你可以从AI的使用体验、速度、准确性、实用性、创新性以及全面性等方面进行评价,晒图分享你的使用体验~"[autoGrow]="true"></ion-textarea>
+  </div>
+
+ 
+  <div class="imgbox">
+    <img src="../../assets/images/1..jpg" alt="" srcset="">
+  </div>
+
+    <!-- 提交评价按钮 -->
+<ion-button fill="clear"
+class="analysis-button" 
+(click)="submitEvaluation()">
+  提交评价
+</ion-button>
+
+
+
+  </div>
 </ion-content>

+ 220 - 0
FitMind-app/src/app/tab2/tab2.page.scss

@@ -0,0 +1,220 @@
+.custom-segment {
+  display: flex;
+  justify-content: flex-start; /* 左对齐 */
+  margin-bottom: 16px; /* 与内容的间距 */
+}
+
+.nav-button {
+  flex: 1; /* 每个按钮占据相等的空间 */
+  margin-right: 16px; /* 按钮之间的间距 */
+  font-size: 16px; /* 默认字体大小 */
+  --color: #000; /* 默认字体颜色 */
+  --background: transparent; /* 背景透明 */
+  border: none; /* 去掉边框 */
+  text-align: left; /* 文本左对齐 */
+}
+
+.nav-button:last-child {
+  margin-right: 0; /* 最后一个按钮不需要右边距 */
+}
+
+.nav-button.activated {
+  font-weight: bold; /* 点击时字体加粗 */
+  font-size: 18px; /* 点击时字体变大 */
+  --color: black; /* 激活状态的字体颜色(可以根据需要调整) */
+}
+
+.info-section {
+  background-color: white; /* 背景色为白色 */
+  padding: 16px;
+  border-radius: 8px; /* 圆角 */
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影 */
+  margin-top: 16px; /* 与导航栏的间距 */
+}
+
+.input-row {
+  display: flex; /* 使用 flexbox 布局 */
+  justify-content: space-between; /* 在行内均匀分布 */
+  margin-bottom: 16px; /* 每一行的间距 */
+}
+
+.input-group {
+  display: flex;
+  align-items: center;
+}
+
+.input-group label {
+  flex: 1; /* 标签占据剩余空间 */
+}
+
+.input-box {
+  border-radius: 15px; /* 圆角矩形 */
+  border: none; /* 去掉边框 */
+  padding: 8px 12px; /* 内边距 */
+  margin-left: 8px; /* 标签与输入框之间的间距 */
+  background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+  width: 75px;
+}
+
+.unit {
+  margin-left: 8px; /* 输入框与单位之间的间距 */
+}
+
+.gender-section {
+  display: flex;
+  align-items: center;
+  margin-left: 16px; /* 性别选择与年龄输入框的间距 */
+}
+
+.gender-option {
+  border-radius: 20px; /* 圆角矩形 */
+  padding: 8px 12px; /* 内边距 */
+  background: rgb(217, 214, 214); /* 默认背景色为灰色 */
+  margin-left: 16px; /* 选项之间的间距 */
+  cursor: pointer; /* 鼠标悬停时显示为指针 */
+}
+
+.gender-option.selected {
+  background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+  color: white; /* 选中时字体颜色为白色 */
+}
+
+
+
+ion-content {
+  --background: #f8f8f8; /* 页面背景颜色 */
+  padding: 16px; /* 内容内边距 */
+}
+
+.textarea-section {
+  position: relative; /* 使得绝对定位的元素相对于此元素定位 */
+  margin-top: 8px; /* 与上方区域的距离 */
+}
+
+.textarea-input {
+  width: 100%; /* 宽度100% */
+  height: auto; /* 允许高度自适应 */
+  min-height: 120px; /* 设置最小高度 */
+  border-radius: 8px; /* 圆角矩形 */
+  border: none; /* 去掉边框 */
+  background-color: white; /* 背景颜色为白色 */
+  padding: 8px; /* 内边距 */
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
+  resize: none; /* 禁止调整大小 */
+  font-size: 17px; /* 字体大小 */
+  color: #000; /* 字体颜色 */
+}
+
+.image-display-section {
+  background-color: white; /* 背景颜色为白色 */
+  border-radius: 8px; /* 圆角矩形 */
+  padding: 16px; /* 内边距 */
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
+  margin-top: 8px; /* 与上方区域的距离 */
+  text-align: center; /* 内容居中 */
+  width: 100%;
+  height: 325px;
+}
+
+.image-display-section img {
+  width: 100%; /* 图片宽度100% */
+  height: 100%; /* 图片高度100% */
+  object-fit: cover; /* 保持比例填充 */
+  border-radius: 5px; /* 图片圆角 */
+  margin-top: 10px; /* 图片与标题之间的间距 */
+}
+
+.analysis-button {
+  --background: linear-gradient(to right, #ffcccb, #add8e6); /* 渐变背景色 */
+  color: white; /* 文字颜色为白色 */
+  border-radius: 8px; /* 圆角矩形 */
+  margin-top: 16px; /* 与上方区域的距离 */
+  width: 100%; /* 按钮宽度100% */
+  padding: 12px; /* 内边距 */
+  text-align: center; /* 文本居中 */
+  font-size: 17px;
+}
+
+.bigbox1 {
+  width: 100%;
+  background-color: #ffffff; // 背景色
+  border: 1px solid #e0e0e0; // 边框
+  border-radius: 12px; // 圆角
+  padding: 20px; // 盒子内边距
+  margin-top: 20px; // 与上方内容的间距
+}
+
+
+/* 当生成文本时的样式 */
+.analysis-button.generating {
+  --background: #808080; /* 生成时的背景颜色为灰色 */
+  color: white; /* 文字颜色保持为白色 */
+}
+
+
+//AI评价
+.evaluation-content {
+display: flex; /* 使用 flexbox 布局 */
+flex-direction: column; /* 垂直方向排列 */
+align-items: center; /* 水平方向居中 */
+justify-content: center; /* 垂直方向居中 */
+padding: 16px;
+height: 100%; /* 填充整个内容区域 */
+}
+
+ion-textarea {
+width: 100%; /* 使文本区域宽度适应父容器 */
+max-width: 500px; /* 设置最大宽度 */
+min-height: 150px; /* 设置最小高度 */
+margin-bottom: 16px; /* 文本区域和按钮之间的间距 */
+}
+
+.submit-button {
+background-color: orange; /* 橙色背景 */
+color: white !important; /* 强制设置文字颜色为白色 */
+border-radius: 20px; /* 椭圆形 */
+padding: 6px 12px; /* 调整内边距以缩小按钮 */
+font-size: 16px; /* 调整字体大小 */
+height: auto; /* 自适应高度 */
+min-width: 80px; /* 最小宽度 */
+}
+.rating-container {
+display: flex; /* 使用 flexbox 布局 */
+
+height:90px;
+padding-left: 16px;
+border-bottom: 1px solid grey; /* 只显示底部边框,设置为黑色 */
+margin-bottom: 10px; /* 评分容器与文本之间的间距 */
+margin-top: 0;
+}
+
+.rating-label {
+display: flex; /* 使用 flexbox 布局 */
+padding-top: 7px;
+font-weight: bold; /* 加粗评分文本 */
+font-size: 22px; /* 调整字体大小 */
+margin-right: 7px; /* 评分文本与星星之间的间距 */
+}
+
+.actual-score {
+display: flex; /* 使用 flexbox 布局 */
+padding-top: 12px;
+margin-left: 50px; /* 分数与星星之间的间距 */
+font-size: 15px; /* 调整字体大小 */
+color: grey;
+}
+
+.textbox{
+width: 100%; /* 使文本区域宽度适应父容器 */
+height: 300px; /* 设置最大宽度 */
+border-bottom: 1px solid grey; /* 只显示底部边框,设置为黑色 */
+}
+
+.imgbox{
+width: 100%; /* 使文本区域宽度适应父容器 */
+height: 150px; /* 设置最大宽度 */
+background-size: contain; /* 图像填充方式 */
+background-position: center; /* 图像居中 */
+background-repeat: no-repeat; /* 不重复图像 */
+margin-top: 50px;
+}

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

@@ -15,4 +15,4 @@ describe('Tab2Page', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
-});
+});

+ 254 - 53
FitMind-app/src/app/tab2/tab2.page.ts

@@ -1,66 +1,267 @@
-import { Component } from '@angular/core';
-import { IonicModule } from '@ionic/angular';
+import { ChangeDetectorRef, Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonItem, IonList, IonSelect, IonSelectOption, IonLabel, IonButton, IonTextarea, AlertController, IonSegment, IonSegmentButton, ModalController, IonAlert, IonButtons } from '@ionic/angular/standalone';
+import { ExploreContainerComponent } from '../explore-container/explore-container.component';
+import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms'; // 导入 FormsModule
-import { CommonModule } from '@angular/common'; // 导入 CommonModule
-import { FmodeChatCompletion,MarkdownPreviewModule } from 'fmode-ng'; // 你的外部 API 相关导入
+/** 引用:从fmode-ng库引用FmodeChatCompletion类 */
+import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatCompletion, FmodeChatMessage, MarkdownPreviewModule, openChatPanelModal } from 'fmode-ng';
+import { Router } from '@angular/router'; // 导入 Router
+import { DalleOptions, ImagineWork } from 'fmode-ng';
+import { PageAiChatComponent } from '../page-ai-chat/page-ai-chat.component';
+import { PageEvaluateComponent } from '../page-evaluate/page-evaluate.component';
+import { EditRatingStarComponent } from '../edit-rating-star/edit-rating-star.component';
+import { CloudUser } from 'src/lib/ncloud';
+import { openUserLoginModal } from 'src/lib/user/modal-user-login/modal-user-login.component';
+
+
 
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   standalone: true,
-  imports: [
-    IonicModule,
-    FormsModule,
-    CommonModule,
-    MarkdownPreviewModule  
-  ]
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent,IonItem,IonList,IonSelect,IonSelectOption,IonButton,CommonModule,IonTextarea,FormsModule,
+    MarkdownPreviewModule,IonSegment,IonSegmentButton,IonLabel,// ASR语音输入模块
+    FmChatModalInput,PageAiChatComponent,PageEvaluateComponent,EditRatingStarComponent,IonAlert,IonButtons],
 })
 export class Tab2Page {
-  // 用户输入的变量
-  userWeight: number = 70;   // 默认体重
-  userHeight: number = 175;  // 默认身高
-  userPreference: string = "跑步"; // 默认运动偏好
-  userGoal: string = "减脂"; // 默认目标
-
-  // 用于显示生成的运动计划
-  responsePlan: string = "";
-isComplete:boolean = false;
-  constructor() {}
-
-  ngOnInit() {}
-
-  // 当用户输入信息后,点击按钮生成运动计划
-  generatePlan() {
-    // 打印输入的信息,便于调试
-    console.log("用户体重:", this.userWeight);
-    console.log("用户身高:", this.userHeight);
-    console.log("用户运动偏好:", this.userPreference);
-    console.log("用户目标:", this.userGoal);
-
-    // 创建一个包含用户输入的消息数组
-    let promptMessage = `根据以下信息,生成一个适合的运动计划:
-      体重: ${this.userWeight}kg
-      身高: ${this.userHeight}cm
-      运动偏好: ${this.userPreference}
-      目标: ${this.userGoal}`;
-
-    // 调用 AI 接口生成运动计划
+  placeholderText: string = "职业:非必填\n喜好:非必填\n运动目标:非必填\n运动类型:非必填\n其他需求:";
+  selectedSegment: string = 'ai-outfit'; // 默认选中的导航项
+  selectedGender: string = ''; // 默认性别
+
+ 
+
+  // 新增变量
+  height: number | null = null; // 身高
+  weight: number | null = null; // 体重
+  age: number | null = null; // 年龄
+  comments: string = ''; // 用户评论
+    // 属性:组件内用于展示消息内容的变量
+    responseMsg:any = ""
+    // 用户输入提示词
+    userPrompt:string = ''
+    isComplete:boolean = false; // 定义完成状态属性,用来标记是否补全完成
+    isGenerating: boolean = false; // 按钮状态
+    imagineWork:ImagineWork|undefined
+    countdownTimer: number = 0; // 计时器初始值
+    timerInterval: any; // 用于存储定时器的引用
+  images:Array<string> = []
+    constructor(private alertController: AlertController,private modalCtrl:ModalController,
+      private router:Router, private cdRef:ChangeDetectorRef) { }
+
+  selectSegment(segment: string) {
+    this.selectedSegment = segment; // 更新选中的导航项
+  }
+
+  isActivated(segment: string): boolean {
+    return this.selectedSegment === segment; // 判断当前导航项是否被激活
+  }
+
+//AI穿搭
+  selectGender(gender: string) {
+    this.selectedGender = gender; // 更新选中的性别
+  }
+  async sendMessage(){
+    //如果未登录弹出登录框
+    let currentUser=new CloudUser()
+    if(!currentUser.id){
+      let user=await openUserLoginModal(this.modalCtrl)
+      if(!user?.id) return; // 未登录则返回
+
+
+    }
+
+    // 如果正在生成,弹出警告框并返回
+    if (this.isGenerating) {
+      const alert = await this.alertController.create({
+        header: '请等待AI回答完毕后再点击!',
+        buttons: ['确定']
+      });
+      await alert.present();
+      return; // 阻止继续执行
+    }
+    this.isComplete = false; // 重置完成状态
+    this.isGenerating = true; // 开始生成时设置为 true
+    this.countdownTimer = 0; // 重置计时器
+    this.imagineWork=undefined;
+    this.images = []; // 清空原来的图片数组
+
+    this.userPrompt =`有一位${this.selectedGender}用户,该用户${this.age}岁,身高${this.height}cm,体重${this.weight}kg,该用户的补充说明是
+    ${this.comments},请你作为一名专业的运动教练并严格按照给定的例子格式,为用户给出适合的个性化运动建议,再给出具体运动方案时里面的1.目标设定 2. 运动类型选择 3. 训练频率 4. 训练强度 5. 训练时间 6. 训练顺序与分配 7. 休息与恢复 8. 营养与水分 9. 心理调节与动机 10. 安全与预防伤害 11. 调整与反馈 12. 个性化与灵活性一定要唯一,不要出现与健康无关的话题。
+    例子:嘿,兄弟,我是Neon。想要变强是吗!这个我最擅长了,让我帮帮你。话不多说告诉我你的实力如何,接下来就是特训你可要听好了!
+
+**一、计算BMI值**
+
+BMI是衡量身体状况的重要指标,计算方法是体重(公斤)除以身高(米)的平方。假设你的体重是60KG,身高是175CM,那么计算结果为:BMI = 体重 ÷ 身高^² = 60KG ÷ (1.75^2)。你的BMI值挺正常的,还不错嘛!要继续保持良好的身体状态!接下来,我们来根据你的目标制定特训计划,让你在运动的路上更加顺利!💪
+
+好的,下面是更加详细的个性化运动方案,包括每个部分的具体内容:
+
+**个性化运动建议**
+
+**一、目标设定**
+- **短期目标**(1个月):提高心肺耐力,能够连续慢跑30分钟;增加力量,能够完成10个标准俯卧撑和15个深蹲。
+- **长期目标**(3个月):体重减轻5公斤,增肌,提升整体体能水平,能够完成更高强度的训练。
+
+**二、运动类型选择**
+- **有氧运动**:慢跑、游泳、骑自行车、跳绳等,选择你喜欢的有氧运动方式,以提高心肺功能。
+- **力量训练**:哑铃训练(卧推、深蹲、划船)、自重训练(俯卧撑、引体向上、平板支撑),以增强肌肉力量。
+
+**三、训练频率**
+- **每周5天训练**:
+  - 3天力量训练(如周一、周三、周五)
+  - 2天有氧运动(如周二、周四)
+  - 周末休息或进行轻松的活动(如散步、瑜伽)
+
+**四、训练强度**
+- **力量训练**:采用中等强度,选择能完成8-12次的重量,保持在70%-80%最大力量。
+- **有氧运动**:保持在中等强度,心率达到最大心率的60%-75%,可以通过谈话测试来判断。
+
+**五、训练时间**
+- **每次训练约60分钟**:
+  - 热身:5-10分钟(动态拉伸或慢跑)
+  - 力量训练:30-35分钟
+  - 有氧运动:20分钟
+  - 冷却与拉伸:5-10分钟
+
+**六、训练顺序与分配**
+- **力量训练顺序**:
+  1. 热身(5-10分钟)
+  2. 哑铃卧推:3组×8-12次
+  3. 深蹲:3组×10-15次
+  4. 划船:3组×8-12次
+  5. 俯卧撑:3组×尽可能多的次数
+  6. 平板支撑:3组×30-60秒
+  7. 冷却与拉伸(5-10分钟)
+
+- **有氧运动顺序**:
+  - 慢跑或游泳:持续20-30分钟,保持中等强度。
+
+**七、休息与恢复**
+- **力量训练后**:确保每个肌肉群有48小时的恢复时间。
+- **有氧运动后**:可以进行轻松活动,如散步,帮助身体恢复。
+- **每周至少休息1-2天**,避免过度训练。
+
+**八、营养与水分**
+- **训练前**:1小时吃一小碗燕麦粥或香蕉,提供能量。
+- **训练后**:补充高蛋白食物,如鸡胸肉、蛋白质奶昔,帮助肌肉恢复。
+- **水分**:每天保持至少2升水,训练期间每20分钟补水150-250毫升。
+
+**九、心理调节与动机**
+- **设定小目标**:每周记录进展,设定本周的训练目标(如增加重量或次数)。
+- **与朋友一起训练**:增加乐趣和动力,互相鼓励。
+
+**十、安全与预防伤害**
+- **热身**:每次训练前务必进行动态拉伸,准备身体。
+- **技术**:确保使用正确的姿势和技术,必要时请教专业教练。
+- **护具**:如有需要,佩戴护膝、护腕等保护装备。
+
+**十一、调整与反馈**
+- **每4周评估一次**:记录体重、力量和耐力的变化,分析训练效果。
+- **根据反馈调整计划**:如果目标未达成,适当增加训练强度或频率。
+
+**十二、个性化与灵活性**
+- **根据身体状况调整**:如果感到疲惫,可以减少训练强度或增加休息日。
+- **适应生活变化**:如有特殊情况,灵活调整训练计划,确保持续进步。
+
+---
+
+希望这个详细的运动方案能够帮助你实现你的健身目标!如果你有任何问题或需要进一步的调整,随时告诉我!
+    `
+    console.log("开始生成")
     let completion = new FmodeChatCompletion([
-      { role: "system", content: "你是一个运动专家,能够根据用户的体重、身高、运动偏好和目标,生成个性化的运动计划。" },
-      { role: "user", content: promptMessage }
-    ]);
-    
-    // 发送请求并订阅生成的计划
+      {role:"system",content:""},
+      {role:"user",content:this.userPrompt}
+    ])
     completion.sendCompletion().subscribe((message: any) => {
-      // 打印生成的计划内容,便于调试
-      console.log("生成的运动计划:", message.content);
-      
-      // 将生成的计划赋值给组件变量,用于展示
-      this.responsePlan = message.content;
-      if(message?.complete){
-        this.isComplete = true
+      // 打印消息体
+      console.log(message.content);
+      // 赋值消息内容给组件内属性
+      this.responseMsg = message.content;
+      if (message?.complete) { // 判断message为完成状态,则设置isComplete为完成
+        
+          this.isComplete = true;
+          
+           // 图片生成
+        this.startTimer(); // 启动计时器
+        this.imagineWork = new ImagineWork();
+        let PicturePrompt = `描述:${this.responseMsg}\n请你作为一名专业的运动教练,根据描述中的方案一描述严格生成对应的训练计划,格式要一致。`
+        let options:DalleOptions = {prompt:PicturePrompt}
+        this.imagineWork?.draw(options).subscribe(work=>{
+            console.log("imagineWork",work?.toJSON())
+            console.log("images",work?.get("images"))
+            if(work?.get("images")?.length){
+              this.images = work?.get("images");
+              this.stopTimer(); // 停止计时器
+              this.isGenerating = false; // 生成完成后设置为 false
+            }
+        })
       }
-    });
-  }
+  });
+
+}
+
+// 启动计时器
+startTimer() {
+  this.timerInterval = setInterval(() => {
+      this.countdownTimer++; // 每秒增加计时
+  }, 1000);
+}
+
+// 停止计时器
+stopTimer() {
+  clearInterval(this.timerInterval); // 清除定时器
+}
+
+
+
+//测试
+goBmipage() {
+  this.router.navigate(['/tabs/bmi']); // 跳转到测试BMI页面
+}
+
+
+//AI评价
+async submitEvaluation() {
+  // 处理评价提交逻辑
+  console.log('发表评价');
+  // 这里可以添加您希望执行的代码,例如将评价发送到服务器
+  // 弹出提示框
+  const alert = await this.alertController.create({
+    header: '提交成功,感谢你的评价!',
+    buttons: ['确定'] // 按钮
+  });
+
+  await alert.present(); // 显示弹出框
+}
+
+//星星打分
+currentScore: number = 0; // 初始分值
+
+handleScoreChange(newScore: number) {
+this.currentScore = newScore;
+console.log('新分值:', newScore); // 处理分值变化
+}
+
+getScoreDescription(score: number): string {
+switch (score) {
+  case 1:
+    return '很差';
+  case 2:
+    return '差';
+  case 3:
+    return '一般';
+  case 4:
+    return '还不错';
+  case 5:
+    return '很满意';
+  default:
+    return ''; // 默认返回空字符串
+}
+}
+alertButtons = ['退出'];
+
+
+
+
+
 }

+ 1 - 1
FitMind-app/src/app/tabs/tabs.page.html

@@ -7,7 +7,7 @@
 
     <ion-tab-button tab="tab2" href="/tabs/tab2">
       <ion-icon aria-hidden="true" name="bicycle"></ion-icon>
-      <ion-label>运动中心</ion-label>
+      <ion-label>Neon</ion-label>
     </ion-tab-button>
 
     <ion-tab-button tab="tab3" href="/tabs/tab3">

+ 37 - 0
FitMind-app/src/app/tabs/tabs.routes.ts

@@ -26,6 +26,42 @@ export const routes: Routes = [
         redirectTo: '/tabs/tab1',
         pathMatch: 'full',
       },
+      {
+        path: 'test',
+        loadComponent: () =>
+          import('../page-test/page-test.component').then((m) => m.PageTestComponent),
+      },
+      {
+        path: 'evaluate',
+        loadComponent: () =>
+          import('../page-evaluate/page-evaluate.component').then((m) => m.PageEvaluateComponent),
+      },
+      {
+        path: 'AIChat',
+        loadComponent: () =>
+          import('../page-ai-chat/page-ai-chat.component').then((m) => m.PageAiChatComponent),
+      },
+      {
+        path: 'bmi',
+        loadComponent: () =>
+          import('../page-bmi/page-bmi.component').then((m) => m.PageBmiComponent),
+      },
+      {
+        path: 'chat',
+        loadComponent: () =>
+          import('../ai-chat-component/ai-chat-component.component').then((m) => m.AiChatComponentComponent),
+      },
+      
+      {
+        path: '',
+        redirectTo: '/tabs/tab1',
+        pathMatch: 'full',
+      },
+            {
+        path: '',
+        redirectTo: '/tabs/tab1',
+        pathMatch: 'full',
+      },
     ],
   },
   {
@@ -33,4 +69,5 @@ export const routes: Routes = [
     redirectTo: '/tabs/tab1',
     pathMatch: 'full',
   },
+ 
 ];

+ 341 - 0
FitMind-app/src/lib/ncloud.ts

@@ -0,0 +1,341 @@
+// CloudObject.ts
+export class CloudObject {
+    id: string | null = null;
+    className: string;
+    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"].includes(key)) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save(): Promise<this> {
+        let method = "POST";
+        let url = `http://1.94.237.145:1339/parse/classes/${this.className}`;
+        
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+        
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "content-type": "text/plain;charset=UTF-8",
+                "x-parse-application-id": "ylj",
+                "Referer": "http://127.0.0.1:4040/",
+                "Referrer-Policy": "strict-origin-when-cross-origin"
+            },
+            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(): Promise<boolean> {
+        if (!this.id) return false;
+        
+        const response = await fetch(`http://1.94.237.145:1339/parse/classes/${this.className}/${this.id}`, {
+            headers: {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "x-parse-application-id": "ylj",
+                "Referer": "http://127.0.0.1:4040/",
+                "Referrer-Policy": "strict-origin-when-cross-origin"
+            },
+            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;
+    whereOptions: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    greaterThan(key: string, value: any) {
+        if (!this.whereOptions[key]) this.whereOptions[key] = {};
+        this.whereOptions[key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.whereOptions[key]) this.whereOptions[key] = {};
+        this.whereOptions[key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.whereOptions[key]) this.whereOptions[key] = {};
+        this.whereOptions[key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.whereOptions[key]) this.whereOptions[key] = {};
+        this.whereOptions[key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        this.whereOptions[key] = value;
+    }
+
+    async get(id: string): Promise<Record<string, any>> {
+        const url = `http://1.94.237.145.1339/parse/classes/${this.className}/${id}?`;
+        const response = await fetch(url, {
+            headers: {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "if-none-match": "W/\"127-p6+dUp7xlGAxzyvVsfuvVhMxjPo\"",
+                "x-parse-application-id": "ylj",
+                "Referer": "http://127.0.0.1:4040/",
+                "Referrer-Policy": "strict-origin-when-cross-origin"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+        const json = await response?.json();
+        return json || {};
+    }
+
+    async find(): Promise<CloudObject[]> {
+        let url = `http://1.94.237.145:1339/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.whereOptions).length) {
+            const whereStr = JSON.stringify(this.whereOptions);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "if-none-match": "W/\"127-p6+dUp7xlGAxzyvVsfuvVhMxjPo\"",
+                "x-parse-application-id": "ylj",
+                "Referer": "http://127.0.0.1:4040/",
+                "Referrer-Policy": "strict-origin-when-cross-origin"
+            },
+            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(): Promise<CloudObject | null> {
+        let url = `http://1.94.237.145:1339/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.whereOptions).length) {
+            const whereStr = JSON.stringify(this.whereOptions);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "accept": "*/*",
+                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+                "if-none-match": "W/\"127-p6+dUp7xlGAxzyvVsfuvVhMxjPo\"",
+                "x-parse-application-id": "ylj",
+                "Referer": "http://127.0.0.1:4040/",
+                "Referrer-Policy": "strict-origin-when-cross-origin"
+            },
+            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;
+    }
+
+    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/ylj/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://1.94.237.145:1337/parse/users/me`, {
+            headers: {
+                "x-parse-application-id": "hcx",
+                "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://1.94.237.145:1339/parse/login`, {
+            headers: {
+                "x-parse-application-id": "ylj",
+                "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/ylj/User",JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(`http://1.94.237.145:1339/parse/logout`, {
+            headers: {
+                "x-parse-application-id": "ylj",
+                "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/ylj/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://1.94.237.145:1339/parse/users`, {
+            headers: {
+                "x-parse-application-id": "ylj",
+                "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/ylj/User",JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+}

+ 98 - 0
FitMind-app/src/lib/recommend.js

@@ -0,0 +1,98 @@
+import { CloudObject, CloudQuery } from "../../../fashion-server/lib/ncloud.js";
+
+export class RecommendationService {
+    constructor() {
+        this.perferQuery = new CloudQuery('Perfer'); // 偏好数据存储在 perfer 类中
+    }
+
+    /**
+     * 获取用户的物品推荐
+     * @param userId 用户ID
+     * @param howMany 推荐数量
+     * @returns 推荐的物品ID列表
+     */
+    async getItemRecommendations(userId, howMany) {
+        // 获取用户的偏好数据
+        const userPreferences = await this.getUserPreferences(userId);
+        if (!userPreferences || userPreferences.length === 0) {
+            return [];
+        }
+
+        // 计算物品相似度
+        const similarItems = await this.calculateItemSimilarity(userPreferences);
+        // 获取推荐物品
+        return this.getTopRecommendations(similarItems, howMany);
+    }
+
+    /**
+     * 获取用户的偏好数据
+     * @param userId 用户ID
+     * @returns 用户偏好物品的列表
+     */
+    async getUserPreferences(userId) {
+        this.perferQuery.equalTo('user_id', userId); // 查询条件
+        const preferences = await this.perferQuery.find(); // 获取用户的偏好数据
+        return preferences; // 返回用户偏好物品列表
+    }
+
+    /**
+     * 计算物品相似度
+     * @param userPreferences 用户偏好物品
+     * @returns 物品相似度的映射
+     */
+    async calculateItemSimilarity(userPreferences) {
+        const itemSimilarity = new Map();
+
+        // 遍历用户偏好物品,计算与其他物品的相似度
+        for (const pref of userPreferences) {
+            const itemId = pref.item_id; // 直接访问属性
+            const preferenceValue = pref.preference; // 直接访问属性
+
+            // 获取所有物品的偏好数据
+            const allPreferences = await this.perferQuery.find();
+            for (const otherPref of allPreferences) {
+                const otherItemId = otherPref.item_id; // 直接访问属性
+                const otherPreferenceValue = otherPref.preference; // 直接访问属性
+
+                if (itemId !== otherItemId) { // 排除自身
+                    const similarityScore = this.computeSimilarity(preferenceValue, otherPreferenceValue);
+                    itemSimilarity.set(otherItemId, (itemSimilarity.get(otherItemId) || 0) + similarityScore);
+                }
+            }
+        }
+
+        return itemSimilarity;
+    }
+
+    /**
+     * 计算两个物品之间的相似度
+     * @param preference1 第一个物品的偏好值
+     * @param preference2 第二个物品的偏好值
+     * @returns 相似度分数
+     */
+    computeSimilarity(preference1, preference2) {
+        // 示例:使用简单的相似度计算(如绝对值差)
+        return Math.abs(preference1 - preference2); // 这里是一个简单的示例
+    }
+
+    /**
+     * 获取前 N 个推荐物品
+     * @param similarItems 物品相似度映射
+     * @param howMany 推荐数量
+     * @returns 推荐物品ID列表
+     */
+    getTopRecommendations(similarItems, howMany) {
+        // 将相似度映射转换为数组并排序
+        const sortedItems = Array.from(similarItems.entries()).sort((a, b) => b[1] - a[1]);
+        // 获取前 N 个物品ID
+        return sortedItems.slice(0, howMany).map(item => item[0]);
+    }
+}
+
+// 使用示例
+let recommendationService = new RecommendationService();
+recommendationService.getItemRecommendations('266024222614683648', 2).then(result => {
+    console.log(result);
+}).catch(error => {
+    console.error("Error fetching recommendations:", error);
+});

+ 112 - 0
FitMind-app/src/lib/recommend.ts

@@ -0,0 +1,112 @@
+import { CloudObject, CloudQuery } from "./ncloud";
+
+export class RecommendationService {
+    private perferQuery: CloudQuery; // 偏好查询对象
+
+    constructor() {
+        this.perferQuery = new CloudQuery('Perfer'); // 偏好数据存储在 perfer 类中
+    }
+
+    /**
+     * 获取用户的物品推荐
+     * @param userId 用户ID
+     * @param howMany 推荐数量
+     * @returns 推荐的物品ID列表
+     */
+    public async getItemRecommendations(userId: string, howMany: number): Promise<string[]> {
+        // 获取用户的偏好数据
+        const userPreferences = await this.getUserPreferences(userId);
+        
+        if (!userPreferences || userPreferences.length === 0) {
+            return [];
+        }
+
+        // 计算物品相似度
+        //console.log(userPreferences);
+        const similarItems = await this.calculateItemSimilarity(userPreferences);
+        // 获取推荐物品
+        return this.getTopRecommendations(similarItems, howMany);
+    }
+
+    /**
+     * 获取用户的偏好数据
+     * @param userId 用户ID
+     * @returns 用户偏好物品的列表
+     */
+    private async getUserPreferences(userId: string): Promise<CloudObject[]> {
+        this.perferQuery.equalTo('user_id', userId); // 查询条件
+        const preferences = await this.perferQuery.find(); // 获取用户的偏好数据
+        console.log(preferences);
+        return preferences; // 返回用户偏好物品列表
+    }
+
+    /**
+     * 计算物品相似度
+     * @param userPreferences 用户偏好物品
+     * @returns 物品相似度的映射
+     */
+    private async calculateItemSimilarity(userPreferences: CloudObject[]): Promise<Map<string, number>> {
+        const itemSimilarity: Map<string, number> = new Map();
+        // 遍历用户偏好物品,计算与其他物品的相似度
+        for (const pref of userPreferences) {
+            
+            const itemId = pref.get('item_id');
+            const preferenceValue = pref.get('perference');
+            //console.log(`${itemId} : ${preferenceValue}`);
+
+            // 获取所有物品的偏好数据
+            const allPreferences = await this.perferQuery.find();
+            for (const otherPref of allPreferences) {
+                const otherItemId = otherPref.get('item_id');
+                const otherPreferenceValue = otherPref.get('perference');
+                //console.log(`${otherItemId} : ${otherPreferenceValue}`);
+
+                if (itemId !== otherItemId) { // 排除自身
+                    console.log(`${itemId} : ${preferenceValue}`)
+                    console.log(`${otherItemId} : ${otherPreferenceValue}`);
+                    const similarityScore = this.computeSimilarity(preferenceValue, otherPreferenceValue);
+                    console.log(`${itemId} : ${similarityScore}`);
+                    itemSimilarity.set(otherItemId, (itemSimilarity.get(otherItemId) || 0) + similarityScore);
+                }
+            }
+        }
+
+        return itemSimilarity;
+    }
+
+    /**
+     * 计算两个物品之间的余弦相似度
+     * @param preference1 第一个物品的偏好值
+     * @param preference2 第二个物品的偏好值
+     * @returns 相似度分数
+     */
+    private computeSimilarity(preference1: number, preference2: number): number {
+        // 计算点积
+        const dotProduct = preference1 * preference2;
+
+        // 计算模
+        const magnitude1 = Math.sqrt(preference1 ** 2);
+        const magnitude2 = Math.sqrt(preference2 ** 2);
+
+        // 计算余弦相似度
+        if (magnitude1 === 0 || magnitude2 === 0) {
+            return 0; // 避免除以零的情况
+        }
+
+        return dotProduct / (magnitude1 * magnitude2);
+    }
+
+    /**
+     * 获取前 N 个推荐物品
+     * @param similarItems 物品相似度映射
+     * @param howMany 推荐数量
+     * @returns 推荐物品ID列表
+     */
+    private getTopRecommendations(similarItems: Map<string, number>, howMany: number): string[] {
+        // 将相似度映射转换为数组并排序
+        const sortedItems = Array.from(similarItems.entries()).sort((a, b) => b[1] - a[1]);
+        // 获取前 N 个物品ID
+        return sortedItems.slice(0, howMany).map(item => item[0]);
+    }
+}
+

+ 3 - 0
FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,3 @@
+<p>
+  modal-user-edit works!
+</p>

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


+ 22 - 0
FitMind-app/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();
+  });
+});

+ 15 - 0
FitMind-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-modal-user-edit',
+  templateUrl: './modal-user-edit.component.html',
+  styleUrls: ['./modal-user-edit.component.scss'],
+  standalone: true,
+})
+export class ModalUserEditComponent  implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {}
+
+}

+ 40 - 0
FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,40 @@
+<!-- 用户登录状态 -->
+<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="密码2" type="password"
+        value="password2"></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
FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
FitMind-app/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();
+  });
+});

+ 84 - 0
FitMind-app/src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -0,0 +1,84 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { IonicModule } from '@ionic/angular';
+import { ModalController } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-login',
+  templateUrl: './modal-user-login.component.html',
+  styleUrls: ['./modal-user-login.component.scss'],
+  standalone: true,
+  imports: [IonicModule],
+})
+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) {
+    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) { }
+
+  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")
+    } else {
+      console.log("登录失败");
+    }
+  }
+
+  async signup() {
+    if (!this.username || !this.password || !this.password2) {
+      console.log("请输入完整")
+      return
+    }
+    if (this.password2 != this.password) {
+      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;
+}

+ 6 - 0
agent-app/package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "agent-app",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}

+ 29 - 0
fearless-server/migration/import-data.js

@@ -0,0 +1,29 @@
+
+async function main(){
+    let fearlessUserList = await getlist();
+    console.log("fearlessUserList count",fearlessUserList?.length)
+}
+main()
+
+
+
+async function getlist(){
+
+
+let response = await fetch("http://dev.fmode.cn:1337/parse/classes/fearlessUser?", {
+    "headers": {
+      "accept": "*/*",
+      "accept-language": "zh-CN,zh;q=0.9",
+      "x-parse-application-id": "dev"
+    },
+    "referrer": "http://127.0.0.1:4040/",
+    "referrerPolicy": "strict-origin-when-cross-origin",
+    "body": null,
+    "method": "GET",
+    "mode": "cors",
+    "credentials": "omit"
+  });
+  let json = await response?.json();
+  console.log(json)
+  return json?.result || []
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff