Răsfoiți Sursa

feat : page-order

Lc 1 săptămână în urmă
părinte
comite
e3d03090de
31 a modificat fișierele cu 1298 adăugiri și 203 ștergeri
  1. 68 0
      menu-web/package-lock.json
  2. 4 0
      menu-web/package.json
  3. 0 195
      menu-web/src/app/app.html
  4. 32 0
      menu-web/src/modules/food/mobile/page-order/README.cd
  5. 12 0
      menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.html
  6. 37 0
      menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.scss
  7. 23 0
      menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.spec.ts
  8. 24 0
      menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.ts
  9. 17 0
      menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.html
  10. 93 0
      menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.scss
  11. 23 0
      menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.spec.ts
  12. 11 0
      menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.ts
  13. 5 0
      menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.html
  14. 36 0
      menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.scss
  15. 23 0
      menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.spec.ts
  16. 13 0
      menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.ts
  17. 1 0
      menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.html
  18. 0 0
      menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.scss
  19. 23 0
      menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.spec.ts
  20. 64 0
      menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.ts
  21. 15 0
      menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.html
  22. 66 0
      menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.scss
  23. 23 0
      menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.spec.ts
  24. 28 0
      menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.ts
  25. 7 0
      menu-web/src/modules/food/mobile/page-order/models/cart-item.model.ts
  26. 6 0
      menu-web/src/modules/food/mobile/page-order/models/history-item.model.ts
  27. 7 0
      menu-web/src/modules/food/mobile/page-order/models/suggestion.model.ts
  28. 135 1
      menu-web/src/modules/food/mobile/page-order/page-order.html
  29. 243 0
      menu-web/src/modules/food/mobile/page-order/page-order.scss
  30. 185 6
      menu-web/src/modules/food/mobile/page-order/page-order.ts
  31. 74 1
      menu-web/src/styles.scss

+ 68 - 0
menu-web/package-lock.json

@@ -14,6 +14,10 @@
         "@angular/forms": "^20.0.0",
         "@angular/platform-browser": "^20.0.0",
         "@angular/router": "^20.0.0",
+        "@fortawesome/angular-fontawesome": "^2.0.1",
+        "@fortawesome/fontawesome-svg-core": "^6.7.2",
+        "@fortawesome/free-solid-svg-icons": "^6.7.2",
+        "chart.js": "^4.5.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.0"
@@ -1212,6 +1216,52 @@
         "node": ">=18"
       }
     },
