瀏覽代碼

feat: FoodStore import query & for

未来全栈 5 天之前
父節點
當前提交
9d3eb606ed
共有 3 個文件被更改,包括 692 次插入11 次删除
  1. 55 10
      myapp/src/app/tab4/tab4.page.html
  2. 217 1
      myapp/src/app/tab4/tab4.page.ts
  3. 420 0
      myapp/src/lib/ncloud.ts

+ 55 - 10
myapp/src/app/tab4/tab4.page.html

@@ -44,18 +44,18 @@
       <ion-card-header>
         <ion-card-title>今日营养目标</ion-card-title>
       </ion-card-header>
-      
+
       <ion-card-content>
         <div class="macro-grid">
           <div *ngFor="let item of nutritionKeys" class="macro-item {{item}}">
             <div class="target">{{nutritionGoals[item].target}}g</div>
-            <div class="progress-bar" 
-                 [style.--progress]="getProgress(nutritionGoals[item].target, nutritionGoals[item].current)">
+            <div class="progress-bar"
+              [style.--progress]="getProgress(nutritionGoals[item].target, nutritionGoals[item].current)">
             </div>
             <div class="current">{{nutritionGoals[item].current}}g 已摄入</div>
           </div>
         </div>
-        
+
         <ion-note class="supplement-tip">
           🧪 检测到维生素D不足,建议增加深海鱼类摄入
         </ion-note>
@@ -64,8 +64,8 @@
   </div>
 
   <div class="restaurant-section">
-    <h2>附近优选餐厅 <ion-chip>高蛋白优先</ion-chip></h2>
-    
+    <h2>附近优选餐厅 <ion-chip>高蛋白优先</ion-chip><ion-chip (click)="importStore()">导入测试数据</ion-chip></h2>
+
     <div *ngFor="let restaurant of restaurants" class="restaurant-card">
       <div class="cover">
         <img [src]="restaurant.dish.image" alt="餐品图片">
@@ -74,17 +74,17 @@
           匹配度93%
         </ion-badge>
       </div>
-      
+
       <div class="restaurant-info">
         <div class="header">
           <h3>{{restaurant.name}} ★{{restaurant.rating}}</h3>
           <ion-text color="medium">{{restaurant.distance}}km</ion-text>
         </div>
-        
+
         <div class="tags">
           <ion-chip *ngFor="let tag of restaurant.tags" outline>{{tag}}</ion-chip>
         </div>
-        
+
         <div class="dish-info">
           <div class="dish-image">
             <img [src]="restaurant.dish.image" alt="餐品">
@@ -107,5 +107,50 @@
         </div>
       </div>
     </div>
+
+
+
+    <div *ngFor="let store of storeList" class="restaurant-card">
+      <div class="cover">
+        <img [src]="store.get('dish').image" alt="餐品图片">
+        <ion-badge color="success" class="match-badge">
+          <ion-icon name="sparkles"></ion-icon>
+          匹配度93%
+        </ion-badge>
+      </div>
+
+      <div class="restaurant-info">
+        <div class="header">
+          <h3>{{store.get('name')}} ★{{store.get('rating')}}</h3>
+          <ion-text color="medium">{{store.get('distance')}}km</ion-text>
+        </div>
+
+        <div class="tags">
+          <ion-chip *ngFor="let tag of store.get('tags')" outline>{{tag}}</ion-chip>
+        </div>
+
+        <div class="dish-info">
+          <div class="dish-image">
+            <img [src]="store.get('dish').image" alt="餐品">
+          </div>
+          <div class="dish-detail">
+            <h4>{{store.get('dish').name}}</h4>
+            <div class="nutrition-tags">
+              <ion-chip color="primary">⚡{{store.get('dish').protein}}g蛋白</ion-chip>
+              <ion-chip color="secondary">🍚 {{store.get('dish').carbs}}</ion-chip>
+            </div>
+            <div class="price">
+              <ion-text color="dark">¥{{store.get('dish').price}}</ion-text>
+              <ion-text color="medium" class="original">¥{{store.get('dish').originalPrice}}</ion-text>
+              <ion-button size="small" fill="outline">
+                <ion-icon slot="start" name="add-circle"></ion-icon>
+                加入计划
+              </ion-button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
   </div>
-</ion-content>
+</ion-content>

+ 217 - 1
myapp/src/app/tab4/tab4.page.ts

@@ -1,5 +1,6 @@
 // src/app/tab4/tab4.page.ts
 import { Component } from '@angular/core';
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
 
 type NutritionKey = 'protein' | 'carbs' | 'fat';
 
@@ -49,10 +50,225 @@ export class Tab4Page {
     }
   ];
 
-  constructor() {}
+  constructor() {
+    this.loadFoodStore()
+  }
+
+  storeList: CloudObject[] = []
+  async loadFoodStore() {
+    let query = new CloudQuery("FoodStore")
+    this.storeList = await query.find();
+  }
 
   getProgress(goal: number, current: number): string {
     const percent = (current / goal) * 100;
     return `${percent}%`;
   }
