cpy 2 months ago
parent
commit
bd59f7f226

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

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

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

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

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


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

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

@@ -0,0 +1,90 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonicModule } from '@ionic/angular';
+import { ModalController } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/app/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){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) { }
+
+  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.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 159 - 0
FitMind-app/src/app/page-edit/page-edit.component.html

@@ -0,0 +1,159 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-button color="medium" (click)="cancel()">取消</ion-button>
+    </ion-buttons>
+    <ion-title>编辑个人信息</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="confirm()" [strong]="true">确认</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <!-- 头像上传区域 -->
+  <ion-item>
+    <ion-label>头像 <span style="color: red;">*</span></ion-label>
+    <ion-avatar slot="start">
+      <!-- 显示头像,如果没有头像,显示默认头像 -->
+      <img [src]="avatar || 'assets/default-avatar.png'" />
+    </ion-avatar>
+    <ion-button fill="outline" slot="end" (click)="uploadAvatar()">上传头像</ion-button>
+  </ion-item>
+
+  <!-- 用户名输入框 -->
+  <ion-item>
+    <ion-label>姓名 <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="name" 
+      placeholder="请输入姓名" 
+      required>
+    </ion-input>
+  </ion-item>
+
+  <!-- 邮箱输入框 -->
+  <ion-item>
+    <ion-label>邮箱 <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="email" 
+      placeholder="请输入邮箱" 
+      type="email" 
+      required
+      [pattern]="emailPattern">
+    </ion-input>
+  </ion-item>
+
+  <!-- 电话输入框 -->
+  <ion-item>
+    <ion-label>电话 <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="phone" 
+      placeholder="请输入电话号码" 
+      type="tel" 
+      required
+      [pattern]="phonePattern">
+    </ion-input>
+  </ion-item>
+
+  <!-- 地址输入框 -->
+  <ion-item>
+    <ion-label>地址</ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="address" 
+      placeholder="请输入地址">
+    </ion-input>
+  </ion-item>
+
+  <!-- 年龄输入框 -->
+  <ion-item>
+    <ion-label>年龄 <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="age" 
+      placeholder="请输入年龄" 
+      type="number" 
+      required 
+      [min]="0" 
+      [max]="150">
+    </ion-input>
+  </ion-item>
+
+  <!-- 性别选择框 -->
+  <ion-item>
+    <ion-label>性别 <span style="color: red;">*</span></ion-label>
+    <ion-select 
+      [(ngModel)]="gender" 
+      interface="popover" 
+      required>
+      <ion-select-option value="male">男</ion-select-option>
+      <ion-select-option value="female">女</ion-select-option>
+      <ion-select-option value="other">其他</ion-select-option>
+    </ion-select>
+  </ion-item>
+
+  <!-- 身高输入框 -->
+  <ion-item>
+    <ion-label>身高(cm) <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="height" 
+      placeholder="请输入身高" 
+      type="number" 
+      required
+      [min]="0" 
+      [pattern]="heightPattern">
+    </ion-input>
+  </ion-item>
+
+  <!-- 体重输入框 -->
+  <ion-item>
+    <ion-label>体重(kg) <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="weight" 
+      placeholder="请输入体重" 
+      type="number" 
+      required
+      [min]="0" 
+      [pattern]="weightPattern">
+    </ion-input>
+  </ion-item>
+
+  <!-- 活动水平选择框 -->
+  <ion-item>
+    <ion-label>活动水平</ion-label>
+    <ion-select [(ngModel)]="activityLevel" interface="popover">
+      <ion-select-option value="low">低</ion-select-option>
+      <ion-select-option value="moderate">中</ion-select-option>
+      <ion-select-option value="high">高</ion-select-option>
+    </ion-select>
+  </ion-item>
+
+  <!-- 饮食偏好选择框 -->
+  <ion-item>
+    <ion-label>饮食偏好 <span style="color: red;">*</span></ion-label>
+    <ion-select 
+      [(ngModel)]="dietPreference" 
+      interface="popover" 
+      required>
+      <ion-select-option value="vegetarian">素食</ion-select-option>
+      <ion-select-option value="nonVegetarian">非素食</ion-select-option>
+      <ion-select-option value="vegan">纯素</ion-select-option>
+      <ion-select-option value="glutenFree">无麸质</ion-select-option>
+    </ion-select>
+  </ion-item>
+
+  <!-- 饮食群体输入框 -->
+  <ion-item>
+    <ion-label>饮食群体 <span style="color: red;">*</span></ion-label>
+    <ion-input 
+      labelPlacement="stacked" 
+      [(ngModel)]="dietGroup" 
+      placeholder="请输入饮食群体">
+    </ion-input>
+  </ion-item>
+</ion-content>