+    "node_modules/@fortawesome/angular-fontawesome": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-2.0.1.tgz",
+      "integrity": "sha512-IdklZkuw+WS2GQWhFnr1EX/tOALnrKaj4YGnUmPaUg2Uf+Amj8Xi+M/qDrr915YJ5MaDxd9tZ1kqOHRcvQqq2A==",
+      "license": "MIT",
+      "dependencies": {
+        "@fortawesome/fontawesome-svg-core": "^6.7.2",
+        "tslib": "^2.8.1"
+      },
+      "peerDependencies": {
+        "@angular/core": "^20.0.0"
+      }
+    },
+    "node_modules/@fortawesome/fontawesome-common-types": {
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+      "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/fontawesome-svg-core": {
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
+      "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
+      "license": "MIT",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "6.7.2"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/free-solid-svg-icons": {
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
+      "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
+      "license": "(CC-BY-4.0 AND MIT)",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "6.7.2"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@inquirer/checkbox": {
       "version": "4.1.8",
       "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz",
@@ -1653,6 +1703,12 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
     "node_modules/@listr2/prompt-adapter-inquirer": {
       "version": "2.0.22",
       "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz",
@@ -3811,6 +3867,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/chart.js": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
+      "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
     "node_modules/chokidar": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",

+ 4 - 0
menu-web/package.json

@@ -26,6 +26,10 @@
     "@angular/forms": "^20.0.0",
     "@angular/platform-browser": "^20.0.0",
     "@angular/router": "^20.0.0",
+    "@fortawesome/angular-fontawesome": "^2.0.1",
+    "@fortawesome/fontawesome-svg-core": "^6.7.2",
+    "@fortawesome/free-solid-svg-icons": "^6.7.2",
+    "chart.js": "^4.5.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"

Fișier diff suprimat deoarece este prea mare
+ 0 - 195
menu-web/src/app/app.html


+ 32 - 0
menu-web/src/modules/food/mobile/page-order/README.cd

@@ -0,0 +1,32 @@
+# 项目框架
+
+src/app/page-order/
+├── page-order.component.ts       # 主组件
+├── page-order.component.html    # 主模板
+├── page-order.component.scss    # 主样式
+├── page-order.component.spec.ts # 测试文件
+├── components/
+│   ├── cart-item/
+│   │   ├── cart-item.component.ts
+│   │   ├── cart-item.component.html
+│   │   ├── cart-item.component.scss
+│   ├── nutrition-chart/
+│   │   ├── nutrition-chart.component.ts
+│   │   ├── nutrition-chart.component.html
+│   │   ├── nutrition-chart.component.scss
+│   ├── budget-progress/
+│   │   ├── budget-progress.component.ts
+│   │   ├── budget-progress.component.html
+│   │   ├── budget-progress.component.scss
+│   ├── suggestion-item/
+│   │   ├── suggestion-item.component.ts
+│   │   ├── suggestion-item.component.html
+│   │   ├── suggestion-item.component.scss
+│   ├── history-item/
+│   │   ├── history-item.component.ts
+│   │   ├── history-item.component.html
+│   │   ├── history-item.component.scss
+└── models/
+    ├── cart-item.model.ts
+    ├── suggestion.model.ts
+    ├── history-item.model.ts

+ 12 - 0
menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.html

@@ -0,0 +1,12 @@
+<div class="budget-container">
+  <div class="budget-header">
+    <div class="budget-title">本月预算</div>
+    <div class="budget-amount">¥{{ total.toFixed(2) }}</div>
+  </div>
+  <div class="progress-container">
+    <div class="progress-bar"></div>
+  </div>
+  <div class="progress-text">
+    已消费 ¥{{ spent.toFixed(2) }} / 剩余 ¥{{ remaining.toFixed(2) }}
+  </div>
+</div>

+ 37 - 0
menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.scss

@@ -0,0 +1,37 @@
+.budget-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.budget-title {
+  font-weight: 600;
+}
+
+.budget-amount {
+  color: var(--primary);
+  font-weight: bold;
+}
+
+.progress-container {
+  height: 12px;
+  background: var(--light-gray);
+  border-radius: 10px;
+  overflow: hidden;
+  margin: 15px 0;
+}
+
+.progress-bar {
+  height: 100%;
+  background: linear-gradient(90deg, var(--primary), var(--primary-light));
+  border-radius: 10px;
+  width: 0;
+  position: relative;
+  transition: width 1s ease;
+}
+
+.progress-text {
+  text-align: center;
+  font-size: 12px;
+  color: var(--gray);
+}

+ 23 - 0
menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BudgetProgressComponent } from './budget-progress.component';
+
+describe('BudgetProgressComponent', () => {
+  let component: BudgetProgressComponent;
+  let fixture: ComponentFixture<BudgetProgressComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [BudgetProgressComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(BudgetProgressComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 24 - 0
menu-web/src/modules/food/mobile/page-order/components/budget-progress/budget-progress.component/budget-progress.component.ts

@@ -0,0 +1,24 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-budget-progress',
+  standalone: true,
+  templateUrl: './budget-progress.component.html',
+  styleUrls: ['./budget-progress.component.scss']
+})
+export class BudgetProgressComponent implements OnInit {
+  @Input() total: number = 0;
+  @Input() spent: number = 0;
+  @Input() remaining: number = 0;
+  @Input() percentage: number = 0;
+
+  ngOnInit(): void {
+    // Animation for progress bar
+    setTimeout(() => {
+      const progressBar = document.querySelector('.progress-bar') as HTMLElement;
+      if (progressBar) {
+        progressBar.style.width = `${this.percentage}%`;
+      }
+    }, 500);
+  }
+}

+ 17 - 0
menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.html

@@ -0,0 +1,17 @@
+<div class="cart-item">
+  <div class="item-img">{{ item.initial }}</div>
+  <div class="item-info">
+    <div class="item-name">{{ item.name }}</div>
+    <div class="item-price">¥{{ item.price.toFixed(2) }}</div>
+    <div class="item-actions">
+      <div class="quantity-control">
+        <button class="quantity-btn minus" (click)="updateQuantity(-1)">-</button>
+        <input type="text" class="quantity-input" [value]="item.quantity" readonly>
+        <button class="quantity-btn plus" (click)="updateQuantity(1)">+</button>
+      </div>
+      <button class="remove-btn" (click)="removeItem()">
+        <fa-icon [icon]="faTrashAlt"></fa-icon>
+      </button>
+    </div>
+  </div>
+</div>

+ 93 - 0
menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.scss

@@ -0,0 +1,93 @@
+.cart-item {
+  display: flex;
+  align-items: center;
+  padding: 12px 0;
+  border-bottom: 1px solid #f0f0f0;
+  transition: opacity 0.3s ease;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.item-img {
+  width: 80px;
+  height: 80px;
+  border-radius: 10px;
+  background: linear-gradient(45deg, #ff9a9e, #fad0c4);
+  margin-right: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--white);
+  font-size: 24px;
+  font-weight: bold;
+}
+
+.item-info {
+  flex: 1;
+}
+
+.item-name {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 5px;
+}
+
+.item-price {
+  color: var(--primary);
+  font-weight: 600;
+  margin-bottom: 8px;
+}
+
+.item-actions {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.quantity-control {
+  display: flex;
+  align-items: center;
+  border: 1px solid #ddd;
+  border-radius: 20px;
+  overflow: hidden;
+}
+
+.quantity-btn {
+  width: 30px;
+  height: 30px;
+  background: var(--light-gray);
+  border: none;
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+}
+
+.quantity-input {
+  width: 40px;
+  text-align: center;
+  border: none;
+  font-size: 16px;
+}
+
+.remove-btn {
+  color: var(--gray);
+  background: none;
+  border: none;
+  font-size: 14px;
+  cursor: pointer;
+
+  &:hover {
+    color: var(--primary);
+  }
+}
+
+@media (max-width: 480px) {
+  .item-img {
+    width: 70px;
+    height: 70px;
+  }
+}

+ 23 - 0
menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CartItemComponent } from './cart-item.component';
+
+describe('CartItemComponent', () => {
+  let component: CartItemComponent;
+  let fixture: ComponentFixture<CartItemComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [CartItemComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(CartItemComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 11 - 0
menu-web/src/modules/food/mobile/page-order/components/cart-item/cart-item.component/cart-item.component.ts

@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'app-cart-item.component',
+  imports: [],
+  templateUrl: './cart-item.component.html',
+  styleUrl: './cart-item.component.scss'
+})
+export class CartItemComponent {
+
+}

+ 5 - 0
menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.html

@@ -0,0 +1,5 @@
+<div class="history-item">
+  <div class="history-logo">{{ item.initial }}</div>
+  <div class="history-name">{{ item.name }}</div>
+  <div class="history-date">{{ item.date }}</div>
+</div>

+ 36 - 0
menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.scss

@@ -0,0 +1,36 @@
+.history-item {
+  background: var(--light-gray);
+  border-radius: 12px;
+  padding: 15px;
+  text-align: center;
+  cursor: pointer;
+  transition: var(--transition);
+
+  &:hover {
+    transform: translateY(-3px);
+    box-shadow: var(--card-shadow);
+  }
+}
+
+.history-logo {
+  width: 50px;
+  height: 50px;
+  border-radius: 10px;
+  background: linear-gradient(45deg, #ffecd2, #fcb69f);
+  margin: 0 auto 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--white);
+  font-weight: bold;
+}
+
+.history-name {
+  font-weight: 600;
+  margin-bottom: 5px;
+}
+
+.history-date {
+  font-size: 12px;
+  color: var(--gray);
+}

+ 23 - 0
menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HistoryItemComponent } from './history-item.component';
+
+describe('HistoryItemComponent', () => {
+  let component: HistoryItemComponent;
+  let fixture: ComponentFixture<HistoryItemComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HistoryItemComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(HistoryItemComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 13 - 0
menu-web/src/modules/food/mobile/page-order/components/history-item/history-item.component/history-item.component.ts

@@ -0,0 +1,13 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+  selector: 'app-history-item',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './history-item.component.html',
+  styleUrls: ['./history-item.component.scss']
+})
+export class HistoryItemComponent {
+  @Input() item: any;
+}

+ 1 - 0
menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.html

@@ -0,0 +1 @@
+<p>nutrition-chart.component works!</p>

+ 0 - 0
menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.scss


+ 23 - 0
menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NutritionChartComponent } from './nutrition-chart.component';
+
+describe('NutritionChartComponent', () => {
+  let component: NutritionChartComponent;
+  let fixture: ComponentFixture<NutritionChartComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [NutritionChartComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(NutritionChartComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 64 - 0
menu-web/src/modules/food/mobile/page-order/components/nutrition-chart/nutrition-chart.component/nutrition-chart.component.ts

@@ -0,0 +1,64 @@
+import { Component, OnInit, afterNextRender } from '@angular/core';
+import { Chart } from 'chart.js';
+
+@Component({
+  selector: 'app-nutrition-chart',
+  standalone: true,
+  template: '<div class="chart-container"><canvas id="nutritionChart"></canvas></div>',
+  styles: [`
+    .chart-container {
+      width: 100%;
+      height: 200px;
+      position: relative;
+      margin: 15px 0;
+    }
+  `]
+})
+export class NutritionChartComponent implements OnInit {
+  constructor() {
+    afterNextRender(() => {
+      this.initializeChart();
+    });
+  }
+
+  ngOnInit(): void {}
+
+  initializeChart(): void {
+    const ctx = document.getElementById('nutritionChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    new Chart(ctx, {
+      type: 'radar',
+      data: {
+        labels: ['蛋白质', '碳水', '脂肪', '纤维', '维生素'],
+        datasets: [{
+          label: '当前营养值',
+          data: [85, 70, 45, 60, 75],
+          fill: true,
+          backgroundColor: 'rgba(255, 107, 107, 0.2)',
+          borderColor: 'rgba(255, 107, 107, 1)',
+          pointBackgroundColor: 'rgba(255, 107, 107, 1)',
+          pointBorderColor: '#fff',
+          pointHoverBackgroundColor: '#fff',
+          pointHoverBorderColor: 'rgba(255, 107, 107, 1)'
+        }]
+      },
+      options: {
+        scales: {
+          r: {
+            angleLines: { display: true },
+            suggestedMin: 0,
+            suggestedMax: 100,
+            ticks: {
+              stepSize: 20,
+              backdropColor: 'transparent'
+            },
+            grid: { color: 'rgba(0, 0, 0, 0.1)' },
+            pointLabels: { font: { size: 11 } }
+          }
+        },
+        plugins: { legend: { display: false } }
+      }
+    });
+  }
+}

+ 15 - 0
menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.html

@@ -0,0 +1,15 @@
+<div class="suggestion-item" [class.replaced]="isReplaced">
+  <div class="suggestion-img">{{ suggestion.initial }}</div>
+  <div class="suggestion-info">
+    <div class="suggestion-name">{{ suggestion.name }}</div>
+    <div class="suggestion-reason">{{ suggestion.reason }}</div>
+    <div class="suggestion-price">¥{{ suggestion.price.toFixed(2) }}</div>
+  </div>
+  <button class="suggestion-btn" (click)="onReplace()">
+    @if (isReplaced) {
+      <fa-icon [icon]="faCheck"></fa-icon> 已替换
+    } @else {
+      替换
+    }
+  </button>
+</div>

+ 66 - 0
menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.scss

@@ -0,0 +1,66 @@
+.suggestion-item {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  border-radius: 10px;
+  background: rgba(78, 205, 196, 0.1);
+  margin-bottom: 10px;
+  transition: var(--transition);
+
+  &.replaced {
+    background: rgba(78, 205, 196, 0.3);
+  }
+}
+
+.suggestion-img {
+  width: 60px;
+  height: 60px;
+  border-radius: 8px;
+  background: linear-gradient(45deg, #a1c4fd, #c2e9fb);
+  margin-right: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--white);
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.suggestion-info {
+  flex: 1;
+}
+
+.suggestion-name {
+  font-weight: 600;
+  margin-bottom: 5px;
+}
+
+.suggestion-reason {
+  font-size: 12px;
+  color: var(--gray);
+  margin-bottom: 5px;
+}
+
+.suggestion-price {
+  color: var(--primary);
+  font-weight: bold;
+}
+
+.suggestion-btn {
+  background: var(--primary);
+  color: var(--white);
+  border: none;
+  border-radius: 15px;
+  padding: 6px 15px;
+  font-size: 13px;
+  cursor: pointer;
+  transition: var(--transition);
+
+  &:hover {
+    background: var(--primary-dark);
+  }
+
+  .replaced & {
+    background: #666;
+  }
+}

+ 23 - 0
menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SuggestionItemComponent } from './suggestion-item.component';
+
+describe('SuggestionItemComponent', () => {
+  let component: SuggestionItemComponent;
+  let fixture: ComponentFixture<SuggestionItemComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [SuggestionItemComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(SuggestionItemComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 28 - 0
menu-web/src/modules/food/mobile/page-order/components/suggestion-item/suggestion-item.component/suggestion-item.component.ts

@@ -0,0 +1,28 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+  selector: 'app-suggestion-item',
+  standalone: true,
+  imports: [CommonModule, FontAwesomeModule],
+  templateUrl: './suggestion-item.component.html',
+  styleUrls: ['./suggestion-item.component.scss']
+})
+export class SuggestionItemComponent {
+  @Input() suggestion: any;
+  @Output() replace = new EventEmitter<void>();
+
+  faCheck = faCheck;
+  isReplaced = false;
+
+  onReplace(): void {
+    this.isReplaced = true;
+    this.replace.emit();
+    
+    setTimeout(() => {
+      this.isReplaced = false;
+    }, 2000);
+  }
+}

+ 7 - 0
menu-web/src/modules/food/mobile/page-order/models/cart-item.model.ts

@@ -0,0 +1,7 @@
+export interface CartItem {
+  id: number;
+  name: string;
+  price: number;
+  quantity: number;
+  initial: string;
+}

+ 6 - 0
menu-web/src/modules/food/mobile/page-order/models/history-item.model.ts

@@ -0,0 +1,6 @@
+export interface HistoryItem {
+  id: number;
+  name: string;
+  date: string;
+  initial: string;
+}

+ 7 - 0
menu-web/src/modules/food/mobile/page-order/models/suggestion.model.ts

@@ -0,0 +1,7 @@
+export interface Suggestion {
+  id: number;
+  name: string;
+  price: number;
+  initial: string;
+  reason: string;
+}

+ 135 - 1
menu-web/src/modules/food/mobile/page-order/page-order.html

@@ -1 +1,135 @@
-<p>page-order works!</p>
+<div class="container">
+  <!-- Header -->
+  <header>
+    <div class="header-container">
+      <div class="logo">
+        <fa-icon [icon]="icons.faCat"></fa-icon>
+        <span>点菜喵</span>
+      </div>
+      <div class="user-actions">
+        <button class="icon-btn">
+          <fa-icon [icon]="icons.faBell"></fa-icon>
+        </button>
+        <button class="icon-btn">
+          <fa-icon [icon]="icons.faUser"></fa-icon>
+        </button>
+      </div>
+    </div>
+  </header>
+
+  <div class="cart-container">
+    <!-- Cart Items -->
+    <div class="cart-card">
+      <h2 class="section-title">购物车</h2>
+      
+      @for (item of cartItems; track item.id) {
+        <app-cart-item 
+          [item]="item"
+          (quantityChange)="updateQuantity(item, $event)"
+          (remove)="removeItem(item.id)">
+        </app-cart-item>
+      }
+    </div>
+
+    <!-- Nutrition Balance -->
+    <div class="nutrition-container">
+      <h2 class="section-title">营养均衡检测</h2>
+      <app-nutrition-chart></app-nutrition-chart>
+      <div class="nutrition-status">
+        <div class="status-item">
+          <div class="status-value">{{ nutrition.balance }}%</div>
+          <div class="status-label">均衡度</div>
+        </div>
+        <div class="status-item">
+          <div class="status-value">{{ nutrition.healthScore }}</div>
+          <div class="status-label">健康分</div>
+        </div>
+        <div class="status-item">
+          <div class="status-value">{{ nutrition.nutrients }}</div>
+          <div class="status-label">营养项</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Budget Progress -->
+    <app-budget-progress 
+      [total]="budget.total" 
+      [spent]="budget.spent" 
+      [remaining]="budget.remaining"
+      [percentage]="budget.percentage">
+    </app-budget-progress>
+
+    <!-- AI Suggestions -->
+    <div class="suggestion-container">
+      <div class="suggestion-title">
+        <fa-icon [icon]="icons.faLightbulb"></fa-icon>
+        <h2 class="section-title">同类更优选</h2>
+      </div>
+      
+      @for (suggestion of suggestions; track suggestion.id) {
+        <app-suggestion-item 
+          [suggestion]="suggestion"
+          (replace)="replaceSuggestion(suggestion.id)">
+        </app-suggestion-item>
+      }
+    </div>
+
+    <!-- History Section -->
+    <div class="history-container">
+      <h2 class="section-title">历史购买店铺</h2>
+      <div class="history-grid">
+        @for (historyItem of historyItems; track historyItem.id) {
+          <app-history-item [item]="historyItem"></app-history-item>
+        }
+      </div>
+    </div>
+
+    <!-- Order Summary -->
+    <div class="summary-container">
+      <h2 class="section-title">订单摘要</h2>
+      <div class="summary-item">
+        <span>商品总额</span>
+        <span>¥{{ orderSummary.subtotal.toFixed(2) }}</span>
+      </div>
+      <div class="summary-item">
+        <span>配送费</span>
+        <span>¥{{ orderSummary.delivery.toFixed(2) }}</span>
+      </div>
+      <div class="summary-item">
+        <span>优惠减免</span>
+        <span class="primary">-¥{{ orderSummary.discount.toFixed(2) }}</span>
+      </div>
+      <div class="summary-total">
+        <span>实付金额</span>
+        <span>¥{{ orderSummary.total.toFixed(2) }}</span>
+      </div>
+      <button class="checkout-btn" (click)="checkout()">
+        结算订单
+      </button>
+    </div>
+  </div>
+</div>
+
+<!-- Bottom Navigation -->
+<div class="bottom-nav">
+  <div class="nav-item">
+    <fa-icon [icon]="icons.faHome"></fa-icon>
+    <div>首页</div>
+  </div>
+  <div class="nav-item">
+    <fa-icon [icon]="icons.faSearch"></fa-icon>
+    <div>发现</div>
+  </div>
+  <div class="nav-item">
+    <fa-icon [icon]="icons.faBookOpen"></fa-icon>
+    <div>故事</div>
+  </div>
+  <div class="nav-item active">
+    <fa-icon [icon]="icons.faShoppingCart"></fa-icon>
+    <div>购物车</div>
+  </div>
+  <div class="nav-item">
+    <fa-icon [icon]="icons.faUser"></fa-icon>
+    <div>我的</div>
+  </div>
+</div>

+ 243 - 0
menu-web/src/modules/food/mobile/page-order/page-order.scss

@@ -0,0 +1,243 @@
+/* 全局样式变量 */
+:root {
+  --primary: #ff6b6b;
+  --primary-light: #ff8e8e;
+  --primary-dark: #e55a5a;
+  --secondary: #4ecdc4;
+  --dark: #333;
+  --gray: #666;
+  --light-gray: #f5f5f5;
+  --white: #fff;
+  --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  --transition: all 0.3s ease;
+}
+
+/* 基础样式 */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  -webkit-tap-highlight-color: transparent;
+}
+
+body {
+  background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
+  color: var(--dark);
+  line-height: 1.6;
+  padding-bottom: 80px;
+}
+
+.container {
+  width: 100%;
+  max-width: 480px;
+  margin: 0 auto;
+  padding: 0 15px;
+}
+
+/* Header Styles */
+header {
+  background: var(--white);
+  padding: 15px 0 10px;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  box-shadow: var(--card-shadow);
+}
+
+.header-container {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 15px;
+}
+
+.logo {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--primary);
+}
+
+.logo fa-icon {
+  font-size: 22px;
+}
+
+.user-actions {
+  display: flex;
+  gap: 15px;
+}
+
+.icon-btn {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--light-gray);
+  color: var(--gray);
+  font-size: 16px;
+  border: none;
+  cursor: pointer;
+}
+
+/* Main Content */
+.cart-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  margin-top: 15px;
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  margin: 0 0 15px;
+  position: relative;
+  padding-left: 12px;
+  font-size: 18px;
+  color: var(--dark);
+}
+
+.section-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 4px;
+  height: 16px;
+  background: var(--primary);
+  border-radius: 2px;
+}
+
+/* Shared Card Styles */
+.cart-card,
+.nutrition-container,
+.budget-container,
+.suggestion-container,
+.history-container,
+.summary-container {
+  background: var(--white);
+  border-radius: 15px;
+  box-shadow: var(--card-shadow);
+  padding: 20px;
+}
+
+/* Nutrition Chart */
+.nutrition-status {
+  display: flex;
+  justify-content: center;
+  gap: 15px;
+  margin-top: 10px;
+}
+
+.status-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.status-value {
+  font-size: 18px;
+  font-weight: bold;
+  color: var(--primary);
+}
+
+.status-label {
+  font-size: 12px;
+  color: var(--gray);
+}
+
+/* Order Summary */
+.summary-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 8px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.summary-total {
+  display: flex;
+  justify-content: space-between;
+  font-weight: bold;
+  font-size: 18px;
+  padding: 15px 0;
+  color: var(--primary);
+}
+
+.primary {
+  color: var(--primary);
+}
+
+.checkout-btn {
+  width: 100%;
+  padding: 15px;
+  background: linear-gradient(135deg, var(--primary), var(--primary-light));
+  color: var(--white);
+  border: none;
+  border-radius: 30px;
+  font-size: 16px;
+  font-weight: bold;
+  cursor: pointer;
+  box-shadow: 0 6px 15px rgba(255, 107, 107, 0.4);
+  transition: var(--transition);
+  margin-top: 10px;
+}
+
+.checkout-btn:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 8px 20px rgba(255, 107, 107, 0.6);
+}
+
+/* Bottom Navigation */
+.bottom-nav {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  background: var(--white);
+  display: flex;
+  border-top: 1px solid #eee;
+  z-index: 100;
+}
+
+.nav-item {
+  flex: 1;
+  text-align: center;
+  padding: 12px 0;
+  color: var(--gray);
+  font-size: 12px;
+  position: relative;
+}
+
+.nav-item.active {
+  color: var(--primary);
+}
+
+.nav-item.active::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 30px;
+  height: 3px;
+  background: var(--primary);
+  border-radius: 3px;
+}
+
+.nav-item fa-icon {
+  font-size: 20px;
+  margin-bottom: 4px;
+}
+
+/* Responsive */
+@media (max-width: 480px) {
+  .history-grid {
+    grid-template-columns: 1fr;
+  }
+}

+ 185 - 6
menu-web/src/modules/food/mobile/page-order/page-order.ts

@@ -1,11 +1,190 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, afterNextRender } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { 
+  faCat, 
+  faBell, 
+  faUser, 
+  faTrashAlt, 
+  faLightbulb, 
+  faCheck, 
+  faSpinner, 
+  faHome, 
+  faSearch, 
+  faBookOpen, 
+  faShoppingCart 
+} from '@fortawesome/free-solid-svg-icons';
+import { Chart } from 'chart.js';
+import { CartItemComponent } from './components/cart-item/cart-item.component/cart-item.component';
+import { NutritionChartComponent } from './components/nutrition-chart/nutrition-chart.component/nutrition-chart.component';
+import { BudgetProgressComponent } from './components/budget-progress/budget-progress.component/budget-progress.component';
+import { SuggestionItemComponent } from './components/suggestion-item/suggestion-item.component/suggestion-item.component';
+import { HistoryItemComponent } from './components/history-item/history-item.component/history-item.component';
 
 @Component({
   selector: 'app-page-order',
-  imports: [],
-  templateUrl: './page-order.html',
-  styleUrl: './page-order.scss'
+  standalone: true,
+  imports: [
+    CommonModule,
+    RouterModule,
+    FontAwesomeModule,
+    CartItemComponent,
+    NutritionChartComponent,
+    BudgetProgressComponent,
+    SuggestionItemComponent,
+    HistoryItemComponent
+  ],
+  templateUrl: './page-order.component.html',
+  styleUrls: ['./page-order.component.scss']
 })
-export class PageOrder {
+export class PageOrderComponent implements OnInit {
+  // Icons
+  icons = {
+    faCat,
+    faBell,
+    faUser,
+    faTrashAlt,
+    faLightbulb,
+    faCheck,
+    faSpinner,
+    faHome,
+    faSearch,
+    faBookOpen,
+    faShoppingCart
+  };
 
-}
+  // Cart items data
+  cartItems = [
+    { id: 1, name: '招牌酸菜鱼', price: 68, quantity: 1, initial: '酸' },
+    { id: 2, name: '香辣小龙虾', price: 98, quantity: 1, initial: '虾' },
+    { id: 3, name: '金汤肥牛锅', price: 58, quantity: 1, initial: '牛' }
+  ];
+
+  // Suggestions data
+  suggestions = [
+    { 
+      id: 1, 
+      name: '清蒸鲈鱼', 
+      price: 62, 
+      initial: '鱼', 
+      reason: '更低热量 · 更高蛋白质' 
+    },
+    { 
+      id: 2, 
+      name: '蒜蓉蒸虾', 
+      price: 88, 
+      initial: '虾', 
+      reason: '更低脂肪 · 更少辛辣' 
+    },
+    { 
+      id: 3, 
+      name: '番茄牛腩汤', 
+      price: 48, 
+      initial: '汤', 
+      reason: '更多维生素 · 更低热量' 
+    }
+  ];
+
+  // History items data
+  historyItems = [
+    { id: 1, name: '点菜喵主题餐厅', date: '06-25 消费¥236', initial: '喵' },
+    { id: 2, name: '渔家小馆', date: '06-20 消费¥182', initial: '鱼' },
+    { id: 3, name: '龙虾先生', date: '06-15 消费¥320', initial: '虾' },
+    { id: 4, name: '火锅物语', date: '06-10 消费¥198', initial: '锅' }
+  ];
+
+  // Order summary data
+  orderSummary = {
+    subtotal: 224,
+    delivery: 3.5,
+    discount: 15,
+    total: 212.5
+  };
+
+  // Budget data
+  budget = {
+    total: 800,
+    spent: 520,
+    remaining: 280,
+    percentage: 65
+  };
+
+  // Nutrition data
+  nutrition = {
+    balance: 72,
+    healthScore: 4.8,
+    nutrients: '3/5'
+  };
+
+  constructor() {
+    afterNextRender(() => {
+      this.initializeChart();
+    });
+  }
+
+  ngOnInit(): void {}
+
+  initializeChart(): void {
+    const ctx = document.getElementById('nutritionChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    new Chart(ctx, {
+      type: 'radar',
+      data: {
+        labels: ['蛋白质', '碳水', '脂肪', '纤维', '维生素'],
+        datasets: [{
+          label: '当前营养值',
+          data: [85, 70, 45, 60, 75],
+          fill: true,
+          backgroundColor: 'rgba(255, 107, 107, 0.2)',
+          borderColor: 'rgba(255, 107, 107, 1)',
+          pointBackgroundColor: 'rgba(255, 107, 107, 1)',
+          pointBorderColor: '#fff',
+          pointHoverBackgroundColor: '#fff',
+          pointHoverBorderColor: 'rgba(255, 107, 107, 1)'
+        }]
+      },
+      options: {
+        scales: {
+          r: {
+            angleLines: { display: true },
+            suggestedMin: 0,
+            suggestedMax: 100,
+            ticks: {
+              stepSize: 20,
+              backdropColor: 'transparent'
+            },
+            grid: { color: 'rgba(0, 0, 0, 0.1)' },
+            pointLabels: { font: { size: 11 } }
+          }
+        },
+        plugins: { legend: { display: false } }
+      }
+    });
+  }
+
+  updateQuantity(item: any, change: number): void {
+    const newQuantity = item.quantity + change;
+    if (newQuantity >= 1) {
+      item.quantity = newQuantity;
+    }
+  }
+
+  removeItem(itemId: number): void {
+    this.cartItems = this.cartItems.filter(item => item.id !== itemId);
+  }
+
+  replaceSuggestion(suggestionId: number): void {
+    const suggestion = this.suggestions.find(s => s.id === suggestionId);
+    if (suggestion) {
+      // In a real app, you would implement the replacement logic here
+      console.log(`Replacing with ${suggestion.name}`);
+    }
+  }
+
+  checkout(): void {
+    // In a real app, you would implement the checkout logic here
+    console.log('Checking out...');
+  }
+}

+ 74 - 1
menu-web/src/styles.scss

@@ -1 +1,74 @@
-/* You can add global styles to this file, and also import other style files */
+/* 全局变量和基础样式 */
+@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
+
+:root {
+  --primary: #ff6b6b;
+  --primary-light: #ff8e8e;
+  --primary-dark: #e55a5a;
+  --secondary: #4ecdc4;
+  --dark: #333;
+  --gray: #666;
+  --light-gray: #f5f5f5;
+  --white: #fff;
+  --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  --transition: all 0.3s ease;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  -webkit-tap-highlight-color: transparent;
+}
+
+body {
+  background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
+  color: var(--dark);
+  line-height: 1.6;
+  padding-bottom: 80px;
+}
+
+.container {
+  width: 100%;
+  max-width: 480px;
+  margin: 0 auto;
+  padding: 0 15px;
+}
+
+/* 通用样式 */
+.section-title {
+  display: flex;
+  align-items: center;
+  margin: 0 0 15px;
+  position: relative;
+  padding-left: 12px;
+  font-size: 18px;
+  color: var(--dark);
+
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 4px;
+    height: 16px;
+    background: var(--primary);
+    border-radius: 2px;
+  }
+}
+
+.card {
+  background: var(--white);
+  border-radius: 15px;
+  box-shadow: var(--card-shadow);
+  padding: 20px;
+  margin-bottom: 15px;
+}
+
+@media (max-width: 480px) {
+  .history-grid {
+    grid-template-columns: 1fr !important;
+  }
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff