Prechádzať zdrojové kódy

Merge branch 'master' of http://git.fmode.cn:3000/15879006906/s202226701040

15179588180 7 mesiacov pred
rodič
commit
ee214ed392

+ 1 - 1
README.md

@@ -1,3 +1,3 @@
 # 慧吃项目仓库
 - smarteat-app 前端代码
-- smarteat-prod 产品文档
+- smarteat-prod 产品文档

+ 4 - 4
smarteat-app/package-lock.json

@@ -22,7 +22,7 @@
         "@capacitor/keyboard": "6.0.3",
         "@capacitor/status-bar": "6.0.2",
         "@ionic/angular": "^8.0.0",
-        "fmode-ng": "^0.0.62",
+        "fmode-ng": "^0.0.63",
         "ionicons": "^7.2.1",
         "mammoth": "^1.8.0",
         "rxjs": "~7.8.0",
@@ -10399,9 +10399,9 @@
       "license": "ISC"
     },
     "node_modules/fmode-ng": {
-      "version": "0.0.62",
-      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.62.tgz",
-      "integrity": "sha512-F0RzEu47NgKpaHp/vBEzjsU4efJ1lKLAbbdPE5hltj1W1cDaeht/i6UlEidid4FAEdAg7c9rrQrLgOh/zUfCsg==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.63.tgz",
+      "integrity": "sha512-gTiDZO2CchcTYAmlaweapasqV/8PdhG2vizJNn5dYZyXjgtrjyW+KeW5k2EVyIDvM1+bMGjjhGmr76Fc0TElxw==",
       "license": "COPYRIGHT © 未来飞马 未来全栈 www.fmode.cn All RIGHTS RESERVED",
       "dependencies": {
         "tslib": "^2.3.0"

+ 38 - 14
smarteat-app/src/app/tab2/tab2.page.html

@@ -8,31 +8,55 @@
 
 <ion-content>
   <div class="input-group-container-wrapper">
+    <!-- 选择规划天数 -->
     <div class="input-group-container">
-      <h1>饮食群体</h1>
-      <ion-input [value]="qunti" (ionInput)="quntiInput($event)" placeholder="例如减肥群体、某某病患" autoGrow="true"></ion-input>
-    </div>
-    <div class="input-group-container">
-      <h1>饮食忌口</h1>
-      <ion-input [value]="jikou" (ionInput)="jikouInput($event)" placeholder="例如过敏食物" autoGrow="true"></ion-input>
+      <h1>规划天数</h1>
+      <ion-select [value]="planningDays"  
+      (ionChange)="planningDaysInput($event)"  
+      placeholder="请选择规划天数"
+      interface="popover"
+      required>
+        <ion-select-option value="1">1天</ion-select-option>
+        <ion-select-option value="2">2天</ion-select-option>
+        <ion-select-option value="3">3天</ion-select-option>
+        <ion-select-option value="4">4天</ion-select-option>
+        <ion-select-option value="5">5天</ion-select-option>
+        <ion-select-option value="6">6天</ion-select-option>
+        <ion-select-option value="7">7天</ion-select-option>
+        <ion-select-option value="8">8天</ion-select-option>
+        <ion-select-option value="9">9天</ion-select-option>
+        <ion-select-option value="10">10天</ion-select-option>
+        <ion-select-option value="11">11天</ion-select-option>
+        <ion-select-option value="12">12天</ion-select-option>
+        <ion-select-option value="13">13天</ion-select-option>
+        <ion-select-option value="14">14天</ion-select-option>
+      </ion-select>
     </div>
   </div>
+
   <!-- 文本域:生成提示词 -->
-   <div class="content-style">
+  <div class="content-style">
     <h1>需求描述</h1>
     <ion-textarea [value]="userPrompt" (ionInput)="promptInput($event)" placeholder="需求描述" autoGrow="true"></ion-textarea>
   </div>
+
   <!-- 按钮:执行消息生成函数 -->
   <ion-button (click)="sendMessage()" expand="block">饮食方案生成</ion-button>
-  
+
   <!-- 展示:返回消息内容 -->
-   @if(!isComplete){
-    <div class="response-container">{{responseMsg}}</div>
+  @if(!isComplete && responseMsg!="" && !isNew){
+    <!-- <div class="response-container">{{responseMsg}}</div> -->
+     <div class="response-container">正在为您生成饮食规划...<br>请不要走开哦</div>
    }
   
-  @if(isComplete){
-    <div class="response-container"><fm-markdown-preview class="content-style" [content]="responseMsg" ></fm-markdown-preview></div>
+  @if(!isComplete && responseMsg!="" && isNew){
+    <!-- <div class="response-container">{{responseMsg}}</div> -->
+     <div class="response-container">正根据您新的要求为您生成饮食规划...<br>请不要走开哦</div>
   }
-
+  
+  @if(isComplete ){
+     <div class="response-container">根据您的个人信息与需求为您生成了以下饮食规划<br><div class="content-style" [innerHTML]="responseMsg0"></div><!--<fm-markdown-preview class="content-style" [content]="responseMsg0" ></fm-markdown-preview> --></div>
+    <ion-button (click)="exportMealPlan()" expand="block" color="primary">导出饮食规划</ion-button>
+  }
+  
 </ion-content>
-

+ 1 - 10
smarteat-app/src/app/tab2/tab2.page.scss

@@ -47,16 +47,7 @@ ion-header {
     --background: white;
   }
   
-  
-  // ion-button {
-  //   --background: #33ffcc; /* 设置按钮背景色 */
-  //   --color: white; /* 设置按钮文字颜色 */
-  //   --border-radius: 8px; /* 设置按钮圆角 */
-  //   padding: 12px;
-  //   font-size: 1.1em;
-  //   margin-top: 20px;
-  //   transition: transform 0.2s ease-in-out;
-  // }
+
   
   ion-button:hover {
     transform: scale(1.05); /* 按钮悬停效果 */

+ 258 - 59
smarteat-app/src/app/tab2/tab2.page.ts

@@ -1,75 +1,274 @@
 import { Component } from '@angular/core';
+import { CloudSeMealPlan } from 'src/lib/cloudplans'; // 引入封装好的 CloudSeMealPlan 类
 import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
-import { IonButton, IonContent, IonHeader, IonInput, IonTextarea, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+import { CloudSeUser } from 'src/lib/cloudSeuser'; // 引入 CloudSeUser 类
 
+import { IonButton, IonContent, IonHeader, IonInput, IonSelect, IonSelectOption, IonTextarea, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
 @Component({
-  selector: 'app-tab2',
-  templateUrl: 'tab2.page.html',
-  styleUrls: ['tab2.page.scss'],
-  standalone: true,
-  imports: [
-    IonContent, IonHeader, IonTitle, IonToolbar, // 引入 IonicModule
-    IonButton,IonTextarea,IonInput,
-    MarkdownPreviewModule,
-  ],
+selector: 'app-tab2',
+templateUrl: 'tab2.page.html',
+styleUrls: ['tab2.page.scss'],
+standalone: true,
+imports: [
+IonContent, IonHeader, IonTitle, IonToolbar,
+IonButton,IonTextarea,IonInput,IonSelectOption,IonSelect,
+MarkdownPreviewModule,
+],
 })
 export class Tab2Page {
-  ngOnInit() {}
-  constructor() {}
 
-  // 用户输入提示词
-  qunti: string = "";
-  jikou: string = "";
-  userPrompt: string = "";
+constructor() {}
 
-  // 用户输入的需求
-  quntiInput(ev: any) {
-    this.qunti = ev.detail.value;
-  }
+ngOnInit() {
+this.loadUserData();
+this.setNextDay();
+}
+
+planningDays: string = "";
+userPrompt: string = "";
+currentDate = new Date();
+dateOnlyString: string = '';
+
+// 用户信息
+age: number | null = null;
+gender: string = '';
+height: number | null = null;
+weight: number | null = null;
+activityLevel: string = '';
+dietPreference: string = '';
+dietGroup: string = '';
+avatar: string | null = null;
+allergies: string = '';
+
+responseMsg: string = "";
+responseMsg0: string = "";
+responseMsg1: string = "";
+isComplete: boolean = false;
+isNew: boolean = false;
+
+async loadUserData() {
+const cloudSeUser = new CloudSeUser();
+const currentUserInfo = await cloudSeUser.getCurrentUserInfo();
+
+if (currentUserInfo) {
+  this.age = currentUserInfo.get('age') || null;
+  this.gender = currentUserInfo.get('gender') || '';
+  this.height = currentUserInfo.get('height') || null;
+  this.weight = currentUserInfo.get('weight') || null;
+  this.activityLevel = currentUserInfo.get('activityLevel') || '';
+  this.dietPreference = currentUserInfo.get('dietPreference') || '';
+  this.dietGroup = currentUserInfo.get('dietGroup') || '';
+  this.allergies = currentUserInfo.get('allergies') || '';
+} else {
+  console.error("未能加载当前用户信息");
+}
+}
+
+setNextDay() {
+const nextDay = new Date(this.currentDate);
+nextDay.setDate(this.currentDate.getDate() + 1);
+this.dateOnlyString = nextDay.toISOString().split('T')[0];
+}
+
+planningDaysInput(ev: any) {
+this.planningDays = ev.detail.value;
+}
+
+promptInput(ev: any) {
+this.userPrompt = ev.detail.value;
+}
+
+sendMessage() {
+console.log("create");
+
+this.responseMsg = "";
+this.responseMsg0="";
+this.isComplete = false;
+
+let newPrompt;
+if(this.responseMsg1==""){
+  newPrompt = `
+  你是一名专业的饮食营养规划师,拥有丰富的营养学背景和实践经验。你的工作是为不同需求的人群提供个性化的饮食规划,帮助他们实现健康目标,如减肥、增肌、健康维护或疾病管理。
+  你需要根据用户的用户信息和需求为客户设计量身定制具体的饮食方案。
+  当前用户的饮食需求是:${this.userPrompt},
+  现在告诉你用户描述:该用户是一个年龄为${this.age}岁,身高和体重分别为${this.height}cm,${this.weight}kg的${this.gender}性。
+  他的活动水平${this.activityLevel},饮食偏好${this.dietPreference},并且他的过敏信息为${this.allergies}。
+  重点是他希望作为一位${this.dietGroup}群体的人,希望你为他或她规划以为${this.dateOnlyString}开始${this.planningDays}天的详细的饮食规划(食物内容以日常生活常见食物为主,每次规划尽量不同,要多样化)。
+  生成的结果不需要再复述用户的信息,直接以形式输出格式为(day(date类型)、breakfast(string类型)、lunch(string类型)、dinner(string类型)、notes(string类型))结构化JSON格式输出
+  (结果不需要用\`\`\`json和\`\`\`将结果包裹,当天数大于一时,用[]括起多天的规划、并且之间用,隔开)
+  `;
+}else{
+  newPrompt = `
+  你是一名专业的饮食营养规划师,拥有丰富的营养学背景和实践经验。你的工作是为不同需求的人群提供个性化的饮食规划,帮助他们实现健康目标,如减肥、增肌、健康维护或疾病管理。
+  你需要根据用户的用户信息和需求为客户设计量身定制具体的饮食方案。
+  现在告诉你用户描述:该用户是一个年龄为${this.age}岁,身高和体重分别为${this.height}cm,${this.weight}kg的${this.gender}性。
+  他的活动水平${this.activityLevel},饮食偏好${this.dietPreference},并且他的过敏信息为${this.allergies}。
+  重点是他希望作为一位${this.dietGroup}群体的人,希望你为他或她规划以为${this.dateOnlyString}开始${this.planningDays}天的详细的饮食规划(食物内容以日常生活常见食物为主)。
+  你可以在该${this.responseMsg1}饮食规划的基础上,再根据该需求${this.userPrompt},生成新的饮食规划,以下格式不变。
+  生成的结果不需要再复述用户的信息,直接以形式输出格式为(day(date类型)、breakfast(string类型)、lunch(string类型)、dinner(string类型)、notes(string类型))结构化JSON格式输出
+  (结果不需要用\`\`\`json和\`\`\`将结果包裹,当天数大于一时,用[]括起多天的规划、并且之间用,隔开)
+  `;
+
+}
+
+
+let completion = new FmodeChatCompletion([
+  { role: "system", content: "" },
+  { role: "user", content: newPrompt }
+]);
+
+completion.sendCompletion().subscribe((message: any) => {
+  console.log(message.content);
+  this.responseMsg = message.content;
+
+  if (message?.complete) {
+    this.isComplete = true;
+    this.isNew = true;
+    this.responseMsg0=this.responseMsg;
+    this.responseMsg1=this.responseMsg;
+    // 1. 替换字段名
+    this.responseMsg0 = this.responseMsg0
+    .replace(/\bday\b/g, '日期')
+    .replace(/\bbreakfast\b/g, '早餐')
+    .replace(/\blunch\b/g, '午餐')
+    .replace(/\bdinner\b/g, '晚餐')
+    .replace(/\bnotes\b/g, '注意事项');
+
+    // 2. 将 " 后面的 , 替换为换行
+    this.responseMsg0 = this.responseMsg0.replace(/",/g, '<br>');
+
+    // 3. 将 } 后面的 , 替换为换行
+    this.responseMsg0 = this.responseMsg0.replace(/},/g, '<br>-------------------<br>');
+
+    // 4. 删除多余的空格(包括首尾空格和中间多余的空格)
+    this.responseMsg0 = this.responseMsg0.replace(/\s+/g, ' ').trim();
+
+    // 5. 删除所有的双引号、方括号、花括号
+    this.responseMsg0 = this.responseMsg0
+    .replace(/["\[\]{}]/g, ''); // 删除双引号、方括号、花括号
 
-  jikouInput(ev: any) {
-    this.jikou = ev.detail.value;
   }
+});
+}
+
+// 保存饮食规划到数据库
+async saveMealPlan() {
+const cloudSeMealPlan = new CloudSeMealPlan();
+
+try {
+  // 清理 responseMsg,并处理数据结构
+  const processedMealPlan = JSON.parse(this.responseMsg).map((plan: any) => {
+    return {
+      day: plan.day,
+      breakfast: plan.breakfast,
+      lunch: plan.lunch,
+      dinner: plan.dinner,
+      notes: plan.notes
+    };
+  });
 
-  promptInput(ev: any) {
-    this.userPrompt = ev.detail.value;
+  // 将处理后的每一天的饮食规划保存到数据库
+  for (let meal of processedMealPlan) {
+    const savedMealPlan = await cloudSeMealPlan.saveMealPlan(meal);
+    if (savedMealPlan) {
+      console.log('饮食规划保存成功:', savedMealPlan);
+    } else {
+      console.error('饮食规划保存失败');
+      // console.log(this.responseMsg)
+    }
   }
+} catch (error) {
+  console.error('保存饮食规划时发生错误:', error);
+}
+}
+
+async exportMealPlan() {
+  if (this.isComplete) {
+    try {
+      const cloudSeMealPlan = new CloudSeMealPlan();
+      const cloudUser = new CloudUser();
+      const cloudSeUser = new CloudSeUser();
+
+      const currentUser = await cloudUser.current();
+      if (!currentUser) {
+        console.error("用户未登录");
+        return;
+      }
+
+      // 获取当前用户的计划天数
+      const currentUserInfo = await cloudSeUser.getCurrentUserInfo();
+      const planDaysFromUserInfo = currentUserInfo?.get('planDays');
 
-  // 属性:组件内用于展示消息内容的变量
-  responseMsg: string = "";
-  // 是否完成生成
-  isComplete: boolean = false;
-
-  sendMessage() {
-    console.log("create");
-
-    // 每次生成新的饮食规划时,首先清空旧的规划
-    this.responseMsg = "";  // 清空旧的响应消息
-    this.isComplete = false; // 重置完成状态
-    // 拼接用户输入的需求
-    let newPrompt = `
-      你是一名专业的饮食营养规划师,拥有丰富的营养学背景和实践经验。你的工作是为不同需求的人群提供个性化的饮食规划,帮助他们实现健康目标,如减肥、增肌、健康维护或疾病管理。
-      你可以根据用户的需求为客户设计量身定制具体的饮食方案。
-      当前来咨询的用户群体是${this.qunti},用户的饮食忌口是${this.jikou},需求是${this.userPrompt}.
-    `;
-
-    // 创建消息请求,传递完整的需求
-    let completion = new FmodeChatCompletion([
-      { role: "system", content: "" },
-      { role: "user", content: newPrompt }
-    ]);
-
-    // 执行生成并订阅返回结果
-    completion.sendCompletion().subscribe((message: any) => {
-      console.log(message.content);
-
-      // 如果有新内容,更新组件的响应消息,覆盖旧的规划
-      this.responseMsg = message.content;
-
-      // 如果返回内容标记为完成,则更新状态
-      if (message?.complete) {
-        this.isComplete = true;
+      // 如果 planDays 存在且不为空,则删除旧的饮食规划
+      if (planDaysFromUserInfo && planDaysFromUserInfo > 0) {
+        console.log(`计划天数为 ${planDaysFromUserInfo},删除旧的饮食规划`);
+        // 直接使用 currentUser 对象,而不需要单独声明 currentUserId
+        
+        // 根据 planDays 执行循环删除操作
+        for (let i = 0; i < planDaysFromUserInfo; i++) {
+          const existingMealPlan = await cloudSeMealPlan.getCurrentUserMealPlan();
+          if (existingMealPlan && existingMealPlan.id) {
+            await cloudSeMealPlan.deleteMealPlan(existingMealPlan.id);
+            console.log('已删除一条旧的饮食规划');
+          }
+        }
+      }else{
+        console.error('无旧的饮食规划,无法删除');
       }
-    });
+
+      // 处理新的饮食规划
+      const processedMealPlan = JSON.parse(this.responseMsg).map((plan: any) => {
+        return {
+          day: plan.day,
+          breakfast: plan.breakfast,
+          lunch: plan.lunch,
+          dinner: plan.dinner,
+          notes: plan.notes
+        };
+      });
+
+      // 标志位,表示饮食规划是否成功保存
+      let mealPlansSavedSuccessfully = true;
+
+      // 保存新的饮食规划
+      for (let meal of processedMealPlan) {
+        const savedMealPlan = await cloudSeMealPlan.saveMealPlan(meal);
+        if (savedMealPlan) {
+          console.log('饮食规划已成功导出并保存');
+        } else {
+          console.error('导出饮食规划失败');
+          mealPlansSavedSuccessfully = false;
+          break;
+        }
+      }
+
+      // 如果饮食规划保存失败,跳过更新 planDays
+      if (!mealPlansSavedSuccessfully) {
+        console.error("饮食规划保存失败,规划天数不会更新");
+        return;
+      }
+
+      // 更新用户的 planDays 字段
+      const planDaysNumber = parseInt(this.planningDays, 10);
+      if (isNaN(planDaysNumber)) {
+        console.error("规划天数无效,无法保存");
+        return;
+      }
+
+      const updatedUser = await cloudSeUser.updateUserInfo({ planDays: planDaysNumber });
+      if (updatedUser) {
+        console.log('用户的计划天数已更新');
+      } else {
+        console.error('更新用户计划天数失败');
+      }
+
+    } catch (error) {
+      console.error('导出饮食规划时发生错误:', error);
+    }
+  } else {
+    console.error('饮食规划尚未生成,无法导出');
   }
 }
+
+}

+ 117 - 0
smarteat-app/src/lib/cloudplans.ts

@@ -0,0 +1,117 @@
+import { CloudObject, CloudQuery, CloudUser } from './ncloud';
+
+export class CloudSeMealPlan extends CloudObject {
+  constructor() {
+    super("seMealPlans");
+  }
+
+  /** 获取单个饮食规划 */
+  async getMealPlan(id: string) {
+    const url = `http://dev.fmode.cn:1337/parse/classes/seMealPlans/${id}?`;
+
+    const response = await fetch(url, {
+      headers: {
+        "x-parse-application-id": "dev"
+      },
+      method: "GET",
+      mode: "cors",
+      credentials: "omit"
+    });
+
+    const result = await response?.json();
+    if (result?.error) {
+      console.error(result?.error);
+      return null;
+    }
+
+    return this.dataToObj(result);
+  }
+
+  /** 获取当前登录用户的饮食规划信息 */
+  async getCurrentUserMealPlan() {
+    const cloudUser = new CloudUser();
+    const currentUser = await cloudUser.current();
+
+    if (!currentUser) {
+      console.error("用户未登录");
+      return null;
+    }
+
+    const currentUserId = currentUser.objectId;
+
+    // 通过 _User 的 objectId 查询 seMealPlans 表中的记录
+    const query = new CloudQuery("seMealPlans");
+    query.equalTo("user", { "__type": "Pointer", "className": "_User", "objectId": currentUserId });
+
+    const seMealPlan = await query.first();
+    return seMealPlan;
+  }
+
+  /** 创建或保存饮食规划信息 */
+  async saveMealPlan(mealPlanData: Record<string, any>) {
+    const cloudUser = new CloudUser();
+    const currentUser = await cloudUser.current();
+
+    if (!currentUser) {
+      console.error("用户未登录");
+      return null;
+    }
+
+    const userPointer = { "__type": "Pointer", "className": "_User", "objectId": currentUser.objectId };
+
+    const newMealPlan = {
+      ...mealPlanData,
+      user: userPointer // 将当前用户作为外键关联到 seMealPlans 表
+    };
+
+    const url = `http://dev.fmode.cn:1337/parse/classes/seMealPlans`;
+
+    const response = await fetch(url, {
+      headers: {
+        "Content-Type": "application/json",
+        "x-parse-application-id": "dev"
+      },
+      method: "POST",
+      body: JSON.stringify(newMealPlan),
+      mode: "cors",
+      credentials: "omit"
+    });
+
+    const result = await response?.json();
+    if (result?.error) {
+      console.error(result?.error);
+      return null;
+    }
+
+    return this.dataToObj(result);
+  }
+
+  /** 更新饮食规划信息 */
+  async updateMealPlan(updatedData: Record<string, any>, mealPlanId: string) {
+    const mealPlan = new CloudSeMealPlan();
+    mealPlan.id = mealPlanId;
+    mealPlan.set(updatedData);
+
+    await mealPlan.save(); // 调用 save 方法更新数据
+    return mealPlan;
+  }
+
+  /** 删除饮食规划 */
+  async deleteMealPlan(mealPlanId: string) {
+    const mealPlan = new CloudSeMealPlan();
+    mealPlan.id = mealPlanId;
+    await mealPlan.destroy();
+
+    return true;
+  }
+
+  /** 将查询结果转换为 CloudObject */
+  private dataToObj(result: any): CloudObject {
+    let mealPlanObject = new CloudObject(this.className);
+    mealPlanObject.set(result);
+    mealPlanObject.id = result.objectId;
+    mealPlanObject.createdAt = result.createdAt;
+    mealPlanObject.updatedAt = result.updatedAt;
+    return mealPlanObject;
+  }
+}