+
+  async importStore() {
+    /**
+     数据范式 FoodStore
+     name String 名称
+     rating Number 评分
+     distance Number距离 公里数
+     tags Array<string> 标签
+     dish Object 招牌菜
+
+    示例数据:
+      {
+      name: '奥力给健身厨房',
+      rating: 4.8,
+      distance: 1.2,
+      tags: ['蛋白质达标', '562kcal/套餐', '30分钟达'],
+      dish: {
+        name: '增肌冠军碗',
+        protein: 42,
+        carbs: '慢碳黑米',
+        veggies: '双倍西兰花',
+        price: 58,
+        originalPrice: 78,
+        image: 'assets/images/chicken-bowl.jpg'
+      }
+    }
+     */
+    const restaurantData = [
+      {
+        name: '蛋白力量厨房',
+        rating: 4.7,
+        distance: 0.8,
+        tags: ['高蛋白', '低脂', '30分钟达'],
+        dish: {
+          name: '蛋白力量碗',
+          protein: 45,
+          carbs: '糙米',
+          veggies: '混合蔬菜',
+          price: 52,
+          originalPrice: 68,
+          image: 'assets/images/protein-bowl.jpg'
+        }
+      },
+      {
+        name: '低碳轻食坊',
+        rating: 4.9,
+        distance: 1.5,
+        tags: ['低碳水', '生酮友好', '45分钟达'],
+        dish: {
+          name: '牛油果鸡肉沙拉',
+          protein: 38,
+          carbs: '低碳蔬菜',
+          veggies: '牛油果+菠菜',
+          price: 65,
+          originalPrice: 85,
+          image: 'assets/images/avocado-salad.jpg'
+        }
+      },
+      {
+        name: '增肌战士餐厅',
+        rating: 4.6,
+        distance: 2.1,
+        tags: ['增肌套餐', '高热量', '60分钟达'],
+        dish: {
+          name: '战士增肌餐',
+          protein: 55,
+          carbs: '红薯+糙米',
+          veggies: '三色蔬菜',
+          price: 68,
+          originalPrice: 88,
+          image: 'assets/images/gain-bowl.jpg'
+        }
+      },
+      {
+        name: '素食健身屋',
+        rating: 4.5,
+        distance: 1.8,
+        tags: ['全素食', '植物蛋白', '40分钟达'],
+        dish: {
+          name: '豆腐能量碗',
+          protein: 35,
+          carbs: '藜麦',
+          veggies: '烤蔬菜',
+          price: 48,
+          originalPrice: 65,
+          image: 'assets/images/tofu-bowl.jpg'
+        }
+      },
+      {
+        name: '均衡营养站',
+        rating: 4.8,
+        distance: 0.5,
+        tags: ['营养均衡', '定制化', '25分钟达'],
+        dish: {
+          name: '均衡营养餐',
+          protein: 40,
+          carbs: '可选糙米/红薯',
+          veggies: '自选蔬菜',
+          price: 55,
+          originalPrice: 75,
+          image: 'assets/images/balance-meal.jpg'
+        }
+      },
+      {
+        name: '减脂实验室',
+        rating: 4.7,
+        distance: 1.3,
+        tags: ['低卡路里', '减脂专用', '35分钟达'],
+        dish: {
+          name: '实验室减脂餐',
+          protein: 30,
+          carbs: '魔芋米',
+          veggies: '羽衣甘蓝',
+          price: 45,
+          originalPrice: 60,
+          image: 'assets/images/lowcal-meal.jpg'
+        }
+      },
+      {
+        name: '健身快餐车',
+        rating: 4.4,
+        distance: 0.9,
+        tags: ['快速出餐', '便携包装', '20分钟达'],
+        dish: {
+          name: '健身快餐盒',
+          protein: 35,
+          carbs: '糙米',
+          veggies: '西兰花+胡萝卜',
+          price: 42,
+          originalPrice: 58,
+          image: 'assets/images/fast-meal.jpg'
+        }
+      },
+      {
+        name: '运动员厨房',
+        rating: 4.9,
+        distance: 2.3,
+        tags: ['专业运动员', '精准营养', '50分钟达'],
+        dish: {
+          name: '运动员特餐',
+          protein: 50,
+          carbs: '定制碳水',
+          veggies: '有机蔬菜',
+          price: 75,
+          originalPrice: 95,
+          image: 'assets/images/athlete-meal.jpg'
+        }
+      },
+      {
+        name: '健康餐大师',
+        rating: 4.7,
+        distance: 1.7,
+        tags: ['五星主厨', '精致摆盘', '40分钟达'],
+        dish: {
+          name: '大师特制餐',
+          protein: 42,
+          carbs: '黑米+藜麦',
+          veggies: '时令蔬菜',
+          price: 62,
+          originalPrice: 82,
+          image: 'assets/images/chef-special.jpg'
+        }
+      },
+      {
+        name: '健身外卖王',
+        rating: 4.6,
+        distance: 0.7,
+        tags: ['超高性价比', '量大实惠', '30分钟达'],
+        dish: {
+          name: '健身王套餐',
+          protein: 38,
+          carbs: '糙米',
+          veggies: '混合蔬菜',
+          price: 38,
+          originalPrice: 55,
+          image: 'assets/images/value-meal.jpg'
+        }
+      }
+    ];
+    const FoodStore = new CloudObject("FoodStore");
+    const query = new CloudQuery("FoodStore");
+
+    for (const restaurant of restaurantData) {
+      try {
+        // 检查是否已存在同名餐厅
+        query.equalTo("name", restaurant.name);
+        const existing = await query.first();
+
+        if (existing) {
+          console.log(`餐厅 "${restaurant.name}" 已存在,跳过保存`);
+          continue;
+        }
+
+        // 创建新餐厅对象
+        const newRestaurant = new CloudObject("FoodStore");
+        newRestaurant.set(restaurant);
+
+        // 保存到数据库
+        await newRestaurant.save();
+        console.log(`餐厅 "${restaurant.name}" 保存成功`);
+      } catch (error) {
+        console.error(`保存餐厅 "${restaurant.name}" 时出错:`, error);
+      }
+    }
+
+    console.log("所有餐厅数据处理完成");
+  }
 }

+ 420 - 0
myapp/src/lib/ncloud.ts

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