CuddleNan 3 giorni fa
parent
commit
ec0a98d3e7

+ 0 - 57
myapp/src/app/tab1/page-aichat/page-aichat.page.html

@@ -1,57 +0,0 @@
-<!-- page-aichat.page.html -->
-<ion-header [translucent]="true">
-  <ion-toolbar class="header-bar">
-    <ion-title class="app-title">AI助手</ion-title>
-    <ion-icon name="sparkles" slot="end" class="header-icon"></ion-icon>
-  </ion-toolbar>
-</ion-header>
-
-<ion-content [fullscreen]="true" class="chat-content">
-  <!-- 消息容器 -->
-  <div #messagesContainer class="messages-container">
-    <div 
-      *ngFor="let message of messages"
-      class="message-wrapper"
-      [class.user]="!message.isAI"
-      [@messageAnim]>
-      <div class="message-bubble">
-        <div class="message-text">{{ message.content }}</div>
-        <div class="message-time">{{ message.time }}</div>
-      </div>
-      <div class="message-status" *ngIf="!message.isAI">
-        <ion-icon 
-          [name]="message.status === 'sending' ? 'time' : 
-                  message.status === 'sent' ? 'checkmark' : 
-                  'alert-circle'" 
-          class="status-icon"></ion-icon>
-      </div>
-    </div>
-  </div>
-
-  <!-- 输入区域 -->
-  <div class="input-wrapper">
-    <div class="input-box">
-      <ion-textarea 
-        #messageInput
-        [(ngModel)]="newMessage"
-        (keyup.enter)="sendMessage()"
-        placeholder="输入消息..."
-        autoGrow
-        rows="1"
-        class="message-input"
-        [class.focused]="isInputFocused"
-        (ionFocus)="isInputFocused = true"
-        (ionBlur)="isInputFocused = false">
-      </ion-textarea>
-      <ion-button 
-        class="send-button"
-        (click)="sendMessage()"
-        [disabled]="!canSend"
-        shape="round">
-        <ion-icon 
-          [name]="canSend ? 'paper-plane' : 'send-outline'" 
-          class="send-icon"></ion-icon>
-      </ion-button>
-    </div>
-  </div>
-</ion-content>

+ 0 - 177
myapp/src/app/tab1/page-aichat/page-aichat.page.scss

@@ -1,177 +0,0 @@
-/* recipe-chat.page.scss */
-.header-bar {
-  --background: linear-gradient(135deg, #48BB78 0%, #38B2AC 100%);
-  --color: white;
-  
-  .app-title {
-    font-family: 'PingFang SC', sans-serif;
-    font-weight: 500;
-    letter-spacing: 1px;
-  }
-  
-  .header-icon {
-    font-size: 1.8em;
-    margin-right: 12px;
-    color: rgba(255,255,255,0.9);
-  }
-}
-
-.chat-content {
-  --background: #f7fafc;
-}
-
-.messages-container {
-  padding: 16px;
-  padding-bottom: 100px;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-
-.message-wrapper {
-  &.user {
-    align-self: flex-end;
-    max-width: 75%;
-    
-    .message-bubble {
-      background: white;
-      border-radius: 18px 4px 18px 18px;
-      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-      padding: 12px 16px;
-      
-      .message-text {
-        color: #2d3748;
-        font-weight: 500;
-      }
-      
-      .message-time {
-        color: #718096;
-        font-size: 12px;
-        margin-top: 4px;
-      }
-    }
-  }
-}
-
-.recipe-card {
-  background: white;
-  border-radius: 16px;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
-  margin: 8px 0;
-  
-  .recipe-image {
-    height: 200px;
-    object-fit: cover;
-    border-radius: 16px 16px 0 0;
-  }
-
-  .recipe-title {
-    font-size: 1.2em;
-    color: #2d3748;
-    font-weight: 600;
-  }
-
-  .section-title {
-    color: #48BB78;
-    font-weight: 500;
-    margin: 16px 0 8px;
-    font-size: 1.1em;
-  }
-
-  ion-item {
-    --padding-start: 0;
-    --inner-padding-end: 0;
-    --min-height: 40px;
-  }
-
-  .step-badge {
-    --background: #48BB78;
-    --color: white;
-    width: 24px;
-    height: 24px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .nutrition-grid {
-    display: grid;
-    grid-template-columns: repeat(3, 1fr);
-    gap: 12px;
-    margin-top: 16px;
-    
-    .nutrition-item {
-      background: #f7fafc;
-      padding: 12px;
-      border-radius: 8px;
-      text-align: center;
-      
-      ion-icon {
-        font-size: 24px;
-        color: #48BB78;
-        margin-bottom: 8px;
-      }
-      
-      div {
-        font-size: 12px;
-        color: #4a5568;
-      }
-    }
-  }
-}
-
-.input-wrapper {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: rgba(255,255,255,0.95);
-  backdrop-filter: blur(8px);
-  padding: 16px;
-  box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
-}
-
-.input-box {
-  display: flex;
-  gap: 12px;
-  align-items: center;
-}
-
-.custom-searchbar {
-  --background: #edf2f7;
-  --border-radius: 24px;
-  --box-shadow: none;
-  flex: 1;
-  
-  &.searchbar-has-focus {
-    --background: white;
-    --box-shadow: 0 0 0 2px #48BB7833;
-  }
-}
-
-.search-button {
-  --border-radius: 50%;
-  --padding-start: 0;
-  --padding-end: 0;
-  width: 48px;
-  height: 48px;
-  --background: linear-gradient(135deg, #48BB78 0%, #38B2AC 100%);
-  
-  &:disabled {
-    opacity: 0.6;
-  }
-  
-  ion-icon {
-    color: white;
-  }
-}
-
-/* 动画 */
-@keyframes fadeIn {
-  from { opacity: 0; transform: translateY(10px); }
-  to { opacity: 1; transform: translateY(0); }
-}
-
-.recipe-card {
-  animation: fadeIn 0.5s ease-out;
-}

+ 0 - 139
myapp/src/app/tab1/page-aichat/page-aichat.page.ts

@@ -1,139 +0,0 @@
-import { Component, OnInit, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
-import { trigger, transition, style, animate } from '@angular/animations';
-
-interface ChatMessage {
-  content: string;
-  isAI: boolean;
-  time: string;
-  status?: 'sending' | 'sent' | 'error';
-}
-
-@Component({
-  selector: 'app-page-aichat',
-  templateUrl: './page-aichat.page.html',
-  styleUrls: ['./page-aichat.page.scss'],
-  standalone: false,
-  animations: [
-    trigger('messageAnim', [
-      transition(':enter', [
-        style({ opacity: 0, transform: 'translateY(20px)' }),
-        animate('300ms cubic-bezier(0.4, 0, 0.2, 1)', 
-          style({ opacity: 1, transform: 'translateY(0)' }))
-      ])
-    ])
-  ]
-})
-export class PageAichatPage implements OnInit, AfterViewChecked {
-  @ViewChild('messagesContainer') private messagesContainer!: ElementRef;
-  @ViewChild('messageInput') messageInput!: ElementRef;
-
-  messages: ChatMessage[] = [];
-  newMessage = '';
-  isInputFocused = false;
-
-  get canSend(): boolean {
-    return this.newMessage.trim().length > 0;
-  }
-
-  ngOnInit() {
-    this.addAIMessage('您好!我是您的智能食谱助手,您可以输入食材名称,我会为您推荐相关食谱。例如:"番茄"、"鸡肉"');
-  }
-
-  ngAfterViewChecked() {
-    this.scrollToBottom();
-  }
-
-  async sendMessage() {
-    if (!this.canSend) return;
-
-    const userMessage = this.newMessage.trim();
-    this.addUserMessage(userMessage);
-    
-    // 模拟发送过程
-    await this.simulateSendDelay();
-    
-    // 添加AI回复
-    this.addAIMessage(await this.generateAIReply(userMessage));
-    
-    this.newMessage = '';
-    this.messageInput.nativeElement.focus();
-  }
-
-  private addUserMessage(content: string) {
-    this.messages.push({
-      content,
-      isAI: false,
-      time: this.getCurrentTime(),
-      status: 'sending'
-    });
-
-    // 更新发送状态
-    setTimeout(() => {
-      const index = this.messages.length - 1;
-      this.messages[index].status = Math.random() > 0.1 ? 'sent' : 'error';
-    }, 1000);
-  }
-
-  private addAIMessage(content: string) {
-    this.messages.push({
-      content,
-      isAI: true,
-      time: this.getCurrentTime()
-    });
-  }
-
-  private scrollToBottom() {
-    try {
-      setTimeout(() => {
-        this.messagesContainer.nativeElement.scrollTop = 
-          this.messagesContainer.nativeElement.scrollHeight;
-      }, 100);
-    } catch(err) { /* 处理滚动错误 */ }
-  }
-
-  private getCurrentTime(): string {
-    return new Date().toLocaleTimeString([], { 
-      hour: '2-digit', 
-      minute: '2-digit' 
-    });
-  }
-
-  private async simulateSendDelay(): Promise<void> {
-    return new Promise(resolve => setTimeout(resolve, 500));
-  }
-
-  private async generateAIReply(input: string): Promise<string> {
-    // 模拟AI回复逻辑
-    await new Promise(resolve => setTimeout(resolve, 800));
-    return this.getRecipeRecommendations(input);
-  }
-
-  private getRecipeRecommendations(input: string): string {
-    const recipes: { [key: string]: string[] } = {
-      番茄: [
-        "番茄炒蛋:将番茄切块,鸡蛋炒熟后加入番茄一起翻炒,调味即可。",
-        "番茄汤:番茄去皮切块,加水煮开后调味,可加入鸡蛋液做成番茄蛋汤。",
-        "番茄意大利面:番茄炒成酱,加入煮好的意大利面拌匀。"
-      ],
-      鸡肉: [
-        "宫保鸡丁:鸡肉切丁,与花生米、青椒等一起炒制。",
-        "香煎鸡胸肉:鸡胸肉用调料腌制后,放入锅中煎至两面金黄。",
-        "鸡汤:鸡肉加水炖煮,加入姜片、葱段等调料。"
-      ],
-      土豆: [
-        "土豆丝:土豆切成丝,清炒或酸辣口味都很不错。",
-        "土豆炖牛肉:土豆与牛肉一起炖煮,口感软糯。",
-        "薯条:土豆切成条,炸至金黄,可搭配番茄酱食用。"
-      ]
-    };
-
-    for (const [ingredient, recipeList] of Object.entries(recipes)) {
-      if (input.includes(ingredient)) {
-        const randomIndex = Math.floor(Math.random() * recipeList.length);
-        return `为您推荐使用 ${ingredient} 的食谱:${recipeList[randomIndex]}`;
-      }
-    }
-    
-    return `很抱歉,暂时没有找到与 "${input}" 相关的食谱。您可以尝试输入其他食材。`;
-  }
-}

+ 21 - 0
myapp/src/app/tab1/page-detail/page-detail-routing.module.ts

@@ -0,0 +1,21 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { PageDetailPage } from './page-detail.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: PageDetailPage
+  },
  {
+    path: 'page-type',
+    loadChildren: () => import('./page-type/page-type.module').then( m => m.PageTypePageModule)
+  }
+
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class PageDetailPageRoutingModule {}

+ 5 - 5
myapp/src/app/tab1/page-aichat/page-aichat.module.ts → myapp/src/app/tab1/page-detail/page-detail.module.ts

@@ -4,17 +4,17 @@ import { FormsModule } from '@angular/forms';
 
 import { IonicModule } from '@ionic/angular';
 
-import { PageAichatPageRoutingModule } from './page-aichat-routing.module';
+import { PageDetailPageRoutingModule } from './page-detail-routing.module';
 
-import { PageAichatPage } from './page-aichat.page';
+import { PageDetailPage } from './page-detail.page';
 
 @NgModule({
   imports: [
     CommonModule,
     FormsModule,
     IonicModule,
-    PageAichatPageRoutingModule
+    PageDetailPageRoutingModule
   ],
-  declarations: [PageAichatPage]
+  declarations: [PageDetailPage]
 })
-export class PageAichatPageModule {}
+export class PageDetailPageModule {}

+ 121 - 0
myapp/src/app/tab1/page-detail/page-detail.page.html

@@ -0,0 +1,121 @@
+<ion-header [translucent]="true">
+  <ion-toolbar color="primary">
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/"></ion-back-button>
+    </ion-buttons>
+    <ion-title>{{ recipe?.title }}</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="toggleFavorite()">
+        <ion-icon [name]="isFavorite ? 'heart' : 'heart-outline'"></ion-icon>
+      </ion-button>
+      <ion-button>
+        <ion-icon name="share-social-outline"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <div class="recipe-container">
+    <!-- 食谱图片 -->
+    <div class="recipe-image-container">
+      <img [src]="recipe?.imageUrl" [alt]="recipe?.title" class="recipe-image">
+      <div class="image-overlay">
+        <ion-chip color="danger">{{ recipe?.category }}</ion-chip>
+        <ion-chip color="dark">{{ recipe?.difficulty }}</ion-chip>
+      </div>
+    </div>
+
+    <!-- 食谱元信息 -->
+    <ion-grid class="recipe-meta">
+      <ion-row>
+        <ion-col size="4" class="meta-item">
+          <ion-icon name="time-outline"></ion-icon>
+          <span>{{ recipe?.prepTime }} 准备</span>
+        </ion-col>
+        <ion-col size="4" class="meta-item">
+          <ion-icon name="flame-outline"></ion-icon>
+          <span>{{ recipe?.cookTime }} 烹饪</span>
+        </ion-col>
+        <ion-col size="4" class="meta-item">
+          <ion-icon name="people-outline"></ion-icon>
+          <span>{{ recipe?.servings }} 人份</span>
+        </ion-col>
+      </ion-row>
+    </ion-grid>
+
+    <!-- 食材部分 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>
+          <ion-icon name="basket-outline" slot="start"></ion-icon>
+          食材
+        </ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <ion-list lines="none">
+          <ion-item *ngFor="let ingredient of recipe?.ingredients">
+            <ion-label>
+              <h3>{{ ingredient.name }}</h3>
+            </ion-label>
+            <ion-note slot="end" color="primary">{{ ingredient.amount }}</ion-note>
+          </ion-item>
+        </ion-list>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 制作步骤 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>
+          <ion-icon name="restaurant-outline" slot="start"></ion-icon>
+          制作步骤
+        </ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <ion-list lines="none">
+          <ion-item *ngFor="let step of recipe?.instructions; let i = index">
+            <ion-avatar slot="start" class="step-number">
+              {{ i + 1 }}
+            </ion-avatar>
+            <ion-label class="ion-text-wrap">
+              <p>{{ step }}</p>
+            </ion-label>
+          </ion-item>
+        </ion-list>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 营养信息 -->
+    <ion-card *ngIf="recipe?.nutrition">
+      <ion-card-header>
+        <ion-card-title>
+          <ion-icon name="nutrition-outline" slot="start"></ion-icon>
+          营养信息 (每份)
+        </ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <ion-grid>
+          <ion-row>
+            <ion-col size="6" size-md="3" class="nutrition-item">
+              <div class="nutrition-value">{{ recipe.nutrition.calories }}</div>
+              <div class="nutrition-label">卡路里</div>
+            </ion-col>
+            <ion-col size="6" size-md="3" class="nutrition-item">
+              <div class="nutrition-value">{{ recipe.nutrition.carbs }}g</div>
+              <div class="nutrition-label">碳水化合物</div>
+            </ion-col>
+            <ion-col size="6" size-md="3" class="nutrition-item">
+              <div class="nutrition-value">{{ recipe.nutrition.protein }}g</div>
+              <div class="nutrition-label">蛋白质</div>
+            </ion-col>
+            <ion-col size="6" size-md="3" class="nutrition-item">
+              <div class="nutrition-value">{{ recipe.nutrition.fat }}g</div>
+              <div class="nutrition-label">脂肪</div>
+            </ion-col>
+          </ion-row>
+        </ion-grid>
+      </ion-card-content>
+    </ion-card>
+  </div>
+</ion-content>

+ 112 - 0
myapp/src/app/tab1/page-detail/page-detail.page.scss

@@ -0,0 +1,112 @@
+.recipe-container {
+    padding-bottom: 20px;
+  }
+  
+  .recipe-image-container {
+    position: relative;
+    width: 100%;
+    height: 250px;
+    overflow: hidden;
+    
+    .recipe-image {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+    
+    .image-overlay {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      display: flex;
+      flex-direction: column;
+      gap: 5px;
+    }
+  }
+  
+  .recipe-meta {
+    padding: 15px 0;
+    text-align: center;
+    background: var(--ion-color-light);
+    
+    .meta-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 5px;
+      
+      ion-icon {
+        font-size: 24px;
+        color: var(--ion-color-primary);
+      }
+      
+      span {
+        font-size: 14px;
+        color: var(--ion-color-medium);
+      }
+    }
+  }
+  
+  ion-card {
+    margin: 15px;
+    
+    ion-card-header {
+      padding-bottom: 0;
+      
+      ion-card-title {
+        display: flex;
+        align-items: center;
+        font-size: 18px;
+        
+        ion-icon {
+          margin-right: 8px;
+          color: var(--ion-color-primary);
+        }
+      }
+    }
+    
+    ion-card-content {
+      padding-top: 0;
+    }
+  }
+  
+  .step-number {
+    --border-radius: 50%;
+    background: var(--ion-color-primary);
+    color: white;
+    font-weight: bold;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 30px;
+    min-height: 30px;
+  }
+  
+  .nutrition-item {
+    text-align: center;
+    padding: 10px;
+    
+    .nutrition-value {
+      font-size: 18px;
+      font-weight: bold;
+      color: var(--ion-color-primary);
+    }
+    
+    .nutrition-label {
+      font-size: 12px;
+      color: var(--ion-color-medium);
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
+  }
+  
+  @media (min-width: 768px) {
+    .recipe-image-container {
+      height: 350px;
+    }
+    
+    ion-card {
+      margin: 20px auto;
+      max-width: 800px;
+    }
+  }

+ 5 - 5
myapp/src/app/tab1/page-aichat/page-aichat.page.spec.ts → myapp/src/app/tab1/page-detail/page-detail.page.spec.ts

@@ -1,12 +1,12 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { PageAichatPage } from './page-aichat.page';
+import { PageDetailPage } from './page-detail.page';
 
-describe('PageAichatPage', () => {
-  let component: PageAichatPage;
-  let fixture: ComponentFixture<PageAichatPage>;
+describe('PageDetailPage', () => {
+  let component: PageDetailPage;
+  let fixture: ComponentFixture<PageDetailPage>;
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(PageAichatPage);
+    fixture = TestBed.createComponent(PageDetailPage);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 89 - 0
myapp/src/app/tab1/page-detail/page-detail.page.ts

@@ -0,0 +1,89 @@
+import { Component, OnInit } from '@angular/core';
+import { IonBackButton, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonChip, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonItem, IonLabel, IonList, IonNote, IonRow, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+
+interface Ingredient {
+  name: string;
+  amount: string;
+}
+
+interface Nutrition {
+  calories: string;
+  carbs: number;
+  protein: number;
+  fat: number;
+}
+
+interface Recipe {
+  title: string;
+  imageUrl: string;
+  prepTime: string;
+  cookTime: string;
+  servings: number;
+  category: string;
+  difficulty: string;
+  ingredients: Ingredient[];
+  instructions: string[];
+  nutrition?: Nutrition;
+}
+
+@Component({
+  selector: 'app-page-detail',
+  templateUrl: './page-detail.page.html',
+  styleUrls: ['./page-detail.page.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonBackButton, IonButtons, IonButton, IonIcon, 
+            IonGrid, IonRow, IonCol, IonCard, IonCardHeader, IonCardTitle, IonCardContent, 
+            IonList, IonItem, IonLabel, IonNote, IonChip]
+})
+export class PageDetailPage implements OnInit {
+  recipe: Recipe = {
+    title: '经典意大利肉酱面',
+    imageUrl: 'https://images.unsplash.com/photo-1555949258-eb67b1ef0ceb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80',
+    prepTime: '20分钟',
+    cookTime: '1小时',
+    servings: 4,
+    category: '意大利菜',
+    difficulty: '中等',
+    ingredients: [
+      { name: '意大利面', amount: '400g' },
+      { name: '牛肉末', amount: '300g' },
+      { name: '洋葱', amount: '1个' },
+      { name: '胡萝卜', amount: '1根' },
+      { name: '芹菜', amount: '1根' },
+      { name: '大蒜', amount: '3瓣' },
+      { name: '番茄酱', amount: '2汤匙' },
+      { name: '红酒', amount: '100ml' },
+      { name: '橄榄油', amount: '2汤匙' },
+      { name: '盐和黑胡椒', amount: '适量' },
+      { name: '帕玛森奶酪', amount: '装饰用' }
+    ],
+    instructions: [
+      '将洋葱、胡萝卜和芹菜切碎成小丁,大蒜切末备用。',
+      '在大锅中加热橄榄油,加入切碎的蔬菜炒至软化,约5分钟。',
+      '加入牛肉末,用中火翻炒至变色,约8-10分钟。',
+      '倒入红酒,煮至酒精挥发,约2分钟。',
+      '加入番茄酱和适量的水,调至小火慢炖45分钟,期间偶尔搅拌。',
+      '在另一个锅中煮意大利面,根据包装指示时间减1分钟。',
+      '将煮好的面条沥干,保留一杯面水。',
+      '将面条加入肉酱中,加入少量面水搅拌至酱汁浓稠裹满意面。',
+      '用盐和黑胡椒调味,撒上帕玛森奶酪即可享用。'
+    ],
+    nutrition: {
+      calories: '650kcal',
+      carbs: 75,
+      protein: 35,
+      fat: 22
+    }
+  };
+
+  isFavorite: boolean = false;
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+  toggleFavorite() {
+    this.isFavorite = !this.isFavorite;
+  }
+}

+ 3 - 3
myapp/src/app/tab1/page-aichat/page-aichat-routing.module.ts → myapp/src/app/tab1/page-type/page-type-routing.module.ts

@@ -1,12 +1,12 @@
 import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
 
-import { PageAichatPage } from './page-aichat.page';
+import { PageTypePage } from './page-type.page';
 
 const routes: Routes = [
   {
     path: '',
-    component: PageAichatPage
+    component: PageTypePage
   }
 ];
 
@@ -14,4 +14,4 @@ const routes: Routes = [
   imports: [RouterModule.forChild(routes)],
   exports: [RouterModule],
 })
-export class PageAichatPageRoutingModule {}
+export class PageTypePageRoutingModule {}

+ 20 - 0
myapp/src/app/tab1/page-type/page-type.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { PageTypePageRoutingModule } from './page-type-routing.module';
+
+import { PageTypePage } from './page-type.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    PageTypePageRoutingModule
+  ],
+  declarations: [PageTypePage]
+})
+export class PageTypePageModule {}

+ 13 - 0
myapp/src/app/tab1/page-type/page-type.page.html

@@ -0,0 +1,13 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>page-type</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">page-type</ion-title>
+    </ion-toolbar>
+  </ion-header>
+</ion-content>

+ 0 - 0
myapp/src/app/tab1/page-type/page-type.page.scss


+ 17 - 0
myapp/src/app/tab1/page-type/page-type.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { PageTypePage } from './page-type.page';
+
+describe('PageTypePage', () => {
+  let component: PageTypePage;
+  let fixture: ComponentFixture<PageTypePage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(PageTypePage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 15 - 0
myapp/src/app/tab1/page-type/page-type.page.ts

@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-page-type',
+  templateUrl: './page-type.page.html',
+  styleUrls: ['./page-type.page.scss'],
+})
+export class PageTypePage implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}

+ 24 - 16
myapp/src/app/tab1/tab1-routing.module.ts

@@ -1,20 +1,28 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { Tab1Page } from './tab1.page';
-
-const routes: Routes = [
-  {
-    path: '',
-    component: Tab1Page,
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { Tab1Page } from './tab1.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: Tab1Page,
+  },
+  {
+    path: 'page-aichat',
+    //loadChildren: () => import('./page-aichat/page-aichat.module').then( m => m.PageAichatPageModule)
+  },
  {
+    path: 'page-detail',
+    loadChildren: () => import('./page-detail/page-detail.module').then( m => m.PageDetailPageModule)
   },
   {
-    path: 'page-aichat',
-    loadChildren: () => import('./page-aichat/page-aichat.module').then( m => m.PageAichatPageModule)
+    path: 'page-type',
+    loadChildren: () => import('./page-type/page-type.module').then( m => m.PageTypePageModule)
   },
-];
 
-@NgModule({
-  imports: [RouterModule.forChild(routes)],
-  exports: [RouterModule]
-})
-export class Tab1PageRoutingModule {}
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class Tab1PageRoutingModule {}

+ 31 - 46
myapp/src/app/tab1/tab1.page.html

@@ -61,51 +61,36 @@
   </ion-grid>
 
   <!-- 推荐食谱 -->
-  <ion-card class="recommend-card">
-    <ion-card-header class="px-4 pt-4">
-      <h2 class="section-title">今日推荐食谱</h2>
-    </ion-card-header>
-    <ion-card-content class="px-4">
-      <ion-grid fixed>
-        <ion-row class="ion-justify-content-around">
-          <ion-col 
-            size="12" 
-            md="6" 
-            lg="4" 
-            class="recipe-card" 
-            *ngFor="let recipe of recommendedRecipes"
-            (click)="goToRecipeDetail(recipe.id)"
-          >
-            <ion-card class="recipe-card-hover">
-              <ion-img [src]="recipe.image"></ion-img>
-              <ion-card-content class="p-4">
-                <h3 class="recipe-title">{{ recipe.title }}</h3>
-                <div class="recipe-meta">
-                  <ion-icon name="time" class="icon-md"></ion-icon> {{ recipe.time }}分钟
-                  <ion-icon name="star" class="icon-md text-primary"></ion-icon> {{ recipe.rating }}评分
-                </div>
-              </ion-card-content>
-            </ion-card>
-          </ion-col>
-        </ion-row>
-      </ion-grid>
-    </ion-card-content>
-  </ion-card>
+<ion-card class="recommend-card">
+  <ion-card-header class="px-4 pt-4">
+    <h2 class="section-title">今日推荐食谱</h2>
+  </ion-card-header>
+  <ion-card-content class="px-4">
+    <ion-grid fixed>
+      <ion-row class="ion-justify-content-around">
+        <ion-col 
+          size="12" 
+          md="6" 
+          lg="4" 
+          class="recipe-card" 
+          *ngFor="let recipe of recommendedRecipes"
+          (click)="goToRecipeDetail(recipe.objectId)"
+        >
+          <ion-card class="recipe-card-hover">
+            <ion-img [src]="recipe.image?.url || '/assets/images/default-food.jpg'"></ion-img>
+            <ion-card-content class="p-4">
+              <h3 class="recipe-title">{{ recipe.title }}</h3>
+              <div class="recipe-meta">
+                <ion-icon name="time" class="icon-md"></ion-icon> {{ recipe.cookingTime || 30 }}分钟
+                <ion-icon name="star" class="icon-md text-primary"></ion-icon> {{ recipe.rating || 4.5 }}评分
+              </div>
+            </ion-card-content>
+          </ion-card>
+        </ion-col>
+      </ion-row>
+    </ion-grid>
+  </ion-card-content>
+</ion-card>
 
-  <!-- 用户信息 -->
-  <ion-card class="user-info-card">
-    <ion-row class="align-items-center p-4 text-white">
-      <ion-col>
-        <p class="greeting">
-          你好,<span class="username">小明</span>!<br>
-          今天想尝试什么新料理呢?
-        </p>
-      </ion-col>
-      <ion-col size="auto">
-        <ion-avatar class="user-avatar" (click)="goToAIChat()">
-          <ion-label>M</ion-label>
-        </ion-avatar>
-      </ion-col>
-    </ion-row>
-  </ion-card>
+  
 </ion-content>

+ 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 = `https://dev.fmode.cn/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(`https://dev.fmode.cn/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 = `https://dev.fmode.cn/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 = `https://dev.fmode.cn/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 = `https://dev.fmode.cn/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(`https://dev.fmode.cn/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(`https://dev.fmode.cn/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(`https://dev.fmode.cn/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(`https://dev.fmode.cn/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 = `https://dev.fmode.cn/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 = `https://dev.fmode.cn`
+        // 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
+    }
+}