+ 124 - 0
FitMind-app/src/app/page-edit/page-edit.component.scss

@@ -0,0 +1,124 @@
+/* 设置整体页面的 padding */
+ion-content {
+  --padding-start: 16px;
+  --padding-end: 16px;
+  --padding-top: 16px;
+  --padding-bottom: 16px;
+}
+
+/* 每个表单项之间的间距 */
+ion-item {
+  --inner-padding-start: 12px;
+  --inner-padding-end: 12px;
+  margin-bottom: 16px;
+}
+
+/* 为每个必填项添加红色星号,方便用户识别 */
+ion-label span {
+  color: red;
+  font-weight: bold;
+  margin-left: 5px;
+}
+
+/* 设置头像上传区域的样式 */
+ion-avatar {
+  margin-right: 16px;
+  border-radius: 50%;
+  overflow: hidden;
+}
+
+ion-item ion-avatar {
+  margin-top: 10px;
+}
+
+/* 上传按钮的样式 */
+ion-button[fill="outline"] {
+  font-size: 14px;
+  padding: 8px 16px;
+  margin-top: 8px;
+}
+
+/* 输入框的样式 */
+ion-input, ion-select {
+  font-size: 14px;
+  padding: 10px 16px;
+  border-radius: 8px;
+  background-color: #f5f5f5;
+}
+
+/* 在输入框聚焦时设置边框颜色 */
+ion-input:focus, ion-select:focus {
+  --border-color: #3880ff;
+}
+
+/* 标题栏的样式 */
+ion-toolbar {
+  --background: #f4f4f4;
+  --color: #333;
+  padding: 10px 20px;
+}
+
+/* 调整按钮的外观 */
+ion-buttons ion-button {
+  font-size: 16px;
+  padding: 10px 16px;
+}
+
+/* 设置页面内容顶部和底部的间距 */
+ion-content {
+  --padding-start: 16px;
+  --padding-end: 16px;
+  --padding-top: 16px;
+  --padding-bottom: 16px;
+}
+
+/* 活动水平选择框的样式 */
+ion-select {
+  font-size: 14px;
+  padding: 10px 16px;
+}
+
+/* 优化输入框和选择框的整体样式 */
+ion-select, ion-input {
+  font-size: 16px;
+  border-radius: 8px;
+  background-color: #f8f8f8;
+  margin-top: 8px;
+}
+
+ion-item {
+  --inner-padding-start: 12px;
+  --inner-padding-end: 12px;
+}
+
+ion-label {
+  font-size: 16px;
+  font-weight: 600;
+}
+
+/* 设置表单项间的间隔 */
+ion-item {
+  margin-bottom: 20px;
+}
+
+/* 页面底部按钮的样式 */
+ion-toolbar ion-buttons {
+  padding: 8px 0;
+}
+
+ion-button[strong] {
+  background-color: #3880ff;
+  color: white;
+  font-weight: bold;
+  padding: 10px 20px;
+  border-radius: 5px;
+}
+
+ion-button[strong]:hover {
+  background-color: #007bff;
+}
+
+ion-button[color="medium"] {
+  font-weight: normal;
+  color: #666;
+}

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

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

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

@@ -0,0 +1,132 @@
+import { Component, OnInit } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+import { IonicModule } from '@ionic/angular';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+  selector: 'app-page-edit',
+  templateUrl: './page-edit.component.html',
+  styleUrls: ['./page-edit.component.scss'],
+  imports: [IonicModule, FormsModule],
+  standalone: true,
+})
+export class PageEditComponent implements OnInit {
+  
+  // 初始化表单字段
+  name: string = '';
+  email: string = '';
+  phone: string = '';
+  address: 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;
+
+  // 正则表达式
+  emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
+  phonePattern = /^[0-9]{10,15}$/;
+  heightPattern = /^[1-9][0-9]{1,2}$/;
+  weightPattern = /^[1-9][0-9]{1,2}$/;
+
+  constructor(private modalCtrl: ModalController) {}
+
+  ngOnInit(): void {
+    this.loadSavedData();  // 加载上次保存的数据
+  }
+
+  // 加载上次保存的数据
+  loadSavedData() {
+    const savedData = JSON.parse(localStorage.getItem('userData') || '{}');
+
+    if (savedData) {
+      // 使用本地存储的数据填充表单
+      this.name = savedData.name || '';
+      this.email = savedData.email || '';
+      this.phone = savedData.phone || '';
+      this.address = savedData.address || '';
+      this.age = savedData.age || null;
+      this.gender = savedData.gender || '';
+      this.height = savedData.height || null;
+      this.weight = savedData.weight || null;
+      this.activityLevel = savedData.activityLevel || '';
+      this.dietPreference = savedData.dietPreference || '';
+      this.dietGroup = savedData.dietGroup || '';
+      this.avatar = savedData.avatar || null;
+    }
+  }
+
+  // 保存数据至本地存储
+  saveData() {
+    const userData = {
+      name: this.name,
+      email: this.email,
+      phone: this.phone,
+      address: this.address,
+      age: this.age,
+      gender: this.gender,
+      height: this.height,
+      weight: this.weight,
+      activityLevel: this.activityLevel,
+      dietPreference: this.dietPreference,
+      dietGroup: this.dietGroup,
+      avatar: this.avatar
+    };
+    localStorage.setItem('userData', JSON.stringify(userData)); // 将数据保存至本地
+  }
+
+  cancel() {
+    return this.modalCtrl.dismiss(null, 'cancel');
+  }
+
+  confirm() {
+    // 验证表单内容是否完整
+    if (!this.name || !this.email || !this.phone || !this.age || !this.gender || !this.height || !this.weight || !this.dietPreference || !this.dietGroup) {
+      alert("请确保所有必填项已填写!");
+      return;
+    }
+
+    // 保存用户输入的数据
+    this.saveData();
+
+    // 返回数据给父组件
+    const userData = {
+      name: this.name,
+      email: this.email,
+      phone: this.phone,
+      address: this.address,
+      age: this.age,
+      gender: this.gender,
+      height: this.height,
+      weight: this.weight,
+      activityLevel: this.activityLevel,
+      dietPreference: this.dietPreference,
+      dietGroup: this.dietGroup,
+      avatar: this.avatar
+    };
+
+    return this.modalCtrl.dismiss(userData, 'confirm');
+  }
+
+  uploadAvatar() {
+    const input = document.createElement('input');
+    input.type = 'file';
+    input.accept = 'image/*';
+
+    input.onchange = (event: any) => {
+      const file = event.target.files[0];
+      if (file) {
+        const reader = new FileReader();
+        reader.onload = (e: any) => {
+          this.avatar = e.target.result;
+        };
+        reader.readAsDataURL(file);
+      }
+    };
+
+    input.click();
+  }
+}

+ 77 - 11
FitMind-app/src/app/tab3/tab3.page.html

@@ -1,17 +1,83 @@
-<ion-header [translucent]="true">
+<ion-header>
   <ion-toolbar>
-    <ion-title>
-      Tab 3
-    </ion-title>
+    <ion-title>我的</ion-title>
   </ion-toolbar>
 </ion-header>
 
-<ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Tab 3</ion-title>
-    </ion-toolbar>
-  </ion-header>
+<ion-content>
+  <!-- 用户信息 -->
+  <ion-item lines="none" class="user-info">
+    <ion-avatar slot="start">
+      <!-- 显示头像,如果没有头像,显示默认头像 -->
+      <img [src]="userData?.avatar || 'assets/img/user-avatar.jpg'" alt="User Avatar" />
+    </ion-avatar>
+    <ion-label>
+      <h2>{{ userData?.name || '游客' }}</h2>
+      <p *ngIf="!currentUser?.id">您没有访问权限,请登录。</p>
+    </ion-label>
+
+    <!-- 登录、注册、编辑和退出登录按钮 -->
+    <div class="user-actions" slot="end">
+      <ion-button *ngIf="!currentUser?.id" (click)="signup()" fill="outline" color="primary">
+        注册
+      </ion-button>
+      <ion-button *ngIf="!currentUser?.id" (click)="login()" fill="outline" color="primary">
+        登录
+      </ion-button>
+
+      <ion-button *ngIf="currentUser?.id" (click)="goToEdit()" fill="clear" color="primary">
+        编辑
+      </ion-button>
+      <ion-button *ngIf="currentUser?.id" (click)="logout()" fill="clear" color="danger">
+        退出登录
+      </ion-button>
+    </div>
+  </ion-item>
+
+  <!-- 我的饮食计划 -->
+  <ion-card>
+    <ion-card-header>
+      <ion-card-title>我的锻炼计划</ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <ion-item>
+        <ion-label>今天的计划</ion-label>
+        <ion-button expand="block" color="secondary">
+          查看详情
+        </ion-button>
+      </ion-item>
+    </ion-card-content>
+  </ion-card>
+
+  <!-- 我的历史记录 -->
+  <ion-card>
+    <ion-card-header>
+      <ion-card-title>历史记录</ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <ion-item>
+        <ion-label>最近三天记录</ion-label>
+        <ion-button expand="block" color="tertiary">
+          查看详情
+        </ion-button>
+      </ion-item>
+    </ion-card-content>
+  </ion-card>
+
+  <!-- 收藏、帮助与反馈、设置按钮 -->
+  <ion-item button (click)="goToFavorites()">
+    <ion-icon slot="start" name="heart-outline"></ion-icon>
+    <ion-label>收藏</ion-label>
+  </ion-item>
+
+  <ion-item button (click)="goToHelp()">
+    <ion-icon slot="start" name="help-circle-outline"></ion-icon>
+    <ion-label>帮助与反馈</ion-label>
+  </ion-item>
+
+  <ion-item button (click)="goToSettings()">
+    <ion-icon slot="start" name="settings-outline"></ion-icon>
+    <ion-label>设置</ion-label>
+  </ion-item>
 
-  <app-explore-container name="Tab 3 page"></app-explore-container>
 </ion-content>

+ 116 - 0
FitMind-app/src/app/tab3/tab3.page.scss

@@ -0,0 +1,116 @@
+/* tab3.page.scss */
+
+ion-header {
+    background-color: #f8f8f8;
+    ion-toolbar {
+      --background: #fff;
+      --color: #333;
+      ion-title {
+        font-size: 22px;
+        font-weight: bold;
+        color: #222;
+      }
+    }
+  }
+  
+  ion-content {
+    padding: 16px;
+    background-color: #f4f5f8;
+  }
+  
+  .user-info {
+    display: flex;
+    align-items: center;
+    padding: 16px;
+    background-color: #ffffff;
+    border-radius: 8px;
+    margin-bottom: 16px;
+  }
+  
+  .user-info ion-avatar {
+    margin-right: 16px;
+  }
+  
+  .user-info ion-label h2 {
+    font-size: 18px;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .user-info ion-label p {
+    color: #888;
+    font-size: 14px;
+  }
+  
+  .user-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+  }
+  
+  ion-card {
+    margin-bottom: 16px;
+    border-radius: 8px;
+  }
+  
+  ion-card-header {
+    background-color: #f8f8f8;
+    ion-card-title {
+      font-size: 18px;
+      font-weight: bold;
+      color: #333;
+    }
+  }
+  
+  ion-card-content {
+    background-color: #fff;
+    ion-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      ion-label {
+        font-size: 16px;
+        color: #444;
+      }
+      ion-button {
+        font-weight: bold;
+      }
+    }
+  }
+  
+  ion-item {
+    margin-bottom: 12px;
+    --inner-padding-start: 12px;
+    --inner-padding-end: 12px;
+  }
+  
+  ion-item button {
+    --background: #f4f5f8;
+    --border-radius: 8px;
+    --padding-start: 16px;
+    --padding-end: 16px;
+    --color: #333;
+  }
+  
+  ion-item button:hover {
+    --background: #e0e0e0;
+  }
+  
+  ion-icon {
+    font-size: 20px;
+    color: #888;
+  }
+  
+  ion-item button ion-icon {
+    margin-right: 12px;
+  }
+  
+  ion-button {
+    border-radius: 8px;
+  }
+  
+  ion-card-title {
+    font-size: 16px;
+    font-weight: 500;
+  }
+  

+ 82 - 8
FitMind-app/src/app/tab3/tab3.page.ts

@@ -1,14 +1,88 @@
-import { Component } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
-import { ExploreContainerComponent } from '../explore-container/explore-container.component';
+
+import { Component, OnInit } from '@angular/core';
+import { IonAvatar, IonCardTitle, IonItem, IonLabel, IonSearchbar, NavController } from '@ionic/angular/standalone';
+import { PageEditComponent } from '../page-edit/page-edit.component';
+import { CloudUser } from 'src/app/lib/ncloud';
+import { openUserLoginModal } from 'src/app/lib/user/modal-user-login/modal-user-login.component';
+import { ModalController } from '@ionic/angular/standalone';
+import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonInput, IonRow, IonTextarea, IonTitle, IonToolbar } from '@ionic/angular/standalone';
 
 @Component({
   selector: 'app-tab3',
-  templateUrl: 'tab3.page.html',
-  styleUrls: ['tab3.page.scss'],
+  templateUrl: './tab3.page.html',
+  styleUrls: ['./tab3.page.scss'],
   standalone: true,
-  imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent],
+  imports: [
+    IonContent, IonHeader, IonTitle, IonToolbar, // 引入 IonicModule
+    IonButton,IonTextarea,IonInput,IonCard,IonCardContent,IonGrid,IonRow,IonCol,IonIcon,
+    IonCardHeader,IonItem,IonAvatar,IonSearchbar,IonLabel,IonCardTitle,
+  ],
 })
-export class Tab3Page {
-  constructor() {}
+export class Tab3Page implements OnInit {
+
+  currentUser:CloudUser|undefined
+  // 用户信息数据
+  userData = {
+    name: '',  // 默认值,可以为空测试默认显示“游客”
+    avatar: 'assets/img/user-avatar.jpg'  // 默认头像路径
+  };
+
+  constructor(private navCtrl: NavController, private modalCtrl: ModalController) {
+    this.currentUser = new CloudUser();
+  }
+  
+  ngOnInit() {
+    // 在这里可以加载用户信息(如果从API或服务中获取)
+  }
+
+  async login(){
+    // 弹出登录窗口
+    let user = await openUserLoginModal(this.modalCtrl);
+    if(user?.id){
+      this.currentUser = user
+    }
+  }
+
+  async signup(){
+    // 弹出注册窗口
+    let user = await openUserLoginModal(this.modalCtrl,"signup");
+    if(user?.id){
+      this.currentUser = user
+    }
+  }
+
+  logout(){
+    this.currentUser?.logout();
+  }
+  
+  async goToEdit() {
+    // 打开编辑模态框
+    const modal = await this.modalCtrl.create({
+      component: PageEditComponent,
+      componentProps: {
+        userData: this.userData  // 将当前用户数据传递给编辑页面
+      }
+    });
+    modal.present();
+
+    // 等待编辑页面关闭并获取数据
+    const { data, role } = await modal.onWillDismiss();
+
+    if (role === 'confirm') {
+      // 更新用户信息
+      this.userData = data;
+    }
+  }
+
+  goToFavorites() {
+    this.navCtrl.navigateForward('/favorites');
+  }
+
+  goToHelp() {
+    this.navCtrl.navigateForward('/help');
+  }
+
+  goToSettings() {
+    this.navCtrl.navigateForward('/settings');
+  }
 }

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

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

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


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

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

@@ -0,0 +1,90 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } 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){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) { }
+
+  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.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}